chore: resolve merge conflict in extra/site-guide.html using origin/main version

This commit is contained in:
2026-08-04 11:04:37 +08:00
83 changed files with 11865 additions and 815 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(python3 -c ' *)"
]
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"liveServer.settings.port": 5503
}
+23
View File
@@ -0,0 +1,23 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "預覽網站",
"detail": "本機 http://127.0.0.1:5501(等同雙擊 preview.bat",
"type": "shell",
"command": "${workspaceFolder}\\preview.bat",
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [],
"presentation": {
"reveal": "always",
"panel": "dedicated",
"focus": false
},
"runOptions": {
"instanceLimit": 1
}
}
]
}
+103
View File
@@ -0,0 +1,103 @@
# 水晶男孩推廣部 — AI 工作上下文
> 這個檔案讓 AI 在任何裝置(家/學校)都能快速恢復上下文。
> **每次對話做了重要決定,請叫我更新這裡,然後 git commit。**
---
## 專案簡介
水晶男孩(Sechs Kies)推廣粉絲網站。靜態 HTML + PHP + 原生 CSS/JS,無框架。
站主(Noopy)2016 年入坑,藝廊式創作風格。
---
## 規範速查(詳細見 `developer-rules/rules/`
| 規範 | 檔案 |
|------|------|
| 改檔行為守則(必問再改) | `agent-must-ask.md` |
| 變更邊界(只改被點名的) | `ai-change-boundaries.md` |
| CSS 顏色 Token | `color-tokens.md` |
| 版面視覺預設 | `design-defaults.md` |
| Git commit 規範 | `git-commit-required.md` |
| 導覽邏輯(portal-nav | `portal-nav-rules.md` |
| 文案語調 | `site-voice.md` |
**最重要的三條:**
1. 沒說「執行」→ 不改檔
2. 只改被點名的那一頁/區塊
3. 改完必附 commit 指令(禁止在 sandbox 跑 git
---
## 架構重點
- **全站導覽**`assets/js/portal-nav.js``COMING_SOON_PAGES` 控制開放)
- **全站 CSS**`assets/css/style.css`(定義所有 color token
- **首頁**`index.html`
- **站主索引**(自用):`extra/site-guide.html``data-portal-nav-all-open`,不對外)
- **內頁 header class**`inner-page-header`(禁改回舊名)
### variety/ 頁面
| 檔案 | 頁面標題 | 說明 |
|------|----------|------|
| `variety/bangxiao.html` | 世紀級寶藏 / 爆笑養老院 | 甜蜜負擔計量器;六人卡片選取 + jQuery 計算;CTA → `totoga2/ebook.html` |
| `variety/variety-shows.html` | 全員發瘋 | 綜藝節目存檔表;tabs + 搜尋;已對齊 albums.html 扁平結構 |
| `variety/NJTW/NJTW5.html` | 新西遊記5 | 各自暴走專頁 |
---
## portal-nav 操作守則(血淚版)
> 每次新增或修改導覽,必須先讀這裡再動手。
1. **先查 `extra/site-guide.html` 確認層級**,再動 `portal-nav.js`
site-guide 的 depth 數字 = nav 的層級:depth-1 是第一層 `<li>`depth-2 是子選單項目,depth-3 是 subgroup 裡的巢狀項目。
2. **`COMING_SOON_PAGES` 只放檔名**(不含路徑)。
`normalizePageKey` 只取 href 最後一段,所以 `variety/bangxiao.html` → 比對 `bangxiao.html`
3. **直接連結不需要 subgroup**
子選單內只要 `<li><a>` 就夠;`portal-nav__subgroup` 只在該項目還有巢狀子項目時才用(例如「傳奇的轉折」有時間軸、電子書等四項)。
4. **site-guide.html 要同步更新**,否則站主索引頁會顯示舊名稱。
5. **commit 前網站不會更新**。本地端強制重整用 `Ctrl+Shift+R`GitHub Pages 需 `git push` 後等約 1 分鐘。
---
## 目前工作狀態
<!-- 每次對話結束後更新這個區塊 -->
**最後更新:** 2026-08-02
**進行中:**
- 水晶熱舞社重啟:30TH 報名與水晶練習室練舞清單已接上後臺;播放器本身仍待加入合法影片來源
**待確認:**
- developer-rules 健檢尚未執行
**已完成:**
- 建立 AGENTS.md 跨裝置上下文檔案
- 新增 `variety/bangxiao.html`(世紀級寶藏 / 爆笑養老院,甜蜜負擔計量器)
- `variety/variety-shows.html` 重構:移除多層包裝,對齊 albums.html 扁平結構
- portal-nav 新增世紀級寶藏,正確放入淪陷瞬間子選單
- `extra/site-guide.html` 同步更新:全員發瘋 → 世紀級寶藏
- 新增 `admin/` 推廣部後臺第一階段:登入、控制台、管理者帳號、通用 CRUD、媒體上傳及公開狀態總覽
- 水晶熱舞社 30TH 募集表單已接上 PHP/PDO/MySQL,可儲存報名、參與內容與參考檔案
- 後臺新增「30TH 應援報名」清單、搜尋、狀態處理、內部備註與受登入保護的檔案下載
- 架構決定:水晶熱舞社暫時共用推廣部後臺;偶像培訓維持獨立資料邊界,之後另拆專案與登入系統
- 水晶練習室的「練舞清單」已改為讀取 `dance_practice_items`,後臺可管理歌曲、分類、練習重點、影片網址、難度、進度、優先順序與公開狀態
- 練舞清單初始資料沿用原頁面的 Couple、Com' Back、Road Fighter;偶像培訓的練習生/課程/紀錄未接入此資料表
---
## 如何使用這個檔案
換裝置後,告訴 AI
> 「請先讀 AGENTS.md,再讀 developer-rules/rules/ 裡的所有規範,然後繼續 [任務]。」
## Imported Claude Cowork project instructions
+71
View File
@@ -0,0 +1,71 @@
# 💛 SECHSKIES 2026 水晶男孩專屬萬年曆 & 專案對話總覽 (Notion 格式)
> **專案說明**:本頁面為水晶男孩(SECHSKIES)推廣網站後端課程「萬年曆」專案統整檔,適合直接複製並貼上至 Notion 頁面。
---
## 📌 2026 水晶男孩專屬萬年曆事件表 (1 ~ 12 月)
| 月份 | 日期 | 分類 | 紀念日名稱 | 描述 / 現場圖片與連結 |
| :--- | :--- | :--- | :--- | :--- |
| **1 月** | 01/21 - 01/22 | 🎤 演唱會 | Yellow Note Final 首爾演唱會 | 2017 首爾終場演唱會 (`assets/images/concert/2017.jpg`) |
| **2 月** | 02/22 | 🎂 生日 | **姜成勳 生日** | 1980.02.22 Main Vocal (👉 [查看姜成勳介紹](member/member.html#poster-ksh)) |
| **3 月** | 03/06 - 03/08 | 🎤 演唱會 | ACCESS 首爾演唱會 | 2020 SECHSKIES Concert [ACCESS] (`assets/images/concert/2020.jpg`) |
| | 03/23 | 🌟 出道 | J-Walk 組合出道日 | 2002.03.23 金在德與張水院發行首張專輯《Suddenly》 |
| **4 月** | 04/14 | 🌟 重逢 | 無限挑戰六六歌2 上岩黃海重逢日 | 2016.04.14 上岩游擊演唱會16年後重逢 (`assets/images/totoga2/toto1.png`) |
| | 04/15 | 🌟 出道 | **SECHSKIES 團體出道紀念日** | 1997.04.15 水晶男孩正式出道,傳奇盛世起點! |
| **5 月** | 05/18 | 🌟 紀念 | 水晶男孩告別宣佈日 | 2000.05.18 告別紀念 |
| | 05/24 | 🌟 出道 | 姜成勳 Solo 出道日 | 2001.05.24 首張專輯《In Connection》 |
| **6 月** | 06/08 | 🎂 生日 | **殷志源 生日** | 1978.06.08 Leader · Rap · Dance (👉 [查看殷志源介紹](member/member.html#poster-ejw)) |
| **7 月** | 07/01 | 🎂 生日 | **高志溶 生日** | 1980.07.01 Vocal (👉 [查看高志溶介紹](member/member.html#poster-kjy)) |
| | 07/13 | 🎂 生日 | **李宰鎮 生日** | 1979.07.13 Rap · Dance (👉 [查看李宰鎮介紹](member/member.html#poster-ljj)) |
| | 07/16 | 🎂 生日 | **張水院 生日** | 1980.07.16 Vocal (👉 [查看張水院介紹](member/member.html#poster-jsw)) |
| **8 月** | 08/07 | 🎂 生日 | **金在德 生日** | 1979.08.07 Rap · Dance (👉 [查看金在德介紹](member/member.html#poster-kjd)) |
| **9 月** | 09/10 - 09/11 | 🎤 演唱會 | Yellow Note 演唱會 (二安日) | 2016 黃海 6 分鐘喊聲喚回二安舞台 (`assets/images/concert/2016.jpg`) |
| | 09/23 | 🎤 演唱會 | 20 週年高尺巨蛋演唱會 | 2017 20th Anniversary Concert (`assets/images/concert/2017.jpg`) |
| **10 月** | 10/01 | 🎤 演唱會 | BOF 釜山演唱會 | 2016 60台遊覽車 6000人黃海盛況 (`assets/images/mission-balloon-sea.png`) |
| | 10/07 | 🌟 回歸 | 《三個詞 Three Words》發行日 | 2016.10.07 回歸單曲音源發行日 |
| | 10/13 - 10/14 | 🎤 演唱會 | YELLOFANTASY 22週年演唱會 | 2018 SECHSKIES 2018 CONCERT (`assets/images/concert/2018.jpg`) |
| | 10/27 | 🌟 出道 | 殷志源 Solo 出道日 | 2000.10.27 單曲《A-Ha》/《G Pop》 |
| **11 月** | 11/19 | 🌟 出道 | 李宰鎮 Solo 出道日 | 2001.11.19 首張專輯《S.Double.J》 |
---
## 🎂 全員 6 位成員生日官方權威對照表
- 💛 **殷志源 (EUN JI WON)**1978 年 6 月 8 日 (06/08)
- 💛 **李宰鎮 (LEE JAE JIN)**1979 年 7 月 13 日 (07/13)
- 💛 **金在德 (KIM JAE DUC)**1979 年 8 月 7 日 (08/07) *(Namuwiki 查證正名)*
- 💛 **姜成勳 (KANG SUNG HOON)**1980 年 2 月 22 日 (02/22)
- 💛 **高志溶 (KO JI YONG)**1980 年 7 月 1 日 (07/01)
- 💛 **張水院 (JANG SU WON)**1980 年 7 月 16 日 (07/16)
---
## 🔗 萬年曆點擊跳轉對照連結
`calendar2026.html` 萬年曆頁面中,點擊成員圖片可直接跳轉至成員介紹頁:
1. **殷志源**`member/member.html#poster-ejw`
2. **李宰鎮**`member/member.html#poster-ljj`
3. **金在德**`member/member.html#poster-kjd`
4. **姜成勳**`member/member.html#poster-ksh`
5. **高志溶**`member/member.html#poster-kjy`
6. **張水院**`member/member.html#poster-jsw`
---
## 🛠️ 本次對話修改與建立檔案
- `calendar2026.html`:萬年曆主頁面(含網格計算、篩選標籤、Modal 與圖片連結)
- `assets/css/calendar.css`:萬年曆專用 CSS 樣式
- `member/member.html`:修正白水晶姜成勳與高志溶肖像圖檔錯置問題
---
## 💻 Git Commit 指令紀錄
```bash
git add calendar2026.html assets/css/calendar.css member/member.html
git commit -m "feat: 完成 2026 水晶專屬萬年曆,更正金在德生日與六顆水晶成員照片"
```
+133
View File
@@ -0,0 +1,133 @@
# 水晶萬年曆 2026 — 設計決策與改版重點
> 來源:Cursor 對話串整理(可直接貼 Notion)
> 範圍:`calendar2026.html`、`assets/css/calendar.css`
> 日期:2026-07
---
## 一、站主設計偏好(從公開頁歸納)
對照已公開頁(首頁、bangxiao、variety-shows、ebook、NJTW5)讀到的設計感:
| 面向 | 偏好 |
|------|------|
| 色調 | 頁面黑底,內容落在奶油色 section;金 `#ffcc00` 當點綴/底線,不當整片 UI 底色 |
| 氣氛 | 偶像粉絲紀念冊、藝廊策展,不是生產力工具/SaaS 儀表板 |
| 版型 | 單欄敘事、扁平區塊;卡片只在「要互動」時出現 |
| 元件 | 金色實心膠囊按鈕、淺底 tabs;陰影溫柔;glow 只用在重點字 |
| 文案感 | 溫柔策展、具體回憶,不堆 emoji 狀態列 |
### 萬年曆初期違和點
- 深色玻璃卡片、霓虹色事件點(紅/綠/橘 glow)
- 篩選 chipemoji(🎂🌟🎤)
- 側欄內嵌捲軸
- 整體偏「AI 日曆 dashboard」,與全站暖紙面藝廊感不符
---
## 二、改版原則(站主明確指示)
### ✅ 可以做
- **只改設計感(換皮)**:顏色、字級、陰影、圓角、照片呈現
- 對齊全站暖紙面 section 視覺
- 用 color token,避免硬寫霓虹色
### ❌ 不可以做
- **不要擅自改版型**(例:雙欄改單欄)
- 沒說「執行」前不應改檔(本次站主後來說直接做)
- 只改被點名的頁/區塊
> 站主原話重點:
> 「我只覺擬改設計,沒有叫你動版型」
---
## 三、已落地的視覺調整
### 整體皮
- 深色玻璃卡 → 暖色 `section` 卡(`--color-surface` / `--color-surface-soft`
- 控制列與篩選:淺底膠囊+金色 active
- 日期格、週末色、today/selected:低調暖色系
- 事件點統一品牌金,拿掉多色霓虹
- Modal 改暖紙底
### 版型(維持原狀)
- `calendar-layout` **維持雙欄**`1fr 380px`
- 手機 `max-width: 900px` 才改單欄
---
## 四、成員/演唱會照片規則(重點)
### 站主意圖
- **要曬成員美照**,不是小頭像+蛋糕 icon
- 成員名字要清楚可見
- 不要一堆 🎂 之類花字
- 右側生日卡可上下排(圖上、字下)
### 裁切規則(A4
| 規則 | 說明 |
|------|------|
| 外框比例 | **A4 直式**`aspect-ratio: 210 / 297` |
| 填滿方式 | `object-fit: cover`**不留黑邊** |
| 縮放原則 | 以 A4 為裁切框,放大縮小盡量保留原圖主體 |
| 對焦 | `object-position: center 28%`(優先臉/上半身) |
> 站主原話重點:
> 「以 A4 尺寸為裁切尺寸,放大縮小盡量保持原本的圖」
> 「怎麼有留黑」→ 禁止用 `contain` 造成黑邊
### 適用範圍(必須一致)
- ✅ 右側生日卡
- ✅ 右側演唱會卡
- ✅ Modal 演唱會圖
- ❌ 不可只改生日、漏掉演唱會
### 右側生日卡版型
- **上下排**:照片在上、名字/英文/日期在下
- 照片用 A4 比例大圖卡(海報感)
- 標題顯示成員名,不顯示「生日快樂🎂」
---
## 五、文案/顯示清理
- 篩選按鈕拿掉 emoji(成員生日/出道重逢/演唱會)
- 事件標題顯示時清掉 `🎂🌟🎤`
- 生日卡拿掉蛋糕 icon
- Modal 生日標題改成員名
---
## 六、待確認/可選下一步
- [ ] 不同成員圖各自對焦點微調(例:`center 20%` / `center 35%`
- [ ] 相紙邊框/海報感細節
- [ ] 字級層級、卡片密度再微調
- [ ] 是否要把月曆格內生日照再放大
---
## 七、相關檔案
| 檔案 | 用途 |
|------|------|
| `calendar2026.html` | 萬年曆頁面與 JS 邏輯 |
| `assets/css/calendar.css` | 萬年曆專用樣式(本次主要改動) |
| `assets/css/style.css` | 全站 color token |
---
## 八、一句話結論
> 萬年曆只換皮、不換版型;右側生日/演唱會照以 **A4 裁切框+cover 填滿** 曬海報,不留黑邊、不堆蛋糕 emoji,成員名字要清楚。
@@ -0,0 +1,170 @@
# 水晶熱舞社頁面改版對話紀錄
> 來源:`yellow-note/crystal-dance-survey.html` 改版討論
> 日期:2026-07-09
> 用途:可貼上 Notion 的整理稿
---
## 一、任務目標
**水晶熱舞社** 頁(`yellow-note/crystal-dance-survey.html`)從「自創黃框、多層卡片、閱讀擁擠」改成:
1. **對齊首頁/全站簡約風格**(黑底 + 白卡片 + 品牌黃)
2. **多套用全站 CSS**`style.css``section``.section-title``.card` 語彙)
3. **熱舞社相關內容不刪**(可改外觀與排版)
4. **Bootstrap class 必須保留**(作業要求)
5. **表單區參考「見面會應援調查表」的 summer-form 邊框排版**,顏色仍用全站黃色 token
---
## 二、核心原則(這次定下來的)
| 原則 | 說明 |
|------|------|
| 內容優先 | 跟熱舞社相關的資訊不要刪;外觀排版可以改 |
| 只改被點名的頁 | 主要動 `crystal-dance-survey.html` |
| Bootstrap 保留 | `row` / `col` / `form-control` / `form-select` / `btn-check` / `ratio` 等 class 留下 |
| 顏色不動 | 用全站 `--color-brand` 系列,不改成參考圖的藍 |
| 美感對齊 | 參考 index 簡約乾淨 + 參考圖的留白與分階段框線 |
---
## 三、舊版問題診斷
### 為什麼「怎麼看都不順眼」
1. **雙重/三重卡片**
全站 `section` 本身已是圓角白卡片,頁內又加 `video-card``song-card``form-panel` → 盒子套盒子。
2. **約 400 行 inline CSS 重做設計系統**
全站已有 `.card``.section-title`,卻自創多套黃框樣式。
3. **`info-card` 深色漸層突兀**
跟全站暖白、輕盈感相反。
4. **導覽太吵**
五顆黃色 pill 按鈕搶眼;首頁錨點是低調文字列。
5. **UX 問題(後續回饋)**
- 表單左右分欄難讀
- 標題跟內容距離太近
- 主影片寬度沒跟下方三支影片左右對齊
- 文字擠在一起、閱讀節奏差
---
## 四、結構重組(6 區 → 3 區)
### 備份
```
yellow-note/crystal-dance-survey.backup-2026-07-09.html
```
(本地備份;改動前版本)
### 新架構
| 新區塊 | ID | 合併來源 | 合併理由 |
|--------|-----|----------|----------|
| ① 認識水晶熱舞社 | `#about` | Hero + 熱舞社介紹 | 都在回答「這是什麼」 |
| ② 從 20TH 到 30TH | `#from-20th-to-30th` | 20TH 影片回顧 + 30TH 應援募集敘事 | 同一條時間軸:過去記憶 → 未來募集 |
| ③ 30TH 應援募集表單 | `#form-30th` | 表單區獨立 | Bootstrap 表單作業清楚;敘事講完才行動 |
### 移除
- 整個 **經典舞曲區**`#song-archive` 四張 song-card
- 歌曲資訊不丟:表單「想練的歌曲」`<select>` 仍保留 Couple / Com' Back / Road Fighter / Pom Saeng Pom Sa / 其他
---
## 五、視覺與版型決策
### 對齊全站
- 骨架:`inner-page-header``portal-nav``particles``container``section`
- 大區塊靠全站 `section` 白卡片浮在星空底上
- 避免 section 內再套一層 `.card`(影片/表單尤甚)
### 影片區
- 主影片與下方三支延伸影片包在同一 `video-grid`
- **主影片寬度與下方三欄左右切齊**(拿掉主影片單獨 `max-width: 720px` 置中)
### 間距與閱讀
- `.section-title` 與內文拉開(約 `2rem`
- 段落行高約 `1.9`、段間距加大
- 子標題(延伸閱讀、30TH)上方留白加大,避免貼住上一段
---
## 六、表單:summer-form 風格
參考「見面會應援調查表」的**排版結構**,**顏色維持全站黃**:
### 結構
1. **填寫須知**(淡黃底邊框盒)
2. **第一階段|參與者基本資料**(暱稱、Email、練舞年數、可參與日期)
3. **第二階段|30TH 參與意向**20TH 經驗、參與內容複選)
4. **第三階段|應援內容與祝福**(歌曲、上傳、留言)
5. **底部按鈕**(重新填寫 / 送出並排)
### 排版特徵
- 階段標題在邊框盒**上方**、左對齊
- 每階段獨立 `summer-form__stage-body`(圓角細邊框)
- label 在上、輸入在下(單欄垂直流)
- 不用表單左右分欄
- Bootstrap class 全保留
### Class 命名(頁面專用)
- `.summer-form`
- `.summer-form__stage`
- `.summer-form__stage-title`
- `.summer-form__stage-body`
- `.summer-form__stage-body--notice`
- `.summer-form__option-row`
- `.summer-form__actions`
---
## 七、必須保留的 Bootstrap class(作業)
```html
row / col-* / g-*
form-label / form-control / form-select / form-text
btn-check / btn
ratio / ratio-16x9
d-flex / flex-wrap / gap-* / mt-*
```
---
## 八、Git 狀態(對話當時)
- Commit`a977e78`
`refactor: 水晶熱舞社頁面重組為三區並對齊全站簡約風格`
- 已 push 至 `origin/main`(後續還有本地再改:間距、影片對齊、summer-form
- 後續的 summer-form/間距微調若尚未再 commit,需另開 commit
---
## 九、給下次改這頁的檢查清單
- [ ] 熱舞社敘事、20TH 影片、30TH 募集說明、表單欄位是否都在
- [ ] 是否又出現「section 外套 + 內層黃框」雙重卡片
- [ ] 主影片是否與下方三支左右對齊
- [ ] 表單是否單欄分階段(summer-form),沒有左右表單/須知分欄
- [ ] 標題與內文、區塊與區塊是否有足夠留白
- [ ] Bootstrap 必要 class 是否還在
- [ ] 顏色是否仍用 `--color-brand` 系列(沒偷換成藍色)
---
## 十、一句話總結
> **讓 Bootstrap 負責格線與表單元件,讓 `style.css` 負責全站美感;頁面專用 CSS 只補缺口。內容依敘事合三區,表單用 summer-form 分階段邊框,顏色不動。**
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
require_login();
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = (string) ($_POST['action'] ?? 'create');
if ($action === 'create') {
$username = trim((string) ($_POST['username'] ?? ''));
$name = trim((string) ($_POST['display_name'] ?? ''));
$password = (string) ($_POST['password'] ?? '');
if (!preg_match('/^[A-Za-z0-9_.-]{4,50}$/', $username) || mb_strlen($name) < 2 || strlen($password) < 10) {
$error = '請確認帳號、名稱與至少 10 字元的密碼。';
} else {
try {
$stmt = db()->prepare('INSERT INTO admin_users (username, password_hash, display_name, email) VALUES (?, ?, ?, ?)');
$stmt->execute([$username, password_hash($password, PASSWORD_DEFAULT), $name, trim((string) ($_POST['email'] ?? '')) ?: null]);
audit('create', 'admin_users', (int) db()->lastInsertId());
flash('success', '管理者已新增。'); redirect('admins.php');
} catch (PDOException $e) { $error = '帳號已存在或資料格式不正確。'; }
}
} elseif ($action === 'toggle') {
$id = (int) ($_POST['id'] ?? 0);
if ($id === (int) $_SESSION['admin_id']) $error = '不能停用目前登入的帳號。';
else { db()->prepare('UPDATE admin_users SET is_active = 1 - is_active WHERE id = ?')->execute([$id]); audit('toggle', 'admin_users', $id); flash('success', '帳號狀態已更新。'); redirect('admins.php'); }
}
}
$users = db()->query('SELECT id, username, display_name, email, is_active, last_login_at, created_at FROM admin_users ORDER BY id')->fetchAll();
render_header('管理者帳號', 'admins');
?>
<?php if ($error): ?><div class="notice notice--error"><?= h($error) ?></div><?php endif; ?>
<section class="panel"><div class="panel__head"><div><h2>新增管理者</h2><p>密碼使用不可逆雜湊儲存</p></div></div><form class="inline-form" method="post"><input type="hidden" name="csrf" value="<?= h(csrf_token()) ?>"><input type="hidden" name="action" value="create"><label>登入帳號<input name="username" required></label><label>顯示名稱<input name="display_name" required></label><label>電子信箱<input name="email" type="email"></label><label>密碼<input name="password" type="password" minlength="10" required></label><button class="button button--primary" type="submit">新增帳號</button></form></section>
<section class="panel"><div class="panel__head"><div><h2>帳號清單</h2><p>停用帳號後將無法登入</p></div></div><div class="table-wrap"><table><thead><tr><th>帳號</th><th>顯示名稱</th><th>信箱</th><th>最後登入</th><th>狀態</th><th>操作</th></tr></thead><tbody><?php foreach ($users as $user): ?><tr><td><?= h($user['username']) ?></td><td><?= h($user['display_name']) ?></td><td><?= h($user['email']) ?></td><td><?= h($user['last_login_at'] ?? '尚未登入') ?></td><td><span class="status status--<?= $user['is_active'] ? 'published' : 'archived' ?>"><?= $user['is_active'] ? '啟用' : '停用' ?></span></td><td><form method="post" data-confirm="確定變更此管理者狀態?"><input type="hidden" name="csrf" value="<?= h(csrf_token()) ?>"><input type="hidden" name="action" value="toggle"><input type="hidden" name="id" value="<?= (int) $user['id'] ?>"><button class="table-button" type="submit" <?= (int) $user['id'] === (int) $_SESSION['admin_id'] ? 'disabled' : '' ?>><?= $user['is_active'] ? '停用' : '啟用' ?></button></form></td></tr><?php endforeach; ?></tbody></table></div></section>
<?php render_footer();
+158
View File
@@ -0,0 +1,158 @@
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;600;700;800;900&display=swap');
:root {
--color-brand: #ffcc00;
--color-brand-light: #fceea7;
--color-brand-dark: #4e492b;
--color-bg: #0a0a0a;
--color-bg-raised: #1a1a1a;
--color-bg-alt: #fcfaf2;
--color-surface: #fffdf7;
--color-surface-soft: #fff9e8;
--color-text: #333333;
--color-text-dark: #1a1a1a;
--color-text-muted: #4d4d4d;
--color-text-inverse: #f4f4f4;
}
* { box-sizing: border-box; }
html { min-height: 100%; background: var(--color-bg); }
body { margin: 0; min-height: 100vh; font-family: 'Noto Sans TC', system-ui, sans-serif; color: var(--color-text); background: var(--color-bg-alt); }
a { color: inherit; }
button, input, select, textarea { font: inherit; }
button, .button { min-height: 42px; }
.admin-topbar { position: sticky; z-index: 20; top: 0; min-height: 68px; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; color: var(--color-text-inverse); background: var(--color-bg); border-bottom: 1px solid rgba(255, 204, 0, .3); }
.admin-brand { display: inline-flex; align-items: center; gap: 12px; text-decoration: none; }
.admin-brand__logo { display: block; width: clamp(190px, 20vw, 250px); max-height: 46px; height: auto; object-fit: contain; }
.admin-brand small { padding-left: 10px; color: var(--color-brand-light); border-left: 1px solid rgba(255, 255, 255, .25); font-weight: 500; }
.admin-topbar__actions { display: flex; align-items: center; gap: 18px; font-size: .9rem; }
.admin-topbar__actions a { color: var(--color-brand-light); text-decoration: none; }
.admin-shell { display: grid; grid-template-columns: 252px minmax(0, 1fr); min-height: calc(100vh - 68px); }
.admin-sidebar { padding: 24px 16px; color: var(--color-text-inverse); background: var(--color-bg-raised); }
.admin-sidebar nav { position: sticky; top: 92px; }
.admin-sidebar p { margin: 24px 12px 8px; color: var(--color-brand-light); font-size: .72rem; font-weight: 800; letter-spacing: .16em; }
.admin-sidebar a { display: flex; align-items: center; gap: 11px; min-height: 43px; margin: 3px 0; padding: 9px 12px; border-radius: 9px; text-decoration: none; color: var(--color-text-inverse); }
.admin-sidebar a:hover { background: rgba(255, 255, 255, .08); }
.admin-sidebar a.active { color: var(--color-text-dark); background: var(--color-brand); font-weight: 800; }
.admin-sidebar i { width: 20px; text-align: center; }
.admin-main { width: 100%; max-width: 1540px; padding: clamp(24px, 4vw, 48px); overflow: hidden; }
.sidebar-toggle { display: none; border: 0; color: var(--color-text-inverse); background: var(--color-bg); border-radius: 8px; }
.page-heading { display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px; }
.page-heading h1 { margin: 3px 0 0; color: var(--color-text-dark); font-size: clamp(1.65rem, 3vw, 2.35rem); line-height: 1.2; }
.eyebrow { margin: 0; color: var(--color-brand-dark); font-size: .72rem; font-weight: 900; letter-spacing: .18em; }
.stat-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px; margin-bottom: 24px; }
.stat-card { position: relative; min-height: 148px; padding: 24px; overflow: hidden; border: 1px solid rgba(78, 73, 43, .2); border-radius: 16px; background: var(--color-surface); box-shadow: 0 12px 28px rgba(10, 10, 10, .06); }
.stat-card span { display: block; color: var(--color-text-muted); font-size: .9rem; }
.stat-card strong { display: block; margin-top: 8px; color: var(--color-text-dark); font-size: 2.35rem; }
.stat-card i { position: absolute; right: 20px; bottom: 12px; color: rgba(78, 73, 43, .16); font-size: 3.6rem; }
.panel { margin-bottom: 24px; padding: 0; border: 1px solid rgba(78, 73, 43, .2); border-radius: 16px; background: var(--color-surface); box-shadow: 0 12px 28px rgba(10, 10, 10, .05); overflow: hidden; }
.panel__head { display: flex; justify-content: space-between; padding: 22px 24px 18px; border-bottom: 1px solid rgba(78, 73, 43, .14); }
.panel__head h2 { margin: 0; color: var(--color-text-dark); font-size: 1.1rem; }
.panel__head p { margin: 4px 0 0; color: var(--color-text-muted); font-size: .86rem; }
.module-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; padding: 20px; }
.module-grid a { display: grid; grid-template-columns: 34px 1fr auto; align-items: center; min-height: 62px; padding: 12px 15px; border: 1px solid rgba(78, 73, 43, .16); border-radius: 10px; text-decoration: none; background: var(--color-surface-soft); }
.module-grid a:hover { border-color: var(--color-brand); }
.module-grid i { color: var(--color-brand-dark); font-size: 1.2rem; }
.module-grid strong { color: var(--color-text-dark); font-size: 1.15rem; }
.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 18px; }
.filters { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.search { position: relative; }
.search i { position: absolute; top: 50%; left: 13px; transform: translateY(-50%); color: var(--color-text-muted); }
.search input { padding-left: 38px; }
input, select, textarea { width: 100%; padding: 10px 12px; color: var(--color-text); background: var(--color-surface); border: 1px solid rgba(78, 73, 43, .38); border-radius: 8px; }
input:focus, select:focus, textarea:focus { outline: 3px solid rgba(255, 204, 0, .3); border-color: var(--color-brand-dark); }
.filters input { width: min(320px, 70vw); }
.filters select { width: auto; min-width: 135px; }
.button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; padding: 9px 16px; border: 1px solid rgba(78, 73, 43, .35); border-radius: 8px; color: var(--color-text-dark); background: var(--color-surface); text-decoration: none; cursor: pointer; white-space: nowrap; }
.button--primary { color: var(--color-text-dark); background: var(--color-brand); border-color: var(--color-brand); font-weight: 800; }
.button:hover { filter: brightness(.97); }
.table-wrap { width: 100%; overflow-x: auto; }
table { width: 100%; border-collapse: collapse; white-space: nowrap; }
th, td { padding: 14px 16px; border-bottom: 1px solid rgba(78, 73, 43, .13); text-align: left; font-size: .88rem; }
th { color: var(--color-brand-dark); background: var(--color-surface-soft); font-size: .77rem; letter-spacing: .04em; }
tbody tr:hover { background: rgba(255, 204, 0, .06); }
.actions { display: flex; align-items: center; gap: 12px; }
.actions a, .actions button, .table-button { padding: 0; border: 0; color: var(--color-brand-dark); background: transparent; cursor: pointer; text-decoration: underline; }
.actions button { color: var(--color-brand-dark); font-weight: 700; }
.table-button:disabled { color: var(--color-text-muted); cursor: not-allowed; }
.empty { padding: 42px; text-align: center; color: var(--color-text-muted); }
.status { display: inline-flex; align-items: center; justify-content: center; min-width: 66px; padding: 4px 9px; border-radius: 999px; font-size: .75rem; font-weight: 800; }
.status--published, .status--available { color: var(--color-brand-dark); background: var(--color-brand-light); }
.status--coming_soon { color: var(--color-brand-dark); background: var(--color-brand-light); }
.status--draft { color: var(--color-text-muted); background: rgba(77, 77, 77, .12); }
.status--archived { color: var(--color-text-muted); background: rgba(77, 77, 77, .12); }
.status--new { color: var(--color-text-dark); background: var(--color-brand); }
.status--contacted { color: var(--color-brand-dark); background: var(--color-brand-light); }
.status--confirmed { color: var(--color-brand-dark); background: var(--color-brand-light); border: 1px solid var(--color-brand); }
.status--declined { color: var(--color-text-muted); background: rgba(77, 77, 77, .12); }
.pagination { display: flex; justify-content: center; gap: 7px; padding: 18px; }
.pagination a { display: grid; place-items: center; width: 36px; height: 36px; border: 1px solid rgba(78, 73, 43, .25); border-radius: 7px; text-decoration: none; }
.pagination a.active { color: var(--color-text-dark); background: var(--color-brand); border-color: var(--color-brand); font-weight: 800; }
.notice { margin-bottom: 20px; padding: 14px 17px; border-radius: 9px; border: 1px solid rgba(78, 73, 43, .25); background: var(--color-surface); }
.notice--success { color: var(--color-brand-dark); border-color: rgba(78, 73, 43, .3); background: var(--color-brand-light); }
.notice--error { color: var(--color-text-dark); border-color: rgba(78, 73, 43, .4); background: var(--color-surface-soft); }
.notice--warning { color: var(--color-brand-dark); background: var(--color-brand-light); }
.editor { padding: clamp(20px, 4vw, 34px); }
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
.field { display: flex; flex-direction: column; gap: 7px; color: var(--color-text-muted); font-size: .87rem; font-weight: 700; }
.field--wide { grid-column: 1 / -1; }
.field small { font-weight: 400; overflow-wrap: anywhere; }
.check { display: flex; align-items: center; min-height: 43px; gap: 8px; }
.check input { width: auto; }
.form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 28px; padding-top: 20px; border-top: 1px solid rgba(78, 73, 43, .14); }
.inline-form { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)) auto; align-items: end; gap: 14px; padding: 22px; }
.inline-form label { display: flex; flex-direction: column; gap: 6px; color: var(--color-text-muted); font-size: .82rem; font-weight: 700; }
.application-detail .panel__head { align-items: center; gap: 16px; }
.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; background: rgba(78, 73, 43, .14); }
.detail-item { min-width: 0; padding: 18px 22px; background: var(--color-surface); }
.detail-item--wide { grid-column: 1 / -1; }
.detail-item > span { display: block; margin-bottom: 7px; color: var(--color-text-muted); font-size: .78rem; font-weight: 800; }
.detail-item strong, .detail-item a, .detail-value { color: var(--color-text-dark); overflow-wrap: anywhere; }
.detail-value { line-height: 1.75; }
.detail-download { width: fit-content; max-width: 100%; white-space: normal; text-align: left; }
.auth-page { display: grid; place-items: center; padding: 24px; color: var(--color-text-inverse); background: var(--color-bg); background-image: radial-gradient(circle at 20% 10%, rgba(255, 204, 0, .16), transparent 32%), radial-gradient(circle at 80% 90%, rgba(252, 238, 167, .1), transparent 34%); }
.auth-card { position: relative; width: min(500px, 100%); padding: clamp(28px, 6vw, 48px); overflow: hidden; border: 1px solid rgba(255, 204, 0, .35); border-radius: 20px; background: var(--color-bg-raised); box-shadow: 0 26px 80px rgba(0, 0, 0, .45); }
.auth-card::before { content: ""; position: absolute; inset: 0 0 auto; height: 4px; background: linear-gradient(90deg, transparent, var(--color-brand), transparent); }
.auth-brand { display: flex; flex-direction: column; align-items: center; gap: 9px; width: 100%; margin: 0 auto 24px; color: var(--color-brand-light); text-decoration: none; }
.auth-brand img { display: block; width: min(340px, 92%); height: auto; object-fit: contain; }
.auth-brand span { font-size: .68rem; font-weight: 800; letter-spacing: .2em; text-align: center; }
.auth-card h1 { margin: 7px 0 10px; color: var(--color-text-inverse); text-align: center; }
.auth-card > p { color: var(--color-brand-light); text-align: center; }
.auth-card .eyebrow { color: var(--color-brand); }
.auth-card form { display: flex; flex-direction: column; gap: 17px; margin-top: 28px; }
.auth-card label { display: flex; flex-direction: column; gap: 7px; color: var(--color-text-inverse); font-size: .9rem; }
.auth-card input { color: var(--color-text-inverse); background: var(--color-bg); border-color: rgba(255, 204, 0, .36); }
.auth-card .text-link { color: var(--color-brand-light); text-align: center; font-size: .86rem; }
@media (max-width: 1100px) {
.stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.module-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.inline-form { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
.admin-topbar { padding-inline: 16px; }
.admin-brand small, .admin-topbar__actions span { display: none; }
.admin-brand__logo { width: min(190px, 48vw); }
.admin-shell { display: block; }
.admin-sidebar { position: fixed; z-index: 30; inset: 68px auto 0 0; width: min(290px, 86vw); transform: translateX(-105%); transition: transform .2s ease; overflow-y: auto; }
.admin-sidebar.is-open { transform: translateX(0); }
.admin-main { padding: 20px 16px 36px; }
.sidebar-toggle { display: inline-flex; align-items: center; justify-content: center; width: 44px; margin-bottom: 18px; }
.toolbar { align-items: stretch; flex-direction: column; }
.filters { display: grid; grid-template-columns: 1fr; }
.filters input, .filters select { width: 100%; }
.stat-grid, .module-grid, .form-grid, .inline-form, .detail-grid { grid-template-columns: 1fr; }
.field--wide, .detail-item--wide { grid-column: auto; }
}
+9
View File
@@ -0,0 +1,9 @@
document.querySelector('[data-sidebar-toggle]')?.addEventListener('click', function () {
document.querySelector('#admin-sidebar')?.classList.toggle('is-open');
});
document.querySelectorAll('form[data-confirm]').forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!window.confirm(form.dataset.confirm || '確定執行?')) event.preventDefault();
});
});
+312
View File
@@ -0,0 +1,312 @@
<?php
declare(strict_types=1);
session_start();
$config = require __DIR__ . '/config.php';
function db(): PDO
{
static $pdo;
global $config;
if ($pdo instanceof PDO) {
return $pdo;
}
$db = $config['db'];
$dsn = "mysql:host={$db['host']};port={$db['port']};dbname={$db['name']};charset=utf8mb4";
$pdo = new PDO($dsn, $db['user'], $db['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
return $pdo;
}
function h(?string $value): string
{
return htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function redirect(string $path): never
{
header('Location: ' . $path);
exit;
}
function csrf_token(): string
{
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf'];
}
function verify_csrf(): void
{
$token = (string) ($_POST['csrf'] ?? '');
if (!hash_equals((string) ($_SESSION['csrf'] ?? ''), $token)) {
http_response_code(419);
exit('表單已過期,請返回後重新操作。');
}
}
function require_login(): void
{
if (empty($_SESSION['admin_id'])) {
redirect('login.php');
}
}
function flash(string $type, string $message): void
{
$_SESSION['flash'] = ['type' => $type, 'message' => $message];
}
function take_flash(): ?array
{
$flash = $_SESSION['flash'] ?? null;
unset($_SESSION['flash']);
return is_array($flash) ? $flash : null;
}
function modules(): array
{
return [
'albums' => [
'label' => '專輯管理', 'icon' => 'disc', 'table' => 'albums', 'order' => 'release_date DESC, id DESC',
'fields' => [
'title' => ['label' => '專輯名稱', 'type' => 'text', 'required' => true],
'title_ko' => ['label' => '韓文/英文名稱', 'type' => 'text'],
'release_date' => ['label' => '發行日期', 'type' => 'date'],
'era' => ['label' => '時期', 'type' => 'select', 'options' => ['classic' => '輝煌全盛期', 'reunion' => '啟動新篇章']],
'cover_media_id' => ['label' => '封面媒體編號', 'type' => 'number'],
'summary' => ['label' => '專輯簡介', 'type' => 'textarea'],
'sort_order' => ['label' => '排序', 'type' => 'number', 'default' => '0'],
'status' => ['label' => '公開狀態', 'type' => 'status', 'required' => true],
],
'list' => ['title', 'release_date', 'era', 'status', 'updated_at'],
],
'songs' => [
'label' => '歌曲管理', 'icon' => 'music-note-list', 'table' => 'songs', 'order' => 'album_id, track_no, id',
'fields' => [
'album_id' => ['label' => '所屬專輯', 'type' => 'relation', 'table' => 'albums', 'required' => true],
'track_no' => ['label' => '曲序', 'type' => 'number'],
'title' => ['label' => '歌名', 'type' => 'text', 'required' => true],
'title_ko' => ['label' => '韓文歌名', 'type' => 'text'],
'lyrics_url' => ['label' => '歌詞頁連結', 'type' => 'url'],
'video_url' => ['label' => '影片連結', 'type' => 'url'],
'is_title_track' => ['label' => '主打歌', 'type' => 'checkbox'],
'status' => ['label' => '公開狀態', 'type' => 'status', 'required' => true],
],
'list' => ['title', 'album_id', 'track_no', 'is_title_track', 'status', 'updated_at'],
],
'concerts' => [
'label' => '演唱會管理', 'icon' => 'ticket-perforated', 'table' => 'concert_events', 'order' => 'event_date DESC, id DESC',
'fields' => [
'tour_name' => ['label' => '巡演/活動名稱', 'type' => 'text', 'required' => true],
'event_date' => ['label' => '演出日期', 'type' => 'date', 'required' => true],
'country' => ['label' => '國家', 'type' => 'text'],
'city' => ['label' => '城市', 'type' => 'text'],
'venue' => ['label' => '場館', 'type' => 'text'],
'member_names' => ['label' => '演出成員', 'type' => 'text'],
'poster_media_id' => ['label' => '海報媒體編號', 'type' => 'number'],
'source_url' => ['label' => '資料來源', 'type' => 'url'],
'notes' => ['label' => '備註', 'type' => 'textarea'],
'status' => ['label' => '公開狀態', 'type' => 'status', 'required' => true],
],
'list' => ['tour_name', 'event_date', 'city', 'venue', 'status', 'updated_at'],
],
'variety' => [
'label' => '綜藝節目管理', 'icon' => 'tv', 'table' => 'variety_shows', 'order' => 'air_start DESC, id DESC',
'fields' => [
'title' => ['label' => '節目名稱', 'type' => 'text', 'required' => true],
'series_name' => ['label' => '系列/單元', 'type' => 'text'],
'platform' => ['label' => '播出平台', 'type' => 'text'],
'air_start' => ['label' => '開始日期', 'type' => 'date'],
'air_end' => ['label' => '結束日期', 'type' => 'date'],
'member_names' => ['label' => '參與成員', 'type' => 'text'],
'episode_info' => ['label' => '集數', 'type' => 'text'],
'video_url' => ['label' => '影片連結', 'type' => 'url'],
'summary' => ['label' => '節目簡介', 'type' => 'textarea'],
'tags' => ['label' => '標籤', 'type' => 'text'],
'status' => ['label' => '公開狀態', 'type' => 'status', 'required' => true],
],
'list' => ['title', 'platform', 'air_start', 'member_names', 'status', 'updated_at'],
],
'locations' => [
'label' => 'Yellow Note', 'icon' => 'geo-alt', 'table' => 'locations', 'order' => 'visit_date DESC, sort_order, id',
'fields' => [
'title' => ['label' => '地點名稱', 'type' => 'text', 'required' => true],
'visit_date' => ['label' => '行程日期', 'type' => 'date'],
'day_label' => ['label' => '日次', 'type' => 'text'],
'address' => ['label' => '地址', 'type' => 'text'],
'latitude' => ['label' => '緯度', 'type' => 'number', 'step' => 'any'],
'longitude' => ['label' => '經度', 'type' => 'number', 'step' => 'any'],
'member_names' => ['label' => '參與成員', 'type' => 'text'],
'description' => ['label' => '行程說明', 'type' => 'textarea'],
'quote_text' => ['label' => '引言', 'type' => 'textarea'],
'tags' => ['label' => '標籤', 'type' => 'text'],
'media_id' => ['label' => '圖片媒體編號', 'type' => 'number'],
'source_url' => ['label' => '來源連結', 'type' => 'url'],
'sort_order' => ['label' => '排序', 'type' => 'number', 'default' => '0'],
'status' => ['label' => '公開狀態', 'type' => 'status', 'required' => true],
],
'list' => ['title', 'visit_date', 'member_names', 'tags', 'status', 'updated_at'],
],
'media' => [
'label' => '媒體庫', 'icon' => 'images', 'table' => 'media_assets', 'order' => 'id DESC',
'fields' => [
'display_name' => ['label' => '管理名稱', 'type' => 'text', 'required' => true],
'file_path' => ['label' => '檔案', 'type' => 'file'],
'media_type' => ['label' => '類型', 'type' => 'select', 'default' => 'image', 'options' => ['image' => '圖片', 'video' => '影片', 'pdf' => 'PDF', 'other' => '其他']],
'alt_text' => ['label' => '替代文字', 'type' => 'text'],
'source_note' => ['label' => '來源/授權註記', 'type' => 'textarea'],
'category' => ['label' => '分類', 'type' => 'text'],
'status' => ['label' => '使用狀態', 'type' => 'select', 'default' => 'draft', 'options' => ['available' => '可使用', 'draft' => '待整理', 'archived' => '封存']],
],
'list' => ['display_name', 'media_type', 'category', 'file_path', 'status', 'updated_at'],
],
'dance_practices' => [
'label' => '練舞清單', 'item_label' => '練舞項目', 'list_title' => '練舞清單', 'icon' => 'list-check', 'table' => 'dance_practice_items', 'order' => 'sort_order, id', 'group' => 'dance',
'fields' => [
'title' => ['label' => '歌曲名稱', 'type' => 'text', 'required' => true],
'tags' => ['label' => '練習分類', 'type' => 'text'],
'practice_focus' => ['label' => '練習重點', 'type' => 'textarea', 'required' => true],
'video_url' => ['label' => '參考影片網址', 'type' => 'url'],
'difficulty' => ['label' => '難度', 'type' => 'select', 'default' => 'medium', 'required' => true, 'options' => ['easy' => '入門', 'medium' => '中等', 'hard' => '挑戰']],
'progress_status' => ['label' => '練習進度', 'type' => 'select', 'default' => 'not_started', 'required' => true, 'options' => ['not_started' => '未開始', 'practicing' => '練習中', 'review' => '待複習', 'completed' => '已完成']],
'note_text' => ['label' => '補充說明', 'type' => 'text'],
'is_featured' => ['label' => '優先練習', 'type' => 'checkbox'],
'sort_order' => ['label' => '排序', 'type' => 'number', 'default' => '0'],
'status' => ['label' => '公開狀態', 'type' => 'status', 'required' => true],
],
'list' => ['title', 'difficulty', 'progress_status', 'is_featured', 'sort_order', 'status', 'updated_at'],
],
];
}
function module_config(string $key): array
{
$all = modules();
if (!isset($all[$key])) {
http_response_code(404);
exit('找不到管理模組。');
}
return $all[$key];
}
function status_label(string $status): string
{
return ['draft' => '草稿', 'coming_soon' => '籌備中', 'published' => '公開', 'archived' => '封存', 'available' => '可使用'][$status] ?? $status;
}
function dance_application_statuses(): array
{
return [
'new' => '新報名',
'contacted' => '已聯絡',
'confirmed' => '確認參與',
'declined' => '暫不參與',
'archived' => '已封存',
];
}
function dance_attendance_label(?string $value): string
{
return [
'attended' => '參加過 20TH',
'watched_video' => '看過 20TH 影片',
'first_time' => '第一次知道',
][$value ?? ''] ?? '未填寫';
}
function dance_song_label(?string $value): string
{
return [
'couple' => 'Couple',
'comeback' => "Com' Back",
'road_fighter' => 'Road Fighter',
'pom_saeng_pom_sa' => 'Pom Saeng Pom Sa',
'other' => '其他',
][$value ?? ''] ?? '未選擇';
}
function dance_content_labels(?string $value): array
{
$labels = [
'cover' => '翻跳',
'medley' => '串燒聯舞',
'blessing' => '祝福影片',
'graphic' => '圖文應援',
'editing' => '影片剪輯',
'archive' => '資料整理',
];
$selected = array_filter(array_map('trim', explode(',', (string) $value)));
return array_map(static fn (string $item): string => $labels[$item] ?? $item, $selected);
}
function audit(string $action, string $entityType, ?int $entityId, array $details = []): void
{
$stmt = db()->prepare('INSERT INTO audit_logs (admin_user_id, action_name, entity_type, entity_id, details_json, ip_address) VALUES (?, ?, ?, ?, ?, ?)');
$stmt->execute([$_SESSION['admin_id'] ?? null, $action, $entityType, $entityId, json_encode($details, JSON_UNESCAPED_UNICODE), $_SERVER['REMOTE_ADDR'] ?? null]);
}
function render_header(string $title, string $active = ''): void
{
global $config;
$flash = take_flash();
$mods = modules();
?>
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= h($title) ?><?= h($config['site_name']) ?> Admin</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/admin.css">
</head>
<body>
<header class="admin-topbar">
<a class="admin-brand" href="index.php" aria-label="水晶男孩推廣部後台首頁">
<img class="admin-brand__logo" src="../assets/images/logov.svg" alt="SECHSKIES 水晶男孩推廣部">
<small>Admin</small>
</a>
<div class="admin-topbar__actions"><span><?= h($_SESSION['admin_name'] ?? '') ?></span><a href="../index.html" target="_blank" rel="noopener">查看前臺</a><a href="logout.php">登出</a></div>
</header>
<div class="admin-shell">
<aside class="admin-sidebar" id="admin-sidebar">
<nav aria-label="後臺管理選單">
<a class="<?= $active === 'dashboard' ? 'active' : '' ?>" href="index.php"><i class="bi bi-speedometer2"></i>控制台</a>
<p>內容資料</p>
<?php foreach ($mods as $key => $mod): ?>
<?php if (($mod['group'] ?? 'content') === 'dance') continue; ?>
<a class="<?= $active === $key ? 'active' : '' ?>" href="records.php?module=<?= h($key) ?>"><i class="bi bi-<?= h($mod['icon']) ?>"></i><?= h($mod['label']) ?></a>
<?php endforeach; ?>
<p>水晶熱舞社</p>
<?php foreach ($mods as $key => $mod): ?>
<?php if (($mod['group'] ?? 'content') !== 'dance') continue; ?>
<a class="<?= $active === $key ? 'active' : '' ?>" href="records.php?module=<?= h($key) ?>"><i class="bi bi-<?= h($mod['icon']) ?>"></i><?= h($mod['label']) ?></a>
<?php endforeach; ?>
<a class="<?= $active === 'dance-applications' ? 'active' : '' ?>" href="dance-applications.php"><i class="bi bi-person-hearts"></i>30TH 應援報名</a>
<p>系統</p>
<a class="<?= $active === 'publishing' ? 'active' : '' ?>" href="publishing.php"><i class="bi bi-eye"></i>公開狀態</a>
<a class="<?= $active === 'admins' ? 'active' : '' ?>" href="admins.php"><i class="bi bi-shield-lock"></i>管理者帳號</a>
</nav>
</aside>
<main class="admin-main">
<button class="sidebar-toggle" type="button" data-sidebar-toggle aria-label="開啟管理選單"><i class="bi bi-list"></i></button>
<?php if ($flash): ?><div class="notice notice--<?= h($flash['type']) ?>"><?= h($flash['message']) ?></div><?php endif; ?>
<div class="page-heading"><div><p class="eyebrow">CONTENT MANAGEMENT</p><h1><?= h($title) ?></h1></div></div>
<?php
}
function render_footer(): void
{
?>
</main>
</div>
<script src="assets/admin.js"></script>
</body>
</html>
<?php
}
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
return [
'site_name' => '水晶男孩推廣部',
'db' => [
'host' => getenv('SECHSKIES_DB_HOST') ?: '127.0.0.1',
'port' => getenv('SECHSKIES_DB_PORT') ?: '3306',
'name' => getenv('SECHSKIES_DB_NAME') ?: 'sechskies_cms',
'user' => getenv('SECHSKIES_DB_USER') ?: 'root',
'pass' => getenv('SECHSKIES_DB_PASS') ?: '',
],
'upload_dir' => dirname(__DIR__) . '/assets/uploads',
'upload_url' => '../assets/uploads',
'max_upload_bytes' => 8 * 1024 * 1024,
'dance_upload_dir' => dirname(__DIR__) . '/storage/dance-applications',
'dance_max_upload_bytes' => 8 * 1024 * 1024,
];
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
require_login();
$id = (int) ($_GET['id'] ?? 0);
$stmt = db()->prepare('SELECT reference_file_name, reference_original_name, reference_mime_type FROM dance_applications WHERE id = ?');
$stmt->execute([$id]);
$file = $stmt->fetch();
if (!$file || !$file['reference_file_name']) {
http_response_code(404);
exit('找不到檔案。');
}
$storedName = basename((string) $file['reference_file_name']);
$path = (string) $config['dance_upload_dir'] . DIRECTORY_SEPARATOR . $storedName;
if (!is_file($path)) {
http_response_code(404);
exit('檔案不存在。');
}
$downloadName = basename((string) ($file['reference_original_name'] ?: $storedName));
$downloadName = str_replace(["\r", "\n", '"'], '', $downloadName);
audit('download', 'dance_applications', $id, ['file' => $downloadName]);
header('Content-Type: ' . ((string) $file['reference_mime_type'] ?: 'application/octet-stream'));
header('Content-Length: ' . filesize($path));
header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($downloadName));
header('X-Content-Type-Options: nosniff');
readfile($path);
exit;
+61
View File
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
require_login();
$id = (int) ($_GET['id'] ?? $_POST['id'] ?? 0);
$statusOptions = dance_application_statuses();
$stmt = db()->prepare('SELECT * FROM dance_applications WHERE id = ?');
$stmt->execute([$id]);
$application = $stmt->fetch();
if (!$application) {
http_response_code(404);
exit('找不到這筆報名資料。');
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$status = trim((string) ($_POST['status'] ?? ''));
$adminNotes = mb_substr(trim((string) ($_POST['admin_notes'] ?? '')), 0, 5000, 'UTF-8');
if (!isset($statusOptions[$status])) {
$error = '請選擇有效的處理狀態。';
} else {
$update = db()->prepare('UPDATE dance_applications SET status = ?, admin_notes = ? WHERE id = ?');
$update->execute([$status, $adminNotes !== '' ? $adminNotes : null, $id]);
audit('update', 'dance_applications', $id, ['status' => $status, 'admin_notes' => $adminNotes]);
flash('success', '報名資料的處理狀態已更新。');
redirect('dance-application.php?id=' . $id);
}
}
render_header('處理熱舞社報名 #' . $id, 'dance-applications');
?>
<?php if ($error): ?><div class="notice notice--error"><?= h($error) ?></div><?php endif; ?>
<div class="toolbar"><a class="button" href="dance-applications.php"><i class="bi bi-arrow-left"></i>回報名清單</a></div>
<section class="panel application-detail">
<div class="panel__head"><div><h2><?= h($application['nickname']) ?></h2><p>送出於 <?= h($application['created_at']) ?></p></div><span class="status status--<?= h($application['status']) ?>"><?= h($statusOptions[$application['status']] ?? $application['status']) ?></span></div>
<div class="detail-grid">
<div class="detail-item"><span>暱稱</span><strong><?= h($application['nickname']) ?></strong></div>
<div class="detail-item"><span>Email</span><a href="mailto:<?= h($application['email']) ?>"><?= h($application['email']) ?></a></div>
<div class="detail-item"><span>練舞經驗</span><strong><?= $application['dance_years'] !== null ? (int) $application['dance_years'] . ' 年' : '未填寫' ?></strong></div>
<div class="detail-item"><span>可參與日期</span><strong><?= h($application['available_date'] ?: '未填寫') ?></strong></div>
<div class="detail-item"><span>20TH 經驗</span><strong><?= h(dance_attendance_label($application['attended_20th'])) ?></strong></div>
<div class="detail-item"><span>想練的歌曲</span><strong><?= h(dance_song_label($application['song'])) ?></strong></div>
<div class="detail-item detail-item--wide"><span>想參與的內容</span><strong><?= h(implode('、', dance_content_labels($application['participate_content']))) ?></strong></div>
<div class="detail-item detail-item--wide"><span>30TH 留言</span><div class="detail-value"><?= $application['message_30th'] ? nl2br(h($application['message_30th'])) : '未填寫' ?></div></div>
<div class="detail-item detail-item--wide"><span>參考檔案</span><?php if ($application['reference_file_name']): ?><a class="button detail-download" href="dance-application-file.php?id=<?= $id ?>"><i class="bi bi-download"></i><?= h($application['reference_original_name'] ?: '下載檔案') ?><?php if ($application['reference_file_size']): ?><?= h(number_format((int) $application['reference_file_size'] / 1024 / 1024, 2)) ?> MB<?php endif; ?></a><?php else: ?><strong>未上傳</strong><?php endif; ?></div>
</div>
</section>
<form class="panel editor" method="post">
<input type="hidden" name="csrf" value="<?= h(csrf_token()) ?>">
<input type="hidden" name="id" value="<?= $id ?>">
<div class="form-grid">
<label class="field"><span>處理狀態 *</span><select name="status" required><?php foreach ($statusOptions as $value => $label): ?><option value="<?= h($value) ?>" <?= $application['status'] === $value ? 'selected' : '' ?>><?= h($label) ?></option><?php endforeach; ?></select></label>
<label class="field field--wide"><span>內部備註</span><textarea name="admin_notes" rows="6" maxlength="5000" placeholder="例如:已寄信、等待確認練習日期……"><?= h((string) $application['admin_notes']) ?></textarea></label>
</div>
<div class="form-actions"><a class="button" href="dance-applications.php">取消</a><button class="button button--primary" type="submit">儲存處理狀態</button></div>
</form>
<?php render_footer();
+84
View File
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
require_login();
$page = max(1, (int) ($_GET['page'] ?? 1));
$perPage = 15;
$query = trim((string) ($_GET['q'] ?? ''));
$status = trim((string) ($_GET['status'] ?? ''));
$statusOptions = dance_application_statuses();
$where = [];
$params = [];
if ($query !== '') {
$where[] = '(nickname LIKE ? OR email LIKE ? OR message_30th LIKE ?)';
$searchValue = '%' . $query . '%';
$params = [$searchValue, $searchValue, $searchValue];
}
if ($status !== '' && isset($statusOptions[$status])) {
$where[] = 'status = ?';
$params[] = $status;
} else {
$status = '';
}
$whereSql = $where ? ' WHERE ' . implode(' AND ', $where) : '';
$countStmt = db()->prepare('SELECT COUNT(*) FROM dance_applications' . $whereSql);
$countStmt->execute($params);
$total = (int) $countStmt->fetchColumn();
$pages = max(1, (int) ceil($total / $perPage));
$page = min($page, $pages);
$offset = ($page - 1) * $perPage;
$stmt = db()->prepare('SELECT * FROM dance_applications' . $whereSql . " ORDER BY created_at DESC, id DESC LIMIT $perPage OFFSET $offset");
$stmt->execute($params);
$rows = $stmt->fetchAll();
render_header('30TH 應援報名', 'dance-applications');
?>
<div class="toolbar">
<form class="filters" method="get">
<label class="search"><i class="bi bi-search"></i><input name="q" value="<?= h($query) ?>" placeholder="搜尋暱稱、Email 或留言"></label>
<select name="status" aria-label="處理狀態">
<option value="">全部狀態</option>
<?php foreach ($statusOptions as $value => $label): ?>
<option value="<?= h($value) ?>" <?= $status === $value ? 'selected' : '' ?>><?= h($label) ?></option>
<?php endforeach; ?>
</select>
<button class="button" type="submit">篩選</button>
</form>
<a class="button" href="../yellow-note/crystal-dance-survey.html#form-30th" target="_blank" rel="noopener"><i class="bi bi-box-arrow-up-right"></i>查看募集表單</a>
</div>
<section class="panel">
<div class="panel__head"><div><h2>報名清單</h2><p>共 <?= $total ?> 筆;Email 與上傳檔案僅供企劃聯絡使用</p></div></div>
<div class="table-wrap">
<table>
<thead><tr><th>編號</th><th>暱稱</th><th>Email</th><th>想參與</th><th>歌曲</th><th>狀態</th><th>送出時間</th><th>操作</th></tr></thead>
<tbody>
<?php foreach ($rows as $row): ?>
<tr>
<td>#<?= (int) $row['id'] ?></td>
<td><?= h($row['nickname']) ?></td>
<td><?= h(mb_strimwidth((string) $row['email'], 0, 34, '…', 'UTF-8')) ?></td>
<td><?= h(implode('、', dance_content_labels($row['participate_content']))) ?></td>
<td><?= h(dance_song_label($row['song'])) ?></td>
<td><span class="status status--<?= h($row['status']) ?>"><?= h($statusOptions[$row['status']] ?? $row['status']) ?></span></td>
<td><?= h($row['created_at']) ?></td>
<td class="actions"><a href="dance-application.php?id=<?= (int) $row['id'] ?>">查看/處理</a></td>
</tr>
<?php endforeach; ?>
<?php if (!$rows): ?><tr><td colspan="8" class="empty">目前沒有符合條件的報名資料。</td></tr><?php endif; ?>
</tbody>
</table>
</div>
<?php if ($pages > 1): ?>
<nav class="pagination" aria-label="報名資料分頁">
<?php for ($i = 1; $i <= $pages; $i++): ?>
<a class="<?= $i === $page ? 'active' : '' ?>" href="?q=<?= urlencode($query) ?>&status=<?= urlencode($status) ?>&page=<?= $i ?>"><?= $i ?></a>
<?php endfor; ?>
</nav>
<?php endif; ?>
</section>
<?php render_footer();
+186
View File
@@ -0,0 +1,186 @@
CREATE DATABASE IF NOT EXISTS sechskies_cms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE sechskies_cms;
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS admin_users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(80) NOT NULL,
email VARCHAR(190) NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
last_login_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS media_assets (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
display_name VARCHAR(160) NOT NULL,
original_name VARCHAR(255) NULL,
file_path VARCHAR(500) NOT NULL,
media_type ENUM('image','video','pdf','other') NOT NULL DEFAULT 'image',
mime_type VARCHAR(100) NULL,
file_size INT UNSIGNED NULL,
alt_text VARCHAR(255) NULL,
source_note TEXT NULL,
category VARCHAR(80) NULL,
status ENUM('available','draft','archived') NOT NULL DEFAULT 'draft',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS albums (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(120) NOT NULL,
title_ko VARCHAR(160) NULL,
release_date DATE NULL,
era ENUM('classic','reunion') NULL,
cover_media_id INT UNSIGNED NULL,
summary TEXT NULL,
sort_order INT NOT NULL DEFAULT 0,
status ENUM('draft','coming_soon','published','archived') NOT NULL DEFAULT 'draft',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_albums_cover FOREIGN KEY (cover_media_id) REFERENCES media_assets(id) ON DELETE SET NULL
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS songs (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
album_id INT UNSIGNED NOT NULL,
track_no SMALLINT UNSIGNED NULL,
title VARCHAR(160) NOT NULL,
title_ko VARCHAR(160) NULL,
lyrics_url VARCHAR(500) NULL,
video_url VARCHAR(500) NULL,
is_title_track TINYINT(1) NOT NULL DEFAULT 0,
status ENUM('draft','coming_soon','published','archived') NOT NULL DEFAULT 'draft',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_songs_album FOREIGN KEY (album_id) REFERENCES albums(id) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS concert_events (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tour_name VARCHAR(180) NOT NULL,
event_date DATE NOT NULL,
country VARCHAR(80) NULL,
city VARCHAR(80) NULL,
venue VARCHAR(180) NULL,
member_names VARCHAR(255) NULL,
poster_media_id INT UNSIGNED NULL,
source_url VARCHAR(500) NULL,
notes TEXT NULL,
status ENUM('draft','coming_soon','published','archived') NOT NULL DEFAULT 'draft',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_concert_poster FOREIGN KEY (poster_media_id) REFERENCES media_assets(id) ON DELETE SET NULL
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS variety_shows (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(180) NOT NULL,
series_name VARCHAR(180) NULL,
platform VARCHAR(100) NULL,
air_start DATE NULL,
air_end DATE NULL,
member_names VARCHAR(255) NULL,
episode_info VARCHAR(100) NULL,
video_url VARCHAR(500) NULL,
summary TEXT NULL,
tags VARCHAR(255) NULL,
status ENUM('draft','coming_soon','published','archived') NOT NULL DEFAULT 'draft',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS locations (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(180) NOT NULL,
visit_date DATE NULL,
day_label VARCHAR(40) NULL,
address VARCHAR(255) NULL,
latitude DECIMAL(10,7) NULL,
longitude DECIMAL(10,7) NULL,
member_names VARCHAR(255) NULL,
description TEXT NULL,
quote_text TEXT NULL,
tags VARCHAR(255) NULL,
media_id INT UNSIGNED NULL,
source_url VARCHAR(500) NULL,
sort_order INT NOT NULL DEFAULT 0,
status ENUM('draft','coming_soon','published','archived') NOT NULL DEFAULT 'draft',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_location_media FOREIGN KEY (media_id) REFERENCES media_assets(id) ON DELETE SET NULL
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS dance_applications (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nickname VARCHAR(80) NOT NULL,
email VARCHAR(190) NOT NULL,
dance_years TINYINT UNSIGNED NULL,
available_date DATE NULL,
attended_20th ENUM('attended','watched_video','first_time') NULL,
participate_content VARCHAR(255) NOT NULL,
song VARCHAR(80) NULL,
reference_file_name VARCHAR(100) NULL,
reference_original_name VARCHAR(255) NULL,
reference_mime_type VARCHAR(100) NULL,
reference_file_size INT UNSIGNED NULL,
message_30th TEXT NULL,
status ENUM('new','contacted','confirmed','declined','archived') NOT NULL DEFAULT 'new',
admin_notes TEXT NULL,
submitted_ip VARCHAR(45) NULL,
user_agent VARCHAR(255) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_dance_applications_status_created (status, created_at),
INDEX idx_dance_applications_email (email)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS dance_practice_items (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(160) NOT NULL,
tags VARCHAR(255) NULL,
practice_focus TEXT NOT NULL,
video_url VARCHAR(500) NULL,
difficulty ENUM('easy','medium','hard') NOT NULL DEFAULT 'medium',
progress_status ENUM('not_started','practicing','review','completed') NOT NULL DEFAULT 'not_started',
note_text VARCHAR(255) NULL,
is_featured TINYINT(1) NOT NULL DEFAULT 0,
sort_order INT NOT NULL DEFAULT 0,
status ENUM('draft','coming_soon','published','archived') NOT NULL DEFAULT 'draft',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_dance_practice_order (status, sort_order, id),
INDEX idx_dance_practice_title (title)
) ENGINE=InnoDB;
INSERT INTO dance_practice_items
(title, tags, practice_focus, difficulty, progress_status, note_text, is_featured, sort_order, status)
SELECT 'Couple', 'Dance,Vocal', '副歌動作、隊形感、甜感表情', 'easy', 'not_started', '可作為第一首示範歌', 1, 10, 'published'
WHERE NOT EXISTS (SELECT 1 FROM dance_practice_items WHERE title = 'Couple');
INSERT INTO dance_practice_items
(title, tags, practice_focus, difficulty, progress_status, note_text, is_featured, sort_order, status)
SELECT 'Com'' Back', 'Dance,Rap', '強拍、Rap 段落、舞台氣勢', 'medium', 'not_started', '未來擴充', 0, 20, 'coming_soon'
WHERE NOT EXISTS (SELECT 1 FROM dance_practice_items WHERE title = 'Com'' Back');
INSERT INTO dance_practice_items
(title, tags, practice_focus, difficulty, progress_status, note_text, is_featured, sort_order, status)
SELECT 'Road Fighter', 'Dance,Rap,Performance', '節奏、力量、舞台感', 'hard', 'not_started', '未來擴充', 0, 30, 'coming_soon'
WHERE NOT EXISTS (SELECT 1 FROM dance_practice_items WHERE title = 'Road Fighter');
CREATE TABLE IF NOT EXISTS audit_logs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
admin_user_id INT UNSIGNED NULL,
action_name VARCHAR(40) NOT NULL,
entity_type VARCHAR(80) NOT NULL,
entity_id INT UNSIGNED NULL,
details_json JSON NULL,
ip_address VARCHAR(45) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_audit_entity (entity_type, entity_id),
CONSTRAINT fk_audit_admin FOREIGN KEY (admin_user_id) REFERENCES admin_users(id) ON DELETE SET NULL
) ENGINE=InnoDB;
+34
View File
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
require_login();
$stats = [];
foreach (modules() as $key => $module) {
$stats[$key] = (int) db()->query('SELECT COUNT(*) FROM `' . $module['table'] . '`')->fetchColumn();
}
$published = 0;
$comingSoon = 0;
foreach (modules() as $key => $module) {
if ($key === 'media') continue;
$published += (int) db()->query("SELECT COUNT(*) FROM `{$module['table']}` WHERE status = 'published'")->fetchColumn();
$comingSoon += (int) db()->query("SELECT COUNT(*) FROM `{$module['table']}` WHERE status = 'coming_soon'")->fetchColumn();
}
$recent = db()->query('SELECT a.*, u.display_name FROM audit_logs a LEFT JOIN admin_users u ON u.id = a.admin_user_id ORDER BY a.id DESC LIMIT 8')->fetchAll();
render_header('控制台', 'dashboard');
?>
<section class="stat-grid">
<article class="stat-card"><span>已公開內容</span><strong><?= $published ?></strong><i class="bi bi-broadcast"></i></article>
<article class="stat-card"><span>籌備中內容</span><strong><?= $comingSoon ?></strong><i class="bi bi-hourglass-split"></i></article>
<article class="stat-card"><span>媒體素材</span><strong><?= $stats['media'] ?></strong><i class="bi bi-images"></i></article>
<article class="stat-card"><span>全部內容</span><strong><?= array_sum($stats) ?></strong><i class="bi bi-archive"></i></article>
</section>
<section class="panel"><div class="panel__head"><div><h2>內容概況</h2><p>依本站真正的資料分類統計</p></div></div><div class="module-grid">
<?php foreach (modules() as $key => $module): ?><a href="records.php?module=<?= h($key) ?>"><i class="bi bi-<?= h($module['icon']) ?>"></i><span><?= h($module['label']) ?></span><strong><?= $stats[$key] ?></strong></a><?php endforeach; ?>
</div></section>
<section class="panel"><div class="panel__head"><div><h2>最近管理紀錄</h2><p>保留新增、修改、刪除與登入軌跡</p></div></div>
<div class="table-wrap"><table><thead><tr><th>時間</th><th>管理者</th><th>動作</th><th>資料類型</th><th>編號</th></tr></thead><tbody>
<?php foreach ($recent as $row): ?><tr><td><?= h($row['created_at']) ?></td><td><?= h($row['display_name'] ?? '系統') ?></td><td><?= h($row['action_name']) ?></td><td><?= h($row['entity_type']) ?></td><td><?= h((string) $row['entity_id']) ?></td></tr><?php endforeach; ?>
<?php if (!$recent): ?><tr><td colspan="5" class="empty">目前尚無管理紀錄。</td></tr><?php endif; ?>
</tbody></table></div></section>
<?php render_footer();
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
if (!empty($_SESSION['admin_id'])) redirect('index.php');
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
try {
$stmt = db()->prepare('SELECT * FROM admin_users WHERE username = ? AND is_active = 1 LIMIT 1');
$stmt->execute([trim((string) ($_POST['username'] ?? ''))]);
$user = $stmt->fetch();
if ($user && password_verify((string) ($_POST['password'] ?? ''), $user['password_hash'])) {
session_regenerate_id(true);
$_SESSION['admin_id'] = (int) $user['id'];
$_SESSION['admin_name'] = $user['display_name'];
db()->prepare('UPDATE admin_users SET last_login_at = NOW() WHERE id = ?')->execute([$user['id']]);
audit('login', 'admin_users', (int) $user['id']);
redirect('index.php');
}
$error = '帳號或密碼輸入錯誤。';
} catch (Throwable $e) {
$error = '資料庫尚未完成設定,請先執行初始化。';
}
}
?>
<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>管理登入|水晶男孩推廣部</title><link rel="stylesheet" href="assets/admin.css"></head>
<body class="auth-page"><main class="auth-card"><a class="auth-brand" href="../index.html" aria-label="返回水晶男孩推廣部前台"><img src="../assets/images/logov.svg" alt="SECHSKIES 水晶男孩推廣部"><span>CONTENT MANAGEMENT SYSTEM</span></a><p class="eyebrow">SECHSKIES ARCHIVE</p><h1>推廣部管理登入</h1><p>把散落的記憶,好好收進同一片黃色海洋。</p><?php if (isset($_GET['setup'])): ?><div class="notice notice--success">管理者建立完成,請登入。</div><?php endif; ?><?php if ($error): ?><div class="notice notice--error"><?= h($error) ?></div><?php endif; ?>
<form method="post"><input type="hidden" name="csrf" value="<?= h(csrf_token()) ?>"><label>管理者帳號<input name="username" required autocomplete="username" autofocus></label><label>密碼<input name="password" type="password" required autocomplete="current-password"></label><button class="button button--primary" type="submit">登入後臺</button><a class="text-link" href="setup.php">第一次使用?初始化系統</a></form></main></body></html>
+7
View File
@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
if (!empty($_SESSION['admin_id'])) audit('logout', 'admin_users', (int) $_SESSION['admin_id']);
$_SESSION = [];
session_destroy();
redirect('login.php');
+16
View File
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
require_login();
$rows = [];
foreach (modules() as $key => $module) {
if ($key === 'media') continue;
$counts = ['draft' => 0, 'coming_soon' => 0, 'published' => 0, 'archived' => 0];
foreach (db()->query("SELECT status, COUNT(*) total FROM `{$module['table']}` GROUP BY status")->fetchAll() as $row) $counts[$row['status']] = (int) $row['total'];
$rows[] = ['key' => $key, 'label' => $module['label'], 'counts' => $counts];
}
render_header('公開狀態', 'publishing');
?>
<div class="notice notice--warning"><strong>公開狀態不會直接改寫 portal-nav.js。</strong> 第一階段先管理內容狀態;正式同步頂欄前仍須依本站規則逐頁確認。</div>
<section class="panel"><div class="panel__head"><div><h2>各模組發布概況</h2><p>公開前先完成資料與來源檢查</p></div></div><div class="table-wrap"><table><thead><tr><th>內容模組</th><th>草稿</th><th>籌備中</th><th>公開</th><th>封存</th><th>管理</th></tr></thead><tbody><?php foreach ($rows as $row): ?><tr><td><?= h($row['label']) ?></td><td><?= $row['counts']['draft'] ?></td><td><?= $row['counts']['coming_soon'] ?></td><td><?= $row['counts']['published'] ?></td><td><?= $row['counts']['archived'] ?></td><td><a href="records.php?module=<?= h($row['key']) ?>">查看資料</a></td></tr><?php endforeach; ?></tbody></table></div></section>
<?php render_footer();
+14
View File
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
require_login();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); exit; }
verify_csrf();
$key = (string) ($_POST['module'] ?? '');
$module = module_config($key);
$id = (int) ($_POST['id'] ?? 0);
$stmt = db()->prepare('DELETE FROM `' . $module['table'] . '` WHERE id = ?');
$stmt->execute([$id]);
audit('delete', $module['table'], $id);
flash('success', '資料已刪除。');
redirect('records.php?module=' . urlencode($key));
+82
View File
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
require_login();
$key = (string) ($_GET['module'] ?? $_POST['module'] ?? '');
$module = module_config($key);
$id = (int) ($_GET['id'] ?? $_POST['id'] ?? 0);
$record = [];
if ($id) {
$stmt = db()->prepare('SELECT * FROM `' . $module['table'] . '` WHERE id = ?');
$stmt->execute([$id]);
$record = $stmt->fetch() ?: [];
if (!$record) { http_response_code(404); exit('資料不存在。'); }
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$data = [];
foreach ($module['fields'] as $name => $field) {
if ($field['type'] === 'file') continue;
$value = $field['type'] === 'checkbox' ? (isset($_POST[$name]) ? 1 : 0) : trim((string) ($_POST[$name] ?? ''));
if (!empty($field['required']) && $value === '') $error = '請完成所有必填欄位。';
$data[$name] = $value === '' ? null : $value;
}
if ($key === 'media' && isset($_FILES['file_path']) && $_FILES['file_path']['error'] !== UPLOAD_ERR_NO_FILE) {
$file = $_FILES['file_path'];
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'application/pdf' => 'pdf', 'video/mp4' => 'mp4'];
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);
if ($file['error'] !== UPLOAD_ERR_OK || $file['size'] > $config['max_upload_bytes'] || !isset($allowed[$mime])) {
$error = '檔案格式不支援或超過 8 MB。';
} else {
if (!is_dir($config['upload_dir'])) mkdir($config['upload_dir'], 0755, true);
$safeName = bin2hex(random_bytes(12)) . '.' . $allowed[$mime];
if (!move_uploaded_file($file['tmp_name'], $config['upload_dir'] . '/' . $safeName)) {
$error = '檔案儲存失敗。';
} else {
$data['file_path'] = $config['upload_url'] . '/' . $safeName;
$data['original_name'] = $file['name'];
$data['mime_type'] = $mime;
$data['file_size'] = $file['size'];
}
}
} elseif ($key === 'media' && $id) {
$data['file_path'] = $record['file_path'];
} elseif ($key === 'media') {
$error = '新增媒體時必須選擇檔案。';
}
if ($error === '') {
if ($id) {
$sets = [];
foreach ($data as $name => $_) $sets[] = "`$name` = ?";
$stmt = db()->prepare('UPDATE `' . $module['table'] . '` SET ' . implode(', ', $sets) . ' WHERE id = ?');
$stmt->execute([...array_values($data), $id]);
audit('update', $module['table'], $id, $data);
flash('success', '資料已更新。');
} else {
$names = array_keys($data);
$stmt = db()->prepare('INSERT INTO `' . $module['table'] . '` (`' . implode('`,`', $names) . '`) VALUES (' . implode(',', array_fill(0, count($names), '?')) . ')');
$stmt->execute(array_values($data));
$id = (int) db()->lastInsertId();
audit('create', $module['table'], $id, $data);
flash('success', '資料已新增。');
}
redirect('records.php?module=' . urlencode($key));
}
$record = array_merge($record, $data);
}
render_header(($id ? '編輯' : '新增') . ($module['item_label'] ?? $module['label']), $key);
?>
<?php if ($error): ?><div class="notice notice--error"><?= h($error) ?></div><?php endif; ?>
<form class="panel editor" method="post" enctype="multipart/form-data"><input type="hidden" name="csrf" value="<?= h(csrf_token()) ?>"><input type="hidden" name="module" value="<?= h($key) ?>"><input type="hidden" name="id" value="<?= $id ?>"><div class="form-grid">
<?php foreach ($module['fields'] as $name => $field): $value = $record[$name] ?? ($field['default'] ?? ''); ?><label class="field <?= $field['type'] === 'textarea' ? 'field--wide' : '' ?>"><span><?= h($field['label']) ?><?= !empty($field['required']) ? ' *' : '' ?></span>
<?php if ($field['type'] === 'textarea'): ?><textarea name="<?= h($name) ?>" rows="6" <?= !empty($field['required']) ? 'required' : '' ?>><?= h((string) $value) ?></textarea>
<?php elseif ($field['type'] === 'select'): ?><select name="<?= h($name) ?>" <?= !empty($field['required']) ? 'required' : '' ?>><option value="">請選擇</option><?php foreach ($field['options'] as $optionValue => $optionLabel): ?><option value="<?= h($optionValue) ?>" <?= (string) $value === (string) $optionValue ? 'selected' : '' ?>><?= h($optionLabel) ?></option><?php endforeach; ?></select>
<?php elseif ($field['type'] === 'status'): ?><select name="<?= h($name) ?>" required><?php foreach (['draft' => '草稿', 'coming_soon' => '籌備中', 'published' => '公開', 'archived' => '封存'] as $optionValue => $optionLabel): ?><option value="<?= $optionValue ?>" <?= (string) ($value ?: 'draft') === $optionValue ? 'selected' : '' ?>><?= $optionLabel ?></option><?php endforeach; ?></select>
<?php elseif ($field['type'] === 'relation'): ?><select name="<?= h($name) ?>" required><option value="">請選擇</option><?php foreach (db()->query("SELECT id, title FROM `{$field['table']}` ORDER BY title")->fetchAll() as $option): ?><option value="<?= (int) $option['id'] ?>" <?= (string) $value === (string) $option['id'] ? 'selected' : '' ?>><?= h($option['title']) ?></option><?php endforeach; ?></select>
<?php elseif ($field['type'] === 'checkbox'): ?><span class="check"><input type="checkbox" name="<?= h($name) ?>" value="1" <?= $value ? 'checked' : '' ?>> 是</span>
<?php elseif ($field['type'] === 'file'): ?><?php if ($value): ?><small>目前檔案:<?= h((string) $value) ?></small><?php endif; ?><input type="file" name="<?= h($name) ?>" accept="image/*,video/mp4,application/pdf" <?= !$id ? 'required' : '' ?>>
<?php else: ?><input type="<?= h($field['type']) ?>" name="<?= h($name) ?>" value="<?= h((string) $value) ?>" <?= isset($field['step']) ? 'step="' . h($field['step']) . '"' : '' ?> <?= !empty($field['required']) ? 'required' : '' ?>><?php endif; ?></label><?php endforeach; ?></div>
<div class="form-actions"><a class="button" href="records.php?module=<?= h($key) ?>">取消</a><button class="button button--primary" type="submit"><?= $id ? '儲存修改' : '建立資料' ?></button></div></form>
<?php render_footer();
+54
View File
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
require_login();
$key = (string) ($_GET['module'] ?? '');
$module = module_config($key);
$page = max(1, (int) ($_GET['page'] ?? 1));
$perPage = 12;
$query = trim((string) ($_GET['q'] ?? ''));
$status = trim((string) ($_GET['status'] ?? ''));
$where = [];
$params = [];
if ($query !== '') {
$searchable = [];
foreach ($module['fields'] as $name => $field) {
if (in_array($field['type'], ['text', 'textarea', 'url'], true)) $searchable[] = "`$name` LIKE ?";
}
if ($searchable) {
$where[] = '(' . implode(' OR ', $searchable) . ')';
foreach ($searchable as $_) $params[] = '%' . $query . '%';
}
}
if ($status !== '' && isset($module['fields']['status'])) {
$where[] = '`status` = ?';
$params[] = $status;
}
$whereSql = $where ? ' WHERE ' . implode(' AND ', $where) : '';
$countStmt = db()->prepare('SELECT COUNT(*) FROM `' . $module['table'] . '`' . $whereSql);
$countStmt->execute($params);
$total = (int) $countStmt->fetchColumn();
$pages = max(1, (int) ceil($total / $perPage));
$page = min($page, $pages);
$offset = ($page - 1) * $perPage;
$stmt = db()->prepare('SELECT * FROM `' . $module['table'] . '`' . $whereSql . ' ORDER BY ' . $module['order'] . " LIMIT $perPage OFFSET $offset");
$stmt->execute($params);
$rows = $stmt->fetchAll();
$relations = [];
foreach ($module['fields'] as $name => $field) {
if ($field['type'] === 'relation') {
$relations[$name] = [];
foreach (db()->query("SELECT id, title FROM `{$field['table']}`")->fetchAll() as $rel) $relations[$name][$rel['id']] = $rel['title'];
}
}
render_header($module['label'], $key);
?>
<div class="toolbar"><form class="filters" method="get"><input type="hidden" name="module" value="<?= h($key) ?>"><label class="search"><i class="bi bi-search"></i><input name="q" value="<?= h($query) ?>" placeholder="搜尋本區資料"></label><?php if (isset($module['fields']['status'])): ?><select name="status"><option value="">全部狀態</option><?php foreach (['draft' => '草稿', 'coming_soon' => '籌備中', 'published' => '公開', 'archived' => '封存', 'available' => '可使用'] as $value => $label): ?><option value="<?= $value ?>" <?= $status === $value ? 'selected' : '' ?>><?= $label ?></option><?php endforeach; ?></select><?php endif; ?><button class="button" type="submit">篩選</button></form><a class="button button--primary" href="record-form.php?module=<?= h($key) ?>"><i class="bi bi-plus-lg"></i>新增資料</a></div>
<section class="panel"><div class="panel__head"><div><h2><?= h($module['list_title'] ?? ($module['label'] . '清單')) ?></h2><p>共 <?= $total ?> 筆;每頁 <?= $perPage ?> 筆</p></div></div><div class="table-wrap"><table><thead><tr><th>編號</th><?php foreach ($module['list'] as $field): ?><th><?= h($module['fields'][$field]['label'] ?? $field) ?></th><?php endforeach; ?><th>操作</th></tr></thead><tbody>
<?php foreach ($rows as $row): ?><tr><td>#<?= (int) $row['id'] ?></td><?php foreach ($module['list'] as $field): ?><td><?php $value = $row[$field] ?? ''; if ($field === 'status'): ?><span class="status status--<?= h($value) ?>"><?= h(status_label($value)) ?></span><?php elseif (isset($relations[$field])): ?><?= h($relations[$field][$value] ?? ('#' . $value)) ?><?php elseif (($module['fields'][$field]['type'] ?? '') === 'checkbox'): ?><?= $value ? '是' : '否' ?><?php elseif (($module['fields'][$field]['type'] ?? '') === 'select'): ?><?= h($module['fields'][$field]['options'][$value] ?? (string) $value) ?><?php else: ?><?= h(mb_strimwidth((string) $value, 0, 42, '…', 'UTF-8')) ?><?php endif; ?></td><?php endforeach; ?><td class="actions"><a href="record-form.php?module=<?= h($key) ?>&id=<?= (int) $row['id'] ?>">編輯</a><form method="post" action="record-delete.php" data-confirm="確定刪除這筆資料?此動作無法復原。"><input type="hidden" name="csrf" value="<?= h(csrf_token()) ?>"><input type="hidden" name="module" value="<?= h($key) ?>"><input type="hidden" name="id" value="<?= (int) $row['id'] ?>"><button type="submit">刪除</button></form></td></tr><?php endforeach; ?>
<?php if (!$rows): ?><tr><td colspan="<?= count($module['list']) + 2 ?>" class="empty">找不到符合條件的資料。</td></tr><?php endif; ?></tbody></table></div>
<?php if ($pages > 1): ?><nav class="pagination" aria-label="資料分頁"><?php for ($i = 1; $i <= $pages; $i++): ?><a class="<?= $i === $page ? 'active' : '' ?>" href="?module=<?= h($key) ?>&q=<?= urlencode($query) ?>&status=<?= urlencode($status) ?>&page=<?= $i ?>"><?= $i ?></a><?php endfor; ?></nav><?php endif; ?></section>
<?php render_footer();
+36
View File
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
$error = '';
try {
$count = (int) db()->query('SELECT COUNT(*) FROM admin_users')->fetchColumn();
if ($count > 0) {
redirect('login.php');
}
} catch (Throwable $e) {
$error = '尚未建立資料表。請先在 phpMyAdmin 匯入 admin/database/schema.sql。';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $error === '') {
verify_csrf();
$username = trim((string) ($_POST['username'] ?? ''));
$displayName = trim((string) ($_POST['display_name'] ?? ''));
$email = trim((string) ($_POST['email'] ?? ''));
$password = (string) ($_POST['password'] ?? '');
if (!preg_match('/^[A-Za-z0-9_.-]{4,50}$/', $username)) {
$error = '帳號須為 450 個英數字或 ._-。';
} elseif (mb_strlen($displayName) < 2) {
$error = '請填寫管理者顯示名稱。';
} elseif (strlen($password) < 10) {
$error = '密碼至少需要 10 個字元。';
} else {
$stmt = db()->prepare('INSERT INTO admin_users (username, password_hash, display_name, email) VALUES (?, ?, ?, ?)');
$stmt->execute([$username, password_hash($password, PASSWORD_DEFAULT), $displayName, $email ?: null]);
redirect('login.php?setup=1');
}
}
?>
<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>初始化後臺</title><link rel="stylesheet" href="assets/admin.css"></head>
<body class="auth-page"><main class="auth-card"><a class="auth-brand" href="../index.html" aria-label="返回水晶男孩推廣部前台"><img src="../assets/images/logov.svg" alt="SECHSKIES 水晶男孩推廣部"><span>CONTENT MANAGEMENT SYSTEM</span></a><p class="eyebrow">FIRST-TIME SETUP</p><h1>建立第一位管理者</h1><p>水晶男孩推廣部內容管理系統</p><?php if ($error): ?><div class="notice notice--error"><?= h($error) ?></div><?php endif; ?>
<form method="post"><input type="hidden" name="csrf" value="<?= h(csrf_token()) ?>"><label>登入帳號<input name="username" required autocomplete="username"></label><label>顯示名稱<input name="display_name" required></label><label>電子信箱<input name="email" type="email"></label><label>登入密碼<input name="password" type="password" required minlength="10" autocomplete="new-password"></label><button class="button button--primary" type="submit">建立管理者</button></form></main></body></html>
+611
View File
@@ -0,0 +1,611 @@
/* ==========================================
水晶男孩 2026 萬年曆專用樣式 (calendar.css)
對齊全站暖紙面 section 視覺
========================================== */
:root {
--cal-grid-gap: 8px;
--cal-border-soft: rgba(255, 204, 0, 0.18);
--cal-border-strong: rgba(255, 204, 0, 0.28);
--cal-panel-shadow: 0 14px 34px rgba(0, 0, 0, 0.2);
}
.calendar-page-container {
max-width: 1120px;
margin: 0 auto;
padding: 30px 20px 60px;
}
/* 控制面板 */
.calendar-controls {
background: var(--color-surface-soft);
border: 1px solid var(--cal-border-soft);
border-radius: 18px;
padding: 22px;
margin-bottom: 22px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 14px;
box-shadow: var(--cal-panel-shadow);
}
.calendar-select-group {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.calendar-select {
background: #fffef9;
color: var(--color-text-dark);
border: 1px solid rgba(255, 204, 0, 0.38);
border-radius: 999px;
padding: 9px 14px;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.calendar-select:hover,
.calendar-select:focus {
border-color: var(--color-brand);
box-shadow: 0 0 0 3px rgba(255, 204, 0, 0.2);
}
.calendar-btn {
background: var(--color-brand);
color: #1a1a1a;
border: 1px solid transparent;
border-radius: 999px;
padding: 8px 16px;
font-weight: 700;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
transition: transform 0.2s ease, background-color 0.2s ease;
}
.calendar-btn:hover {
background: #ffe066;
transform: translateY(-1px);
}
/* 分類標籤按鈕列 */
.calendar-filters {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
}
.filter-chip {
background: #fffdf6;
color: var(--color-text-dark);
border: 1px solid rgba(255, 204, 0, 0.38);
border-radius: 999px;
padding: 7px 15px;
font-size: 0.9rem;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
}
.filter-chip.active,
.filter-chip:hover {
background: var(--color-brand);
color: #1a1a1a;
border-color: var(--color-brand);
}
/* 萬年曆主要區塊:維持原雙欄版型 */
.calendar-layout {
display: grid;
grid-template-columns: 1fr 380px;
gap: 22px;
align-items: start;
}
@media (max-width: 900px) {
.calendar-layout {
grid-template-columns: 1fr;
}
}
/* 日曆卡片區 */
.calendar-card {
background: var(--color-surface);
border: 1px solid var(--cal-border-soft);
border-radius: 18px;
padding: 24px;
box-shadow: var(--cal-panel-shadow);
}
.calendar-header-title {
text-align: center;
font-size: 1.52rem;
color: var(--color-text-dark);
margin-bottom: 18px;
font-weight: 900;
letter-spacing: 0.04em;
}
/* 星期標頭 */
.calendar-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
text-align: center;
font-weight: 700;
color: var(--color-brand-dark);
padding-bottom: 11px;
border-bottom: 1px solid rgba(255, 204, 0, 0.22);
margin-bottom: 11px;
}
.calendar-weekday {
padding: 6px 0;
}
.calendar-weekday.weekend {
color: #9b5e47;
}
/* 日期網格 */
.calendar-days-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: var(--cal-grid-gap);
}
.day-cell {
aspect-ratio: 1;
min-height: 68px;
background: #fffefb;
border: 1px solid rgba(255, 204, 0, 0.12);
border-radius: 10px;
padding: 7px;
position: relative;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: all 0.18s ease;
}
.day-cell:hover {
background: var(--color-brand-light);
border-color: var(--cal-border-strong);
transform: translateY(-1px);
}
.day-cell.other-month {
opacity: 0.4;
cursor: default;
}
.day-cell.other-month:hover {
transform: none;
border-color: rgba(255, 204, 0, 0.12);
background: #fffefb;
}
.day-cell.today {
border: 2px solid var(--color-brand);
box-shadow: inset 0 0 0 1px rgba(255, 204, 0, 0.16);
}
.day-cell.selected {
background: #fff4c7;
border-color: var(--color-brand);
}
.day-num {
font-size: 0.94rem;
font-weight: 700;
color: var(--color-text-dark);
}
.day-cell.weekend .day-num {
color: #8a4e40;
}
/* 事件標記改為低調金色點 */
.day-events {
display: flex;
flex-wrap: wrap;
gap: 3px;
margin-top: 4px;
}
.day-birthday-link {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
margin-top: 2px;
text-decoration: none;
}
.event-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-brand);
box-shadow: none;
}
.event-dot.birthday,
.event-dot.debut,
.event-dot.concert {
background: var(--color-brand);
}
/* 生日頭像縮圖小標籤 */
.day-avatar-badge {
width: 38px;
height: 38px;
border-radius: 50%;
border: 2px solid var(--color-brand);
object-fit: cover;
margin: 0;
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.18);
}
.day-member-name {
font-size: 0.68rem;
font-weight: 700;
color: var(--color-text-dark);
line-height: 1;
letter-spacing: 0.01em;
}
/* 演唱會微縮標籤 */
.day-concert-badge {
width: 26px;
height: 26px;
border-radius: 4px;
border: 1px solid var(--color-brand);
object-fit: cover;
margin: 2px auto 0;
}
/* 當月紀念日清單 */
.events-sidebar {
background: var(--color-surface);
border: 1px solid var(--cal-border-soft);
border-radius: 18px;
padding: 24px;
box-shadow: var(--cal-panel-shadow);
}
.sidebar-title {
font-size: 1.25rem;
color: var(--color-text-dark);
margin-bottom: 18px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 204, 0, 0.2);
display: flex;
align-items: center;
justify-content: space-between;
}
.sidebar-title i {
color: var(--color-brand);
}
.events-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.events-empty {
color: var(--color-text-muted);
text-align: center;
padding: 28px 0;
}
/* 個別事件卡片 */
.event-card {
background: var(--color-surface-soft);
border: 1px solid rgba(255, 204, 0, 0.16);
border-left: 4px solid var(--color-brand);
border-radius: 10px;
padding: 14px 16px;
transition: transform 0.2s ease, border-color 0.2s ease;
}
.event-card:hover {
transform: translateX(2px);
border-color: rgba(255, 204, 0, 0.32);
}
.event-card.type-birthday,
.event-card.type-debut,
.event-card.type-concert {
border-left-color: var(--color-brand);
}
.event-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.event-date-badge {
font-size: 0.82rem;
font-weight: 700;
color: var(--color-brand-dark);
background: rgba(255, 204, 0, 0.2);
padding: 3px 9px;
border-radius: 999px;
}
.event-title {
font-size: 1.02rem;
font-weight: 800;
color: var(--color-text-dark);
margin-bottom: 4px;
}
.event-desc {
font-size: 0.9rem;
color: var(--color-text-muted);
line-height: 1.48;
}
.event-media-img {
width: 100%;
max-height: 150px;
object-fit: cover;
border-radius: 8px;
margin-top: 8px;
border: 1px solid rgba(255, 204, 0, 0.24);
}
/* 生日卡片呈現 */
.birthday-featured-card {
background: var(--color-surface-soft);
border: 1px solid rgba(255, 204, 0, 0.22);
border-radius: 12px;
padding: 14px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
margin-bottom: 8px;
text-align: center;
}
.birthday-photo-wrap {
width: 100%;
max-width: 280px;
aspect-ratio: 210 / 297;
border-radius: 12px;
border: 2px solid var(--color-brand);
overflow: hidden;
flex-shrink: 0;
background: #111;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.16);
}
.birthday-photo-wrap img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 28%;
}
.birthday-info {
width: 100%;
}
.birthday-info h4 {
font-size: 1.3rem;
color: var(--color-text-dark);
margin: 0 0 2px;
line-height: 1.15;
}
.birthday-info h4 a {
color: inherit;
text-decoration: none;
}
.birthday-info .en-name {
font-size: 0.82rem;
color: var(--color-brand-dark);
letter-spacing: 0.05em;
margin-bottom: 7px;
}
.birthday-info .bday-date {
font-size: 0.92rem;
color: var(--color-text-muted);
font-weight: 700;
}
/* 演唱會卡片呈現 */
.concert-featured-card {
background: var(--color-surface-soft);
border: 1px solid rgba(255, 204, 0, 0.22);
border-radius: 12px;
padding: 14px;
margin-bottom: 8px;
}
.concert-featured-card img {
width: 100%;
max-width: 280px;
aspect-ratio: 210 / 297;
height: auto;
object-fit: cover;
object-position: center 28%;
display: block;
margin-left: auto;
margin-right: auto;
border-radius: 8px;
margin-bottom: 10px;
border: 1px solid rgba(255, 204, 0, 0.24);
}
/* Modal 彈窗 */
.calendar-modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.calendar-modal-backdrop.active {
opacity: 1;
pointer-events: auto;
}
.calendar-modal-content {
background: var(--color-surface);
border: 1px solid var(--cal-border-strong);
border-radius: 16px;
padding: 24px;
max-width: 480px;
width: 90%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
position: relative;
text-align: center;
}
.modal-close-btn {
position: absolute;
top: 14px;
right: 18px;
background: none;
border: none;
color: #7d7464;
font-size: 1.5rem;
cursor: pointer;
}
.modal-close-btn:hover {
color: var(--color-text-dark);
}
.modal-photo {
width: 120px;
height: 120px;
border-radius: 50%;
border: 3px solid var(--color-brand);
object-fit: cover;
margin: 0 auto 16px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
.modal-date-title {
color: var(--color-brand-dark);
margin-bottom: 14px;
}
.modal-member-name {
color: var(--color-text-dark);
font-size: 1.32rem;
margin-bottom: 4px;
}
.modal-member-name a {
color: inherit;
text-decoration: none;
}
.modal-member-meta {
color: var(--color-brand-dark);
font-size: 0.9rem;
margin-bottom: 10px;
}
.modal-desc {
color: var(--color-text-muted);
font-size: 0.92rem;
line-height: 1.5;
margin-bottom: 12px;
}
.modal-member-link {
text-decoration: none;
display: inline-block;
font-size: 0.9rem;
}
.modal-event-box {
background: rgba(255, 204, 0, 0.1);
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
text-align: left;
}
.modal-event-title {
color: var(--color-brand-dark);
margin-bottom: 4px;
}
.modal-concert-photo {
width: 100%;
max-width: 320px;
aspect-ratio: 210 / 297;
max-height: none;
object-fit: cover;
object-position: center 28%;
display: block;
margin-left: auto;
margin-right: auto;
border-radius: 8px;
border: 1px solid rgba(255, 204, 0, 0.28);
margin-bottom: 14px;
}
@media (max-width: 900px) {
.calendar-page-container {
padding: 24px 14px 50px;
}
.calendar-controls,
.calendar-card,
.events-sidebar {
padding: 16px;
}
.calendar-header-title {
font-size: 1.33rem;
}
.day-cell {
min-height: 56px;
}
.day-avatar-badge {
width: 30px;
height: 30px;
}
.day-member-name {
font-size: 0.62rem;
}
.birthday-photo-wrap {
max-width: 100%;
}
}
+101
View File
@@ -0,0 +1,101 @@
/* 隊長的氣場 — 殷志源台北見面會存檔頁(2018-07-21 */
body.captain-aura-page {
background-color: var(--color-bg);
background-image: url('../images/concert/captain-aura/bg-digital-placeholder.svg');
background-repeat: repeat;
}
body.captain-aura-page .captain-aura-hero {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
box-sizing: border-box;
}
body.captain-aura-page .captain-aura-hero img {
display: block;
width: 100%;
height: auto;
border-radius: 4px;
}
body.captain-aura-page .captain-aura-main {
max-width: 893px;
margin: 0 auto;
padding: 50px 20px 72px;
box-sizing: border-box;
}
body.captain-aura-page .captain-aura-act {
margin-bottom: 2.5rem;
}
body.captain-aura-page .captain-aura-act:last-child {
margin-bottom: 0;
}
body.captain-aura-page .captain-aura-act__title {
margin: 0 0 1rem;
text-align: center;
color: var(--color-brand);
font-size: 1.65rem;
font-weight: 700;
line-height: 1.35;
text-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
body.captain-aura-page .captain-aura-act__title--plain {
color: var(--color-text-inverse);
text-shadow: none;
font-size: 1.35rem;
margin-bottom: 0.65rem;
}
body.captain-aura-page .captain-aura-act__title--plain + .captain-aura-act__title--plain {
margin-top: 0;
}
body.captain-aura-page .captain-aura-divider {
margin: 2.75rem 0;
text-align: center;
color: var(--color-brand);
font-size: 1.1rem;
font-weight: 700;
letter-spacing: 0.2em;
}
body.captain-aura-page .captain-aura-videos {
display: flex;
flex-direction: column;
gap: 1rem;
}
body.captain-aura-page .captain-aura-video {
position: relative;
width: 100%;
padding-bottom: 56.25%;
background: #000;
border-radius: 4px;
overflow: hidden;
}
body.captain-aura-page .captain-aura-video iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
}
@media (max-width: 640px) {
body.captain-aura-page .captain-aura-main {
padding-top: 32px;
padding-bottom: 48px;
}
body.captain-aura-page .captain-aura-act__title {
font-size: 1.35rem;
}
}
+216
View File
@@ -2256,6 +2256,45 @@ header.inner-page-header .sub-title {
padding: 4px 20px;
}
/* 地區切換(台灣/海外):比成員 tab 高一階,用深底色跟它區隔 */
.region-tabs {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 10px;
margin: 18px auto 0;
max-width: 1100px;
padding: 4px 20px;
}
.region-tab-btn {
border: 1px solid var(--color-bg-raised);
background: var(--color-bg-raised);
color: #f4f4f4;
border-radius: 999px;
padding: 8px 20px;
font-size: 0.9rem;
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer;
transition: all 0.2s ease;
}
.region-tab-btn:hover,
.region-tab-btn.active {
background: var(--color-brand);
color: #2f2f2f;
border-color: var(--color-brand);
}
.region-empty-state {
text-align: center;
color: #8b7423;
font-size: 0.95rem;
padding: 40px 20px;
}
#itinerary-search {
display: block;
max-width: 1100px;
@@ -2328,6 +2367,16 @@ header.inner-page-header .sub-title {
gap: 16px;
}
.map-embed-empty {
text-align: center;
color: #8b7423;
font-size: 0.88rem;
padding: 24px 16px;
background: #fffef8;
border: 1px dashed #e4dbb8;
border-radius: 10px;
}
.map-embed-label {
margin: 0 0 8px;
font-size: 0.88rem;
@@ -2450,6 +2499,7 @@ header.inner-page-header .sub-title {
.photo-frame {
width: 118px;
flex: 0 0 118px;
position: relative;
}
.photo-frame img {
@@ -2459,6 +2509,21 @@ header.inner-page-header .sub-title {
border-radius: 8px;
}
/* 圖片授權標示:僅供合規(CC BY / CC BY-SA 需姓名標示),刻意做得很小、低對比,不搶視覺 */
.photo-credit {
position: absolute;
right: 4px;
bottom: 3px;
font-size: 0.5rem;
line-height: 1;
color: rgba(255, 255, 255, 0.7);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.85);
letter-spacing: 0.01em;
pointer-events: none;
user-select: none;
white-space: nowrap;
}
.card-content .date {
display: inline-block;
font-size: 0.72rem;
@@ -2495,6 +2560,10 @@ header.inner-page-header .sub-title {
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.06);
}
.detail-photo {
position: relative;
}
.detail-photo img {
width: 100%;
max-height: 300px;
@@ -2502,6 +2571,12 @@ header.inner-page-header .sub-title {
display: block;
}
.detail-photo .photo-credit {
font-size: 0.62rem;
right: 8px;
bottom: 6px;
}
.detail-content {
padding: 16px;
}
@@ -3480,6 +3555,11 @@ body.site-guide-page .site-guide-legend__off {
font-weight: 700;
}
body.site-guide-page .site-guide-legend__extra {
color: #8a8a8a;
font-weight: 700;
}
body.site-guide-page .site-guide-nav-table-wrap {
width: 100%;
overflow-x: auto;
@@ -3590,6 +3670,142 @@ body.site-guide-page .site-guide-mini-table a {
text-decoration: none;
}
/* 主選單樹:心智圖巢狀清單(取代原表格),主選單→次選單→子節點以連接線呈現 */
body.site-guide-page .site-guide-nav-caption--standalone {
text-align: center;
padding: 0 0.5rem 0.65rem;
font-size: 0.8rem;
color: #777;
line-height: 1.4;
}
body.site-guide-page .site-guide-mindmap,
body.site-guide-page .site-guide-mindmap ul {
list-style: none;
margin: 0;
padding: 0;
}
body.site-guide-page .site-guide-mindmap {
padding: 0.5rem 0.75rem 0.5rem 0.25rem;
min-width: 20rem;
}
body.site-guide-page .site-guide-mindmap ul {
margin-left: 1.5rem;
}
body.site-guide-page .site-guide-mindmap li {
position: relative;
padding: 0.32rem 0 0.32rem 1.35rem;
}
body.site-guide-page .site-guide-mindmap li::before,
body.site-guide-page .site-guide-mindmap li::after {
content: "";
position: absolute;
left: 0;
}
/* 節點到垂直線的橫向短連接線 */
body.site-guide-page .site-guide-mindmap li::before {
top: 1.1rem;
width: 1.15rem;
height: 0;
border-top: 2px solid rgba(150, 115, 20, 0.4);
}
/* 同層兄弟節點間的垂直連接線 */
body.site-guide-page .site-guide-mindmap li::after {
top: 0;
bottom: 0;
width: 0;
border-left: 2px solid rgba(150, 115, 20, 0.4);
}
/* 每層最後一個節點:垂直線只畫到分支點,不繼續往下延伸 */
body.site-guide-page .site-guide-mindmap li:last-child::after {
height: 1.1rem;
bottom: auto;
}
body.site-guide-page .sg-node {
display: inline-flex;
align-items: center;
padding: 0.3em 0.85em;
border-radius: 999px;
font-size: 0.88rem;
line-height: 1.3;
background: #fff;
border: 1px solid rgba(255, 204, 0, 0.45);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
body.site-guide-page .sg-node a {
color: #5c4010;
font-weight: 700;
text-decoration: none;
}
body.site-guide-page .sg-node a:hover {
text-decoration: underline;
}
body.site-guide-page .sg-node--nolink {
border-style: dashed;
border-color: rgba(0, 0, 0, 0.2);
background: rgba(0, 0, 0, 0.03);
color: #959595;
font-weight: 600;
}
/* 不在頂欄的延伸內容(頁內錨點/草稿/備查):灰色虛線,與頂欄真實連結區隔 */
body.site-guide-page .sg-node--extra {
border-style: dashed;
border-color: rgba(0, 0, 0, 0.22);
background: rgba(0, 0, 0, 0.02);
}
body.site-guide-page .sg-node--extra a {
color: #6e6e6e;
}
body.site-guide-page .sg-node--extra code {
font-size: 0.85em;
color: #7a6a30;
}
body.site-guide-page .sg-node--extra.sg-node--nolink {
color: #9a9a9a;
font-style: italic;
}
body.site-guide-page .site-guide-mindmap li[data-site-guide-depth="1"] > .sg-node {
font-weight: 800;
font-size: 0.98rem;
background: rgba(255, 220, 150, 0.55);
border-color: rgba(255, 204, 0, 0.6);
}
body.site-guide-page .site-guide-mindmap li[data-site-guide-depth="2"] > .sg-node {
background: rgba(255, 238, 200, 0.55);
}
body.site-guide-page .site-guide-mindmap li[data-site-guide-depth="3"] > .sg-node {
background: #fffaf2;
}
body.site-guide-page .site-guide-mindmap li[data-site-guide-depth="4"] > .sg-node {
background: #f5f8ff;
font-size: 0.82rem;
}
/* 延伸內容維持灰色虛線,不隨深度套用金黃底色(放在深度規則之後以蓋過同層底色) */
body.site-guide-page .site-guide-mindmap li[data-site-guide-depth] > .sg-node--extra {
background: rgba(0, 0, 0, 0.02);
border-color: rgba(0, 0, 0, 0.22);
}
body.site-guide-page .site-guide-nav-nolink {
color: #959595;
font-weight: 600;
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" role="img" aria-hidden="true">
<rect width="64" height="64" fill="#0d0d0d"/>
<path d="M0 32h64M32 0v64" stroke="#1f1f1f" stroke-width="1"/>
<circle cx="32" cy="32" r="1.5" fill="#2a2a2a"/>
</svg>

After

Width:  |  Height:  |  Size: 285 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1003" viewBox="0 0 1920 1003" role="img" aria-label="kEG sli01.png ">
<defs>
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1a1a1a"/>
<stop offset="50%" stop-color="#2a2410"/>
<stop offset="100%" stop-color="#0a0a0a"/>
</linearGradient>
</defs>
<rect width="1920" height="1003" fill="url(#g)"/>
<rect x="80" y="80" width="1760" height="843" fill="none" stroke="#ffcc00" stroke-width="4" stroke-dasharray="16 12" opacity="0.45"/>
<text x="960" y="470" text-anchor="middle" fill="#ffcc00" font-family="sans-serif" font-size="42" font-weight="700">G1 private 1theland in Taipei</text>
<text x="960" y="540" text-anchor="middle" fill="#f4f4f4" font-family="sans-serif" font-size="28" opacity="0.85">kEG hero.jpg sli01.png </text>
</svg>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+179 -79
View File
@@ -56,55 +56,64 @@
});
}
const tabs = Array.from(document.querySelectorAll(".tab-btn"));
const cards = Array.from(document.querySelectorAll(".polaroid-card"));
const searchInput = document.getElementById("itinerary-search");
const itineraryList = document.querySelector(".itinerary-list");
const detailPanel = document.getElementById("detail-panel");
const detailDate = document.getElementById("detail-date");
const detailTitle = document.getElementById("detail-title");
const detailDesc = document.getElementById("detail-desc");
const detailQuote = document.getElementById("detail-quote");
const detailTag = document.getElementById("detail-tag");
const detailLink = document.getElementById("detail-link");
const detailImage = document.getElementById("detail-image");
const memberMapFrame = document.getElementById("member-map");
const mapEmbedSingle = document.getElementById("map-embed-single");
const mapEmbedAll = document.getElementById("map-embed-all");
const openMapLink = document.getElementById("open-map-link");
const openMapLinksDual = document.getElementById("open-map-links-dual");
const openMapLinkKo = document.getElementById("open-map-link-ko");
const openMapLinkEun = document.getElementById("open-map-link-eun");
/**
* 每個 .region-panel例如 #taipei#global都是獨立的地區區塊各自的
* member-tabs地圖地點卡片互不影響新增行程時只要在對應 .region-panel
* 裡加一顆 .tab-btn data-trip-name / data-map-embed / data-map-open
* 沒有地圖就不要寫 data-map-embed以及對應的 .polaroid-card不需要動這支
* 檔案全部行程的地圖會依當時有設定地圖的 tab 自動組出來完全沒有
* 設定地圖的行程地圖區塊會自動收起來只留卡片列表
*/
function initRegionController(panel) {
const tabs = Array.from(panel.querySelectorAll(":scope > .member-tabs .tab-btn"));
const cards = Array.from(panel.querySelectorAll(".polaroid-card"));
const itineraryList = panel.querySelector(".itinerary-list");
const detailPanel = panel.querySelector(".detail-panel");
const detailImage = panel.querySelector(".detail-photo img");
const detailDate = panel.querySelector(".detail-content .date");
const detailTitle = panel.querySelector(".detail-content h3");
const detailDesc = panel.querySelector(".detail-content p");
const detailQuote = panel.querySelector(".detail-content blockquote");
const detailTag = panel.querySelector(".detail-content .tag");
const detailLink = panel.querySelector(".detail-content .detail-link");
const mapSticky = panel.querySelector(".map-sticky");
const mapEmbedSingle = mapSticky ? mapSticky.querySelector(".map-embed-single") : null;
const memberMapFrame = mapEmbedSingle ? mapEmbedSingle.querySelector(".map-my-maps-frame") : null;
const mapEmbedEmpty = mapSticky ? mapSticky.querySelector(".map-embed-empty") : null;
const mapEmbedAll = mapSticky ? mapSticky.querySelector(".map-embed-all") : null;
const openMapLink = mapSticky ? mapSticky.querySelector(".map-link") : null;
const openMapLinksDual = mapSticky ? mapSticky.querySelector(".map-links-dual") : null;
const dayGroups = new Map();
let currentMember = "eun";
let currentKeyword = "";
const memberMaps = {
eun: {
embed: "https://www.google.com/maps/d/embed?mid=1VGZ76SPPjm3NWCBQe-hkm08oe5WPRJI&ehbc=2E312F",
open: "https://www.google.com/maps/d/u/0/viewer?mid=1VGZ76SPPjm3NWCBQe-hkm08oe5WPRJI"
},
ko: {
embed: "https://www.google.com/maps/d/embed?mid=1xpAsSoO8gNmMiW4x2sfkndISFTtjMQDw&hl=zh-TW&ehbc=2E312F",
open: "https://www.google.com/maps/d/u/0/viewer?mid=1xpAsSoO8gNmMiW4x2sfkndISFTtjMQDw"
}
};
const memberTripPrefix = {
ko: "高高父子遊台灣",
eun: "殷空少搭飛機去台灣高雄"
};
const memberSortOrder = { eun: 0, ko: 1 };
/* 舊資料裡卡片用 data-member="kogo"tab 用 data-member="ko";新地區請直接讓兩邊字串一致,不需要這個對照 */
function normalizeMember(member) {
if (member === "kogo") return "ko";
return member;
}
const tripConfig = {};
tabs.forEach((tab) => {
const member = normalizeMember(tab.dataset.member || "");
if (!member || member === "all") return;
tripConfig[member] = {
tripName: tab.dataset.tripName || tab.textContent.trim(),
memberLabel: tab.dataset.memberLabel || tab.textContent.trim(),
sortOrder: Number.isFinite(Number(tab.dataset.sortOrder)) ? Number(tab.dataset.sortOrder) : 99,
mapEmbed: tab.dataset.mapEmbed || "",
mapOpen: tab.dataset.mapOpen || "",
};
});
const initialTab = tabs.find((tab) => tab.classList.contains("active")) || tabs[0];
let currentMember = initialTab ? normalizeMember(initialTab.dataset.member || "all") : "all";
let currentKeyword = "";
function getMemberLabel(member) {
const normalized = normalizeMember(member);
if (normalized === "eun") return "殷志源";
if (normalized === "ko") return "高志溶";
return "未分類";
return (tripConfig[member] && tripConfig[member].memberLabel) || "未分類";
}
function getDayToken(rawDate) {
@@ -115,36 +124,92 @@
function getGroupLabel(card) {
const date = getDayToken(card.dataset.date || "未分類");
const normalized = normalizeMember(card.dataset.member || "");
const prefix = memberTripPrefix[normalized] || getMemberLabel(normalized);
const member = normalizeMember(card.dataset.member || "");
const prefix = (tripConfig[member] && tripConfig[member].tripName) || getMemberLabel(member);
return prefix + "_" + date;
}
function setDualMapVisible(isAll) {
if (mapEmbedSingle && mapEmbedAll) {
mapEmbedSingle.hidden = isAll ? true : false;
mapEmbedAll.hidden = isAll ? false : true;
mapEmbedAll.setAttribute("aria-hidden", isAll ? "false" : "true");
function tripsWithMaps() {
return Object.keys(tripConfig)
.filter((member) => tripConfig[member].mapEmbed)
.sort((a, b) => tripConfig[a].sortOrder - tripConfig[b].sortOrder);
}
if (openMapLink && openMapLinksDual) {
openMapLink.hidden = isAll ? true : false;
openMapLinksDual.hidden = isAll ? false : true;
function buildMapPairs(members) {
if (!mapEmbedAll) return;
mapEmbedAll.innerHTML = "";
members.forEach((member) => {
const trip = tripConfig[member];
const pair = document.createElement("div");
pair.className = "map-embed-pair";
const label = document.createElement("p");
label.className = "map-embed-label";
label.textContent = trip.tripName;
const iframe = document.createElement("iframe");
iframe.className = "map-my-maps-frame";
iframe.loading = "lazy";
iframe.referrerPolicy = "no-referrer-when-downgrade";
iframe.title = trip.tripName + " 踩點地圖";
iframe.src = trip.mapEmbed;
pair.appendChild(label);
pair.appendChild(iframe);
mapEmbedAll.appendChild(pair);
});
}
function buildOpenLinksDual(members) {
if (!openMapLinksDual) return;
openMapLinksDual.innerHTML = "";
members.forEach((member) => {
const trip = tripConfig[member];
if (!trip.mapOpen) return;
const a = document.createElement("a");
a.className = "map-link";
a.target = "_blank";
a.href = trip.mapOpen;
a.textContent = "開啟「" + trip.tripName + "」My Maps";
openMapLinksDual.appendChild(a);
});
}
function updateMapByMember(member) {
const normalized = normalizeMember(member);
if (normalized === "all") {
setDualMapVisible(true);
if (openMapLinkKo) openMapLinkKo.href = memberMaps.ko.open;
if (openMapLinkEun) openMapLinkEun.href = memberMaps.eun.open;
if (!mapSticky) return;
if (!Object.keys(tripConfig).length) {
mapSticky.hidden = true;
return;
}
setDualMapVisible(false);
const mapConfig = memberMaps[normalized];
if (!mapConfig || !memberMapFrame) return;
memberMapFrame.src = mapConfig.embed;
if (openMapLink) openMapLink.href = mapConfig.open;
mapSticky.hidden = false;
if (member === "all") {
const withMaps = tripsWithMaps();
if (mapEmbedSingle) mapEmbedSingle.hidden = true;
if (openMapLink) openMapLink.hidden = true;
if (mapEmbedAll) {
buildMapPairs(withMaps);
mapEmbedAll.hidden = withMaps.length === 0;
}
if (openMapLinksDual) {
buildOpenLinksDual(withMaps);
openMapLinksDual.hidden = withMaps.length === 0;
}
if (mapEmbedEmpty) mapEmbedEmpty.hidden = withMaps.length > 0;
return;
}
const trip = tripConfig[member];
const hasMap = !!(trip && trip.mapEmbed);
if (mapEmbedAll) mapEmbedAll.hidden = true;
if (openMapLinksDual) openMapLinksDual.hidden = true;
if (mapEmbedSingle) mapEmbedSingle.hidden = !hasMap;
if (memberMapFrame && hasMap) memberMapFrame.src = trip.mapEmbed;
if (openMapLink) {
openMapLink.hidden = !hasMap;
if (hasMap) openMapLink.href = trip.mapOpen || "#";
}
if (mapEmbedEmpty) mapEmbedEmpty.hidden = hasMap;
}
function getCardSearchText(card) {
@@ -185,11 +250,13 @@
}
function initializeDayGroups() {
if (!itineraryList || !cards.length) return;
const sortedCards = [...cards].sort((a, b) => {
const memberA = normalizeMember(a.dataset.member || "");
const memberB = normalizeMember(b.dataset.member || "");
const memberDiff = (memberSortOrder[memberA] ?? 99) - (memberSortOrder[memberB] ?? 99);
if (memberDiff !== 0) return memberDiff;
const sortA = (tripConfig[memberA] && tripConfig[memberA].sortOrder) ?? 99;
const sortB = (tripConfig[memberB] && tripConfig[memberB].sortOrder) ?? 99;
if (sortA !== sortB) return sortA - sortB;
const dayA = Number((getDayToken(a.dataset.date).match(/\d+/) || [999])[0]);
const dayB = Number((getDayToken(b.dataset.date).match(/\d+/) || [999])[0]);
@@ -226,18 +293,21 @@
if (expandDay) {
ensureExpandedForCard(card);
}
const mapKey = currentMember === "all" ? "all" : currentMember;
updateMapByMember(mapKey);
updateMapByMember(currentMember === "all" ? "all" : currentMember);
detailDate.textContent = card.dataset.date || "";
detailTitle.textContent = card.dataset.title || "";
detailDesc.textContent = card.dataset.desc || "";
detailQuote.textContent = "「" + (card.dataset.quote || "") + "」";
detailTag.textContent = card.dataset.tag || "";
detailLink.href = card.dataset.link || "#";
detailImage.src = card.querySelector("img").src;
if (!detailPanel) return;
if (detailDate) detailDate.textContent = card.dataset.date || "";
if (detailTitle) detailTitle.textContent = card.dataset.title || "";
if (detailDesc) detailDesc.textContent = card.dataset.desc || "";
if (detailQuote) detailQuote.textContent = "「" + (card.dataset.quote || "") + "」";
if (detailTag) detailTag.textContent = card.dataset.tag || "";
if (detailLink) detailLink.href = card.dataset.link || "#";
const cardImg = card.querySelector("img");
if (detailImage && cardImg) {
detailImage.src = cardImg.src;
detailImage.alt = "節目截圖 - " + (card.dataset.title || "行程地點");
}
}
function applyFilter() {
let firstVisible = null;
@@ -272,13 +342,7 @@
});
});
if (searchInput) {
searchInput.addEventListener("input", (event) => {
currentKeyword = event.target.value.trim().toLowerCase();
applyFilter();
});
}
if (itineraryList) {
itineraryList.addEventListener("click", (event) => {
const toggle = event.target.closest(".day-toggle");
if (!toggle) return;
@@ -289,11 +353,12 @@
const isExpanded = dayGroup.toggle.getAttribute("aria-expanded") === "true";
setDayExpanded(dayGroup, !isExpanded);
});
}
cards.forEach((card) => {
card.addEventListener("click", () => {
setActiveCard(card, { expandDay: true });
if (window.innerWidth < 920) {
if (window.innerWidth < 920 && detailPanel) {
detailPanel.scrollIntoView({ behavior: "smooth", block: "start" });
}
});
@@ -301,4 +366,39 @@
initializeDayGroups();
applyFilter();
return {
setKeyword(keyword) {
currentKeyword = keyword;
applyFilter();
},
};
}
const regionPanels = Array.from(document.querySelectorAll(".region-panel"));
const controllers = regionPanels.map((panel) => initRegionController(panel));
if (searchInput) {
searchInput.addEventListener("input", (event) => {
const keyword = event.target.value.trim().toLowerCase();
controllers.forEach((controller) => controller.setKeyword(keyword));
});
}
const regionTabs = Array.from(document.querySelectorAll("[data-region-tabs] .region-tab-btn"));
if (regionTabs.length && regionPanels.length) {
regionTabs.forEach((tab) => {
tab.addEventListener("click", () => {
const region = tab.dataset.region;
regionPanels.forEach((panel) => {
panel.hidden = panel.dataset.regionPanel !== region;
});
regionTabs.forEach((item) => {
const isActive = item === tab;
item.classList.toggle("active", isActive);
item.setAttribute("aria-pressed", isActive ? "true" : "false");
});
});
});
}
})();
+460
View File
@@ -0,0 +1,460 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>水晶萬年曆 2026 — SECHSKIES SPECIAL CALENDAR</title>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="assets/css/calendar.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="assets/css/tablet.css">
</head>
<body class="calendar-page">
<header class="inner-page-header">
<div class="header-inner site-header-inner">
<h1 class="main-title">水晶萬年曆</h1>
<div class="subtitle-wrap">
<span class="sub-title">2026 SECHSKIES SPECIAL CALENDAR</span>
</div>
</div>
</header>
<nav class="portal-nav" aria-label="水晶男孩推廣部導覽" data-portal-nav></nav>
<canvas id="particles"></canvas>
<main class="calendar-page-container">
<!-- 上方控制控制面板 -->
<section class="calendar-controls" aria-label="萬年曆控制選項">
<div class="calendar-select-group">
<button type="button" class="calendar-btn" id="prev-month-btn">
<i class="fa-solid fa-chevron-left" aria-hidden="true"></i> 上個月
</button>
<select id="year-select" class="calendar-select" aria-label="選擇年份">
<option value="2025">2025 年</option>
<option value="2026" selected>2026 年</option>
<option value="2027">2027 年</option>
</select>
<select id="month-select" class="calendar-select" aria-label="選擇月份">
<option value="1">1 月</option>
<option value="2">2 月</option>
<option value="3">3 月</option>
<option value="4">4 月</option>
<option value="5">5 月</option>
<option value="6">6 月</option>
<option value="7">7 月</option>
<option value="8">8 月</option>
<option value="9">9 月</option>
<option value="10">10 月</option>
<option value="11">11 月</option>
<option value="12">12 月</option>
</select>
<button type="button" class="calendar-btn" id="next-month-btn">
下個月 <i class="fa-solid fa-chevron-right" aria-hidden="true"></i>
</button>
</div>
<!-- 分類篩選標籤 -->
<div class="calendar-filters" role="group" aria-label="紀念日分類篩選">
<button type="button" class="filter-chip active" data-category="all">全部紀念日</button>
<button type="button" class="filter-chip" data-category="birthday">成員生日</button>
<button type="button" class="filter-chip" data-category="debut">出道 / 重逢</button>
<button type="button" class="filter-chip" data-category="concert">演唱會 / 見面會</button>
</div>
</section>
<!-- 萬年曆主要版面 (雙欄結構) -->
<div class="calendar-layout">
<!-- 左欄:動態日曆網格卡片 -->
<section class="calendar-card">
<h2 class="calendar-header-title" id="calendar-month-year-display">2026 年 8 月</h2>
<div class="calendar-weekdays">
<div class="calendar-weekday weekend"></div>
<div class="calendar-weekday"></div>
<div class="calendar-weekday"></div>
<div class="calendar-weekday"></div>
<div class="calendar-weekday"></div>
<div class="calendar-weekday"></div>
<div class="calendar-weekday weekend"></div>
</div>
<div class="calendar-days-grid" id="calendar-days-grid">
<!-- JS 動態生成日期網格 -->
</div>
</section>
<!-- 右欄:當月水晶紀念日明細清單 -->
<aside class="events-sidebar">
<h3 class="sidebar-title">
<span id="sidebar-month-text">8 月專屬紀念日</span>
<i class="fa-solid fa-star" aria-hidden="true"></i>
</h3>
<div class="events-list" id="events-list">
<!-- JS 動態生成事件列表與成員生日/演唱會照片卡片 -->
</div>
</aside>
</div>
</main>
<!-- 點擊日期的 Modal 彈窗 -->
<div class="calendar-modal-backdrop" id="event-modal">
<div class="calendar-modal-content">
<button type="button" class="modal-close-btn" id="modal-close" aria-label="關閉視窗">&times;</button>
<div id="modal-body-content">
<!-- 動態注入內容 -->
</div>
</div>
</div>
<footer>
<p>SECHSKIES Fanpage &copy; 2026 — 2026 水晶男孩專屬萬年曆</p>
<a href="https://jekkinoopy.github.io/Portfolio/" class="studio-link" target="_blank" rel="noopener noreferrer">
Design by <span class="mark_b">Jekkinoopy Studio</span>
</a>
</footer>
<!-- 全站 JavaScript 資源 -->
<script src="assets/js/portal-nav.js"></script>
<script src="assets/js/particles.js"></script>
<!-- 萬年曆核心邏輯 -->
<script>
// 水晶男孩專屬紀念日數據集 (含成員介紹頁連結錨點)
const sechskiesEvents = [
// 1月
{
month: 1, day: 21, endDay: 22, type: 'concert', title: 'Yellow Note Final 首爾演唱會 🎤',
photo: 'assets/images/concert/2017.jpg', desc: '2017 年首爾終場演唱會紀念'
},
// 2月
{
month: 2, day: 22, type: 'birthday', title: '姜成勳 生日 🎂', memberName: '姜成勳', enName: 'KANG SUNG HOON', role: 'Main Vocal',
photo: 'assets/images/member/kpoplover082006___Bsc2U5lhXuK___.jpg', desc: '1980.02.22 主唱姜成勳生日', memberUrl: 'member/member.html#poster-ksh'
},
// 3月
{
month: 3, day: 6, endDay: 8, type: 'concert', title: 'ACCESS 演唱會紀念 🎤',
photo: 'assets/images/concert/2020.jpg', desc: '2020 SECHSKIES Concert [ACCESS]'
},
{ month: 3, day: 23, type: 'debut', title: 'J-Walk 組合出道日 🌟', desc: '2002.03.23 金在德與張水院發行首張專輯《Suddenly》' },
// 4月
{
month: 4, day: 14, type: 'concert', title: '六六歌2 上岩黃海重逢日 🌟',
photo: 'assets/images/totoga2/toto1.png', desc: '2016.04.14 無限挑戰六六歌2 上岩游擊演唱會16年後重逢'
},
{ month: 4, day: 15, type: 'debut', title: 'SECHSKIES 團體出道紀念日 🌟', desc: '1997.04.15 水晶男孩正式出道,傳奇盛世起點!' },
// 5月
{ month: 5, day: 18, type: 'debut', title: '水晶男孩告別宣佈日', desc: '2000.05.18 告別紀念' },
{ month: 5, day: 24, type: 'debut', title: '姜成勳 Solo 出道日 🌟', desc: '2001.05.24 首張專輯《In Connection》' },
// 6月
{
month: 6, day: 8, type: 'birthday', title: '殷志源 生日 🎂', memberName: '殷志源', enName: 'EUN JI WON', role: 'Leader · Rap · Dance',
photo: 'assets/images/Eun Ji Won.jpeg', desc: '1978.06.08 水晶男孩隊長殷志源生日', memberUrl: 'member/member.html#poster-ejw'
},
// 7月
{
month: 7, day: 1, type: 'birthday', title: '高志溶 生日 🎂', memberName: '高志溶', enName: 'KO JI YONG', role: 'Vocal',
photo: 'assets/images/member/92599-502183.jpg', desc: '1980.07.01 高志溶生日', memberUrl: 'member/member.html#poster-kjy'
},
{
month: 7, day: 13, type: 'birthday', title: '李宰鎮 生日 🎂', memberName: '李宰鎮', enName: 'LEE JAE JIN', role: 'Rap · Dance',
photo: 'assets/images/member/1595330620293.jpg', desc: '1979.07.13 李宰鎮生日', memberUrl: 'member/member.html#poster-ljj'
},
{
month: 7, day: 16, type: 'birthday', title: '張水院 生日 🎂', memberName: '張水院', enName: 'JANG SU WON', role: 'Vocal',
photo: 'assets/images/member/seven7lai_1___BuRBxcPAIsP___.jpg', desc: '1980.07.16 張水院生日', memberUrl: 'member/member.html#poster-jsw'
},
// 8月
{
month: 8, day: 7, type: 'birthday', title: '金在德 生日 🎂', memberName: '金在德', enName: 'KIM JAE DUC', role: 'Rap · Dance',
photo: 'assets/images/member/4c0e013b341e85622979475bc230719b.jpg', desc: '1979.08.07 天使笑顏與舞蹈機器金在德生日', memberUrl: 'member/member.html#poster-kjd'
},
// 9月
{
month: 9, day: 10, endDay: 11, type: 'concert', title: 'Yellow Note 演唱會 (二安日) 🎤',
photo: 'assets/images/concert/2016.jpg', desc: '2016 黃海 6 分鐘喊聲喚回大爺們重返舞台'
},
{
month: 9, day: 23, type: 'concert', title: '20 週年高尺巨蛋演唱會 🎤',
photo: 'assets/images/concert/2017.jpg', desc: '2017 20th Anniversary Concert 高尺天空巨蛋'
},
// 10月
{
month: 10, day: 1, type: 'concert', title: 'BOF 釜山演唱會 🎤',
photo: 'assets/images/mission-balloon-sea.png', desc: '2016 60台遊覽車 6000人黃海盛況「黑幫黃」傳奇'
},
{ month: 10, day: 7, type: 'debut', title: '《三個詞 Three Words》發行日 🌟', desc: '2016.10.07 回歸單曲音源發行日' },
{
month: 10, day: 13, endDay: 14, type: 'concert', title: 'YELLOFANTASY 22週年演唱會 🎤',
photo: 'assets/images/concert/2018.jpg', desc: '2018 SECHSKIES 2018 CONCERT YELLOFANTASY'
},
{ month: 10, day: 27, type: 'debut', title: '殷志源 Solo 出道日 🌟', desc: '2000.10.27 單曲《A-Ha》/《G Pop》' },
// 11月
{ month: 11, day: 19, type: 'debut', title: '李宰鎮 Solo 出道日 🌟', desc: '2001.11.19 首張專輯《S.Double.J》' }
];
// 狀態變數
let currentYear = 2026;
let currentMonth = 8; // 預設 8月
let activeCategory = 'all';
// DOM 元素
const yearSelect = document.getElementById('year-select');
const monthSelect = document.getElementById('month-select');
const prevBtn = document.getElementById('prev-month-btn');
const nextBtn = document.getElementById('next-month-btn');
const daysGrid = document.getElementById('calendar-days-grid');
const monthYearDisplay = document.getElementById('calendar-month-year-display');
const eventsList = document.getElementById('events-list');
const sidebarMonthText = document.getElementById('sidebar-month-text');
const eventModal = document.getElementById('event-modal');
const modalClose = document.getElementById('modal-close');
const modalBody = document.getElementById('modal-body-content');
function cleanEventTitle(title) {
return String(title || '').replace(/[🎂🌟🎤]/g, '').trim();
}
// 初始化
function initCalendar() {
yearSelect.value = currentYear;
monthSelect.value = currentMonth;
renderCalendar(currentYear, currentMonth);
// 事件監聽
yearSelect.addEventListener('change', (e) => {
currentYear = parseInt(e.target.value);
renderCalendar(currentYear, currentMonth);
});
monthSelect.addEventListener('change', (e) => {
currentMonth = parseInt(e.target.value);
renderCalendar(currentYear, currentMonth);
});
prevBtn.addEventListener('click', () => {
currentMonth--;
if (currentMonth < 1) {
currentMonth = 12;
currentYear--;
}
yearSelect.value = currentYear;
monthSelect.value = currentMonth;
renderCalendar(currentYear, currentMonth);
});
nextBtn.addEventListener('click', () => {
currentMonth++;
if (currentMonth > 12) {
currentMonth = 1;
currentYear++;
}
yearSelect.value = currentYear;
monthSelect.value = currentMonth;
renderCalendar(currentYear, currentMonth);
});
// 分類標籤篩選
document.querySelectorAll('.filter-chip').forEach(chip => {
chip.addEventListener('click', (e) => {
document.querySelectorAll('.filter-chip').forEach(c => c.classList.remove('active'));
e.target.classList.add('active');
activeCategory = e.target.getAttribute('data-category');
renderCalendar(currentYear, currentMonth);
});
});
// Modal 關閉
modalClose.addEventListener('click', () => {
eventModal.classList.remove('active');
});
eventModal.addEventListener('click', (e) => {
if (e.target === eventModal) eventModal.classList.remove('active');
});
}
// 渲染日曆
function renderCalendar(year, month) {
monthYearDisplay.textContent = `${year} 年 ${month} 月`;
sidebarMonthText.textContent = `${month} 月水晶紀念日`;
daysGrid.innerHTML = '';
// 1. 計算該月第一天是星期幾 & 總天數
const firstDayIndex = new Date(year, month - 1, 1).getDay(); // 0 = 星期日
const totalDays = new Date(year, month, 0).getDate();
const prevMonthTotalDays = new Date(year, month - 1, 0).getDate();
// 取得當月屬於水晶的事件
const monthEvents = sechskiesEvents.filter(ev => ev.month === month && (activeCategory === 'all' || ev.type === activeCategory));
// 2. 渲染上個月填補空格
for (let i = firstDayIndex - 1; i >= 0; i--) {
const dayCell = document.createElement('div');
dayCell.className = 'day-cell other-month';
dayCell.innerHTML = `<span class="day-num">${prevMonthTotalDays - i}</span>`;
daysGrid.appendChild(dayCell);
}
// 3. 渲染當月每一天
for (let day = 1; day <= totalDays; day++) {
const dayCell = document.createElement('div');
dayCell.className = 'day-cell';
const dayOfWeek = new Date(year, month - 1, day).getDay();
if (dayOfWeek === 0 || dayOfWeek === 6) {
dayCell.classList.add('weekend');
}
// 檢查是否有水晶事件
const matchedEvents = monthEvents.filter(ev => {
if (ev.endDay) {
return day >= ev.day && day <= ev.endDay;
}
return ev.day === day;
});
let dayContent = `<span class="day-num">${day}</span>`;
if (matchedEvents.length > 0) {
// 有生日事件時顯示頭像
const bdayEvent = matchedEvents.find(ev => ev.type === 'birthday');
const concertEvent = matchedEvents.find(ev => ev.type === 'concert' && ev.photo);
if (bdayEvent && bdayEvent.photo) {
const linkUrl = bdayEvent.memberUrl || 'member/member.html';
dayContent += `<a href="${linkUrl}" class="day-birthday-link" onclick="event.stopPropagation();" title="點擊查看 ${bdayEvent.memberName} 詳細介紹"><img src="${bdayEvent.photo}" alt="${bdayEvent.memberName}" class="day-avatar-badge"><span class="day-member-name">${bdayEvent.memberName}</span></a>`;
} else if (concertEvent && concertEvent.photo) {
dayContent += `<img src="${concertEvent.photo}" alt="${concertEvent.title}" class="day-concert-badge" title="${concertEvent.title}">`;
} else {
// 一般事件點點
dayContent += '<div class="day-events">';
matchedEvents.forEach(ev => {
dayContent += `<span class="event-dot ${ev.type}" title="${ev.title}"></span>`;
});
dayContent += '</div>';
}
// 點擊格點觸發 Modal (若非點擊在直接連結上)
dayCell.addEventListener('click', () => {
showDayModal(year, month, day, matchedEvents);
});
}
dayCell.innerHTML = dayContent;
daysGrid.appendChild(dayCell);
}
// 4. 渲染下個月填補空格
const totalRendered = firstDayIndex + totalDays;
const remainingGrid = (totalRendered % 7 === 0) ? 0 : 7 - (totalRendered % 7);
for (let i = 1; i <= remainingGrid; i++) {
const dayCell = document.createElement('div');
dayCell.className = 'day-cell other-month';
dayCell.innerHTML = `<span class="day-num">${i}</span>`;
daysGrid.appendChild(dayCell);
}
// 5. 渲染右側事件列表
renderSidebarEvents(monthEvents);
}
// 渲染右欄當月事件明細
function renderSidebarEvents(events) {
eventsList.innerHTML = '';
if (events.length === 0) {
eventsList.innerHTML = '<p class="events-empty">本月無符合條件之水晶紀念日</p>';
return;
}
events.forEach(ev => {
if (ev.type === 'birthday' && ev.photo) {
// 生日精美照片卡片 (點圖片連成員介紹頁面)
const linkUrl = ev.memberUrl || 'member/member.html';
const bdayCard = document.createElement('div');
bdayCard.className = 'birthday-featured-card';
bdayCard.innerHTML = `
<a href="${linkUrl}" class="birthday-photo-wrap" title="點擊前往 ${ev.memberName} 個人介紹頁面">
<img src="${ev.photo}" alt="${ev.memberName}">
</a>
<div class="birthday-info">
<h4><a href="${linkUrl}" title="前往成員介紹">${ev.memberName}</a></h4>
<div class="en-name">${ev.enName} • ${ev.role}</div>
<div class="bday-date">${ev.month} 月 ${ev.day} 日</div>
</div>
`;
eventsList.appendChild(bdayCard);
} else if (ev.type === 'concert' && ev.photo) {
// 演唱會帶圖卡片
const dateStr = ev.endDay ? `${ev.month}/${ev.day} ~ ${ev.month}/${ev.endDay}` : `${ev.month}/${ev.day}`;
const concertCard = document.createElement('div');
concertCard.className = 'concert-featured-card';
concertCard.innerHTML = `
<img src="${ev.photo}" alt="${ev.title}">
<div class="event-card-header">
<span class="event-date-badge">${dateStr}</span>
</div>
<div class="event-title">${cleanEventTitle(ev.title)}</div>
<div class="event-desc">${ev.desc}</div>
`;
eventsList.appendChild(concertCard);
} else {
// 一般事件卡片
const dateStr = ev.endDay ? `${ev.month}/${ev.day} ~ ${ev.month}/${ev.endDay}` : `${ev.month}/${ev.day}`;
const card = document.createElement('div');
card.className = `event-card type-${ev.type}`;
card.innerHTML = `
<div class="event-card-header">
<span class="event-date-badge">${dateStr}</span>
</div>
<div class="event-title">${cleanEventTitle(ev.title)}</div>
<div class="event-desc">${ev.desc}</div>
`;
eventsList.appendChild(card);
}
});
}
// 顯示 Modal 詳情
function showDayModal(year, month, day, events) {
let html = `<h3 class="modal-date-title">${year} 年 ${month} 月 ${day} 日</h3>`;
events.forEach(ev => {
if (ev.type === 'birthday' && ev.photo) {
const linkUrl = ev.memberUrl || 'member/member.html';
html += `
<a href="${linkUrl}" title="點擊前往 ${ev.memberName} 個人介紹頁面">
<img src="${ev.photo}" alt="${ev.memberName}" class="modal-photo">
</a>
<h4 class="modal-member-name"><a href="${linkUrl}">${ev.memberName}</a></h4>
<p class="modal-member-meta">${ev.enName} (${ev.role})</p>
<p class="modal-desc">${ev.desc}</p>
<a href="${linkUrl}" class="calendar-btn modal-member-link">前往 ${ev.memberName} 專屬頁面 <i class="fa-solid fa-arrow-right"></i></a>
`;
} else if (ev.type === 'concert' && ev.photo) {
html += `
<img src="${ev.photo}" alt="${ev.title}" class="modal-concert-photo">
<h4 class="modal-event-title">${cleanEventTitle(ev.title)}</h4>
<p class="modal-desc">${ev.desc}</p>
`;
} else {
html += `
<div class="modal-event-box">
<h4 class="modal-event-title">${cleanEventTitle(ev.title)}</h4>
<p class="modal-desc">${ev.desc}</p>
</div>
`;
}
});
modalBody.innerHTML = html;
eventModal.classList.add('active');
}
// 頁面載入完成後執行 init
document.addEventListener('DOMContentLoaded', initCalendar);
</script>
</body>
</html>
+300
View File
@@ -0,0 +1,300 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>水晶男孩|殷志源台北見面會</title>
<link rel="stylesheet" href="../assets/css/style.css">
<link rel="stylesheet" href="../assets/css/captain-aura.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="../assets/css/tablet.css">
</head>
<body class="captain-aura-page">
<header class="inner-page-header">
<div class="header-inner site-header-inner">
<h1 class="main-title">殷志源台北見面會</h1>
<div class="subtitle-wrap">
<span class="sub-title">G1 private · 1theland in Taipei · 2018.07.21</span>
</div>
</div>
</header>
<nav class="portal-nav" aria-label="水晶男孩推廣部導覽" data-portal-nav></nav>
<canvas id="particles"></canvas>
<div class="captain-aura-hero" aria-hidden="true">
<img src="../assets/images/concert/captain-aura/hero-placeholder.svg"
alt="見面會橫幅假圖(待替換 hero.jpg" width="1920" height="1003" loading="eager">
</div>
<main class="captain-aura-main content-container" id="captain-aura-archive">
<p class="captain-aura-divider" role="presentation">上半場</p>
<section class="captain-aura-act" aria-labelledby="act-truck">
<h2 class="captain-aura-act__title" id="act-truck">8t. Truck</h2>
<div class="captain-aura-videos">
<div class="captain-aura-video">
<iframe title="8t. Truck ①" src="https://www.youtube.com/embed/phsOnzzHrZs"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
<div class="captain-aura-video">
<iframe title="8t. Truck ②" src="https://www.youtube.com/embed/FccoV2LboWI"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
</div>
</section>
<section class="captain-aura-act" aria-labelledby="act-entry">
<h2 class="captain-aura-act__title" id="act-entry">入境表格</h2>
<div class="captain-aura-videos">
<div class="captain-aura-video">
<iframe title="入境表格 ①" src="https://www.youtube.com/embed/R9k6Cs4Rwqc"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
<div class="captain-aura-video">
<iframe title="入境表格 ②" src="https://www.youtube.com/embed/tRPHtT0-JkE"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
</div>
</section>
<section class="captain-aura-act" aria-labelledby="act-fan-q">
<h2 class="captain-aura-act__title captain-aura-act__title--plain" id="act-fan-q">粉絲提問</h2>
<h2 class="captain-aura-act__title captain-aura-act__title--plain">Taipei in Melody</h2>
<h2 class="captain-aura-act__title captain-aura-act__title--plain">G1's Great Moment</h2>
<h2 class="captain-aura-act__title" id="act-dance">Dance</h2>
<div class="captain-aura-videos">
<div class="captain-aura-video">
<iframe title="Dance ①" src="https://www.youtube.com/embed/9uOETcuFjbE"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
<div class="captain-aura-video">
<iframe title="Dance ②" src="https://www.youtube.com/embed/lOdKgsr9ttU"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
</div>
</section>
<section class="captain-aura-act" aria-labelledby="act-aegyo">
<h2 class="captain-aura-act__title" id="act-aegyo">撒嬌</h2>
<div class="captain-aura-videos">
<div class="captain-aura-video">
<iframe title="撒嬌" src="https://www.youtube.com/embed/S7HJAZ4Kgso"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
</div>
</section>
<section class="captain-aura-act" aria-labelledby="act-1theland">
<h2 class="captain-aura-act__title captain-aura-act__title--plain" id="act-1theland">1 the land</h2>
</section>
<section class="captain-aura-act" aria-labelledby="act-ukulele">
<h2 class="captain-aura-act__title" id="act-ukulele">烏克麗麗</h2>
<div class="captain-aura-videos">
<div class="captain-aura-video">
<iframe title="烏克麗麗 ①" src="https://www.youtube.com/embed/p5_PiYMsoZc"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
<div class="captain-aura-video">
<iframe title="烏克麗麗 ②" src="https://www.youtube.com/embed/7A_0l9wVoNE"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
</div>
</section>
<section class="captain-aura-act" aria-labelledby="act-couple">
<h2 class="captain-aura-act__title" id="act-couple">Couple大合唱</h2>
<div class="captain-aura-videos">
<div class="captain-aura-video">
<iframe title="Couple大合唱 ①" src="https://www.youtube.com/embed/sQ5P3BWL15E"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
<div class="captain-aura-video">
<iframe title="Couple大合唱 ②" src="https://www.youtube.com/embed/XZvyS4yDulk"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
</div>
</section>
<section class="captain-aura-act" aria-labelledby="act-never">
<h2 class="captain-aura-act__title" id="act-never">Never, Ever…</h2>
<div class="captain-aura-videos">
<div class="captain-aura-video">
<iframe title="Never, Ever… ①" src="https://www.youtube.com/embed/v8XBssrcM0U"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
</div>
</section>
<p class="captain-aura-divider" role="presentation">下半場</p>
<section class="captain-aura-act" aria-labelledby="act-never-2">
<div class="captain-aura-videos">
<div class="captain-aura-video">
<iframe title="Never, Ever… ②" src="https://www.youtube.com/embed/0uQgXToOyiA"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
</div>
</section>
<section class="captain-aura-act" aria-labelledby="act-drunk">
<h2 class="captain-aura-act__title" id="act-drunk">酒醉</h2>
<div class="captain-aura-videos">
<div class="captain-aura-video">
<iframe title="酒醉" src="https://www.youtube.com/embed/6mUl85XOyQw"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
</div>
</section>
<section class="captain-aura-act" aria-labelledby="act-melody">
<h2 class="captain-aura-act__title" id="act-melody">滿醉 in Melody</h2>
<div class="captain-aura-videos">
<div class="captain-aura-video">
<iframe title="滿醉 in Melody ①" src="https://www.youtube.com/embed/EJcpA7bubqM"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
<div class="captain-aura-video">
<iframe title="滿醉 in Melody ②" src="https://www.youtube.com/embed/Ovzu07lYVVU"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
</div>
</section>
<section class="captain-aura-act" aria-labelledby="act-allfamy">
<h2 class="captain-aura-act__title" id="act-allfamy">All Famy</h2>
<div class="captain-aura-videos">
<div class="captain-aura-video">
<iframe title="All Famy ①" src="https://www.youtube.com/embed/b6S-CAk-RAA"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
<div class="captain-aura-video">
<iframe title="All Famy ②" src="https://www.youtube.com/embed/G8u7D3Xleeo"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
</div>
</section>
<section class="captain-aura-act" aria-labelledby="act-suddenly">
<h2 class="captain-aura-act__title" id="act-suddenly">Suddenly</h2>
<div class="captain-aura-videos">
<div class="captain-aura-video">
<iframe title="Suddenly" src="https://www.youtube.com/embed/a70ojjXoB4M"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
</div>
</section>
<section class="captain-aura-act" aria-labelledby="act-one">
<h2 class="captain-aura-act__title" id="act-one">ONE</h2>
<div class="captain-aura-videos">
<div class="captain-aura-video">
<iframe title="ONE ①" src="https://www.youtube.com/embed/fhgboeZ-J0s"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
<div class="captain-aura-video">
<iframe title="ONE ②" src="https://www.youtube.com/embed/CIhAi_gw0fI"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
</div>
</section>
<section class="captain-aura-act" aria-labelledby="act-encore">
<h2 class="captain-aura-act__title" id="act-encore">安可 / Adios</h2>
<div class="captain-aura-videos">
<div class="captain-aura-video">
<iframe title="安可 / Adios ①" src="https://www.youtube.com/embed/Jz3zUZolzjM"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
<div class="captain-aura-video">
<iframe title="安可 / Adios ②" src="https://www.youtube.com/embed/ycydd8Bfkb8"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen loading="lazy"></iframe>
</div>
</div>
</section>
</main>
<script>
const canvas = document.getElementById('particles');
const ctx = canvas.getContext('2d');
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
let particles = [];
class Particle {
constructor() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.size = Math.random() * 2 + 0.5;
this.speedX = (Math.random() - 0.5) * 0.3;
this.speedY = (Math.random() - 0.5) * 0.3;
}
update() {
this.x += this.speedX;
this.y += this.speedY;
if (this.x < 0 || this.x > canvas.width) this.speedX *= -1;
if (this.y < 0 || this.y > canvas.height) this.speedY *= -1;
}
draw() {
ctx.fillStyle = 'rgba(255, 204, 0, 0.35)';
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
function initParticles() {
particles = [];
for (let i = 0; i < 80; i++) particles.push(new Particle());
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const p of particles) {
p.update();
p.draw();
}
requestAnimationFrame(animate);
}
initParticles();
animate();
</script>
<script src="../assets/js/portal-nav.js"></script>
</body>
</html>
@@ -0,0 +1,39 @@
# 對話紀錄:Bootstrap 5 作業(2026-07-08
> 這份是這次對話的重點備份,不是規範檔。規範仍以 `developer-rules/rules/` 為準。
## 起因
老師的 Bootstrap 5 課堂範例重點:Navbar、RWD 格線(container/row/col)、錨點導覽、Carousel(同頁多組需不同 id、可放進 grid 欄位)。目標是做成符合本站風格的作業/作品展示頁,而不是照抄範例或做成教學筆記。
## 過程與轉折
1. **第一版**`extra/hw-bootstrap.html` 初版):做成「Bootstrap 元件練習誌」,內容偏向解釋 navbar/grid/carousel 是什麼 → 被打回,因為變成讀書筆記,沒有應用在專案內容上。
2. **第二版**:改寫同一檔案為「精華牆」,用真實內容(淪陷瞬間/完整的六顆/黃色留聲機/現場的震撼/Yellow Note)取代教學說明,Bootstrap 降到最後一行小字帶過。
3. 另外新增一頁 **`extra/bootstrap-navbar-carousel.html`**(跨時空精選):HeroSticky Navbar 錨點導覽+兩組不同 id 的 Carousel(一組放進 grid 欄位)+Grid 卡片區+結尾備註,符合站主指定的頁面結構規範。
4. 最後一個任務:**把 Bootstrap 實際套進首頁**,但不能動到 `index.html` 本體 → 建立備份 **`index-bootstrap.html`**,只在這份備份裡:
- 把既有的 `<nav class="portal-nav" data-portal-nav>` 疊加 `navbar navbar-expand-lg navbar-dark` 等真正的 Bootstrap navbar class(不碰 `assets/js/portal-nav.js`)。
- `#timeline`「SECHSKIES STORY ARCHIVE」下方新增一排錨點快覽(傳奇誕生/凍結世界/完整回歸/夢境成真/九月首演/溫柔陪伴/未完待續),對應到下面每張時間軸卡片的新增 `id`
## 過程中修掉的 bug(都只影響 `index-bootstrap.html`
- **Bootstrap Reboot 蓋掉全站底色/字體/邊距** → 用同量級純標籤選擇器(`body.home-page{}``h1~h6,p,ul...{}``a{}`)疊在 Bootstrap 後面蓋回來。
- **`.section-title` 間距被吃掉**:一開始誤用 `body.home-page h2{margin:0}`(class+type)優先權太高,蓋過 `.section-title` 自己的 `margin-bottom:20px`。改回純標籤選擇器 `h2{margin:0}`,優先權只贏 Bootstrap Reboot,不會贏 class 選擇器。
- **頁尾連結變 Bootstrap 預設藍色底線**`.studio-link` 沒有專屬顏色規則,靠全站 `a{color:...}`撐著,被 Bootstrap 同量級的 `a{}` 蓋掉,補回 `a{color:var(--color-text);text-decoration:none}`
- **手機版漢堡選單失效**Bootstrap 的 `.navbar{display:flex}` 沒寫在 media query 裡,跟站上桌機/手機兩種 `.portal-nav` 版面同量級、且載入在後,兩種寬度都被蓋成桌機版。改成照原本 `max-width:991px` 斷點分別把桌機/手機版面疊回去;另外先前加的 `#portal-primary-menu` id 選擇器優先權太強、蓋掉手機收合邏輯,已移除(内層 `.portal-nav>ul` 等選擇器本來就贏過 Bootstrap 的 `navbar-nav`/`nav-link`,不需要額外處理)。
- **錨點跳轉被 sticky nav 擋住/卡片年代標籤被裁切**:改用 JS 即時量測 `.portal-nav` 實際高度,加 24px 緩衝(年代標籤 `::after` 用絕對定位貼在卡片外側),寫進 CSS 變數 `--home-nav-offset`,取代原本用猜的固定值。
## 最終異動檔案
| 檔案 | 狀態 |
|---|---|
| `extra/hw-bootstrap.html` | 新增(精華牆) |
| `extra/bootstrap-navbar-carousel.html` | 新增(跨時空精選) |
| `extra/site-guide.html` | 新增兩行參照(未動其他內容) |
| `index-bootstrap.html` | 新增(`index.html` 的 Bootstrap 版備份,`index.html` 本體全程未動) |
## 學到的教訓(給下次疊 Bootstrap 到既有頁面用)
- 疊 Bootstrap 到已有完整樣式系統的頁面時,**优先權要「剛好贏過 Bootstrap 同層級選擇器」就好,不要過度加 class/id 硬贏**,否則會反過來蓋掉站上原本更細緻的規則(尤其是響應式 media query 那些)。
- 純標籤選擇器(`h2{}``a{}`)配合「寫在 Bootstrap CDN 連結之後」就足以贏過 Bootstrap Reboot,且不會蓋過站上任何 class 選擇器。
- 凡是原本就有分斷點(`@media`)的規則,疊加保護時也要照原本的斷點分開寫,不能只複製其中一種寬度的版本。
+292
View File
@@ -0,0 +1,292 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>水晶男孩|跨時空精選</title>
<link rel="stylesheet" href="../assets/css/style.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="../assets/css/tablet.css">
<style>
/* ── 跨時空精選頁專用樣式(bnc-page 範圍內,不影響全站) ── */
.bnc-page .bnc-hero-lead {
text-align: center;
color: var(--color-text-muted);
font-size: 1rem;
max-width: 620px;
margin: 0 auto 1.5rem;
}
.bnc-page .bnc-navbar {
background: var(--color-bg-raised) !important;
}
.bnc-page .bnc-navbar .navbar-brand,
.bnc-page .bnc-navbar .nav-link {
color: var(--color-text-inverse);
}
.bnc-page .bnc-navbar .nav-link:hover,
.bnc-page .bnc-navbar .nav-link.active {
color: var(--color-brand);
}
.bnc-page .bnc-section {
scroll-margin-top: 78px;
}
.bnc-page .bnc-lead {
text-align: center;
color: var(--color-text-muted);
font-size: 1rem;
max-width: 620px;
margin: 0 auto 1.75rem;
}
.bnc-page .bnc-card {
border: 1px solid rgba(255, 204, 0, 0.25);
border-radius: 12px;
}
.bnc-page .bnc-card h3 {
color: var(--color-brand-dark);
}
.bnc-page .bnc-card p {
color: var(--color-text-muted);
}
.bnc-page .carousel-item img {
height: 340px;
object-fit: cover;
}
.bnc-page .bnc-tech-note {
text-align: center;
color: var(--color-text-muted);
font-size: 0.78rem;
margin: 2rem auto 0;
max-width: 640px;
}
</style>
</head>
<body class="bnc-page">
<header class="inner-page-header" id="top">
<div class="header-inner site-header-inner">
<h1 class="main-title">跨時空精選</h1>
<div class="subtitle-wrap">
<span class="sub-title">ALBUMS · STAGES · SIX CRYSTALS</span>
</div>
</div>
</header>
<nav class="portal-nav" aria-label="水晶男孩推廣部導覽" data-portal-nav></nav>
<p class="bnc-hero-lead">從出道專輯到近期巡演,這頁抓幾段最想讓你先看的片段——點下面導覽跳段落,或直接往下滑逛完。</p>
<nav class="navbar navbar-expand-lg navbar-dark bnc-navbar sticky-top">
<div class="container">
<a class="navbar-brand fw-bold" href="#top">跨時空精選</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#bncNav"
aria-controls="bncNav" aria-expanded="false" aria-label="切換頁內導覽">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="bncNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item"><a class="nav-link" href="#cover">封面精選</a></li>
<li class="nav-item"><a class="nav-link" href="#picks">精選作品</a></li>
<li class="nav-item"><a class="nav-link" href="#stages">舞台片段</a></li>
</ul>
</div>
</div>
</nav>
<canvas id="particles"></canvas>
<main class="content-container">
<section id="cover" class="bnc-section">
<div class="container">
<h2 class="section-title">封面精選</h2>
<p class="bnc-lead">巡迴現場的幾個瞬間,先讓眼睛暖身。</p>
<div class="row justify-content-center">
<div class="col-12 col-lg-10">
<div id="carouselCover" class="carousel slide shadow-sm rounded overflow-hidden"
data-bs-ride="carousel">
<div class="carousel-indicators">
<button type="button" data-bs-target="#carouselCover" data-bs-slide-to="0"
class="active" aria-current="true" aria-label="2016"></button>
<button type="button" data-bs-target="#carouselCover" data-bs-slide-to="1"
aria-label="2017"></button>
<button type="button" data-bs-target="#carouselCover" data-bs-slide-to="2"
aria-label="2018"></button>
<button type="button" data-bs-target="#carouselCover" data-bs-slide-to="3"
aria-label="2020"></button>
</div>
<div class="carousel-inner">
<div class="carousel-item active">
<img src="../assets/images/concert/2016.jpg" class="d-block w-100" alt="2016 演唱會">
<div class="carousel-caption d-none d-md-block">
<p>2016 巡演</p>
</div>
</div>
<div class="carousel-item">
<img src="../assets/images/concert/2017.jpg" class="d-block w-100" alt="2017 演唱會">
<div class="carousel-caption d-none d-md-block">
<p>2017 巡演</p>
</div>
</div>
<div class="carousel-item">
<img src="../assets/images/concert/2018.jpg" class="d-block w-100" alt="2018 演唱會">
<div class="carousel-caption d-none d-md-block">
<p>2018 巡演</p>
</div>
</div>
<div class="carousel-item">
<img src="../assets/images/concert/2020.jpg" class="d-block w-100" alt="2020 演唱會">
<div class="carousel-caption d-none d-md-block">
<p>2020 巡演</p>
</div>
</div>
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#carouselCover"
data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">上一張</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carouselCover"
data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">下一張</span>
</button>
</div>
</div>
</div>
</div>
</section>
<section id="picks" class="bnc-section">
<div class="container">
<h2 class="section-title">精選作品</h2>
<p class="bnc-lead">推廣部這幾個單元,是新朋友最快認識水晶男孩的入口。</p>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
<div class="col">
<div class="card bnc-card h-100 p-3">
<h3 class="h6">淪陷瞬間</h3>
<p class="small mb-0">入坑敘事與跨時空軌跡時間軸。</p>
</div>
</div>
<div class="col">
<div class="card bnc-card h-100 p-3">
<h3 class="h6">完整的六顆</h3>
<p class="small mb-0">黑水晶、白水晶六人海報板與團名典故。</p>
</div>
</div>
<div class="col">
<div class="card bnc-card h-100 p-3">
<h3 class="h6">現場的震撼</h3>
<p class="small mb-0">巡迴場次、場館與曲目一次展開。</p>
</div>
</div>
<div class="col">
<div class="card bnc-card h-100 p-3">
<h3 class="h6">黃色留聲機</h3>
<p class="small mb-0">全盛期年表與代表曲目整理。</p>
</div>
</div>
<div class="col">
<div class="card bnc-card h-100 p-3">
<h3 class="h6">Yellow Note</h3>
<p class="small mb-0">視覺演進手記與追星足跡地圖。</p>
</div>
</div>
<div class="col">
<div class="card bnc-card h-100 p-3">
<h3 class="h6">全員發瘋</h3>
<p class="small mb-0">綜藝節目存檔表,六人各自暴走實錄。</p>
</div>
</div>
</div>
</div>
</section>
<section id="stages" class="bnc-section">
<div class="container">
<h2 class="section-title">舞台片段</h2>
<p class="bnc-lead">從專輯封面回望,那些年一起站上的舞台。</p>
<div class="row justify-content-center">
<div class="col-12 col-lg-8">
<div id="carouselStages" class="carousel slide shadow-sm rounded overflow-hidden"
data-bs-ride="carousel">
<div class="carousel-indicators">
<button type="button" data-bs-target="#carouselStages" data-bs-slide-to="0"
class="active" aria-current="true" aria-label="1997"></button>
<button type="button" data-bs-target="#carouselStages" data-bs-slide-to="1"
aria-label="1998"></button>
<button type="button" data-bs-target="#carouselStages" data-bs-slide-to="2"
aria-label="1999"></button>
<button type="button" data-bs-target="#carouselStages" data-bs-slide-to="3"
aria-label="2016"></button>
</div>
<div class="carousel-inner">
<div class="carousel-item active">
<img src="../assets/images/albums/1997.jpg" class="d-block w-100" alt="1997 出道專輯">
<div class="carousel-caption d-none d-md-block">
<p>1997 · 出道</p>
</div>
</div>
<div class="carousel-item">
<img src="../assets/images/albums/1998.jpg" class="d-block w-100" alt="1998 專輯">
<div class="carousel-caption d-none d-md-block">
<p>1998</p>
</div>
</div>
<div class="carousel-item">
<img src="../assets/images/albums/1999.jpg" class="d-block w-100"
alt="1999 全盛期專輯">
<div class="carousel-caption d-none d-md-block">
<p>1999 · 全盛期</p>
</div>
</div>
<div class="carousel-item">
<img src="../assets/images/albums/2016.jpg" class="d-block w-100" alt="2016 合體專輯">
<div class="carousel-caption d-none d-md-block">
<p>2016 · 合體</p>
</div>
</div>
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#carouselStages"
data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">上一張</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carouselStages"
data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">下一張</span>
</button>
</div>
</div>
</div>
</div>
</section>
<p class="bnc-tech-note">本頁使用 Bootstrap 5 完成響應式導覽、Grid 排版、錨點跳轉與 Carousel 展示。</p>
</main>
<footer>
<p>SECHSKIES Fanpage &copy; 2026 - 黃色氣球永遠飄揚</p>
<a href="https://jekkinoopy.github.io/Portfolio/" class="studio-link" target="_blank" rel="noopener noreferrer">
Design by <span class="mark_b">Jekkinoopy Studio</span>
</a>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="../assets/js/particles.js"></script>
<script src="../assets/js/portal-nav.js"></script>
</body>
</html>
+344
View File
@@ -0,0 +1,344 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>水晶男孩|精華牆</title>
<link rel="stylesheet" href="../assets/css/style.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="../assets/css/tablet.css">
<style>
/* ── 精華牆頁專用樣式 ── */
.hwfp-navbar {
background: var(--color-bg-raised) !important;
}
.hwfp-navbar .navbar-brand,
.hwfp-navbar .nav-link {
color: var(--color-text-inverse);
}
.hwfp-navbar .nav-link:hover,
.hwfp-navbar .nav-link.active {
color: var(--color-brand);
}
.hwfp-section {
scroll-margin-top: 78px;
}
.hwfp-lead {
text-align: center;
color: var(--color-text-muted);
font-size: 1rem;
margin: 0 auto 1.75rem;
max-width: 620px;
}
.hwfp-card {
border: 1px solid rgba(255, 204, 0, 0.25);
border-radius: 12px;
}
.hwfp-card h3 {
color: var(--color-brand-dark);
}
.hwfp-card p {
color: var(--color-text-muted);
}
.hwfp-section .carousel-item img {
height: 320px;
object-fit: cover;
}
.hwfp-cta {
text-align: center;
margin-top: 1.5rem;
}
.hwfp-tech-note {
text-align: center;
color: var(--color-text-muted);
font-size: 0.78rem;
margin: 2rem auto 0;
max-width: 640px;
}
</style>
</head>
<body class="hw-bootstrap-page">
<header class="inner-page-header" id="top">
<div class="header-inner site-header-inner">
<h1 class="main-title">精華牆</h1>
<div class="subtitle-wrap">
<span class="sub-title">ALBUMS · MEMBERS · CONCERTS · YELLOW NOTE</span>
</div>
</div>
</header>
<nav class="portal-nav" aria-label="水晶男孩推廣部導覽" data-portal-nav></nav>
<nav class="navbar navbar-expand-lg navbar-dark hwfp-navbar sticky-top">
<div class="container">
<a class="navbar-brand fw-bold" href="#top">推廣部精華牆</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#hwfpNav"
aria-controls="hwfpNav" aria-expanded="false" aria-label="切換頁內導覽">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="hwfpNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item"><a class="nav-link" href="#chronicle">淪陷瞬間</a></li>
<li class="nav-item"><a class="nav-link" href="#members">完整的六顆</a></li>
<li class="nav-item"><a class="nav-link" href="#albums">黃色留聲機</a></li>
<li class="nav-item"><a class="nav-link" href="#concerts">現場的震撼</a></li>
<li class="nav-item"><a class="nav-link" href="#yellownote">Yellow Note</a></li>
</ul>
</div>
</div>
</nav>
<canvas id="particles"></canvas>
<main class="content-container">
<section id="chronicle" class="hwfp-section">
<div class="container">
<h2 class="section-title">淪陷瞬間</h2>
<p class="hwfp-lead">從出道、解散到多年後的驚喜合體,再走到現役四人體制——這是水晶男孩走了快三十年的路。</p>
<div class="row row-cols-2 row-cols-md-4 g-3">
<div class="col">
<div class="card hwfp-card h-100 text-center p-3">
<h3 class="h6 mb-1">1997</h3>
<p class="small mb-0">出道</p>
</div>
</div>
<div class="col">
<div class="card hwfp-card h-100 text-center p-3">
<h3 class="h6 mb-1">2000</h3>
<p class="small mb-0">解散</p>
</div>
</div>
<div class="col">
<div class="card hwfp-card h-100 text-center p-3">
<h3 class="h6 mb-1">2016</h3>
<p class="small mb-0">合體</p>
</div>
</div>
<div class="col">
<div class="card hwfp-card h-100 text-center p-3">
<h3 class="h6 mb-1">2019</h3>
<p class="small mb-0">現役四人</p>
</div>
</div>
</div>
</div>
</section>
<section id="members" class="hwfp-section">
<div class="container">
<h2 class="section-title">完整的六顆</h2>
<p class="hwfp-lead">黑水晶負責 Rap 與舞蹈,白水晶撐起主唱線——六顆水晶各自發光,合體時更亮。</p>
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-6 g-3">
<div class="col">
<div class="card hwfp-card h-100 text-center p-3">
<h3 class="h6 mb-1">殷志源</h3>
<p class="small mb-0">Leader・Rapper</p>
</div>
</div>
<div class="col">
<div class="card hwfp-card h-100 text-center p-3">
<h3 class="h6 mb-1">李宰鎮</h3>
<p class="small mb-0">Rapper・Dancer</p>
</div>
</div>
<div class="col">
<div class="card hwfp-card h-100 text-center p-3">
<h3 class="h6 mb-1">金在德</h3>
<p class="small mb-0">Rapper・Dancer</p>
</div>
</div>
<div class="col">
<div class="card hwfp-card h-100 text-center p-3">
<h3 class="h6 mb-1">姜成勳</h3>
<p class="small mb-0">Vocalist</p>
</div>
</div>
<div class="col">
<div class="card hwfp-card h-100 text-center p-3">
<h3 class="h6 mb-1">張水院</h3>
<p class="small mb-0">Vocalist</p>
</div>
</div>
<div class="col">
<div class="card hwfp-card h-100 text-center p-3">
<h3 class="h6 mb-1">高志溶</h3>
<p class="small mb-0">Vocalist</p>
</div>
</div>
</div>
</div>
</section>
<section id="albums" class="hwfp-section">
<div class="container">
<h2 class="section-title">黃色留聲機</h2>
<p class="hwfp-lead">從出道專輯到合體回歸,聲音一路留下的痕跡。</p>
<div class="row justify-content-center">
<div class="col-12 col-lg-8">
<div id="carouselAlbums" class="carousel slide shadow-sm rounded overflow-hidden"
data-bs-ride="carousel">
<div class="carousel-indicators">
<button type="button" data-bs-target="#carouselAlbums" data-bs-slide-to="0"
class="active" aria-current="true" aria-label="1997"></button>
<button type="button" data-bs-target="#carouselAlbums" data-bs-slide-to="1"
aria-label="1998"></button>
<button type="button" data-bs-target="#carouselAlbums" data-bs-slide-to="2"
aria-label="1999"></button>
<button type="button" data-bs-target="#carouselAlbums" data-bs-slide-to="3"
aria-label="2016"></button>
</div>
<div class="carousel-inner">
<div class="carousel-item active">
<img src="../assets/images/albums/1997.jpg" class="d-block w-100" alt="1997 出道專輯">
<div class="carousel-caption d-none d-md-block">
<p>1997 · 出道</p>
</div>
</div>
<div class="carousel-item">
<img src="../assets/images/albums/1998.jpg" class="d-block w-100" alt="1998 專輯">
<div class="carousel-caption d-none d-md-block">
<p>1998</p>
</div>
</div>
<div class="carousel-item">
<img src="../assets/images/albums/1999.jpg" class="d-block w-100"
alt="1999 全盛期專輯">
<div class="carousel-caption d-none d-md-block">
<p>1999 · 全盛期</p>
</div>
</div>
<div class="carousel-item">
<img src="../assets/images/albums/2016.jpg" class="d-block w-100" alt="2016 合體專輯">
<div class="carousel-caption d-none d-md-block">
<p>2016 · 合體</p>
</div>
</div>
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#carouselAlbums"
data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">上一張</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carouselAlbums"
data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">下一張</span>
</button>
</div>
</div>
</div>
</div>
</section>
<section id="concerts" class="hwfp-section">
<div class="container">
<h2 class="section-title">現場的震撼</h2>
<p class="hwfp-lead">巡迴現場的燈光與尖叫聲,是照片也留不住的那種震撼。</p>
<div class="row justify-content-center">
<div class="col-12 col-md-10">
<div id="carouselConcerts" class="carousel slide shadow-sm rounded overflow-hidden"
data-bs-ride="carousel">
<div class="carousel-indicators">
<button type="button" data-bs-target="#carouselConcerts" data-bs-slide-to="0"
class="active" aria-current="true" aria-label="2016"></button>
<button type="button" data-bs-target="#carouselConcerts" data-bs-slide-to="1"
aria-label="2017"></button>
<button type="button" data-bs-target="#carouselConcerts" data-bs-slide-to="2"
aria-label="2018"></button>
<button type="button" data-bs-target="#carouselConcerts" data-bs-slide-to="3"
aria-label="2020"></button>
</div>
<div class="carousel-inner">
<div class="carousel-item active">
<img src="../assets/images/concert/2016.jpg" class="d-block w-100" alt="2016 演唱會">
<div class="carousel-caption d-none d-md-block">
<p>2016 巡演</p>
</div>
</div>
<div class="carousel-item">
<img src="../assets/images/concert/2017.jpg" class="d-block w-100" alt="2017 演唱會">
<div class="carousel-caption d-none d-md-block">
<p>2017 巡演</p>
</div>
</div>
<div class="carousel-item">
<img src="../assets/images/concert/2018.jpg" class="d-block w-100" alt="2018 演唱會">
<div class="carousel-caption d-none d-md-block">
<p>2018 巡演</p>
</div>
</div>
<div class="carousel-item">
<img src="../assets/images/concert/2020.jpg" class="d-block w-100" alt="2020 演唱會">
<div class="carousel-caption d-none d-md-block">
<p>2020 巡演</p>
</div>
</div>
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#carouselConcerts"
data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">上一張</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carouselConcerts"
data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">下一張</span>
</button>
</div>
</div>
</div>
<p class="hwfp-cta"><a class="btn" href="../concert/concert.html">看完整場次整理 →</a></p>
</div>
</section>
<section id="yellownote" class="hwfp-section">
<div class="container">
<h2 class="section-title">Yellow Note</h2>
<p class="hwfp-lead">推廣部的手記本,記錄視覺演進與追星足跡。</p>
<div class="row row-cols-1 row-cols-md-2 g-3">
<div class="col">
<div class="card hwfp-card p-3">
<h3 class="h6">視覺進化論</h3>
<p class="small mb-0">從好奇到全端,站主的自述與設計時間軸。</p>
</div>
</div>
<div class="col">
<div class="card hwfp-card p-3">
<h3 class="h6">聖地巡禮</h3>
<p class="small mb-0">台灣行程整理與地圖,成員足跡一覽。</p>
</div>
</div>
</div>
</div>
</section>
<p class="hwfp-tech-note">本頁使用 Bootstrap 5 完成導覽列、響應式格線、錨點導覽與輪播整合。</p>
</main>
<footer>
<p>SECHSKIES Fanpage &copy; 2026 - 黃色氣球永遠飄揚</p>
<a href="https://jekkinoopy.github.io/Portfolio/" class="studio-link" target="_blank" rel="noopener noreferrer">
Design by <span class="mark_b">Jekkinoopy Studio</span>
</a>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="../assets/js/particles.js"></script>
<script src="../assets/js/portal-nav.js"></script>
</body>
</html>
+141 -446
View File
@@ -23,57 +23,7 @@
overflow: visible;
}
/* 主選單樹巢狀:data-site-guide-depth + .site-guide-nav-name */
body.site-guide-page .site-guide-nav-table .site-guide-nav-menu-cell {
text-align: left;
}
body.site-guide-page .site-guide-nav-table tr[data-site-guide-depth="1"] .site-guide-nav-menu-cell {
padding-left: 0.65rem;
}
body.site-guide-page .site-guide-nav-table tr[data-site-guide-depth="2"] .site-guide-nav-menu-cell {
padding-left: 1.1rem;
}
body.site-guide-page .site-guide-nav-table tr[data-site-guide-depth="3"] .site-guide-nav-menu-cell {
padding-left: 1.85rem;
}
body.site-guide-page .site-guide-nav-table tr[data-site-guide-depth="4"] .site-guide-nav-menu-cell {
padding-left: 2.6rem;
}
body.site-guide-page .site-guide-nav-table tr[data-site-guide-depth="2"] .site-guide-nav-name::before {
content: "——\00a0";
}
body.site-guide-page .site-guide-nav-table tr[data-site-guide-depth="3"] .site-guide-nav-name::before {
content: "————\00a0";
}
body.site-guide-page .site-guide-nav-table tr[data-site-guide-depth="4"] .site-guide-nav-name::before {
content: "——————\00a0";
}
body.site-guide-page .site-guide-nav-table tr[data-site-guide-depth] .site-guide-nav-name::before {
display: inline;
font-family: inherit;
font-size: 0.92em;
color: rgba(0, 0, 0, 0.38);
letter-spacing: 0.02em;
}
/* 選單名稱(短字層):單行不斷行;欄寬不足時由表格外層橫向捲動 */
body.site-guide-page .site-guide-nav-table td.site-guide-nav-menu-cell {
white-space: nowrap;
width: 1%;
}
body.site-guide-page .site-guide-nav-table .site-guide-nav-name,
body.site-guide-page .site-guide-nav-table .site-guide-nav-name a {
white-space: nowrap;
}
/* 主選單樹已改心智圖巢狀清單,樣式集中於 assets/css/style.css 的 .site-guide-mindmap 規則 */
</style>
</head>
@@ -100,194 +50,139 @@
<section class="site-guide-group site-guide-group--nav-hero" aria-labelledby="sg-nav-tree">
<h2 class="section-title" id="sg-nav-tree">主選單樹</h2>
<p class="site-guide-legend">
<span><span class="site-guide-legend__on"></span> 連結</span>
<span><span class="site-guide-legend__off"></span> 無連結</span>
<span><span class="site-guide-legend__on"></span> 實線金框:頂欄真實連結</span>
<span><span class="site-guide-legend__off"></span> 虛線框:頂欄有此項但無連結/籌備中</span>
<span><span class="site-guide-legend__extra"></span> 灰色虛線:不在頂欄,頁面內錨點/草稿/備查</span>
</p>
<p class="site-guide-nav-caption site-guide-nav-caption--standalone">路徑相對站根;主選單→次選單→子節點→延伸內容,全部收在同一張心智圖裡,不用再往下翻分散的表格。</p>
<div class="site-guide-nav-table-wrap">
<table class="site-guide-nav-table">
<caption class="site-guide-nav-caption">路徑相對站根。</caption>
<thead>
<tr>
<th scope="col">選單名稱</th>
<th scope="col">頁面簡介</th>
<th scope="col">連結</th>
</tr>
</thead>
<tbody>
<tr data-site-guide-depth="1">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../index.html">淪陷瞬間</a></span></td>
<td class="site-guide-nav-desc">Hero、入坑敘事與站方初衷;LOGO 理念、宣言與跨時空軌跡時間軸。</td>
<td class="site-guide-nav-link-cell"><a href="../index.html"><code>index.html</code></a>
</td>
</tr>
<tr data-site-guide-depth="2">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><span
class="site-guide-nav-nolink">傳奇的轉折(六六歌2</span></span></td>
<td class="site-guide-nav-desc">四種入口(順序):時間軸、電子書為獨立單頁;網頁版為 TOTOGA2 整合長頁(章節錨點);影片為嵌入單頁。</td>
<td class="site-guide-nav-link-cell"><span class="site-guide-nav-nolink"></span></td>
</tr>
<tr data-site-guide-depth="3">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../totoga2/timeline.html">時間軸</a></span></td>
<td class="site-guide-nav-desc">時間軸與紀實八章長文單頁。</td>
<td class="site-guide-nav-link-cell"><a
href="../totoga2/timeline.html"><code>totoga2/timeline.html</code></a>
</td>
</tr>
<tr data-site-guide-depth="3">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../totoga2/ebook.html">電子書</a></span></td>
<td class="site-guide-nav-desc">紀實電子書內容獨立成頁。</td>
<td class="site-guide-nav-link-cell"><a
href="../totoga2/ebook.html"><code>totoga2/ebook.html</code></a></td>
</tr>
<tr data-site-guide-depth="3">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../totoga2/readable.html">圖文好讀版</a></span></td>
<td class="site-guide-nav-desc">單頁整合:影片/電子書/長文模式切換與八章錨點。</td>
<td class="site-guide-nav-link-cell"><a
href="../totoga2/readable.html"><code>totoga2/readable.html</code></a></td>
</tr>
<tr data-site-guide-depth="4">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../totoga2/readable%20copy.html">圖文好讀版 · 複本</a></span></td>
<td class="site-guide-nav-desc">與正式版同結構之草稿複本。</td>
<td class="site-guide-nav-link-cell"><a
href="../totoga2/readable%20copy.html"><code>totoga2/readable copy.html</code></a>
</td>
</tr>
<tr data-site-guide-depth="4">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../totoga2/readable%20copy%202.html">圖文好讀版 · 複本 2</a></span></td>
<td class="site-guide-nav-desc">與正式版同結構之草稿複本。</td>
<td class="site-guide-nav-link-cell"><a
href="../totoga2/readable%20copy%202.html"><code>totoga2/readable copy 2.html</code></a>
</td>
</tr>
<tr data-site-guide-depth="3">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../totoga2/video.html">影片</a></span></td>
<td class="site-guide-nav-desc">無限挑戰 · 六六歌2 嵌入影片單頁。</td>
<td class="site-guide-nav-link-cell"><a
href="../totoga2/video.html"><code>totoga2/video.html</code></a></td>
</tr>
<tr data-site-guide-depth="1">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../variety/bangxiao.html">世紀級寶藏</a></span></td>
<td class="site-guide-nav-desc"></td>
<td class="site-guide-nav-link-cell"><a
href="../variety/bangxiao.html"><code>variety/bangxiao.html</code></a></td>
</tr>
<tr data-site-guide-depth="2">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../variety/bangxiao.html">爆笑養老院(計量器)</a></span></td>
<td class="site-guide-nav-desc">甜蜜負擔計量器;勾本命算出要補幾個節目。</td>
<td class="site-guide-nav-link-cell"><a
href="../variety/bangxiao.html"><code>variety/bangxiao.html</code></a>
</td>
</tr>
<tr data-site-guide-depth="3">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../variety/variety-shows.html">節目存檔表</a></span></td>
<td class="site-guide-nav-desc">綜藝節目存檔表;含隊長帶弟弟、各自暴走等區塊錨點。</td>
<td class="site-guide-nav-link-cell"><a
href="../variety/variety-shows.html"><code>variety/variety-shows.html</code></a>
</td>
</tr>
<tr data-site-guide-depth="3">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../variety/variety.html#variety">隊長帶弟弟</a></span></td>
<td class="site-guide-nav-desc">綜藝頁「瘋狂出演中」主區;與頂欄「隊長帶弟弟」同頁錨點。</td>
<td class="site-guide-nav-link-cell"><a
href="../variety/variety.html#variety"><code>variety/variety.html#variety</code></a>
</td>
</tr>
<tr data-site-guide-depth="3">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><span
class="site-guide-nav-nolink">各自暴走</span></span></td>
<td class="site-guide-nav-desc"></td>
<td class="site-guide-nav-link-cell"><span class="site-guide-nav-nolink"></span></td>
</tr>
<tr data-site-guide-depth="4">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../variety/NJTW/NJTW5.html">新西遊記</a></span></td>
<td class="site-guide-nav-desc">與鬼神同行企劃文案、六人介紹與 OB/YB 雙隊版面。</td>
<td class="site-guide-nav-link-cell"><a
href="../variety/NJTW/NJTW5.html"><code>variety/NJTW/NJTW5.html</code></a></td>
</tr>
<tr data-site-guide-depth="1">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../member/member.html">完整的六顆</a></span></td>
<td class="site-guide-nav-desc">黑/白水晶六人海報板、團名典故與成員靜態介紹。</td>
<td class="site-guide-nav-link-cell"><a
href="../member/member.html"><code>member/member.html</code></a></td>
</tr>
<tr data-site-guide-depth="1">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../concert/concert.html">現場的震撼</a></span></td>
<td class="site-guide-nav-desc">巡迴場次、場館與曲目展開;搜尋曲目或場次。</td>
<td class="site-guide-nav-link-cell"><a
href="../concert/concert.html"><code>concert/concert.html</code></a></td>
</tr>
<tr data-site-guide-depth="2">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../concert/concert.html">集結水晶</a></span></td>
<td class="site-guide-nav-desc">巡迴場次、場館與曲目展開;搜尋曲目或場次。</td>
<td class="site-guide-nav-link-cell"><a
href="../concert/concert.html"><code>concert/concert.html</code></a></td>
</tr>
<tr data-site-guide-depth="2">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><span
class="site-guide-nav-nolink">隊長的氣場</span></span></td>
<td class="site-guide-nav-desc"></td>
<td class="site-guide-nav-link-cell"><span class="site-guide-nav-nolink"></span></td>
</tr>
<tr data-site-guide-depth="1">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../albums/albums.html">黃色留聲機</a></span></td>
<td class="site-guide-nav-desc">導聽文案;新篇章與全盛期年表、深水連結與曲目表。</td>
<td class="site-guide-nav-link-cell"><a
href="../albums/albums.html"><code>albums/albums.html</code></a></td>
</tr>
<tr data-site-guide-depth="2">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../albums/albums.html#new">啟動新篇章</a></span></td>
<td class="site-guide-nav-desc">回歸後數位單曲/專輯敘事與代表曲目表。</td>
<td class="site-guide-nav-link-cell"><a
href="../albums/albums.html#new"><code>albums/albums.html#new</code></a></td>
</tr>
<tr data-site-guide-depth="2">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../albums/albums.html#classic">輝煌全盛期</a></span></td>
<td class="site-guide-nav-desc">1997–1999 全盛期年表、專輯列與主打記憶整理。</td>
<td class="site-guide-nav-link-cell"><a
href="../albums/albums.html#classic"><code>albums/albums.html#classic</code></a>
</td>
</tr>
<tr data-site-guide-depth="1">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../yellow-note/map.html">Yellow Note</a></span></td>
<td class="site-guide-nav-desc">台灣行程表、成員行程篩選、搜尋與地圖主體。</td>
<td class="site-guide-nav-link-cell"><a
href="../yellow-note/map.html"><code>yellow-note/map.html</code></a></td>
</tr>
<tr data-site-guide-depth="2">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../yellow-note/minister.html">視覺進化論</a></span></td>
<td class="site-guide-nav-desc">「從好奇到全端」自述與時間軸式章節段落。</td>
<td class="site-guide-nav-link-cell"><a
href="../yellow-note/minister.html"><code>yellow-note/minister.html</code></a></td>
</tr>
<tr data-site-guide-depth="2">
<td class="site-guide-nav-menu-cell"><span class="site-guide-nav-name"><a
href="../yellow-note/map.html">聖地巡禮</a></span></td>
<td class="site-guide-nav-desc">台灣行程表、成員行程篩選、搜尋與地圖主體。</td>
<td class="site-guide-nav-link-cell"><a
href="../yellow-note/map.html"><code>yellow-note/map.html</code></a></td>
</tr>
</tbody>
</table>
<ul class="site-guide-mindmap">
<li data-site-guide-depth="1">
<span class="sg-node"><a href="../index.html">淪陷瞬間</a></span>
<ul>
<li data-site-guide-depth="2">
<span class="sg-node sg-node--nolink">傳奇的轉折(六六歌2</span>
<ul>
<li data-site-guide-depth="3"><span class="sg-node"><a href="../totoga2/timeline.html">時間軸</a></span></li>
<li data-site-guide-depth="3"><span class="sg-node"><a href="../totoga2/ebook.html">電子書</a></span></li>
<li data-site-guide-depth="3">
<span class="sg-node"><a href="../totoga2/readable.html">圖文好讀版</a></span>
<ul>
<li data-site-guide-depth="4"><span class="sg-node sg-node--extra"><a href="../totoga2/readable%20copy.html">複本(草稿)</a></span></li>
<li data-site-guide-depth="4"><span class="sg-node sg-node--extra"><a href="../totoga2/readable%20copy%202.html">複本 2(草稿)</a></span></li>
<li data-site-guide-depth="4"><span class="sg-node sg-node--extra" title="#challenge 無限挑戰・#ssg2 六六歌2・#sechskies-profile 水晶男孩・#gather 召集・#negotiate 交涉・#variables 變數・#surprise 驚喜・#final-stage 登場">章節錨點(8,滑鼠移入看清單)</span></li>
</ul>
</li>
<li data-site-guide-depth="3"><span class="sg-node"><a href="../totoga2/video.html">影片</a></span></li>
</ul>
</li>
<li data-site-guide-depth="2">
<span class="sg-node sg-node--extra">首頁區塊錨點:<code>#start</code>·<code>#mission</code>·<code>#timeline</code><code>#story</code> 未開放)</span>
</li>
<li data-site-guide-depth="2">
<span class="sg-node"><a href="../variety/bangxiao.html">世紀級寶藏</a></span>
<ul>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra">頁內「爆笑養老院」甜蜜負擔計量器區塊(同頁,非另一個檔案)</span></li>
<li data-site-guide-depth="3">
<span class="sg-node sg-node--extra">variety/ 其他相關頁(不在頂欄)</span>
<ul>
<li data-site-guide-depth="4"><span class="sg-node sg-node--extra"><a href="../variety/variety.html#variety">綜藝主頁・隊長帶弟弟</a></span></li>
<li data-site-guide-depth="4"><span class="sg-node sg-node--extra"><a href="../variety/variety-shows.html">節目存檔表</a></span></li>
<li data-site-guide-depth="4">
<span class="sg-node sg-node--extra sg-node--nolink">各自暴走</span>
<ul>
<li data-site-guide-depth="4"><span class="sg-node sg-node--extra"><a href="../variety/NJTW/NJTW5.html">新西遊記</a></span></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li data-site-guide-depth="1">
<span class="sg-node"><a href="../member/member.html">完整的六顆</a></span>
<ul>
<li data-site-guide-depth="2"><span class="sg-node sg-node--extra">錨點:<a href="../member/member.html#crystal-board"><code>#crystal-board</code></a></span></li>
</ul>
</li>
<li data-site-guide-depth="1">
<span class="sg-node"><a href="../concert/concert.html">現場的震撼</a></span>
<ul>
<li data-site-guide-depth="2">
<span class="sg-node"><a href="../concert/concert.html">集結水晶</a></span>
<ul>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra">錨點:<code>#stage-now</code>·<code>#stage-past</code></span></li>
</ul>
</li>
<li data-site-guide-depth="2"><span class="sg-node sg-node--nolink">隊長的氣場(固定籌備中)</span></li>
</ul>
</li>
<li data-site-guide-depth="1">
<span class="sg-node"><a href="../albums/albums.html">黃色留聲機</a></span>
<ul>
<li data-site-guide-depth="2"><span class="sg-node"><a href="../albums/albums.html#new">啟動新篇章</a></span></li>
<li data-site-guide-depth="2"><span class="sg-node"><a href="../albums/albums.html#classic">輝煌全盛期</a></span></li>
<li data-site-guide-depth="2"><span class="sg-node sg-node--extra"><a href="lyrics.html">歌詞(未列頂欄)</a></span></li>
</ul>
</li>
<li data-site-guide-depth="1">
<span class="sg-node"><a href="../yellow-note/map.html">Yellow Note</a></span>
<ul>
<li data-site-guide-depth="2">
<span class="sg-node"><a href="../yellow-note/minister.html">視覺進化論</a></span>
<ul>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="minister_ge.html">minister 長版備份(未列頂欄)</a></span></li>
</ul>
</li>
<li data-site-guide-depth="2">
<span class="sg-node"><a href="../yellow-note/map.html">聖地巡禮</a></span>
<ul>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra">錨點:<code>#taipei</code>·<code>#global</code></span></li>
</ul>
</li>
</ul>
</li>
<li data-site-guide-depth="1">
<span class="sg-node sg-node--extra sg-node--nolink">站內其他頁面(草稿/作業/備查,不在頂欄)</span>
<ul>
<li data-site-guide-depth="2">
<span class="sg-node sg-node--extra sg-node--nolink">extra/ 草稿與作業</span>
<ul>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="site-guide.html">site-guide.html(本頁)</a></span></li>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="grok.html">grok.html(實驗頁)</a></span></li>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="Untitled-1.html">Untitled-1.html(草稿)</a></span></li>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="sunghoon.html">sunghoon.html</a></span></li>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="hw-bootstrap.html">hw-bootstrap.html(學校作業)</a></span></li>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="bootstrap-navbar-carousel.html">bootstrap-navbar-carousel.html(學校作業)</a></span></li>
</ul>
</li>
<li data-site-guide-depth="2">
<span class="sg-node sg-node--extra sg-node--nolink">站根與其他資料夾雜項</span>
<ul>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="../fansform.html">fansform.html(入坑申請轉址)</a></span></li>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="../index-bootstrap.html">index-bootstrap.html(首頁改版草稿)</a></span></li>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="../s20260612_04_jq_calculator.html">s20260612_04_jq_calculator.html(學校作業)</a></span></li>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="../sechskies_brand_color_spec.html">sechskies_brand_color_spec.html(品牌色規範)</a></span></li>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="../totoga2/readable_ebookcontent.html">totoga2/readable_ebookcontent.html(電子書內容草稿)</a></span></li>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="../yellow-note/crystal-dance-survey.html">yellow-note/crystal-dance-survey.html(問卷頁)</a></span></li>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="../variety/vedio-list.html">variety/vedio-list.html(深夜清單,追劇進度預覽頁)</a></span></li>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra" title="admin/api/back 等 PHP 後台,非靜態站主線">variety/vedio-list/(追劇清單後台練習,非本站主線)</span></li>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="../yellow-note/practice-room.html">yellow-note/practice-room.html(水晶練習室,舞蹈練習播放器)</a></span></li>
</ul>
</li>
<li data-site-guide-depth="2"><span class="sg-node sg-node--extra"><a href="../sechskies-admin-hw/index.html">sechskies-admin-hw/(另一份學校作業,非本站,與上方水晶練習室頁面無關)</a></span></li>
<li data-site-guide-depth="2">
<span class="sg-node sg-node--extra sg-node--nolink" title="2026-05-16 15:56 備份,基準 commit fa8b04d,內容為當時尚未提交的修改與未追蹤新檔">待確認/(舊備份快照,滑鼠移入看說明)</span>
<ul>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="../待確認/index.html">待確認/index.html</a></span></li>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="../待確認/extra/site-guide.html">待確認/extra/site-guide.html</a></span></li>
<li data-site-guide-depth="3"><span class="sg-node sg-node--extra"><a href="../待確認/.cursor/_tmp-index-2cd.html">待確認/.cursor/_tmp-index-2cd.html</a></span></li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
<h3 class="site-guide-subh" id="sg-rules-quick">一般頁籌備中(掃一眼)</h3>
@@ -311,230 +206,30 @@
</tbody>
</table>
</div>
<p class="site-guide-fine-print site-guide-fine-print--center">綜藝主頁:<a
href="../variety/variety.html">variety.html</a></p>
</section>
<section class="site-guide-group" aria-labelledby="sg-nav-origin">
<h2 class="section-title" id="sg-nav-origin">淪陷瞬間 <span class="site-guide-nav-sub">站根</span></h2>
<section class="site-guide-group site-guide-group--nav-hero" aria-labelledby="sg-git-notes">
<h2 class="section-title" id="sg-git-notes">Git 同步備忘</h2>
<div class="site-guide-nav-table-wrap">
<table class="site-guide-mini-table">
<thead>
<tr>
<th scope="col">頁/錨點</th>
<th scope="col">用途</th>
</tr>
</thead>
<table class="site-guide-mini-table" aria-describedby="sg-git-notes">
<tbody>
<tr>
<td><a href="../index.html">index.html</a></td>
<td>首頁 Hero</td>
<th scope="row">2026-07-30 分岔</th>
<td>本機原有 4 個未 push commit<code>variety/vedio-list.html</code> 深夜清單、<code>concert/captain-aura.html</code><code>yellow-note/practice-room.html</code> 等),同時遠端 origin/main 多了 1 個新 commit<code>calendar2026.html</code> 2026 行事曆頁+<code>admin/</code> 後台系統,改動 <code>assets/js/map.js</code><code>member/member.html</code></td>
</tr>
<tr>
<td><a href="../index.html#start">#start</a></td>
<td>區塊錨點(<code>#story</code> 不在頂欄白名單)</td>
<th scope="row">衝突檔案</th>
<td><code>AGENTS.md</code>(工作狀態記錄不同)、<code>.vscode/settings.json</code>liveServer port 5501 vs 5503);已採「AGENTS.md 用遠端較新版本」「port 改 5503」解決並完成合併 commit</td>
</tr>
<tr>
<td><a href="../totoga2/timeline.html">時間軸</a></td>
<td>平行單頁 · <code>totoga2/timeline.html</code></td>
</tr>
<tr>
<td><a href="../totoga2/ebook.html">電子書</a></td>
<td>平行單頁 · <code>totoga2/ebook.html</code></td>
</tr>
<tr>
<td><a href="../totoga2/readable.html">圖文好讀版</a></td>
<td>整合長頁 · <code>totoga2/readable.html</code></td>
</tr>
<tr>
<td><a href="../totoga2/readable%20copy.html">圖文好讀版 · 複本</a></td>
<td>草稿複本 · <code>readable copy.html</code><code>readable copy 2.html</code></td>
</tr>
<tr>
<td><a href="../totoga2/video.html">影片</a></td>
<td>平行單頁 · <code>totoga2/video.html</code></td>
</tr>
</tbody>
</table>
</div>
<div class="site-guide-grid site-guide-grid--compact">
<article class="site-guide-card site-guide-card--totoga2">
<h3><a href="../totoga2/readable.html">六六歌2 · 四入口</a></h3>
<p class="site-guide-note">章節錨點僅在圖文好讀版(<code>totoga2/readable.html</code>)內。
</p>
<ul class="site-guide-inline-links">
<li><a href="../totoga2/timeline.html">時間軸</a></li>
<li><a href="../totoga2/ebook.html">電子書</a></li>
<li><a href="../totoga2/readable.html">圖文好讀版</a></li>
<li><a href="../totoga2/readable%20copy.html">複本</a></li>
<li><a href="../totoga2/readable%20copy%202.html">複本 2</a></li>
<li><a href="../totoga2/video.html">影片</a></li>
</ul>
<nav class="site-guide-chapter-nav" aria-label="totoga2 章節">
<a href="../totoga2/readable.html#challenge">無限挑戰</a>
<a href="../totoga2/readable.html#ssg2">六六歌2</a>
<a href="../totoga2/readable.html#sechskies-profile">水晶男孩</a>
<a href="../totoga2/readable.html#gather">召集</a>
<a href="../totoga2/readable.html#negotiate">交涉</a>
<a href="../totoga2/readable.html#variables">變數</a>
<a href="../totoga2/readable.html#surprise">驚喜</a>
<a href="../totoga2/readable.html#final-stage">登場</a>
</nav>
</article>
</div>
</section>
<section class="site-guide-group" aria-labelledby="sg-nav-member">
<h2 class="section-title" id="sg-nav-member">完整的六顆</h2>
<div class="site-guide-nav-table-wrap">
<table class="site-guide-mini-table">
<tbody>
<tr>
<th scope="row"></th>
<td><a href="../member/member.html">member/member.html</a></td>
</tr>
<tr>
<th scope="row">錨點</th>
<td><a href="../member/member.html#crystal-board">#crystal-board</a></td>
<th scope="row">目前狀態</th>
<td>本機領先 origin/main 5 個 commit(遠端 1 個+本機原本 4 個+合併 1 個),<strong>尚未 push 回 GitHub</strong><code>variety/NJTW/NJTW5.html</code> 另有 8 行未 commit 的修改,維持原樣待處理</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="site-guide-group" aria-labelledby="sg-nav-concert">
<h2 class="section-title" id="sg-nav-concert">現場的震撼</h2>
<div class="site-guide-nav-table-wrap">
<table class="site-guide-mini-table">
<tbody>
<tr>
<th scope="row">集結水晶</th>
<td><a href="../concert/concert.html">concert.html</a> · <a
href="../concert/concert.html#stage-now">#stage-now</a> · <a
href="../concert/concert.html#stage-past">#stage-past</a></td>
</tr>
<tr>
<th scope="row">隊長的氣場</th>
<td></td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="site-guide-group" aria-labelledby="sg-nav-albums">
<h2 class="section-title" id="sg-nav-albums">黃色留聲機</h2>
<div class="site-guide-nav-table-wrap">
<table class="site-guide-mini-table">
<tbody>
<tr>
<th scope="row">典藏</th>
<td><a href="../albums/albums.html">albums.html</a> · <a
href="../albums/albums.html#new">#new</a> · <a
href="../albums/albums.html#classic">#classic</a></td>
</tr>
<tr>
<th scope="row">歌詞</th>
<td><a href="lyrics.html">extra/lyrics.html</a>(未列主欄)</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="site-guide-group" aria-labelledby="sg-nav-yellow-note">
<h2 class="section-title" id="sg-nav-yellow-note">Yellow Note</h2>
<div class="site-guide-nav-table-wrap">
<table class="site-guide-mini-table">
<tbody>
<tr>
<th scope="row">視覺</th>
<td><a href="../yellow-note/minister.html">yellow-note/minister.html</a></td>
</tr>
<tr>
<th scope="row">minister 長版</th>
<td><a href="minister_ge.html">minister_ge.html</a>(備份;未列頂欄)</td>
</tr>
<tr>
<th scope="row">地圖</th>
<td><a href="../yellow-note/map.html">map.html</a> · <a
href="../yellow-note/map.html#taipei">#taipei</a> · <a
href="../yellow-note/map.html#global">#global</a></td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="site-guide-group" aria-labelledby="sg-nav-variety">
<h2 class="section-title" id="sg-nav-variety">variety/(無主欄)</h2>
<div class="site-guide-nav-table-wrap">
<table class="site-guide-mini-table">
<tbody>
<tr>
<th scope="row">主頁</th>
<td><a href="../variety/variety.html">variety.html</a> · <a
href="../variety/variety.html#variety">#variety</a></td>
</tr>
<tr>
<th scope="row">世紀級寶藏</th>
<td><a href="../variety/bangxiao.html">bangxiao.html</a></td>
</tr>
<tr>
<th scope="row">節目存檔表</th>
<td><a href="../variety/variety-shows.html">variety-shows.html</a> · <a
href="../variety/variety-shows.html#variety-shows">#variety-shows</a> · <a
href="../variety/variety-shows.html#captain-brothers">#captain-brothers</a> · <a
href="../variety/variety-shows.html#each-rampage">#each-rampage</a></td>
</tr>
<tr>
<th scope="row">NJTW5</th>
<td><a href="../variety/NJTW/NJTW5.html">NJTW5.html</a></td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="site-guide-group site-guide-group--extra" aria-labelledby="sg-nav-extra">
<h2 class="section-title" id="sg-nav-extra">extra/ 其餘</h2>
<p class="site-guide-folder-hint">未列頂欄主模板之草稿/實驗頁。主選單對應頁面已移至
<code>member/</code><code>yellow-note/</code><code>variety/</code> 等資料夾;六六歌2 圖文好讀版之草稿複本在
<code>totoga2/readable copy*.html</code>(主選單樹第四層與正式版並列)。
</p>
<div class="site-guide-nav-table-wrap">
<table class="site-guide-mini-table">
<thead>
<tr>
<th scope="col">檔案</th>
<th scope="col">備註</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="site-guide.html">site-guide.html</a></td>
<td>全站導覽(本頁)</td>
</tr>
<tr>
<td><a href="grok.html">grok.html</a></td>
<td>實驗單頁</td>
</tr>
<tr>
<td><a href="Untitled-1.html">Untitled-1.html</a></td>
<td>草稿</td>
</tr>
<tr>
<td><a href="sunghoon.html">sunghoon.html</a></td>
<td></td>
</tr>
<tr>
<td><a href="minister_ge.html">minister_ge.html</a></td>
<td>minister 長版備份(Yellow Note 小節亦列)</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
<footer>
+410
View File
@@ -0,0 +1,410 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SECHSKIES - 水晶男孩傳奇介紹</title>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="assets/css/index.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="assets/css/tablet.css">
<style>
/* ── Bootstrap 疊加後的還原保護:Bootstrap Reboot 會重設 body/標題/段落的邊距與底色,
這裡把原本 style.css 的數值原封不動疊回來,確保外觀跟改版前一致 ── */
body.home-page {
background-color: var(--color-bg);
color: var(--color-text);
height: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
font-family: 'Noto Sans TC', system-ui, sans-serif;
line-height: 1.6;
}
/* 刻意用純標籤選擇器(不加 .home-page class):
優先權只需贏過 Bootstrap Reboot 的同層級標籤選擇器即可(載入順序在後即贏),
不能贏過 .section-title 這類 class 選擇器——才不會蓋掉個別標題本來就設定好的間距 */
h1,
h2,
h3,
h4,
h5,
h6,
p,
ul,
ol,
dl,
dd,
blockquote,
hr {
margin: 0;
padding: 0;
}
/* 同理:Bootstrap 也用純標籤選擇器 a{color:...;text-decoration:underline}
跟 style.css 的 a{color:var(--color-text);text-decoration:none} 同量級、
但載入在後會贏,把沒有專屬 class 的連結(例如頁尾 .studio-link)變成
Bootstrap 預設的藍色底線連結——這裡疊回原本數值 */
a {
color: var(--color-text);
text-decoration: none;
}
/* ── 現有 portal-nav 疊上 Bootstrap navbar / navbar-expand-lg / navbar-dark class 後的還原保護:
Bootstrap 的 .navbar{display:flex...} 沒有寫在 media query 裡、卻跟 style.css
原本桌機/手機兩種 .portal-nav 版面同量級,且整包 bootstrap.min.css 載入在後,
兩種寬度都會被它蓋掉——這是手機漢堡版面失效的原因。這裡照原本的中斷點分別疊回去。
內層 .portal-nav>ul、.portal-nav>ul>li>a 這些選擇器本來就贏過 Bootstrap 的
.navbar-nav.nav-link(多一層 combinator,優先權本來就比較高),不需要額外處理,
上一版加的 #portal-primary-menu id 選擇器優先權太高,反而蓋掉手機版的收合邏輯,已移除。── */
body.home-page .portal-nav {
width: 100%;
margin: 0 0 14px;
padding: 0.8rem 100px;
background-color: var(--color-bg-raised);
border-radius: 0;
box-shadow: none;
overflow: visible;
position: sticky;
top: 0;
z-index: 100;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem 1rem;
box-sizing: border-box;
}
@media (max-width: 991px) {
body.home-page .portal-nav {
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas:
"logo toggle"
"menu menu";
align-items: center;
column-gap: 0.75rem;
row-gap: 0;
}
body.home-page .portal-nav.portal-nav--open {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
height: 100dvh;
margin: 0;
padding:
max(0.85rem, env(safe-area-inset-top, 0px)) clamp(14px, 2vw + 10px, 32px) max(1rem, env(safe-area-inset-bottom, 0px));
z-index: 10000;
overflow: hidden;
background-color: var(--color-bg-raised);
grid-template-columns: 1fr;
grid-template-rows: auto minmax(0, 1fr);
grid-template-areas:
"top"
"menu";
row-gap: 1.75rem;
align-content: start;
box-sizing: border-box;
}
}
/* ── 新增:跨時空軌跡段落快覽(錨點導覽用,低調文字列,不做按鈕感) ── */
.home-page .story-archive-nav-wrap {
margin: 0 auto 2rem;
max-width: 720px;
}
.home-page .story-archive-nav-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 4px 0;
}
.home-page .story-archive-year {
display: inline-block;
padding: 4px 14px;
color: var(--color-text-muted);
font-size: 0.85rem;
letter-spacing: 1px;
text-decoration: none;
border-right: 1px solid rgba(255, 204, 0, 0.3);
transition: color 0.2s ease;
}
.home-page .story-archive-year:last-child {
border-right: none;
}
.home-page .story-archive-year:hover {
color: var(--color-brand);
}
.home-page .story-node {
scroll-margin-top: var(--home-nav-offset, 84px);
}
</style>
</head>
<body class="home-page">
<header id="origin">
<div class="hero">
<div class="header-inner site-header-inner">
<h1 class="main-title">SECHSKIES<br>水晶男孩</h1>
<div class="subtitle-wrap">
<span class="sub-title">六顆永恆的水晶 • 左右命運的傳奇</span>
</div>
</div>
<!-- <a href="#start" class="btn">從這裡開始淪陷</a> -->
</div>
</header>
<nav class="portal-nav navbar navbar-expand-lg navbar-dark" aria-label="水晶男孩推廣部導覽" data-portal-nav></nav>
<canvas id="particles"></canvas>
<div class="container">
<section id="start">
<h2 class="section-title">好奇的開端</h2>
<div class="intro-text">
<p class="core-description">
這段旅程始於 2016 年 4 月,透過國民綜藝<span class="mark_b mark_y">《無限挑戰》</span>的回歸企劃,讓原本不認識這組傳奇的我,因一時好奇點開節目。
即便錯過了那個屬於他們的世紀末,但跟著節目的編排鋪陳,從歡笑到感動,我被這些熱血、真誠且專業的大男孩所震撼。
</p>
<p class="call-to-action">
身為在回歸後才被這場 <span class="mark_b mark_y">「黃色奇蹟」</span>
擊中的新一代粉絲,我深刻體會那種從好奇觀看到最終徹底淪陷的悸動。這種從<span class="mark_b"><span class="fan-journey-wave"
role="text">路人&nbsp;<i class="fa-solid fa-circle-right"
aria-hidden="true"></i>&nbsp;入坑否定期&nbsp;<i class="fa-solid fa-circle-right"
aria-hidden="true"></i>&nbsp;瘋狂考古期&nbsp;<i class="fa-solid fa-circle-right"
aria-hidden="true"></i>&nbsp;坑底躺平深陷其中</span></span>的過程,證明了魅力可以跨越時間,也正是這一切自我覺醒的起點,驅動我不斷進化的無限可能。
</p>
</div>
</section>
<section id="mission" class="mission-section">
<div class="mission-zigzag">
<div class="mission-row mission-row--intro">
<div class="mission-row__visual">
<div class="logo-wrapper">
<img src="./assets/images/logo.png" alt="SECHSKIES Logo Concept">
<div class="caption">
<span class="caption__line caption__line--title"><span class="arrow-icon mark_b"
aria-hidden="true">↑</span>設計概念:</span>
<span class="caption__line">六角形水晶、黃色海洋與我們的初心Six</span>
</div>
</div>
</div>
<div class="mission-row__body">
<h2 class="section-title mission-block-title">成立的初衷</h2>
<p class="mission-lead">
以推廣 <span class="mark_y">SECHSKIES 水晶男孩</span> 為核心,旨在透過 專業的視覺策展 宣傳成員魅力與小黃們的應援活動。嘗試以水晶男孩為
<span class="mark_b">靈感主軸</span>,延伸至 網頁開發、視覺設計與內容企劃 等多重領域,將對偶像的喜愛轉化為 <span
class="mark_b">實質的專業養分</span>。在這裡,你不只能探索他們的 <span class="mark_b">傳奇軌跡</span>,更能見證追星如何激發
<span class="mark_b">創作潛能</span>
</p>
<div class="slogan-quote slogan-quote--mission">
<span class="slogan-quote__inner">我要用這個<span
class="mark_b">網站</span>證明:<br>追星的孩子不僅不會變壞,更會因為那份<span
class="mark_b">守護並推廣的熱情</span><br>讓現在的自己變得比追星前的自己還要厲害!</span>
</div>
</div>
</div>
<div class="mission-row mission-row--concept">
<div class="mission-row__body">
<h3 class="mission-logo-heading">LOGO 設計理念</h3>
<p>
<span class="mark_b">「晶」</span>字發想來自<span class="mark_b">六角形的水晶</span>,為了強調水晶男孩<span
class="mark_b">初心為六人</span>。上半部形似<span class="mark_b">數字
6</span>,搭配演唱會必備的<span class="mark_b">手燈圖案</span>,代表粉絲和水晶<span
class="mark_b">密不可分的關係</span><br>色彩配置發想來自無限挑戰六六歌特輯的回歸舞台,成員脫下眼罩的剎那,整個場地從<span
class="mark_b mark_y">一片漆黑瞬間發亮</span>,映入眼簾的是一片<span class="mark_b">黃色海洋</span><span
class="mark_y">讓成員和粉絲的世界跟著亮了起來</span>。以此片段為契機,設計成<span
class="mark_b">黑色系與黃色系</span>的色彩搭配,來記錄那瞬間的感動。
</p>
</div>
<div class="mission-row__visual">
<div class="mission-moment-wrap">
<div class="mission-gif-frame" role="group" aria-label="回歸演唱會/黃色氣球海(16:9)">
<!-- 若改為 GIF 動圖,將底下 src 換成 ./assets/images/mission-balloon-sea.gif 即可 -->
<img src="./assets/images/黃色海洋.gif" alt="無限挑戰 六六歌2 放送畫面:全場黃色氣球海">
</div>
<span class="caption">
<span class="arrow-icon" aria-hidden="true"></span>
回歸演唱會的動人瞬間-黃色氣球海
</span>
</div>
</div>
</div>
</div>
</section>
<section id="timeline" class="story-archive">
<h2 class="section-title">跨時空軌跡</h2>
<div class="subtitle-wrap">
<span class="sub-title">SECHSKIES STORY ARCHIVE</span>
</div>
<div class="container-fluid story-archive-nav-wrap">
<div class="story-archive-nav-list">
<a class="story-archive-year" href="#era-1997">傳奇誕生</a>
<a class="story-archive-year" href="#era-2000">凍結世界</a>
<a class="story-archive-year" href="#era-2016">完整回歸</a>
<a class="story-archive-year" href="#era-2016-05">夢境成真</a>
<a class="story-archive-year" href="#era-2016-09">九月首演</a>
<a class="story-archive-year" href="#era-2016-12">溫柔陪伴</a>
<a class="story-archive-year" href="#era-tbc">未完待續</a>
</div>
</div>
<div class="merged-storyline">
<article id="era-1997" class="story-node" data-era="1997">
<h3>傳奇的誕生:六道光芒劃破韓流夜空</h3>
<p>1997年4月15日,水晶男孩以強悍的《學園別曲》為起點,「黑水晶|Rap與舞蹈」、「白水晶|主唱」的雙色魅力交織,奠定了
K-POP第一代傳奇的地位,開創了屬於黃色海洋的盛世與巔峰。</p>
<p>在那段被韓劇<a
href="https://www.disneyplus.com/browse/entity-3979fb82-8681-44e8-b7be-e313091f0d73?distributionPartner=google"
class="story-external-inline" target="_blank" rel="noopener noreferrer"><span
class="story-external-inline__label">《請回答1997》</span><i
class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i></a>奉為經典的歲月裡,水晶男孩與
H.O.T.
並稱為兩大巔峰。無論是雨中那場被劇集致敬的粉絲大戰,還是為了買票露宿街頭的狂熱,白與黃的旗幟對決,成了那個世代集體記憶的起點。</p>
</article>
<article id="era-2000" class="story-node" data-era="2000">
<h3>遺憾的休止符:凍結有你的世界</h3>
<p>在最燦爛的時刻,這首華麗的樂章卻被迫畫下了休止符,2000年5月正式解散。成員各自發展走出了不同道路,水晶進入休眠期。
</p>
<p>那一場如同劇中描寫般、讓全韓無數少女世界崩塌的夢想演唱會告別現場,將未完的旋律封存在冷冽的空氣中。這一停頓,就是長達十六年的靜默,成為我們心中最完整、卻也最令人心碎的凝煉模樣。
</p>
</article>
<article id="era-2016" class="story-node story-node--ebook-ssg2" data-era="2016">
<h3>感動合體:那一晚,我們找回了完整</h3>
<p>塵封十六年的時光被溫柔拂去,跨越六千多天的等待,黃色氣球海再次淹沒了上岩運動場。當六個身影重新站在一起,那幅曾被凍結在 2000
年的畫面終於再次「溶解」並重合。那一晚,淚水證明了奇蹟真的存在,我們終於找回了那份久違的完整,以為故事從此能圓滿續寫。</p>
<span class="timeline-ebook-cta" aria-label="傳奇書寫中,連結暫停">
<span>點我見證傳奇</span><i class="fa-solid fa-hand-pointer" aria-hidden="true"></i>
</span>
</article>
<article id="era-2016-05" class="story-node" data-era="2016.05">
<h3>從路人到鐵粉:從夢境進現實</h3>
<p>當《無限挑戰》的燈光熄滅,原以為那場重逢只是曇花一現。當我們陷入瘋狂但卻不敢過度期待合體、只在螢幕前等待每個oppa消息的時光,終於盼到了 5月 11
日與 YG的簽約消息,所有新舊小黃從絕望中打醒。從未知的空虛到塵埃落定的感動——原來這不是終點,奇蹟真的在現實生了根。</p>
</article>
<article id="era-2016-09" class="story-node" data-era="2016.09">
<h3>九月首場:黃色海洋裡的羈絆</h3>
<p>四月的重逢是老黃們守候十六年的淚水;而九月的單獨演唱會,則讓無數新小黃見證了那份無法動搖的羈絆。在那片黃色海洋裡,我們不只愛上了水晶男孩,更愛上了他們與小黃之間的愛情。雖然遺憾沒能親眼看見六人並肩的畫面,但那股新舊交織的生命力,確實為水晶注入了新的靈魂。
</p>
</article>
<article id="era-2016-12" class="story-node" data-era="2016.12">
<h3>第六顆水晶:另一種形式的陪伴</h3>
<p>而志溶雖然未參與合體,卻在十二月聖誕夜那場釜山演唱會落幕當天,帶著孩子在《超人回來了》的預告驚喜現身。這不是告別,而是他選擇以另一種最真實、最溫馨的形式,在現實生活中繼續陪伴著大家。舞台上的空位雖然令人難過,但看到他帶著孩子幸福的模樣,我們才明白:原來水晶與粉絲的羈絆,早已超越了表演的定義,而是要在彼此的生活裡,一起寫下新的篇章。
</p>
</article>
<article id="era-tbc" class="story-node story-node--tbc" data-era="未完">
<h3>未完待續</h3>
</article>
</div>
</section>
<!--
<section id="story" class="story-archive">
<h2 class="section-title">傳奇的轉折</h2>
<div class="subtitle-wrap">
<span class="sub-title">PLOT TWIST</span>
</div>
<p class="intro-text">
像劇情的反轉一樣過癮:這幾個節點,剛好是許多人從「旁觀綜藝」滑進「正式入坑」的轉軌。若想讀對白級細節,請開
<a href="totoga2/readable.html">六六歌2 整合專區</a>(影片/電子書/紀實長文八章,同一頁切換模式即可)。
</p>
<div class="story-ebook-cta">
<a href="totoga2/readable.html#ebook" class="btn story-ebook-btn">開啟六六歌2 紀實電子書</a>
<a href="totoga2/readable.html" class="btn story-ebook-btn">六六歌2 整合專區</a>
</div>
<div class="merged-storyline">
<article id="special" class="story-node" data-era="六六歌">
<h3>六六歌實錄</h3>
<p>把懸念、試探與哭點縫在同一條時間軸上;很多人第一次真正把「水晶男孩」打進搜尋框,就是從這種瘋狂的剪輯節奏開始。</p>
<p><a href="totoga2/readable.html#ssg2">對照這一段幕後敘事</a></p>
</article>
<article id="negotiate" class="story-node" data-era="交涉">
<h3>命運的交涉</h3>
<p>KTV、游擊演唱會規則、與其說講道理不如說把真心擺上桌面——交涉的不是合約字面,而是還願不願意再一次肩並肩。</p>
<p><a href="totoga2/readable.html#negotiate">讀交涉與對話肌理</a></p>
</article>
<article id="complete" class="story-node" data-era="歸位">
<h3>拼圖的歸位</h3>
<p>變數一個個被排除之後,驚喜接上登場段落;缺一角舞台的視覺,終於對齊觀眾席上堅守的那片黃。</p>
<p><a href="totoga2/readable.html#gather">從「召集」段開始補這條線</a></p>
</article>
<article id="past" class="story-node" data-era="最初">
<h3>巔峰與雨天</h3>
<p>急凍、告別、各自的跑道——並不是要你沉浸遺憾,而是讓你懂:後來每一次亮燈,為什麼會更值得尖叫。</p>
<p><a href="totoga2/readable.html#sechskies-profile">讀這條脈絡的起點</a></p>
</article>
</div>
</section> -->
</div>
<footer>
<p>SECHSKIES Fanpage &copy; 2026 - 黃色氣球永遠飄揚</p>
<a href="https://jekkinoopy.github.io/Portfolio/" class="studio-link" target="_blank">
Design by <span class="mark_b">Jekkinoopy Studio</span>
</a>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/particles.js"></script>
<script src="assets/js/portal-nav.js"></script>
<script>
/* portal-nav.js 產生選單後,疊上 Bootstrap navbar-nav / nav-item / nav-link class
只加 class,不動原本結構與行為,不碰 assets/js/portal-nav.js 本體 */
(function () {
var menu = document.getElementById('portal-primary-menu');
if (!menu) return;
menu.classList.add('navbar-nav');
Array.from(menu.children).forEach(function (li) {
li.classList.add('nav-item');
var link = li.querySelector(':scope > a');
if (link) link.classList.add('nav-link');
});
})();
/* 錨點跳轉距離:實際量測 sticky nav 的高度(桌機/手機漢堡展開高度不同),
寫進 --home-nav-offset,取代猜測值,避免跳轉後蓋住或跳過頭。
另外 .story-node::after(年代標籤)用絕對定位貼在卡片「上緣外側」
style.css:≥1181px top:-20px、≤900px top:-14px),
這段浮出去的高度不算在 story-node 自己的框裡,
所以量完 nav 高度後要再加一點緩衝,才不會讓標籤被 nav 蓋住。 */
(function () {
var nav = document.querySelector('.portal-nav');
if (!nav) return;
var ERA_TAG_CLEARANCE = 24; // 年代標籤外凸的緩衝空間
function syncNavOffset() {
var height = Math.ceil(nav.getBoundingClientRect().height);
document.documentElement.style.setProperty('--home-nav-offset', (height + ERA_TAG_CLEARANCE) + 'px');
}
syncNavOffset();
window.addEventListener('resize', syncNavOffset);
window.addEventListener('load', syncNavOffset);
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(syncNavOffset);
}
})();
</script>
</body>
</html>
+2 -2
View File
@@ -99,7 +99,7 @@
<div class="member-poster__wanted">WHITE</div>
<div class="member-poster__sheet">
<div class="member-poster__photo">
<img src="../assets/images/member/92599-502183.jpg" alt="姜成勳肖像" width="280" height="340" loading="lazy">
<img src="../assets/images/member/kpoplover082006___Bsc2U5lhXuK___.jpg" alt="姜成勳肖像" width="280" height="340" loading="lazy">
</div>
<p class="member-poster__role">Main Vocal</p>
<h2 class="member-poster__name" id="poster-ksh">姜成勳</h2>
@@ -114,7 +114,7 @@
<div class="member-poster__wanted">WHITE</div>
<div class="member-poster__sheet">
<div class="member-poster__photo">
<img src="../assets/images/member/noona7273___Bt7EZHRAGgs___.jpg" alt="高志溶肖像" width="280" height="340" loading="lazy">
<img src="../assets/images/member/92599-502183.jpg" alt="高志溶肖像" width="280" height="340" loading="lazy">
</div>
<p class="member-poster__role">Vocal</p>
<h2 class="member-poster__name" id="poster-kjy">高志溶</h2>
+28
View File
@@ -0,0 +1,28 @@
@echo off
chcp 65001 >nul
cd /d "%~dp0"
echo.
echo ═══════════════════════════════════════
echo 水晶男孩推廣部 — 本地預覽
echo 網址:http://127.0.0.1:5501
echo 關閉此視窗 = 停止預覽
echo ═══════════════════════════════════════
echo.
where py >nul 2>&1
if %errorlevel%==0 (
start "" "http://127.0.0.1:5501/index.html"
py -m http.server 5501
goto :eof
)
where python >nul 2>&1
if %errorlevel%==0 (
start "" "http://127.0.0.1:5501/index.html"
python -m http.server 5501
goto :eof
)
echo [錯誤] 找不到 Python。請安裝 Python 後再試,或使用 CursorCtrl+Shift+P →「預覽網站」
pause
+133
View File
@@ -0,0 +1,133 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>後台首頁|水晶練習室 Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="../css/style.css" rel="stylesheet">
</head>
<body>
<!-- 後台上方 Navbar -->
<nav class="navbar navbar-expand navbar-dark sk-navbar">
<div class="container-fluid">
<span class="navbar-brand mb-0"><i class="bi bi-gem me-1"></i>水晶練習室 <span class="fw-light">Admin</span></span>
<div class="d-flex gap-2">
<a class="btn btn-brand btn-sm" href="../index.html">回前台</a>
<a class="btn btn-outline-brand btn-sm" href="../index.html">登出</a>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<!-- 左側管理選單 -->
<aside class="col-lg-2 border-end sk-admin-sidebar py-4 px-3 min-vh-100">
<nav class="nav flex-column">
<a class="nav-link active" href="index.html"><i class="bi bi-speedometer2 me-2"></i>後台首頁</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-people me-2"></i>練習生管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-mortarboard me-2"></i>課程管理</a>
<a class="nav-link" href="practices/index.html"><i class="bi bi-journal-text me-2"></i>練習紀錄管理</a>
<a class="nav-link" href="songs/index.html"><i class="bi bi-music-note-list me-2"></i>歌曲資料管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-list-task me-2"></i>訓練任務管理</a>
</nav>
</aside>
<!-- 主內容 -->
<main class="col-lg-10 py-4 px-4">
<h1 class="h3 mb-4 text-white">後台首頁</h1>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-xl-3">
<div class="card sk-card shadow-sm h-100 p-3">
<div class="card-body">
<i class="bi bi-people fs-3 text-white"></i>
<div class="text-body-secondary small mt-2 mb-1">練習生人數</div>
<div class="sk-stat-value">6</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card sk-card shadow-sm h-100 p-3">
<div class="card-body">
<i class="bi bi-lightning-charge fs-3 text-white"></i>
<div class="text-body-secondary small mt-2 mb-1">今日練習次數</div>
<div class="sk-stat-value">9</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card sk-card shadow-sm h-100 p-3">
<div class="card-body">
<i class="bi bi-mortarboard fs-3 text-white"></i>
<div class="text-body-secondary small mt-2 mb-1">課程數量</div>
<div class="sk-stat-value">8</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card sk-card shadow-sm h-100 p-3">
<div class="card-body">
<i class="bi bi-check2-circle fs-3 text-white"></i>
<div class="text-body-secondary small mt-2 mb-1">已完成任務數</div>
<div class="sk-stat-value">23</div>
</div>
</div>
</div>
</div>
<h2 class="sk-section-title"><i class="bi bi-journal-text"></i>最近練習紀錄</h2>
<div class="card sk-card sk-surface">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>練習生</th>
<th>日期</th>
<th>訓練類型</th>
<th>歌曲</th>
<th>練習時間</th>
<th>完成度</th>
</tr>
</thead>
<tbody>
<tr>
<td>努比</td>
<td>2026-07-16</td>
<td><span class="badge badge-type badge-type--dance">舞蹈</span></td>
<td>Road Fighter</td>
<td>40 分鐘</td>
<td>85%</td>
</tr>
<tr>
<td>昇材</td>
<td>2026-07-16</td>
<td><span class="badge badge-type badge-type--vocal">vocal</span></td>
<td>Couple</td>
<td>35 分鐘</td>
<td>78%</td>
</tr>
<tr>
<td>水院</td>
<td>2026-07-15</td>
<td><span class="badge badge-type badge-type--rap">Rap</span></td>
<td>Chivalry</td>
<td>25 分鐘</td>
<td>60%</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
@@ -0,0 +1,119 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>新增練習紀錄|水晶練習室 Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="../../css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand navbar-dark sk-navbar">
<div class="container-fluid">
<span class="navbar-brand mb-0"><i class="bi bi-gem me-1"></i>水晶練習室 <span class="fw-light">Admin</span></span>
<div class="d-flex gap-2">
<a class="btn btn-brand btn-sm" href="../../index.html">回前台</a>
<a class="btn btn-outline-brand btn-sm" href="../../index.html">登出</a>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<aside class="col-lg-2 border-end sk-admin-sidebar py-4 px-3 min-vh-100">
<nav class="nav flex-column">
<a class="nav-link" href="../index.html"><i class="bi bi-speedometer2 me-2"></i>後台首頁</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-people me-2"></i>練習生管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-mortarboard me-2"></i>課程管理</a>
<a class="nav-link active" href="index.html"><i class="bi bi-journal-text me-2"></i>練習紀錄管理</a>
<a class="nav-link" href="../songs/index.html"><i class="bi bi-music-note-list me-2"></i>歌曲資料管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-list-task me-2"></i>訓練任務管理</a>
</nav>
</aside>
<main class="col-lg-10 py-4 px-4">
<h1 class="h3 mb-4 text-white">新增練習紀錄</h1>
<div class="card sk-card sk-surface p-4" style="max-width: 42rem;">
<form>
<div class="row g-3">
<div class="col-md-6">
<label for="traineeName" class="form-label">練習生</label>
<select class="form-select" id="traineeName">
<option>努比</option>
<option>昇材</option>
<option>宰鎮</option>
<option>水院</option>
</select>
</div>
<div class="col-md-6">
<label for="practiceDate" class="form-label">日期</label>
<input type="date" class="form-control" id="practiceDate">
</div>
<div class="col-md-6">
<label for="practiceType" class="form-label">訓練類型</label>
<select class="form-select" id="practiceType">
<option>vocal</option>
<option>舞蹈</option>
<option>Rap</option>
<option>舞台表現</option>
<option>體能</option>
</select>
</div>
<div class="col-md-6">
<label for="practiceSong" class="form-label">歌曲</label>
<select class="form-select" id="practiceSong">
<option>Couple</option>
<option>Road Fighter</option>
<option>Com' Back</option>
<option>Chivalry</option>
<option>The Way This Guy Lives</option>
</select>
</div>
<div class="col-md-6">
<label for="practiceMinutes" class="form-label">練習時間(分鐘)</label>
<input type="number" class="form-control" id="practiceMinutes" placeholder="例如:40">
</div>
<div class="col-md-6">
<label for="practiceStatus" class="form-label">完成狀態</label>
<select class="form-select" id="practiceStatus">
<option>未開始</option>
<option>進行中</option>
<option>已完成</option>
</select>
</div>
<div class="col-12">
<label for="practiceCompletion" class="form-label d-flex justify-content-between">
<span>完成度</span>
<span class="text-body-secondary" id="completionValue">50%</span>
</label>
<input type="range" class="form-range" id="practiceCompletion" min="0" max="100" value="50"
oninput="document.getElementById('completionValue').textContent = this.value + '%'">
</div>
</div>
<div class="d-flex gap-2 mt-4">
<button type="submit" class="btn btn-brand">儲存</button>
<a class="btn btn-outline-brand" href="index.html">取消</a>
</div>
</form>
</div>
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
@@ -0,0 +1,68 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>刪除練習紀錄|水晶練習室 Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="../../css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand navbar-dark sk-navbar">
<div class="container-fluid">
<span class="navbar-brand mb-0"><i class="bi bi-gem me-1"></i>水晶練習室 <span class="fw-light">Admin</span></span>
<div class="d-flex gap-2">
<a class="btn btn-brand btn-sm" href="../../index.html">回前台</a>
<a class="btn btn-outline-brand btn-sm" href="../../index.html">登出</a>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<aside class="col-lg-2 border-end sk-admin-sidebar py-4 px-3 min-vh-100">
<nav class="nav flex-column">
<a class="nav-link" href="../index.html"><i class="bi bi-speedometer2 me-2"></i>後台首頁</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-people me-2"></i>練習生管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-mortarboard me-2"></i>課程管理</a>
<a class="nav-link active" href="index.html"><i class="bi bi-journal-text me-2"></i>練習紀錄管理</a>
<a class="nav-link" href="../songs/index.html"><i class="bi bi-music-note-list me-2"></i>歌曲資料管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-list-task me-2"></i>訓練任務管理</a>
</nav>
</aside>
<main class="col-lg-10 py-4 px-4">
<h1 class="h3 mb-4 text-white">刪除練習紀錄</h1>
<div class="card sk-card sk-surface p-4" style="max-width: 32rem;">
<div class="alert alert-danger mb-4" role="alert">
<i class="bi bi-exclamation-triangle-fill me-1"></i>確定要刪除這筆練習紀錄嗎?此動作無法復原。
</div>
<dl class="row mb-4">
<dt class="col-4 text-body-secondary">練習生</dt>
<dd class="col-8">努比</dd>
<dt class="col-4 text-body-secondary">日期</dt>
<dd class="col-8">2026-07-16</dd>
<dt class="col-4 text-body-secondary">訓練類型</dt>
<dd class="col-8">舞蹈</dd>
<dt class="col-4 text-body-secondary">歌曲</dt>
<dd class="col-8">Road Fighter</dd>
</dl>
<div class="d-flex gap-2">
<button type="button" class="btn btn-danger">確認刪除</button>
<a class="btn btn-outline-brand" href="index.html">取消</a>
</div>
</div>
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
@@ -0,0 +1,119 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>編輯練習紀錄|水晶練習室 Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="../../css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand navbar-dark sk-navbar">
<div class="container-fluid">
<span class="navbar-brand mb-0"><i class="bi bi-gem me-1"></i>水晶練習室 <span class="fw-light">Admin</span></span>
<div class="d-flex gap-2">
<a class="btn btn-brand btn-sm" href="../../index.html">回前台</a>
<a class="btn btn-outline-brand btn-sm" href="../../index.html">登出</a>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<aside class="col-lg-2 border-end sk-admin-sidebar py-4 px-3 min-vh-100">
<nav class="nav flex-column">
<a class="nav-link" href="../index.html"><i class="bi bi-speedometer2 me-2"></i>後台首頁</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-people me-2"></i>練習生管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-mortarboard me-2"></i>課程管理</a>
<a class="nav-link active" href="index.html"><i class="bi bi-journal-text me-2"></i>練習紀錄管理</a>
<a class="nav-link" href="../songs/index.html"><i class="bi bi-music-note-list me-2"></i>歌曲資料管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-list-task me-2"></i>訓練任務管理</a>
</nav>
</aside>
<main class="col-lg-10 py-4 px-4">
<h1 class="h3 mb-4 text-white">編輯練習紀錄</h1>
<div class="card sk-card sk-surface p-4" style="max-width: 42rem;">
<form>
<div class="row g-3">
<div class="col-md-6">
<label for="traineeName" class="form-label">練習生</label>
<select class="form-select" id="traineeName">
<option selected>努比</option>
<option>昇材</option>
<option>宰鎮</option>
<option>水院</option>
</select>
</div>
<div class="col-md-6">
<label for="practiceDate" class="form-label">日期</label>
<input type="date" class="form-control" id="practiceDate" value="2026-07-16">
</div>
<div class="col-md-6">
<label for="practiceType" class="form-label">訓練類型</label>
<select class="form-select" id="practiceType">
<option>vocal</option>
<option selected>舞蹈</option>
<option>Rap</option>
<option>舞台表現</option>
<option>體能</option>
</select>
</div>
<div class="col-md-6">
<label for="practiceSong" class="form-label">歌曲</label>
<select class="form-select" id="practiceSong">
<option>Couple</option>
<option selected>Road Fighter</option>
<option>Com' Back</option>
<option>Chivalry</option>
<option>The Way This Guy Lives</option>
</select>
</div>
<div class="col-md-6">
<label for="practiceMinutes" class="form-label">練習時間(分鐘)</label>
<input type="number" class="form-control" id="practiceMinutes" value="40">
</div>
<div class="col-md-6">
<label for="practiceStatus" class="form-label">完成狀態</label>
<select class="form-select" id="practiceStatus">
<option>未開始</option>
<option>進行中</option>
<option selected>已完成</option>
</select>
</div>
<div class="col-12">
<label for="practiceCompletion" class="form-label d-flex justify-content-between">
<span>完成度</span>
<span class="text-body-secondary" id="completionValue">85%</span>
</label>
<input type="range" class="form-range" id="practiceCompletion" min="0" max="100" value="85"
oninput="document.getElementById('completionValue').textContent = this.value + '%'">
</div>
</div>
<div class="d-flex gap-2 mt-4">
<button type="submit" class="btn btn-brand">儲存變更</button>
<a class="btn btn-outline-brand" href="index.html">取消</a>
</div>
</form>
</div>
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
@@ -0,0 +1,125 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>練習紀錄管理|水晶練習室 Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="../../css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand navbar-dark sk-navbar">
<div class="container-fluid">
<span class="navbar-brand mb-0"><i class="bi bi-gem me-1"></i>水晶練習室 <span class="fw-light">Admin</span></span>
<div class="d-flex gap-2">
<a class="btn btn-brand btn-sm" href="../../index.html">回前台</a>
<a class="btn btn-outline-brand btn-sm" href="../../index.html">登出</a>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<aside class="col-lg-2 border-end sk-admin-sidebar py-4 px-3 min-vh-100">
<nav class="nav flex-column">
<a class="nav-link" href="../index.html"><i class="bi bi-speedometer2 me-2"></i>後台首頁</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-people me-2"></i>練習生管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-mortarboard me-2"></i>課程管理</a>
<a class="nav-link active" href="index.html"><i class="bi bi-journal-text me-2"></i>練習紀錄管理</a>
<a class="nav-link" href="../songs/index.html"><i class="bi bi-music-note-list me-2"></i>歌曲資料管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-list-task me-2"></i>訓練任務管理</a>
</nav>
</aside>
<main class="col-lg-10 py-4 px-4">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-4">
<h1 class="h3 mb-0 text-white">練習紀錄管理</h1>
<div class="d-flex gap-2">
<a class="btn btn-brand" href="create.html"><i class="bi bi-plus-lg me-1"></i>新增練習紀錄</a>
<a class="btn btn-outline-brand" href="../index.html">回後台首頁</a>
</div>
</div>
<div class="card sk-card sk-surface">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>練習生</th>
<th>日期</th>
<th>訓練類型</th>
<th>歌曲</th>
<th>練習時間</th>
<th>完成度</th>
<th>完成狀態</th>
<th class="text-end">操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>努比</td>
<td>2026-07-16</td>
<td><span class="badge badge-type badge-type--dance">舞蹈</span></td>
<td>Road Fighter</td>
<td>40 分鐘</td>
<td>85%</td>
<td><span class="badge badge-status badge-status--done">已完成</span></td>
<td class="text-end">
<a class="btn btn-sm btn-outline-brand" href="edit.html">編輯</a>
<a class="btn btn-sm btn-outline-danger" href="delete.html">刪除</a>
</td>
</tr>
<tr>
<td>昇材</td>
<td>2026-07-16</td>
<td><span class="badge badge-type badge-type--vocal">vocal</span></td>
<td>Couple</td>
<td>35 分鐘</td>
<td>78%</td>
<td><span class="badge badge-status badge-status--done">已完成</span></td>
<td class="text-end">
<a class="btn btn-sm btn-outline-brand" href="edit.html">編輯</a>
<a class="btn btn-sm btn-outline-danger" href="delete.html">刪除</a>
</td>
</tr>
<tr>
<td>宰鎮</td>
<td>2026-07-15</td>
<td><span class="badge badge-type badge-type--rap">Rap</span></td>
<td>Chivalry</td>
<td>25 分鐘</td>
<td>60%</td>
<td><span class="badge badge-status badge-status--progress">進行中</span></td>
<td class="text-end">
<a class="btn btn-sm btn-outline-brand" href="edit.html">編輯</a>
<a class="btn btn-sm btn-outline-danger" href="delete.html">刪除</a>
</td>
</tr>
<tr>
<td>水院</td>
<td>2026-07-14</td>
<td><span class="badge badge-type badge-type--stage">舞台表現</span></td>
<td>Com' Back</td>
<td>50 分鐘</td>
<td>0%</td>
<td><span class="badge badge-status badge-status--pending">未開始</span></td>
<td class="text-end">
<a class="btn btn-sm btn-outline-brand" href="edit.html">編輯</a>
<a class="btn btn-sm btn-outline-danger" href="delete.html">刪除</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
@@ -0,0 +1,96 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>新增歌曲|水晶練習室 Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="../../css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand navbar-dark sk-navbar">
<div class="container-fluid">
<span class="navbar-brand mb-0"><i class="bi bi-gem me-1"></i>水晶練習室 <span class="fw-light">Admin</span></span>
<div class="d-flex gap-2">
<a class="btn btn-brand btn-sm" href="../../index.html">回前台</a>
<a class="btn btn-outline-brand btn-sm" href="../../index.html">登出</a>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<aside class="col-lg-2 border-end sk-admin-sidebar py-4 px-3 min-vh-100">
<nav class="nav flex-column">
<a class="nav-link" href="../index.html"><i class="bi bi-speedometer2 me-2"></i>後台首頁</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-people me-2"></i>練習生管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-mortarboard me-2"></i>課程管理</a>
<a class="nav-link" href="../practices/index.html"><i class="bi bi-journal-text me-2"></i>練習紀錄管理</a>
<a class="nav-link active" href="index.html"><i class="bi bi-music-note-list me-2"></i>歌曲資料管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-list-task me-2"></i>訓練任務管理</a>
</nav>
</aside>
<main class="col-lg-10 py-4 px-4">
<h1 class="h3 mb-4 text-white">新增歌曲</h1>
<div class="card sk-card sk-surface p-4">
<form>
<div class="row g-3">
<div class="col-md-6">
<label for="songName" class="form-label">歌名</label>
<input type="text" class="form-control" id="songName" placeholder="例如:Couple">
</div>
<div class="col-md-6">
<label for="songAlbum" class="form-label">專輯</label>
<input type="text" class="form-control" id="songAlbum" placeholder="例如:Come Back Again">
</div>
<div class="col-md-6">
<label for="songYear" class="form-label">發行年份</label>
<input type="number" class="form-control" id="songYear" placeholder="例如:1997">
</div>
<div class="col-md-6">
<label for="songYoutube" class="form-label">YouTube 連結</label>
<input type="url" class="form-control" id="songYoutube" placeholder="https://www.youtube.com/...">
</div>
<div class="col-md-6">
<label for="songDifficulty" class="form-label">舞蹈難度</label>
<select class="form-select" id="songDifficulty">
<option></option>
<option>中等</option>
<option></option>
</select>
</div>
<div class="col-md-6">
<label class="form-label d-block">是否顯示</label>
<div class="form-check form-switch mt-2">
<input class="form-check-input" type="checkbox" role="switch" id="songVisible" checked>
<label class="form-check-label" for="songVisible">於前台顯示</label>
</div>
</div>
</div>
<div class="d-flex gap-2 mt-4">
<button type="submit" class="btn btn-brand">儲存</button>
<a class="btn btn-outline-brand" href="index.html">取消</a>
</div>
</form>
</div>
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
@@ -0,0 +1,68 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>刪除歌曲|水晶練習室 Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="../../css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand navbar-dark sk-navbar">
<div class="container-fluid">
<span class="navbar-brand mb-0"><i class="bi bi-gem me-1"></i>水晶練習室 <span class="fw-light">Admin</span></span>
<div class="d-flex gap-2">
<a class="btn btn-brand btn-sm" href="../../index.html">回前台</a>
<a class="btn btn-outline-brand btn-sm" href="../../index.html">登出</a>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<aside class="col-lg-2 border-end sk-admin-sidebar py-4 px-3 min-vh-100">
<nav class="nav flex-column">
<a class="nav-link" href="../index.html"><i class="bi bi-speedometer2 me-2"></i>後台首頁</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-people me-2"></i>練習生管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-mortarboard me-2"></i>課程管理</a>
<a class="nav-link" href="../practices/index.html"><i class="bi bi-journal-text me-2"></i>練習紀錄管理</a>
<a class="nav-link active" href="index.html"><i class="bi bi-music-note-list me-2"></i>歌曲資料管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-list-task me-2"></i>訓練任務管理</a>
</nav>
</aside>
<main class="col-lg-10 py-4 px-4">
<h1 class="h3 mb-4 text-white">刪除歌曲</h1>
<div class="card sk-card sk-surface p-4" style="max-width: 32rem;">
<div class="alert alert-danger mb-4" role="alert">
<i class="bi bi-exclamation-triangle-fill me-1"></i>確定要刪除這首歌曲嗎?此動作無法復原。
</div>
<dl class="row mb-4">
<dt class="col-4 text-body-secondary">編號</dt>
<dd class="col-8">1</dd>
<dt class="col-4 text-body-secondary">歌名</dt>
<dd class="col-8">Couple</dd>
<dt class="col-4 text-body-secondary">專輯</dt>
<dd class="col-8">Come Back Again</dd>
<dt class="col-4 text-body-secondary">發行年份</dt>
<dd class="col-8">1997</dd>
</dl>
<div class="d-flex gap-2">
<button type="button" class="btn btn-danger">確認刪除</button>
<a class="btn btn-outline-brand" href="index.html">取消</a>
</div>
</div>
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
+103
View File
@@ -0,0 +1,103 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>編輯歌曲|水晶練習室 Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="../../css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand navbar-dark sk-navbar">
<div class="container-fluid">
<span class="navbar-brand mb-0"><i class="bi bi-gem me-1"></i>水晶練習室 <span class="fw-light">Admin</span></span>
<div class="d-flex gap-2">
<a class="btn btn-brand btn-sm" href="../../index.html">回前台</a>
<a class="btn btn-outline-brand btn-sm" href="../../index.html">登出</a>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<aside class="col-lg-2 border-end sk-admin-sidebar py-4 px-3 min-vh-100">
<nav class="nav flex-column">
<a class="nav-link" href="../index.html"><i class="bi bi-speedometer2 me-2"></i>後台首頁</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-people me-2"></i>練習生管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-mortarboard me-2"></i>課程管理</a>
<a class="nav-link" href="../practices/index.html"><i class="bi bi-journal-text me-2"></i>練習紀錄管理</a>
<a class="nav-link active" href="index.html"><i class="bi bi-music-note-list me-2"></i>歌曲資料管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-list-task me-2"></i>訓練任務管理</a>
</nav>
</aside>
<main class="col-lg-10 py-4 px-4">
<h1 class="h3 mb-4 text-white">編輯歌曲</h1>
<div class="card sk-card sk-surface p-4">
<form>
<div class="row g-3">
<div class="col-md-6">
<label for="songId" class="form-label">編號</label>
<input type="text" class="form-control" id="songId" value="1" disabled>
</div>
<div class="col-md-6"></div>
<div class="col-md-6">
<label for="songName" class="form-label">歌名</label>
<input type="text" class="form-control" id="songName" value="Couple">
</div>
<div class="col-md-6">
<label for="songAlbum" class="form-label">專輯</label>
<input type="text" class="form-control" id="songAlbum" value="Come Back Again">
</div>
<div class="col-md-6">
<label for="songYear" class="form-label">發行年份</label>
<input type="number" class="form-control" id="songYear" value="1997">
</div>
<div class="col-md-6">
<label for="songYoutube" class="form-label">YouTube 連結</label>
<input type="url" class="form-control" id="songYoutube" value="https://www.youtube.com/">
</div>
<div class="col-md-6">
<label for="songDifficulty" class="form-label">舞蹈難度</label>
<select class="form-select" id="songDifficulty">
<option></option>
<option selected>中等</option>
<option></option>
</select>
</div>
<div class="col-md-6">
<label class="form-label d-block">是否顯示</label>
<div class="form-check form-switch mt-2">
<input class="form-check-input" type="checkbox" role="switch" id="songVisible" checked>
<label class="form-check-label" for="songVisible">於前台顯示</label>
</div>
</div>
</div>
<div class="d-flex gap-2 mt-4">
<button type="submit" class="btn btn-brand">儲存變更</button>
<a class="btn btn-outline-brand" href="index.html">取消</a>
</div>
</form>
</div>
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
+112
View File
@@ -0,0 +1,112 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>歌曲資料管理|水晶練習室 Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="../../css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand navbar-dark sk-navbar">
<div class="container-fluid">
<span class="navbar-brand mb-0"><i class="bi bi-gem me-1"></i>水晶練習室 <span class="fw-light">Admin</span></span>
<div class="d-flex gap-2">
<a class="btn btn-brand btn-sm" href="../../index.html">回前台</a>
<a class="btn btn-outline-brand btn-sm" href="../../index.html">登出</a>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<aside class="col-lg-2 border-end sk-admin-sidebar py-4 px-3 min-vh-100">
<nav class="nav flex-column">
<a class="nav-link" href="../index.html"><i class="bi bi-speedometer2 me-2"></i>後台首頁</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-people me-2"></i>練習生管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-mortarboard me-2"></i>課程管理</a>
<a class="nav-link" href="../practices/index.html"><i class="bi bi-journal-text me-2"></i>練習紀錄管理</a>
<a class="nav-link active" href="index.html"><i class="bi bi-music-note-list me-2"></i>歌曲資料管理</a>
<a class="nav-link disabled" href="#" aria-disabled="true"><i class="bi bi-list-task me-2"></i>訓練任務管理</a>
</nav>
</aside>
<main class="col-lg-10 py-4 px-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0 text-white">歌曲資料管理</h1>
<div class="d-flex gap-2">
<a class="btn btn-brand" href="create.html"><i class="bi bi-plus-lg me-1"></i>新增歌曲</a>
<a class="btn btn-outline-brand" href="../index.html">回後台首頁</a>
</div>
</div>
<div class="card sk-card sk-surface">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th>編號</th>
<th>歌名</th>
<th>專輯</th>
<th>發行年份</th>
<th>YouTube 連結</th>
<th>舞蹈難度</th>
<th>是否顯示</th>
<th class="text-end">操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Couple</td>
<td>Come Back Again</td>
<td>1997</td>
<td><a href="https://www.youtube.com/" target="_blank" rel="noopener">連結</a></td>
<td><span class="badge text-bg-warning">中等</span></td>
<td><span class="badge text-bg-success">顯示</span></td>
<td class="text-end">
<a class="btn btn-sm btn-outline-brand" href="edit.html">編輯</a>
<a class="btn btn-sm btn-outline-danger" href="delete.html">刪除</a>
</td>
</tr>
<tr>
<td>2</td>
<td>Road Fighter</td>
<td>Road Fighter</td>
<td>2000</td>
<td><a href="https://www.youtube.com/" target="_blank" rel="noopener">連結</a></td>
<td><span class="badge text-bg-danger"></span></td>
<td><span class="badge text-bg-success">顯示</span></td>
<td class="text-end">
<a class="btn btn-sm btn-outline-brand" href="edit.html">編輯</a>
<a class="btn btn-sm btn-outline-danger" href="delete.html">刪除</a>
</td>
</tr>
<tr>
<td>3</td>
<td>Couple 재수사</td>
<td>The Story of Kies Part 1</td>
<td>2016</td>
<td><a href="https://www.youtube.com/" target="_blank" rel="noopener">連結</a></td>
<td><span class="badge text-bg-secondary"></span></td>
<td><span class="badge text-bg-secondary">隱藏</span></td>
<td class="text-end">
<a class="btn btn-sm btn-outline-brand" href="edit.html">編輯</a>
<a class="btn btn-sm btn-outline-danger" href="delete.html">刪除</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
+391
View File
@@ -0,0 +1,391 @@
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100..900&display=swap');
/* ============================================
水晶練習室偶像練習生訓練管理系統
主題專輯質感 黑底 + 銀白金屬漸層呼應 G1 專輯視覺
重點行動區保留少量烈焰紅點綴
============================================ */
:root {
/* ── 環境色(銀白金屬,用於底色/邊框/卡片) ── */
--sk-primary: #9aa0a8;
--sk-primary-dark: #4b4e54;
--sk-secondary: #e4e6ea;
--sk-accent: #ffffff;
--sk-gradient: linear-gradient(135deg, #3a3c40 0%, #8b8f96 45%, #d6d9dd 75%, #ffffff 100%);
/* ── 重點色(烈焰紅,僅用於關鍵行動/狀態) ── */
--sk-red: #e2102b;
--sk-red-dark: #9c0b1e;
--sk-red-gradient: linear-gradient(135deg, #ff2d3d 0%, #9c0b1e 100%);
/* ── 背景色(舞台黑) ── */
--sk-bg: #0a0a0a;
--sk-bg-raised: #161616;
/* ── 玻璃卡片面 ── */
--sk-surface: rgba(180, 184, 191, 0.06);
--sk-surface-soft: rgba(180, 184, 191, 0.12);
--sk-border: rgba(180, 184, 191, 0.35);
/* ── 文字色(銀白,呼應專輯封面) ── */
--sk-text: #f1f2f4;
--sk-text-muted: #a7abb2;
}
html,
body {
background-color: var(--sk-bg);
color: var(--sk-text);
}
body {
font-family: 'Noto Sans TC', system-ui, sans-serif;
line-height: 1.6;
position: relative;
}
a {
color: var(--sk-secondary);
}
.text-body-secondary {
color: silver !important;
}
/* 舞台感光暈:兩顆柔光暈,僅作背景點綴,不搶內容 */
body::before,
body::after {
content: "";
position: fixed;
width: 40vw;
height: 40vw;
border-radius: 50%;
filter: blur(80px);
opacity: 0.16;
z-index: -1;
pointer-events: none;
}
body::before {
top: -10vw;
left: -10vw;
background: var(--sk-primary);
}
body::after {
bottom: -10vw;
right: -10vw;
background: var(--sk-accent);
}
/* ============ Navbar(前台/後台共用) ============ */
.sk-navbar {
background-color: var(--sk-bg-raised) !important;
border-bottom: 1px solid var(--sk-border);
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.sk-navbar .navbar-brand,
.sk-brand-text {
background: var(--sk-gradient);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 700;
letter-spacing: 1px;
}
.sk-navbar .nav-link {
color: var(--sk-text);
}
.sk-navbar .nav-link:hover,
.sk-navbar .nav-link.active {
color: var(--sk-secondary);
}
.sk-navbar .nav-link.disabled {
color: #5c5f64;
}
/* ============ 品牌按鈕 ============ */
.btn-brand {
background: var(--sk-red-gradient);
border: none;
color: #fff;
font-weight: 600;
}
.btn-brand:hover,
.btn-brand:focus {
filter: brightness(1.1);
color: #fff;
}
.btn-outline-brand {
color: var(--sk-secondary);
border-color: var(--sk-border);
background-color: transparent;
}
.btn-outline-brand:hover {
background: var(--sk-red-gradient);
border-color: transparent;
color: #fff;
}
/* ============ 主視覺(嵌入影片背景,尺寸依影片比例) ============ */
.sk-hero {
border-bottom: 1px solid var(--sk-border);
overflow: hidden;
}
.sk-hero h1 {
color: #fff;
letter-spacing: 2px;
}
.sk-hero .sk-title-bar {
width: 60px;
height: 3px;
background: var(--sk-gradient);
margin: 18px auto 0;
border-radius: 2px;
}
.sk-hero-video-frame {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
background: #000;
}
.sk-hero-video-frame iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
pointer-events: none;
}
.sk-hero-overlay {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(10, 10, 10, 0.35) 0%, rgba(10, 10, 10, 0.8) 100%);
}
.sk-hero-content {
position: relative;
z-index: 2;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1rem;
}
/* ============ 區塊標題 ============ */
.sk-section-title {
color: var(--sk-text);
font-size: 1.25rem;
font-weight: 700;
letter-spacing: 1px;
margin-bottom: 1.25rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.sk-section-title i {
color: var(--sk-accent);
}
/* ============ 玻璃卡片(練習/統計/表格/表單容器共用) ============ */
.sk-card,
.sk-surface {
background-color: var(--sk-surface);
border: 1px solid var(--sk-border);
backdrop-filter: blur(10px);
color: var(--sk-text);
border-radius: 0.75rem;
}
.sk-card {
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.sk-card:hover {
transform: translateY(-4px);
border-color: var(--sk-primary);
box-shadow: 0 0.75rem 1.75rem rgba(180, 184, 191, 0.3);
}
.sk-card .card-title {
color: #fff;
}
.sk-stat-value {
font-size: 2rem;
font-weight: 700;
background: var(--sk-gradient);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
/* ============ 練習生歡迎區 ============ */
.sk-welcome-icon {
width: 3.25rem;
height: 3.25rem;
border-radius: 50%;
background: var(--sk-gradient);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: #fff;
flex-shrink: 0;
}
/* ============ 訓練類型徽章(紅/金/黑/米白家族) ============ */
.badge-type {
border-radius: 999px;
padding: 0.35em 0.75em;
font-weight: 600;
border: 1px solid transparent;
}
.badge-type--vocal { background: rgba(200, 205, 214, 0.18); color: #dde0e6; border-color: rgba(200, 205, 214, 0.45); }
.badge-type--dance { background: rgba(154, 160, 168, 0.18); color: #c9ccd2; border-color: rgba(154, 160, 168, 0.45); }
.badge-type--rap { background: rgba(110, 114, 121, 0.30); color: #b7bac0; border-color: rgba(110, 114, 121, 0.55); }
.badge-type--stage { background: var(--sk-text); color: #1a1a1a; border-color: var(--sk-text); }
.badge-type--fitness { background: rgba(150, 154, 161, 0.16); color: #c4c7cd; border-color: rgba(150, 154, 161, 0.4); }
/* ============ 完成狀態徽章 ============ */
.badge-status {
border-radius: 999px;
padding: 0.35em 0.75em;
font-weight: 600;
border: 1px solid transparent;
}
.badge-status--pending { background: rgba(150, 154, 161, 0.16); color: var(--sk-text-muted); border-color: rgba(150, 154, 161, 0.35); }
.badge-status--progress { background: rgba(226, 16, 43, 0.18); color: #ff8a94; border-color: rgba(226, 16, 43, 0.45); }
.badge-status--done { background: rgba(52, 211, 153, 0.18); color: #6ee7b7; border-color: rgba(52, 211, 153, 0.45); }
/* ============ Progress(能力進度) ============ */
.sk-progress {
height: 0.6rem;
background-color: var(--sk-bg-raised);
border: 1px solid var(--sk-border);
border-radius: 999px;
}
.sk-progress .progress-bar {
background: var(--sk-gradient);
}
/* ============ 後台側邊選單 ============ */
.sk-admin-sidebar {
background-color: var(--sk-bg-raised);
border-color: var(--sk-border) !important;
}
.sk-admin-sidebar .nav-link {
color: var(--sk-text);
border-radius: 0.5rem;
margin-bottom: 0.25rem;
}
.sk-admin-sidebar .nav-link.active {
background: var(--sk-gradient);
color: #fff;
font-weight: 600;
}
.sk-admin-sidebar .nav-link:not(.active):hover {
background-color: var(--sk-surface-soft);
}
.sk-admin-sidebar .nav-link.disabled {
color: #5c5f64;
}
/* ============ 表格 ============ */
.sk-surface .table {
--bs-table-color: var(--sk-text);
--bs-table-bg: transparent;
--bs-table-striped-bg: rgba(180, 184, 191, 0.06);
--bs-table-hover-bg: rgba(180, 184, 191, 0.1);
--bs-table-border-color: var(--sk-border);
vertical-align: middle;
}
.sk-surface .table thead {
background-color: var(--sk-surface-soft);
color: #fff;
}
.sk-surface a:not(.btn):not(.nav-link) {
color: var(--sk-secondary);
}
/* ============ 表單元件(暗色輸入框) ============ */
.form-control,
.form-select {
background-color: var(--sk-bg-raised);
border: 1px solid var(--sk-border);
color: var(--sk-text);
}
.form-control:focus,
.form-select:focus {
background-color: var(--sk-bg-raised);
border-color: var(--sk-primary);
color: var(--sk-text);
box-shadow: 0 0 0 0.25rem rgba(180, 184, 191, 0.25);
}
.form-control::placeholder {
color: #6c7076;
}
.form-control:disabled {
background-color: var(--sk-bg-raised);
color: var(--sk-text-muted);
opacity: 0.7;
}
.form-label {
color: var(--sk-text-muted);
font-weight: 500;
}
.form-check-input {
background-color: var(--sk-bg-raised);
border-color: var(--sk-border);
}
.form-check-input:checked {
background-color: var(--sk-primary);
border-color: var(--sk-primary);
}
.form-range::-webkit-slider-thumb {
background: var(--sk-primary);
}
.form-range::-moz-range-thumb {
background: var(--sk-primary);
}
/* ============ Footer ============ */
.sk-footer {
background-color: var(--sk-bg-raised);
color: var(--sk-text-muted);
border-top: 1px solid var(--sk-border);
}
+225
View File
@@ -0,0 +1,225 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>全能Leader</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<!-- 上方導覽列 -->
<nav class="navbar navbar-expand-lg navbar-dark sk-navbar sticky-top">
<div class="container">
<a class="navbar-brand" href="index.html"><i class="bi bi-gem me-1"></i>全能Leader練習室</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-1">
<!-- <li class="nav-item"><a class="nav-link active" href="index.html">練習室首頁</a></li> -->
<li class="nav-item"><a class="nav-link" href="#today">今日訓練</a></li>
<li class="nav-item"><a class="nav-link" href="practice/index.html">我的練習紀錄</a></li>
<li class="nav-item"><a class="nav-link disabled" href="#" aria-disabled="true">課程庫</a></li>
<li class="nav-item"><a class="nav-link" href="#progress">個人進度</a></li>
<li class="nav-item ms-lg-2">
<a class="btn btn-brand btn-sm" href="admin/index.html">後台管理</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- 主視覺(嵌入影片背景) -->
<header class="sk-hero">
<div class="sk-hero-video-frame">
<iframe
src="https://www.youtube.com/embed/ARi0cfpv6mE?autoplay=1&mute=1&loop=1&playlist=ARi0cfpv6mE&controls=0&modestbranding=1&rel=0&showinfo=0"
title="水晶練習室主視覺影片"
allow="autoplay; encrypted-media"
loading="lazy"></iframe>
<div class="sk-hero-overlay"></div>
<div class="sk-hero-content container text-center">
<h1 class="display-4 fw-bold mb-0">偶像練習生</h1>
<div class="sk-title-bar"></div>
<p class="lead mb-0 mt-4 text-body-secondary">全能Leader 殷志源打造全新綜藝</p>
</div>
</div>
</header>
<main class="container py-5">
<!-- 練習生歡迎區 -->
<section class="mb-5">
<div class="card sk-card p-4">
<div class="d-flex flex-wrap align-items-center gap-3">
<div class="sk-welcome-icon"><i class="bi bi-person-video3"></i></div>
<div class="me-auto">
<div class="fs-5 fw-bold text-white">練習生:努比</div>
<span class="badge badge-status badge-status--progress"><i class="bi bi-record-circle me-1"></i>今日狀態:訓練中</span>
</div>
<div class="d-flex gap-4 text-center">
<div>
<div class="sk-stat-value fs-3">4</div>
<div class="small text-body-secondary">本週完成練習(次)</div>
</div>
<div>
<div class="sk-stat-value fs-3">3</div>
<div class="small text-body-secondary">連續練習(天)</div>
</div>
</div>
</div>
</div>
</section>
<!-- 今日訓練任務 -->
<section class="mb-5" id="today">
<h2 class="sk-section-title"><i class="bi bi-list-task"></i>今日訓練任務</h2>
<div class="row g-4">
<div class="col-md-4">
<div class="card sk-card h-100 p-4">
<div class="d-flex align-items-center gap-2 mb-3">
<i class="bi bi-mic-fill fs-4 text-white"></i>
<span class="badge badge-type badge-type--vocal">vocal</span>
</div>
<h3 class="h5 text-white">完成一首歌曲錄唱</h3>
<p class="text-body-secondary small mb-3"><i class="bi bi-clock me-1"></i>預計時間:30 分鐘</p>
<span class="badge badge-status badge-status--pending mb-3 align-self-start">未開始</span>
<a class="btn btn-brand mt-auto" href="practice/create.html">開始練習</a>
</div>
</div>
<div class="col-md-4">
<div class="card sk-card h-100 p-4">
<div class="d-flex align-items-center gap-2 mb-3">
<i class="bi bi-person-arms-up fs-4 text-white"></i>
<span class="badge badge-type badge-type--dance">舞蹈</span>
</div>
<h3 class="h5 text-white">練習一段K-POP舞蹈</h3>
<p class="text-body-secondary small mb-3"><i class="bi bi-clock me-1"></i>預計時間:45 分鐘</p>
<span class="badge badge-status badge-status--progress mb-3 align-self-start">進行中</span>
<a class="btn btn-brand mt-auto" href="practice/create.html">開始練習</a>
</div>
</div>
<div class="col-md-4">
<div class="card sk-card h-100 p-4">
<div class="d-flex align-items-center gap-2 mb-3">
<i class="bi bi-soundwave fs-4 text-white"></i>
<span class="badge badge-type badge-type--rap">Rap</span>
</div>
<h3 class="h5 text-white">完成節奏與咬字訓練</h3>
<p class="text-body-secondary small mb-3"><i class="bi bi-clock me-1"></i>預計時間:20 分鐘</p>
<span class="badge badge-status badge-status--done mb-3 align-self-start">已完成</span>
<a class="btn btn-outline-brand mt-auto" href="practice/create.html">再次練習</a>
</div>
</div>
</div>
</section>
<!-- 能力進度 -->
<section class="mb-5" id="progress">
<h2 class="sk-section-title"><i class="bi bi-graph-up-arrow"></i>能力進度</h2>
<div class="card sk-card p-4">
<div class="row g-4">
<div class="col-md-6">
<div class="d-flex justify-content-between small mb-1">
<span><i class="bi bi-mic-fill me-1"></i>vocal</span>
<span class="text-body-secondary">99%</span>
</div>
<div class="progress sk-progress">
<div class="progress-bar" style="width: 99%" role="progressbar" aria-valuenow="99" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-between small mb-1">
<span><i class="bi bi-person-arms-up me-1"></i>舞蹈</span>
<span class="text-body-secondary">85%</span>
</div>
<div class="progress sk-progress">
<div class="progress-bar" style="width: 85%" role="progressbar" aria-valuenow="85" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-between small mb-1">
<span><i class="bi bi-soundwave me-1"></i>Rap</span>
<span class="text-body-secondary">66%</span>
</div>
<div class="progress sk-progress">
<div class="progress-bar" style="width: 66%" role="progressbar" aria-valuenow="66" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-between small mb-1">
<span><i class="bi bi-stars me-1"></i>舞台表現</span>
<span class="text-body-secondary">80%</span>
</div>
<div class="progress sk-progress">
<div class="progress-bar" style="width: 80%" role="progressbar" aria-valuenow="80" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
</div>
</section>
<!-- 最近練習紀錄 -->
<section>
<h2 class="sk-section-title"><i class="bi bi-journal-text"></i>最近練習紀錄</h2>
<div class="card sk-card sk-surface">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>日期</th>
<th>練習項目</th>
<th>歌曲</th>
<th>練習時間</th>
<th>完成度</th>
<th class="text-end">查看紀錄</th>
</tr>
</thead>
<tbody>
<tr>
<td>2026-07-16</td>
<td><span class="badge badge-type badge-type--dance">舞蹈</span></td>
<td>Road Fighter</td>
<td>40 分鐘</td>
<td>85%</td>
<td class="text-end"><a class="btn btn-sm btn-outline-brand" href="practice/show.html">查看</a></td>
</tr>
<tr>
<td>2026-07-15</td>
<td><span class="badge badge-type badge-type--vocal">vocal</span></td>
<td>Couple</td>
<td>30 分鐘</td>
<td>70%</td>
<td class="text-end"><a class="btn btn-sm btn-outline-brand" href="practice/show.html">查看</a></td>
</tr>
<tr>
<td>2026-07-14</td>
<td><span class="badge badge-type badge-type--rap">Rap</span></td>
<td>Chivalry</td>
<td>25 分鐘</td>
<td>60%</td>
<td class="text-end"><a class="btn btn-sm btn-outline-brand" href="practice/show.html">查看</a></td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
</main>
<footer class="sk-footer text-center py-4">
<small>&copy; 2026 水晶練習室</small>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
+115
View File
@@ -0,0 +1,115 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>新增練習紀錄|水晶練習室</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="../css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark sk-navbar sticky-top">
<div class="container">
<a class="navbar-brand" href="../index.html"><i class="bi bi-gem me-1"></i>水晶練習室</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-1">
<li class="nav-item"><a class="nav-link" href="../index.html">練習室首頁</a></li>
<li class="nav-item"><a class="nav-link" href="../index.html#today">今日訓練</a></li>
<li class="nav-item"><a class="nav-link active" href="index.html">我的練習紀錄</a></li>
<li class="nav-item"><a class="nav-link disabled" href="#" aria-disabled="true">課程庫</a></li>
<li class="nav-item"><a class="nav-link" href="../index.html#progress">個人進度</a></li>
<li class="nav-item ms-lg-2">
<a class="btn btn-brand btn-sm" href="../admin/index.html">後台管理</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container py-5">
<h1 class="h3 mb-4 text-white"><i class="bi bi-plus-circle me-2"></i>新增練習紀錄</h1>
<div class="card sk-card sk-surface p-4" style="max-width: 42rem;">
<form>
<div class="row g-3">
<div class="col-md-6">
<label for="practiceDate" class="form-label">練習日期</label>
<input type="date" class="form-control" id="practiceDate">
</div>
<div class="col-md-6">
<label for="practiceType" class="form-label">訓練類型</label>
<select class="form-select" id="practiceType">
<option>vocal</option>
<option>舞蹈</option>
<option>Rap</option>
<option>舞台表現</option>
<option>體能</option>
</select>
</div>
<div class="col-md-6">
<label for="practiceSong" class="form-label">練習歌曲</label>
<select class="form-select" id="practiceSong">
<option>Couple</option>
<option>Road Fighter</option>
<option>Com' Back</option>
<option>Chivalry</option>
<option>The Way This Guy Lives</option>
</select>
</div>
<div class="col-md-6">
<label for="practiceMinutes" class="form-label">練習分鐘數</label>
<input type="number" class="form-control" id="practiceMinutes" placeholder="例如:30">
</div>
<div class="col-12">
<label for="practiceContent" class="form-label">練習內容</label>
<textarea class="form-control" id="practiceContent" rows="2" placeholder="例如:副歌走位+隊形轉換"></textarea>
</div>
<div class="col-12">
<label for="practiceCompletion" class="form-label d-flex justify-content-between">
<span>今日完成度</span>
<span class="text-body-secondary" id="completionValue">50%</span>
</label>
<input type="range" class="form-range" id="practiceCompletion" min="0" max="100" value="50"
oninput="document.getElementById('completionValue').textContent = this.value + '%'">
</div>
<div class="col-12">
<label for="practiceNote" class="form-label">練習心得</label>
<textarea class="form-control" id="practiceNote" rows="3" placeholder="今天練習的感想與需要加強的地方"></textarea>
</div>
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="practiceDone">
<label class="form-check-label" for="practiceDone">是否完成</label>
</div>
</div>
</div>
<div class="d-flex gap-2 mt-4">
<button type="submit" class="btn btn-brand">儲存</button>
<a class="btn btn-outline-brand" href="index.html">取消</a>
</div>
</form>
</div>
</main>
<footer class="sk-footer text-center py-4">
<small>&copy; 2026 水晶練習室</small>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
+66
View File
@@ -0,0 +1,66 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>刪除練習紀錄|水晶練習室</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="../css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark sk-navbar sticky-top">
<div class="container">
<a class="navbar-brand" href="../index.html"><i class="bi bi-gem me-1"></i>水晶練習室</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-1">
<li class="nav-item"><a class="nav-link" href="../index.html">練習室首頁</a></li>
<li class="nav-item"><a class="nav-link" href="../index.html#today">今日訓練</a></li>
<li class="nav-item"><a class="nav-link active" href="index.html">我的練習紀錄</a></li>
<li class="nav-item"><a class="nav-link disabled" href="#" aria-disabled="true">課程庫</a></li>
<li class="nav-item"><a class="nav-link" href="../index.html#progress">個人進度</a></li>
<li class="nav-item ms-lg-2">
<a class="btn btn-brand btn-sm" href="../admin/index.html">後台管理</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container py-5">
<h1 class="h3 mb-4 text-white"><i class="bi bi-trash3 me-2"></i>刪除練習紀錄</h1>
<div class="card sk-card sk-surface p-4" style="max-width: 32rem;">
<div class="alert alert-danger mb-4" role="alert">
<i class="bi bi-exclamation-triangle-fill me-1"></i>確定要刪除這筆練習紀錄嗎?此動作無法復原。
</div>
<dl class="row mb-4">
<dt class="col-4 text-body-secondary">練習日期</dt>
<dd class="col-8">2026-07-16</dd>
<dt class="col-4 text-body-secondary">訓練類型</dt>
<dd class="col-8">舞蹈</dd>
<dt class="col-4 text-body-secondary">練習歌曲</dt>
<dd class="col-8">Road Fighter</dd>
<dt class="col-4 text-body-secondary">練習時間</dt>
<dd class="col-8">40 分鐘</dd>
</dl>
<div class="d-flex gap-2">
<button type="button" class="btn btn-danger">確認刪除</button>
<a class="btn btn-outline-brand" href="index.html">取消</a>
</div>
</div>
</main>
<footer class="sk-footer text-center py-4">
<small>&copy; 2026 水晶練習室</small>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
+115
View File
@@ -0,0 +1,115 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>編輯練習紀錄|水晶練習室</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="../css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark sk-navbar sticky-top">
<div class="container">
<a class="navbar-brand" href="../index.html"><i class="bi bi-gem me-1"></i>水晶練習室</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-1">
<li class="nav-item"><a class="nav-link" href="../index.html">練習室首頁</a></li>
<li class="nav-item"><a class="nav-link" href="../index.html#today">今日訓練</a></li>
<li class="nav-item"><a class="nav-link active" href="index.html">我的練習紀錄</a></li>
<li class="nav-item"><a class="nav-link disabled" href="#" aria-disabled="true">課程庫</a></li>
<li class="nav-item"><a class="nav-link" href="../index.html#progress">個人進度</a></li>
<li class="nav-item ms-lg-2">
<a class="btn btn-brand btn-sm" href="../admin/index.html">後台管理</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container py-5">
<h1 class="h3 mb-4 text-white"><i class="bi bi-pencil-square me-2"></i>編輯練習紀錄</h1>
<div class="card sk-card sk-surface p-4" style="max-width: 42rem;">
<form>
<div class="row g-3">
<div class="col-md-6">
<label for="practiceDate" class="form-label">練習日期</label>
<input type="date" class="form-control" id="practiceDate" value="2026-07-16">
</div>
<div class="col-md-6">
<label for="practiceType" class="form-label">訓練類型</label>
<select class="form-select" id="practiceType">
<option>vocal</option>
<option selected>舞蹈</option>
<option>Rap</option>
<option>舞台表現</option>
<option>體能</option>
</select>
</div>
<div class="col-md-6">
<label for="practiceSong" class="form-label">練習歌曲</label>
<select class="form-select" id="practiceSong">
<option>Couple</option>
<option selected>Road Fighter</option>
<option>Com' Back</option>
<option>Chivalry</option>
<option>The Way This Guy Lives</option>
</select>
</div>
<div class="col-md-6">
<label for="practiceMinutes" class="form-label">練習分鐘數</label>
<input type="number" class="form-control" id="practiceMinutes" value="40">
</div>
<div class="col-12">
<label for="practiceContent" class="form-label">練習內容</label>
<textarea class="form-control" id="practiceContent" rows="2">整段主歌+副歌走位</textarea>
</div>
<div class="col-12">
<label for="practiceCompletion" class="form-label d-flex justify-content-between">
<span>今日完成度</span>
<span class="text-body-secondary" id="completionValue">85%</span>
</label>
<input type="range" class="form-range" id="practiceCompletion" min="0" max="100" value="85"
oninput="document.getElementById('completionValue').textContent = this.value + '%'">
</div>
<div class="col-12">
<label for="practiceNote" class="form-label">練習心得</label>
<textarea class="form-control" id="practiceNote" rows="3">隊形轉換卡點抓得比昨天準,副歌動作幅度還要再放大。</textarea>
</div>
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="practiceDone" checked>
<label class="form-check-label" for="practiceDone">是否完成</label>
</div>
</div>
</div>
<div class="d-flex gap-2 mt-4">
<button type="submit" class="btn btn-brand">儲存變更</button>
<a class="btn btn-outline-brand" href="index.html">取消</a>
</div>
</form>
</div>
</main>
<footer class="sk-footer text-center py-4">
<small>&copy; 2026 水晶練習室</small>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
+115
View File
@@ -0,0 +1,115 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>我的練習紀錄|水晶練習室</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="../css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark sk-navbar sticky-top">
<div class="container">
<a class="navbar-brand" href="../index.html"><i class="bi bi-gem me-1"></i>水晶練習室</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-1">
<li class="nav-item"><a class="nav-link" href="../index.html">練習室首頁</a></li>
<li class="nav-item"><a class="nav-link" href="../index.html#today">今日訓練</a></li>
<li class="nav-item"><a class="nav-link active" href="index.html">我的練習紀錄</a></li>
<li class="nav-item"><a class="nav-link disabled" href="#" aria-disabled="true">課程庫</a></li>
<li class="nav-item"><a class="nav-link" href="../index.html#progress">個人進度</a></li>
<li class="nav-item ms-lg-2">
<a class="btn btn-brand btn-sm" href="../admin/index.html">後台管理</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container py-5">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-4">
<h1 class="h3 mb-0 text-white"><i class="bi bi-journal-text me-2"></i>我的練習紀錄</h1>
<div class="d-flex gap-2">
<a class="btn btn-brand" href="create.html"><i class="bi bi-plus-lg me-1"></i>新增練習紀錄</a>
<a class="btn btn-outline-brand" href="../index.html">回練習室首頁</a>
</div>
</div>
<div class="card sk-card sk-surface">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>編號</th>
<th>練習日期</th>
<th>訓練類型</th>
<th>練習歌曲</th>
<th>練習內容</th>
<th>練習時間</th>
<th>完成度</th>
<th class="text-end">操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>2026-07-16</td>
<td><span class="badge badge-type badge-type--dance">舞蹈</span></td>
<td>Road Fighter</td>
<td>整段主歌+副歌走位</td>
<td>40 分鐘</td>
<td>85%</td>
<td class="text-end">
<a class="btn btn-sm btn-outline-brand" href="show.html">查看</a>
<a class="btn btn-sm btn-outline-brand" href="edit.html">編輯</a>
<a class="btn btn-sm btn-outline-danger" href="delete.html">刪除</a>
</td>
</tr>
<tr>
<td>2</td>
<td>2026-07-15</td>
<td><span class="badge badge-type badge-type--vocal">vocal</span></td>
<td>Couple</td>
<td>副歌高音音準練習</td>
<td>30 分鐘</td>
<td>70%</td>
<td class="text-end">
<a class="btn btn-sm btn-outline-brand" href="show.html">查看</a>
<a class="btn btn-sm btn-outline-brand" href="edit.html">編輯</a>
<a class="btn btn-sm btn-outline-danger" href="delete.html">刪除</a>
</td>
</tr>
<tr>
<td>3</td>
<td>2026-07-14</td>
<td><span class="badge badge-type badge-type--rap">Rap</span></td>
<td>Chivalry</td>
<td>咬字與節奏卡點</td>
<td>25 分鐘</td>
<td>60%</td>
<td class="text-end">
<a class="btn btn-sm btn-outline-brand" href="show.html">查看</a>
<a class="btn btn-sm btn-outline-brand" href="edit.html">編輯</a>
<a class="btn btn-sm btn-outline-danger" href="delete.html">刪除</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
<footer class="sk-footer text-center py-4">
<small>&copy; 2026 水晶練習室</small>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
+81
View File
@@ -0,0 +1,81 @@
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>練習紀錄詳情|水晶練習室</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="../css/style.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark sk-navbar sticky-top">
<div class="container">
<a class="navbar-brand" href="../index.html"><i class="bi bi-gem me-1"></i>水晶練習室</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-1">
<li class="nav-item"><a class="nav-link" href="../index.html">練習室首頁</a></li>
<li class="nav-item"><a class="nav-link" href="../index.html#today">今日訓練</a></li>
<li class="nav-item"><a class="nav-link active" href="index.html">我的練習紀錄</a></li>
<li class="nav-item"><a class="nav-link disabled" href="#" aria-disabled="true">課程庫</a></li>
<li class="nav-item"><a class="nav-link" href="../index.html#progress">個人進度</a></li>
<li class="nav-item ms-lg-2">
<a class="btn btn-brand btn-sm" href="../admin/index.html">後台管理</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container py-5">
<h1 class="h3 mb-4 text-white"><i class="bi bi-journal-check me-2"></i>練習紀錄詳情</h1>
<div class="card sk-card sk-surface p-4" style="max-width: 42rem;">
<div class="d-flex align-items-center gap-2 mb-4">
<span class="badge badge-type badge-type--dance">舞蹈</span>
<span class="badge badge-status badge-status--done">已完成</span>
</div>
<dl class="row mb-0">
<dt class="col-4 text-body-secondary">練習日期</dt>
<dd class="col-8">2026-07-16</dd>
<dt class="col-4 text-body-secondary">訓練類型</dt>
<dd class="col-8">舞蹈</dd>
<dt class="col-4 text-body-secondary">練習歌曲</dt>
<dd class="col-8">Road Fighter</dd>
<dt class="col-4 text-body-secondary">練習時間</dt>
<dd class="col-8">40 分鐘</dd>
<dt class="col-4 text-body-secondary">完成度</dt>
<dd class="col-8">
<div class="progress sk-progress" style="max-width: 12rem;">
<div class="progress-bar" style="width: 85%" role="progressbar" aria-valuenow="85" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</dd>
<dt class="col-4 text-body-secondary">練習心得</dt>
<dd class="col-8">隊形轉換卡點抓得比昨天準,副歌動作幅度還要再放大。</dd>
</dl>
<div class="d-flex gap-2 mt-4">
<a class="btn btn-brand" href="edit.html">編輯這筆紀錄</a>
<a class="btn btn-outline-brand" href="index.html">回練習紀錄列表</a>
</div>
</div>
</main>
<footer class="sk-footer text-center py-4">
<small>&copy; 2026 水晶練習室</small>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
Options -Indexes
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Deny from all
</IfModule>
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
<!doctype html><title>Not Found</title>
+8
View File
@@ -432,6 +432,14 @@
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div>
</div>
<div class="video-container">
<h2 class="video-title">S5 EP01・180930(神叨字幕組)</h2>
<div class="video-wrapper">
<iframe class="video-embed" title="新西遊記5 EP01 神叨字幕組"
src="//player.bilibili.com/player.html?isOutside=true&bvid=BV12t411U7Ni&p=1" scrolling="no"
border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>
</div>
</div>
</body>
</html>
+551
View File
@@ -0,0 +1,551 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>水晶男孩|追劇清單</title>
<link rel="stylesheet" href="../assets/css/style.css">
<link rel="stylesheet" href="../assets/css/index.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="../assets/css/tablet.css">
<style>
/* ── 追劇清單頁面專用樣式 ── */
.vedio-list-page .vedio-intro {
text-align: left;
}
.vedio-list-page .vedio-intro p {
color: var(--color-text-muted);
font-size: 1rem;
line-height: 1.8;
margin: 0 0 0.85rem;
text-align: left;
}
.vedio-list-page .vedio-intro p:last-child {
margin-bottom: 0;
}
.vedio-list-page .list-lead {
color: var(--color-text-muted);
font-size: 1rem;
margin: 0 0 1.25rem;
text-align: left;
}
/* 類型篩選 */
.vedio-list-page .type-tabs {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin: 0 0 1.75rem;
}
.vedio-list-page .type-tab {
appearance: none;
border: 2px solid rgba(255, 204, 0, 0.22);
background: var(--color-surface-soft);
color: var(--color-text-dark);
font-weight: 700;
font-size: 0.92rem;
padding: 0.45rem 1.1rem;
border-radius: 999px;
cursor: pointer;
transition: border-color 0.18s ease, background 0.18s ease, color 0.18s ease;
white-space: nowrap;
}
.vedio-list-page .type-tab:hover {
border-color: var(--color-brand);
}
.vedio-list-page .type-tab.is-active {
border-color: var(--color-brand);
background: #fffce8;
color: var(--color-brand-dark);
}
/* 卡片格線 */
.vedio-list-page .drama-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
margin: 0 0 2rem;
}
.vedio-list-page .drama-card {
display: flex;
flex-direction: column;
align-items: stretch;
text-align: left;
padding: 1.15rem 1.2rem 1.25rem;
background: var(--color-surface-soft);
border: 2px solid rgba(255, 204, 0, 0.18);
border-radius: 12px;
cursor: pointer;
transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
box-sizing: border-box;
min-height: 100%;
}
.vedio-list-page .drama-card:hover,
.vedio-list-page .drama-card.is-active {
border-color: var(--color-brand);
background: #fffce8;
}
.vedio-list-page .drama-card__poster {
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 16 / 9;
margin: 0 0 0.95rem;
border-radius: 8px;
background:
linear-gradient(145deg, rgba(255, 204, 0, 0.22), rgba(0, 0, 0, 0.08)),
var(--color-surface-soft);
color: var(--color-brand-dark);
font-weight: 800;
font-size: 0.95rem;
letter-spacing: 0.04em;
text-align: center;
padding: 0.75rem;
box-sizing: border-box;
}
.vedio-list-page .drama-card__meta {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 0 0 0.55rem;
}
.vedio-list-page .drama-tag {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.18rem 0.55rem;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.03em;
white-space: nowrap;
background: rgba(255, 204, 0, 0.18);
color: var(--color-brand-dark);
}
.vedio-list-page .drama-tag--platform {
background: rgba(0, 0, 0, 0.06);
color: var(--color-text-muted);
}
.vedio-list-page .drama-card__title {
margin: 0 0 0.45rem;
font-size: 1.15rem;
font-weight: 800;
color: var(--color-text-dark);
line-height: 1.35;
text-align: center;
}
.vedio-list-page .drama-card__progress,
.vedio-list-page .drama-card__note {
margin: 0 0 0.35rem;
color: var(--color-text-muted);
font-size: 0.9rem;
line-height: 1.65;
text-align: left;
}
.vedio-list-page .drama-card__note {
font-size: 0.84rem;
margin-bottom: 0.85rem;
}
.vedio-list-page .drama-card__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: auto;
padding-top: 0.35rem;
}
.vedio-list-page .drama-card__actions a,
.vedio-list-page .drama-card__actions button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 38px;
padding: 0.35rem 0.9rem;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 700;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
border: 2px solid rgba(255, 204, 0, 0.35);
background: #fff;
color: var(--color-brand-dark);
transition: background 0.18s ease, border-color 0.18s ease;
}
.vedio-list-page .drama-card__actions a:hover,
.vedio-list-page .drama-card__actions button:hover {
border-color: var(--color-brand);
background: #fffce8;
}
.vedio-list-page .drama-card__actions a.is-primary {
background: var(--color-brand);
border-color: var(--color-brand);
color: #1a1a1a;
}
/* 詳情區 */
.vedio-list-page .detail-panel {
display: none;
padding: 1.35rem 1.4rem 1.5rem;
border-radius: 12px;
background: var(--color-surface-soft);
border: 1px solid rgba(255, 204, 0, 0.25);
margin-bottom: 1.5rem;
}
.vedio-list-page .detail-panel.is-open {
display: block;
}
.vedio-list-page .detail-panel__eyebrow {
display: block;
text-align: center;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.08em;
color: var(--color-text-muted);
margin: 0 0 0.45rem;
white-space: nowrap;
}
.vedio-list-page .detail-panel__title {
margin: 0 0 0.75rem;
text-align: center;
font-size: 1.35rem;
font-weight: 800;
color: var(--color-brand-dark);
line-height: 1.35;
}
.vedio-list-page .detail-panel p {
margin: 0 0 0.75rem;
color: var(--color-text-muted);
font-size: 0.98rem;
line-height: 1.8;
text-align: left;
}
.vedio-list-page .detail-panel__progress-box {
margin: 1rem 0;
padding: 0.9rem 1rem;
border-radius: 10px;
background: #fffce8;
border: 1px solid rgba(255, 204, 0, 0.35);
}
.vedio-list-page .detail-panel__progress-box p {
margin: 0 0 0.35rem;
}
.vedio-list-page .detail-panel__progress-box p:last-child {
margin-bottom: 0;
}
.vedio-list-page .detail-panel__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 1.1rem;
}
.vedio-list-page .detail-panel__actions a,
.vedio-list-page .detail-panel__actions button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 40px;
padding: 0.4rem 1.05rem;
border-radius: 999px;
font-size: 0.9rem;
font-weight: 700;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
border: 2px solid rgba(255, 204, 0, 0.35);
background: #fff;
color: var(--color-brand-dark);
}
.vedio-list-page .detail-panel__actions a.is-primary {
background: var(--color-brand);
border-color: var(--color-brand);
color: #1a1a1a;
}
.vedio-list-page .empty-hint {
display: none;
text-align: center;
color: var(--color-text-muted);
padding: 1.5rem 1rem;
margin: 0 0 1.5rem;
}
.vedio-list-page .empty-hint.is-visible {
display: block;
}
@media (max-width: 640px) {
.vedio-list-page .drama-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body class="vedio-list-page">
<header class="inner-page-header">
<div class="header-inner site-header-inner">
<h1 class="main-title">深夜清單</h1>
<div class="subtitle-wrap">
<span class="sub-title">DRAMA &amp; VARIETY WATCH LIST</span>
</div>
</div>
</header>
<nav class="portal-nav" aria-label="水晶男孩推廣部導覽" data-portal-nav></nav>
<canvas id="particles"></canvas>
<div class="container">
<section class="vedio-intro">
<h2 class="section-title">正在追的那些夜晚</h2>
<p>
有些夜晚適合考古黃色奇蹟,有些夜晚則是——螢幕一亮,人就栽進另一個世界。
綜藝裡的失控笑聲、戲劇裡的心跳停拍,都值得被好好記下來:看到哪一季、哪一集、哪一句台詞剛把你釘在沙發上。
</p>
<p>
這裡先收兩部最近在追的:一部是把人丟進肯亞還能笑到缺氧的<span class="mark_y">韓綜</span>
一部是把 1997 年的青春寫得又甜又疼的<span class="mark_b">韓劇</span>
點進去,進度與入口都在;先別急著一次追完,慢慢養也很好看。
</p>
<p>
此頁為線上靜態預覽。完整資料庫與後台 CRUD(乙級第三題骨架)請本機 XAMPP 匯入
<code>variety/vedio-list/dramas.sql</code> 後,開啟
<a href="vedio-list/"><code>variety/vedio-list/</code></a>
</p>
</section>
<section class="list-section" aria-labelledby="vedio-list-heading">
<h2 class="section-title" id="vedio-list-heading">追劇清單</h2>
<p class="list-lead">先用類型找——綜藝還是戲劇,跟 OTT 首頁那排分類一樣直覺。</p>
<div class="type-tabs" role="tablist" aria-label="作品類型">
<button type="button" class="type-tab is-active" role="tab" aria-selected="true"
data-type="all">全部</button>
<button type="button" class="type-tab" role="tab" aria-selected="false"
data-type="variety">綜藝</button>
<button type="button" class="type-tab" role="tab" aria-selected="false"
data-type="drama">戲劇</button>
</div>
<div class="drama-grid" id="dramaGrid">
<article class="drama-card" data-type="variety" data-id="kenya" tabindex="0"
aria-label="三傻遊肯亞,綜藝">
<div class="drama-card__poster" aria-hidden="true">NETFLIX · 綜藝</div>
<div class="drama-card__meta">
<span class="drama-tag">綜藝</span>
<span class="drama-tag drama-tag--platform">Netflix</span>
</div>
<h3 class="drama-card__title">三傻遊肯亞</h3>
<p class="drama-card__progress">觀看進度:第 1 季・第 2 集・看到 28:40</p>
<p class="drama-card__note">三人剛被扔進野外,表情比獅子還慌。</p>
<div class="drama-card__actions">
<a class="is-primary" href="https://www.netflix.com/title/82031207"
target="_blank" rel="noopener noreferrer"
onclick="event.stopPropagation()">繼續觀看</a>
<button type="button" data-open-detail="kenya">查看詳情</button>
</div>
</article>
<article class="drama-card" data-type="drama" data-id="reply1997" tabindex="0"
aria-label="請回答1997,戲劇">
<div class="drama-card__poster" aria-hidden="true">DISNEY+ · 戲劇</div>
<div class="drama-card__meta">
<span class="drama-tag">戲劇</span>
<span class="drama-tag drama-tag--platform">Disney+</span>
</div>
<h3 class="drama-card__title">請回答1997</h3>
<p class="drama-card__progress">觀看進度:第 1 季・第 5 集・看到 41:12</p>
<p class="drama-card__note">宿舍夜談剛結束,收音機還在響,心卻已經開始亂。</p>
<div class="drama-card__actions">
<a class="is-primary" href="https://www.disneyplus.com/en-tw/browse/entity-3979fb82-8681-44e8-b7be-e313091f0d73"
target="_blank" rel="noopener noreferrer"
onclick="event.stopPropagation()">繼續觀看</a>
<button type="button" data-open-detail="reply1997">查看詳情</button>
</div>
</article>
</div>
<p class="empty-hint" id="emptyHint" role="status">這個類型目前還沒有作品——換「全部」看看?</p>
<div class="detail-panel" id="detailPanel" aria-live="polite">
<span class="detail-panel__eyebrow" id="detailEyebrow">單部作品</span>
<h3 class="detail-panel__title" id="detailTitle"></h3>
<p id="detailIntro"></p>
<div class="detail-panel__progress-box">
<p id="detailProgress"></p>
<p id="detailNote"></p>
</div>
<p id="detailExtra"></p>
<div class="detail-panel__actions">
<a class="is-primary" id="detailWatch" href="#" target="_blank" rel="noopener noreferrer">繼續觀看</a>
<button type="button" id="detailClose">先收起來</button>
</div>
</div>
</section>
</div>
<footer>
<p>SECHSKIES Fanpage &copy; 2026 - 黃色氣球永遠飄揚</p>
<a href="https://jekkinoopy.github.io/Portfolio/" class="studio-link" target="_blank" rel="noopener noreferrer">
Design by <span class="mark_b">Jekkinoopy Studio</span>
</a>
</footer>
<script>
(function () {
const dramas = {
kenya: {
typeLabel: '綜藝|Netflix',
title: '三傻遊肯亞',
intro: '三個人、一座肯亞,以及無數個「這也能出事?」的現場。笑點來得又急又野,像把旅行節目的安全感整袋拆掉,只留下腎上腺素與默契。',
progress: '第 1 季|第 2 集|28:40',
note: '三人剛被扔進野外,表情比獅子還慌。',
extra: '狀態:追劇中 · 開始看的那天:最近才入坑,進度還很乖。',
watchUrl: 'https://www.netflix.com/title/82031207'
},
reply1997: {
typeLabel: '戲劇|Disney+',
title: '請回答1997',
intro: '把 1997 寫進宿舍、收音機與那一聲來不及說出口的喜歡。甜的時候甜到發亮,疼的時候又老實得讓人不敢快轉——那種青春,看完會想立刻打電話給誰。',
progress: '第 1 季|第 5 集|41:12',
note: '宿舍夜談剛結束,收音機還在響,心卻已經開始亂。',
extra: '狀態:追劇中 · 提醒自己:下一集容易哭,衛生紙先備好。',
watchUrl: 'https://www.disneyplus.com/en-tw/browse/entity-3979fb82-8681-44e8-b7be-e313091f0d73'
}
};
const tabs = document.querySelectorAll('.type-tab');
const cards = document.querySelectorAll('.drama-card');
const emptyHint = document.getElementById('emptyHint');
const detailPanel = document.getElementById('detailPanel');
const detailEyebrow = document.getElementById('detailEyebrow');
const detailTitle = document.getElementById('detailTitle');
const detailIntro = document.getElementById('detailIntro');
const detailProgress = document.getElementById('detailProgress');
const detailNote = document.getElementById('detailNote');
const detailExtra = document.getElementById('detailExtra');
const detailWatch = document.getElementById('detailWatch');
const detailClose = document.getElementById('detailClose');
function setFilter(type) {
tabs.forEach(function (tab) {
const active = tab.getAttribute('data-type') === type;
tab.classList.toggle('is-active', active);
tab.setAttribute('aria-selected', active ? 'true' : 'false');
});
let visible = 0;
cards.forEach(function (card) {
const show = type === 'all' || card.getAttribute('data-type') === type;
card.hidden = !show;
if (show) visible += 1;
});
emptyHint.classList.toggle('is-visible', visible === 0);
const activeCard = document.querySelector('.drama-card.is-active');
if (activeCard && activeCard.hidden) {
closeDetail();
}
}
function openDetail(id) {
const data = dramas[id];
if (!data) return;
cards.forEach(function (card) {
card.classList.toggle('is-active', card.getAttribute('data-id') === id);
});
detailEyebrow.textContent = data.typeLabel;
detailTitle.textContent = data.title;
detailIntro.textContent = data.intro;
detailProgress.textContent = '觀看進度:' + data.progress;
detailNote.textContent = data.note;
detailExtra.textContent = data.extra;
detailWatch.href = data.watchUrl;
detailPanel.classList.add('is-open');
detailPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function closeDetail() {
detailPanel.classList.remove('is-open');
cards.forEach(function (card) {
card.classList.remove('is-active');
});
}
tabs.forEach(function (tab) {
tab.addEventListener('click', function () {
setFilter(tab.getAttribute('data-type'));
});
});
cards.forEach(function (card) {
card.addEventListener('click', function (event) {
if (event.target.closest('a, button')) return;
openDetail(card.getAttribute('data-id'));
});
card.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
openDetail(card.getAttribute('data-id'));
}
});
});
document.querySelectorAll('[data-open-detail]').forEach(function (btn) {
btn.addEventListener('click', function (event) {
event.stopPropagation();
openDetail(btn.getAttribute('data-open-detail'));
});
});
detailClose.addEventListener('click', closeDetail);
})();
</script>
<script src="../assets/js/particles.js"></script>
<script src="../assets/js/portal-nav.js"></script>
</body>
</html>
+44
View File
@@ -0,0 +1,44 @@
<?php include_once "./api/db.php"; ?>
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>深夜清單|管理後台</title>
<link rel="stylesheet" href="../../assets/css/style.css">
<link rel="stylesheet" href="css/vedio-list.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
</head>
<body class="vedio-admin-page">
<header class="inner-page-header">
<div class="header-inner site-header-inner">
<h1 class="main-title">深夜清單後台</h1>
<div class="subtitle-wrap">
<span class="sub-title">ADMIN · CRUD</span>
</div>
</div>
</header>
<div class="container admin-wrap">
<nav class="admin-nav">
<a href="index.php">回前台</a>
<a href="admin.php">後台首頁</a>
<a href="?do=drama">追劇管理</a>
<a href="?do=add">新增作品</a>
</nav>
<?php
$do = $_GET['do'] ?? 'main';
$file = "back/$do.php";
if (file_exists($file)) {
include $file;
} else {
include "back/main.php";
}
?>
</div>
</body>
</html>
+125
View File
@@ -0,0 +1,125 @@
<?php
session_start();
date_default_timezone_set("Asia/Taipei");
class DB
{
protected $dsn = "mysql:host=localhost;charset=utf8;dbname=vedio_list";
protected $pdo;
protected $table;
function __construct($table)
{
$this->table = $table;
$this->pdo = new PDO($this->dsn, 'root', '');
}
function all(...$arg)
{
$sql = "SELECT * FROM `$this->table` ";
if (isset($arg[0])) {
if (is_array($arg[0])) {
$tmp = $this->a2s($arg[0]);
$sql .= " WHERE " . join(" AND ", $tmp);
} else {
$sql .= $arg[0];
}
}
if (isset($arg[1])) {
$sql .= $arg[1];
}
return $this->pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
}
function count(...$arg)
{
$sql = "SELECT count(*) FROM `$this->table`";
if (isset($arg[0])) {
if (is_array($arg[0])) {
$tmp = $this->a2s($arg[0]);
$sql .= " WHERE " . join(" AND ", $tmp);
} else {
$sql .= $arg[0];
}
}
if (isset($arg[1])) {
$sql .= $arg[1];
}
return $this->pdo->query($sql)->fetchColumn();
}
function find($arg)
{
$sql = "SELECT * FROM `$this->table` ";
if (is_array($arg)) {
$tmp = $this->a2s($arg);
$sql .= " WHERE " . join(" AND ", $tmp);
} else {
$sql .= " WHERE `id`='$arg'";
}
return $this->pdo->query($sql)->fetch(PDO::FETCH_ASSOC);
}
function save($arg)
{
if (isset($arg['id'])) {
$tmp = $this->a2s($arg);
$sql = "UPDATE `$this->table` SET " . join(" , ", $tmp);
$sql .= " WHERE `id`='{$arg['id']}'";
} else {
$keys = array_keys($arg);
$sql = "INSERT INTO `$this->table` (`" . join("`,`", $keys) . "`) VALUES('" . join("','", $arg) . "');";
}
return $this->pdo->exec($sql);
}
function del($arg)
{
$sql = "DELETE FROM `$this->table` ";
if (is_array($arg)) {
$tmp = $this->a2s($arg);
$sql .= " WHERE " . join(" AND ", $tmp);
} else {
$sql .= " WHERE `id`='$arg'";
}
return $this->pdo->exec($sql);
}
protected function a2s($array)
{
$tmp = [];
foreach ($array as $key => $val) {
$tmp[] = "`$key`='$val'";
}
return $tmp;
}
function q($sql)
{
return $this->pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
}
}
function dd($array)
{
echo "<pre>";
print_r($array);
echo "</pre>";
}
function to($url)
{
header("location:$url");
}
$Drama = new DB('dramas');
+5
View File
@@ -0,0 +1,5 @@
<?php
include_once "db.php";
$table = ${$_POST['table']};
$table->del($_POST['id']);
+6
View File
@@ -0,0 +1,6 @@
<?php
include_once "db.php";
$row = $Drama->find($_POST['id']);
$row['sh'] = ((int)$row['sh'] + 1) % 2;
$Drama->save($row);
+13
View File
@@ -0,0 +1,13 @@
<?php
include_once "db.php";
$table = ${$_POST['table']};
$row1 = $table->find($_POST['ids'][0]);
$row2 = $table->find($_POST['ids'][1]);
$tmp = $row1['rank'];
$row1['rank'] = $row2['rank'];
$row2['rank'] = $tmp;
$table->save($row1);
$table->save($row2);
+96
View File
@@ -0,0 +1,96 @@
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$maxRank = $Drama->q("SELECT MAX(`rank`) AS m FROM `dramas`");
$nextRank = ((int)($maxRank[0]['m'] ?? 0)) + 1;
$data = [
'title' => trim($_POST['title'] ?? ''),
'type' => $_POST['type'] ?? 'drama',
'platform' => $_POST['platform'] ?? '其他',
'watch_url' => trim($_POST['watch_url'] ?? ''),
'current_season' => max(1, (int)($_POST['current_season'] ?? 1)),
'current_episode' => max(0, (int)($_POST['current_episode'] ?? 0)),
'current_position' => trim($_POST['current_position'] ?? ''),
'progress_note' => trim($_POST['progress_note'] ?? ''),
'status' => $_POST['status'] ?? 'want',
'rating' => $_POST['rating'] !== '' ? $_POST['rating'] : null,
'intro' => trim($_POST['intro'] ?? ''),
'note' => trim($_POST['note'] ?? ''),
'sh' => 1,
'rank' => $nextRank,
];
// DB::save 用字串拼接;null 改空字串避免 SQL 破掉
if ($data['rating'] === null) {
$data['rating'] = '';
}
$Drama->save($data);
to("?do=drama");
exit;
}
?>
<section>
<h2 class="section-title">新增作品</h2>
<form class="admin-form" method="post" action="?do=add">
<label for="title">劇名/節目名</label>
<input type="text" id="title" name="title" required>
<label for="type">類型</label>
<select id="type" name="type">
<option value="variety">綜藝</option>
<option value="drama" selected>戲劇</option>
</select>
<label for="platform">上架平台</label>
<select id="platform" name="platform">
<option value="Netflix">Netflix</option>
<option value="Disney+">Disney+</option>
<option value="friDay影音">friDay影音</option>
<option value="愛奇藝">愛奇藝</option>
<option value="LINE TV">LINE TV</option>
<option value="YouTube">YouTube</option>
<option value="其他">其他</option>
</select>
<label for="watch_url">直接觀看網址</label>
<input type="url" id="watch_url" name="watch_url" placeholder="https://...">
<label for="current_season">目前看到第幾季</label>
<input type="number" id="current_season" name="current_season" min="1" value="1">
<label for="current_episode">目前看到第幾集</label>
<input type="number" id="current_episode" name="current_episode" min="0" value="0">
<label for="current_position">這一集看到哪裡</label>
<input type="text" id="current_position" name="current_position" placeholder="例如 35:20">
<label for="progress_note">進度情節備註</label>
<input type="text" id="progress_note" name="progress_note" placeholder="例如:女主角剛看到手機裡的影片">
<label for="status">狀態</label>
<select id="status" name="status">
<option value="want">想看</option>
<option value="watching" selected>追劇中</option>
<option value="done">已追完</option>
<option value="paused">暫停</option>
<option value="dropped">棄劇</option>
</select>
<label for="rating">評分</label>
<input type="number" id="rating" name="rating" min="0" max="10" step="0.1" placeholder="例如 8.5">
<label for="intro">簡介</label>
<textarea id="intro" name="intro" rows="4"></textarea>
<label for="note">心得備註</label>
<textarea id="note" name="note" rows="3"></textarea>
<div class="form-actions" style="margin-top:1.25rem;display:flex;gap:10px;justify-content:center;">
<button type="submit" class="is-primary" style="min-height:40px;padding:0.4rem 1.05rem;border-radius:999px;border:2px solid var(--color-brand);background:var(--color-brand);font-weight:700;cursor:pointer;">儲存</button>
<a href="?do=drama" style="min-height:40px;padding:0.4rem 1.05rem;border-radius:999px;border:2px solid rgba(255,204,0,0.35);background:#fff;font-weight:700;text-decoration:none;display:inline-flex;align-items:center;">取消</a>
</div>
</form>
</section>
+71
View File
@@ -0,0 +1,71 @@
<?php
$typeLabels = [
'variety' => '綜藝',
'drama' => '戲劇',
];
$rows = $Drama->all(" ORDER BY `rank` ASC, `id` ASC");
?>
<section>
<h2 class="section-title">追劇管理</h2>
<div class="admin-toolbar">
<a href="?do=add">新增作品</a>
</div>
<div class="drama-list">
<?php foreach ($rows as $idx => $row): ?>
<?php
$prev = ($idx === 0) ? $row['id'] : $rows[$idx - 1]['id'];
$next = ($idx === count($rows) - 1) ? $row['id'] : $rows[$idx + 1]['id'];
$typeText = $typeLabels[$row['type']] ?? $row['type'];
?>
<div class="drama-row">
<div class="drama-row__info">
<strong><?= htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8') ?></strong><br>
<?= htmlspecialchars($typeText . '' . $row['platform'], ENT_QUOTES, 'UTF-8') ?><br>
進度:第 <?= (int)$row['current_season'] ?> 季・第 <?= (int)$row['current_episode'] ?> 集
<?php if (!empty($row['current_position'])): ?>
<?= htmlspecialchars($row['current_position'], ENT_QUOTES, 'UTF-8') ?>
<?php endif; ?>
</div>
<div class="drama-row__actions">
<button type="button" class="show" data-id="<?= (int)$row['id'] ?>">
<?= ((int)$row['sh'] === 1) ? '顯示' : '隱藏' ?>
</button>
<button type="button" class="switch-rank" data-ids="<?= (int)$row['id'] . '-' . (int)$prev ?>">往上</button>
<button type="button" class="switch-rank" data-ids="<?= (int)$row['id'] . '-' . (int)$next ?>">往下</button>
<a class="btn-like" href="?do=edit&amp;id=<?= (int)$row['id'] ?>">編輯</a>
<button type="button" class="del-drama" data-id="<?= (int)$row['id'] ?>">刪除</button>
</div>
</div>
<?php endforeach; ?>
<?php if (count($rows) === 0): ?>
<p class="admin-lead">尚無資料,先新增一部作品吧。</p>
<?php endif; ?>
</div>
</section>
<script>
$(".switch-rank").on("click", function () {
let ids = $(this).data("ids").toString().split("-");
$.post("./api/sw.php", { ids, table: "Drama" }, function () {
location.reload();
});
});
$(".show").on("click", function () {
let id = $(this).data("id");
$.post("./api/show.php", { id }, function () {
location.reload();
});
});
$(".del-drama").on("click", function () {
if (!confirm("確定刪除這部作品?")) return;
let id = $(this).data("id");
$.post("./api/del.php", { id, table: "Drama" }, function () {
location.reload();
});
});
</script>
+107
View File
@@ -0,0 +1,107 @@
<?php
$id = $_GET['id'] ?? 0;
$row = $Drama->find($id);
if (!$row) {
echo '<p class="admin-lead">找不到這筆資料。</p>';
return;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$row['title'] = trim($_POST['title'] ?? $row['title']);
$row['type'] = $_POST['type'] ?? $row['type'];
$row['platform'] = $_POST['platform'] ?? $row['platform'];
$row['watch_url'] = trim($_POST['watch_url'] ?? '');
$row['current_season'] = max(1, (int)($_POST['current_season'] ?? 1));
$row['current_episode'] = max(0, (int)($_POST['current_episode'] ?? 0));
$row['current_position'] = trim($_POST['current_position'] ?? '');
$row['progress_note'] = trim($_POST['progress_note'] ?? '');
$row['status'] = $_POST['status'] ?? $row['status'];
$row['rating'] = ($_POST['rating'] !== '') ? $_POST['rating'] : '';
$row['intro'] = trim($_POST['intro'] ?? '');
$row['note'] = trim($_POST['note'] ?? '');
$Drama->save($row);
to("?do=drama");
exit;
}
?>
<section>
<h2 class="section-title">編輯作品</h2>
<form class="admin-form" method="post" action="?do=edit&amp;id=<?= (int)$row['id'] ?>">
<label for="title">劇名/節目名</label>
<input type="text" id="title" name="title" required
value="<?= htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8') ?>">
<label for="type">類型</label>
<select id="type" name="type">
<option value="variety"<?= $row['type'] === 'variety' ? ' selected' : '' ?>>綜藝</option>
<option value="drama"<?= $row['type'] === 'drama' ? ' selected' : '' ?>>戲劇</option>
</select>
<label for="platform">上架平台</label>
<select id="platform" name="platform">
<?php
$platforms = ['Netflix', 'Disney+', 'friDay影音', '愛奇藝', 'LINE TV', 'YouTube', '其他'];
foreach ($platforms as $p):
?>
<option value="<?= htmlspecialchars($p, ENT_QUOTES, 'UTF-8') ?>"<?= $row['platform'] === $p ? ' selected' : '' ?>>
<?= htmlspecialchars($p, ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
<label for="watch_url">直接觀看網址</label>
<input type="url" id="watch_url" name="watch_url"
value="<?= htmlspecialchars($row['watch_url'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
placeholder="https://...">
<label for="current_season">目前看到第幾季</label>
<input type="number" id="current_season" name="current_season" min="1"
value="<?= (int)$row['current_season'] ?>">
<label for="current_episode">目前看到第幾集</label>
<input type="number" id="current_episode" name="current_episode" min="0"
value="<?= (int)$row['current_episode'] ?>">
<label for="current_position">這一集看到哪裡</label>
<input type="text" id="current_position" name="current_position"
value="<?= htmlspecialchars($row['current_position'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
<label for="progress_note">進度情節備註</label>
<input type="text" id="progress_note" name="progress_note"
value="<?= htmlspecialchars($row['progress_note'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
<label for="status">狀態</label>
<select id="status" name="status">
<?php
$statuses = [
'want' => '想看',
'watching' => '追劇中',
'done' => '已追完',
'paused' => '暫停',
'dropped' => '棄劇',
];
foreach ($statuses as $key => $label):
?>
<option value="<?= $key ?>"<?= $row['status'] === $key ? ' selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
<label for="rating">評分</label>
<input type="number" id="rating" name="rating" min="0" max="10" step="0.1"
value="<?= htmlspecialchars($row['rating'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
<label for="intro">簡介</label>
<textarea id="intro" name="intro" rows="4"><?= htmlspecialchars($row['intro'] ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
<label for="note">心得備註</label>
<textarea id="note" name="note" rows="3"><?= htmlspecialchars($row['note'] ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
<div class="form-actions" style="margin-top:1.25rem;display:flex;gap:10px;justify-content:center;">
<button type="submit" class="is-primary" style="min-height:40px;padding:0.4rem 1.05rem;border-radius:999px;border:2px solid var(--color-brand);background:var(--color-brand);font-weight:700;cursor:pointer;">儲存</button>
<a href="?do=drama" style="min-height:40px;padding:0.4rem 1.05rem;border-radius:999px;border:2px solid rgba(255,204,0,0.35);background:#fff;font-weight:700;text-decoration:none;display:inline-flex;align-items:center;">取消</a>
</div>
</form>
</section>
+7
View File
@@ -0,0 +1,7 @@
<section>
<h2 class="section-title">後台首頁</h2>
<p class="admin-lead">乙級第三題骨架:清單、詳情、進度、後台 CRUD。訂票不做。</p>
<div class="admin-toolbar">
<a href="?do=drama">進入追劇管理</a>
</div>
</section>
+429
View File
@@ -0,0 +1,429 @@
/* ── 追劇清單/後台專用樣式 ── */
.vedio-list-page .vedio-intro {
text-align: left;
}
.vedio-list-page .vedio-intro p {
color: var(--color-text-muted);
font-size: 1rem;
line-height: 1.8;
margin: 0 0 0.85rem;
text-align: left;
}
.vedio-list-page .vedio-intro p:last-child {
margin-bottom: 0;
}
.vedio-list-page .list-lead {
color: var(--color-text-muted);
font-size: 1rem;
margin: 0 0 1.25rem;
text-align: left;
}
.vedio-list-page .type-tabs {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin: 0 0 1.75rem;
}
.vedio-list-page .type-tab {
display: inline-flex;
align-items: center;
justify-content: center;
border: 2px solid rgba(255, 204, 0, 0.22);
background: var(--color-surface-soft);
color: var(--color-text-dark);
font-weight: 700;
font-size: 0.92rem;
padding: 0.45rem 1.1rem;
border-radius: 999px;
text-decoration: none;
white-space: nowrap;
transition: border-color 0.18s ease, background 0.18s ease, color 0.18s ease;
}
.vedio-list-page .type-tab:hover {
border-color: var(--color-brand);
}
.vedio-list-page .type-tab.is-active {
border-color: var(--color-brand);
background: #fffce8;
color: var(--color-brand-dark);
}
.vedio-list-page .drama-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
margin: 0 0 2rem;
}
.vedio-list-page .drama-card {
display: flex;
flex-direction: column;
align-items: stretch;
text-align: left;
padding: 1.15rem 1.2rem 1.25rem;
background: var(--color-surface-soft);
border: 2px solid rgba(255, 204, 0, 0.18);
border-radius: 12px;
box-sizing: border-box;
min-height: 100%;
}
.vedio-list-page .drama-card:hover {
border-color: var(--color-brand);
background: #fffce8;
}
.vedio-list-page .drama-card__poster {
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 16 / 9;
margin: 0 0 0.95rem;
border-radius: 8px;
background:
linear-gradient(145deg, rgba(255, 204, 0, 0.22), rgba(0, 0, 0, 0.08)),
var(--color-surface-soft);
color: var(--color-brand-dark);
font-weight: 800;
font-size: 0.95rem;
letter-spacing: 0.04em;
text-align: center;
padding: 0.75rem;
box-sizing: border-box;
}
.vedio-list-page .drama-card__meta {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 0 0 0.55rem;
}
.vedio-list-page .drama-tag {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.18rem 0.55rem;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.03em;
white-space: nowrap;
background: rgba(255, 204, 0, 0.18);
color: var(--color-brand-dark);
}
.vedio-list-page .drama-tag--platform {
background: rgba(0, 0, 0, 0.06);
color: var(--color-text-muted);
}
.vedio-list-page .drama-card__title {
margin: 0 0 0.45rem;
font-size: 1.15rem;
font-weight: 800;
color: var(--color-text-dark);
line-height: 1.35;
text-align: center;
}
.vedio-list-page .drama-card__progress,
.vedio-list-page .drama-card__note {
margin: 0 0 0.35rem;
color: var(--color-text-muted);
font-size: 0.9rem;
line-height: 1.65;
text-align: left;
}
.vedio-list-page .drama-card__note {
font-size: 0.84rem;
margin-bottom: 0.85rem;
}
.vedio-list-page .drama-card__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: auto;
padding-top: 0.35rem;
}
.vedio-list-page .drama-card__actions a {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 38px;
padding: 0.35rem 0.9rem;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 700;
text-decoration: none;
white-space: nowrap;
border: 2px solid rgba(255, 204, 0, 0.35);
background: #fff;
color: var(--color-brand-dark);
}
.vedio-list-page .drama-card__actions a:hover {
border-color: var(--color-brand);
background: #fffce8;
}
.vedio-list-page .drama-card__actions a.is-primary {
background: var(--color-brand);
border-color: var(--color-brand);
color: #1a1a1a;
}
.vedio-list-page .detail-block {
padding: 1.35rem 1.4rem 1.5rem;
border-radius: 12px;
background: var(--color-surface-soft);
border: 1px solid rgba(255, 204, 0, 0.25);
margin-bottom: 1.5rem;
}
.vedio-list-page .detail-block__eyebrow {
display: block;
text-align: center;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.08em;
color: var(--color-text-muted);
margin: 0 0 0.45rem;
}
.vedio-list-page .detail-block__title {
margin: 0 0 0.75rem;
text-align: center;
font-size: 1.35rem;
font-weight: 800;
color: var(--color-brand-dark);
line-height: 1.35;
}
.vedio-list-page .detail-block p {
margin: 0 0 0.75rem;
color: var(--color-text-muted);
font-size: 0.98rem;
line-height: 1.8;
text-align: left;
}
.vedio-list-page .detail-block__progress-box {
margin: 1rem 0;
padding: 0.9rem 1rem;
border-radius: 10px;
background: #fffce8;
border: 1px solid rgba(255, 204, 0, 0.35);
}
.vedio-list-page .detail-block__progress-box p {
margin: 0 0 0.35rem;
}
.vedio-list-page .detail-block__progress-box p:last-child {
margin-bottom: 0;
}
.vedio-list-page .detail-block__actions,
.vedio-list-page .form-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 1.1rem;
}
.vedio-list-page .detail-block__actions a,
.vedio-list-page .form-actions a,
.vedio-list-page .form-actions button,
.vedio-list-page .progress-form button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 40px;
padding: 0.4rem 1.05rem;
border-radius: 999px;
font-size: 0.9rem;
font-weight: 700;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
border: 2px solid rgba(255, 204, 0, 0.35);
background: #fff;
color: var(--color-brand-dark);
}
.vedio-list-page .detail-block__actions a.is-primary,
.vedio-list-page .form-actions button.is-primary,
.vedio-list-page .progress-form button.is-primary {
background: var(--color-brand);
border-color: var(--color-brand);
color: #1a1a1a;
}
.vedio-list-page .empty-hint {
text-align: center;
color: var(--color-text-muted);
padding: 1.5rem 1rem;
margin: 0 0 1.5rem;
}
.vedio-list-page .progress-form,
.vedio-admin-page .admin-form {
max-width: 560px;
margin: 0 auto 2rem;
padding: 1.25rem 1.35rem;
background: var(--color-surface-soft);
border: 1px solid rgba(255, 204, 0, 0.25);
border-radius: 12px;
}
.vedio-list-page .progress-form label,
.vedio-admin-page .admin-form label {
display: block;
margin: 0.85rem 0 0.35rem;
font-weight: 700;
color: var(--color-text-dark);
text-align: left;
}
.vedio-list-page .progress-form input,
.vedio-list-page .progress-form select,
.vedio-list-page .progress-form textarea,
.vedio-admin-page .admin-form input,
.vedio-admin-page .admin-form select,
.vedio-admin-page .admin-form textarea {
width: 100%;
box-sizing: border-box;
padding: 0.55rem 0.7rem;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 8px;
font-size: 0.95rem;
}
.vedio-list-page .vedio-admin-link,
.vedio-admin-page .admin-nav {
text-align: center;
margin: 1rem 0;
}
.vedio-list-page .vedio-admin-link a {
color: var(--color-brand-dark);
font-weight: 700;
}
.vedio-admin-page .admin-wrap {
padding-top: 1.5rem;
padding-bottom: 2.5rem;
}
.vedio-admin-page .admin-nav {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-bottom: 1.5rem;
}
.vedio-admin-page .admin-nav a {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.4rem 1rem;
border-radius: 999px;
border: 2px solid rgba(255, 204, 0, 0.35);
background: #fffce8;
color: var(--color-brand-dark);
font-weight: 700;
text-decoration: none;
white-space: nowrap;
}
.vedio-admin-page .admin-lead {
text-align: center;
color: var(--color-text-muted);
margin: 0 0 1.25rem;
}
.vedio-admin-page .drama-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: space-between;
align-items: center;
padding: 12px 14px;
margin: 0 0 10px;
background: #fff;
border: 1px solid rgba(255, 204, 0, 0.25);
border-radius: 10px;
color: #222;
}
.vedio-admin-page .drama-row__info {
flex: 1 1 220px;
text-align: left;
}
.vedio-admin-page .drama-row__actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: flex-end;
}
.vedio-admin-page .drama-row__actions button,
.vedio-admin-page .drama-row__actions a.btn-like {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 34px;
padding: 0.25rem 0.7rem;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 6px;
background: #fffce8;
color: #222;
font-size: 0.85rem;
font-weight: 700;
cursor: pointer;
text-decoration: none;
white-space: nowrap;
}
.vedio-admin-page .admin-toolbar {
text-align: center;
margin: 0 0 1rem;
}
.vedio-admin-page .admin-toolbar a {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.45rem 1.1rem;
border-radius: 999px;
background: var(--color-brand);
color: #1a1a1a;
font-weight: 800;
text-decoration: none;
}
@media (max-width: 640px) {
.vedio-list-page .drama-grid {
grid-template-columns: 1fr;
}
}
+62
View File
@@ -0,0 +1,62 @@
-- 深夜清單 / 追劇資料庫
-- 本機 XAMPPphpMyAdmin 匯入此檔即可
CREATE DATABASE IF NOT EXISTS `vedio_list` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `vedio_list`;
DROP TABLE IF EXISTS `dramas`;
CREATE TABLE `dramas` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL COMMENT '劇名/節目名',
`type` varchar(20) NOT NULL DEFAULT 'drama' COMMENT 'variety=綜藝, drama=戲劇',
`platform` varchar(50) NOT NULL COMMENT '上架平台',
`watch_url` varchar(500) DEFAULT NULL COMMENT '作品直接觀看網址',
`current_season` int(10) UNSIGNED NOT NULL DEFAULT 1 COMMENT '目前第幾季',
`current_episode` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '目前第幾集',
`current_position` varchar(50) DEFAULT NULL COMMENT '該集看到的位置',
`progress_note` varchar(255) DEFAULT NULL COMMENT '進度情節備註',
`status` varchar(20) NOT NULL DEFAULT 'watching' COMMENT 'want/watching/done/paused/dropped',
`rating` decimal(3,1) DEFAULT NULL COMMENT '評分',
`intro` text COMMENT '簡介',
`note` text COMMENT '心得備註',
`sh` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '是否顯示',
`rank` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '排列順序',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `dramas`
(`title`, `type`, `platform`, `watch_url`, `current_season`, `current_episode`, `current_position`, `progress_note`, `status`, `rating`, `intro`, `note`, `sh`, `rank`)
VALUES
(
'三傻遊肯亞',
'variety',
'Netflix',
'https://www.netflix.com/title/82031207',
1,
2,
'28:40',
'三人剛被扔進野外,表情比獅子還慌。',
'watching',
8.5,
'三個人、一座肯亞,以及無數個「這也能出事?」的現場。笑點來得又急又野,像把旅行節目的安全感整袋拆掉,只留下腎上腺素與默契。',
'羅PD 首登 Netflix;志源在裡面,小黃更沒有藉口不補。',
1,
1
),
(
'請回答1997',
'drama',
'Disney+',
'https://www.disneyplus.com/en-tw/browse/entity-3979fb82-8681-44e8-b7be-e313091f0d73',
1,
5,
'41:12',
'宿舍夜談剛結束,收音機還在響,心卻已經開始亂。',
'watching',
9.0,
'把 1997 寫進宿舍、收音機與那一聲來不及說出口的喜歡。甜的時候甜到發亮,疼的時候又老實得讓人不敢快轉。',
'下一集容易哭,衛生紙先備好。',
1,
2
);
+65
View File
@@ -0,0 +1,65 @@
<?php
$id = $_GET['id'] ?? 0;
$row = $Drama->find($id);
if (!$row || (int)$row['sh'] !== 1) {
echo '<p class="empty-hint">找不到這部作品,或它目前未公開顯示。</p>';
echo '<p class="empty-hint"><a href="?do=main">回清單</a></p>';
return;
}
$typeLabels = [
'variety' => '綜藝',
'drama' => '戲劇',
];
$statusLabels = [
'want' => '想看',
'watching' => '追劇中',
'done' => '已追完',
'paused' => '暫停',
'dropped' => '棄劇',
];
$typeText = $typeLabels[$row['type']] ?? $row['type'];
$statusText = $statusLabels[$row['status']] ?? $row['status'];
?>
<section class="detail-block">
<span class="detail-block__eyebrow"><?= htmlspecialchars($typeText . '' . $row['platform'], ENT_QUOTES, 'UTF-8') ?></span>
<h2 class="detail-block__title"><?= htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8') ?></h2>
<?php if (!empty($row['intro'])): ?>
<p><?= nl2br(htmlspecialchars($row['intro'], ENT_QUOTES, 'UTF-8')) ?></p>
<?php endif; ?>
<div class="detail-block__progress-box">
<p>
觀看進度:第 <?= (int)$row['current_season'] ?> 季|第 <?= (int)$row['current_episode'] ?> 集
<?php if (!empty($row['current_position'])): ?>
<?= htmlspecialchars($row['current_position'], ENT_QUOTES, 'UTF-8') ?>
<?php endif; ?>
</p>
<?php if (!empty($row['progress_note'])): ?>
<p><?= htmlspecialchars($row['progress_note'], ENT_QUOTES, 'UTF-8') ?></p>
<?php endif; ?>
</div>
<p>狀態:<?= htmlspecialchars($statusText, ENT_QUOTES, 'UTF-8') ?>
<?php if ($row['rating'] !== null && $row['rating'] !== ''): ?>
 · 評分:<?= htmlspecialchars($row['rating'], ENT_QUOTES, 'UTF-8') ?>
<?php endif; ?>
</p>
<?php if (!empty($row['note'])): ?>
<p><?= nl2br(htmlspecialchars($row['note'], ENT_QUOTES, 'UTF-8')) ?></p>
<?php endif; ?>
<div class="detail-block__actions">
<?php if (!empty($row['watch_url'])): ?>
<a class="is-primary" href="<?= htmlspecialchars($row['watch_url'], ENT_QUOTES, 'UTF-8') ?>"
target="_blank" rel="noopener noreferrer">繼續觀看</a>
<?php endif; ?>
<a href="?do=progress&amp;id=<?= (int)$row['id'] ?>">更新進度</a>
<a href="?do=main">回清單</a>
</div>
</section>
+84
View File
@@ -0,0 +1,84 @@
<?php
$typeLabels = [
'variety' => '綜藝',
'drama' => '戲劇',
];
$statusLabels = [
'want' => '想看',
'watching' => '追劇中',
'done' => '已追完',
'paused' => '暫停',
'dropped' => '棄劇',
];
$filter = $_GET['type'] ?? 'all';
$where = ['sh' => 1];
if ($filter === 'variety' || $filter === 'drama') {
$where['type'] = $filter;
}
$rows = $Drama->all($where, " ORDER BY `rank` ASC, `id` ASC");
?>
<section class="vedio-intro">
<h2 class="section-title">正在追的那些夜晚</h2>
<p>
有些夜晚適合考古黃色奇蹟,有些夜晚則是——螢幕一亮,人就栽進另一個世界。
綜藝裡的失控笑聲、戲劇裡的心跳停拍,都值得被好好記下來:看到哪一季、哪一集、哪一句台詞剛把你釘在沙發上。
</p>
<p>
這裡的清單來自資料庫:類型先分<span class="mark_y">綜藝</span><span class="mark_b">戲劇</span>
進度與「繼續觀看」入口都能改。慢慢養也很好看。
</p>
</section>
<section class="list-section" aria-labelledby="vedio-list-heading">
<h2 class="section-title" id="vedio-list-heading">追劇清單</h2>
<p class="list-lead">先用類型找——綜藝還是戲劇,跟 OTT 首頁那排分類一樣直覺。</p>
<div class="type-tabs" role="tablist" aria-label="作品類型">
<a class="type-tab<?= $filter === 'all' ? ' is-active' : '' ?>" href="?do=main&amp;type=all">全部</a>
<a class="type-tab<?= $filter === 'variety' ? ' is-active' : '' ?>" href="?do=main&amp;type=variety">綜藝</a>
<a class="type-tab<?= $filter === 'drama' ? ' is-active' : '' ?>" href="?do=main&amp;type=drama">戲劇</a>
</div>
<?php if (count($rows) === 0): ?>
<p class="empty-hint">這個類型目前還沒有作品——換「全部」看看,或到後台新增一筆。</p>
<?php else: ?>
<div class="drama-grid">
<?php foreach ($rows as $row): ?>
<?php
$typeText = $typeLabels[$row['type']] ?? $row['type'];
$statusText = $statusLabels[$row['status']] ?? $row['status'];
$posterLabel = htmlspecialchars(strtoupper($row['platform']) . ' · ' . $typeText, ENT_QUOTES, 'UTF-8');
?>
<article class="drama-card" data-type="<?= htmlspecialchars($row['type'], ENT_QUOTES, 'UTF-8') ?>">
<div class="drama-card__poster" aria-hidden="true"><?= $posterLabel ?></div>
<div class="drama-card__meta">
<span class="drama-tag"><?= htmlspecialchars($typeText, ENT_QUOTES, 'UTF-8') ?></span>
<span class="drama-tag drama-tag--platform"><?= htmlspecialchars($row['platform'], ENT_QUOTES, 'UTF-8') ?></span>
</div>
<h3 class="drama-card__title"><?= htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8') ?></h3>
<p class="drama-card__progress">
觀看進度:第 <?= (int)$row['current_season'] ?> 季・第 <?= (int)$row['current_episode'] ?> 集
<?php if (!empty($row['current_position'])): ?>
・看到 <?= htmlspecialchars($row['current_position'], ENT_QUOTES, 'UTF-8') ?>
<?php endif; ?>
</p>
<?php if (!empty($row['progress_note'])): ?>
<p class="drama-card__note"><?= htmlspecialchars($row['progress_note'], ENT_QUOTES, 'UTF-8') ?></p>
<?php else: ?>
<p class="drama-card__note">狀態:<?= htmlspecialchars($statusText, ENT_QUOTES, 'UTF-8') ?></p>
<?php endif; ?>
<div class="drama-card__actions">
<?php if (!empty($row['watch_url'])): ?>
<a class="is-primary" href="<?= htmlspecialchars($row['watch_url'], ENT_QUOTES, 'UTF-8') ?>"
target="_blank" rel="noopener noreferrer">繼續觀看</a>
<?php endif; ?>
<a href="?do=detail&amp;id=<?= (int)$row['id'] ?>">查看詳情</a>
<a href="?do=progress&amp;id=<?= (int)$row['id'] ?>">更新進度</a>
</div>
</article>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
+69
View File
@@ -0,0 +1,69 @@
<?php
$id = $_GET['id'] ?? 0;
$row = $Drama->find($id);
if (!$row) {
echo '<p class="empty-hint">找不到這部作品。</p>';
echo '<p class="empty-hint"><a href="?do=main">回清單</a></p>';
return;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$row['current_season'] = max(1, (int)($_POST['current_season'] ?? 1));
$row['current_episode'] = max(0, (int)($_POST['current_episode'] ?? 0));
$row['current_position'] = trim($_POST['current_position'] ?? '');
$row['progress_note'] = trim($_POST['progress_note'] ?? '');
$row['status'] = $_POST['status'] ?? $row['status'];
$Drama->save($row);
to("?do=detail&id=" . (int)$row['id']);
exit;
}
?>
<section class="vedio-intro">
<h2 class="section-title">更新進度</h2>
<p>記下你停在哪一季、哪一集、哪一秒——以及當時螢幕上正在發生什麼,之後才找得回那個心跳瞬間。</p>
</section>
<form class="progress-form" method="post" action="?do=progress&amp;id=<?= (int)$row['id'] ?>">
<h3 class="drama-card__title"><?= htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8') ?></h3>
<label for="current_season">目前看到第幾季</label>
<input type="number" id="current_season" name="current_season" min="1"
value="<?= (int)$row['current_season'] ?>">
<label for="current_episode">目前看到第幾集</label>
<input type="number" id="current_episode" name="current_episode" min="0"
value="<?= (int)$row['current_episode'] ?>">
<label for="current_position">這一集看到哪裡</label>
<input type="text" id="current_position" name="current_position"
value="<?= htmlspecialchars($row['current_position'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
placeholder="例如 35:20、剩下 10 分鐘、片尾前">
<label for="progress_note">進度情節備註</label>
<input type="text" id="progress_note" name="progress_note"
value="<?= htmlspecialchars($row['progress_note'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
placeholder="例如:女主角剛看到手機裡的影片">
<label for="status">狀態</label>
<select id="status" name="status">
<?php
$statuses = [
'want' => '想看',
'watching' => '追劇中',
'done' => '已追完',
'paused' => '暫停',
'dropped' => '棄劇',
];
foreach ($statuses as $key => $label):
?>
<option value="<?= $key ?>"<?= $row['status'] === $key ? ' selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
<div class="form-actions">
<button type="submit" class="is-primary">儲存進度</button>
<a href="?do=detail&amp;id=<?= (int)$row['id'] ?>">取消</a>
</div>
</form>
+54
View File
@@ -0,0 +1,54 @@
<?php include_once "./api/db.php"; ?>
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>水晶男孩|深夜清單</title>
<link rel="stylesheet" href="../../assets/css/style.css">
<link rel="stylesheet" href="../../assets/css/index.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="../../assets/css/tablet.css">
<link rel="stylesheet" href="css/vedio-list.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
</head>
<body class="vedio-list-page">
<header class="inner-page-header">
<div class="header-inner site-header-inner">
<h1 class="main-title">深夜清單</h1>
<div class="subtitle-wrap">
<span class="sub-title">DRAMA &amp; VARIETY WATCH LIST</span>
</div>
</div>
</header>
<nav class="portal-nav" aria-label="水晶男孩推廣部導覽" data-portal-nav></nav>
<canvas id="particles"></canvas>
<div class="container">
<?php
$do = $_GET['do'] ?? 'main';
$file = "front/$do.php";
if (file_exists($file)) {
include $file;
} else {
include "front/main.php";
}
?>
</div>
<footer>
<p>SECHSKIES Fanpage &copy; 2026 - 黃色氣球永遠飄揚</p>
<a href="https://jekkinoopy.github.io/Portfolio/" class="studio-link" target="_blank" rel="noopener noreferrer">
Design by <span class="mark_b">Jekkinoopy Studio</span>
</a>
<p class="vedio-admin-link"><a href="admin.php">管理後台</a></p>
</footer>
<script src="../../assets/js/particles.js"></script>
<script src="../../assets/js/portal-nav.js"></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

@@ -0,0 +1,724 @@
<!doctype html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>瘞湔?瑕酋?典誨?剁?瘞湔?梯?蝷?/title>
<link rel="stylesheet" href="../assets/css/style.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="../assets/css/tablet.css">
<style>
/* ?€?€ Bootstrap ??敺???靽風嚗? index-bootstrap.html ??嚗?
Bootstrap Reboot ?函?璅惜?豢??券?閮?body嚗?憿?畾菔嚗€??嚗? ?ㄐ?典?????撖怠 Bootstrap 敺??璅惜?豢??函??蝡??祆?潘?
銝?韐? .section-title ?? class ?豢??剁?銝蔣?踵?隞見撘??€?€ */
body {
background-color: var(--color-bg);
color: var(--color-text);
font-family: 'Noto Sans TC', 'Microsoft JhengHei', system-ui, sans-serif;
line-height: 1.6;
}
h1,
h2,
h3,
h4,
h5,
h6,
p,
ul,
ol,
dl,
dd,
blockquote,
hr {
margin: 0;
padding: 0;
}
a {
color: var(--color-text);
text-decoration: none;
}
/* 瘞湔?梯?蝷橘?瘞湔?瑕酋?典誨?冽?銝?頩??港??€? 憿銝€敺?朣蝡?color-tokens嚗?-color-brand 蝟餃?嚗?銝?菔??*/
:root {
--dance-yellow: var(--color-brand);
--dance-yellow-light: var(--color-brand-light);
--dance-yellow-dark: var(--color-brand-dark);
--dance-ink: var(--color-brand-dark);
}
/* 憭扳?摨?嚗???函??蝷曉??閬死嚗??芾????銝??函??梁??header.jpg */
body.crystal-dance-page header.inner-page-header {
background-image: url('../assets/images/crystal-dance-hero.jpg');
background-position: center 68%;
}
/* ?€?€ Hero ?€嚗窒??inner-page-header ?嗡蜓璅??ㄐ?芣撘????批?閬踝? ?€?€ */
.crystal-dance-hero {
text-align: center;
}
.crystal-dance-hero .category-tag {
display: inline-block;
background: var(--dance-yellow-light);
color: var(--dance-ink);
padding: .35rem 1.1rem;
border-radius: 999px;
font-size: .8rem;
font-weight: 700;
letter-spacing: 1px;
margin-bottom: 1rem;
}
.crystal-dance-hero .hero-desc {
color: var(--color-text-muted);
line-height: 1.85;
max-width: 640px;
margin: 0 auto 1.5rem;
}
.crystal-dance-hero .quick-nav {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
justify-content: center;
gap: .6rem;
padding: .25rem 0 .5rem;
}
.crystal-dance-hero .quick-nav a {
white-space: nowrap;
text-decoration: none;
}
/* --- ?挾?賢?冽?憿見撘?--- */
.section-alt {
background: rgba(255, 204, 0, 0.08);
}
.section-kicker {
font-weight: 800;
color: var(--dance-yellow-dark);
letter-spacing: 2px;
font-size: .85rem;
text-transform: uppercase;
margin-bottom: .5rem;
}
.crystal-dance-page .section-title {
text-align: left;
margin-bottom: 1.1rem;
}
.section-lead {
color: var(--color-text-muted);
line-height: 1.85;
max-width: 760px;
}
.section-lead+.section-lead {
margin-top: .9rem;
}
/* --- 瘣餃?銝餉?閬?/ 銵典 Banner --- */
.event-banner {
background: linear-gradient(135deg, var(--dance-yellow) 0%, var(--dance-yellow-light) 100%);
border-radius: 24px;
padding: 2.25rem 2.5rem;
text-align: center;
color: var(--dance-ink);
box-shadow: 0 12px 30px rgba(255, 204, 0, .25);
}
.event-banner .event-badge {
display: inline-block;
background: var(--dance-ink);
color: var(--dance-yellow-light);
padding: .35rem 1.1rem;
border-radius: 999px;
font-size: .8rem;
font-weight: 700;
letter-spacing: 1px;
margin-bottom: .9rem;
}
.event-banner h2 {
font-weight: 900;
letter-spacing: 3px;
font-size: 1.9rem;
margin-bottom: .6rem;
}
.event-banner p {
margin: 0 auto;
font-weight: 600;
letter-spacing: .5px;
max-width: 640px;
}
/* --- ?梯?蝷曆?蝝對??摮?chip --- */
.keyword-row {
margin-top: 1.5rem;
}
.keyword-chip {
background: #fff;
border: 1.5px solid var(--dance-yellow);
border-radius: 999px;
padding: .5rem 1.1rem;
font-weight: 700;
color: var(--dance-ink);
display: inline-flex;
align-items: center;
gap: .4rem;
}
/* --- 20TH 敶梁??“ --- */
.video-card {
background: #fff;
border: 1.5px solid var(--dance-yellow);
border-radius: 16px;
padding: 1rem;
height: 100%;
box-shadow: 0 6px 18px rgba(255, 204, 0, .1);
}
.video-tag {
display: inline-block;
background: var(--dance-yellow-dark);
color: #fff;
font-weight: 700;
font-size: .75rem;
padding: .3rem .8rem;
border-radius: 999px;
margin-bottom: .75rem;
}
.video-card .ratio {
border-radius: 10px;
overflow: hidden;
}
/* --- 蝬??€ --- */
.song-card {
background: #fff;
border: 1.5px solid var(--dance-yellow);
border-radius: 16px;
padding: 1.5rem;
height: 100%;
box-shadow: 0 6px 18px rgba(255, 204, 0, .1);
}
.song-card h4 {
color: var(--dance-yellow-dark);
font-weight: 800;
margin-bottom: .8rem;
}
.song-card .song-meta {
font-size: .78rem;
font-weight: 800;
color: var(--dance-ink);
background: var(--dance-yellow-light);
display: inline-block;
padding: .15rem .6rem;
border-radius: 999px;
margin-bottom: .4rem;
}
.song-card p {
font-size: .9rem;
color: var(--color-text-muted);
line-height: 1.7;
margin-bottom: .9rem;
}
/* --- 30TH ?隡? --- */
.countdown-strip {
background: var(--dance-ink);
color: var(--dance-yellow-light);
border-radius: 16px;
padding: .9rem 1.5rem;
display: inline-flex;
align-items: center;
gap: .6rem;
font-weight: 800;
letter-spacing: 1px;
margin-top: 1.5rem;
}
/* --- 30TH ???銵典 --- */
.form-intro {
background: var(--color-surface-soft);
border-left: 4px solid var(--dance-yellow-dark);
padding: 1rem 1.25rem;
border-radius: 0 12px 12px 0;
color: var(--dance-ink);
font-weight: 600;
margin-bottom: 1.75rem;
}
.form-panel {
background: #fff;
border: 1.5px solid var(--dance-yellow);
border-radius: 20px;
padding: 2rem;
height: 100%;
box-shadow: 0 8px 24px rgba(255, 204, 0, .12);
}
.panel-title {
font-weight: 800;
color: var(--dance-yellow-dark);
margin-bottom: 1.5rem;
font-size: 1.3rem;
}
.form-label {
font-weight: 700;
color: var(--dance-ink);
}
.form-control,
.form-select {
border: 1.5px solid var(--color-brand-light);
border-radius: 12px;
padding: .65rem .9rem;
}
.form-control:focus,
.form-select:focus {
border-color: var(--dance-yellow-dark);
box-shadow: 0 0 0 .2rem rgba(255, 204, 0, .25);
}
.form-control[type="file"] {
border-style: dashed;
cursor: pointer;
}
.form-text {
color: var(--color-text-muted);
}
/* 頨思遢 / ?閎隞交??? radio?heckbox ?嚗??身憭??湔餈?撖虫??? */
.btn-outline-dance {
border: 1.5px solid var(--dance-yellow-dark);
color: var(--dance-yellow-dark);
border-radius: 999px;
font-weight: 600;
padding: .4rem 1.15rem;
}
.btn-check:checked+.btn-outline-dance,
.btn-outline-dance:hover {
background: var(--dance-yellow);
color: var(--dance-ink);
border-color: var(--dance-yellow-dark);
}
.btn-dance {
background: var(--dance-yellow);
color: var(--dance-ink);
border: 2px solid var(--dance-yellow-dark);
border-radius: 999px;
font-weight: 800;
padding: .75rem;
}
.btn-dance:hover {
background: var(--dance-yellow-dark);
color: #fff;
}
.btn-reset-dance {
background: #fff;
color: var(--dance-yellow-dark);
border: 2px solid var(--dance-yellow-dark);
border-radius: 999px;
font-weight: 700;
padding: .75rem;
}
.btn-reset-dance:hover {
background: #fdecec;
border-color: #dc3545;
color: #dc3545;
}
/* --- 憛怠神???--- */
.info-card {
background: linear-gradient(160deg, var(--dance-ink) 0%, var(--color-bg-raised) 100%);
color: var(--dance-yellow-light);
border-radius: 20px;
padding: 2rem;
height: 100%;
box-shadow: 0 8px 24px rgba(78, 73, 43, .3);
}
.info-tag {
display: inline-block;
background: var(--dance-yellow);
color: var(--dance-ink);
padding: .3rem 1rem;
border-radius: 999px;
font-size: .75rem;
font-weight: 700;
margin-bottom: 1rem;
}
.info-card h3 {
color: #fff;
font-weight: 800;
margin-bottom: 1rem;
}
.info-list {
list-style: none;
padding: 0;
margin: 0 0 1.5rem;
display: flex;
flex-direction: column;
gap: .6rem;
}
.info-list li {
background: rgba(255, 255, 255, .08);
padding: .6rem 1rem;
border-radius: 12px;
font-weight: 600;
font-size: .9rem;
line-height: 1.5;
}
.info-footer {
display: flex;
flex-wrap: wrap;
gap: .5rem;
}
.mini-badge {
border: 1px solid var(--dance-yellow);
color: var(--dance-yellow-light);
padding: .25rem .8rem;
border-radius: 999px;
font-size: .75rem;
}
@media (max-width: 575.98px) {
.event-banner {
padding: 1.75rem 1.25rem;
border-radius: 18px;
}
.event-banner h2 {
font-size: 1.5rem;
letter-spacing: 1px;
}
.form-panel,
.info-card {
padding: 1.5rem;
}
}
</style>
</head>
<body class="crystal-dance-page">
<header class="inner-page-header">
<div class="header-inner site-header-inner">
<h1 class="main-title">瘞湔?梯?蝷?/h1>
<div class="subtitle-wrap">
<span class="sub-title">SECHSKIES DANCE ARCHIVE &amp; 30TH FAN PROJECT</span>
</div>
</div>
</header>
<nav class="portal-nav" aria-label="瘞湔?瑕酋?典誨?典?閬? data-portal-nav></nav>
<canvas id="particles"></canvas>
<main class="content-container crystal-dance-main">
<section class="crystal-dance-hero">
<span class="category-tag">瘞湔?瑕酋?典誨?具???隡?</span>
<p class="hero-desc">瘞湔?梯?蝷暹瘞湔?瑕酋?典誨?其葉??頩??港???敺?20TH
?啁撠?銝脩??航???擃??嗅?潘??渡?蝬??蕃頝喟????銵?嚗蒂????2027/04/15 30TH ??????€€?/p>
<nav class="quick-nav" aria-label="?撠汗">
<a href="#about" class="btn btn-outline-dance btn-sm">02 ?梯?蝷曆?蝝?/a>
<a href="#throwback-20th" class="btn btn-outline-dance btn-sm">03 20TH 敶梁??“</a>
<a href="#song-archive" class="btn btn-outline-dance btn-sm">04 蝬??€</a>
<a href="#project-30th" class="btn btn-outline-dance btn-sm">05 30TH ?隡?</a>
<a href="#form-30th" class="btn btn-outline-dance btn-sm">06 ???銵典</a>
</nav>
</section>
<!-- 02 ?梯?蝷曆?蝝?-->
<section id="about" class="section-alt">
<div class="section-kicker">02 繚 ?梯?蝷曆?蝝?/div>
<h2 class="section-title">瘞湔?梯?蝷曆??臭??祉冗??/h2>
<p class="section-lead">瘞湔?梯?蝷曆??舐蝡?蝡?蝷曉?嚗€?偌?嗥摮拇撱???銝????蕃頝喋€??渲?敶梁?蝝€??憿€?航? SECHSKIES
????毀蝧€??曇?蝝€???賣?鋡急?脤€€??ㄐ嚗甇瑕??啁撠??亙??渡??辣蝥€?/p>
<div class="keyword-row d-flex flex-wrap gap-2">
<span class="keyword-chip">?? 蝬?</span>
<span class="keyword-chip">? ???</span>
<span class="keyword-chip">? 蝧餉歲蝝€??/span>
<span class="keyword-chip">?? 銝脩??航?</span>
<span class="keyword-chip">?? ?啁撠?</span>
</div>
</section>
<!-- 03 20TH 敶梁??“ -->
<section id="throwback-20th">
<div class="section-kicker">03 繚 20TH 敶梁??“</div>
<h2 class="section-title">20TH 敶梁??“</h2>
<p class="section-lead">2017 撟湛??啁撠?隞交偌?嗥摮拙??20 ?典僑?箏?璈?蝐齒鈭??豢??脖葡????氬€€??芣銝€甈∟歲?暑??????乓€?敺€?甇貉???賊?瞈葬?典?銝€?臬蔣?ㄐ??擃??嗚€?/p>
<p class="section-lead">瘞湔?梯?蝷曉??辣蝥€遢閮嚗? 20TH ?銵€銵??渡??隞亥◤?活??????銋 2027/04/15 ??30TH ?隡????啁?????/p>
<div class="row g-4 mt-1">
<div class="col-12 col-sm-6">
<div class="video-card">
<span class="video-tag">20TH ??敶梁?</span>
<div class="ratio ratio-16x9">
<iframe src="https://www.youtube.com/embed/ARi0cfpv6mE" title="20TH ??敶梁?"
allowfullscreen></iframe>
</div>
</div>
</div>
<div class="col-12 col-sm-6">
<div class="video-card">
<span class="video-tag">20TH 甇??敶梁?</span>
<div class="ratio ratio-16x9">
<iframe src="https://www.youtube.com/embed/fkJmOMPirBc" title="20TH 甇??敶梁?"
allowfullscreen></iframe>
</div>
</div>
</div>
</div>
</section>
<!-- 04 蝬??€ -->
<section id="song-archive" class="section-alt">
<div class="section-kicker">04 繚 蝬??€</div>
<h2 class="section-title">蝬??€</h2>
<p class="section-lead">隞乩??撟暸??冽??渲?蝧餉歲銝剜?撣詨?曄??嚗???30TH 蝺渡??葡??楊???€??柴€?/p>
<div class="row g-4 mt-1">
<div class="col-12 col-sm-6 col-lg-3">
<div class="song-card">
<span class="song-meta">??啗情</span>
<h4>Couple</h4>
<p>頛翰???望??嚗??貊??犖鈭?韏唬?嚗虜鋡怠?脫風撟渡????嗆捏?毽?芸蔣?ㄐ??/p>
<span class="song-meta">?拙???</span>
<p>??憟質???敶Z???銴?嚗??拙??嗡??????€?脩??/p>
</div>
</div>
<div class="col-12 col-sm-6 col-lg-3">
<div class="song-card">
<span class="song-meta">??啗情</span>
<h4>Com' Back</h4>
<p>?飛?隞?”?脖?銝€嚗?憟?敹怒€??撘瘀?敺捆?葆??湔除瘞€?/p>
<span class="song-meta">?拙???</span>
<p>?舀????折?嚗停蝞蝚砌?甈∪????渡?撠?銋敹恍€?銝€?/p>
</div>
</div>
<div class="col-12 col-sm-6 col-lg-3">
<div class="song-card">
<span class="song-meta">??啗情</span>
<h4>Road Fighter</h4>
<p>??撟漲憭扼€?憟?敹恬??舫???暺??€?嚗??唳除??頞喋€?/p>
<span class="song-meta">?拙???</span>
<p>?拙?撌脫??箇????€?蝺渲絲靘??擛交?嚗??拙??曉銝脩???頠詻€?/p>
</div>
</div>
<div class="col-12 col-sm-6 col-lg-3">
<div class="song-card">
<span class="song-meta">??啗情</span>
<h4>Pom Saeng Pom Sa</h4>
<p>??敺??朣?敶g??舫€?甇甇瑕僑蝧餉歲銝剛◤??€憭活????銝€??/p>
<span class="song-meta">?拙???</span>
<p>颲刻?摨阡?嚗?其葡???銝剜挾敺捆???勗?湔除瘞€?/p>
</div>
</div>
</div>
</section>
<!-- 05 30TH ?隡? -->
<section id="project-30th">
<div class="section-kicker">05 繚 30TH ?隡?</div>
<h2 class="section-title">30TH ???</h2>
<p class="section-lead">2027/04/15 ??SECHSKIES ?粹? 30 ?典僑?摮€偌?嗥?冗????30TH
???唳??頩??游摰對????喃?韏瑕????啁撠?嚗?蝬??蕃頝喟毀蝧€蔣?頛臬蟡???嚗??啁??銵?敺€遢銵典??蝝舐???/p>
<p class="section-lead">雿??€閬???撠勗??歲???芾???敺偌?嗥摮拍?????莎?????雿???祇?嚗?臭誑?券€ㄐ??雿????孵???/p>
<div class="countdown-strip" id="countdown">?? 頝 30TH 繚 2027/04/15 ?閮?銝?..</div>
</section>
<!-- 06 30TH ???銵典 -->
<section id="form-30th" class="section-alt">
<div class="section-kicker">06 繚 30TH ???銵典</div>
<div class="event-banner mb-4">
<span class="event-badge">?? SECHSKIES 30TH 繚 2027.04.15</span>
<h2>30TH ???銵典</h2>
<p>?遢銵典?舀偌?嗥?冗??2027/04/15 30TH ?????游??芋蝯??其??渡????蝺渡?甇??舀?摰寡??喟?銝?蟡???/p>
</div>
<div class="row g-4">
<div class="col-12 col-sm-7">
<div class="form-panel">
<h3 class="panel-title">?? ???鞈?</h3>
<form action="#">
<div class="row g-3">
<div class="col-12 col-sm-6">
<label for="nickname" class="form-label">?梁迂</label>
<input type="text" class="form-control" id="nickname" name="nickname"
placeholder="靘??梯?撠?" required>
</div>
<div class="col-12 col-sm-6">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email"
placeholder="dance@example.com" required>
</div>
<div class="col-12 col-sm-6">
<label for="dance_years" class="form-label">蝺渲?蝬?撟湔</label>
<input type="number" class="form-control" id="dance_years" name="dance_years"
min="0" max="30" placeholder="0">
</div>
<div class="col-12 col-sm-6">
<label for="available_date" class="form-label">?臬????/label>
<input type="date" class="form-control" id="available_date" name="available_date">
</div>
<div class="col-12">
<label class="form-label d-block">?臬????20TH</label>
<div class="d-flex flex-wrap gap-2">
<input type="radio" class="btn-check" name="attended_20th"
id="attended_20th_yes" value="attended" autocomplete="off">
<label class="btn btn-outline-dance" for="attended_20th_yes">????20TH</label>
<input type="radio" class="btn-check" name="attended_20th"
id="attended_20th_video" value="watched_video" autocomplete="off">
<label class="btn btn-outline-dance" for="attended_20th_video">?? 20TH
敶梁?</label>
<input type="radio" class="btn-check" name="attended_20th"
id="attended_20th_first" value="first_time" autocomplete="off">
<label class="btn btn-outline-dance" for="attended_20th_first">蝚砌?甈∠??/label>
</div>
</div>
<div class="col-12">
<label class="form-label d-block">?喳??? 30TH ?批捆嚗銴嚗?/label>
<div class="d-flex flex-wrap gap-2">
<input type="checkbox" class="btn-check" name="participate_content"
id="content_cover" value="cover" autocomplete="off">
<label class="btn btn-outline-dance" for="content_cover">蝧餉歲</label>
<input type="checkbox" class="btn-check" name="participate_content"
id="content_medley" value="medley" autocomplete="off">
<label class="btn btn-outline-dance" for="content_medley">銝脩??航?</label>
<input type="checkbox" class="btn-check" name="participate_content"
id="content_blessing" value="blessing" autocomplete="off">
<label class="btn btn-outline-dance" for="content_blessing">蟡?敶梁?</label>
<input type="checkbox" class="btn-check" name="participate_content"
id="content_graphic" value="graphic" autocomplete="off">
<label class="btn btn-outline-dance" for="content_graphic">???</label>
<input type="checkbox" class="btn-check" name="participate_content"
id="content_editing" value="editing" autocomplete="off">
<label class="btn btn-outline-dance" for="content_editing">敶梁??芾摩</label>
<input type="checkbox" class="btn-check" name="participate_content"
id="content_archive" value="archive" autocomplete="off">
<label class="btn btn-outline-dance" for="content_archive">鞈??渡?</label>
</div>
</div>
<div class="col-12 col-sm-6">
<label for="song" class="form-label">?喟毀????/label>
<select class="form-select" id="song" name="song">
<option value="" selected>隢????/option>
<option value="couple">Couple</option>
<option value="comeback">Com' Back</option>
<option value="road_fighter">Road Fighter</option>
<option value="pom_saeng_pom_sa">Pom Saeng Pom Sa</option>
<option value="other">?嗡?</option>
</select>
</div>
<div class="col-12 col-sm-6">
<label for="reference_file" class="form-label">銝?€蔣????</label>
<input class="form-control" type="file" id="reference_file" name="reference_file"
accept="image/*,video/*">
<div class="form-text">蝧餉歲敶梁????啁???芾摩蝝??</div>
</div>
<div class="col-12">
<label for="message_30th" class="form-label">?喳? 30TH 隤芰?閰?/label>
<textarea class="form-control" id="message_30th" name="message_30th" rows="4"
placeholder="?喳?瘞湔?梯?蝷曇牧?店?鞎Y??€閬隤啜€????蝳€色€?></textarea>
</div>
</div>
<div class="d-grid d-sm-flex gap-3 mt-4">
<button type="reset"
class="btn btn-reset-dance flex-fill order-2 order-sm-1">?憛怠神</button>
<button type="submit"
class="btn btn-dance flex-fill order-1 order-sm-2">??銵典 ??</button>
</div>
</form>
</div>
</div>
<div class="col-12 col-sm-5">
<div class="info-card">
<span class="info-tag">憛怠神?</span>
<h3>???蝣箄?銝€銝?/h3>
<ul class="info-list">
<li>?? 瑼??澆?嚗蔣?????嚗遣霅啣瑼?100MB 隞亙</li>
<li>?? ?遢鞈???瘞湔?梯?蝷曉?冽??30TH ?隡?雿輻</li>
<li>?? ?敺蝣箏???嚗?敺??西??砍?蝺渡???鋆賣???/li>
</ul>
<div class="info-footer">
<span class="mini-badge">瘞湔?瑕酋?典誨??/span>
<span class="mini-badge">30TH ?隡?</span>
</div>
</div>
</div>
</div>
</section>
</main>
<footer>
<p>SECHSKIES Fanpage &copy; 2026 - 暺瘞??瘞賊?憌?</p>
<a href="https://jekkinoopy.github.io/Portfolio/" class="studio-link" target="_blank" rel="noopener noreferrer">
Design by <span class="mark_b">Jekkinoopy Studio</span>
</a>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<script src="../assets/js/particles.js"></script>
<script src="../assets/js/portal-nav.js"></script>
<script>
(function () {
var target = new Date('2027-04-15T00:00:00+08:00');
var now = new Date();
var diffDays = Math.ceil((target - now) / (1000 * 60 * 60 * 24));
var el = document.getElementById('countdown');
if (el) {
el.textContent = diffDays > 0
? '?? 頝 30TH 繚 2027/04/15 ?? ' + diffDays + ' 憭?
: '?? 30TH 繚 2027/04/15 撌脩??唬?嚗?韏瑕???嚗?;
}
})();
</script>
</body>
</html>
+722
View File
@@ -0,0 +1,722 @@
<!doctype html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>水晶男孩推廣部|水晶熱舞社</title>
<link rel="stylesheet" href="../assets/css/style.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="../assets/css/tablet.css">
<style>
/* Bootstrap Reboot 還原 */
body.crystal-dance-page {
background-color: var(--color-bg);
color: var(--color-text);
font-family: 'Noto Sans TC', 'Microsoft JhengHei', system-ui, sans-serif;
line-height: 1.6;
}
h1, h2, h3, h4, h5, h6, p, ul, ol, dl, dd, blockquote, hr {
margin: 0;
padding: 0;
}
a {
color: var(--color-text);
text-decoration: none;
}
/* 標題與內文:拉開垂直節奏,避免擠在一起 */
body.crystal-dance-page .section-title {
margin-bottom: 2rem;
}
body.crystal-dance-page .section-title::after {
margin-top: 1rem;
}
body.crystal-dance-page .section-body {
max-width: 760px;
margin: 0 auto 2.5rem;
}
body.crystal-dance-page .section-body p,
body.crystal-dance-page .form-lead {
color: var(--color-text-muted);
line-height: 1.9;
text-align: left;
margin-bottom: 1.15rem;
}
body.crystal-dance-page .section-body p:last-child {
margin-bottom: 0;
}
body.crystal-dance-page .form-lead {
max-width: 760px;
margin: 0 auto 2.5rem;
}
/* ① 認識區 */
body.crystal-dance-page #about {
text-align: center;
padding-bottom: 0;
overflow: hidden;
}
body.crystal-dance-page .category-tag {
display: inline-block;
background: var(--color-brand-light);
color: var(--color-brand-dark);
padding: 0.35rem 1.15rem;
border-radius: 999px;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 1px;
margin-bottom: 1.5rem;
}
body.crystal-dance-page .hero-banner {
display: block;
width: 100%;
max-width: 100%;
aspect-ratio: 21 / 9;
object-fit: cover;
border-radius: 12px;
border: 1px solid rgba(255, 204, 0, 0.22);
margin: 0 auto 2.25rem;
}
body.crystal-dance-page #about .section-title {
margin-top: 0.25rem;
}
body.crystal-dance-page .keyword-row {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.6rem;
margin: 2.25rem auto 2.5rem;
max-width: 640px;
}
body.crystal-dance-page .keyword-chip {
display: inline-flex;
align-items: center;
gap: 0.25rem;
background: var(--color-brand-light);
color: var(--color-brand-dark);
font-size: 0.82rem;
font-weight: 700;
padding: 0.4rem 0.9rem;
border-radius: 999px;
border: 1px solid rgba(255, 204, 0, 0.35);
}
/* 頁內導覽:深色底條(對照參考圖) */
body.crystal-dance-page .page-nav-bar {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 0.25rem 0;
margin: 0 -36px;
padding: 0.95rem 1.5rem;
background: var(--color-bg-raised);
border-top: 1px solid rgba(255, 204, 0, 0.2);
}
body.crystal-dance-page .page-nav-bar a {
color: var(--color-brand);
font-size: 0.82rem;
font-weight: 700;
letter-spacing: 0.5px;
padding: 0.2rem 0.85rem;
border-right: 1px solid rgba(255, 204, 0, 0.25);
transition: color 0.2s ease;
}
body.crystal-dance-page .page-nav-bar a:last-child {
border-right: none;
}
body.crystal-dance-page .page-nav-bar a:hover {
color: var(--color-text-inverse);
}
/* ② 影片區:主影片與下方三欄同寬對齊 */
body.crystal-dance-page .video-grid {
width: 100%;
}
body.crystal-dance-page .sub-section-title {
margin-top: 3rem;
margin-bottom: 1.25rem;
font-weight: 800;
color: var(--color-brand-dark);
font-size: 1.05rem;
text-align: center;
letter-spacing: 0.08em;
}
body.crystal-dance-page .video-label {
display: block;
color: var(--color-brand-dark);
font-weight: 800;
font-size: 0.9rem;
margin-bottom: 0.75rem;
}
body.crystal-dance-page .video-feature {
width: 100%;
margin-bottom: 0;
}
body.crystal-dance-page .video-feature .video-label {
text-align: left;
}
body.crystal-dance-page .video-feature .ratio,
body.crystal-dance-page .video-thumb .ratio {
border-radius: 10px;
overflow: hidden;
border: 1px solid rgba(255, 204, 0, 0.18);
}
body.crystal-dance-page .video-thumb {
height: 100%;
}
body.crystal-dance-page .video-thumb .video-label {
font-size: 0.85rem;
margin-bottom: 0.6rem;
}
body.crystal-dance-page .countdown-note {
display: block;
margin-top: 2.5rem;
text-align: center;
font-weight: 700;
color: var(--color-brand-dark);
font-size: 0.95rem;
letter-spacing: 0.5px;
padding: 0.75rem 1rem;
background: var(--color-surface-soft);
border-radius: 999px;
max-width: 420px;
margin-left: auto;
margin-right: auto;
}
/* ③ summer-form:分階段邊框排版(風格參考見面會應援調查表,顏色用全站 token) */
body.crystal-dance-page .summer-form {
max-width: 720px;
margin: 0 auto;
}
body.crystal-dance-page .summer-form__stage + .summer-form__stage {
margin-top: 2rem;
}
body.crystal-dance-page .summer-form__stage-title {
font-weight: 800;
color: var(--color-brand-dark);
font-size: 0.95rem;
letter-spacing: 0.04em;
margin-bottom: 0.75rem;
text-align: left;
}
body.crystal-dance-page .summer-form__stage-body {
border: 1px solid rgba(255, 204, 0, 0.35);
border-radius: 12px;
padding: 1.5rem 1.75rem;
background: #fff;
}
body.crystal-dance-page .summer-form__stage-body--notice {
background: var(--color-surface-soft);
}
body.crystal-dance-page .summer-form__stage-body .row {
--bs-gutter-y: 1.35rem;
}
body.crystal-dance-page .summer-form .form-label {
font-weight: 700;
color: var(--color-brand-dark);
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
body.crystal-dance-page .summer-form .form-control,
body.crystal-dance-page .summer-form .form-select {
border: 1px solid rgba(255, 204, 0, 0.35);
border-radius: 10px;
padding: 0.65rem 0.9rem;
background: #fff;
}
body.crystal-dance-page .summer-form .form-control:focus,
body.crystal-dance-page .summer-form .form-select:focus {
border-color: var(--color-brand);
box-shadow: 0 0 0 0.2rem rgba(255, 204, 0, 0.2);
}
body.crystal-dance-page .summer-form .form-control[type="file"] {
border-style: dashed;
cursor: pointer;
background: var(--color-surface-soft);
}
body.crystal-dance-page .summer-form .form-text {
color: var(--color-text-muted);
font-size: 0.82rem;
margin-top: 0.35rem;
}
body.crystal-dance-page .summer-form .btn-outline-dance {
border: 1.5px solid var(--color-brand-dark);
color: var(--color-brand-dark);
border-radius: 999px;
font-weight: 600;
font-size: 0.88rem;
padding: 0.38rem 1rem;
background: #fff;
}
body.crystal-dance-page .summer-form .btn-check:checked + .btn-outline-dance,
body.crystal-dance-page .summer-form .btn-outline-dance:hover {
background: var(--color-brand);
color: var(--color-brand-dark);
border-color: var(--color-brand-dark);
}
body.crystal-dance-page .summer-form__option-row {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.15rem;
}
body.crystal-dance-page .summer-form .info-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.85rem;
}
body.crystal-dance-page .summer-form .info-list li {
color: var(--color-text-muted);
font-size: 0.9rem;
line-height: 1.75;
}
body.crystal-dance-page .summer-form .info-footer-text {
color: var(--color-text-muted);
font-size: 0.8rem;
opacity: 0.85;
margin-top: 1rem;
}
body.crystal-dance-page .summer-form__actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-top: 2rem;
}
body.crystal-dance-page .summer-form__feedback {
max-width: 860px;
margin: 0 auto 20px;
padding: 14px 18px;
border: 1px solid rgba(78, 73, 43, 0.3);
border-radius: 10px;
color: var(--color-text-dark);
background: var(--color-surface-soft);
text-align: left;
}
body.crystal-dance-page .summer-form__feedback--success {
border-color: var(--color-brand);
background: var(--color-brand-light);
}
body.crystal-dance-page .summer-form__feedback--error {
border-color: var(--color-brand-dark);
}
body.crystal-dance-page .summer-form__trap {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
body.crystal-dance-page .summer-form .btn-dance {
background: var(--color-brand);
color: var(--color-brand-dark);
border: none;
border-radius: 999px;
font-weight: 800;
padding: 0.8rem 1.5rem;
letter-spacing: 0.5px;
}
body.crystal-dance-page .summer-form .btn-dance:hover {
background: var(--color-brand-dark);
color: var(--color-text-inverse);
}
body.crystal-dance-page .summer-form .btn-reset-dance {
background: #fff;
color: var(--color-brand-dark);
border: 1.5px solid var(--color-brand-dark);
border-radius: 999px;
font-weight: 700;
padding: 0.8rem 1.5rem;
}
body.crystal-dance-page .summer-form .btn-reset-dance:hover {
background: var(--color-surface-soft);
}
@media (max-width: 575.98px) {
body.crystal-dance-page .summer-form__stage-body {
padding: 1.25rem 1.15rem;
}
body.crystal-dance-page .summer-form__actions {
grid-template-columns: 1fr;
}
}
@media (max-width: 575.98px) {
body.crystal-dance-page .page-nav-bar {
margin: 0 -20px;
padding: 0.85rem 1rem;
}
body.crystal-dance-page .page-nav-bar a {
font-size: 0.76rem;
padding: 0.2rem 0.55rem;
}
}
</style>
</head>
<body class="crystal-dance-page">
<header class="inner-page-header">
<div class="header-inner site-header-inner">
<h1 class="main-title">水晶熱舞社</h1>
<div class="subtitle-wrap">
<span class="sub-title">SECHSKIES DANCE ARCHIVE &amp; 30TH FAN PROJECT</span>
</div>
</div>
</header>
<nav class="portal-nav" aria-label="水晶男孩推廣部導覽" data-portal-nav></nav>
<canvas id="particles"></canvas>
<div class="container">
<!-- ① 認識水晶熱舞社 -->
<section id="about">
<img class="hero-banner" src="../assets/images/crystal-dance-hero.jpg" alt="水晶熱舞社應援視覺">
<span class="category-tag">水晶男孩推廣部・舞蹈應援企劃</span>
<!-- <h2 class="section-title">水晶熱舞社不是一般社團</h2> -->
<div class="section-body">
<p>水晶熱舞社是<span class="mark_b mark_y">水晶男孩推廣部</span>中的舞蹈應援企劃,從 20TH台灣小黃串燒聯舞的集體記憶出發,整理經典舞台、翻跳紀錄與應援行動,並<span class="fan-journey-wave">預備在 2027/04/15 30TH 前</span>募集新的參與者。</p>
<p>水晶熱舞社不是獨立成立的社團,而是「水晶男孩推廣部」底下的舞蹈、翻跳、應援與影片紀錄分類。凡是跟 SECHSKIE舞台有關的練習、重現與紀錄,都會被收進這個企劃裡,由歷屆台灣小黃接力整理與延續。</p>
</div>
<!-- <div class="keyword-row">
<span class="keyword-chip">💛 經典舞台</span>
<span class="keyword-chip">🕺 應援舞蹈</span>
<span class="keyword-chip">🎥 翻跳紀錄</span>
<span class="keyword-chip">💃 串燒聯舞</span>
<span class="keyword-chip">🇹🇼 台灣小黃</span>
</div>
<nav class="page-nav-bar" aria-label="頁面導覽">
<a href="#about">認識熱舞社</a>
<a href="#from-20th-to-30th">20TH 到 30TH</a>
<a href="#form-30th">應援表單</a>
</nav> -->
</section>
<!-- ② 從 20TH 到 30TH -->
<section id="from-20th-to-30th">
<div class="video-grid">
<h2 class="section-title">從 20TH 到 30TH</h2>
<div class="video-feature">
<!-- <span class="video-label">20TH 正式影片</span> -->
<div class="ratio ratio-16x9">
<iframe src="https://www.youtube.com/embed/fkJmOMPirBc" title="20TH 正式影片:SECHSKIES 20週年紀念串燒舞蹈"
allowfullscreen></iframe>
</div>
<div class="section-body">
<p>水晶熱舞社是<span class="mark_b mark_y">水晶男孩推廣部</span>底下的舞蹈應援企劃,從 20TH 台灣小黃串燒聯舞的集體記憶出發,整理 SECHSKIES 經典舞台、翻跳紀錄與應援行動,並<span class="fan-journey-wave">預備在 2027/04/15 30TH 前</span>募集新的參與者,由歷屆台灣小黃接力延續。</p>
</div>
</div>
<h3 class="sub-section-title">延伸閱讀</h3>
<div class="row g-4">
<div class="col-12 col-sm-6 col-lg-4">
<div class="video-thumb">
<div class="ratio ratio-16x9">
<iframe src="https://www.youtube.com/embed/ARi0cfpv6mE" title="20TH 預告影片"
allowfullscreen></iframe>
</div>
<span class="video-label">預告</span>
</div>
</div>
<div class="col-12 col-sm-6 col-lg-4">
<div class="video-thumb">
<div class="ratio ratio-16x9">
<iframe src="https://www.youtube.com/embed/nygNm5mtsJM" title="幕後花絮 #1"
allowfullscreen></iframe>
</div>
<span class="video-label">幕後花絮 #1</span>
</div>
</div>
<div class="col-12 col-sm-6 col-lg-4">
<div class="video-thumb">
<div class="ratio ratio-16x9">
<iframe src="https://www.youtube.com/embed/f04neSErUEY" title="幕後花絮 #2・雨中共舞"
allowfullscreen></iframe>
</div>
<span class="video-label">幕後花絮 #2・雨中共舞</span>
</div>
</div>
</div>
</div>
<!-- <h3 class="sub-section-title">30TH 應援募集</h3> -->
<div class="section-body">
<p>2027/04/15 是 SECHSKIES 出道 30 周年的日子。水晶熱舞社預計在 30TH
前重新整理舞蹈應援內容,募集想一起參與的台灣小黃,從經典舞曲、翻跳練習、影片剪輯到祝福文字,讓新的應援行動從這份表單開始累積。</p>
<p>你不需要一開始就很會跳舞,只要還記得水晶男孩的舞台、歌曲,或某個讓你入坑的瞬間,都可以在這裡留下你的參與方式。</p>
</div>
<span class="countdown-note" id="countdown">💛 距離 30TH · 2027/04/15 倒數計算中...</span>
</section>
<!-- ③ 30TH 應援募集表單 -->
<section id="form-30th">
<h2 class="section-title">30TH 應援募集表單</h2>
<p class="form-lead">這份表單是水晶熱舞社為 2027/04/15 30TH 預備的應援募集模組,用來整理參與者想練的歌曲、可支援的內容與想留下的祝福。</p>
<div class="summer-form__feedback" data-form-feedback hidden tabindex="-1" aria-live="polite"></div>
<form class="summer-form" action="dance-application-submit.php" method="post" enctype="multipart/form-data" data-dance-application-form>
<label class="summer-form__trap" aria-hidden="true">網站<input type="text" name="website" tabindex="-1" autocomplete="off"></label>
<div class="summer-form__stage">
<h3 class="summer-form__stage-title">填寫須知</h3>
<div class="summer-form__stage-body summer-form__stage-body--notice">
<ul class="info-list">
<li>📎 檔案格式:影片或圖片皆可,單檔上限 8 MB</li>
<li>🔒 這份資料僅供水晶熱舞社內部整理 30TH 應援企劃使用</li>
<li>📅 送出後若確定參與,之後會另行公告練習與錄製時間</li>
</ul>
<p class="info-footer-text">水晶男孩推廣部・30TH 應援企劃</p>
</div>
</div>
<div class="summer-form__stage">
<h3 class="summer-form__stage-title">第一階段|參與者基本資料</h3>
<div class="summer-form__stage-body">
<div class="row g-4">
<div class="col-12 col-sm-6">
<label for="nickname" class="form-label">暱稱</label>
<input type="text" class="form-control" id="nickname" name="nickname"
maxlength="80" placeholder="例:熱舞小黃" required>
</div>
<div class="col-12 col-sm-6">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email"
maxlength="190" placeholder="dance@example.com" required>
</div>
<div class="col-12 col-sm-6">
<label for="dance_years" class="form-label">練舞經驗年數</label>
<input type="number" class="form-control" id="dance_years" name="dance_years"
min="0" max="30" placeholder="0">
</div>
<div class="col-12 col-sm-6">
<label for="available_date" class="form-label">可參與日期</label>
<input type="date" class="form-control" id="available_date" name="available_date">
</div>
</div>
</div>
</div>
<div class="summer-form__stage">
<h3 class="summer-form__stage-title">第二階段|30TH 參與意向</h3>
<div class="summer-form__stage-body">
<div class="row g-4">
<div class="col-12">
<label class="form-label d-block">是否參加過 20TH</label>
<div class="summer-form__option-row">
<input type="radio" class="btn-check" name="attended_20th"
id="attended_20th_yes" value="attended" autocomplete="off" required>
<label class="btn btn-outline-dance" for="attended_20th_yes">參加過 20TH</label>
<input type="radio" class="btn-check" name="attended_20th"
id="attended_20th_video" value="watched_video" autocomplete="off">
<label class="btn btn-outline-dance" for="attended_20th_video">看過 20TH 影片</label>
<input type="radio" class="btn-check" name="attended_20th"
id="attended_20th_first" value="first_time" autocomplete="off">
<label class="btn btn-outline-dance" for="attended_20th_first">第一次知道</label>
</div>
</div>
<div class="col-12">
<label class="form-label d-block">想參與的 30TH 內容(可複選)</label>
<div class="summer-form__option-row">
<input type="checkbox" class="btn-check" name="participate_content[]"
id="content_cover" value="cover" autocomplete="off">
<label class="btn btn-outline-dance" for="content_cover">翻跳</label>
<input type="checkbox" class="btn-check" name="participate_content[]"
id="content_medley" value="medley" autocomplete="off">
<label class="btn btn-outline-dance" for="content_medley">串燒聯舞</label>
<input type="checkbox" class="btn-check" name="participate_content[]"
id="content_blessing" value="blessing" autocomplete="off">
<label class="btn btn-outline-dance" for="content_blessing">祝福影片</label>
<input type="checkbox" class="btn-check" name="participate_content[]"
id="content_graphic" value="graphic" autocomplete="off">
<label class="btn btn-outline-dance" for="content_graphic">圖文應援</label>
<input type="checkbox" class="btn-check" name="participate_content[]"
id="content_editing" value="editing" autocomplete="off">
<label class="btn btn-outline-dance" for="content_editing">影片剪輯</label>
<input type="checkbox" class="btn-check" name="participate_content[]"
id="content_archive" value="archive" autocomplete="off">
<label class="btn btn-outline-dance" for="content_archive">資料整理</label>
</div>
</div>
</div>
</div>
</div>
<div class="summer-form__stage">
<h3 class="summer-form__stage-title">第三階段|應援內容與祝福</h3>
<div class="summer-form__stage-body">
<div class="row g-4">
<div class="col-12 col-sm-6">
<label for="song" class="form-label">想練的歌曲</label>
<select class="form-select" id="song" name="song">
<option value="" selected>請選擇歌曲</option>
<option value="couple">Couple</option>
<option value="comeback">Com' Back</option>
<option value="road_fighter">Road Fighter</option>
<option value="pom_saeng_pom_sa">Pom Saeng Pom Sa</option>
<option value="other">其他</option>
</select>
</div>
<div class="col-12 col-sm-6">
<label for="reference_file" class="form-label">上傳參考影片或圖片</label>
<input class="form-control" type="file" id="reference_file" name="reference_file"
accept="image/*,video/*">
<div class="form-text">翻跳影片、舞台照片或剪輯素材皆可</div>
</div>
<div class="col-12">
<label for="message_30th" class="form-label">想對 30TH 說的話</label>
<textarea class="form-control" id="message_30th" name="message_30th" rows="4" maxlength="5000"
placeholder="想對水晶熱舞社說的話、想貢獻的部分、想見到誰、想留下的祝福……"></textarea>
</div>
</div>
</div>
</div>
<div class="summer-form__actions">
<button type="reset" class="btn btn-reset-dance">重新填寫</button>
<button type="submit" class="btn btn-dance" data-submit-button>送出應援表單 💛</button>
</div>
</form>
</section>
</div>
<footer>
<p>SECHSKIES Fanpage &copy; 2026 - 黃色氣球永遠飄揚</p>
<a href="https://jekkinoopy.github.io/Portfolio/" class="studio-link" target="_blank" rel="noopener noreferrer">
Design by <span class="mark_b">Jekkinoopy Studio</span>
</a>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<script src="../assets/js/particles.js"></script>
<script src="../assets/js/portal-nav.js"></script>
<script>
(function () {
var target = new Date('2027-04-15T00:00:00+08:00');
var now = new Date();
var diffDays = Math.ceil((target - now) / (1000 * 60 * 60 * 24));
var el = document.getElementById('countdown');
if (el) {
el.textContent = diffDays > 0
? '💛 距離 30TH · 2027/04/15 還有 ' + diffDays + ' 天'
: '💛 30TH · 2027/04/15 已經到來,一起再集合!';
}
})();
(function () {
var form = document.querySelector('[data-dance-application-form]');
var feedback = document.querySelector('[data-form-feedback]');
var submitButton = document.querySelector('[data-submit-button]');
var result = new URLSearchParams(window.location.search).get('form');
var messages = {
success: ['success', '已收到你的應援報名!水晶熱舞社整理後會依你留下的 Email 聯絡。'],
invalid: ['error', '有欄位尚未完成或格式不正確,請檢查後再送出。'],
file_too_large: ['error', '上傳檔案超過 8 MB,請縮小檔案後再送出。'],
file_type: ['error', '檔案格式不支援,請改用圖片、MP4、WebM 或 MOV。'],
too_many: ['error', '短時間內送出次數較多,請稍候兩分鐘再試。'],
unavailable: ['error', '目前暫時無法儲存報名,請稍後再試。']
};
function showFeedback(type, message) {
if (!feedback) return;
feedback.hidden = false;
feedback.className = 'summer-form__feedback summer-form__feedback--' + type;
feedback.textContent = message;
feedback.focus();
}
if (result && messages[result]) {
showFeedback(messages[result][0], messages[result][1]);
}
if (form) {
form.addEventListener('submit', function (event) {
var selectedContent = form.querySelectorAll('input[name="participate_content[]"]:checked');
if (!selectedContent.length) {
event.preventDefault();
showFeedback('error', '請至少選擇一項想參與的 30TH 內容。');
var firstContent = form.querySelector('input[name="participate_content[]"]');
if (firstContent) firstContent.focus();
return;
}
if (submitButton) {
submitButton.disabled = true;
submitButton.textContent = '送出中…';
}
});
}
})();
</script>
</body>
</html>
+170
View File
@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
$config = require __DIR__ . '/../admin/config.php';
function return_to_dance_form(string $result): never
{
header('Location: crystal-dance-survey.html?form=' . rawurlencode($result) . '#form-30th', true, 303);
exit;
}
function post_value(string $key, int $maxLength): string
{
$value = trim((string) ($_POST[$key] ?? ''));
return mb_substr($value, 0, $maxLength, 'UTF-8');
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
header('Allow: POST');
exit('Method Not Allowed');
}
$maxUploadBytes = (int) $config['dance_max_upload_bytes'];
$contentLength = (int) ($_SERVER['CONTENT_LENGTH'] ?? 0);
if ($contentLength > $maxUploadBytes + 1024 * 1024) {
return_to_dance_form('file_too_large');
}
// 隱藏欄位有內容時視為機器送件;不透露攔截規則。
if (trim((string) ($_POST['website'] ?? '')) !== '') {
return_to_dance_form('success');
}
$nickname = post_value('nickname', 80);
$email = mb_strtolower(post_value('email', 190), 'UTF-8');
$danceYearsRaw = trim((string) ($_POST['dance_years'] ?? ''));
$availableDate = trim((string) ($_POST['available_date'] ?? ''));
$attended20th = trim((string) ($_POST['attended_20th'] ?? ''));
$song = trim((string) ($_POST['song'] ?? ''));
$message30th = post_value('message_30th', 5000);
$allowedAttendance = ['attended', 'watched_video', 'first_time'];
$allowedContent = ['cover', 'medley', 'blessing', 'graphic', 'editing', 'archive'];
$allowedSongs = ['', 'couple', 'comeback', 'road_fighter', 'pom_saeng_pom_sa', 'other'];
$selectedContent = $_POST['participate_content'] ?? [];
if (!is_array($selectedContent)) {
$selectedContent = [$selectedContent];
}
$selectedContent = array_values(array_unique(array_intersect(
$allowedContent,
array_map(static fn ($value): string => trim((string) $value), $selectedContent)
)));
if ($nickname === '' || !filter_var($email, FILTER_VALIDATE_EMAIL) || !$selectedContent) {
return_to_dance_form('invalid');
}
if ($attended20th !== '' && !in_array($attended20th, $allowedAttendance, true)) {
return_to_dance_form('invalid');
}
if (!in_array($song, $allowedSongs, true)) {
return_to_dance_form('invalid');
}
$danceYears = null;
if ($danceYearsRaw !== '') {
$danceYearsFilter = filter_var($danceYearsRaw, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 30]]);
if ($danceYearsFilter === false) {
return_to_dance_form('invalid');
}
$danceYears = (int) $danceYearsFilter;
}
if ($availableDate !== '') {
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $availableDate);
if (!$date || $date->format('Y-m-d') !== $availableDate) {
return_to_dance_form('invalid');
}
}
$db = $config['db'];
$dsn = "mysql:host={$db['host']};port={$db['port']};dbname={$db['name']};charset=utf8mb4";
try {
$pdo = new PDO($dsn, $db['user'], $db['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
} catch (PDOException) {
return_to_dance_form('unavailable');
}
$submittedIp = mb_substr((string) ($_SERVER['REMOTE_ADDR'] ?? ''), 0, 45, 'UTF-8');
if ($submittedIp !== '') {
$rate = $pdo->prepare('SELECT COUNT(*) FROM dance_applications WHERE submitted_ip = ? AND created_at >= DATE_SUB(NOW(), INTERVAL 2 MINUTE)');
$rate->execute([$submittedIp]);
if ((int) $rate->fetchColumn() >= 3) {
return_to_dance_form('too_many');
}
}
$storedFileName = null;
$originalFileName = null;
$mimeType = null;
$fileSize = null;
$file = $_FILES['reference_file'] ?? null;
if (is_array($file) && (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) {
if ((int) $file['error'] !== UPLOAD_ERR_OK || (int) $file['size'] > $maxUploadBytes) {
return_to_dance_form('file_too_large');
}
$allowedMimeTypes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
'video/mp4' => 'mp4',
'video/webm' => 'webm',
'video/quicktime' => 'mov',
];
$mimeType = (new finfo(FILEINFO_MIME_TYPE))->file((string) $file['tmp_name']) ?: '';
if (!isset($allowedMimeTypes[$mimeType])) {
return_to_dance_form('file_type');
}
$uploadDir = (string) $config['dance_upload_dir'];
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0750, true) && !is_dir($uploadDir)) {
return_to_dance_form('unavailable');
}
$storedFileName = bin2hex(random_bytes(20)) . '.' . $allowedMimeTypes[$mimeType];
if (!move_uploaded_file((string) $file['tmp_name'], $uploadDir . DIRECTORY_SEPARATOR . $storedFileName)) {
return_to_dance_form('unavailable');
}
$originalFileName = mb_substr(basename((string) $file['name']), 0, 255, 'UTF-8');
$fileSize = (int) $file['size'];
}
try {
$stmt = $pdo->prepare(
'INSERT INTO dance_applications
(nickname, email, dance_years, available_date, attended_20th, participate_content, song,
reference_file_name, reference_original_name, reference_mime_type, reference_file_size,
message_30th, submitted_ip, user_agent)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
);
$stmt->execute([
$nickname,
$email,
$danceYears,
$availableDate !== '' ? $availableDate : null,
$attended20th !== '' ? $attended20th : null,
implode(',', $selectedContent),
$song !== '' ? $song : null,
$storedFileName,
$originalFileName,
$mimeType,
$fileSize,
$message30th !== '' ? $message30th : null,
$submittedIp !== '' ? $submittedIp : null,
mb_substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255, 'UTF-8'),
]);
} catch (PDOException) {
if ($storedFileName !== null) {
@unlink((string) $config['dance_upload_dir'] . DIRECTORY_SEPARATOR . $storedFileName);
}
return_to_dance_form('unavailable');
}
return_to_dance_form('success');
+52
View File
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
$config = require __DIR__ . '/../admin/config.php';
$db = $config['db'];
$dsn = "mysql:host={$db['host']};port={$db['port']};dbname={$db['name']};charset=utf8mb4";
try {
$pdo = new PDO($dsn, $db['user'], $db['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$rows = $pdo->query(
"SELECT id, title, tags, practice_focus, video_url, difficulty, progress_status,
note_text, is_featured, sort_order, status
FROM dance_practice_items
WHERE status IN ('published', 'coming_soon')
ORDER BY sort_order, id"
)->fetchAll();
} catch (PDOException) {
http_response_code(503);
echo json_encode(['ok' => false, 'items' => []], JSON_UNESCAPED_UNICODE);
exit;
}
$difficultyLabels = ['easy' => '入門', 'medium' => '中等', 'hard' => '挑戰'];
$progressLabels = ['not_started' => '未開始', 'practicing' => '練習中', 'review' => '待複習', 'completed' => '已完成'];
$items = array_map(static function (array $row) use ($difficultyLabels, $progressLabels): array {
$videoUrl = trim((string) ($row['video_url'] ?? ''));
if ($videoUrl !== '' && (!filter_var($videoUrl, FILTER_VALIDATE_URL) || !preg_match('/^https?:\/\//i', $videoUrl))) {
$videoUrl = '';
}
return [
'id' => (int) $row['id'],
'title' => (string) $row['title'],
'tags' => array_values(array_filter(array_map('trim', explode(',', (string) ($row['tags'] ?? ''))))),
'practiceFocus' => (string) $row['practice_focus'],
'videoUrl' => $videoUrl,
'difficulty' => $difficultyLabels[$row['difficulty']] ?? (string) $row['difficulty'],
'progress' => $progressLabels[$row['progress_status']] ?? (string) $row['progress_status'],
'note' => (string) ($row['note_text'] ?? ''),
'featured' => (bool) $row['is_featured'],
'comingSoon' => $row['status'] === 'coming_soon',
];
}, $rows);
echo json_encode(['ok' => true, 'items' => $items], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+84 -55
View File
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SECHSKIES 台灣行程地圖</title>
<title>SECHSKIES 節目景點地圖</title>
<link rel="stylesheet" href="../assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="../assets/css/tablet.css">
@@ -13,7 +13,7 @@
body.map-page .map-embed-all[hidden],
body.map-page .map-embed-single[hidden],
body.map-page .map-links-dual[hidden],
body.map-page #open-map-link[hidden] {
body.map-page .map-link[hidden] {
display: none !important;
}
@@ -29,19 +29,18 @@
否則會變成瀏覽器的「片段模式」,樣式常常吃不到或跑版。 -->
<header class="inner-page-header">
<div class="header-inner site-header-inner">
<h1 class="main-title">台灣行程列表</h1>
<h1 class="main-title">節目景點列表</h1>
<div class="subtitle-wrap">
<span class="sub-title">SECHSKIES TAIWAN ARCHIVE</span>
<span class="sub-title">SECHSKIES LOCATION ARCHIVE</span>
</div>
</div>
</header>
<nav class="portal-nav" aria-label="水晶男孩推廣部導覽" data-portal-nav></nav>
<div class="member-tabs" aria-label="成員篩選">
<button class="tab-btn active" data-member="eun" aria-pressed="true">殷空少搭飛機去台灣高雄</button>
<button class="tab-btn" data-member="ko" aria-pressed="false">高高父子遊台灣</button>
<button class="tab-btn" data-member="all" aria-pressed="false">全部行程</button>
<div class="region-tabs" aria-label="地區篩選" data-region-tabs>
<button class="region-tab-btn active" data-region="taipei" aria-pressed="true">台灣行程</button>
<button class="region-tab-btn" data-region="global" aria-pressed="false">海外行程</button>
</div>
<canvas id="particles"></canvas>
@@ -49,10 +48,26 @@
<input id="itinerary-search" type="search" aria-label="行程搜尋"
placeholder="搜尋地點、描述、標籤(例如:九份、夜市、Day 2)">
<main id="taipei" class="content-container map-layout">
<!-- 備註:Google My Maps 嵌入。預設殷志源單圖;「全部行程」並排兩張(殷志源在上/左)。
右側列表為敘事與導覽;地圖與標籤在 map.js 的 memberMaps 設定。 -->
<section class="map-sticky">
<main class="content-container">
<!-- 備註:每個 .region-panel 是獨立地區區塊,member-tabs 上的 data-map-embeddata-map-open
是該行程的 Google My Maps(可留空=該行程沒有地圖,只顯示卡片)。新增行程只要加一顆 tab 按鈕,
不需要改 map.js。「全部行程」的地圖會依目前分頁裡「有設定地圖」的 tab 動態組出來。 -->
<section id="taipei" class="region-panel" data-region-panel="taipei">
<div class="member-tabs" aria-label="成員篩選" data-member-tabs>
<button class="tab-btn active" data-member="eun" aria-pressed="true"
data-trip-name="殷空少搭飛機去台灣高雄" data-member-label="殷志源" data-sort-order="0"
data-map-embed="https://www.google.com/maps/d/embed?mid=1VGZ76SPPjm3NWCBQe-hkm08oe5WPRJI&amp;ehbc=2E312F"
data-map-open="https://www.google.com/maps/d/u/0/viewer?mid=1VGZ76SPPjm3NWCBQe-hkm08oe5WPRJI">殷空少搭飛機去台灣高雄</button>
<button class="tab-btn" data-member="ko" aria-pressed="false"
data-trip-name="高高父子遊台灣" data-member-label="高志溶" data-sort-order="1"
data-map-embed="https://www.google.com/maps/d/embed?mid=1xpAsSoO8gNmMiW4x2sfkndISFTtjMQDw&amp;hl=zh-TW&amp;ehbc=2E312F"
data-map-open="https://www.google.com/maps/d/u/0/viewer?mid=1xpAsSoO8gNmMiW4x2sfkndISFTtjMQDw">高高父子遊台灣</button>
<button class="tab-btn" data-member="all" aria-pressed="false">全部行程</button>
</div>
<div class="map-layout">
<section class="map-sticky" data-map-sticky>
<h2 class="map-title">水晶男孩台灣行程地圖</h2>
<div class="map-embed-single" id="map-embed-single">
<iframe id="member-map" class="map-my-maps-frame" title="SECHSKIES 踩點地圖(單一行程)" loading="lazy"
@@ -60,33 +75,14 @@
src="https://www.google.com/maps/d/embed?mid=1VGZ76SPPjm3NWCBQe-hkm08oe5WPRJI&ehbc=2E312F">
</iframe>
</div>
<div class="map-embed-all" id="map-embed-all" hidden>
<div class="map-embed-pair">
<p class="map-embed-label">殷空少搭飛機去台灣高雄</p>
<iframe id="member-map-eun" class="map-my-maps-frame" title="殷志源高雄行程 踩點地圖" loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
src="https://www.google.com/maps/d/embed?mid=1VGZ76SPPjm3NWCBQe-hkm08oe5WPRJI&ehbc=2E312F">
</iframe>
</div>
<div class="map-embed-pair">
<p class="map-embed-label">高高父子遊台灣</p>
<iframe id="member-map-ko" class="map-my-maps-frame" title="高高父子遊台灣 踩點地圖" loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
src="https://www.google.com/maps/d/embed?mid=1xpAsSoO8gNmMiW4x2sfkndISFTtjMQDw&hl=zh-TW&ehbc=2E312F">
</iframe>
</div>
</div>
<p class="map-embed-empty" data-map-embed-empty hidden>這個行程目前沒有設定地圖,只有下方的地點卡片。</p>
<div class="map-embed-all" id="map-embed-all" hidden></div>
<div class="map-links-wrap">
<a id="open-map-link" class="map-link" target="_blank"
href="https://www.google.com/maps/d/u/0/viewer?mid=1VGZ76SPPjm3NWCBQe-hkm08oe5WPRJI">
在 Google My Maps 開啟完整地圖
</a>
<div id="open-map-links-dual" class="map-links-dual" hidden>
<a id="open-map-link-eun" class="map-link" target="_blank"
href="https://www.google.com/maps/d/u/0/viewer?mid=1VGZ76SPPjm3NWCBQe-hkm08oe5WPRJI">開啟「殷空少搭飛機去台灣高雄」My Maps</a>
<a id="open-map-link-ko" class="map-link" target="_blank"
href="https://www.google.com/maps/d/u/0/viewer?mid=1xpAsSoO8gNmMiW4x2sfkndISFTtjMQDw">開啟「高高父子遊台灣」My Maps</a>
</div>
<div id="open-map-links-dual" class="map-links-dual" hidden></div>
</div>
</section>
@@ -95,7 +91,7 @@
你只要改 data-date/title/desc/quote/tag/link 和圖片,就會同步更新上方介紹。 -->
<article class="detail-panel" id="detail-panel" aria-live="polite">
<div class="detail-photo">
<img id="detail-image" src="https://picsum.photos/seed/day1-airport/900/520" alt="行程節目截圖">
<img id="detail-image" src="https://images.pexels.com/photos/31504986/pexels-photo-31504986.jpeg" alt="臺灣桃園國際機場停機坪與客機">
</div>
<div class="detail-content">
<span id="detail-date" class="date">Day 1</span>
@@ -112,7 +108,7 @@
data-quote="高雄,我們來了。" data-tag="#殷志源 #Day1 #出發"
data-link="https://maps.google.com/?q=仁川國際機場">
<div class="photo-frame">
<img src="https://picsum.photos/seed/eun-khh-day1-1/640/420" alt="殷志源高雄行程節目截圖-仁川國際機場">
<img src="https://images.pexels.com/photos/32211619/pexels-photo-32211619.jpeg" alt="仁川國際機場明亮現代化航廈">
</div>
<div class="card-content">
<span class="date">殷志源 Day 1</span>
@@ -125,7 +121,7 @@
data-desc="抵達高雄,正式開始在地行程。"
data-quote="落地就是旅行模式開啟。" data-tag="#殷志源 #Day1 #高雄"
data-link="https://maps.google.com/?q=高雄國際機場">
<div class="photo-frame"><img src="https://picsum.photos/seed/eun-khh-day1-2/640/420" alt="殷志源高雄行程節目截圖-高雄國際機場"></div>
<div class="photo-frame"><img src="https://upload.wikimedia.org/wikipedia/commons/d/d7/Outside_of_KHH_Terminal.jpg" alt="高雄國際機場航廈外觀" title="圖片來源:Wikimedia Commons,© ABOVE THE SKYCC BY-SA 4.0"><span class="photo-credit">© ABOVE THE SKY</span></div>
<div class="card-content"><span class="date">殷志源 Day 1</span><h3>高雄國際機場</h3><p class="description">殷志源高雄行程第二站。</p><div class="tag">#殷志源 #Day1</div></div>
</article>
<article class="polaroid-card" data-member="eun" data-date="殷志源 Day 1" data-title="星巴克小港國際站門市"
@@ -160,28 +156,28 @@
data-desc="高雄經典景點,白天景色與步行路線都很適合取景。"
data-quote="每一步都有風景可以拍。" data-tag="#殷志源 #Day2 #景點"
data-link="https://maps.google.com/?q=蓮池潭">
<div class="photo-frame"><img src="https://picsum.photos/seed/eun-khh-day2-2/640/420" alt="殷志源高雄行程節目截圖-蓮池潭"></div>
<div class="photo-frame"><img src="https://upload.wikimedia.org/wikipedia/commons/b/b3/Lotus_Lake%2C_Kaohsiung%2C_panorama%2C_Dec_06.jpg" alt="蓮池潭與春秋閣全景" title="圖片來源:Wikimedia Commons,© SengkangCC BY-SA 3.0"><span class="photo-credit">© Sengkang</span></div>
<div class="card-content"><span class="date">殷志源 Day 2</span><h3>蓮池潭</h3><p class="description">殷志源高雄行程 Day 2 第二站。</p><div class="tag">#殷志源 #Day2</div></div>
</article>
<article class="polaroid-card" data-member="eun" data-date="殷志源 Day 2" data-title="龍虎塔"
data-desc="蓮池潭代表地標,適合搭配導覽與拍照段落。"
data-quote="高雄的經典場景之一。" data-tag="#殷志源 #Day2 #地標"
data-link="https://maps.google.com/?q=龍虎塔">
<div class="photo-frame"><img src="https://picsum.photos/seed/eun-khh-day2-3/640/420" alt="殷志源高雄行程節目截圖-龍虎塔"></div>
<div class="photo-frame"><img src="https://upload.wikimedia.org/wikipedia/commons/b/b6/Dragon_and_Tiger_Pagodas.jpg" alt="蓮池潭龍虎塔與九曲橋" title="圖片來源:Wikimedia Commons,© SelvamsathishCC BY-SA 4.0"><span class="photo-credit">© Selvamsathish</span></div>
<div class="card-content"><span class="date">殷志源 Day 2</span><h3>龍虎塔</h3><p class="description">殷志源高雄行程 Day 2 第三站。</p><div class="tag">#殷志源 #Day2</div></div>
</article>
<article class="polaroid-card" data-member="eun" data-date="殷志源 Day 2" data-title="輕軌高雄展覽館站(新光碼頭)"
data-desc="港區路線轉乘點,串接後續海線行程。"
data-quote="搭上輕軌,城市風景一路展開。" data-tag="#殷志源 #Day2 #輕軌"
data-link="https://maps.google.com/?q=輕軌高雄展覽館站(新光碼頭)">
<div class="photo-frame"><img src="https://picsum.photos/seed/eun-khh-day2-4/640/420" alt="殷志源高雄行程節目截圖-輕軌高雄展覽館站"></div>
<div class="photo-frame"><img src="https://upload.wikimedia.org/wikipedia/commons/1/1c/Kaohsiung_LRT_C8_Station.jpg" alt="高雄輕軌車站與列車" title="圖片來源:Wikimedia Commons,© Tze Chiang HaoCC BY-SA 4.0"><span class="photo-credit">© Tze Chiang Hao</span></div>
<div class="card-content"><span class="date">殷志源 Day 2</span><h3>輕軌高雄展覽館站(新光碼頭)</h3><p class="description">殷志源高雄行程 Day 2 第四站。</p><div class="tag">#殷志源 #Day2</div></div>
</article>
<article class="polaroid-card" data-member="eun" data-date="殷志源 Day 2" data-title="捷運哈瑪星站(輕軌哈瑪星站)"
data-desc="山海交界的人氣轉運點,步行動線完整。"
data-quote="到了哈瑪星,海港味就很濃。" data-tag="#殷志源 #Day2 #捷運"
data-link="https://maps.google.com/?q=捷運哈瑪星站">
<div class="photo-frame"><img src="https://picsum.photos/seed/eun-khh-day2-5/640/420" alt="殷志源高雄行程節目截圖-捷運哈瑪星站"></div>
<div class="photo-frame"><img src="https://upload.wikimedia.org/wikipedia/commons/7/7c/Hamasen%2C_Kaohsiung_2011-04-24.jpg" alt="哈瑪星港區歷史街景" title="圖片來源:Wikimedia Commons,© Yi ChenCC BY 2.0"><span class="photo-credit">© Yi Chen</span></div>
<div class="card-content"><span class="date">殷志源 Day 2</span><h3>捷運哈瑪星站(輕軌哈瑪星站)</h3><p class="description">殷志源高雄行程 Day 2 第五站。</p><div class="tag">#殷志源 #Day2</div></div>
</article>
<article class="polaroid-card" data-member="eun" data-date="殷志源 Day 2" data-title="仁武烤鴨"
@@ -202,14 +198,14 @@
data-desc="前往旗津的重要交通節點。"
data-quote="坐上渡輪,旅程感直接拉滿。" data-tag="#殷志源 #Day2 #輪渡"
data-link="https://maps.google.com/?q=鼓山輪渡站">
<div class="photo-frame"><img src="https://picsum.photos/seed/eun-khh-day2-8/640/420" alt="殷志源高雄行程節目截圖-鼓山輪渡站"></div>
<div class="photo-frame"><img src="https://upload.wikimedia.org/wikipedia/commons/e/e0/Gushan_Ferry_Station-20250403.jpg" alt="鼓山輪渡站站體" title="圖片來源:Wikimedia Commons,© AllervousCC BY-SA 4.0"><span class="photo-credit">© Allervous</span></div>
<div class="card-content"><span class="date">殷志源 Day 2</span><h3>鼓山輪渡站</h3><p class="description">殷志源高雄行程 Day 2 第八站。</p><div class="tag">#殷志源 #Day2</div></div>
</article>
<article class="polaroid-card" data-member="eun" data-date="殷志源 Day 2" data-title="旗津輪渡站"
data-desc="抵達旗津,展開海岸線路段。"
data-quote="到了旗津,風都不一樣。" data-tag="#殷志源 #Day2 #旗津"
data-link="https://maps.google.com/?q=旗津輪渡站">
<div class="photo-frame"><img src="https://picsum.photos/seed/eun-khh-day2-9/640/420" alt="殷志源高雄行程節目截圖-旗津輪渡站"></div>
<div class="photo-frame"><img src="https://upload.wikimedia.org/wikipedia/commons/4/4a/%E6%97%97%E6%B4%A5%E8%BC%AA%E6%B8%A1%E7%AB%99%E5%BB%BA%E7%AF%89%E5%A4%96%E8%A7%80.jpg" alt="旗津輪渡站建築外觀" title="圖片來源:Wikimedia Commons,© Yu-Cian WuCC BY-SA 4.0"><span class="photo-credit">© Yu-Cian Wu</span></div>
<div class="card-content"><span class="date">殷志源 Day 2</span><h3>旗津輪渡站</h3><p class="description">殷志源高雄行程 Day 2 第九站。</p><div class="tag">#殷志源 #Day2</div></div>
</article>
<article class="polaroid-card" data-member="eun" data-date="殷志源 Day 2" data-title="樂騎腳踏車、電動車、遊園車(旗津)"
@@ -223,14 +219,14 @@
data-desc="旗津拍照地標之一。"
data-quote="彩虹教堂是旗津必拍清單。" data-tag="#殷志源 #Day2 #旗津"
data-link="https://maps.google.com/?q=旗津彩虹教堂">
<div class="photo-frame"><img src="https://picsum.photos/seed/eun-khh-day2-11/640/420" alt="殷志源高雄行程節目截圖-旗津彩虹教堂"></div>
<div class="photo-frame"><img src="https://upload.wikimedia.org/wikipedia/commons/3/37/%E3%80%90%E9%AB%98%E9%9B%84%E6%99%AF%E9%BB%9E%E3%80%91%E6%97%97%E6%B4%A5%E5%BD%A9%E8%99%B9%E6%95%99%E5%A0%82_%2831505226685%29.jpg" alt="旗津彩虹教堂繽紛拱門" title="圖片來源:Wikimedia Commons,© fullfen666CC BY-SA 2.0"><span class="photo-credit">© fullfen666</span></div>
<div class="card-content"><span class="date">殷志源 Day 2</span><h3>旗津彩虹教堂</h3><p class="description">殷志源高雄行程 Day 2 第十一站。</p><div class="tag">#殷志源 #Day2</div></div>
</article>
<article class="polaroid-card" data-member="eun" data-date="殷志源 Day 2" data-title="旗津貝殼館"
data-desc="旗津主題展點,補足海線內容。"
data-quote="一站一站把海線故事拼起來。" data-tag="#殷志源 #Day2 #旗津"
data-link="https://maps.google.com/?q=旗津貝殼館">
<div class="photo-frame"><img src="https://picsum.photos/seed/eun-khh-day2-12/640/420" alt="殷志源高雄行程節目截圖-旗津貝殼館"></div>
<div class="photo-frame"><img src="https://upload.wikimedia.org/wikipedia/commons/f/fa/Cijin_Shell_Museum.JPG" alt="旗津貝殼館外觀" title="圖片來源:Wikimedia Commons,© SSR2000CC BY-SA 3.0"><span class="photo-credit">© SSR2000</span></div>
<div class="card-content"><span class="date">殷志源 Day 2</span><h3>旗津貝殼館</h3><p class="description">殷志源高雄行程 Day 2 第十二站。</p><div class="tag">#殷志源 #Day2</div></div>
</article>
<article class="polaroid-card" data-member="eun" data-date="殷志源 Day 2" data-title="海忠寶活海產店"
@@ -246,7 +242,7 @@
data-quote="旅程從落地那一刻就開始了。" data-tag="#高高父子遊台灣 #Day1 #機場"
data-link="https://maps.google.com/?q=臺灣桃園國際機場">
<div class="photo-frame">
<img src="https://picsum.photos/seed/day1-airport/640/420" alt="高高父子遊台灣節目截圖-臺灣桃園國際機場">
<img src="https://images.pexels.com/photos/31504986/pexels-photo-31504986.jpeg" alt="臺灣桃園國際機場停機坪與客機">
</div>
<div class="card-content">
<span class="date">高志溶 Day 1</span>
@@ -261,7 +257,7 @@
data-quote="從這裡開始感受台北節奏。" data-tag="#高高父子遊台灣 #Day1 #西門"
data-link="https://maps.google.com/?q=捷運西門站">
<div class="photo-frame">
<img src="https://picsum.photos/seed/day1-ximen-mrt/640/420" alt="高高父子遊台灣節目截圖-捷運西門站">
<img src="https://images.pexels.com/photos/32222711/pexels-photo-32222711.jpeg" alt="西門町街景">
</div>
<div class="card-content">
<span class="date">Day 1</span>
@@ -306,7 +302,7 @@
data-quote="台灣夜市是不能錯過的必修行程。" data-tag="#高高父子遊台灣 #Day1 #夜市"
data-link="https://maps.google.com/?q=士林夜市">
<div class="photo-frame">
<img src="https://picsum.photos/seed/day1-nightmarket/640/420" alt="高高父子遊台灣節目截圖-士林夜市">
<img src="https://images.pexels.com/photos/35558289/pexels-photo-35558289.jpeg" alt="士林夜市夜間街景">
</div>
<div class="card-content">
<span class="date">Day 1</span>
@@ -351,7 +347,8 @@
data-quote="第二天從白天的城市日常開始。" data-tag="#高高父子遊台灣 #Day2 #校園"
data-link="https://maps.google.com/?q=臺灣師範大學">
<div class="photo-frame">
<img src="https://picsum.photos/seed/day2-ntnu/640/420" alt="高高父子遊台灣節目截圖-臺灣師範大學">
<img src="https://upload.wikimedia.org/wikipedia/commons/b/b0/202407_Main_gate_of_NTNU_Main_Campus.jpg" alt="臺灣師範大學校門" title="圖片來源:Wikimedia Commons,© JonashtandCC BY-SA 4.0">
<span class="photo-credit">© Jonashtand</span>
</div>
<div class="card-content">
<span class="date">Day 2</span>
@@ -381,7 +378,7 @@
data-quote="九份是會讓人想慢慢走慢慢看的地方。" data-tag="#高高父子遊台灣 #Day2 #九份"
data-link="https://maps.google.com/?q=九份老街">
<div class="photo-frame">
<img src="https://picsum.photos/seed/day2-jiufen/640/420" alt="高高父子遊台灣節目截圖-九份老街">
<img src="https://images.pexels.com/photos/37975552/pexels-photo-37975552.jpeg" alt="九份老街紅燈籠夜景">
</div>
<div class="card-content">
<span class="date">Day 2</span>
@@ -426,7 +423,8 @@
data-quote="第三天就用溫泉感把節奏放慢。" data-tag="#高高父子遊台灣 #Day3 #北投"
data-link="https://maps.google.com/?q=北投公園露天溫泉">
<div class="photo-frame">
<img src="https://picsum.photos/seed/day3-hotspring/640/420" alt="高高父子遊台灣節目截圖-北投公園露天溫泉">
<img src="https://upload.wikimedia.org/wikipedia/commons/d/da/Hot_river_%283823489104%29.jpg" alt="北投溫泉公園地熱溪流" title="圖片來源:Wikimedia Commons,© Caitriana NicholsonCC BY-SA 2.0">
<span class="photo-credit">© Caitriana Nicholson</span>
</div>
<div class="card-content">
<span class="date">Day 3</span>
@@ -441,7 +439,8 @@
data-quote="在橋上看風景,旅程也會變慢。" data-tag="#高高父子遊台灣 #Day3"
data-link="https://maps.google.com/?q=靜安吊橋">
<div class="photo-frame">
<img src="https://picsum.photos/seed/day3-bridge/640/420" alt="高高父子遊台灣節目截圖-靜安吊橋">
<img src="https://upload.wikimedia.org/wikipedia/commons/a/a8/Jing%27an_Suspension_Bridge_New_Taipei_%E9%9D%9C%E5%AE%89%E6%A9%8B_%282025%29_-_img_08.jpg" alt="靜安吊橋" title="圖片來源:Wikimedia Commons,© Chainwit.CC BY 4.0">
<span class="photo-credit">© Chainwit.</span>
</div>
<div class="card-content">
<span class="date">Day 3</span>
@@ -456,7 +455,7 @@
data-quote="十分的氛圍,跟平溪線一樣有故事感。" data-tag="#高高父子遊台灣 #Day3 #十分"
data-link="https://maps.google.com/?q=十分老街">
<div class="photo-frame">
<img src="https://picsum.photos/seed/day3-shifen/640/420" alt="高高父子遊台灣節目截圖-十分老街">
<img src="https://images.pexels.com/photos/30131488/pexels-photo-30131488.jpeg" alt="十分老街火車穿越街道">
</div>
<div class="card-content">
<span class="date">Day 3</span>
@@ -501,7 +500,8 @@
data-quote="把願望寫上天燈,這趟旅程就有了句點。" data-tag="#高高父子遊台灣 #Day3 #天燈"
data-link="https://maps.google.com/?q=胡家天燈">
<div class="photo-frame">
<img src="https://picsum.photos/seed/day3-sky-lantern/640/420" alt="高高父子遊台灣節目截圖-胡家天燈">
<img src="https://upload.wikimedia.org/wikipedia/commons/5/50/20250707_lantern2.jpg" alt="十分天燈夜間施放" title="圖片來源:Wikimedia Commons,© WombatjpwCC0">
<span class="photo-credit">© Wombatjpw</span>
</div>
<div class="card-content">
<span class="date">Day 3</span>
@@ -511,9 +511,38 @@
</div>
</article>
</section>
</div>
</section>
<section id="global" class="region-panel" data-region-panel="global" hidden>
<div class="member-tabs" aria-label="成員篩選" data-member-tabs>
<button class="tab-btn active" data-member="all" aria-pressed="true">全部行程</button>
</div>
<div class="map-layout">
<section class="map-sticky" data-map-sticky hidden>
<h2 class="map-title">海外行程地圖</h2>
<div class="map-embed-single" id="map-embed-single-global">
<iframe id="member-map-global" class="map-my-maps-frame" title="SECHSKIES 踩點地圖(單一行程)" loading="lazy"
referrerpolicy="no-referrer-when-downgrade" src="about:blank"></iframe>
</div>
<p class="map-embed-empty" data-map-embed-empty hidden>這個行程目前沒有設定地圖,只有下方的地點卡片。</p>
<div class="map-embed-all" id="map-embed-all-global" hidden></div>
<div class="map-links-wrap">
<a class="map-link" target="_blank" href="#" hidden>在 Google My Maps 開啟完整地圖</a>
<div class="map-links-dual" hidden></div>
</div>
</section>
<section class="itinerary-list">
<p class="region-empty-state" data-region-empty>
海外行程整理中,之後會陸續補上節目裡出現過的國家與地點。
</p>
</section>
</div>
</section>
</main>
<footer>
<div id="global"></div>
<p>SECHSKIES Fanpage &copy; 2026 - 黃色氣球永遠飄揚</p>
<a href="https://jekkinoopy.github.io/Portfolio/" class="studio-link" target="_blank">
Design by <span class="mark_b">Jekkinoopy Studio</span>
File diff suppressed because it is too large Load Diff