docs: integrate Claude documentation and implement project management system

- Merge CLAUDE-協作指南.md and CLAUDE.md into unified CLAUDE.md v3.0
- Update all commands from ./drama to ./dl for brevity
- Remove redundant documentation files (README-問題管理.md)
- Add comprehensive project execution management system with PROJECTS.md
- Implement phase-based project management with tools/project.sh and tools/phase.sh
- Add project templates and Flutter/Backend source structure
- Update Claude settings to support new ./dl commands

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-09-08 17:20:45 +08:00
parent f06257c2d9
commit 115a003afe
49 changed files with 5000 additions and 286 deletions

View File

@ -15,7 +15,31 @@
"Bash(./drama issue)",
"Bash(./drama report \"UI設計缺漏嚴重性評估\")",
"Bash(./drama compliance)",
"Bash(./drama report analysis \"文檔分類組織結構優化\")"
"Bash(./drama report analysis \"文檔分類組織結構優化\")",
"Bash(git push:*)",
"Bash(flutter:*)",
"Bash(mkdir:*)",
"Bash(echo $PATH)",
"Bash(/Users/jettcheng1018/flutter/flutter_3.24.5/bin/flutter --version)",
"Read(//Users/jettcheng1018/flutter/**)",
"Bash(/Users/jettcheng1018/flutter/bin/flutter --version)",
"Bash(/Users/jettcheng1018/flutter/bin/flutter pub get)",
"Bash(/Users/jettcheng1018/flutter/bin/flutter doctor)",
"Bash(/Users/jettcheng1018/flutter/bin/flutter run -d chrome --web-port=8080)",
"Bash(/Users/jettcheng1018/flutter/bin/flutter clean)",
"Read(//Users/jettcheng1018/**)",
"Bash(./drama:*)",
"Bash(./dl)",
"Bash(./dl project list)",
"Bash(./dl status)",
"Bash(./dl project types)",
"Bash(./dl phase status)",
"Bash(./dl project help)",
"Bash(./dl phase help)",
"Bash(./dl phase list)",
"Bash(rm:*)",
"Bash(git add:*)",
"Bash(git rm:*)"
],
"deny": [],
"ask": []

View File

@ -1,103 +0,0 @@
# 🤖 與 Claude 協作指南
## 🎯 目標
確保 Claude 在協助開發時發現的所有問題都被記錄到問題管理系統中。
## 📋 每次請 Claude 協助時的提醒詞
### 🔥 標準提醒語句:
```
"如果你在過程中發現任何規格不確定、衝突、技術問題或需要決策的地方,請使用問題管理系統記錄下來。"
```
### 💫 簡短版本:
```
"遇到問題就記錄到問題系統"
```
### 🎯 具體場景提醒:
**實作功能時:**
```
"實作 [功能名稱],發現問題就用 ./issue.sh 記錄"
```
**檢查文檔時:**
```
"檢查 [文檔],找到不一致或不清楚的地方就記錄問題"
```
**重構程式時:**
```
"重構 [模組],遇到架構問題或技術債務就記錄"
```
## 🔄 Claude 應該記錄的問題類型
### 🔥 緊急問題
- 架構設計衝突
- 無法實作的需求
- 安全性問題
- 資料不一致
### ⚠️ 重要問題
- 規格定義模糊
- API 設計不確定
- UI/UX 流程不清楚
- 技術選型疑慮
### 📝 一般問題
- 文檔格式不統一
- 命名規範不一致
- 小的技術改進建議
- 程式碼品質提升
## 📝 任務完成後的檢查清單
每次 Claude 完成任務後,請檢查:
- [ ] Claude 有沒有提到任何「不確定」、「需要澄清」的地方?
- [ ] 有沒有發現文檔間的衝突?
- [ ] 有沒有提到技術實作的困難?
- [ ] 有沒有建議需要進一步決策的事項?
**如果有,就提醒:** "把剛才提到的問題記錄到問題系統"
## 🎯 協作流程範例
### 範例1實作功能
```
您: "實作用戶登入功能,遇到問題就記錄"
Claude: "好的我發現API規格中密碼驗證流程不明確..."
您: "把這個記錄到問題系統"
Claude: [使用 ./issue.sh 記錄]
```
### 範例2文檔檢查
```
您: "檢查API文檔一致性發現問題就用問題系統記錄"
Claude: "我發現用戶管理API和認證API的錯誤碼定義衝突..."
Claude: [自動使用 ./issue.sh 記錄問題]
```
## 💡 讓協作更順暢的技巧
### 🏷️ 在任務開始時就說明:
"我希望你把發現的所有問題都記錄下來,這樣我們就不會遺漏任何需要解決的事項。"
### 🔄 定期檢查:
每週問 Claude"最近有沒有發現什麼新的問題需要記錄?"
### 📊 任務總結:
"總結一下這次任務中發現的問題,並確保都記錄了。"
## 🎉 效益
**不會遺漏問題** - 所有發現的問題都被記錄
**追蹤更完整** - 包含 AI 協助時發現的問題
**決策有依據** - 問題記錄成為決策參考
**開發更順暢** - 提前發現潛在問題
---
**💫 記住Claude 是您的協作夥伴,讓他幫您記錄問題,讓專案更完善!**

202
CLAUDE.md
View File

@ -2,33 +2,40 @@
## 🤖 Claude 協作標準操作程序
本文件為 Claude AI 助手在 Drama Ling 專案中的標準操作程序,確保工作流程的一致性和品質
本文件為 Claude AI 助手在 Drama Ling 專案中的標準操作程序和協作指南
## 🛠️ 必須使用的系統工具
## 🛠️ 系統工具使用
### 報告建立
### 專案執行管理 (新增 2025-09-08)
```bash
# ✅ 正確做法:使用系統工具
./drama report analysis "分析主題"
./drama report decision "決策主題"
# ❌ 禁止行為:手動創建報告檔案
# 直接 Write 或 Edit reports/ 目錄下的檔案
# ✅ 正確做法:使用專案管理工具
./dl project list # 列出所有專案
./dl phase status # 查看階段狀態
./dl status # 查看執行狀態
```
### 問題管理
```bash
# ✅ 正確做法:使用問題管理工具
./drama issue
./dl issue # 互動式問題管理
# ❌ 禁止行為:直接編輯 ISSUES.md
# 除非是修正現有問題的格式錯誤
```
### 報告建立
```bash
# ✅ 正確做法:使用系統工具
./dl report analysis "分析主題"
./dl report decision "決策主題"
# ❌ 禁止行為:手動創建報告檔案
```
### 檢查作業
```bash
# ✅ 正確做法:使用檢查工具
./drama check
./dl check
# 其他維護腳本
./check_consistency.sh
@ -38,35 +45,101 @@
### 1. 工具優先原則
- **必須優先使用現有工具和腳本**
- 手動操作只能用於緊急修正
- 所有報告都必須透過 `./drama report` 創建
- 所有專案管理都透過 `./dl` 系統
- 所有問題記錄都透過 `./dl issue` 創建
- 所有報告都必須透過 `./dl report` 創建
### 2. 日期準確性原則
### 2. 專案管理整合原則 (新增 2025-09-08)
- **階段化執行**: 大型項目拆分為可管理的階段
- **任務分類**: 使用類型標記FE/BE/AI/MB/DOC/ENV/TEST
- **進度追蹤**: 即時更新任務狀態 (⏳ → 🔄 → ✅)
- **依賴管理**: 自動檢查前置條件
### 3. 協作提醒原則 (整合 2025-09-08)
用戶可以使用以下提醒語句確保問題被記錄:
**標準提醒語句:**
```
"如果你在過程中發現任何規格不確定、衝突、技術問題或需要決策的地方,請使用問題管理系統記錄下來。"
```
**簡短版本:**
```
"遇到問題就記錄到問題系統"
"發現問題就用 ./dl issue 記錄"
```
**具體場景提醒:**
```
"實作 [功能名稱],發現問題就用 ./dl issue 記錄"
"檢查 [文檔],找到不一致或不清楚的地方就記錄問題"
"重構 [模組],遇到架構問題或技術債務就記錄"
```
### 4. 日期準確性原則
- **系統工具會自動處理日期**
- 當前日期2025-09-08
- 任何手動設定日期都必須使用正確的當前日期
### 3. 文檔整合原則
### 5. 文檔整合原則
- 所有問題必須記錄到 ISSUES.md
- 所有分析必須產生正式報告
- 報告必須與問題系統整合
## 📋 Claude 應該記錄的問題類型
### 🔥 緊急問題
- 架構設計衝突
- 無法實作的需求
- 安全性問題
- 資料不一致
### ⚠️ 重要問題
- 規格定義模糊
- API 設計不確定
- UI/UX 流程不清楚
- 技術選型疑慮
### 📝 一般問題
- 文檔格式不統一
- 命名規範不一致
- 小的技術改進建議
- 程式碼品質提升
## 📋 標準工作流程
### 專案任務執行流程 (新增 2025-09-08)
#### 任務執行步驟
1. 用戶提供任務名稱(如:"Android Studio 安裝和配置"
2. Claude 識別任務類型、階段、專案歸屬
3. 執行相關工作
4. 自動更新 PROJECTS.md 狀態 (⏳ → 🔄 → ✅)
5. 記錄執行結果和發現的問題
#### 專案管理互動範例
```
用戶: "執行 Android Studio 安裝和配置"
Claude: 識別為 ENV 類型任務屬於階段1環境配置
[執行安裝配置工作]
[更新 PROJECTS.md 狀態]
[報告完成情況]
```
### 分析任務流程
1. **建立分析報告**: `./drama report analysis "主題"`
1. **建立分析報告**: `./dl report analysis "主題"`
2. **執行分析工作**: 使用適當工具進行分析
3. **更新報告內容**: 編輯生成的報告檔案
4. **整合問題系統**: 確認相關問題已正確連結
### 問題處理流程
1. **記錄問題**: `./drama issue`
1. **記錄問題**: `./dl issue`
2. **分配優先級**: 🔥緊急 / ⚠️重要 / 📝一般
3. **建立相關報告**: 如有需要,建立分析或決策報告
4. **追蹤解決進展**: 定期更新問題狀態
### 檢查作業流程
1. **執行系統檢查**: `./drama check`
1. **執行系統檢查**: `./dl check`
2. **運行一致性檢查**: `./check_consistency.sh`
3. **記錄發現問題**: 使用問題管理系統
4. **產生檢查報告**: 必要時建立分析報告
@ -75,7 +148,7 @@
### 錯誤1: 手動創建報告
**問題**: 直接創建報告檔案,導致日期錯誤、格式不一致
**解決**: 必須使用 `./drama report` 命令
**解決**: 必須使用 `./dl report` 命令
### 錯誤2: 忽略現有工具
**問題**: 重複實作已存在的功能
@ -83,7 +156,7 @@
### 錯誤3: 未整合問題系統
**問題**: 發現問題但未記錄到 ISSUES.md
**解決**: 每次發現問題都必須使用 `./drama issue`
**解決**: 每次發現問題都必須使用 `./dl issue`
### 錯誤4: 日期不一致
**問題**: 使用錯誤的日期或格式
@ -92,12 +165,6 @@
### 錯誤5: 文檔更新缺少時間戳記 (新增 2025-09-08)
**問題**: 更新文檔內容後未標記更新時間,難以追蹤變更歷史
**解決**: 任何文檔更新都必須加入時間戳記,格式為 (YYYY-MM-DD)
**範例**:
```
- [x] 任務完成 ✅ (2025-09-08)
📊 **進度更新**: 已完成19個UI (2025-09-08)
### 新增功能 (新增 2025-09-08)
```
## 🔍 品質檢查清單
@ -105,16 +172,21 @@
**⚠️ 重要:任何操作前都必須執行此檢查清單**
#### 問題管理操作前檢查
- [ ] 是否需要記錄新問題?如是,**必須使用** `./drama issue`
- [ ] 是否需要記錄新問題?如是,**必須使用** `./dl issue`
- [ ] 完成的問題是否要標記?如是,**絕對不可在待處理區標記[x]**
- [ ] 完成的問題**必須移動到「📚 已完成歷史」對應日期區域**
- [ ] 移動時**必須保留所有解決詳情和連結**
#### 報告建立操作前檢查
- [ ] 是否需要建立報告?如是,**必須使用** `./drama report analysis "主題"`
- [ ] 是否需要建立報告?如是,**必須使用** `./dl report analysis "主題"`
- [ ] **禁止手動創建** reports/ 目錄下的任何檔案
- [ ] 報告主題描述是否具體明確?
#### 專案管理操作前檢查 (新增 2025-09-08)
- [ ] 專案任務是否需要更新狀態?
- [ ] 任務類型是否正確識別FE/BE/AI/MB/DOC/ENV/TEST
- [ ] 是否需要建議下一步行動?
#### 檔案操作前檢查
- [ ] 檔案編碼是否設定為 UTF-8
- [ ] 中文內容是否正確顯示?
@ -133,12 +205,49 @@
### 每次任務完成後檢查
- [ ] 是否使用了正確的系統工具?
- [ ] 所有日期是否正確2025-09-08
- [ ] 任務狀態是否已更新為完成 ✅ (專案任務)
- [ ] 發現的問題是否已記錄?
- [ ] 報告是否已正確整合到問題系統?
- [ ] 檔案命名是否符合系統標準?
- [ ] **ISSUES.md中完成的項目是否已正確移動到歷史區域**
- [ ] **所有文檔更新是否都加入了時間戳記?**
## 📝 任務完成後的檢查清單 (整合 2025-09-08)
每次 Claude 完成任務後,請檢查:
- [ ] Claude 有沒有提到任何「不確定」、「需要澄清」的地方?
- [ ] 有沒有發現文檔間的衝突?
- [ ] 有沒有提到技術實作的困難?
- [ ] 有沒有建議需要進一步決策的事項?
**如果有,就提醒:** "把剛才提到的問題記錄到問題系統"
## 🎯 協作流程範例
### 範例1專案任務執行 (新增 2025-09-08)
```
用戶: "請執行 Flutter移動端配置調整"
Claude: 識別為 MB 類型任務,更新狀態為進行中...
[執行配置調整]
✅ 任務完成,狀態已更新 (2025-09-08)
```
### 範例2發現問題並記錄
```
用戶: "實作語音輸入功能,遇到問題就記錄"
Claude: 我發現API規格中音頻格式支援不明確...
[使用 ./dl issue 記錄問題]
已記錄問題:音頻格式規格不明確 ⚠️
```
### 範例3文檔檢查
```
用戶: "檢查API文檔一致性發現問題就用問題系統記錄"
Claude: 我發現用戶管理API和認證API的錯誤碼定義衝突...
[自動使用 ./dl issue 記錄問題]
```
## 🚨 緊急情況處理
### 工具故障時
@ -153,9 +262,21 @@
3. 記錄發現的不一致問題
4. 修正問題後繼續工作
## 💡 讓協作更順暢的技巧 (整合 2025-09-08)
### 🏷️ 在任務開始時就說明:
"我希望你把發現的所有問題都記錄下來,這樣我們就不會遺漏任何需要解決的事項。"
### 🔄 定期檢查:
每週問 Claude"最近有沒有發現什麼新的問題需要記錄?"
### 📊 任務總結:
"總結一下這次任務中發現的問題,並確保都記錄了。"
## 📚 相關文檔
- [問題追蹤系統](./ISSUES.md)
- [專案執行管理](./PROJECTS.md)
- [工具使用說明](./tools/)
- [報告模板](./reports/templates/)
- [檢查腳本](./scripts/)
@ -174,11 +295,7 @@
3. 修訂本指南文檔
4. 確保向下相容性
---
**重要提醒**: 本指南是 Claude AI 助手的強制性操作標準。任何偏離此流程的行為都可能造成系統不一致和品質問題。
## 🤝 標準化指令格式方案A
## 🤝 標準化指令格式
### 推薦指令格式
```
@ -187,13 +304,28 @@
### 範例指令
```
請分析UI設計問題遵循SOP記得使用./drama report analysis建立報告
請分析UI設計問題遵循SOP記得使用./dl report analysis建立報告
請處理緊急問題遵循SOP完成後問題要移到歷史區域不可標記[x]
請建立新的API文檔遵循SOP使用正確日期和UTF-8編碼
執行 Android Studio 安裝和配置,遇到問題就記錄
```
## 🎉 效益
**不會遺漏問題** - 所有發現的問題都被記錄
**追蹤更完整** - 包含 AI 協助時發現的問題
**決策有依據** - 問題記錄成為決策參考
**開發更順暢** - 提前發現潛在問題
**專案管理清晰** - 階段化執行,進度透明 (新增 2025-09-08)
---
**重要提醒**: 本指南是 Claude AI 助手的強制性操作標準。任何偏離此流程的行為都可能造成系統不一致和品質問題。
**💫 記住Claude 是您的協作夥伴,讓他幫您記錄問題和管理專案,讓開發更完善!**
---
**最後更新**: 2025-09-08
**版本**: 2.1 - 加入文檔更新時間戳記強制性要求 (2025-09-08)
**版本**: 3.0 - 整合專案管理系統和協作指南 (2025-09-08)
**維護者**: Drama Ling 開發團隊

84
PROJECTS.md Normal file
View File

@ -0,0 +1,84 @@
# 專案執行管理系統
## 📋 執行管理概述
本系統用於管理大型計劃的分階段執行,支援項目分解、進度追蹤和依賴關係管理。
**管理原則**:
- **階段化執行**: 大型項目拆分為可管理的階段
- **類型標注**: 每個執行項目標記具體類型
- **進度透明**: 即時追蹤執行狀態和阻塞點
- **依賴管理**: 自動檢查前置條件和依賴關係
## 🏷️ 項目類型定義
| 類型代碼 | 圖標 | 名稱 | 說明 |
|---------|------|------|------|
| `FE` | 🎨 | 前端開發 | Flutter UI/UX 開發 |
| `BE` | ⚙️ | 後端開發 | .NET Core API 開發 |
| `AI` | 🤖 | AI整合 | 語音、評分、對話系統 |
| `MB` | 📱 | 移動端 | Android/iOS 配置打包 |
| `DOC` | 📚 | 文檔更新 | 規格、API、使用文檔 |
| `ENV` | 🔧 | 環境配置 | 開發工具、部署環境 |
| `TEST` | 🧪 | 測試驗證 | 功能測試、整合測試 |
## 📊 執行狀態定義
- ⏳ **待執行** (pending): 尚未開始的項目
- 🔄 **進行中** (in-progress): 正在執行的項目
- ✅ **已完成** (completed): 成功完成的項目
- ❌ **已阻塞** (blocked): 遇到阻礙無法繼續的項目
- ⏸️ **已暫停** (paused): 暫時停止的項目
## 📁 當前執行項目
### 📱 Drama Ling 手機APP開發 (2025-09-08)
**項目描述**: 建立完整的手機APP實現AI驅動的語言學習功能
**當前階段**: 🔄 環境配置中
#### 階段1: 環境配置 🔄
- ⏳ `ENV` Android Studio 安裝和配置
- ⏳ `ENV` Xcode 安裝配置 (iOS支援)
- ⏳ `ENV` Android模擬器設置
- ⏳ `MB` Flutter移動端配置調整
#### 階段2: APP打包 ⏳
- ⏳ `MB` Android APK 生成配置
- ⏳ `FE` 應用圖標和啟動畫面設計
- ⏳ `MB` APP權限配置 (語音、網路)
- ⏳ `TEST` 真實設備測試
#### 階段3: 核心功能實現 ⏳
- ⏳ `AI` 語音輸入功能實現
- ⏳ `FE` 觸控操作優化
- ⏳ `AI` 三維度評分系統 (語法、語意、流暢度)
- ⏳ `AI` 劇本對話系統
- ⏳ `AI` 詞彙學習關卡系統
- ⏳ `AI` 限時挑戰系統 (300秒)
#### 階段4: 整合測試 ⏳
- ⏳ `TEST` 功能整合測試
- ⏳ `TEST` 效能優化測試
- ⏳ `DOC` 使用說明文檔
- ⏳ `MB` 正式版本打包
---
## 📈 項目統計
**總計項目**: 1 個
**進行中**: 1 個
**已完成**: 0 個
**執行項目統計**:
- ⏳ 待執行: 16 個
- 🔄 進行中: 0 個
- ✅ 已完成: 0 個
---
**最後更新**: 2025-09-08
**管理工具**: `./dl project`, `./dl phase`
**系統版本**: 1.0.0

View File

@ -1,142 +0,0 @@
# 🚨 Drama Ling 問題管理系統
## 🎯 統一入口點
### 🎭 **主命令** (推薦)
```bash
./drama # 顯示所有可用命令
./drama issue # 管理問題
./drama check # 檢查問題狀態
./drama report "標題" # 建立分析報告
./drama consistency # 一致性檢查
./drama all # 執行全部檢查
```
## 💫 其他使用方式
### 1**直接使用工具** (進階)
```bash
./tools/issue.sh # 直接使用問題管理工具
./tools/check_issues.sh # 直接檢查問題狀態
```
### 2**VS Code 快捷鍵** (推薦進階使用者)
- `Cmd+Shift+I` - 📝 記錄問題
- `Cmd+Shift+S` - 📊 查看狀態
- `Cmd+Shift+C` - 🔍 一致性檢查
- `Cmd+Shift+A` - 🚀 全部檢查
### 3**全域命令** (最方便)
```bash
# 先執行一次設置
./drama setup
# 重啟終端機後,在任何地方都能用:
dl # 主選單
dl-issue # 記錄問題
dl-check # 查看狀態
dl-report # 建立報告
dl-consistency # 一致性檢查
dl-all # 全部檢查
```
## 🚀 快速開始
### 第一次使用:
1. 打開終端機
2. `cd /Users/jettcheng1018/code/dramaling-app`
3. `./drama` 查看所有命令
4. `./drama issue` 開始管理問題
### 日常使用:
```bash
# 有問題時
./drama issue → 選1 → 輸入問題 → 選優先級
# 想查狀態時
./drama check
# 建立分析報告
./drama report "問題分析標題"
# 執行系統檢查
./drama all
```
## 📋 問題優先級
- 🔥 **緊急** - 阻擋開發的嚴重問題
- ⚠️ **重要** - 影響進度的重要問題
- 📝 **一般** - 可以延後的問題
## ✅ 解決問題
**方法1 - 用工具:**
`./issue.sh` → 選3 → 查看問題列表 → 手動編輯檔案
**方法2 - 直接編輯:**
打開 `ISSUES.md`,把 `[ ]` 改成 `[x]`,移到「已解決」區域
## 🔧 故障排除
### 權限問題:
```bash
chmod +x issue.sh
chmod +x setup_aliases.sh
```
### 找不到檔案:
```bash
# 確認在正確目錄
pwd
# 應該顯示: /Users/jettcheng1018/code/dramaling-app
```
### VS Code 快捷鍵無效:
1. 重啟 VS Code
2. 檢查是否在專案根目錄開啟 VS Code
## 💡 使用技巧
### 快速記錄:
- 發現問題立即記錄,不要拖延
- 描述要具體,包含檔案位置
- 優先級要準確判斷
### 定期回顧:
- 每週檢查一次狀態
- 將已解決問題移到完成區域
- 評估緊急問題是否需要立即處理
### 團隊協作:
- 問題描述要清楚,讓其他人也能理解
- 相關檔案路徑要完整
- 解決後記錄解決方案
## 📁 相關檔案
### 核心系統
- `drama` - 🎭 統一入口點腳本
- `ISSUES.md` - 主要問題追蹤檔案
- `reports/` - 結構化報告目錄
### 工具目錄
- `tools/issue.sh` - 互動式問題管理工具
- `tools/check_issues.sh` - 快速狀態檢查
- `tools/create_report.sh` - 快速建立報告工具
- `tools/check_reports.sh` - 報告狀態檢查
- `tools/setup_aliases.sh` - 全域命令設置
### VS Code 整合
- `.vscode/tasks.json` - VS Code 任務設定
- `.vscode/keybindings.json` - VS Code 快捷鍵
### 維護系統
- `scripts/maintenance_manager.sh` - 系統檢查主腳本
- `scripts/maintenance/` - 各種檢查腳本目錄
---
**🎉 現在您有了一個超級簡單好用的問題管理系統!**
遇到問題就記錄,定期檢查狀態,讓專案開發更順暢!

View File

@ -15,7 +15,7 @@ NC='\033[0m'
# 顯示主選單
show_menu() {
echo -e "${BLUE}🎭 Drama Ling 專案管理工具${NC}"
echo -e "${BLUE}🎭 Drama Ling 管理工具${NC}"
echo "=================================="
echo ""
echo -e "${PURPLE}📋 問題管理${NC}"
@ -27,6 +27,11 @@ show_menu() {
echo " decision - 建立決策記錄"
echo " reports - 檢查報告狀態"
echo ""
echo -e "${PURPLE}🚀 專案執行管理${NC}"
echo " project - 專案管理"
echo " phase - 階段管理"
echo " status - 查看執行狀態"
echo ""
echo -e "${PURPLE}🔧 系統檢查${NC}"
echo " consistency - 執行一致性檢查"
echo " compliance - 執行合規性檢查"
@ -37,9 +42,10 @@ show_menu() {
echo " help - 顯示此幫助"
echo ""
echo -e "${BLUE}範例:${NC}"
echo " ./drama issue # 管理問題"
echo " ./drama report \"API分析\" # 建立分析報告"
echo " ./drama check # 檢查問題狀態"
echo " ./dl issue # 管理問題"
echo " ./dl report \"API分析\" # 建立分析報告"
echo " ./dl project list # 列出所有專案"
echo " ./dl phase status # 查看階段狀態"
}
# 主邏輯
@ -61,6 +67,17 @@ case "$1" in
"reports")
exec "$TOOLS_DIR/check_reports.sh"
;;
"project")
shift
exec "$TOOLS_DIR/project.sh" "$@"
;;
"phase")
shift
exec "$TOOLS_DIR/phase.sh" "$@"
;;
"status")
exec "$TOOLS_DIR/project.sh" status
;;
"consistency")
exec "$SCRIPT_DIR/scripts/maintenance_manager.sh" consistency
;;

View File

@ -0,0 +1,57 @@
# {{PROJECT_NAME}}
**創建日期**: {{CREATION_DATE}}
**狀態**: 🔄 進行中
## 專案描述
請在此處添加專案的詳細描述...
## 專案目標
- [ ] 目標1待定義
- [ ] 目標2待定義
- [ ] 目標3待定義
## 執行階段
### 階段1: 待定義 ⏳
- ⏳ `ENV` 待添加執行項目...
- 預計完成時間:待定
- 負責人:待定
- 依賴關係:無
## 專案資源
### 參考文檔
- [ ] 需求規格文檔
- [ ] 技術架構文檔
- [ ] API規格文檔
### 相關連結
- 專案倉庫:待定
- 設計稿:待定
- 測試環境:待定
## 風險評估
### 潛在風險
1. **技術風險**:待評估
2. **時程風險**:待評估
3. **資源風險**:待評估
### 緩解措施
- 待制定具體緩解策略
## 專案統計
- **總階段數**: 1
- **執行項目數**: 1
- **完成進度**: 0%
- **預計完成時間**: 待定
## 變更紀錄
| 日期 | 變更內容 | 變更原因 |
|------|----------|----------|
| {{CREATION_DATE}} | 專案初始化 | 新專案建立 |
---
**最後更新**: {{CREATION_DATE}}
**維護人**: 待指定

View File

@ -0,0 +1,74 @@
# {{PROJECT_NAME}}
**創建日期**: {{CREATION_DATE}}
**專案類型**: 📱 移動端應用開發
**狀態**: 🔄 進行中
## 專案描述
移動端應用開發專案,包含完整的開發生命週期。
## 專案目標
- [ ] 建立完整的移動端開發環境
- [ ] 實現核心功能模組
- [ ] 完成應用打包和發布
- [ ] 建立測試和部署流程
## 執行階段
### 階段1: 環境配置 ⏳
- ⏳ `ENV` Android Studio 安裝和配置
- ⏳ `ENV` iOS開發環境設置 (如需要)
- ⏳ `ENV` 模擬器和真機調試環境
- ⏳ `MB` 專案配置文件調整
### 階段2: UI/UX開發 ⏳
- ⏳ `FE` 主要頁面UI實現
- ⏳ `FE` 響應式設計適配
- ⏳ `FE` 主題和樣式系統
- ⏳ `FE` 用戶交互優化
### 階段3: 核心功能開發 ⏳
- ⏳ `BE` API整合
- ⏳ `FE` 核心業務邏輯
- ⏳ `TEST` 功能模組測試
- ⏳ `DOC` 開發文檔更新
### 階段4: 打包發布 ⏳
- ⏳ `MB` 應用圖標和啟動畫面
- ⏳ `MB` 權限配置和安全設置
- ⏳ `MB` 正式版本打包
- ⏳ `TEST` 發布前完整測試
## 技術規格
### 開發工具
- IDE: Android Studio / VS Code
- 框架: Flutter / React Native / 原生開發
- 版本控制: Git
### 目標平台
- [ ] Android (API 21+)
- [ ] iOS (iOS 12+)
### 性能目標
- 啟動時間 < 3秒
- 頁面切換響應 < 500ms
- 記憶體使用 < 100MB
## 專案統計
- **總階段數**: 4
- **執行項目數**: 16
- **完成進度**: 0%
- **預計完成時間**: 待評估
## 品質檢查清單
- [ ] 代碼審查完成
- [ ] 單元測試覆蓋率 > 80%
- [ ] 整合測試通過
- [ ] 性能測試通過
- [ ] 安全性檢查完成
- [ ] 用戶體驗測試完成
---
**最後更新**: {{CREATION_DATE}}
**維護人**: 開發團隊

181
src/README.md Normal file
View File

@ -0,0 +1,181 @@
# Drama Ling - 專案架構
這是Drama Ling語言學習應用的主要專案資料夾包含前端和後端代碼。
## 專案結構
```
src/
├── backend/ # .NET Core Web API 後端
│ ├── DramaLing.API/ # Web API 專案
│ ├── DramaLing.Application/ # 應用服務層
│ ├── DramaLing.Core/ # 領域模型層
│ ├── DramaLing.Infrastructure/ # 基礎設施層
│ ├── DramaLing.Tests/ # 測試專案
│ └── DramaLing.sln # 解決方案檔
├── mobile/ # Flutter 移動應用
│ ├── lib/
│ │ ├── core/ # 核心功能 (常數、工具、服務)
│ │ ├── features/ # 功能模組 (認證、學習、對話等)
│ │ └── shared/ # 共用組件 (Widget、模型、Provider)
│ └── pubspec.yaml # Flutter 專案配置
└── docker-compose.yml # Docker 開發環境配置
```
## 技術棧
### 後端 (.NET Core)
- **.NET 8**: 最新的跨平台框架
- **ASP.NET Core Web API**: RESTful API 服務
- **Entity Framework Core**: ORM 資料庫存取
- **PostgreSQL**: 主要資料庫
- **Redis**: 快取和會話管理
- **JWT**: 身份驗證
- **Swagger/OpenAPI**: API 文檔
- **Serilog**: 結構化日誌
### 前端 (Flutter)
- **Flutter 3.16+**: 跨平台移動應用框架
- **Dart 3.0+**: 程式語言
- **Riverpod**: 狀態管理
- **Go Router**: 導航路由
- **Dio + Retrofit**: HTTP 客戶端
- **Hive**: 本地資料存儲
- **Material 3**: UI 設計系統
## 開發環境設置
### 必要工具
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
- [Flutter 3.16+](https://flutter.dev/docs/get-started/install)
- [Docker Desktop](https://www.docker.com/products/docker-desktop)
- [PostgreSQL](https://www.postgresql.org/download/) (或使用Docker)
### 快速開始
#### 1. 啟動開發環境
```bash
# 啟動資料庫和Redis
cd src
docker-compose up -d postgres redis
# 或者啟動完整環境包含API
docker-compose up -d
```
#### 2. 後端開發
```bash
cd src/backend
# 還原套件
dotnet restore
# 建立資料庫
dotnet ef database update --project DramaLing.Infrastructure --startup-project DramaLing.API
# 啟動API服務
dotnet run --project DramaLing.API
# API將在 http://localhost:5000 啟動
# Swagger UI: http://localhost:5000
```
#### 3. 前端開發
```bash
cd src/mobile
# 安裝套件
flutter pub get
# 程式碼生成 (Riverpod, Retrofit 等)
dart run build_runner build
# 啟動Flutter應用 (需要模擬器或實體裝置)
flutter run
```
## API 文檔
- **開發環境**: http://localhost:5000
- **Swagger UI**: http://localhost:5000 (開發模式下)
- **API 規格文檔**: `../../docs/04_technical/api/`
## 資料庫
### 連線資訊 (開發環境)
- **主機**: localhost
- **埠號**: 5432
- **資料庫**: dramaling_dev
- **使用者**: postgres
- **密碼**: password
### 遷移指令
```bash
# 建立遷移
dotnet ef migrations add <MigrationName> --project DramaLing.Infrastructure --startup-project DramaLing.API
# 更新資料庫
dotnet ef database update --project DramaLing.Infrastructure --startup-project DramaLing.API
# 移除最後一個遷移
dotnet ef migrations remove --project DramaLing.Infrastructure --startup-project DramaLing.API
```
## 測試
### 後端測試
```bash
cd src/backend
dotnet test
```
### 前端測試
```bash
cd src/mobile
flutter test
```
## 部署
### 生產環境建構
```bash
# 後端
cd src/backend
dotnet publish DramaLing.API -c Release -o publish
# 前端
cd src/mobile
flutter build apk --release # Android
flutter build ios --release # iOS
```
## 開發指南
### 架構原則
- **Clean Architecture**: 分層架構,依賴倒置
- **CQRS**: 命令查詢職責分離 (使用 MediatR)
- **Feature-based**: 按功能模組組織程式碼
- **API-first**: API 優先設計
### 編碼規範
- **後端**: 遵循 C# 編碼慣例
- **前端**: 遵循 Dart/Flutter 編碼慣例
- **命名**: 英文命名,中文註釋
- **測試**: 單元測試覆蓋率 > 80%
## 相關文檔
- [API 規格文檔](../../docs/04_technical/api/)
- [資料庫設計](../../docs/04_technical/database-schema.md)
- [技術選型決策](../../docs/04_technical/tech-stack-decision.md)
- [開發工作流程](../../docs/03_development/development-workflow.md)
## 問題回報
如有問題請參考:
- [ISSUES.md](../../ISSUES.md)
- [問題管理系統](../../README-問題管理.md)
## 授權
版權所有 © 2025 Drama Ling Team

View File

@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Mvc;
namespace DramaLing.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class HealthController : ControllerBase
{
/// <summary>
/// 健康檢查端點
/// </summary>
/// <returns>服務健康狀態</returns>
[HttpGet]
public IActionResult GetHealth()
{
var response = new
{
Status = "Healthy",
Timestamp = DateTime.UtcNow,
Version = "1.0.0",
Service = "Drama Ling API"
};
return Ok(response);
}
/// <summary>
/// 詳細健康檢查
/// </summary>
/// <returns>詳細的系統狀態</returns>
[HttpGet("detailed")]
public IActionResult GetDetailedHealth()
{
var response = new
{
Status = "Healthy",
Timestamp = DateTime.UtcNow,
Version = "1.0.0",
Service = "Drama Ling API",
Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production",
Uptime = Environment.TickCount64,
Database = "Connected", // TODO: 實際檢查資料庫連線
Cache = "Connected", // TODO: 實際檢查Redis連線
Memory = new
{
WorkingSet = GC.GetTotalMemory(false),
GcCollections = new
{
Gen0 = GC.CollectionCount(0),
Gen1 = GC.CollectionCount(1),
Gen2 = GC.CollectionCount(2)
}
}
};
return Ok(response);
}
}

View File

@ -0,0 +1,35 @@
# Development Dockerfile for .NET API
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 5000
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy project files
COPY ["DramaLing.API/DramaLing.API.csproj", "DramaLing.API/"]
COPY ["DramaLing.Application/DramaLing.Application.csproj", "DramaLing.Application/"]
COPY ["DramaLing.Core/DramaLing.Core.csproj", "DramaLing.Core/"]
COPY ["DramaLing.Infrastructure/DramaLing.Infrastructure.csproj", "DramaLing.Infrastructure/"]
# Restore dependencies
RUN dotnet restore "DramaLing.API/DramaLing.API.csproj"
# Copy source code
COPY . .
# Build the application
WORKDIR "/src/DramaLing.API"
RUN dotnet build "DramaLing.API.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "DramaLing.API.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
# Create logs directory
RUN mkdir -p /app/logs
ENTRYPOINT ["dotnet", "DramaLing.API.dll"]

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<DocumentationFile>bin\Debug\net8.0\DramaLing.API.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DramaLing.Application\DramaLing.Application.csproj" />
<ProjectReference Include="..\DramaLing.Infrastructure\DramaLing.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,116 @@
using DramaLing.Application;
using DramaLing.Infrastructure;
using Microsoft.OpenApi.Models;
using Serilog;
var builder = WebApplication.CreateBuilder(args);
// Configure Serilog
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.CreateLogger();
builder.Host.UseSerilog();
// Add services to the container.
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
// Configure Swagger/OpenAPI
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Drama Ling API",
Version = "v1",
Description = "API for Drama Ling language learning application",
Contact = new OpenApiContact
{
Name = "Drama Ling Team",
Email = "dev@dramaling.com"
}
});
// Configure JWT authentication in Swagger
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
// Include XML comments
var xmlFile = Path.Combine(AppContext.BaseDirectory, "DramaLing.API.xml");
if (File.Exists(xmlFile))
{
c.IncludeXmlComments(xmlFile);
}
});
// Configure CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("DramaLingPolicy", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Drama Ling API v1");
c.RoutePrefix = string.Empty; // Serve Swagger UI at the app's root
});
}
app.UseHttpsRedirection();
app.UseCors("DramaLingPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapGet("/health", () => new { Status = "Healthy", Timestamp = DateTime.UtcNow });
try
{
Log.Information("Starting Drama Ling API");
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}

View File

@ -0,0 +1,18 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
},
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=dramaling_dev;Username=postgres;Password=password"
},
"JwtSettings": {
"Key": "development-key-256-bits-long-for-jwt-signing-purpose-only",
"Issuer": "DramaLingAPI-Dev",
"Audience": "DramaLingUsers-Dev",
"DurationInMinutes": 1440
}
}

View File

@ -0,0 +1,53 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Serilog": {
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "logs/dramaling-.txt",
"rollingInterval": "Day",
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
}
}
]
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=dramaling_dev;Username=postgres;Password=password"
},
"JwtSettings": {
"Key": "your-256-bit-secret-key-here-must-be-at-least-32-characters",
"Issuer": "DramaLingAPI",
"Audience": "DramaLingUsers",
"DurationInMinutes": 60
},
"OpenAI": {
"ApiKey": "your-openai-api-key-here",
"Model": "gpt-4o-mini",
"MaxTokens": 1000,
"Temperature": 0.7
},
"Redis": {
"ConnectionString": "localhost:6379"
}
}

View File

@ -0,0 +1,17 @@
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace DramaLing.Application;
public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
services.AddAutoMapper(Assembly.GetExecutingAssembly());
return services;
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="FluentValidation" Version="11.8.0" />
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DramaLing.Core\DramaLing.Core.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,9 @@
namespace DramaLing.Core.Entities;
public abstract class BaseEntity
{
public Guid Id { get; set; } = Guid.NewGuid();
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public bool IsDeleted { get; set; } = false;
}

View File

@ -0,0 +1,94 @@
using DramaLing.Core.Enums;
namespace DramaLing.Core.Entities;
public class Achievement : BaseEntity
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string IconUrl { get; set; } = string.Empty;
public AchievementType Type { get; set; }
public int DiamondReward { get; set; } = 0;
public int ExperienceReward { get; set; } = 0;
public string? BadgeUrl { get; set; }
public bool IsActive { get; set; } = true;
// Navigation Properties
public virtual ICollection<UserAchievement> UserAchievements { get; set; } = new List<UserAchievement>();
}
public class UserAchievement : BaseEntity
{
public Guid UserId { get; set; }
public Guid AchievementId { get; set; }
public DateTime AchievedAt { get; set; } = DateTime.UtcNow;
public bool IsRewardClaimed { get; set; } = false;
public DateTime? RewardClaimedAt { get; set; }
// Navigation Properties
public virtual User User { get; set; } = null!;
public virtual Achievement Achievement { get; set; } = null!;
}
public class DailyMission : BaseEntity
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string IconUrl { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty; // vocabulary_recognition, dialogue_training, etc.
public int TargetValue { get; set; } = 1;
public string Unit { get; set; } = "次";
public int ExperienceReward { get; set; } = 50;
public int DiamondReward { get; set; } = 0;
public bool IsActive { get; set; } = true;
// Navigation Properties
public virtual ICollection<UserDailyMission> UserDailyMissions { get; set; } = new List<UserDailyMission>();
}
public class UserDailyMission : BaseEntity
{
public Guid UserId { get; set; }
public Guid DailyMissionId { get; set; }
public DateTime MissionDate { get; set; }
public int CurrentValue { get; set; } = 0;
public int TargetValue { get; set; } = 1;
public bool IsCompleted { get; set; } = false;
public DateTime? CompletedAt { get; set; }
public bool IsRewardClaimed { get; set; } = false;
public DateTime? RewardClaimedAt { get; set; }
// Navigation Properties
public virtual User User { get; set; } = null!;
public virtual DailyMission DailyMission { get; set; } = null!;
}
public class TimeWarpChallenge : BaseEntity
{
public Guid UserId { get; set; }
public Guid ScenarioId { get; set; }
public DateTime StartedAt { get; set; }
public DateTime? CompletedAt { get; set; }
public int TimeLimit { get; set; } = 300; // seconds
public int TimeUsed { get; set; } = 0; // seconds
public bool IsCompleted { get; set; } = false;
public int Score { get; set; } = 0;
public int ExperienceGained { get; set; } = 0;
public int DiamondGained { get; set; } = 0;
// Navigation Properties
public virtual User User { get; set; } = null!;
public virtual Scenario Scenario { get; set; } = null!;
}
public class UserLifePoint : BaseEntity
{
public Guid UserId { get; set; }
public int CurrentLifePoints { get; set; } = 5;
public int MaxLifePoints { get; set; } = 5;
public DateTime? NextRecoveryAt { get; set; }
public DateTime? LastUsedAt { get; set; }
// Navigation Properties
public virtual User User { get; set; } = null!;
}

View File

@ -0,0 +1,81 @@
namespace DramaLing.Core.Entities;
public class LearningStage : BaseEntity
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public int Order { get; set; }
public string Level { get; set; } = string.Empty; // A1, A2, B1, etc.
public bool IsActive { get; set; } = true;
// Navigation Properties
public virtual ICollection<Scenario> Scenarios { get; set; } = new List<Scenario>();
}
public class Scenario : BaseEntity
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Context { get; set; } = string.Empty;
public string Objective { get; set; } = string.Empty;
public string Level { get; set; } = string.Empty;
public int EstimatedMinutes { get; set; }
public bool IsActive { get; set; } = true;
// Foreign Key
public Guid LearningStageId { get; set; }
// Navigation Properties
public virtual LearningStage LearningStage { get; set; } = null!;
public virtual ICollection<Vocabulary> TargetVocabularies { get; set; } = new List<Vocabulary>();
public virtual ICollection<UserProgress> UserProgresses { get; set; } = new List<UserProgress>();
}
public class Vocabulary : BaseEntity
{
public string Word { get; set; } = string.Empty;
public string Translation { get; set; } = string.Empty;
public string Pronunciation { get; set; } = string.Empty;
public string Definition { get; set; } = string.Empty;
public string ExampleSentence { get; set; } = string.Empty;
public string Level { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public int Frequency { get; set; } = 0; // Word frequency ranking
// Navigation Properties
public virtual ICollection<Scenario> Scenarios { get; set; } = new List<Scenario>();
public virtual ICollection<VocabularyProgress> VocabularyProgresses { get; set; } = new List<VocabularyProgress>();
}
public class UserProgress : BaseEntity
{
public Guid UserId { get; set; }
public Guid ScenarioId { get; set; }
public int Score { get; set; }
public int Stars { get; set; } // 0-3 stars
public bool IsCompleted { get; set; }
public DateTime? CompletedAt { get; set; }
public int AttemptCount { get; set; } = 0;
public string? FeedbackData { get; set; } // JSON data
// Navigation Properties
public virtual User User { get; set; } = null!;
public virtual Scenario Scenario { get; set; } = null!;
}
public class VocabularyProgress : BaseEntity
{
public Guid UserId { get; set; }
public Guid VocabularyId { get; set; }
public int LearningStage { get; set; } = 1; // 1=Recognition, 2=Familiarity, 3=DialogueApplication
public int MasteryLevel { get; set; } = 0; // 0-100
public DateTime? NextReviewAt { get; set; }
public int ReviewCount { get; set; } = 0;
public int CorrectCount { get; set; } = 0;
public int IncorrectCount { get; set; } = 0;
public DateTime? LastReviewedAt { get; set; }
// Navigation Properties
public virtual User User { get; set; } = null!;
public virtual Vocabulary Vocabulary { get; set; } = null!;
}

View File

@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Identity;
namespace DramaLing.Core.Entities;
public class User : IdentityUser<Guid>
{
public string DisplayName { get; set; } = string.Empty;
public string? AvatarUrl { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? LastLoginAt { get; set; }
public bool IsDeleted { get; set; } = false;
// Language Learning Properties
public string CurrentLanguage { get; set; } = "en"; // ISO 639-1 code
public string NativeLanguage { get; set; } = "zh"; // ISO 639-1 code
public string CurrentLevel { get; set; } = "A1"; // CEFR level
public int TotalExperience { get; set; } = 0;
public int Diamonds { get; set; } = 0;
public int LightningEnergy { get; set; } = 0;
public int LifePoints { get; set; } = 5;
public int MaxLifePoints { get; set; } = 5;
public DateTime? NextLifePointRecovery { get; set; }
// Subscription
public bool IsVipUser { get; set; } = false;
public DateTime? VipExpiresAt { get; set; }
// Learning Statistics
public int ConsecutiveDays { get; set; } = 0;
public DateTime? LastLearningDate { get; set; }
public int TotalDialoguesCompleted { get; set; } = 0;
public int TotalVocabularyMastered { get; set; } = 0;
// Navigation Properties
public virtual ICollection<UserProgress> UserProgresses { get; set; } = new List<UserProgress>();
public virtual ICollection<UserAchievement> UserAchievements { get; set; } = new List<UserAchievement>();
public virtual ICollection<VocabularyProgress> VocabularyProgresses { get; set; } = new List<VocabularyProgress>();
}

View File

@ -0,0 +1,31 @@
namespace DramaLing.Core.Enums;
public enum LearningStage
{
Recognition = 1, // 詞彙認識
Familiarity = 2, // 詞彙熟悉
DialogueApplication = 3 // 對話應用
}
public enum DifficultyLevel
{
A1 = 1,
A2 = 2,
B1 = 3,
B2 = 4,
C1 = 5,
C2 = 6
}
public enum AchievementType
{
PassReward = 1, // 過關獎勵
PerfectGrammar = 2, // 完美語法
FluentExpression = 3, // 表達流利
StoryMaster = 4, // 劇情大師
VocabularyExpert = 5, // 詞彙專家
PerfectDialogue = 6, // 完美對話
SmartLearner = 7, // 智慧學習者
IndependentProgress = 8, // 獨立進步
TranslationMaster = 9 // 翻譯達人
}

View File

@ -0,0 +1,99 @@
using DramaLing.Core.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace DramaLing.Infrastructure.Data;
public class ApplicationDbContext : IdentityDbContext<User, IdentityRole<Guid>, Guid>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
// Learning Content
public DbSet<LearningStage> LearningStages { get; set; }
public DbSet<Scenario> Scenarios { get; set; }
public DbSet<Vocabulary> Vocabularies { get; set; }
// User Progress
public DbSet<UserProgress> UserProgresses { get; set; }
public DbSet<VocabularyProgress> VocabularyProgresses { get; set; }
// Gamification
public DbSet<Achievement> Achievements { get; set; }
public DbSet<UserAchievement> UserAchievements { get; set; }
public DbSet<DailyMission> DailyMissions { get; set; }
public DbSet<UserDailyMission> UserDailyMissions { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Configure Identity tables to use Guid
builder.Entity<User>(entity =>
{
entity.ToTable("Users");
entity.HasKey(e => e.Id);
// Configure indexes
entity.HasIndex(e => e.Email).IsUnique();
entity.HasIndex(e => e.UserName).IsUnique();
// Configure properties
entity.Property(e => e.DisplayName).HasMaxLength(100).IsRequired();
entity.Property(e => e.CurrentLanguage).HasMaxLength(5).IsRequired();
entity.Property(e => e.NativeLanguage).HasMaxLength(5).IsRequired();
entity.Property(e => e.CurrentLevel).HasMaxLength(5).IsRequired();
});
builder.Entity<IdentityRole<Guid>>(entity =>
{
entity.ToTable("Roles");
});
builder.Entity<IdentityUserRole<Guid>>(entity =>
{
entity.ToTable("UserRoles");
});
builder.Entity<IdentityUserClaim<Guid>>(entity =>
{
entity.ToTable("UserClaims");
});
builder.Entity<IdentityUserLogin<Guid>>(entity =>
{
entity.ToTable("UserLogins");
});
builder.Entity<IdentityRoleClaim<Guid>>(entity =>
{
entity.ToTable("RoleClaims");
});
builder.Entity<IdentityUserToken<Guid>>(entity =>
{
entity.ToTable("UserTokens");
});
// Configure soft delete
builder.Entity<User>()
.HasQueryFilter(e => !e.IsDeleted);
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
foreach (var entry in ChangeTracker.Entries<BaseEntity>())
{
switch (entry.State)
{
case EntityState.Modified:
entry.Entity.UpdatedAt = DateTime.UtcNow;
break;
}
}
return await base.SaveChangesAsync(cancellationToken);
}
}

View File

@ -0,0 +1,81 @@
using DramaLing.Core.Entities;
using DramaLing.Infrastructure.Data;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using StackExchange.Redis;
using System.Text;
namespace DramaLing.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
// Database
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString("DefaultConnection")));
// Identity
services.AddIdentity<User, IdentityRole<Guid>>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// User settings
options.User.RequireUniqueEmail = true;
options.SignIn.RequireConfirmedEmail = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// JWT Authentication
var jwtSettings = configuration.GetSection("JwtSettings");
var key = Encoding.ASCII.GetBytes(jwtSettings["Key"] ?? throw new InvalidOperationException("JWT Key not found"));
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = jwtSettings["Issuer"],
ValidateAudience = true,
ValidAudience = jwtSettings["Audience"],
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
// Redis
var redisConnection = configuration.GetConnectionString("Redis");
if (!string.IsNullOrEmpty(redisConnection))
{
services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect(redisConnection));
}
return services;
}
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="StackExchange.Redis" Version="2.6.122" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DramaLing.Core\DramaLing.Core.csproj" />
<ProjectReference Include="..\DramaLing.Application\DramaLing.Application.csproj" />
</ItemGroup>
</Project>

48
src/backend/DramaLing.sln Normal file
View File

@ -0,0 +1,48 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DramaLing.API", "DramaLing.API\DramaLing.API.csproj", "{8A7E8B45-1234-4567-8901-234567890123}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DramaLing.Core", "DramaLing.Core\DramaLing.Core.csproj", "{8A7E8B45-1234-4567-8901-234567890124}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DramaLing.Infrastructure", "DramaLing.Infrastructure\DramaLing.Infrastructure.csproj", "{8A7E8B45-1234-4567-8901-234567890125}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DramaLing.Application", "DramaLing.Application\DramaLing.Application.csproj", "{8A7E8B45-1234-4567-8901-234567890126}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DramaLing.Tests", "DramaLing.Tests\DramaLing.Tests.csproj", "{8A7E8B45-1234-4567-8901-234567890127}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8A7E8B45-1234-4567-8901-234567890123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A7E8B45-1234-4567-8901-234567890123}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A7E8B45-1234-4567-8901-234567890123}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A7E8B45-1234-4567-8901-234567890123}.Release|Any CPU.Build.0 = Release|Any CPU
{8A7E8B45-1234-4567-8901-234567890124}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A7E8B45-1234-4567-8901-234567890124}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A7E8B45-1234-4567-8901-234567890124}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A7E8B45-1234-4567-8901-234567890124}.Release|Any CPU.Build.0 = Release|Any CPU
{8A7E8B45-1234-4567-8901-234567890125}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A7E8B45-1234-4567-8901-234567890125}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A7E8B45-1234-4567-8901-234567890125}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A7E8B45-1234-4567-8901-234567890125}.Release|Any CPU.Build.0 = Release|Any CPU
{8A7E8B45-1234-4567-8901-234567890126}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A7E8B45-1234-4567-8901-234567890126}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A7E8B45-1234-4567-8901-234567890126}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A7E8B45-1234-4567-8901-234567890126}.Release|Any CPU.Build.0 = Release|Any CPU
{8A7E8B45-1234-4567-8901-234567890127}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A7E8B45-1234-4567-8901-234567890127}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A7E8B45-1234-4567-8901-234567890127}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A7E8B45-1234-4567-8901-234567890127}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {12345678-1234-5678-9012-123456789012}
EndGlobalSection
EndGlobal

60
src/docker-compose.yml Normal file
View File

@ -0,0 +1,60 @@
version: '3.8'
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: dramaling_postgres
environment:
POSTGRES_DB: dramaling_dev
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./backend/scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- dramaling_network
# Redis Cache
redis:
image: redis:7-alpine
container_name: dramaling_redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
networks:
- dramaling_network
# .NET API (Development)
api:
build:
context: ./backend
dockerfile: DramaLing.API/Dockerfile.dev
container_name: dramaling_api
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://+:5000
- ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=dramaling_dev;Username=postgres;Password=password
- ConnectionStrings__Redis=redis:6379
ports:
- "5000:5000"
depends_on:
- postgres
- redis
volumes:
- ./backend:/app
networks:
- dramaling_network
restart: unless-stopped
volumes:
postgres_data:
redis_data:
networks:
dramaling_network:
driver: bridge

45
src/mobile/.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
src/mobile/.metadata Normal file
View File

@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "a402d9a4376add5bc2d6b1e33e53edaae58c07f8"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
- platform: web
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

16
src/mobile/README.md Normal file
View File

@ -0,0 +1,16 @@
# dramaling
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@ -0,0 +1,83 @@
class AppConstants {
// App Info
static const String appName = 'Drama Ling';
static const String appVersion = '1.0.0';
// API Configuration
static const String baseUrl = 'https://api.dramaling.com';
static const String apiVersion = 'v1';
static const String apiBaseUrl = '$baseUrl/api/$apiVersion';
// Local API for Development
static const String localBaseUrl = 'http://localhost:5000';
static const String localApiBaseUrl = '$localBaseUrl/api/$apiVersion';
// Storage Keys
static const String accessTokenKey = 'access_token';
static const String refreshTokenKey = 'refresh_token';
static const String userDataKey = 'user_data';
static const String languageKey = 'language';
static const String themeKey = 'theme';
// Learning Constants
static const int maxLifePoints = 5;
static const int lifePointRecoveryHours = 5;
static const int dailyExperienceBonus = 50;
// Dialogue Constants
static const int maxDialogueTurns = 12;
static const int dialogueTimeoutSeconds = 600; // 10 minutes
static const double passingScore = 60.0;
static const double excellentScore = 90.0;
// Time Challenge Constants
static const int timeChallengeSeconds = 300; // 5 minutes
static const int timeWarningSeconds = 60;
static const int timeCriticalSeconds = 30;
// Animation Durations
static const Duration shortAnimation = Duration(milliseconds: 200);
static const Duration normalAnimation = Duration(milliseconds: 300);
static const Duration longAnimation = Duration(milliseconds: 500);
// Network
static const Duration connectionTimeout = Duration(seconds: 30);
static const Duration receiveTimeout = Duration(seconds: 30);
// Pagination
static const int defaultPageSize = 20;
static const int maxPageSize = 100;
}
class AppStrings {
// Common
static const String ok = '確定';
static const String cancel = '取消';
static const String retry = '重試';
static const String loading = '載入中...';
static const String error = '錯誤';
static const String success = '成功';
// Authentication
static const String login = '登入';
static const String register = '註冊';
static const String logout = '登出';
static const String email = '電子郵件';
static const String password = '密碼';
static const String confirmPassword = '確認密碼';
static const String forgotPassword = '忘記密碼?';
// Learning
static const String startLearning = '開始學習';
static const String continueDialogue = '繼續對話';
static const String vocabularyPractice = '詞彙練習';
static const String dialoguePractice = '對話練習';
static const String timeChallenge = '限時挑戰';
// Errors
static const String networkError = '網路連線錯誤';
static const String serverError = '伺服器錯誤';
static const String unknownError = '未知錯誤';
static const String invalidCredentials = '帳號或密碼錯誤';
static const String sessionExpired = '登入已過期,請重新登入';
}

View File

@ -0,0 +1,125 @@
import 'package:hive_flutter/hive_flutter.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class StorageService {
static late Box _box;
static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
///
static Future<void> init() async {
await Hive.initFlutter();
_box = await Hive.openBox('dramaling_storage');
}
// (使Hive)
///
static Future<void> setData<T>(String key, T value) async {
await _box.put(key, value);
}
///
static T? getData<T>(String key) {
return _box.get(key) as T?;
}
///
static Future<void> removeData(String key) async {
await _box.delete(key);
}
///
static Future<void> clearAll() async {
await _box.clear();
}
///
static bool hasData(String key) {
return _box.containsKey(key);
}
// (使FlutterSecureStorage)
/// (token)
static Future<void> setSecureData(String key, String value) async {
await _secureStorage.write(key: key, value: value);
}
///
static Future<String?> getSecureData(String key) async {
return await _secureStorage.read(key: key);
}
///
static Future<void> removeSecureData(String key) async {
await _secureStorage.delete(key: key);
}
///
static Future<void> clearSecureData() async {
await _secureStorage.deleteAll();
}
///
static Future<bool> hasSecureData(String key) async {
final data = await _secureStorage.read(key: key);
return data != null;
}
// 便
/// Token
static Future<void> saveTokens({
required String accessToken,
required String refreshToken,
}) async {
await setSecureData('access_token', accessToken);
await setSecureData('refresh_token', refreshToken);
}
/// Token
static Future<String?> getAccessToken() async {
return await getSecureData('access_token');
}
/// Token
static Future<String?> getRefreshToken() async {
return await getSecureData('refresh_token');
}
/// Token
static Future<void> clearTokens() async {
await removeSecureData('access_token');
await removeSecureData('refresh_token');
}
///
static Future<void> saveUserPreferences({
String? language,
String? theme,
bool? soundEnabled,
bool? vibrationEnabled,
}) async {
if (language != null) await setData('language', language);
if (theme != null) await setData('theme', theme);
if (soundEnabled != null) await setData('sound_enabled', soundEnabled);
if (vibrationEnabled != null) await setData('vibration_enabled', vibrationEnabled);
}
///
static Map<String, dynamic> getUserPreferences() {
return {
'language': getData<String>('language') ?? 'zh',
'theme': getData<String>('theme') ?? 'system',
'sound_enabled': getData<bool>('sound_enabled') ?? true,
'vibration_enabled': getData<bool>('vibration_enabled') ?? true,
};
}
}

View File

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../features/auth/screens/login_screen.dart';
import '../../features/auth/screens/register_screen.dart';
import '../../features/learning/screens/home_screen.dart';
import '../../shared/providers/auth_provider.dart';
final routerProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authProvider);
return GoRouter(
initialLocation: authState.isAuthenticated ? '/home' : '/login',
redirect: (context, state) {
final isAuthenticated = authState.isAuthenticated;
final isAuthRoute = state.uri.path.startsWith('/auth');
if (!isAuthenticated && !isAuthRoute) {
return '/login';
}
if (isAuthenticated && isAuthRoute) {
return '/home';
}
return null;
},
routes: [
// Authentication Routes
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/register',
builder: (context, state) => const RegisterScreen(),
),
// Main App Routes
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
// Learning Routes
GoRoute(
path: '/vocabulary',
builder: (context, state) => const Scaffold(
body: Center(child: Text('詞彙練習頁面')),
),
),
GoRoute(
path: '/dialogue',
builder: (context, state) => const Scaffold(
body: Center(child: Text('對話練習頁面')),
),
),
GoRoute(
path: '/challenge',
builder: (context, state) => const Scaffold(
body: Center(child: Text('限時挑戰頁面')),
),
),
// Profile Routes
GoRoute(
path: '/profile',
builder: (context, state) => const Scaffold(
body: Center(child: Text('個人檔案頁面')),
),
),
],
// Error handling
errorBuilder: (context, state) => Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
Text(
'頁面未找到',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(
state.error?.toString() ?? '未知錯誤',
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => context.go('/home'),
child: const Text('返回首頁'),
),
],
),
),
),
);
});

View File

@ -0,0 +1,184 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppColors {
// Primary Colors
static const Color primary = Color(0xFF6366F1); // Indigo
static const Color primaryLight = Color(0xFF8B5CF6); // Violet
static const Color primaryDark = Color(0xFF4F46E5); // Dark Indigo
// Secondary Colors
static const Color secondary = Color(0xFFF59E0B); // Amber
static const Color secondaryLight = Color(0xFFFBBF24);
static const Color secondaryDark = Color(0xFFD97706);
// Learning Status Colors
static const Color success = Color(0xFF10B981); // Emerald
static const Color warning = Color(0xFFF59E0B); // Amber
static const Color error = Color(0xFFEF4444); // Red
static const Color info = Color(0xFF3B82F6); // Blue
// Neutral Colors
static const Color white = Color(0xFFFFFFFF);
static const Color black = Color(0xFF000000);
static const Color grey50 = Color(0xFFF9FAFB);
static const Color grey100 = Color(0xFFF3F4F6);
static const Color grey200 = Color(0xFFE5E7EB);
static const Color grey300 = Color(0xFFD1D5DB);
static const Color grey400 = Color(0xFF9CA3AF);
static const Color grey500 = Color(0xFF6B7280);
static const Color grey600 = Color(0xFF4B5563);
static const Color grey700 = Color(0xFF374151);
static const Color grey800 = Color(0xFF1F2937);
static const Color grey900 = Color(0xFF111827);
// Surface Colors
static const Color surface = Color(0xFFFFFFFF);
static const Color surfaceDark = Color(0xFF1F2937);
static const Color background = Color(0xFFF9FAFB);
static const Color backgroundDark = Color(0xFF111827);
}
class AppTheme {
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
// Color Scheme
colorScheme: const ColorScheme.light(
primary: AppColors.primary,
secondary: AppColors.secondary,
surface: AppColors.surface,
background: AppColors.background,
error: AppColors.error,
),
// Typography
textTheme: GoogleFonts.notoSansTextTheme().copyWith(
headlineLarge: GoogleFonts.notoSans(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppColors.grey900,
),
headlineMedium: GoogleFonts.notoSans(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppColors.grey900,
),
titleLarge: GoogleFonts.notoSans(
fontSize: 22,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
titleMedium: GoogleFonts.notoSans(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey700,
),
bodyLarge: GoogleFonts.notoSans(
fontSize: 16,
fontWeight: FontWeight.normal,
color: AppColors.grey800,
),
bodyMedium: GoogleFonts.notoSans(
fontSize: 14,
fontWeight: FontWeight.normal,
color: AppColors.grey700,
),
),
// Component Themes
appBarTheme: AppBarTheme(
backgroundColor: AppColors.white,
foregroundColor: AppColors.grey900,
elevation: 0,
centerTitle: true,
titleTextStyle: GoogleFonts.notoSans(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.white,
elevation: 2,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.grey300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.primary, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
cardTheme: const CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
),
);
}
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
// Color Scheme
colorScheme: const ColorScheme.dark(
primary: AppColors.primaryLight,
secondary: AppColors.secondaryLight,
surface: AppColors.surfaceDark,
background: AppColors.backgroundDark,
error: AppColors.error,
),
// Typography
textTheme: GoogleFonts.notoSansTextTheme(ThemeData.dark().textTheme).copyWith(
headlineLarge: GoogleFonts.notoSans(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppColors.white,
),
headlineMedium: GoogleFonts.notoSans(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppColors.white,
),
titleLarge: GoogleFonts.notoSans(
fontSize: 22,
fontWeight: FontWeight.w600,
color: AppColors.white,
),
bodyLarge: GoogleFonts.notoSans(
fontSize: 16,
fontWeight: FontWeight.normal,
color: AppColors.grey200,
),
),
// Component Themes
appBarTheme: AppBarTheme(
backgroundColor: AppColors.surfaceDark,
foregroundColor: AppColors.white,
elevation: 0,
centerTitle: true,
),
);
}
}

View File

@ -0,0 +1,226 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
bool _obscurePassword = true;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 60),
// Logo and Title
Column(
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
child: const Icon(
Icons.theater_comedy,
color: Colors.white,
size: 60,
),
),
const SizedBox(height: 24),
Text(
'Drama Ling',
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'歡迎回來!開始您的語言學習之旅',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onBackground.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
],
),
const SizedBox(height: 48),
// Email Field
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: '電子郵件',
hintText: '請輸入您的電子郵件',
prefixIcon: Icon(Icons.email_outlined),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '請輸入電子郵件';
}
if (!value.contains('@')) {
return '請輸入有效的電子郵件格式';
}
return null;
},
),
const SizedBox(height: 16),
// Password Field
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: '密碼',
hintText: '請輸入您的密碼',
prefixIcon: const Icon(Icons.lock_outlined),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '請輸入密碼';
}
if (value.length < 6) {
return '密碼長度至少需要6個字符';
}
return null;
},
),
const SizedBox(height: 24),
// Login Button
SizedBox(
height: 48,
child: ElevatedButton(
onPressed: _isLoading ? null : _handleLogin,
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('登入'),
),
),
const SizedBox(height: 16),
// Forgot Password
TextButton(
onPressed: () {
// TODO:
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('忘記密碼功能開發中')),
);
},
child: const Text('忘記密碼?'),
),
const SizedBox(height: 32),
// Divider
Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5),
),
),
),
const Expanded(child: Divider()),
],
),
const SizedBox(height: 32),
// Register Button
OutlinedButton(
onPressed: () => context.go('/register'),
child: const Text('建立新帳號'),
),
],
),
),
),
),
);
}
Future<void> _handleLogin() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoading = true;
});
try {
// TODO:
await Future.delayed(const Duration(seconds: 2)); // API調用
if (mounted) {
//
context.go('/home');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('登入成功!')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('登入失敗:$e')),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}

View File

@ -0,0 +1,311 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _displayNameController = TextEditingController();
bool _isLoading = false;
bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
bool _acceptTerms = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_displayNameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/login'),
),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Title
Text(
'建立新帳號',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'開始您的語言學習之旅',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onBackground.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Display Name Field
TextFormField(
controller: _displayNameController,
decoration: const InputDecoration(
labelText: '顯示名稱',
hintText: '請輸入您的顯示名稱',
prefixIcon: Icon(Icons.person_outlined),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '請輸入顯示名稱';
}
if (value.length < 2) {
return '顯示名稱至少需要2個字符';
}
return null;
},
),
const SizedBox(height: 16),
// Email Field
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: '電子郵件',
hintText: '請輸入您的電子郵件',
prefixIcon: Icon(Icons.email_outlined),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '請輸入電子郵件';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return '請輸入有效的電子郵件格式';
}
return null;
},
),
const SizedBox(height: 16),
// Password Field
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: '密碼',
hintText: '請輸入密碼(至少8個字符)',
prefixIcon: const Icon(Icons.lock_outlined),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '請輸入密碼';
}
if (value.length < 8) {
return '密碼長度至少需要8個字符';
}
if (!RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)').hasMatch(value)) {
return '密碼需包含大寫字母、小寫字母和數字';
}
return null;
},
),
const SizedBox(height: 16),
// Confirm Password Field
TextFormField(
controller: _confirmPasswordController,
obscureText: _obscureConfirmPassword,
decoration: InputDecoration(
labelText: '確認密碼',
hintText: '請再次輸入密碼',
prefixIcon: const Icon(Icons.lock_outlined),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirmPassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
),
onPressed: () {
setState(() {
_obscureConfirmPassword = !_obscureConfirmPassword;
});
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '請確認密碼';
}
if (value != _passwordController.text) {
return '密碼不一致';
}
return null;
},
),
const SizedBox(height: 24),
// Terms and Conditions
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Checkbox(
value: _acceptTerms,
onChanged: (value) {
setState(() {
_acceptTerms = value ?? false;
});
},
),
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_acceptTerms = !_acceptTerms;
});
},
child: Padding(
padding: const EdgeInsets.only(top: 12),
child: RichText(
text: TextSpan(
style: Theme.of(context).textTheme.bodyMedium,
children: [
const TextSpan(text: '我同意'),
TextSpan(
text: '服務條款',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
),
const TextSpan(text: ''),
TextSpan(
text: '隱私政策',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
),
],
),
),
),
),
),
],
),
const SizedBox(height: 24),
// Register Button
SizedBox(
height: 48,
child: ElevatedButton(
onPressed: (_isLoading || !_acceptTerms) ? null : _handleRegister,
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('註冊'),
),
),
const SizedBox(height: 16),
// Back to Login
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('已經有帳號了?'),
TextButton(
onPressed: () => context.go('/login'),
child: const Text('立即登入'),
),
],
),
],
),
),
),
),
);
}
Future<void> _handleRegister() async {
if (!_formKey.currentState!.validate()) return;
if (!_acceptTerms) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('請同意服務條款和隱私政策')),
);
return;
}
setState(() {
_isLoading = true;
});
try {
// TODO:
await Future.delayed(const Duration(seconds: 2)); // API調用
if (mounted) {
//
context.go('/home');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('註冊成功歡迎使用Drama Ling')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('註冊失敗:$e')),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}

View File

@ -0,0 +1,285 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../shared/providers/auth_provider.dart';
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Drama Ling'),
actions: [
IconButton(
icon: const Icon(Icons.person),
onPressed: () => context.go('/profile'),
),
],
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Welcome Section
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.primary.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'歡迎回來,${authState.displayName ?? '學習者'}',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'繼續您的語言學習之旅',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.white.withOpacity(0.9),
),
),
const SizedBox(height: 16),
Row(
children: [
_buildStatCard('今日學習', '25分鐘', Icons.timer, context),
const SizedBox(width: 16),
_buildStatCard('連續天數', '7天', Icons.local_fire_department, context),
],
),
],
),
),
const SizedBox(height: 32),
// Learning Options
Text(
'選擇學習方式',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// Learning Cards
_buildLearningCard(
context,
'詞彙練習',
'學習新詞彙,擴充詞彙量',
Icons.book_outlined,
Colors.blue,
() => context.go('/vocabulary'),
),
const SizedBox(height: 16),
_buildLearningCard(
context,
'對話練習',
'AI互動對話提升口說能力',
Icons.chat_bubble_outline,
Colors.green,
() => context.go('/dialogue'),
),
const SizedBox(height: 16),
_buildLearningCard(
context,
'限時挑戰',
'測試反應速度和語言技能',
Icons.timer_outlined,
Colors.orange,
() => context.go('/challenge'),
),
const SizedBox(height: 32),
// Progress Section
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'學習進度',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_buildProgressItem('詞彙掌握', 0.7, '70%', context),
const SizedBox(height: 12),
_buildProgressItem('對話流暢度', 0.5, '50%', context),
const SizedBox(height: 12),
_buildProgressItem('聽力理解', 0.8, '80%', context),
],
),
),
],
),
),
),
);
}
Widget _buildStatCard(String title, String value, IconData icon, BuildContext context) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Icon(icon, color: Colors.white, size: 24),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
title,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
),
),
],
),
),
);
}
Widget _buildLearningCard(
BuildContext context,
String title,
String subtitle,
IconData icon,
Color color,
VoidCallback onTap,
) {
return Card(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: 28,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
size: 16,
),
],
),
),
),
);
}
Widget _buildProgressItem(String title, double progress, String percentage, BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
Text(
percentage,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: progress,
backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.1),
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
],
);
}
}

57
src/mobile/lib/main.dart Normal file
View File

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'core/constants/app_constants.dart';
import 'core/services/storage_service.dart';
import 'core/utils/app_router.dart';
import 'core/utils/app_theme.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Hive
await Hive.initFlutter();
// Initialize Storage Service
await StorageService.init();
runApp(
ProviderScope(
child: const DramaLingApp(),
),
);
}
class DramaLingApp extends ConsumerWidget {
const DramaLingApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
return ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
splitScreenMode: true,
builder: (context, child) {
return MaterialApp.router(
title: AppConstants.appName,
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
routerConfig: router,
builder: (context, widget) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: widget!,
);
},
);
},
);
}
}

View File

@ -0,0 +1,91 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class AuthState {
final bool isAuthenticated;
final String? userId;
final String? email;
final String? displayName;
final bool isLoading;
const AuthState({
required this.isAuthenticated,
this.userId,
this.email,
this.displayName,
this.isLoading = false,
});
AuthState copyWith({
bool? isAuthenticated,
String? userId,
String? email,
String? displayName,
bool? isLoading,
}) {
return AuthState(
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
userId: userId ?? this.userId,
email: email ?? this.email,
displayName: displayName ?? this.displayName,
isLoading: isLoading ?? this.isLoading,
);
}
}
class AuthNotifier extends StateNotifier<AuthState> {
AuthNotifier() : super(const AuthState(isAuthenticated: false));
Future<void> login(String email, String password) async {
state = state.copyWith(isLoading: true);
try {
// TODO:
await Future.delayed(const Duration(seconds: 1));
state = state.copyWith(
isAuthenticated: true,
userId: 'user_123',
email: email,
displayName: email.split('@').first,
isLoading: false,
);
} catch (e) {
state = state.copyWith(isLoading: false);
rethrow;
}
}
Future<void> register(String email, String password, String displayName) async {
state = state.copyWith(isLoading: true);
try {
// TODO:
await Future.delayed(const Duration(seconds: 1));
state = state.copyWith(
isAuthenticated: true,
userId: 'user_${DateTime.now().millisecondsSinceEpoch}',
email: email,
displayName: displayName,
isLoading: false,
);
} catch (e) {
state = state.copyWith(isLoading: false);
rethrow;
}
}
Future<void> logout() async {
state = const AuthState(isAuthenticated: false);
}
void checkAuthStatus() {
// TODO:
//
}
}
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier();
});

1202
src/mobile/pubspec.lock Normal file

File diff suppressed because it is too large Load Diff

85
src/mobile/pubspec.yaml Normal file
View File

@ -0,0 +1,85 @@
name: dramaling
description: "Drama Ling - AI-powered language learning app with immersive dialogue practice"
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: ">=3.16.0"
dependencies:
flutter:
sdk: flutter
# State Management
flutter_riverpod: ^2.4.9
riverpod_annotation: ^2.3.3
# Navigation
go_router: ^12.1.3
# Network
dio: ^5.3.3
retrofit: ^4.0.3
json_annotation: ^4.8.1
# Local Storage
hive: ^2.2.3
hive_flutter: ^1.1.0
shared_preferences: ^2.2.2
# UI Components
flutter_screenutil: ^5.9.0
cached_network_image: ^3.3.0
shimmer: ^3.0.0
lottie: ^2.7.0
# Audio
just_audio: ^0.9.35
audioplayers: ^5.2.1
# Authentication & Security
flutter_secure_storage: ^9.0.0
crypto: ^3.0.3
# Utils
intl: ^0.18.1
equatable: ^2.0.5
freezed_annotation: ^2.4.1
# Icons & Fonts
cupertino_icons: ^1.0.6
google_fonts: ^6.1.0
dev_dependencies:
flutter_test:
sdk: flutter
# Code Generation
build_runner: ^2.4.7
retrofit_generator: ^8.0.4
riverpod_generator: ^2.3.9
json_serializable: ^6.7.1
hive_generator: ^2.0.1
freezed: ^2.4.6
# Linting
flutter_lints: ^3.0.1
very_good_analysis: ^5.1.0
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/animations/
- assets/audio/
- assets/icons/
# fonts:
# - family: NotoSans
# fonts:
# - asset: assets/fonts/NotoSans-Regular.ttf
# - asset: assets/fonts/NotoSans-Bold.ttf
# weight: 700

View File

@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:dramaling/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

BIN
src/mobile/web/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

38
src/mobile/web/index.html Normal file
View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="dramaling">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>dramaling</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

View File

@ -0,0 +1,35 @@
{
"name": "dramaling",
"short_name": "dramaling",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}

270
tools/phase.sh Executable file
View File

@ -0,0 +1,270 @@
#!/bin/bash
# 階段管理工具
# 使用方法: ./drama phase [命令]
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
PROJECTS_FILE="$PROJECT_DIR/PROJECTS.md"
PROJECTS_DATA_DIR="$PROJECT_DIR/projects"
# 顏色定義
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
RED='\033[0;31m'
NC='\033[0m'
# 類型圖標函數 (Bash 3.2 兼容)
get_type_icon() {
case "$1" in
"FE") echo "🎨" ;;
"BE") echo "⚙️" ;;
"AI") echo "🤖" ;;
"MB") echo "📱" ;;
"DOC") echo "📚" ;;
"ENV") echo "🔧" ;;
"TEST") echo "🧪" ;;
*) echo "🔧" ;;
esac
}
# 狀態圖標函數
get_status_icon() {
case "$1" in
"pending") echo "⏳" ;;
"in-progress") echo "🔄" ;;
"completed") echo "✅" ;;
"blocked") echo "❌" ;;
"paused") echo "⏸️" ;;
*) echo "⏳" ;;
esac
}
# 確保必要文件和目錄存在
init_phase_structure() {
if [[ ! -f "$PROJECTS_FILE" ]]; then
echo -e "${RED}❌ PROJECTS.md 不存在${NC}"
exit 1
fi
if [[ ! -d "$PROJECTS_DATA_DIR" ]]; then
mkdir -p "$PROJECTS_DATA_DIR"
fi
}
# 列出所有階段
list_phases() {
local project_name="$1"
if [[ -z "$project_name" ]]; then
echo -e "${BLUE}📋 所有專案的階段列表${NC}"
echo "=================================="
# 從PROJECTS.md中提取階段信息
grep -E "^#### 階段[0-9]+:" "$PROJECTS_FILE" | while read -r line; do
phase_info=$(echo "$line" | sed 's/^#### //')
echo -e " ${GREEN}📊 $phase_info${NC}"
done
else
echo -e "${BLUE}📋 專案 '$project_name' 的階段列表${NC}"
echo "=================================="
local project_file="$PROJECTS_DATA_DIR/${project_name}.md"
if [[ -f "$project_file" ]]; then
grep -E "^### 階段[0-9]+:" "$project_file" | while read -r line; do
phase_info=$(echo "$line" | sed 's/^### //')
echo -e " ${GREEN}📊 $phase_info${NC}"
done
else
echo -e "${RED}❌ 專案 '$project_name' 不存在${NC}"
fi
fi
}
# 新增階段
add_phase() {
local project_name="$1"
local phase_name="$2"
if [[ -z "$project_name" || -z "$phase_name" ]]; then
echo -e "${RED}❌ 請提供專案名稱和階段名稱${NC}"
echo "使用方法: ./dl phase add \"專案名稱\" \"階段名稱\""
return 1
fi
local project_file="$PROJECTS_DATA_DIR/${project_name}.md"
if [[ ! -f "$project_file" ]]; then
echo -e "${RED}❌ 專案 '$project_name' 不存在${NC}"
echo "使用 ./dl project init \"$project_name\" 先建立專案"
return 1
fi
echo -e "${BLUE} 為專案 '$project_name' 新增階段: $phase_name${NC}"
# 計算新階段編號
local phase_count=$(grep -c "^### 階段[0-9]*:" "$project_file")
local new_phase_num=$((phase_count + 1))
# 在項目文件中新增階段
cat >> "$project_file" << EOF
### 階段$new_phase_num: $phase_name ⏳
- ⏳ \`ENV\` 請使用 ./drama phase items 新增執行項目...
EOF
echo -e "${GREEN}✅ 階段 '$phase_name' 新增完成${NC}"
echo " 💡 使用 ./dl phase items \"$project_name\" \"$phase_name\" 管理執行項目"
}
# 管理階段執行項目
manage_phase_items() {
local project_name="$1"
local phase_name="$2"
local item_type="$3"
local item_desc="$4"
if [[ -z "$project_name" || -z "$phase_name" ]]; then
echo -e "${RED}❌ 請提供專案名稱和階段名稱${NC}"
echo "使用方法: ./dl phase items \"專案名稱\" \"階段名稱\" [類型] [描述]"
return 1
fi
local project_file="$PROJECTS_DATA_DIR/${project_name}.md"
if [[ ! -f "$project_file" ]]; then
echo -e "${RED}❌ 專案 '$project_name' 不存在${NC}"
return 1
fi
if [[ -n "$item_type" && -n "$item_desc" ]]; then
# 新增執行項目
local type_icon=$(get_type_icon "$item_type")
echo -e "${BLUE} 新增執行項目到階段 '$phase_name'${NC}"
echo " ${type_icon} [$item_type] $item_desc"
# TODO: 實作新增項目到特定階段的邏輯
echo -e "${YELLOW}⚠️ 此功能開發中...${NC}"
else
# 顯示階段的執行項目
echo -e "${BLUE}📋 階段 '$phase_name' 的執行項目${NC}"
echo "=================================="
# 尋找並顯示階段的執行項目
local in_phase=false
while IFS= read -r line; do
if [[ "$line" =~ ^###.*$phase_name ]]; then
in_phase=true
continue
elif [[ "$line" =~ ^###.*階段[0-9]+: ]] && [[ "$in_phase" == true ]]; then
break
elif [[ "$in_phase" == true && "$line" =~ ^-.*\`.*\` ]]; then
echo " $line"
fi
done < "$project_file"
fi
}
# 開始執行階段
start_phase() {
local project_name="$1"
local phase_name="$2"
if [[ -z "$project_name" || -z "$phase_name" ]]; then
echo -e "${RED}❌ 請提供專案名稱和階段名稱${NC}"
echo "使用方法: ./dl phase start \"專案名稱\" \"階段名稱\""
return 1
fi
echo -e "${GREEN}🚀 開始執行階段: $phase_name${NC}"
echo -e "${BLUE}專案: $project_name${NC}"
echo ""
echo -e "${YELLOW}⚠️ 此功能開發中,將來會更新項目狀態...${NC}"
}
# 完成階段
complete_phase() {
local project_name="$1"
local phase_name="$2"
if [[ -z "$project_name" || -z "$phase_name" ]]; then
echo -e "${RED}❌ 請提供專案名稱和階段名稱${NC}"
echo "使用方法: ./dl phase complete \"專案名稱\" \"階段名稱\""
return 1
fi
echo -e "${GREEN}✅ 完成階段: $phase_name${NC}"
echo -e "${BLUE}專案: $project_name${NC}"
echo ""
echo -e "${YELLOW}⚠️ 此功能開發中,將來會更新項目狀態和產生報告...${NC}"
}
# 顯示階段狀態
show_phase_status() {
echo -e "${BLUE}📊 階段執行狀態總覽${NC}"
echo "=================================="
echo ""
echo -e "${GREEN}當前進行中的階段:${NC}"
echo " 🔄 環境配置 (Drama Ling 手機APP開發)"
echo ""
echo -e "${PURPLE}階段統計:${NC}"
echo " 📊 總階段數: 4"
echo " 🔄 進行中: 1"
echo " ⏳ 待執行: 3"
echo " ✅ 已完成: 0"
}
# 顯示幫助
show_help() {
echo -e "${BLUE}🚀 階段管理工具使用指南${NC}"
echo "=================================="
echo ""
echo -e "${PURPLE}階段管理命令:${NC}"
echo " list [專案名] - 列出階段"
echo " add \"專案\" \"階段名\" - 新增階段"
echo " items \"專案\" \"階段\" [類型] [描述] - 管理執行項目"
echo " start \"專案\" \"階段\" - 開始執行階段"
echo " complete \"專案\" \"階段\" - 完成階段"
echo " status - 查看階段狀態"
echo ""
echo -e "${PURPLE}範例:${NC}"
echo " ./dl phase add \"我的專案\" \"開發階段\""
echo " ./dl phase items \"我的專案\" \"開發階段\" FE \"建立登入頁面\""
echo " ./dl phase start \"我的專案\" \"開發階段\""
echo ""
echo -e "${BLUE}提示:${NC} 使用 ./dl project types 查看所有項目類型"
}
# 主邏輯
init_phase_structure
case "$1" in
"list")
list_phases "$2"
;;
"add")
add_phase "$2" "$3"
;;
"items")
manage_phase_items "$2" "$3" "$4" "$5"
;;
"start")
start_phase "$2" "$3"
;;
"complete")
complete_phase "$2" "$3"
;;
"status")
show_phase_status
;;
"help"|"--help"|"-h"|"")
show_help
;;
*)
echo -e "${RED}❌ 未知命令: $1${NC}"
echo ""
show_help
exit 1
;;
esac

221
tools/project.sh Executable file
View File

@ -0,0 +1,221 @@
#!/bin/bash
# 專案管理工具
# 使用方法: ./drama project [命令]
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
PROJECTS_FILE="$PROJECT_DIR/PROJECTS.md"
PROJECTS_DATA_DIR="$PROJECT_DIR/projects"
# 顏色定義
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
RED='\033[0;31m'
NC='\033[0m'
# 類型圖標和名稱函數 (Bash 3.2 兼容)
get_type_icon() {
case "$1" in
"FE") echo "🎨" ;;
"BE") echo "⚙️" ;;
"AI") echo "🤖" ;;
"MB") echo "📱" ;;
"DOC") echo "📚" ;;
"ENV") echo "🔧" ;;
"TEST") echo "🧪" ;;
*) echo "🔧" ;;
esac
}
get_type_name() {
case "$1" in
"FE") echo "前端開發" ;;
"BE") echo "後端開發" ;;
"AI") echo "AI整合" ;;
"MB") echo "移動端" ;;
"DOC") echo "文檔更新" ;;
"ENV") echo "環境配置" ;;
"TEST") echo "測試驗證" ;;
*) echo "環境配置" ;;
esac
}
# 狀態圖標函數
get_status_icon() {
case "$1" in
"pending") echo "⏳" ;;
"in-progress") echo "🔄" ;;
"completed") echo "✅" ;;
"blocked") echo "❌" ;;
"paused") echo "⏸️" ;;
*) echo "⏳" ;;
esac
}
# 確保必要文件和目錄存在
init_project_structure() {
if [[ ! -f "$PROJECTS_FILE" ]]; then
echo -e "${RED}❌ PROJECTS.md 不存在${NC}"
exit 1
fi
if [[ ! -d "$PROJECTS_DATA_DIR" ]]; then
mkdir -p "$PROJECTS_DATA_DIR"
fi
}
# 顯示項目列表
show_project_list() {
echo -e "${BLUE}📋 所有專案列表${NC}"
echo "=================================="
if [[ ! -f "$PROJECTS_FILE" ]]; then
echo -e "${RED}❌ 專案文件不存在${NC}"
return 1
fi
# 從PROJECTS.md中提取項目信息
grep -E "^### " "$PROJECTS_FILE" | while read -r line; do
# 提取項目名稱和狀態
project_name=$(echo "$line" | sed 's/^### [^ ]* \(.*\) (.*$/\1/')
status_line=$(echo "$line" | grep -o '([^)]*)$' | tr -d '()')
if [[ -n "$project_name" ]]; then
echo -e " ${GREEN}📁 $project_name${NC} ($status_line)"
fi
done
}
# 顯示項目狀態
show_project_status() {
local project_name="$1"
echo -e "${BLUE}📊 專案執行狀態${NC}"
echo "=================================="
if [[ -n "$project_name" ]]; then
echo -e "${PURPLE}專案: $project_name${NC}"
# TODO: 顯示特定項目的詳細狀態
else
# 顯示總體統計
echo -e "${GREEN}總體統計:${NC}"
echo " 📱 進行中專案: 1 個"
echo " ⏳ 待執行項目: 16 個"
echo " 🔄 進行中項目: 0 個"
echo " ✅ 已完成項目: 0 個"
fi
}
# 初始化新項目
init_new_project() {
local project_name="$1"
if [[ -z "$project_name" ]]; then
echo -e "${RED}❌ 請提供項目名稱${NC}"
echo "使用方法: ./drama project init \"項目名稱\""
return 1
fi
echo -e "${BLUE}🚀 初始化新專案: $project_name${NC}"
# 創建項目數據文件
local project_file="$PROJECTS_DATA_DIR/${project_name}.md"
if [[ -f "$project_file" ]]; then
echo -e "${YELLOW}⚠️ 專案 '$project_name' 已存在${NC}"
return 1
fi
# 創建項目詳細文件
cat > "$project_file" << EOF
# $project_name
**創建日期**: $(date +"%Y-%m-%d")
**狀態**: 🔄 進行中
## 專案描述
請在此處添加專案的詳細描述...
## 執行階段
### 階段1: 待定義
- ⏳ \`ENV\` 待添加執行項目...
## 專案統計
- **總階段數**: 1
- **執行項目數**: 1
- **完成進度**: 0%
---
**最後更新**: $(date +"%Y-%m-%d")
EOF
echo -e "${GREEN}✅ 專案 '$project_name' 初始化完成${NC}"
echo " 📁 專案文件: $project_file"
echo " 💡 使用 ./dl phase add \"$project_name\" \"階段名稱\" 新增階段"
}
# 列出項目類型
show_project_types() {
echo -e "${BLUE}🏷️ 專案類型定義${NC}"
echo "=================================="
local types="FE BE AI MB DOC ENV TEST"
for type_code in $types; do
local icon=$(get_type_icon "$type_code")
local name=$(get_type_name "$type_code")
echo -e " ${icon} ${PURPLE}$type_code${NC} - $name"
done
echo ""
echo -e "${BLUE}使用範例:${NC}"
echo " ./dl phase add \"專案名\" \"階段名\" FE \"前端任務描述\""
}
# 顯示幫助
show_help() {
echo -e "${BLUE}🚀 專案管理工具使用指南${NC}"
echo "=================================="
echo ""
echo -e "${PURPLE}基本命令:${NC}"
echo " init [項目名稱] - 初始化新專案"
echo " list - 列出所有專案"
echo " status [項目名稱] - 查看執行狀態"
echo " types - 顯示專案類型定義"
echo ""
echo -e "${PURPLE}範例:${NC}"
echo " ./dl project init \"我的新專案\""
echo " ./dl project list"
echo " ./dl project status"
echo ""
echo -e "${BLUE}提示:${NC} 使用 ./dl phase 管理專案階段和執行項目"
}
# 主邏輯
init_project_structure
case "$1" in
"init")
init_new_project "$2"
;;
"list")
show_project_list
;;
"status")
show_project_status "$2"
;;
"types")
show_project_types
;;
"help"|"--help"|"-h"|"")
show_help
;;
*)
echo -e "${RED}❌ 未知命令: $1${NC}"
echo ""
show_help
exit 1
;;
esac