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 issue)",
|
||||||
"Bash(./drama report \"UI設計缺漏嚴重性評估\")",
|
"Bash(./drama report \"UI設計缺漏嚴重性評估\")",
|
||||||
"Bash(./drama compliance)",
|
"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": [],
|
"deny": [],
|
||||||
"ask": []
|
"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 是您的協作夥伴,讓他幫您記錄問題,讓專案更完善!**
|
|
||||||
204
CLAUDE.md
204
CLAUDE.md
|
|
@ -2,33 +2,40 @@
|
||||||
|
|
||||||
## 🤖 Claude 協作標準操作程序
|
## 🤖 Claude 協作標準操作程序
|
||||||
|
|
||||||
本文件為 Claude AI 助手在 Drama Ling 專案中的標準操作程序,確保工作流程的一致性和品質。
|
本文件為 Claude AI 助手在 Drama Ling 專案中的標準操作程序和協作指南。
|
||||||
|
|
||||||
## 🛠️ 必須使用的系統工具
|
## 🛠️ 系統工具使用
|
||||||
|
|
||||||
### 報告建立
|
### 專案執行管理 (新增 2025-09-08)
|
||||||
```bash
|
```bash
|
||||||
# ✅ 正確做法:使用系統工具
|
# ✅ 正確做法:使用專案管理工具
|
||||||
./drama report analysis "分析主題"
|
./dl project list # 列出所有專案
|
||||||
./drama report decision "決策主題"
|
./dl phase status # 查看階段狀態
|
||||||
|
./dl status # 查看執行狀態
|
||||||
# ❌ 禁止行為:手動創建報告檔案
|
|
||||||
# 直接 Write 或 Edit reports/ 目錄下的檔案
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 問題管理
|
### 問題管理
|
||||||
```bash
|
```bash
|
||||||
# ✅ 正確做法:使用問題管理工具
|
# ✅ 正確做法:使用問題管理工具
|
||||||
./drama issue
|
./dl issue # 互動式問題管理
|
||||||
|
|
||||||
# ❌ 禁止行為:直接編輯 ISSUES.md
|
# ❌ 禁止行為:直接編輯 ISSUES.md
|
||||||
# 除非是修正現有問題的格式錯誤
|
# 除非是修正現有問題的格式錯誤
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 報告建立
|
||||||
|
```bash
|
||||||
|
# ✅ 正確做法:使用系統工具
|
||||||
|
./dl report analysis "分析主題"
|
||||||
|
./dl report decision "決策主題"
|
||||||
|
|
||||||
|
# ❌ 禁止行為:手動創建報告檔案
|
||||||
|
```
|
||||||
|
|
||||||
### 檢查作業
|
### 檢查作業
|
||||||
```bash
|
```bash
|
||||||
# ✅ 正確做法:使用檢查工具
|
# ✅ 正確做法:使用檢查工具
|
||||||
./drama check
|
./dl check
|
||||||
|
|
||||||
# 其他維護腳本
|
# 其他維護腳本
|
||||||
./check_consistency.sh
|
./check_consistency.sh
|
||||||
|
|
@ -38,35 +45,101 @@
|
||||||
|
|
||||||
### 1. 工具優先原則
|
### 1. 工具優先原則
|
||||||
- **必須優先使用現有工具和腳本**
|
- **必須優先使用現有工具和腳本**
|
||||||
- 手動操作只能用於緊急修正
|
- 所有專案管理都透過 `./dl` 系統
|
||||||
- 所有報告都必須透過 `./drama report` 創建
|
- 所有問題記錄都透過 `./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
|
- 當前日期:2025-09-08
|
||||||
- 任何手動設定日期都必須使用正確的當前日期
|
- 任何手動設定日期都必須使用正確的當前日期
|
||||||
|
|
||||||
### 3. 文檔整合原則
|
### 5. 文檔整合原則
|
||||||
- 所有問題必須記錄到 ISSUES.md
|
- 所有問題必須記錄到 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. **執行分析工作**: 使用適當工具進行分析
|
2. **執行分析工作**: 使用適當工具進行分析
|
||||||
3. **更新報告內容**: 編輯生成的報告檔案
|
3. **更新報告內容**: 編輯生成的報告檔案
|
||||||
4. **整合問題系統**: 確認相關問題已正確連結
|
4. **整合問題系統**: 確認相關問題已正確連結
|
||||||
|
|
||||||
### 問題處理流程
|
### 問題處理流程
|
||||||
1. **記錄問題**: `./drama issue`
|
1. **記錄問題**: `./dl issue`
|
||||||
2. **分配優先級**: 🔥緊急 / ⚠️重要 / 📝一般
|
2. **分配優先級**: 🔥緊急 / ⚠️重要 / 📝一般
|
||||||
3. **建立相關報告**: 如有需要,建立分析或決策報告
|
3. **建立相關報告**: 如有需要,建立分析或決策報告
|
||||||
4. **追蹤解決進展**: 定期更新問題狀態
|
4. **追蹤解決進展**: 定期更新問題狀態
|
||||||
|
|
||||||
### 檢查作業流程
|
### 檢查作業流程
|
||||||
1. **執行系統檢查**: `./drama check`
|
1. **執行系統檢查**: `./dl check`
|
||||||
2. **運行一致性檢查**: `./check_consistency.sh`
|
2. **運行一致性檢查**: `./check_consistency.sh`
|
||||||
3. **記錄發現問題**: 使用問題管理系統
|
3. **記錄發現問題**: 使用問題管理系統
|
||||||
4. **產生檢查報告**: 必要時建立分析報告
|
4. **產生檢查報告**: 必要時建立分析報告
|
||||||
|
|
@ -75,7 +148,7 @@
|
||||||
|
|
||||||
### 錯誤1: 手動創建報告
|
### 錯誤1: 手動創建報告
|
||||||
**問題**: 直接創建報告檔案,導致日期錯誤、格式不一致
|
**問題**: 直接創建報告檔案,導致日期錯誤、格式不一致
|
||||||
**解決**: 必須使用 `./drama report` 命令
|
**解決**: 必須使用 `./dl report` 命令
|
||||||
|
|
||||||
### 錯誤2: 忽略現有工具
|
### 錯誤2: 忽略現有工具
|
||||||
**問題**: 重複實作已存在的功能
|
**問題**: 重複實作已存在的功能
|
||||||
|
|
@ -83,7 +156,7 @@
|
||||||
|
|
||||||
### 錯誤3: 未整合問題系統
|
### 錯誤3: 未整合問題系統
|
||||||
**問題**: 發現問題但未記錄到 ISSUES.md
|
**問題**: 發現問題但未記錄到 ISSUES.md
|
||||||
**解決**: 每次發現問題都必須使用 `./drama issue`
|
**解決**: 每次發現問題都必須使用 `./dl issue`
|
||||||
|
|
||||||
### 錯誤4: 日期不一致
|
### 錯誤4: 日期不一致
|
||||||
**問題**: 使用錯誤的日期或格式
|
**問題**: 使用錯誤的日期或格式
|
||||||
|
|
@ -92,12 +165,6 @@
|
||||||
### 錯誤5: 文檔更新缺少時間戳記 (新增 2025-09-08)
|
### 錯誤5: 文檔更新缺少時間戳記 (新增 2025-09-08)
|
||||||
**問題**: 更新文檔內容後未標記更新時間,難以追蹤變更歷史
|
**問題**: 更新文檔內容後未標記更新時間,難以追蹤變更歷史
|
||||||
**解決**: 任何文檔更新都必須加入時間戳記,格式為 (YYYY-MM-DD)
|
**解決**: 任何文檔更新都必須加入時間戳記,格式為 (YYYY-MM-DD)
|
||||||
**範例**:
|
|
||||||
```
|
|
||||||
- [x] 任務完成 ✅ (2025-09-08)
|
|
||||||
📊 **進度更新**: 已完成19個UI (2025-09-08)
|
|
||||||
### 新增功能 (新增 2025-09-08)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 品質檢查清單
|
## 🔍 品質檢查清單
|
||||||
|
|
||||||
|
|
@ -105,16 +172,21 @@
|
||||||
**⚠️ 重要:任何操作前都必須執行此檢查清單**
|
**⚠️ 重要:任何操作前都必須執行此檢查清單**
|
||||||
|
|
||||||
#### 問題管理操作前檢查
|
#### 問題管理操作前檢查
|
||||||
- [ ] 是否需要記錄新問題?如是,**必須使用** `./drama issue`
|
- [ ] 是否需要記錄新問題?如是,**必須使用** `./dl issue`
|
||||||
- [ ] 完成的問題是否要標記?如是,**絕對不可在待處理區標記[x]**
|
- [ ] 完成的問題是否要標記?如是,**絕對不可在待處理區標記[x]**
|
||||||
- [ ] 完成的問題**必須移動到「📚 已完成歷史」對應日期區域**
|
- [ ] 完成的問題**必須移動到「📚 已完成歷史」對應日期區域**
|
||||||
- [ ] 移動時**必須保留所有解決詳情和連結**
|
- [ ] 移動時**必須保留所有解決詳情和連結**
|
||||||
|
|
||||||
#### 報告建立操作前檢查
|
#### 報告建立操作前檢查
|
||||||
- [ ] 是否需要建立報告?如是,**必須使用** `./drama report analysis "主題"`
|
- [ ] 是否需要建立報告?如是,**必須使用** `./dl report analysis "主題"`
|
||||||
- [ ] **禁止手動創建** reports/ 目錄下的任何檔案
|
- [ ] **禁止手動創建** reports/ 目錄下的任何檔案
|
||||||
- [ ] 報告主題描述是否具體明確?
|
- [ ] 報告主題描述是否具體明確?
|
||||||
|
|
||||||
|
#### 專案管理操作前檢查 (新增 2025-09-08)
|
||||||
|
- [ ] 專案任務是否需要更新狀態?
|
||||||
|
- [ ] 任務類型是否正確識別(FE/BE/AI/MB/DOC/ENV/TEST)?
|
||||||
|
- [ ] 是否需要建議下一步行動?
|
||||||
|
|
||||||
#### 檔案操作前檢查
|
#### 檔案操作前檢查
|
||||||
- [ ] 檔案編碼是否設定為 UTF-8?
|
- [ ] 檔案編碼是否設定為 UTF-8?
|
||||||
- [ ] 中文內容是否正確顯示?
|
- [ ] 中文內容是否正確顯示?
|
||||||
|
|
@ -133,12 +205,49 @@
|
||||||
### 每次任務完成後檢查
|
### 每次任務完成後檢查
|
||||||
- [ ] 是否使用了正確的系統工具?
|
- [ ] 是否使用了正確的系統工具?
|
||||||
- [ ] 所有日期是否正確(2025-09-08)?
|
- [ ] 所有日期是否正確(2025-09-08)?
|
||||||
|
- [ ] 任務狀態是否已更新為完成 ✅ (專案任務)
|
||||||
- [ ] 發現的問題是否已記錄?
|
- [ ] 發現的問題是否已記錄?
|
||||||
- [ ] 報告是否已正確整合到問題系統?
|
- [ ] 報告是否已正確整合到問題系統?
|
||||||
- [ ] 檔案命名是否符合系統標準?
|
- [ ] 檔案命名是否符合系統標準?
|
||||||
- [ ] **ISSUES.md中完成的項目是否已正確移動到歷史區域?**
|
- [ ] **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. 記錄發現的不一致問題
|
3. 記錄發現的不一致問題
|
||||||
4. 修正問題後繼續工作
|
4. 修正問題後繼續工作
|
||||||
|
|
||||||
|
## 💡 讓協作更順暢的技巧 (整合 2025-09-08)
|
||||||
|
|
||||||
|
### 🏷️ 在任務開始時就說明:
|
||||||
|
"我希望你把發現的所有問題都記錄下來,這樣我們就不會遺漏任何需要解決的事項。"
|
||||||
|
|
||||||
|
### 🔄 定期檢查:
|
||||||
|
每週問 Claude:"最近有沒有發現什麼新的問題需要記錄?"
|
||||||
|
|
||||||
|
### 📊 任務總結:
|
||||||
|
"總結一下這次任務中發現的問題,並確保都記錄了。"
|
||||||
|
|
||||||
## 📚 相關文檔
|
## 📚 相關文檔
|
||||||
|
|
||||||
- [問題追蹤系統](./ISSUES.md)
|
- [問題追蹤系統](./ISSUES.md)
|
||||||
|
- [專案執行管理](./PROJECTS.md)
|
||||||
- [工具使用說明](./tools/)
|
- [工具使用說明](./tools/)
|
||||||
- [報告模板](./reports/templates/)
|
- [報告模板](./reports/templates/)
|
||||||
- [檢查腳本](./scripts/)
|
- [檢查腳本](./scripts/)
|
||||||
|
|
@ -174,11 +295,7 @@
|
||||||
3. 修訂本指南文檔
|
3. 修訂本指南文檔
|
||||||
4. 確保向下相容性
|
4. 確保向下相容性
|
||||||
|
|
||||||
---
|
## 🤝 標準化指令格式
|
||||||
|
|
||||||
**重要提醒**: 本指南是 Claude AI 助手的強制性操作標準。任何偏離此流程的行為都可能造成系統不一致和品質問題。
|
|
||||||
|
|
||||||
## 🤝 標準化指令格式(方案A)
|
|
||||||
|
|
||||||
### 推薦指令格式
|
### 推薦指令格式
|
||||||
```
|
```
|
||||||
|
|
@ -187,13 +304,28 @@
|
||||||
|
|
||||||
### 範例指令
|
### 範例指令
|
||||||
```
|
```
|
||||||
請分析UI設計問題,遵循SOP,記得使用./drama report analysis建立報告
|
請分析UI設計問題,遵循SOP,記得使用./dl report analysis建立報告
|
||||||
請處理緊急問題,遵循SOP,完成後問題要移到歷史區域不可標記[x]
|
請處理緊急問題,遵循SOP,完成後問題要移到歷史區域不可標記[x]
|
||||||
請建立新的API文檔,遵循SOP,使用正確日期和UTF-8編碼
|
請建立新的API文檔,遵循SOP,使用正確日期和UTF-8編碼
|
||||||
|
執行 Android Studio 安裝和配置,遇到問題就記錄
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🎉 效益
|
||||||
|
|
||||||
|
✅ **不會遺漏問題** - 所有發現的問題都被記錄
|
||||||
|
✅ **追蹤更完整** - 包含 AI 協助時發現的問題
|
||||||
|
✅ **決策有依據** - 問題記錄成為決策參考
|
||||||
|
✅ **開發更順暢** - 提前發現潛在問題
|
||||||
|
✅ **專案管理清晰** - 階段化執行,進度透明 (新增 2025-09-08)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**重要提醒**: 本指南是 Claude AI 助手的強制性操作標準。任何偏離此流程的行為都可能造成系統不一致和品質問題。
|
||||||
|
|
||||||
|
**💫 記住:Claude 是您的協作夥伴,讓他幫您記錄問題和管理專案,讓開發更完善!**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**最後更新**: 2025-09-08
|
**最後更新**: 2025-09-08
|
||||||
**版本**: 2.1 - 加入文檔更新時間戳記強制性要求 (2025-09-08)
|
**版本**: 3.0 - 整合專案管理系統和協作指南 (2025-09-08)
|
||||||
**維護者**: Drama Ling 開發團隊
|
**維護者**: 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() {
|
show_menu() {
|
||||||
echo -e "${BLUE}🎭 Drama Ling 專案管理工具${NC}"
|
echo -e "${BLUE}🎭 Drama Ling 管理工具${NC}"
|
||||||
echo "=================================="
|
echo "=================================="
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${PURPLE}📋 問題管理${NC}"
|
echo -e "${PURPLE}📋 問題管理${NC}"
|
||||||
|
|
@ -27,6 +27,11 @@ show_menu() {
|
||||||
echo " decision - 建立決策記錄"
|
echo " decision - 建立決策記錄"
|
||||||
echo " reports - 檢查報告狀態"
|
echo " reports - 檢查報告狀態"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo -e "${PURPLE}🚀 專案執行管理${NC}"
|
||||||
|
echo " project - 專案管理"
|
||||||
|
echo " phase - 階段管理"
|
||||||
|
echo " status - 查看執行狀態"
|
||||||
|
echo ""
|
||||||
echo -e "${PURPLE}🔧 系統檢查${NC}"
|
echo -e "${PURPLE}🔧 系統檢查${NC}"
|
||||||
echo " consistency - 執行一致性檢查"
|
echo " consistency - 執行一致性檢查"
|
||||||
echo " compliance - 執行合規性檢查"
|
echo " compliance - 執行合規性檢查"
|
||||||
|
|
@ -37,9 +42,10 @@ show_menu() {
|
||||||
echo " help - 顯示此幫助"
|
echo " help - 顯示此幫助"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}範例:${NC}"
|
echo -e "${BLUE}範例:${NC}"
|
||||||
echo " ./drama issue # 管理問題"
|
echo " ./dl issue # 管理問題"
|
||||||
echo " ./drama report \"API分析\" # 建立分析報告"
|
echo " ./dl report \"API分析\" # 建立分析報告"
|
||||||
echo " ./drama check # 檢查問題狀態"
|
echo " ./dl project list # 列出所有專案"
|
||||||
|
echo " ./dl phase status # 查看階段狀態"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 主邏輯
|
# 主邏輯
|
||||||
|
|
@ -61,6 +67,17 @@ case "$1" in
|
||||||
"reports")
|
"reports")
|
||||||
exec "$TOOLS_DIR/check_reports.sh"
|
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")
|
"consistency")
|
||||||
exec "$SCRIPT_DIR/scripts/maintenance_manager.sh" 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