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:
parent
f06257c2d9
commit
115a003afe
|
|
@ -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": []
|
||||
|
|
|
|||
103
CLAUDE-協作指南.md
103
CLAUDE-協作指南.md
|
|
@ -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
202
CLAUDE.md
|
|
@ -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 開發團隊
|
||||
|
|
@ -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
|
||||
142
README-問題管理.md
142
README-問題管理.md
|
|
@ -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/` - 各種檢查腳本目錄
|
||||
|
||||
---
|
||||
|
||||
**🎉 現在您有了一個超級簡單好用的問題管理系統!**
|
||||
|
||||
遇到問題就記錄,定期檢查狀態,讓專案開發更順暢!
|
||||
25
drama → dl
25
drama → dl
|
|
@ -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
|
||||
;;
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
# {{PROJECT_NAME}}
|
||||
|
||||
**創建日期**: {{CREATION_DATE}}
|
||||
**狀態**: 🔄 進行中
|
||||
|
||||
## 專案描述
|
||||
請在此處添加專案的詳細描述...
|
||||
|
||||
## 專案目標
|
||||
- [ ] 目標1:待定義
|
||||
- [ ] 目標2:待定義
|
||||
- [ ] 目標3:待定義
|
||||
|
||||
## 執行階段
|
||||
|
||||
### 階段1: 待定義 ⏳
|
||||
- ⏳ `ENV` 待添加執行項目...
|
||||
- 預計完成時間:待定
|
||||
- 負責人:待定
|
||||
- 依賴關係:無
|
||||
|
||||
## 專案資源
|
||||
|
||||
### 參考文檔
|
||||
- [ ] 需求規格文檔
|
||||
- [ ] 技術架構文檔
|
||||
- [ ] API規格文檔
|
||||
|
||||
### 相關連結
|
||||
- 專案倉庫:待定
|
||||
- 設計稿:待定
|
||||
- 測試環境:待定
|
||||
|
||||
## 風險評估
|
||||
|
||||
### 潛在風險
|
||||
1. **技術風險**:待評估
|
||||
2. **時程風險**:待評估
|
||||
3. **資源風險**:待評估
|
||||
|
||||
### 緩解措施
|
||||
- 待制定具體緩解策略
|
||||
|
||||
## 專案統計
|
||||
- **總階段數**: 1
|
||||
- **執行項目數**: 1
|
||||
- **完成進度**: 0%
|
||||
- **預計完成時間**: 待定
|
||||
|
||||
## 變更紀錄
|
||||
| 日期 | 變更內容 | 變更原因 |
|
||||
|------|----------|----------|
|
||||
| {{CREATION_DATE}} | 專案初始化 | 新專案建立 |
|
||||
|
||||
---
|
||||
**最後更新**: {{CREATION_DATE}}
|
||||
**維護人**: 待指定
|
||||
|
|
@ -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}}
|
||||
**維護人**: 開發團隊
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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!;
|
||||
}
|
||||
|
|
@ -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!;
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
|
|
@ -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 // 翻譯達人
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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 = '登入已過期,請重新登入';
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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('返回首頁'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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!,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 917 B |
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue