init
93
.gitignore
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
# 编译文件
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.test
|
||||
*.out
|
||||
|
||||
# Go工作区文件
|
||||
go.work
|
||||
|
||||
# 依赖目录
|
||||
vendor/
|
||||
|
||||
# 编译输出
|
||||
bin/
|
||||
dist/
|
||||
build/
|
||||
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
logs/
|
||||
serve/logs/
|
||||
|
||||
# 数据库文件
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# IDE和编辑器
|
||||
.vscode/
|
||||
.idea/
|
||||
*.iml
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 环境变量文件
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Flutter/Dart
|
||||
client/.dart_tool/
|
||||
client/.flutter-plugins
|
||||
client/.flutter-plugins-dependencies
|
||||
client/.packages
|
||||
client/build/
|
||||
client/pubspec.lock
|
||||
|
||||
# 测试覆盖率
|
||||
coverage/
|
||||
*.cover
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# 数据文件(可选)
|
||||
data/*.xlsx
|
||||
data/*.csv
|
||||
!data/README.md
|
||||
|
||||
.windsurf/
|
||||
|
||||
docs/
|
||||
*.zip
|
||||
|
||||
**/build/**
|
||||
206
Makefile
Normal file
@@ -0,0 +1,206 @@
|
||||
# Makefile for AI English Learning Project
|
||||
|
||||
# 变量定义
|
||||
APP_NAME := ai-english-learning
|
||||
VERSION := $(shell git describe --tags --always --dirty)
|
||||
BUILD_TIME := $(shell date +%Y-%m-%d_%H:%M:%S)
|
||||
GO_VERSION := $(shell go version | awk '{print $$3}')
|
||||
GIT_COMMIT := $(shell git rev-parse HEAD)
|
||||
|
||||
# Go相关变量
|
||||
GOCMD := go
|
||||
GOBUILD := $(GOCMD) build
|
||||
GOCLEAN := $(GOCMD) clean
|
||||
GOTEST := $(GOCMD) test
|
||||
GOGET := $(GOCMD) get
|
||||
GOMOD := $(GOCMD) mod
|
||||
|
||||
# 构建标志
|
||||
LDFLAGS := -ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.GitCommit=$(GIT_COMMIT)"
|
||||
|
||||
# 目录
|
||||
SERVE_DIR := ./serve
|
||||
CLIENT_DIR := ./client
|
||||
DOCS_DIR := ./docs
|
||||
|
||||
.PHONY: help build clean test deps docker-build docker-run docker-stop dev prod lint format check
|
||||
|
||||
# 默认目标
|
||||
all: clean deps test build
|
||||
|
||||
# 帮助信息
|
||||
help:
|
||||
@echo "AI English Learning Project Makefile"
|
||||
@echo ""
|
||||
@echo "Available targets:"
|
||||
@echo " build - Build the Go application"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo " test - Run tests"
|
||||
@echo " deps - Download dependencies"
|
||||
@echo " dev - Run in development mode"
|
||||
@echo " prod - Run in production mode"
|
||||
@echo " docker-build - Build Docker image"
|
||||
@echo " docker-run - Run production environment"
|
||||
@echo " docker-dev - Run development environment"
|
||||
@echo " docker-stop - Stop all Docker services"
|
||||
@echo " restart - Restart production environment"
|
||||
@echo " restart-dev - Restart development environment"
|
||||
@echo " logs - Show production logs"
|
||||
@echo " logs-dev - Show development logs"
|
||||
@echo " lint - Run linter"
|
||||
@echo " format - Format code"
|
||||
@echo " check - Run all checks (lint, test, build)"
|
||||
@echo " migrate - Run database migrations"
|
||||
@echo " seed - Seed database with test data"
|
||||
|
||||
# 构建应用
|
||||
build:
|
||||
@echo "Building $(APP_NAME)..."
|
||||
cd $(SERVE_DIR) && $(GOBUILD) $(LDFLAGS) -o $(APP_NAME) .
|
||||
@echo "Build completed: $(SERVE_DIR)/$(APP_NAME)"
|
||||
|
||||
# 清理构建产物
|
||||
clean:
|
||||
@echo "Cleaning..."
|
||||
cd $(SERVE_DIR) && $(GOCLEAN)
|
||||
rm -f $(SERVE_DIR)/$(APP_NAME)
|
||||
rm -rf $(SERVE_DIR)/logs/*
|
||||
docker-compose down --volumes --remove-orphans 2>/dev/null || true
|
||||
docker system prune -f 2>/dev/null || true
|
||||
|
||||
# 下载依赖
|
||||
deps:
|
||||
@echo "Downloading dependencies..."
|
||||
cd $(SERVE_DIR) && $(GOMOD) download
|
||||
cd $(SERVE_DIR) && $(GOMOD) tidy
|
||||
|
||||
# 运行测试
|
||||
test:
|
||||
@echo "Running tests..."
|
||||
cd $(SERVE_DIR) && $(GOTEST) -v ./...
|
||||
|
||||
# 开发模式运行
|
||||
dev: deps
|
||||
@echo "Starting development server..."
|
||||
cd $(SERVE_DIR) && go run main.go
|
||||
|
||||
# 生产模式运行
|
||||
prod: build
|
||||
@echo "Starting production server..."
|
||||
cd $(SERVE_DIR) && ./$(APP_NAME)
|
||||
|
||||
# Docker构建
|
||||
docker-build:
|
||||
@echo "Building Docker image..."
|
||||
docker-compose build ai-english-backend
|
||||
|
||||
# Docker运行(生产环境)
|
||||
docker-run:
|
||||
@echo "Starting services with Docker Compose..."
|
||||
docker-compose up -d
|
||||
@echo "Services started. Frontend available at http://localhost:80"
|
||||
@echo "Backend available at http://localhost:8080"
|
||||
@echo "Use 'make logs' to view logs"
|
||||
|
||||
# Docker运行(开发环境)
|
||||
docker-dev:
|
||||
@echo "Starting development environment..."
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
@echo "Development environment started!"
|
||||
@echo "Backend: http://localhost:8080"
|
||||
@echo "Database Admin: http://localhost:8081"
|
||||
@echo "Redis Admin: http://localhost:8082"
|
||||
|
||||
# Docker停止
|
||||
docker-stop:
|
||||
@echo "Stopping Docker Compose services..."
|
||||
docker-compose down
|
||||
docker-compose -f docker-compose.dev.yml down
|
||||
|
||||
# 代码检查
|
||||
lint:
|
||||
@echo "Running linter..."
|
||||
cd $(SERVE_DIR) && golangci-lint run ./... || echo "golangci-lint not installed, skipping..."
|
||||
|
||||
# 代码格式化
|
||||
format:
|
||||
@echo "Formatting code..."
|
||||
cd $(SERVE_DIR) && go fmt ./...
|
||||
cd $(SERVE_DIR) && goimports -w . 2>/dev/null || echo "goimports not installed, skipping..."
|
||||
|
||||
# 运行所有检查
|
||||
check: format lint test build
|
||||
@echo "All checks passed!"
|
||||
|
||||
# 数据库迁移
|
||||
migrate:
|
||||
@echo "Running database migrations..."
|
||||
@echo "Please ensure database is running and execute SQL files manually"
|
||||
@echo "SQL files location: $(DOCS_DIR)/database_schema.sql"
|
||||
|
||||
# 数据库种子数据
|
||||
seed:
|
||||
@echo "Seeding database..."
|
||||
@echo "Please implement seed data scripts"
|
||||
|
||||
# 查看日志(生产环境)
|
||||
logs:
|
||||
@echo "Showing application logs..."
|
||||
docker-compose logs -f
|
||||
|
||||
# 查看日志(开发环境)
|
||||
logs-dev:
|
||||
@echo "Showing development logs..."
|
||||
docker-compose -f docker-compose.dev.yml logs -f
|
||||
|
||||
# 查看所有服务状态
|
||||
status:
|
||||
@echo "Service status:"
|
||||
docker-compose ps
|
||||
|
||||
# 重启服务(生产环境)
|
||||
restart: docker-stop docker-run
|
||||
|
||||
# 重启服务(开发环境)
|
||||
restart-dev:
|
||||
@echo "Restarting development environment..."
|
||||
docker-compose -f docker-compose.dev.yml restart
|
||||
|
||||
# 安装开发工具
|
||||
install-tools:
|
||||
@echo "Installing development tools..."
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
go install golang.org/x/tools/cmd/goimports@latest
|
||||
|
||||
# 生成API文档
|
||||
docs:
|
||||
@echo "Generating API documentation..."
|
||||
@echo "API documentation available in $(DOCS_DIR)/API接口文档.md"
|
||||
|
||||
# 备份数据库
|
||||
backup:
|
||||
@echo "Creating database backup..."
|
||||
mkdir -p ./backups
|
||||
docker-compose exec mysql mysqldump -u ai_english -pai_english_password ai_english_learning > ./backups/backup_$(shell date +%Y%m%d_%H%M%S).sql
|
||||
|
||||
# 恢复数据库
|
||||
restore:
|
||||
@echo "To restore database, run:"
|
||||
@echo "docker-compose exec -T mysql mysql -u ai_english -pai_english_password ai_english_learning < ./backups/your_backup_file.sql"
|
||||
|
||||
# 性能测试
|
||||
bench:
|
||||
@echo "Running benchmarks..."
|
||||
cd $(SERVE_DIR) && go test -bench=. -benchmem ./...
|
||||
|
||||
# 安全扫描
|
||||
security:
|
||||
@echo "Running security scan..."
|
||||
cd $(SERVE_DIR) && gosec ./... 2>/dev/null || echo "gosec not installed, skipping..."
|
||||
|
||||
# 版本信息
|
||||
version:
|
||||
@echo "Version: $(VERSION)"
|
||||
@echo "Build Time: $(BUILD_TIME)"
|
||||
@echo "Go Version: $(GO_VERSION)"
|
||||
@echo "Git Commit: $(GIT_COMMIT)"
|
||||
800
README.md
Normal file
@@ -0,0 +1,800 @@
|
||||
# AI英语学习平台
|
||||
|
||||
一个基于AI技术的智能英语学习平台,提供个性化的英语学习体验,包括词汇学习、听力训练、阅读理解、写作练习和口语练习等功能。
|
||||
|
||||
## 🌟 特性
|
||||
|
||||
- **智能词汇学习**:基于艾宾浩斯遗忘曲线的智能复习系统
|
||||
- **听力训练**:多样化的听力材料和智能评估
|
||||
- **阅读理解**:分级阅读材料和理解测试
|
||||
- **写作练习**:AI智能批改和写作建议
|
||||
- **口语练习**:AI对话伙伴和发音评估
|
||||
- **学习统计**:详细的学习进度和成绩分析
|
||||
- **个性化推荐**:基于学习数据的智能内容推荐
|
||||
|
||||
## 🏗️ 技术架构
|
||||
|
||||
### 后端技术栈
|
||||
- **语言**:Go 1.19+
|
||||
- **框架**:Gin Web Framework
|
||||
- **数据库**:MySQL 8.0+
|
||||
- **缓存**:Redis 7.0+
|
||||
- **认证**:JWT Token
|
||||
- **日志**:Logrus
|
||||
- **配置**:Viper
|
||||
- **容器化**:Docker & Docker Compose
|
||||
|
||||
### 前端技术栈
|
||||
- **框架**:Flutter
|
||||
- **状态管理**:Provider/Riverpod
|
||||
- **网络请求**:Dio
|
||||
- **本地存储**:SharedPreferences/Hive
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
ai_english_learning/
|
||||
├── client/ # Flutter前端应用
|
||||
│ ├── lib/
|
||||
│ │ ├── models/ # 数据模型
|
||||
│ │ ├── services/ # 业务服务
|
||||
│ │ ├── screens/ # 页面组件
|
||||
│ │ ├── widgets/ # 通用组件
|
||||
│ │ └── utils/ # 工具类
|
||||
│ └── pubspec.yaml
|
||||
├── serve/ # Go后端服务
|
||||
│ ├── api/ # API处理器
|
||||
│ ├── internal/ # 内部模块
|
||||
│ │ ├── config/ # 配置管理
|
||||
│ │ ├── database/ # 数据库操作
|
||||
│ │ ├── logger/ # 日志系统
|
||||
│ │ ├── middleware/ # 中间件
|
||||
│ │ ├── models/ # 数据模型
|
||||
│ │ └── services/ # 业务服务
|
||||
│ ├── config/ # 配置文件
|
||||
│ ├── logs/ # 日志文件
|
||||
│ ├── main.go # 应用入口
|
||||
│ ├── router.go # 路由配置
|
||||
│ ├── Dockerfile # Docker配置
|
||||
│ ├── Makefile # 构建脚本
|
||||
│ └── start.sh # 启动脚本
|
||||
├── docs/ # 项目文档
|
||||
│ ├── API接口文档.md
|
||||
│ ├── 需求文档.md
|
||||
│ ├── 技术架构文档.md
|
||||
│ └── database_schema.sql
|
||||
├── docker-compose.yml # Docker Compose配置
|
||||
├── DEPLOYMENT.md # 部署指南
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Go 1.19+
|
||||
- MySQL 8.0+
|
||||
- Redis 7.0+
|
||||
- Docker & Docker Compose (可选)
|
||||
- Flutter 3.0+ (前端开发)
|
||||
|
||||
### 使用Docker Compose(推荐)
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd ai_english_learning
|
||||
```
|
||||
|
||||
2. **启动所有服务**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
3. **查看服务状态**
|
||||
```bash
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
4. **访问应用**
|
||||
- 后端API:http://localhost:8080
|
||||
- 健康检查:http://localhost:8080/health
|
||||
- API文档:查看 `docs/API接口文档.md`
|
||||
|
||||
### 本地开发
|
||||
|
||||
#### 后端开发
|
||||
|
||||
1. **进入后端目录**
|
||||
```bash
|
||||
cd serve
|
||||
```
|
||||
|
||||
2. **安装依赖**
|
||||
```bash
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
3. **配置数据库**
|
||||
```bash
|
||||
# 创建数据库
|
||||
mysql -u root -p -e "CREATE DATABASE ai_english_learning CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
|
||||
# 导入数据库结构
|
||||
mysql -u root -p ai_english_learning < ../docs/database_schema.sql
|
||||
```
|
||||
|
||||
4. **配置应用**
|
||||
```bash
|
||||
# 复制配置文件
|
||||
cp config/config.yaml config/config.local.yaml
|
||||
|
||||
# 编辑配置文件,修改数据库连接等信息
|
||||
vim config/config.local.yaml
|
||||
```
|
||||
|
||||
5. **启动应用**
|
||||
```bash
|
||||
# 使用启动脚本(推荐)
|
||||
./start.sh -d
|
||||
|
||||
# 或者直接运行
|
||||
go run .
|
||||
|
||||
# 或者使用Makefile
|
||||
make dev
|
||||
```
|
||||
|
||||
#### 前端开发
|
||||
|
||||
1. **进入前端目录**
|
||||
```bash
|
||||
cd client
|
||||
```
|
||||
|
||||
2. **安装依赖**
|
||||
```bash
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
3. **运行应用**
|
||||
```bash
|
||||
flutter run
|
||||
```
|
||||
4. **使用安卓模拟器启动预览**
|
||||
```
|
||||
检查设备
|
||||
$ flutter devices
|
||||
软件加速模式启动模拟器
|
||||
$ emulator -avd flutter_emulator -no-accel
|
||||
使用完整路径来启动模拟器
|
||||
$ ANDROID_HOME/emulator/emulator -avd flutter_emulator -no-accel
|
||||
|
||||
等待模拟器启动后,启动服务
|
||||
$ flutter run -d emulator-5554
|
||||
flutter run -d chrome --web-port=3003
|
||||
```
|
||||
# 进入前端目录
|
||||
cd /home/nanqipro01/gitlocal/YunQue-Tech-Projects/ai_english_learning/client
|
||||
|
||||
# 在模拟器上运行应用
|
||||
flutter run -d emulator-5554
|
||||
|
||||
# 或者让Flutter自动选择设备
|
||||
flutter run
|
||||
|
||||
|
||||
## 📖 API文档
|
||||
|
||||
详细的API文档请查看:[API接口文档.md](docs/API接口文档.md)
|
||||
|
||||
### 主要API端点
|
||||
|
||||
- **认证相关**
|
||||
- `POST /api/auth/register` - 用户注册
|
||||
- `POST /api/auth/login` - 用户登录
|
||||
- `POST /api/auth/refresh` - 刷新Token
|
||||
|
||||
- **用户管理**
|
||||
- `GET /api/users/profile` - 获取用户信息
|
||||
- `PUT /api/users/profile` - 更新用户信息
|
||||
- `GET /api/users/stats` - 获取学习统计
|
||||
|
||||
- **词汇学习**
|
||||
- `GET /api/vocabulary/words` - 获取单词列表
|
||||
- `POST /api/vocabulary/learn` - 学习单词
|
||||
- `GET /api/vocabulary/review` - 获取复习单词
|
||||
|
||||
- **健康检查**
|
||||
- `GET /health` - 综合健康检查
|
||||
- `GET /health/liveness` - 存活检查
|
||||
- `GET /health/readiness` - 就绪检查
|
||||
- `GET /version` - 版本信息
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 环境变量
|
||||
|
||||
```bash
|
||||
# 服务器配置
|
||||
SERVER_PORT=8080
|
||||
SERVER_MODE=release
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_PORT=3306
|
||||
DATABASE_USER=ai_english
|
||||
DATABASE_PASSWORD=your_password
|
||||
DATABASE_NAME=ai_english_learning
|
||||
|
||||
# Redis配置
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET=your-super-secret-jwt-key
|
||||
JWT_ACCESS_TOKEN_TTL=3600
|
||||
JWT_REFRESH_TOKEN_TTL=604800
|
||||
|
||||
# 应用配置
|
||||
APP_ENVIRONMENT=production
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
### 配置文件
|
||||
|
||||
配置文件位于 `serve/config/config.yaml`,支持多环境配置。
|
||||
|
||||
## 🧪 测试
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 后端测试
|
||||
cd serve
|
||||
make test
|
||||
|
||||
# 前端测试
|
||||
cd client
|
||||
flutter test
|
||||
```
|
||||
|
||||
### 性能测试
|
||||
|
||||
```bash
|
||||
cd serve
|
||||
make bench
|
||||
```
|
||||
|
||||
## 📦 部署
|
||||
|
||||
详细的部署指南请查看:[DEPLOYMENT.md](DEPLOYMENT.md)
|
||||
|
||||
### 生产环境部署
|
||||
|
||||
1. **构建Docker镜像**
|
||||
```bash
|
||||
make docker-build
|
||||
```
|
||||
|
||||
2. **部署到生产环境**
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
3. **配置反向代理**
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔍 监控和维护
|
||||
|
||||
### 日志查看
|
||||
|
||||
```bash
|
||||
# 查看应用日志
|
||||
tail -f serve/logs/app.log
|
||||
|
||||
# Docker环境日志
|
||||
docker-compose logs -f ai-english-backend
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
|
||||
```bash
|
||||
# 检查服务状态
|
||||
curl http://localhost:8080/health
|
||||
|
||||
# 检查版本信息
|
||||
curl http://localhost:8080/version
|
||||
```
|
||||
|
||||
### 数据库备份
|
||||
|
||||
```bash
|
||||
# 备份数据库
|
||||
mysqldump -u root -p ai_english_learning > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||
|
||||
# 使用Makefile
|
||||
make backup
|
||||
```
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 打开 Pull Request
|
||||
|
||||
### 代码规范
|
||||
|
||||
- **Go代码**:遵循 `gofmt` 和 `golint` 规范
|
||||
- **Flutter代码**:遵循 Dart 官方代码规范
|
||||
- **提交信息**:使用语义化提交信息
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||
|
||||
## 📞 联系我们
|
||||
|
||||
- **项目维护者**:[Your Name]
|
||||
- **邮箱**:[your.email@example.com]
|
||||
- **问题反馈**:[GitHub Issues](https://github.com/your-username/ai-english-learning/issues)
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
感谢所有为这个项目做出贡献的开发者和用户。
|
||||
|
||||
---
|
||||
|
||||
**注意**:这是一个学习项目,仅供教育和研究目的使用。
|
||||
|
||||
## 核心特色
|
||||
|
||||
### 🤖 AI驱动的个性化学习
|
||||
- 基于用户学习数据的智能推荐系统
|
||||
- 自适应学习路径规划
|
||||
- 个性化学习内容匹配
|
||||
- 智能学习进度调整
|
||||
|
||||
### 📚 全技能覆盖
|
||||
- **词汇学习**:科学记忆算法,多维度词汇训练
|
||||
- **听力训练**:分级听力材料,智能语音识别
|
||||
- **阅读理解**:多样化文章,智能阅读辅助
|
||||
- **写作练习**:AI智能批改,实时反馈改进
|
||||
- **口语练习**:AI对话伙伴,发音智能评估
|
||||
|
||||
### 🎯 考试导向支持
|
||||
- 四六级英语考试专项训练
|
||||
- 托福、雅思考试备考模块
|
||||
- 考研英语专业辅导
|
||||
- 商务英语实用技能
|
||||
|
||||
### 📊 数据驱动的学习分析
|
||||
- 详细的学习数据统计
|
||||
- 多维度能力分析报告
|
||||
- 学习进度可视化展示
|
||||
- 个性化改进建议
|
||||
|
||||
## 主要功能模块
|
||||
|
||||
### 1. 个人主页
|
||||
- 学习数据概览(已学单词数、连续打卡天数、平均得分)
|
||||
- 学习进度可视化(词库进度、技能雷达图)
|
||||
- 今日学习推荐
|
||||
- 个人信息管理
|
||||
|
||||
### 2. 单词学习模块
|
||||
- **分级词库**:从小学到专业级别的完整词汇体系
|
||||
- **智能记忆**:基于艾宾浩斯记忆曲线的复习算法
|
||||
- **多模式学习**:卡片背诵、测试练习、语境学习
|
||||
- **AI助手**:智能例句生成、词汇关联、记忆技巧
|
||||
|
||||
### 3. 听力训练模块
|
||||
- **分级内容**:从日常对话到学术讲座的全覆盖
|
||||
- **智能播放**:语速调节、重复播放、字幕控制
|
||||
- **多样练习**:理解练习、听写练习、跟读练习
|
||||
- **能力分析**:语音识别、语义理解、语速适应能力评估
|
||||
|
||||
### 4. 阅读理解模块
|
||||
- **双模式阅读**:休闲阅读和练习阅读
|
||||
- **智能辅助**:即点即译、段落摘要、结构分析
|
||||
- **多样题型**:主旨大意、细节理解、推理判断
|
||||
- **技能训练**:快速阅读、精读、扫读、略读
|
||||
|
||||
### 5. 写作练习模块
|
||||
- **多种模式**:中译英练习、话题写作
|
||||
- **考试专项**:四六级、考研、托福、雅思写作
|
||||
- **AI智能批改**:语法检查、词汇评估、表达流畅度分析
|
||||
- **写作辅助**:模板库、素材库、例句参考
|
||||
|
||||
### 6. 口语练习模块
|
||||
- **AI对话伙伴**:商务、日常、旅行、学术等专业导师
|
||||
- **场景训练**:生活、职场、学术等真实场景对话
|
||||
- **智能评估**:发音准确度、流利度、语法正确性
|
||||
- **个性化训练**:能力诊断、适应性训练
|
||||
|
||||
### 7. AI智能助手
|
||||
- **个性化推荐**:学习内容和路径智能推荐
|
||||
- **智能答疑**:语言问题解答、学习方法指导
|
||||
- **学习分析**:行为分析、能力诊断
|
||||
- **多模态交互**:文本、语音、视觉交互支持
|
||||
|
||||
## 技术架构
|
||||
|
||||
### 前端技术
|
||||
- **Flutter**:跨平台移动应用开发
|
||||
- 支持iOS、Android、Web、桌面多平台
|
||||
- 响应式设计,优秀的用户体验
|
||||
|
||||
### 后端技术
|
||||
- **Go Gin**:高性能API服务
|
||||
- **微服务架构**:模块化、可扩展的系统设计
|
||||
- **MySQL 8.0**:可靠的关系型数据库
|
||||
|
||||
### AI技术栈
|
||||
- **Hugging Face Transformers**:自然语言处理
|
||||
- **PyTorch**:深度学习框架
|
||||
- **spaCy**:高级自然语言处理
|
||||
- **语音识别与合成**:智能语音处理
|
||||
|
||||
### 部署与运维
|
||||
- **Docker + Docker Compose**:容器化部署
|
||||
- **GitHub Actions/Codemagic**:CI/CD自动化
|
||||
- **Celery + RabbitMQ/Redis**:异步任务处理
|
||||
|
||||
## 项目结构
|
||||
|
||||
### 整体项目结构
|
||||
|
||||
```
|
||||
ai_english_learning/
|
||||
├── docs/ # 项目文档
|
||||
│ ├── UI界面设计/ # UI设计文件
|
||||
│ ├── 详细需求文档.md # 功能需求文档
|
||||
│ └── 技术选型.md # 技术架构文档
|
||||
├── client/ # 前端代码(Flutter)
|
||||
├── serve/ # 后端代码(Go)
|
||||
├── deployment/ # 部署配置
|
||||
├── tests/ # 测试代码
|
||||
├── docker-compose.yml # Docker编排文件
|
||||
└── README.md # 项目说明文档
|
||||
```
|
||||
|
||||
### 前端项目结构(Flutter)
|
||||
|
||||
```
|
||||
client/
|
||||
├── android/ # Android平台配置
|
||||
├── ios/ # iOS平台配置
|
||||
├── lib/ # 主要源代码目录
|
||||
│ ├── main.dart # 应用入口文件
|
||||
│ ├── core/ # 核心功能模块
|
||||
│ │ ├── constants/ # 常量定义
|
||||
│ │ │ ├── app_constants.dart
|
||||
│ │ │ ├── api_constants.dart
|
||||
│ │ │ └── color_constants.dart
|
||||
│ │ ├── services/ # 核心服务
|
||||
│ │ │ ├── api_service.dart # API服务封装
|
||||
│ │ │ ├── auth_service.dart # 认证服务
|
||||
│ │ │ ├── storage_service.dart # 本地存储
|
||||
│ │ │ └── notification_service.dart # 通知服务
|
||||
│ │ ├── utils/ # 工具类
|
||||
│ │ │ ├── validators.dart # 表单验证
|
||||
│ │ │ ├── formatters.dart # 数据格式化
|
||||
│ │ │ └── helpers.dart # 辅助函数
|
||||
│ │ └── widgets/ # 通用组件
|
||||
│ │ ├── custom_button.dart
|
||||
│ │ ├── custom_text_field.dart
|
||||
│ │ ├── loading_widget.dart
|
||||
│ │ └── error_widget.dart
|
||||
│ ├── features/ # 功能模块
|
||||
│ │ ├── auth/ # 认证模块
|
||||
│ │ │ ├── models/ # 数据模型
|
||||
│ │ │ ├── providers/ # 状态管理
|
||||
│ │ │ ├── screens/ # 页面组件
|
||||
│ │ │ └── widgets/ # 功能组件
|
||||
│ │ ├── home/ # 主页模块
|
||||
│ │ ├── vocabulary/ # 词汇学习模块
|
||||
│ │ ├── listening/ # 听力训练模块
|
||||
│ │ ├── reading/ # 阅读理解模块
|
||||
│ │ ├── writing/ # 写作练习模块
|
||||
│ │ ├── speaking/ # 口语练习模块
|
||||
│ │ ├── profile/ # 个人中心模块
|
||||
│ │ └── settings/ # 设置模块
|
||||
│ ├── models/ # 全局数据模型
|
||||
│ │ ├── user_model.dart
|
||||
│ │ ├── vocabulary_model.dart
|
||||
│ │ ├── learning_model.dart
|
||||
│ │ └── response_model.dart
|
||||
│ ├── providers/ # 全局状态管理
|
||||
│ │ ├── auth_provider.dart
|
||||
│ │ ├── user_provider.dart
|
||||
│ │ ├── theme_provider.dart
|
||||
│ │ └── language_provider.dart
|
||||
│ ├── routes/ # 路由配置
|
||||
│ │ ├── app_routes.dart
|
||||
│ │ └── route_generator.dart
|
||||
│ └── themes/ # 主题配置
|
||||
│ ├── app_theme.dart
|
||||
│ ├── light_theme.dart
|
||||
│ └── dark_theme.dart
|
||||
├── assets/ # 静态资源
|
||||
│ ├── images/ # 图片资源
|
||||
│ ├── icons/ # 图标资源
|
||||
│ ├── fonts/ # 字体文件
|
||||
│ └── audio/ # 音频文件
|
||||
├── test/ # 测试文件
|
||||
├── pubspec.yaml # 依赖配置文件
|
||||
└── analysis_options.yaml # 代码分析配置
|
||||
```
|
||||
|
||||
### 后端项目结构(Go)
|
||||
|
||||
```
|
||||
serve/
|
||||
├── main.go # 应用入口文件
|
||||
├── start.sh # 启动脚本
|
||||
├── go.mod # Go模块依赖
|
||||
├── go.sum # 依赖校验文件
|
||||
├── Dockerfile # Docker构建文件
|
||||
├── .dockerignore # Docker忽略文件
|
||||
├── config/ # 配置管理
|
||||
│ ├── config.go # 配置结构定义
|
||||
│ └── config.yaml # 配置文件
|
||||
├── api/ # API层
|
||||
│ ├── router.go # 路由配置
|
||||
│ ├── middleware.go # 中间件配置
|
||||
│ └── handlers/ # 请求处理器
|
||||
│ ├── auth_handler.go # 认证处理
|
||||
│ ├── user_handler.go # 用户管理
|
||||
│ ├── vocabulary_handler.go # 词汇功能
|
||||
│ ├── listening_handler.go # 听力功能
|
||||
│ ├── reading_handler.go # 阅读功能
|
||||
│ ├── writing_handler.go # 写作功能
|
||||
│ ├── speaking_handler.go # 口语功能
|
||||
│ └── health_handler.go # 健康检查
|
||||
├── internal/ # 内部模块
|
||||
│ ├── common/ # 通用组件
|
||||
│ │ ├── errors.go # 错误定义
|
||||
│ │ └── response.go # 响应格式
|
||||
│ ├── database/ # 数据库层
|
||||
│ │ ├── database.go # 数据库连接
|
||||
│ │ ├── migrate.go # 数据库迁移
|
||||
│ │ └── seed.go # 数据初始化
|
||||
│ ├── handler/ # 业务处理器
|
||||
│ │ ├── ai_handler.go # AI服务处理
|
||||
│ │ └── upload_handler.go # 文件上传处理
|
||||
│ ├── logger/ # 日志系统
|
||||
│ │ └── logger.go # 日志配置
|
||||
│ ├── middleware/ # 中间件
|
||||
│ │ ├── auth.go # 认证中间件
|
||||
│ │ ├── cors.go # 跨域中间件
|
||||
│ │ ├── error_handler.go # 错误处理中间件
|
||||
│ │ └── logger.go # 日志中间件
|
||||
│ ├── models/ # 数据模型
|
||||
│ │ ├── user.go # 用户模型
|
||||
│ │ ├── vocabulary.go # 词汇模型
|
||||
│ │ ├── learning.go # 学习记录模型
|
||||
│ │ └── ai_models.go # AI相关模型
|
||||
│ ├── services/ # 业务服务层
|
||||
│ │ ├── user_service.go # 用户服务
|
||||
│ │ ├── vocabulary_service.go # 词汇服务
|
||||
│ │ ├── listening_service.go # 听力服务
|
||||
│ │ ├── reading_service.go # 阅读服务
|
||||
│ │ ├── writing_service.go # 写作服务
|
||||
│ │ ├── speaking_service.go # 口语服务
|
||||
│ │ ├── ai_service.go # AI服务
|
||||
│ │ └── upload_service.go # 文件上传服务
|
||||
│ └── utils/ # 工具函数
|
||||
│ └── utils.go # 通用工具
|
||||
└── uploads/ # 文件上传目录
|
||||
├── audio/ # 音频文件
|
||||
└── images/ # 图片文件
|
||||
```
|
||||
|
||||
### 核心技术架构
|
||||
|
||||
#### 前端架构特点
|
||||
- **模块化设计**:按功能模块组织代码,便于维护和扩展
|
||||
- **状态管理**:使用Provider进行全局状态管理
|
||||
- **组件复用**:通用组件和功能组件分离,提高代码复用性
|
||||
- **主题系统**:支持明暗主题切换,提供良好的用户体验
|
||||
- **路由管理**:统一的路由配置和导航管理
|
||||
- **响应式设计**:适配不同屏幕尺寸的设备
|
||||
|
||||
#### 后端架构特点
|
||||
- **分层架构**:Handler -> Service -> Model 的清晰分层
|
||||
- **中间件系统**:认证、日志、错误处理等中间件
|
||||
- **配置管理**:统一的配置文件和环境变量管理
|
||||
- **数据库设计**:GORM ORM框架,支持自动迁移和数据初始化
|
||||
- **API设计**:RESTful API设计,统一的响应格式
|
||||
- **日志系统**:结构化日志,支持不同级别和输出方式
|
||||
- **文件管理**:支持音频、图片等多媒体文件上传和管理
|
||||
|
||||
#### 数据库设计
|
||||
- **用户系统**:用户信息、偏好设置、社交链接
|
||||
- **词汇系统**:词汇分类、词汇定义、例句、图片
|
||||
- **学习记录**:听力、阅读、写作、口语的学习记录和进度
|
||||
- **AI服务**:AI相关的配置和使用记录
|
||||
- **关系设计**:合理的外键关系和索引优化
|
||||
|
||||
#### 安全特性
|
||||
- **JWT认证**:基于Token的无状态认证
|
||||
- **密码加密**:bcrypt加密存储用户密码
|
||||
- **CORS配置**:跨域请求安全控制
|
||||
- **输入验证**:前后端双重数据验证
|
||||
- **错误处理**:统一的错误处理和日志记录
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
- Flutter SDK 3.0+
|
||||
- Go 1.19+
|
||||
- MySQL 8.0+
|
||||
- Docker & Docker Compose
|
||||
- Node.js 16+ (用于部分工具)
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone https://github.com/your-org/ai_english_learning.git
|
||||
cd ai_english_learning
|
||||
```
|
||||
|
||||
2. **后端环境设置**
|
||||
```bash
|
||||
cd backend
|
||||
# 初始化Go模块
|
||||
go mod init ai_english_learning
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
3. **数据库设置**
|
||||
```bash
|
||||
# 启动MySQL数据库
|
||||
docker-compose up -d mysql
|
||||
|
||||
# 运行数据库迁移
|
||||
go run cmd/migrate/main.go
|
||||
```
|
||||
|
||||
4. **前端环境设置**
|
||||
```bash
|
||||
cd frontend
|
||||
flutter pub get
|
||||
flutter run
|
||||
```
|
||||
|
||||
5. **启动开发服务器**
|
||||
```bash
|
||||
# 后端API服务
|
||||
cd backend
|
||||
go run main.go
|
||||
|
||||
# AI服务
|
||||
celery -A ai_services worker --loglevel=info
|
||||
```
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 代码规范
|
||||
- 遵循Go官方代码规范(gofmt, golint)
|
||||
- 使用Flutter官方代码规范
|
||||
- 提交前运行代码格式化和静态检查
|
||||
|
||||
### 测试
|
||||
```bash
|
||||
# 后端测试
|
||||
go test ./...
|
||||
|
||||
# 前端测试
|
||||
flutter test
|
||||
```
|
||||
|
||||
### 部署
|
||||
```bash
|
||||
# 使用Docker Compose部署
|
||||
docker-compose up -d
|
||||
|
||||
# 生产环境部署
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
## 学习目标用户
|
||||
|
||||
### 学生群体
|
||||
- **小学生**:基础词汇学习,简单对话练习
|
||||
- **中学生**:考试备考,技能全面提升
|
||||
- **大学生**:四六级备考,学术英语提升
|
||||
- **研究生**:考研英语,学术写作训练
|
||||
|
||||
### 成人学习者
|
||||
- **职场人士**:商务英语,职业发展需求
|
||||
- **出国留学**:托福雅思备考,留学准备
|
||||
- **兴趣学习**:日常英语,文化交流
|
||||
- **专业提升**:行业英语,专业技能
|
||||
|
||||
## 学习效果
|
||||
|
||||
### 短期效果(1-3个月)
|
||||
- 词汇量显著增加(500-1500词)
|
||||
- 听力理解能力明显提升
|
||||
- 基础语法掌握更加牢固
|
||||
- 口语表达更加自信
|
||||
|
||||
### 中期效果(3-6个月)
|
||||
- 阅读速度和理解能力大幅提升
|
||||
- 写作表达更加地道和流畅
|
||||
- 口语交流基本无障碍
|
||||
- 考试成绩显著提高
|
||||
|
||||
### 长期效果(6个月以上)
|
||||
- 英语思维逐步建立
|
||||
- 能够进行复杂的英语交流
|
||||
- 具备独立的英语学习能力
|
||||
- 达到目标英语水平
|
||||
|
||||
## 贡献指南
|
||||
|
||||
我们欢迎社区贡献!请遵循以下步骤:
|
||||
|
||||
1. Fork 项目仓库
|
||||
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 创建 Pull Request
|
||||
|
||||
### 贡献类型
|
||||
- 🐛 Bug修复
|
||||
- ✨ 新功能开发
|
||||
- 📚 文档改进
|
||||
- 🎨 UI/UX优化
|
||||
- ⚡ 性能优化
|
||||
- 🧪 测试覆盖
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||
|
||||
## 联系我们
|
||||
|
||||
- **项目主页**:https://github.com/your-org/ai_english_learning
|
||||
- **问题反馈**:https://github.com/your-org/ai_english_learning/issues
|
||||
- **邮箱联系**:contact@ai-english-learning.com
|
||||
- **官方网站**:https://www.ai-english-learning.com
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2024-01-01)
|
||||
- 🎉 项目初始版本发布
|
||||
- ✨ 完整的词汇学习模块
|
||||
- ✨ 基础的听说读写功能
|
||||
- ✨ AI智能助手集成
|
||||
- ✨ 用户数据统计分析
|
||||
|
||||
### 即将发布
|
||||
- 🔄 更多AI功能集成
|
||||
- 📱 移动端应用优化
|
||||
- 🌐 多语言界面支持
|
||||
- 🎮 游戏化学习元素
|
||||
- 👥 社交学习功能
|
||||
|
||||
---
|
||||
|
||||
**让AI助力您的英语学习之旅!** 🚀
|
||||
|
||||
通过科学的学习方法和先进的AI技术,我们相信每个人都能够高效地掌握英语,实现自己的学习目标。立即开始您的智能英语学习体验吧!
|
||||
73
client/.dockerignore
Normal file
@@ -0,0 +1,73 @@
|
||||
# Git相关
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Flutter构建产物
|
||||
build/
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
|
||||
# IDE文件
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# 操作系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 测试文件
|
||||
test/
|
||||
tests/
|
||||
coverage/
|
||||
|
||||
# 文档
|
||||
*.md
|
||||
README*
|
||||
DOCS*
|
||||
docs/
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# 临时文件
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# 环境文件
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Android相关
|
||||
android/.gradle/
|
||||
android/app/build/
|
||||
android/build/
|
||||
android/gradle/
|
||||
android/gradlew
|
||||
android/gradlew.bat
|
||||
android/local.properties
|
||||
android/key.properties
|
||||
|
||||
# iOS相关
|
||||
ios/Pods/
|
||||
ios/Runner.xcworkspace/xcuserdata/
|
||||
ios/Runner.xcodeproj/xcuserdata/
|
||||
ios/Flutter/flutter_export_environment.sh
|
||||
|
||||
# Web相关(保留build/web用于生产构建)
|
||||
# build/web
|
||||
|
||||
# 其他
|
||||
*.bak
|
||||
*.backup
|
||||
node_modules/
|
||||
45
client/.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
45
client/.metadata
Normal file
@@ -0,0 +1,45 @@
|
||||
# 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: "05db9689081f091050f01aed79f04dce0c750154"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||
base_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||
- platform: android
|
||||
create_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||
base_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||
- platform: ios
|
||||
create_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||
base_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||
- platform: linux
|
||||
create_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||
base_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||
- platform: macos
|
||||
create_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||
base_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||
- platform: web
|
||||
create_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||
base_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||
- platform: windows
|
||||
create_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||
base_revision: 05db9689081f091050f01aed79f04dce0c750154
|
||||
|
||||
# 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'
|
||||
32
client/Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
||||
# 使用官方Flutter镜像作为构建环境
|
||||
FROM cirrusci/flutter:stable AS build
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制pubspec文件
|
||||
COPY pubspec.yaml pubspec.lock ./
|
||||
|
||||
# 获取依赖
|
||||
RUN flutter pub get
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建Web应用
|
||||
RUN flutter build web --release
|
||||
|
||||
# 使用nginx作为生产环境
|
||||
FROM nginx:alpine
|
||||
|
||||
# 复制构建产物到nginx目录
|
||||
COPY --from=build /app/build/web /usr/share/nginx/html
|
||||
|
||||
# 复制nginx配置
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 80
|
||||
|
||||
# 启动nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
186
client/ENVIRONMENT_CONFIG.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# 多环境配置说明
|
||||
|
||||
## 概述
|
||||
|
||||
前端应用现在支持多环境后端 API 配置,可以在开发、预发布和生产环境之间切换。
|
||||
|
||||
## 环境类型
|
||||
|
||||
### 1. 开发环境 (Development)
|
||||
- **默认 API 地址**: `http://localhost:8080/api/v1`
|
||||
- **Android 模拟器**: `http://10.0.2.2:8080/api/v1`
|
||||
- **用途**: 本地开发和测试
|
||||
|
||||
### 2. 预发布环境 (Staging)
|
||||
- **默认 API 地址**: `http://your-staging-domain.com/api/v1`
|
||||
- **用途**: 上线前测试
|
||||
|
||||
### 3. 生产环境 (Production)
|
||||
- **默认 API 地址**: `http://your-production-domain.com/api/v1`
|
||||
- **用途**: 正式上线
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 方法一:通过命令行参数设置
|
||||
|
||||
#### 开发环境
|
||||
```bash
|
||||
flutter run --dart-define=ENVIRONMENT=development
|
||||
```
|
||||
|
||||
#### 预发布环境
|
||||
```bash
|
||||
flutter run --dart-define=ENVIRONMENT=staging
|
||||
```
|
||||
|
||||
#### 生产环境
|
||||
```bash
|
||||
flutter run --dart-define=ENVIRONMENT=production
|
||||
```
|
||||
|
||||
#### 自定义 API 地址
|
||||
```bash
|
||||
flutter run --dart-define=API_BASE_URL=http://192.168.1.100:8080/api/v1
|
||||
```
|
||||
|
||||
### 方法二:通过开发者设置页面(推荐开发环境使用)
|
||||
|
||||
1. 在应用的设置页面找到"开发者设置"选项
|
||||
2. 选择目标环境或输入自定义 API 地址
|
||||
3. 保存设置并重启应用
|
||||
|
||||
## 构建配置
|
||||
|
||||
### Android 构建
|
||||
|
||||
#### 开发版本
|
||||
```bash
|
||||
flutter build apk --dart-define=ENVIRONMENT=development
|
||||
```
|
||||
|
||||
#### 生产版本
|
||||
```bash
|
||||
flutter build apk --dart-define=ENVIRONMENT=production --release
|
||||
```
|
||||
|
||||
### iOS 构建
|
||||
|
||||
#### 开发版本
|
||||
```bash
|
||||
flutter build ios --dart-define=ENVIRONMENT=development
|
||||
```
|
||||
|
||||
#### 生产版本
|
||||
```bash
|
||||
flutter build ios --dart-define=ENVIRONMENT=production --release
|
||||
```
|
||||
|
||||
### Web 构建
|
||||
|
||||
#### 开发版本
|
||||
```bash
|
||||
flutter build web --dart-define=ENVIRONMENT=development
|
||||
```
|
||||
|
||||
#### 生产版本
|
||||
```bash
|
||||
flutter build web --dart-define=ENVIRONMENT=production --release
|
||||
```
|
||||
|
||||
## 配置文件位置
|
||||
|
||||
环境配置文件位于:
|
||||
```
|
||||
lib/core/config/environment.dart
|
||||
```
|
||||
|
||||
## 自定义环境配置
|
||||
|
||||
如需修改环境配置,编辑 `environment.dart` 文件:
|
||||
|
||||
```dart
|
||||
static const Map<String, String> productionConfig = {
|
||||
'baseUrl': 'http://your-production-domain.com/api/v1',
|
||||
'wsUrl': 'ws://your-production-domain.com/ws',
|
||||
};
|
||||
```
|
||||
|
||||
## 常见场景
|
||||
|
||||
### 场景 1: 本地开发(Web)
|
||||
- **设备**: 开发电脑浏览器
|
||||
- **API 地址**: `http://localhost:8080/api/v1`
|
||||
- **运行命令**: `flutter run -d chrome`
|
||||
|
||||
### 场景 2: Android 模拟器开发
|
||||
- **设备**: Android 模拟器
|
||||
- **API 地址**: `http://10.0.2.2:8080/api/v1`
|
||||
- **运行命令**: `flutter run -d android`
|
||||
- **说明**: 10.0.2.2 是 Android 模拟器访问宿主机 localhost 的特殊地址
|
||||
|
||||
### 场景 3: 真机调试
|
||||
- **设备**: 手机真机
|
||||
- **API 地址**: `http://你的电脑IP:8080/api/v1`
|
||||
- **设置方法**:
|
||||
1. 确保手机和电脑在同一局域网
|
||||
2. 查看电脑 IP 地址(如 192.168.1.100)
|
||||
3. 在开发者设置中输入: `http://192.168.1.100:8080/api/v1`
|
||||
4. 或使用命令: `flutter run --dart-define=API_BASE_URL=http://192.168.1.100:8080/api/v1`
|
||||
|
||||
### 场景 4: 生产环境部署
|
||||
- **设备**: 正式用户设备
|
||||
- **API 地址**: 生产服务器地址
|
||||
- **构建命令**: `flutter build apk --dart-define=ENVIRONMENT=production --release`
|
||||
|
||||
## 环境检测
|
||||
|
||||
在代码中可以使用以下方法检测当前环境:
|
||||
|
||||
```dart
|
||||
import 'package:your_app/core/config/environment.dart';
|
||||
|
||||
// 检查是否为开发环境
|
||||
if (EnvironmentConfig.isDevelopment) {
|
||||
print('当前是开发环境');
|
||||
}
|
||||
|
||||
// 检查是否为生产环境
|
||||
if (EnvironmentConfig.isProduction) {
|
||||
print('当前是生产环境');
|
||||
}
|
||||
|
||||
// 获取当前 API 地址
|
||||
String apiUrl = EnvironmentConfig.baseUrl;
|
||||
print('API 地址: $apiUrl');
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **重启应用**: 修改环境配置后必须重启应用才能生效
|
||||
2. **生产环境**: 生产环境配置应该在构建时通过命令行参数指定,不要在代码中硬编码
|
||||
3. **安全性**: 不要在代码中提交敏感信息,如生产环境的真实 API 地址
|
||||
4. **测试**: 切换环境后应该进行充分测试,确保 API 连接正常
|
||||
5. **网络权限**: Android 需要在 `AndroidManifest.xml` 中添加网络权限
|
||||
6. **HTTPS**: 生产环境建议使用 HTTPS 协议
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题 1: Android 模拟器无法连接 localhost
|
||||
**解决方案**: 使用 `10.0.2.2` 代替 `localhost`
|
||||
|
||||
### 问题 2: 真机无法连接开发服务器
|
||||
**解决方案**:
|
||||
- 确保手机和电脑在同一网络
|
||||
- 检查防火墙设置
|
||||
- 使用电脑的局域网 IP 地址
|
||||
|
||||
### 问题 3: 环境切换后仍然连接旧地址
|
||||
**解决方案**: 完全关闭并重启应用
|
||||
|
||||
### 问题 4: iOS 模拟器无法连接
|
||||
**解决方案**: iOS 模拟器可以直接使用 `localhost`,无需特殊配置
|
||||
|
||||
## 扩展阅读
|
||||
|
||||
- [Flutter 环境变量配置](https://flutter.dev/docs/development/tools/sdk/overview#environment-variables)
|
||||
- [Dart 编译时常量](https://dart.dev/guides/language/language-tour#const-keyword)
|
||||
20
client/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# client
|
||||
|
||||
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.
|
||||
|
||||
|
||||
flutter build apk --dart-define=ENVIRONMENT=production --release
|
||||
flutter run -d chrome --web-port=3003
|
||||
28
client/analysis_options.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
14
client/android/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
43
client/android/app/build.gradle.kts
Normal file
@@ -0,0 +1,43 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.aienglish.learning"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// AI英语学习应用的唯一 Application ID
|
||||
applicationId = "com.aienglish.learning"
|
||||
// 支持 Android 5.0 (Lollipop) 及以上版本
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
7
client/android/app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
64
client/android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,64 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 网络权限 -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<!-- 音频录制权限 -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<!-- 存储权限 -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- Android 13+ 媒体权限 -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
|
||||
<!-- 唤醒锁权限(用于音频播放时保持屏幕唤醒) -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<application
|
||||
android:label="WOW Talk"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.aienglish.learning
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 140 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
client/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
client/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
client/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
18
client/android/app/src/main/res/values-night/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
4
client/android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
18
client/android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||
</domain-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system"/>
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
7
client/android/app/src/profile/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
24
client/android/build.gradle.kts
Normal file
@@ -0,0 +1,24 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
15
client/android/gradle.properties
Normal file
@@ -0,0 +1,15 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dhttps.protocols=TLSv1.2,TLSv1.3 -Djdk.tls.client.protocols=TLSv1.2,TLSv1.3
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
# Network and TLS configuration
|
||||
systemProp.https.protocols=TLSv1.2,TLSv1.3
|
||||
systemProp.jdk.tls.client.protocols=TLSv1.2,TLSv1.3
|
||||
systemProp.javax.net.ssl.trustStore=
|
||||
systemProp.javax.net.ssl.trustStorePassword=
|
||||
|
||||
# Gradle daemon configuration
|
||||
org.gradle.daemon=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.configureondemand=true
|
||||
org.gradle.caching=true
|
||||
5
client/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||
26
client/android/settings.gradle.kts
Normal file
@@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.9.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
BIN
client/assets/images/logo.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
85
client/build_prod.bat
Normal file
@@ -0,0 +1,85 @@
|
||||
@echo off
|
||||
REM 生产环境构建脚本 (Windows)
|
||||
|
||||
echo ========================================
|
||||
echo Building AI English Learning App for Production
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
:menu
|
||||
echo Please select build target:
|
||||
echo 1. Android APK
|
||||
echo 2. Android App Bundle (AAB)
|
||||
echo 3. iOS
|
||||
echo 4. Web
|
||||
echo 5. Windows
|
||||
echo 6. Exit
|
||||
echo.
|
||||
set /p choice=Enter your choice (1-6):
|
||||
|
||||
if "%choice%"=="1" goto android_apk
|
||||
if "%choice%"=="2" goto android_aab
|
||||
if "%choice%"=="3" goto ios
|
||||
if "%choice%"=="4" goto web
|
||||
if "%choice%"=="5" goto windows
|
||||
if "%choice%"=="6" goto end
|
||||
echo Invalid choice. Please try again.
|
||||
echo.
|
||||
goto menu
|
||||
|
||||
:android_apk
|
||||
echo.
|
||||
echo Building Android APK...
|
||||
flutter build apk --dart-define=ENVIRONMENT=production --release
|
||||
echo.
|
||||
echo Build completed! APK location:
|
||||
echo build\app\outputs\flutter-apk\app-release.apk
|
||||
echo.
|
||||
pause
|
||||
goto end
|
||||
|
||||
:android_aab
|
||||
echo.
|
||||
echo Building Android App Bundle...
|
||||
flutter build appbundle --dart-define=ENVIRONMENT=production --release
|
||||
echo.
|
||||
echo Build completed! AAB location:
|
||||
echo build\app\outputs\bundle\release\app-release.aab
|
||||
echo.
|
||||
pause
|
||||
goto end
|
||||
|
||||
:ios
|
||||
echo.
|
||||
echo Building iOS...
|
||||
flutter build ios --dart-define=ENVIRONMENT=production --release
|
||||
echo.
|
||||
echo Build completed! Please open Xcode to archive and distribute.
|
||||
echo.
|
||||
pause
|
||||
goto end
|
||||
|
||||
:web
|
||||
echo.
|
||||
echo Building Web...
|
||||
flutter build web --dart-define=ENVIRONMENT=production --release
|
||||
echo.
|
||||
echo Build completed! Web files location:
|
||||
echo build\web
|
||||
echo.
|
||||
pause
|
||||
goto end
|
||||
|
||||
:windows
|
||||
echo.
|
||||
echo Building Windows...
|
||||
flutter build windows --dart-define=ENVIRONMENT=production --release
|
||||
echo.
|
||||
echo Build completed! Windows executable location:
|
||||
echo build\windows\runner\Release
|
||||
echo.
|
||||
pause
|
||||
goto end
|
||||
|
||||
:end
|
||||
exit
|
||||
3
client/devtools_options.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
34
client/ios/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
**/dgph
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/ephemeral/
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
26
client/ios/Flutter/AppFrameworkInfo.plist
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
1
client/ios/Flutter/Debug.xcconfig
Normal file
@@ -0,0 +1 @@
|
||||
#include "Generated.xcconfig"
|
||||
1
client/ios/Flutter/Release.xcconfig
Normal file
@@ -0,0 +1 @@
|
||||
#include "Generated.xcconfig"
|
||||
616
client/ios/Runner.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,616 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C8080294A63A400263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C807F294A63A400263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
331C807D294A63A400263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.client;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.client.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.client.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.client.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.client;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.client;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
331C8088294A63A400263BE5 /* Debug */,
|
||||
331C8089294A63A400263BE5 /* Release */,
|
||||
331C808A294A63A400263BE5 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147031CF9000F007C117D /* Debug */,
|
||||
97C147041CF9000F007C117D /* Release */,
|
||||
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147061CF9000F007C117D /* Debug */,
|
||||
97C147071CF9000F007C117D /* Release */,
|
||||
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
7
client/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
7
client/ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
13
client/ios/Runner/AppDelegate.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 553 KiB |
|
After Width: | Height: | Size: 895 B |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 27 KiB |
23
client/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
client/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||
37
client/ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
||||
26
client/ios/Runner/Base.lproj/Main.storyboard
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
49
client/ios/Runner/Info.plist
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Client</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>client</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
1
client/ios/Runner/Runner-Bridging-Header.h
Normal file
@@ -0,0 +1 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
12
client/ios/RunnerTests/RunnerTests.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
class RunnerTests: XCTestCase {
|
||||
|
||||
func testExample() {
|
||||
// If you add code to the Runner application, consider adding tests here.
|
||||
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||
}
|
||||
|
||||
}
|
||||
133
client/lib/core/config/environment.dart
Normal file
@@ -0,0 +1,133 @@
|
||||
/// 环境配置
|
||||
enum Environment {
|
||||
development,
|
||||
staging,
|
||||
production,
|
||||
}
|
||||
|
||||
/// 环境配置管理
|
||||
class EnvironmentConfig {
|
||||
static Environment _currentEnvironment = Environment.development;
|
||||
|
||||
/// 获取当前环境
|
||||
static Environment get current => _currentEnvironment;
|
||||
|
||||
/// 设置当前环境
|
||||
static void setEnvironment(Environment env) {
|
||||
_currentEnvironment = env;
|
||||
}
|
||||
|
||||
/// 从字符串设置环境
|
||||
static void setEnvironmentFromString(String? envString) {
|
||||
switch (envString?.toLowerCase()) {
|
||||
case 'production':
|
||||
case 'prod':
|
||||
_currentEnvironment = Environment.production;
|
||||
break;
|
||||
case 'staging':
|
||||
case 'stage':
|
||||
_currentEnvironment = Environment.staging;
|
||||
break;
|
||||
case 'development':
|
||||
case 'dev':
|
||||
default:
|
||||
_currentEnvironment = Environment.development;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前环境的API基础URL
|
||||
static String get baseUrl {
|
||||
switch (_currentEnvironment) {
|
||||
case Environment.production:
|
||||
return 'https://loukao.cn/api/v1';
|
||||
case Environment.staging:
|
||||
return 'http://localhost:8080/api/v1';
|
||||
case Environment.development:
|
||||
default:
|
||||
// 开发环境:localhost 用于 Web,10.0.2.2 用于 Android 模拟器
|
||||
return const String.fromEnvironment(
|
||||
'API_BASE_URL',
|
||||
defaultValue: 'http://localhost:8080/api/v1',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取环境名称
|
||||
static String get environmentName {
|
||||
switch (_currentEnvironment) {
|
||||
case Environment.production:
|
||||
return 'Production';
|
||||
case Environment.staging:
|
||||
return 'Staging';
|
||||
case Environment.development:
|
||||
return 'Development';
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否为开发环境
|
||||
static bool get isDevelopment => _currentEnvironment == Environment.development;
|
||||
|
||||
/// 是否为生产环境
|
||||
static bool get isProduction => _currentEnvironment == Environment.production;
|
||||
|
||||
/// 是否为预发布环境
|
||||
static bool get isStaging => _currentEnvironment == Environment.staging;
|
||||
|
||||
/// 开发环境配置
|
||||
static const Map<String, String> developmentConfig = {
|
||||
'baseUrl': 'http://localhost:8080/api/v1',
|
||||
'baseUrlAndroid': 'http://10.0.2.2:8080/api/v1',
|
||||
'wsUrl': 'ws://localhost:8080/ws',
|
||||
};
|
||||
|
||||
/// 预发布环境配置
|
||||
static const Map<String, String> stagingConfig = {
|
||||
'baseUrl': 'https://loukao.cn/api/v1',
|
||||
'wsUrl': 'ws://your-staging-domain.com/ws',
|
||||
};
|
||||
|
||||
/// 生产环境配置
|
||||
static const Map<String, String> productionConfig = {
|
||||
'baseUrl': 'https://loukao.cn/api/v1',
|
||||
'wsUrl': 'ws://your-production-domain.com/ws',
|
||||
};
|
||||
|
||||
/// 获取当前环境配置
|
||||
static Map<String, String> get config {
|
||||
switch (_currentEnvironment) {
|
||||
case Environment.production:
|
||||
return productionConfig;
|
||||
case Environment.staging:
|
||||
return stagingConfig;
|
||||
case Environment.development:
|
||||
default:
|
||||
return developmentConfig;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取WebSocket URL
|
||||
static String get wsUrl {
|
||||
return config['wsUrl'] ?? '';
|
||||
}
|
||||
|
||||
/// 获取连接超时时间(毫秒)
|
||||
static int get connectTimeout {
|
||||
return isProduction ? 10000 : 30000;
|
||||
}
|
||||
|
||||
/// 获取接收超时时间(毫秒)
|
||||
static int get receiveTimeout {
|
||||
return isProduction ? 10000 : 30000;
|
||||
}
|
||||
|
||||
/// 是否启用日志
|
||||
static bool get enableLogging {
|
||||
return !isProduction;
|
||||
}
|
||||
|
||||
/// 是否启用调试模式
|
||||
static bool get debugMode {
|
||||
return isDevelopment;
|
||||
}
|
||||
}
|
||||
88
client/lib/core/constants/app_constants.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
import '../config/environment.dart';
|
||||
|
||||
/// 应用常量配置
|
||||
class AppConstants {
|
||||
// 应用信息
|
||||
static const String appName = 'AI英语学习';
|
||||
static const String appVersion = '1.0.0';
|
||||
|
||||
// API配置 - 从环境配置获取
|
||||
static String get baseUrl => EnvironmentConfig.baseUrl;
|
||||
static int get connectTimeout => EnvironmentConfig.connectTimeout;
|
||||
static int get receiveTimeout => EnvironmentConfig.receiveTimeout;
|
||||
|
||||
// 存储键名
|
||||
static const String accessTokenKey = 'access_token';
|
||||
static const String refreshTokenKey = 'refresh_token';
|
||||
static const String userInfoKey = 'user_info';
|
||||
static const String settingsKey = 'app_settings';
|
||||
|
||||
// 分页配置
|
||||
static const int defaultPageSize = 20;
|
||||
static const int maxPageSize = 100;
|
||||
|
||||
// 学习配置
|
||||
static const int dailyWordGoal = 50;
|
||||
static const int maxRetryAttempts = 3;
|
||||
static const Duration studySessionDuration = Duration(minutes: 25);
|
||||
|
||||
// 音频配置
|
||||
static const double defaultPlaybackSpeed = 1.0;
|
||||
static const double minPlaybackSpeed = 0.5;
|
||||
static const double maxPlaybackSpeed = 2.0;
|
||||
|
||||
// 图片配置
|
||||
static const int maxImageSize = 5 * 1024 * 1024; // 5MB
|
||||
static const List<String> supportedImageFormats = ['jpg', 'jpeg', 'png', 'webp'];
|
||||
|
||||
// 缓存配置
|
||||
static const Duration cacheExpiration = Duration(hours: 24);
|
||||
static const int maxCacheSize = 100 * 1024 * 1024; // 100MB
|
||||
}
|
||||
|
||||
/// 路由常量
|
||||
class RouteConstants {
|
||||
static const String splash = '/splash';
|
||||
static const String onboarding = '/onboarding';
|
||||
static const String login = '/login';
|
||||
static const String register = '/register';
|
||||
static const String home = '/home';
|
||||
static const String profile = '/profile';
|
||||
static const String vocabulary = '/vocabulary';
|
||||
static const String vocabularyTest = '/vocabulary/test';
|
||||
static const String listening = '/listening';
|
||||
static const String reading = '/reading';
|
||||
static const String writing = '/writing';
|
||||
static const String speaking = '/speaking';
|
||||
static const String settings = '/settings';
|
||||
}
|
||||
|
||||
/// 学习等级常量
|
||||
enum LearningLevel {
|
||||
beginner('beginner', '初级'),
|
||||
intermediate('intermediate', '中级'),
|
||||
advanced('advanced', '高级');
|
||||
|
||||
const LearningLevel(this.value, this.label);
|
||||
|
||||
final String value;
|
||||
final String label;
|
||||
}
|
||||
|
||||
/// 词库类型常量
|
||||
enum VocabularyType {
|
||||
elementary('elementary', '小学'),
|
||||
junior('junior', '初中'),
|
||||
senior('senior', '高中'),
|
||||
cet4('cet4', '四级'),
|
||||
cet6('cet6', '六级'),
|
||||
toefl('toefl', '托福'),
|
||||
ielts('ielts', '雅思'),
|
||||
business('business', '商务'),
|
||||
daily('daily', '日常');
|
||||
|
||||
const VocabularyType(this.value, this.label);
|
||||
|
||||
final String value;
|
||||
final String label;
|
||||
}
|
||||
299
client/lib/core/errors/app_error.dart
Normal file
@@ -0,0 +1,299 @@
|
||||
/// 应用错误基类
|
||||
abstract class AppError implements Exception {
|
||||
final String message;
|
||||
final String? code;
|
||||
final dynamic originalError;
|
||||
|
||||
const AppError({
|
||||
required this.message,
|
||||
this.code,
|
||||
this.originalError,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppError(message: $message, code: $code)';
|
||||
}
|
||||
}
|
||||
|
||||
/// 网络错误
|
||||
class NetworkError extends AppError {
|
||||
const NetworkError({
|
||||
required super.message,
|
||||
super.code,
|
||||
super.originalError,
|
||||
});
|
||||
|
||||
factory NetworkError.connectionTimeout() {
|
||||
return const NetworkError(
|
||||
message: '连接超时,请检查网络连接',
|
||||
code: 'CONNECTION_TIMEOUT',
|
||||
);
|
||||
}
|
||||
|
||||
factory NetworkError.noInternet() {
|
||||
return const NetworkError(
|
||||
message: '网络连接不可用,请检查网络设置',
|
||||
code: 'NO_INTERNET',
|
||||
);
|
||||
}
|
||||
|
||||
factory NetworkError.serverError(int statusCode, [String? message]) {
|
||||
return NetworkError(
|
||||
message: message ?? '服务器错误 ($statusCode)',
|
||||
code: 'SERVER_ERROR_$statusCode',
|
||||
);
|
||||
}
|
||||
|
||||
factory NetworkError.unknown([dynamic error]) {
|
||||
return NetworkError(
|
||||
message: '网络请求失败',
|
||||
code: 'UNKNOWN_NETWORK_ERROR',
|
||||
originalError: error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 认证错误
|
||||
class AuthError extends AppError {
|
||||
const AuthError({
|
||||
required super.message,
|
||||
super.code,
|
||||
super.originalError,
|
||||
});
|
||||
|
||||
factory AuthError.unauthorized() {
|
||||
return const AuthError(
|
||||
message: '未授权访问,请重新登录',
|
||||
code: 'UNAUTHORIZED',
|
||||
);
|
||||
}
|
||||
|
||||
factory AuthError.tokenExpired() {
|
||||
return const AuthError(
|
||||
message: '登录已过期,请重新登录',
|
||||
code: 'TOKEN_EXPIRED',
|
||||
);
|
||||
}
|
||||
|
||||
factory AuthError.invalidCredentials() {
|
||||
return const AuthError(
|
||||
message: '用户名或密码错误',
|
||||
code: 'INVALID_CREDENTIALS',
|
||||
);
|
||||
}
|
||||
|
||||
factory AuthError.accountLocked() {
|
||||
return const AuthError(
|
||||
message: '账户已被锁定,请联系客服',
|
||||
code: 'ACCOUNT_LOCKED',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证错误
|
||||
class ValidationError extends AppError {
|
||||
final Map<String, List<String>>? fieldErrors;
|
||||
|
||||
const ValidationError({
|
||||
required super.message,
|
||||
super.code,
|
||||
super.originalError,
|
||||
this.fieldErrors,
|
||||
});
|
||||
|
||||
factory ValidationError.required(String field) {
|
||||
return ValidationError(
|
||||
message: '$field不能为空',
|
||||
code: 'FIELD_REQUIRED',
|
||||
fieldErrors: {field: ['不能为空']},
|
||||
);
|
||||
}
|
||||
|
||||
factory ValidationError.invalid(String field, String reason) {
|
||||
return ValidationError(
|
||||
message: '$field格式不正确:$reason',
|
||||
code: 'FIELD_INVALID',
|
||||
fieldErrors: {field: [reason]},
|
||||
);
|
||||
}
|
||||
|
||||
factory ValidationError.multiple(Map<String, List<String>> errors) {
|
||||
return ValidationError(
|
||||
message: '表单验证失败',
|
||||
code: 'VALIDATION_FAILED',
|
||||
fieldErrors: errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 业务逻辑错误
|
||||
class BusinessError extends AppError {
|
||||
const BusinessError({
|
||||
required super.message,
|
||||
super.code,
|
||||
super.originalError,
|
||||
});
|
||||
|
||||
factory BusinessError.notFound(String resource) {
|
||||
return BusinessError(
|
||||
message: '$resource不存在',
|
||||
code: 'RESOURCE_NOT_FOUND',
|
||||
);
|
||||
}
|
||||
|
||||
factory BusinessError.alreadyExists(String resource) {
|
||||
return BusinessError(
|
||||
message: '$resource已存在',
|
||||
code: 'RESOURCE_ALREADY_EXISTS',
|
||||
);
|
||||
}
|
||||
|
||||
factory BusinessError.operationNotAllowed(String operation) {
|
||||
return BusinessError(
|
||||
message: '不允许执行操作:$operation',
|
||||
code: 'OPERATION_NOT_ALLOWED',
|
||||
);
|
||||
}
|
||||
|
||||
factory BusinessError.quotaExceeded(String resource) {
|
||||
return BusinessError(
|
||||
message: '$resource配额已用完',
|
||||
code: 'QUOTA_EXCEEDED',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 存储错误
|
||||
class StorageError extends AppError {
|
||||
const StorageError({
|
||||
required super.message,
|
||||
super.code,
|
||||
super.originalError,
|
||||
});
|
||||
|
||||
factory StorageError.readFailed(String key) {
|
||||
return StorageError(
|
||||
message: '读取数据失败:$key',
|
||||
code: 'STORAGE_READ_FAILED',
|
||||
);
|
||||
}
|
||||
|
||||
factory StorageError.writeFailed(String key) {
|
||||
return StorageError(
|
||||
message: '写入数据失败:$key',
|
||||
code: 'STORAGE_WRITE_FAILED',
|
||||
);
|
||||
}
|
||||
|
||||
factory StorageError.notInitialized() {
|
||||
return const StorageError(
|
||||
message: '存储服务未初始化',
|
||||
code: 'STORAGE_NOT_INITIALIZED',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 文件错误
|
||||
class FileError extends AppError {
|
||||
const FileError({
|
||||
required super.message,
|
||||
super.code,
|
||||
super.originalError,
|
||||
});
|
||||
|
||||
factory FileError.notFound(String path) {
|
||||
return FileError(
|
||||
message: '文件不存在:$path',
|
||||
code: 'FILE_NOT_FOUND',
|
||||
);
|
||||
}
|
||||
|
||||
factory FileError.accessDenied(String path) {
|
||||
return FileError(
|
||||
message: '文件访问被拒绝:$path',
|
||||
code: 'FILE_ACCESS_DENIED',
|
||||
);
|
||||
}
|
||||
|
||||
factory FileError.formatNotSupported(String format) {
|
||||
return FileError(
|
||||
message: '不支持的文件格式:$format',
|
||||
code: 'FILE_FORMAT_NOT_SUPPORTED',
|
||||
);
|
||||
}
|
||||
|
||||
factory FileError.sizeTooLarge(int size, int maxSize) {
|
||||
return FileError(
|
||||
message: '文件大小超出限制:${size}B > ${maxSize}B',
|
||||
code: 'FILE_SIZE_TOO_LARGE',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 音频错误
|
||||
class AudioError extends AppError {
|
||||
const AudioError({
|
||||
required super.message,
|
||||
super.code,
|
||||
super.originalError,
|
||||
});
|
||||
|
||||
factory AudioError.playbackFailed() {
|
||||
return const AudioError(
|
||||
message: '音频播放失败',
|
||||
code: 'AUDIO_PLAYBACK_FAILED',
|
||||
);
|
||||
}
|
||||
|
||||
factory AudioError.recordingFailed() {
|
||||
return const AudioError(
|
||||
message: '音频录制失败',
|
||||
code: 'AUDIO_RECORDING_FAILED',
|
||||
);
|
||||
}
|
||||
|
||||
factory AudioError.permissionDenied() {
|
||||
return const AudioError(
|
||||
message: '音频权限被拒绝',
|
||||
code: 'AUDIO_PERMISSION_DENIED',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 学习相关错误
|
||||
class LearningError extends AppError {
|
||||
const LearningError({
|
||||
required super.message,
|
||||
super.code,
|
||||
super.originalError,
|
||||
});
|
||||
|
||||
factory LearningError.progressNotFound() {
|
||||
return const LearningError(
|
||||
message: '学习进度不存在',
|
||||
code: 'LEARNING_PROGRESS_NOT_FOUND',
|
||||
);
|
||||
}
|
||||
|
||||
factory LearningError.vocabularyNotFound() {
|
||||
return const LearningError(
|
||||
message: '词汇不存在',
|
||||
code: 'VOCABULARY_NOT_FOUND',
|
||||
);
|
||||
}
|
||||
|
||||
factory LearningError.testNotCompleted() {
|
||||
return const LearningError(
|
||||
message: '测试未完成',
|
||||
code: 'TEST_NOT_COMPLETED',
|
||||
);
|
||||
}
|
||||
|
||||
factory LearningError.levelNotUnlocked() {
|
||||
return const LearningError(
|
||||
message: '等级未解锁',
|
||||
code: 'LEVEL_NOT_UNLOCKED',
|
||||
);
|
||||
}
|
||||
}
|
||||
62
client/lib/core/errors/app_exception.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
/// 应用异常基类
|
||||
class AppException implements Exception {
|
||||
final String message;
|
||||
final String? code;
|
||||
final dynamic details;
|
||||
|
||||
const AppException(
|
||||
this.message, {
|
||||
this.code,
|
||||
this.details,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppException: $message';
|
||||
}
|
||||
}
|
||||
|
||||
/// 网络异常
|
||||
class NetworkException extends AppException {
|
||||
const NetworkException(
|
||||
super.message, {
|
||||
super.code,
|
||||
super.details,
|
||||
});
|
||||
}
|
||||
|
||||
/// 认证异常
|
||||
class AuthException extends AppException {
|
||||
const AuthException(
|
||||
super.message, {
|
||||
super.code,
|
||||
super.details,
|
||||
});
|
||||
}
|
||||
|
||||
/// 服务器异常
|
||||
class ServerException extends AppException {
|
||||
const ServerException(
|
||||
super.message, {
|
||||
super.code,
|
||||
super.details,
|
||||
});
|
||||
}
|
||||
|
||||
/// 缓存异常
|
||||
class CacheException extends AppException {
|
||||
const CacheException(
|
||||
super.message, {
|
||||
super.code,
|
||||
super.details,
|
||||
});
|
||||
}
|
||||
|
||||
/// 验证异常
|
||||
class ValidationException extends AppException {
|
||||
const ValidationException(
|
||||
super.message, {
|
||||
super.code,
|
||||
super.details,
|
||||
});
|
||||
}
|
||||
121
client/lib/core/models/api_response.dart
Normal file
@@ -0,0 +1,121 @@
|
||||
/// API响应基础模型
|
||||
class ApiResponse<T> {
|
||||
final bool success;
|
||||
final String message;
|
||||
final T? data;
|
||||
final int? code;
|
||||
final Map<String, dynamic>? errors;
|
||||
|
||||
const ApiResponse({
|
||||
required this.success,
|
||||
required this.message,
|
||||
this.data,
|
||||
this.code,
|
||||
this.errors,
|
||||
});
|
||||
|
||||
factory ApiResponse.success({
|
||||
required String message,
|
||||
T? data,
|
||||
int? code,
|
||||
}) {
|
||||
return ApiResponse<T>(
|
||||
success: true,
|
||||
message: message,
|
||||
data: data,
|
||||
code: code ?? 200,
|
||||
);
|
||||
}
|
||||
|
||||
factory ApiResponse.error({
|
||||
required String message,
|
||||
int? code,
|
||||
Map<String, dynamic>? errors,
|
||||
}) {
|
||||
return ApiResponse<T>(
|
||||
success: false,
|
||||
message: message,
|
||||
code: code ?? 400,
|
||||
errors: errors,
|
||||
);
|
||||
}
|
||||
|
||||
factory ApiResponse.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
T Function(dynamic)? fromJsonT,
|
||||
) {
|
||||
return ApiResponse<T>(
|
||||
success: json['success'] ?? false,
|
||||
message: json['message'] ?? '',
|
||||
data: json['data'] != null && fromJsonT != null
|
||||
? fromJsonT(json['data'])
|
||||
: json['data'],
|
||||
code: json['code'],
|
||||
errors: json['errors'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'success': success,
|
||||
'message': message,
|
||||
'data': data,
|
||||
'code': code,
|
||||
'errors': errors,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ApiResponse{success: $success, message: $message, data: $data, code: $code}';
|
||||
}
|
||||
}
|
||||
|
||||
/// 分页响应模型
|
||||
class PaginatedResponse<T> {
|
||||
final List<T> data;
|
||||
final int total;
|
||||
final int page;
|
||||
final int pageSize;
|
||||
final int totalPages;
|
||||
final bool hasNext;
|
||||
final bool hasPrevious;
|
||||
|
||||
const PaginatedResponse({
|
||||
required this.data,
|
||||
required this.total,
|
||||
required this.page,
|
||||
required this.pageSize,
|
||||
required this.totalPages,
|
||||
required this.hasNext,
|
||||
required this.hasPrevious,
|
||||
});
|
||||
|
||||
factory PaginatedResponse.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
T Function(Map<String, dynamic>) fromJsonT,
|
||||
) {
|
||||
final List<dynamic> dataList = json['data'] ?? [];
|
||||
return PaginatedResponse<T>(
|
||||
data: dataList.map((item) => fromJsonT(item)).toList(),
|
||||
total: json['total'] ?? 0,
|
||||
page: json['page'] ?? 1,
|
||||
pageSize: json['page_size'] ?? 10,
|
||||
totalPages: json['total_pages'] ?? 0,
|
||||
hasNext: json['has_next'] ?? false,
|
||||
hasPrevious: json['has_previous'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'data': data,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'page_size': pageSize,
|
||||
'total_pages': totalPages,
|
||||
'has_next': hasNext,
|
||||
'has_previous': hasPrevious,
|
||||
};
|
||||
}
|
||||
}
|
||||
280
client/lib/core/models/user_model.dart
Normal file
@@ -0,0 +1,280 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'user_model.g.dart';
|
||||
|
||||
/// 用户模型
|
||||
@JsonSerializable()
|
||||
class User {
|
||||
final String id;
|
||||
final String username;
|
||||
final String email;
|
||||
final String? phone;
|
||||
final String? avatar;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final UserProfile? profile;
|
||||
final UserSettings? settings;
|
||||
|
||||
const User({
|
||||
required this.id,
|
||||
required this.username,
|
||||
required this.email,
|
||||
this.phone,
|
||||
this.avatar,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.profile,
|
||||
this.settings,
|
||||
});
|
||||
|
||||
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$UserToJson(this);
|
||||
|
||||
User copyWith({
|
||||
String? id,
|
||||
String? username,
|
||||
String? email,
|
||||
String? phone,
|
||||
String? avatar,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
UserProfile? profile,
|
||||
UserSettings? settings,
|
||||
}) {
|
||||
return User(
|
||||
id: id ?? this.id,
|
||||
username: username ?? this.username,
|
||||
email: email ?? this.email,
|
||||
phone: phone ?? this.phone,
|
||||
avatar: avatar ?? this.avatar,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
profile: profile ?? this.profile,
|
||||
settings: settings ?? this.settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户资料
|
||||
@JsonSerializable()
|
||||
class UserProfile {
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final String? phone;
|
||||
final String? bio;
|
||||
final String? avatar;
|
||||
final String? realName;
|
||||
final String? gender;
|
||||
final DateTime? birthday;
|
||||
final String? location;
|
||||
final String? occupation;
|
||||
final String? education;
|
||||
final List<String>? interests;
|
||||
final LearningGoal? learningGoal;
|
||||
final EnglishLevel? currentLevel;
|
||||
final EnglishLevel? targetLevel;
|
||||
final EnglishLevel? englishLevel;
|
||||
final UserSettings? settings;
|
||||
|
||||
const UserProfile({
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.phone,
|
||||
this.bio,
|
||||
this.avatar,
|
||||
this.realName,
|
||||
this.gender,
|
||||
this.birthday,
|
||||
this.location,
|
||||
this.occupation,
|
||||
this.education,
|
||||
this.interests,
|
||||
this.learningGoal,
|
||||
this.currentLevel,
|
||||
this.targetLevel,
|
||||
this.englishLevel,
|
||||
this.settings,
|
||||
});
|
||||
|
||||
factory UserProfile.fromJson(Map<String, dynamic> json) => _$UserProfileFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$UserProfileToJson(this);
|
||||
|
||||
UserProfile copyWith({
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
String? phone,
|
||||
String? bio,
|
||||
String? avatar,
|
||||
String? realName,
|
||||
String? gender,
|
||||
DateTime? birthday,
|
||||
String? location,
|
||||
String? occupation,
|
||||
String? education,
|
||||
List<String>? interests,
|
||||
LearningGoal? learningGoal,
|
||||
EnglishLevel? currentLevel,
|
||||
EnglishLevel? targetLevel,
|
||||
EnglishLevel? englishLevel,
|
||||
UserSettings? settings,
|
||||
}) {
|
||||
return UserProfile(
|
||||
firstName: firstName ?? this.firstName,
|
||||
lastName: lastName ?? this.lastName,
|
||||
phone: phone ?? this.phone,
|
||||
bio: bio ?? this.bio,
|
||||
avatar: avatar ?? this.avatar,
|
||||
realName: realName ?? this.realName,
|
||||
gender: gender ?? this.gender,
|
||||
birthday: birthday ?? this.birthday,
|
||||
location: location ?? this.location,
|
||||
occupation: occupation ?? this.occupation,
|
||||
education: education ?? this.education,
|
||||
interests: interests ?? this.interests,
|
||||
learningGoal: learningGoal ?? this.learningGoal,
|
||||
currentLevel: currentLevel ?? this.currentLevel,
|
||||
targetLevel: targetLevel ?? this.targetLevel,
|
||||
englishLevel: englishLevel ?? this.englishLevel,
|
||||
settings: settings ?? this.settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户设置
|
||||
@JsonSerializable()
|
||||
class UserSettings {
|
||||
final bool notificationsEnabled;
|
||||
final bool soundEnabled;
|
||||
final bool vibrationEnabled;
|
||||
final String language;
|
||||
final String theme;
|
||||
final int dailyGoal;
|
||||
final int dailyWordGoal;
|
||||
final int dailyStudyMinutes;
|
||||
final List<String> reminderTimes;
|
||||
final bool autoPlayAudio;
|
||||
final double audioSpeed;
|
||||
final bool showTranslation;
|
||||
final bool showPronunciation;
|
||||
|
||||
const UserSettings({
|
||||
this.notificationsEnabled = true,
|
||||
this.soundEnabled = true,
|
||||
this.vibrationEnabled = true,
|
||||
this.language = 'zh-CN',
|
||||
this.theme = 'system',
|
||||
this.dailyGoal = 30,
|
||||
this.dailyWordGoal = 20,
|
||||
this.dailyStudyMinutes = 30,
|
||||
this.reminderTimes = const ['09:00', '20:00'],
|
||||
this.autoPlayAudio = true,
|
||||
this.audioSpeed = 1.0,
|
||||
this.showTranslation = true,
|
||||
this.showPronunciation = true,
|
||||
});
|
||||
|
||||
factory UserSettings.fromJson(Map<String, dynamic> json) => _$UserSettingsFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$UserSettingsToJson(this);
|
||||
|
||||
UserSettings copyWith({
|
||||
bool? notificationsEnabled,
|
||||
bool? soundEnabled,
|
||||
bool? vibrationEnabled,
|
||||
String? language,
|
||||
String? theme,
|
||||
int? dailyGoal,
|
||||
int? dailyWordGoal,
|
||||
int? dailyStudyMinutes,
|
||||
List<String>? reminderTimes,
|
||||
bool? autoPlayAudio,
|
||||
double? audioSpeed,
|
||||
bool? showTranslation,
|
||||
bool? showPronunciation,
|
||||
}) {
|
||||
return UserSettings(
|
||||
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
|
||||
soundEnabled: soundEnabled ?? this.soundEnabled,
|
||||
vibrationEnabled: vibrationEnabled ?? this.vibrationEnabled,
|
||||
language: language ?? this.language,
|
||||
theme: theme ?? this.theme,
|
||||
dailyGoal: dailyGoal ?? this.dailyGoal,
|
||||
dailyWordGoal: dailyWordGoal ?? this.dailyWordGoal,
|
||||
dailyStudyMinutes: dailyStudyMinutes ?? this.dailyStudyMinutes,
|
||||
reminderTimes: reminderTimes ?? this.reminderTimes,
|
||||
autoPlayAudio: autoPlayAudio ?? this.autoPlayAudio,
|
||||
audioSpeed: audioSpeed ?? this.audioSpeed,
|
||||
showTranslation: showTranslation ?? this.showTranslation,
|
||||
showPronunciation: showPronunciation ?? this.showPronunciation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 学习目标
|
||||
enum LearningGoal {
|
||||
@JsonValue('daily_communication')
|
||||
dailyCommunication,
|
||||
@JsonValue('business_english')
|
||||
businessEnglish,
|
||||
@JsonValue('academic_study')
|
||||
academicStudy,
|
||||
@JsonValue('exam_preparation')
|
||||
examPreparation,
|
||||
@JsonValue('travel')
|
||||
travel,
|
||||
@JsonValue('hobby')
|
||||
hobby,
|
||||
}
|
||||
|
||||
/// 英语水平
|
||||
enum EnglishLevel {
|
||||
@JsonValue('beginner')
|
||||
beginner,
|
||||
@JsonValue('elementary')
|
||||
elementary,
|
||||
@JsonValue('intermediate')
|
||||
intermediate,
|
||||
@JsonValue('upper_intermediate')
|
||||
upperIntermediate,
|
||||
@JsonValue('advanced')
|
||||
advanced,
|
||||
@JsonValue('proficient')
|
||||
proficient,
|
||||
@JsonValue('expert')
|
||||
expert,
|
||||
}
|
||||
|
||||
/// 认证响应
|
||||
@JsonSerializable()
|
||||
class AuthResponse {
|
||||
final User user;
|
||||
final String token;
|
||||
final String? refreshToken;
|
||||
final DateTime expiresAt;
|
||||
|
||||
const AuthResponse({
|
||||
required this.user,
|
||||
required this.token,
|
||||
this.refreshToken,
|
||||
required this.expiresAt,
|
||||
});
|
||||
|
||||
factory AuthResponse.fromJson(Map<String, dynamic> json) => _$AuthResponseFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$AuthResponseToJson(this);
|
||||
}
|
||||
|
||||
/// Token刷新响应
|
||||
@JsonSerializable()
|
||||
class TokenRefreshResponse {
|
||||
final String token;
|
||||
final String? refreshToken;
|
||||
final DateTime expiresAt;
|
||||
|
||||
const TokenRefreshResponse({
|
||||
required this.token,
|
||||
this.refreshToken,
|
||||
required this.expiresAt,
|
||||
});
|
||||
|
||||
factory TokenRefreshResponse.fromJson(Map<String, dynamic> json) => _$TokenRefreshResponseFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$TokenRefreshResponseToJson(this);
|
||||
}
|
||||
172
client/lib/core/models/user_model.g.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'user_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
User _$UserFromJson(Map<String, dynamic> json) => User(
|
||||
id: json['id'] as String,
|
||||
username: json['username'] as String,
|
||||
email: json['email'] as String,
|
||||
phone: json['phone'] as String?,
|
||||
avatar: json['avatar'] as String?,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
profile: json['profile'] == null
|
||||
? null
|
||||
: UserProfile.fromJson(json['profile'] as Map<String, dynamic>),
|
||||
settings: json['settings'] == null
|
||||
? null
|
||||
: UserSettings.fromJson(json['settings'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'username': instance.username,
|
||||
'email': instance.email,
|
||||
'phone': instance.phone,
|
||||
'avatar': instance.avatar,
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
'updatedAt': instance.updatedAt.toIso8601String(),
|
||||
'profile': instance.profile,
|
||||
'settings': instance.settings,
|
||||
};
|
||||
|
||||
UserProfile _$UserProfileFromJson(Map<String, dynamic> json) => UserProfile(
|
||||
firstName: json['firstName'] as String?,
|
||||
lastName: json['lastName'] as String?,
|
||||
phone: json['phone'] as String?,
|
||||
bio: json['bio'] as String?,
|
||||
avatar: json['avatar'] as String?,
|
||||
realName: json['realName'] as String?,
|
||||
gender: json['gender'] as String?,
|
||||
birthday: json['birthday'] == null
|
||||
? null
|
||||
: DateTime.parse(json['birthday'] as String),
|
||||
location: json['location'] as String?,
|
||||
occupation: json['occupation'] as String?,
|
||||
education: json['education'] as String?,
|
||||
interests: (json['interests'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
learningGoal:
|
||||
$enumDecodeNullable(_$LearningGoalEnumMap, json['learningGoal']),
|
||||
currentLevel:
|
||||
$enumDecodeNullable(_$EnglishLevelEnumMap, json['currentLevel']),
|
||||
targetLevel:
|
||||
$enumDecodeNullable(_$EnglishLevelEnumMap, json['targetLevel']),
|
||||
englishLevel:
|
||||
$enumDecodeNullable(_$EnglishLevelEnumMap, json['englishLevel']),
|
||||
settings: json['settings'] == null
|
||||
? null
|
||||
: UserSettings.fromJson(json['settings'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UserProfileToJson(UserProfile instance) =>
|
||||
<String, dynamic>{
|
||||
'firstName': instance.firstName,
|
||||
'lastName': instance.lastName,
|
||||
'phone': instance.phone,
|
||||
'bio': instance.bio,
|
||||
'avatar': instance.avatar,
|
||||
'realName': instance.realName,
|
||||
'gender': instance.gender,
|
||||
'birthday': instance.birthday?.toIso8601String(),
|
||||
'location': instance.location,
|
||||
'occupation': instance.occupation,
|
||||
'education': instance.education,
|
||||
'interests': instance.interests,
|
||||
'learningGoal': _$LearningGoalEnumMap[instance.learningGoal],
|
||||
'currentLevel': _$EnglishLevelEnumMap[instance.currentLevel],
|
||||
'targetLevel': _$EnglishLevelEnumMap[instance.targetLevel],
|
||||
'englishLevel': _$EnglishLevelEnumMap[instance.englishLevel],
|
||||
'settings': instance.settings,
|
||||
};
|
||||
|
||||
const _$LearningGoalEnumMap = {
|
||||
LearningGoal.dailyCommunication: 'daily_communication',
|
||||
LearningGoal.businessEnglish: 'business_english',
|
||||
LearningGoal.academicStudy: 'academic_study',
|
||||
LearningGoal.examPreparation: 'exam_preparation',
|
||||
LearningGoal.travel: 'travel',
|
||||
LearningGoal.hobby: 'hobby',
|
||||
};
|
||||
|
||||
const _$EnglishLevelEnumMap = {
|
||||
EnglishLevel.beginner: 'beginner',
|
||||
EnglishLevel.elementary: 'elementary',
|
||||
EnglishLevel.intermediate: 'intermediate',
|
||||
EnglishLevel.upperIntermediate: 'upper_intermediate',
|
||||
EnglishLevel.advanced: 'advanced',
|
||||
EnglishLevel.proficient: 'proficient',
|
||||
EnglishLevel.expert: 'expert',
|
||||
};
|
||||
|
||||
UserSettings _$UserSettingsFromJson(Map<String, dynamic> json) => UserSettings(
|
||||
notificationsEnabled: json['notificationsEnabled'] as bool? ?? true,
|
||||
soundEnabled: json['soundEnabled'] as bool? ?? true,
|
||||
vibrationEnabled: json['vibrationEnabled'] as bool? ?? true,
|
||||
language: json['language'] as String? ?? 'zh-CN',
|
||||
theme: json['theme'] as String? ?? 'system',
|
||||
dailyGoal: (json['dailyGoal'] as num?)?.toInt() ?? 30,
|
||||
dailyWordGoal: (json['dailyWordGoal'] as num?)?.toInt() ?? 20,
|
||||
dailyStudyMinutes: (json['dailyStudyMinutes'] as num?)?.toInt() ?? 30,
|
||||
reminderTimes: (json['reminderTimes'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const ['09:00', '20:00'],
|
||||
autoPlayAudio: json['autoPlayAudio'] as bool? ?? true,
|
||||
audioSpeed: (json['audioSpeed'] as num?)?.toDouble() ?? 1.0,
|
||||
showTranslation: json['showTranslation'] as bool? ?? true,
|
||||
showPronunciation: json['showPronunciation'] as bool? ?? true,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UserSettingsToJson(UserSettings instance) =>
|
||||
<String, dynamic>{
|
||||
'notificationsEnabled': instance.notificationsEnabled,
|
||||
'soundEnabled': instance.soundEnabled,
|
||||
'vibrationEnabled': instance.vibrationEnabled,
|
||||
'language': instance.language,
|
||||
'theme': instance.theme,
|
||||
'dailyGoal': instance.dailyGoal,
|
||||
'dailyWordGoal': instance.dailyWordGoal,
|
||||
'dailyStudyMinutes': instance.dailyStudyMinutes,
|
||||
'reminderTimes': instance.reminderTimes,
|
||||
'autoPlayAudio': instance.autoPlayAudio,
|
||||
'audioSpeed': instance.audioSpeed,
|
||||
'showTranslation': instance.showTranslation,
|
||||
'showPronunciation': instance.showPronunciation,
|
||||
};
|
||||
|
||||
AuthResponse _$AuthResponseFromJson(Map<String, dynamic> json) => AuthResponse(
|
||||
user: User.fromJson(json['user'] as Map<String, dynamic>),
|
||||
token: json['token'] as String,
|
||||
refreshToken: json['refreshToken'] as String?,
|
||||
expiresAt: DateTime.parse(json['expiresAt'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AuthResponseToJson(AuthResponse instance) =>
|
||||
<String, dynamic>{
|
||||
'user': instance.user,
|
||||
'token': instance.token,
|
||||
'refreshToken': instance.refreshToken,
|
||||
'expiresAt': instance.expiresAt.toIso8601String(),
|
||||
};
|
||||
|
||||
TokenRefreshResponse _$TokenRefreshResponseFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
TokenRefreshResponse(
|
||||
token: json['token'] as String,
|
||||
refreshToken: json['refreshToken'] as String?,
|
||||
expiresAt: DateTime.parse(json['expiresAt'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TokenRefreshResponseToJson(
|
||||
TokenRefreshResponse instance) =>
|
||||
<String, dynamic>{
|
||||
'token': instance.token,
|
||||
'refreshToken': instance.refreshToken,
|
||||
'expiresAt': instance.expiresAt.toIso8601String(),
|
||||
};
|
||||
209
client/lib/core/network/ai_api_service.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../services/storage_service.dart';
|
||||
import 'api_endpoints.dart';
|
||||
import '../config/environment.dart';
|
||||
|
||||
/// AI相关API服务
|
||||
class AIApiService {
|
||||
static String get _baseUrl => EnvironmentConfig.baseUrl;
|
||||
|
||||
/// 获取认证头部
|
||||
Map<String, String> _getAuthHeaders() {
|
||||
final storageService = StorageService.instance;
|
||||
final token = storageService.getString(StorageKeys.accessToken);
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
if (token != null) 'Authorization': 'Bearer $token',
|
||||
};
|
||||
}
|
||||
|
||||
/// 写作批改
|
||||
Future<Map<String, dynamic>> correctWriting({
|
||||
required String content,
|
||||
required String taskType,
|
||||
}) async {
|
||||
try {
|
||||
final headers = _getAuthHeaders();
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/api/v1/ai/writing/correct'),
|
||||
headers: headers,
|
||||
body: json.encode({
|
||||
'content': content,
|
||||
'task_type': taskType,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to correct writing: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error correcting writing: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 口语评估
|
||||
Future<Map<String, dynamic>> evaluateSpeaking({
|
||||
required String audioText,
|
||||
required String prompt,
|
||||
}) async {
|
||||
try {
|
||||
final headers = _getAuthHeaders();
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/api/v1/ai/speaking/evaluate'),
|
||||
headers: headers,
|
||||
body: json.encode({
|
||||
'audio_text': audioText,
|
||||
'prompt': prompt,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to evaluate speaking: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error evaluating speaking: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取AI使用统计
|
||||
Future<Map<String, dynamic>> getAIUsageStats() async {
|
||||
try {
|
||||
final headers = _getAuthHeaders();
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/api/v1/ai/stats'),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to get AI stats: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error getting AI stats: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传音频文件
|
||||
Future<Map<String, dynamic>> uploadAudio(File audioFile) async {
|
||||
try {
|
||||
final storageService = StorageService.instance;
|
||||
final token = storageService.getString(StorageKeys.accessToken);
|
||||
final request = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse('$_baseUrl/api/v1/upload/audio'),
|
||||
);
|
||||
|
||||
if (token != null) {
|
||||
request.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
request.files.add(
|
||||
await http.MultipartFile.fromPath('audio', audioFile.path),
|
||||
);
|
||||
|
||||
final streamedResponse = await request.send();
|
||||
final response = await http.Response.fromStream(streamedResponse);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to upload audio: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error uploading audio: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传图片文件
|
||||
Future<Map<String, dynamic>> uploadImage(File imageFile) async {
|
||||
try {
|
||||
final storageService = StorageService.instance;
|
||||
final token = storageService.getString(StorageKeys.accessToken);
|
||||
final request = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse('$_baseUrl/api/v1/upload/image'),
|
||||
);
|
||||
|
||||
if (token != null) {
|
||||
request.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
request.files.add(
|
||||
await http.MultipartFile.fromPath('image', imageFile.path),
|
||||
);
|
||||
|
||||
final streamedResponse = await request.send();
|
||||
final response = await http.Response.fromStream(streamedResponse);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to upload image: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error uploading image: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除文件
|
||||
Future<Map<String, dynamic>> deleteFile(String fileId) async {
|
||||
try {
|
||||
final headers = _getAuthHeaders();
|
||||
final response = await http.delete(
|
||||
Uri.parse('$_baseUrl/api/v1/upload/file/$fileId'),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to delete file: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error deleting file: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取文件信息
|
||||
Future<Map<String, dynamic>> getFileInfo(String fileId) async {
|
||||
try {
|
||||
final headers = _getAuthHeaders();
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/api/v1/upload/file/$fileId'),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to get file info: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error getting file info: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取上传统计
|
||||
Future<Map<String, dynamic>> getUploadStats({int days = 30}) async {
|
||||
try {
|
||||
final headers = _getAuthHeaders();
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/api/v1/upload/stats?days=$days'),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to get upload stats: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error getting upload stats: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
252
client/lib/core/network/api_client.dart
Normal file
@@ -0,0 +1,252 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../constants/app_constants.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../services/navigation_service.dart';
|
||||
import '../routes/app_routes.dart';
|
||||
|
||||
/// API客户端配置
|
||||
class ApiClient {
|
||||
static ApiClient? _instance;
|
||||
late Dio _dio;
|
||||
late StorageService _storageService;
|
||||
|
||||
ApiClient._internal() {
|
||||
_dio = Dio();
|
||||
}
|
||||
|
||||
static Future<ApiClient> getInstance() async {
|
||||
if (_instance == null) {
|
||||
_instance = ApiClient._internal();
|
||||
_instance!._storageService = await StorageService.getInstance();
|
||||
await _instance!._setupInterceptors();
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
static ApiClient get instance {
|
||||
if (_instance == null) {
|
||||
throw Exception('ApiClient not initialized. Call ApiClient.getInstance() first.');
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Dio get dio => _dio;
|
||||
|
||||
/// 配置拦截器
|
||||
Future<void> _setupInterceptors() async {
|
||||
// 基础配置
|
||||
_dio.options = BaseOptions(
|
||||
baseUrl: AppConstants.baseUrl,
|
||||
connectTimeout: Duration(milliseconds: AppConstants.connectTimeout),
|
||||
receiveTimeout: Duration(milliseconds: AppConstants.receiveTimeout),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
// 请求拦截器
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
// 添加认证token
|
||||
final token = await _storageService.getToken();
|
||||
if (token != null && token.isNotEmpty) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
handler.next(options);
|
||||
},
|
||||
onResponse: (response, handler) {
|
||||
handler.next(response);
|
||||
},
|
||||
onError: (error, handler) async {
|
||||
// 处理401错误,尝试刷新token
|
||||
if (error.response?.statusCode == 401) {
|
||||
final refreshed = await _refreshToken();
|
||||
if (refreshed) {
|
||||
// 重新发送请求
|
||||
final options = error.requestOptions;
|
||||
final token = await _storageService.getToken();
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
|
||||
try {
|
||||
final response = await _dio.fetch(options);
|
||||
handler.resolve(response);
|
||||
return;
|
||||
} catch (e) {
|
||||
// 刷新后仍然失败,清除token并跳转登录
|
||||
await _clearTokensAndRedirectToLogin();
|
||||
}
|
||||
} else {
|
||||
// 刷新失败,清除token并跳转登录
|
||||
await _clearTokensAndRedirectToLogin();
|
||||
}
|
||||
}
|
||||
|
||||
handler.next(error);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// 日志拦截器(仅在调试模式下)
|
||||
if (const bool.fromEnvironment('dart.vm.product') == false) {
|
||||
_dio.interceptors.add(
|
||||
LogInterceptor(
|
||||
requestHeader: true,
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
responseHeader: false,
|
||||
error: true,
|
||||
logPrint: (obj) => print(obj),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新token
|
||||
Future<bool> _refreshToken() async {
|
||||
try {
|
||||
final refreshToken = await _storageService.getRefreshToken();
|
||||
if (refreshToken == null || refreshToken.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final response = await _dio.post(
|
||||
'/auth/refresh',
|
||||
data: {'refresh_token': refreshToken},
|
||||
options: Options(
|
||||
headers: {'Authorization': null}, // 移除Authorization头
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
await _storageService.saveToken(data['access_token']);
|
||||
if (data['refresh_token'] != null) {
|
||||
await _storageService.saveRefreshToken(data['refresh_token']);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Token refresh failed: $e');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 清除token并跳转登录
|
||||
Future<void> _clearTokensAndRedirectToLogin() async {
|
||||
await _storageService.clearTokens();
|
||||
|
||||
// 跳转到登录页面并清除所有历史记录
|
||||
NavigationService.instance.navigateToAndClearStack(Routes.login);
|
||||
|
||||
// 显示提示信息
|
||||
NavigationService.instance.showErrorSnackBar('登录已过期,请重新登录');
|
||||
}
|
||||
|
||||
/// GET请求
|
||||
Future<Response<T>> get<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
return await _dio.get<T>(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// POST请求
|
||||
Future<Response<T>> post<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
return await _dio.post<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// PUT请求
|
||||
Future<Response<T>> put<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
return await _dio.put<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// DELETE请求
|
||||
Future<Response<T>> delete<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
return await _dio.delete<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// 上传文件
|
||||
Future<Response<T>> upload<T>(
|
||||
String path,
|
||||
FormData formData, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
}) async {
|
||||
return await _dio.post<T>(
|
||||
path,
|
||||
data: formData,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
);
|
||||
}
|
||||
|
||||
/// 下载文件
|
||||
Future<Response> download(
|
||||
String urlPath,
|
||||
String savePath, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) async {
|
||||
return await _dio.download(
|
||||
urlPath,
|
||||
savePath,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
}
|
||||
77
client/lib/core/network/api_endpoints.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
import '../config/environment.dart';
|
||||
|
||||
/// API端点配置
|
||||
class ApiEndpoints {
|
||||
// 基础URL - 从环境配置获取
|
||||
static String get baseUrl => EnvironmentConfig.baseUrl;
|
||||
|
||||
// 认证相关
|
||||
static const String login = '/auth/login';
|
||||
static const String register = '/auth/register';
|
||||
static const String logout = '/auth/logout';
|
||||
static const String refreshToken = '/auth/refresh';
|
||||
static const String forgotPassword = '/auth/forgot-password';
|
||||
static const String resetPassword = '/auth/reset-password';
|
||||
static const String changePassword = '/auth/change-password';
|
||||
static const String socialLogin = '/auth/social-login';
|
||||
static const String verifyEmail = '/auth/verify-email';
|
||||
static const String resendVerificationEmail = '/auth/resend-verification';
|
||||
|
||||
// 用户相关
|
||||
static const String userInfo = '/user/profile';
|
||||
static const String updateProfile = '/user/profile';
|
||||
static const String uploadAvatar = '/user/avatar';
|
||||
static const String checkUsername = '/user/check-username';
|
||||
static const String checkEmail = '/user/check-email';
|
||||
|
||||
// 学习相关
|
||||
static const String learningProgress = '/learning/progress';
|
||||
static const String learningStats = '/learning/stats';
|
||||
static const String dailyGoal = '/learning/daily-goal';
|
||||
|
||||
// 词汇相关
|
||||
static const String vocabulary = '/vocabulary';
|
||||
static const String vocabularyTest = '/vocabulary/test';
|
||||
static const String vocabularyProgress = '/vocabulary/progress';
|
||||
static const String wordBooks = '/vocabulary/books';
|
||||
static const String wordLists = '/vocabulary/lists';
|
||||
|
||||
// 听力相关
|
||||
static const String listening = '/listening';
|
||||
static const String listeningMaterials = '/listening/materials';
|
||||
static const String listeningRecords = '/listening/records';
|
||||
static const String listeningStats = '/listening/stats';
|
||||
|
||||
// 阅读相关
|
||||
static const String reading = '/reading';
|
||||
static const String readingMaterials = '/reading/materials';
|
||||
static const String readingRecords = '/reading/records';
|
||||
static const String readingStats = '/reading/stats';
|
||||
|
||||
// 写作相关
|
||||
static const String writing = '/writing';
|
||||
static const String writingPrompts = '/writing/prompts';
|
||||
static const String writingSubmissions = '/writing/submissions';
|
||||
static const String writingStats = '/writing/stats';
|
||||
|
||||
// 口语相关
|
||||
static const String speaking = '/speaking';
|
||||
static const String speakingScenarios = '/speaking/scenarios';
|
||||
static const String speakingRecords = '/speaking/records';
|
||||
static const String speakingStats = '/speaking/stats';
|
||||
|
||||
// AI相关
|
||||
static const String aiChat = '/ai/chat';
|
||||
static const String aiCorrection = '/ai/correction';
|
||||
static const String aiSuggestion = '/ai/suggestion';
|
||||
|
||||
// 文件上传
|
||||
static const String upload = '/upload';
|
||||
static const String uploadAudio = '/upload/audio';
|
||||
static const String uploadImage = '/upload/image';
|
||||
|
||||
// 系统相关
|
||||
static const String version = '/version';
|
||||
static const String config = '/system/config';
|
||||
static const String feedback = '/system/feedback';
|
||||
}
|
||||
154
client/lib/core/providers/app_state_provider.dart
Normal file
@@ -0,0 +1,154 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../shared/providers/auth_provider.dart';
|
||||
import '../../shared/providers/vocabulary_provider.dart';
|
||||
import '../../shared/services/auth_service.dart';
|
||||
import '../../shared/services/vocabulary_service.dart';
|
||||
import '../network/api_client.dart';
|
||||
|
||||
/// 全局应用状态管理
|
||||
class AppStateNotifier extends StateNotifier<AppState> {
|
||||
AppStateNotifier() : super(const AppState());
|
||||
|
||||
void updateTheme(ThemeMode themeMode) {
|
||||
state = state.copyWith(themeMode: themeMode);
|
||||
}
|
||||
|
||||
void updateLocale(String locale) {
|
||||
state = state.copyWith(locale: locale);
|
||||
}
|
||||
|
||||
void updateNetworkStatus(bool isOnline) {
|
||||
state = state.copyWith(isOnline: isOnline);
|
||||
}
|
||||
|
||||
void updateLoading(bool isLoading) {
|
||||
state = state.copyWith(isGlobalLoading: isLoading);
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用状态模型
|
||||
class AppState {
|
||||
final ThemeMode themeMode;
|
||||
final String locale;
|
||||
final bool isOnline;
|
||||
final bool isGlobalLoading;
|
||||
|
||||
const AppState({
|
||||
this.themeMode = ThemeMode.light,
|
||||
this.locale = 'zh_CN',
|
||||
this.isOnline = true,
|
||||
this.isGlobalLoading = false,
|
||||
});
|
||||
|
||||
AppState copyWith({
|
||||
ThemeMode? themeMode,
|
||||
String? locale,
|
||||
bool? isOnline,
|
||||
bool? isGlobalLoading,
|
||||
}) {
|
||||
return AppState(
|
||||
themeMode: themeMode ?? this.themeMode,
|
||||
locale: locale ?? this.locale,
|
||||
isOnline: isOnline ?? this.isOnline,
|
||||
isGlobalLoading: isGlobalLoading ?? this.isGlobalLoading,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 全局状态Provider
|
||||
final appStateProvider = StateNotifierProvider<AppStateNotifier, AppState>(
|
||||
(ref) => AppStateNotifier(),
|
||||
);
|
||||
|
||||
/// API客户端Provider
|
||||
final apiClientProvider = Provider<ApiClient>(
|
||||
(ref) => ApiClient.instance,
|
||||
);
|
||||
|
||||
/// 认证服务Provider
|
||||
final authServiceProvider = Provider<AuthService>(
|
||||
(ref) => AuthService(),
|
||||
);
|
||||
|
||||
/// 词汇服务Provider
|
||||
final vocabularyServiceProvider = Provider<VocabularyService>(
|
||||
(ref) => VocabularyService(),
|
||||
);
|
||||
|
||||
/// 认证Provider
|
||||
final authProvider = ChangeNotifierProvider<AuthProvider>(
|
||||
(ref) {
|
||||
final authService = ref.read(authServiceProvider);
|
||||
return AuthProvider()..initialize();
|
||||
},
|
||||
);
|
||||
|
||||
/// 词汇Provider
|
||||
final vocabularyProvider = ChangeNotifierProvider<VocabularyProvider>(
|
||||
(ref) {
|
||||
final vocabularyService = ref.read(vocabularyServiceProvider);
|
||||
return VocabularyProvider(vocabularyService);
|
||||
},
|
||||
);
|
||||
|
||||
/// 网络状态Provider
|
||||
final networkStatusProvider = StreamProvider<bool>(
|
||||
(ref) async* {
|
||||
// 这里可以实现网络状态监听
|
||||
yield true; // 默认在线状态
|
||||
},
|
||||
);
|
||||
|
||||
/// 缓存管理Provider
|
||||
final cacheManagerProvider = Provider<CacheManager>(
|
||||
(ref) => CacheManager(),
|
||||
);
|
||||
|
||||
/// 缓存管理器
|
||||
class CacheManager {
|
||||
final Map<String, dynamic> _cache = {};
|
||||
final Map<String, DateTime> _cacheTimestamps = {};
|
||||
final Duration _defaultCacheDuration = const Duration(minutes: 30);
|
||||
|
||||
/// 设置缓存
|
||||
void set(String key, dynamic value, {Duration? duration}) {
|
||||
_cache[key] = value;
|
||||
_cacheTimestamps[key] = DateTime.now();
|
||||
}
|
||||
|
||||
/// 获取缓存
|
||||
T? get<T>(String key, {Duration? duration}) {
|
||||
final timestamp = _cacheTimestamps[key];
|
||||
if (timestamp == null) return null;
|
||||
|
||||
final cacheDuration = duration ?? _defaultCacheDuration;
|
||||
if (DateTime.now().difference(timestamp) > cacheDuration) {
|
||||
remove(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return _cache[key] as T?;
|
||||
}
|
||||
|
||||
/// 移除缓存
|
||||
void remove(String key) {
|
||||
_cache.remove(key);
|
||||
_cacheTimestamps.remove(key);
|
||||
}
|
||||
|
||||
/// 清空缓存
|
||||
void clear() {
|
||||
_cache.clear();
|
||||
_cacheTimestamps.clear();
|
||||
}
|
||||
|
||||
/// 检查缓存是否存在且有效
|
||||
bool isValid(String key, {Duration? duration}) {
|
||||
final timestamp = _cacheTimestamps[key];
|
||||
if (timestamp == null) return false;
|
||||
|
||||
final cacheDuration = duration ?? _defaultCacheDuration;
|
||||
return DateTime.now().difference(timestamp) <= cacheDuration;
|
||||
}
|
||||
}
|
||||
85
client/lib/core/providers/providers.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'app_state_provider.dart';
|
||||
import '../../shared/providers/network_provider.dart';
|
||||
import '../../shared/providers/error_provider.dart';
|
||||
import '../../features/auth/providers/auth_provider.dart' as auth;
|
||||
import '../../features/vocabulary/providers/vocabulary_provider.dart' as vocab;
|
||||
import '../../features/comprehensive_test/providers/test_riverpod_provider.dart' as test;
|
||||
|
||||
/// 全局Provider配置
|
||||
class GlobalProviders {
|
||||
static final List<Override> overrides = [
|
||||
// 这里可以添加测试时的Provider覆盖
|
||||
];
|
||||
|
||||
static final List<ProviderObserver> observers = [
|
||||
ProviderLogger(),
|
||||
];
|
||||
|
||||
/// 获取所有核心Provider
|
||||
static List<ProviderBase> get coreProviders => [
|
||||
// 应用状态
|
||||
appStateProvider,
|
||||
networkProvider,
|
||||
errorProvider,
|
||||
|
||||
// 认证相关
|
||||
auth.authProvider,
|
||||
|
||||
// 词汇相关
|
||||
vocab.vocabularyProvider,
|
||||
|
||||
// 综合测试相关
|
||||
test.testProvider,
|
||||
];
|
||||
|
||||
/// 预加载Provider
|
||||
static Future<void> preloadProviders(ProviderContainer container) async {
|
||||
// 预加载网络状态
|
||||
container.read(networkProvider.notifier).refreshNetworkStatus();
|
||||
|
||||
// 认证状态会在AuthNotifier构造时自动检查
|
||||
// 这里只需要读取provider来触发初始化
|
||||
container.read(auth.authProvider);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider状态监听器
|
||||
class ProviderLogger extends ProviderObserver {
|
||||
@override
|
||||
void didUpdateProvider(
|
||||
ProviderBase provider,
|
||||
Object? previousValue,
|
||||
Object? newValue,
|
||||
ProviderContainer container,
|
||||
) {
|
||||
print('Provider ${provider.name ?? provider.runtimeType} updated: $newValue');
|
||||
}
|
||||
|
||||
@override
|
||||
void didAddProvider(
|
||||
ProviderBase provider,
|
||||
Object? value,
|
||||
ProviderContainer container,
|
||||
) {
|
||||
print('Provider ${provider.name ?? provider.runtimeType} added: $value');
|
||||
}
|
||||
|
||||
@override
|
||||
void didDisposeProvider(
|
||||
ProviderBase provider,
|
||||
ProviderContainer container,
|
||||
) {
|
||||
print('Provider ${provider.name ?? provider.runtimeType} disposed');
|
||||
}
|
||||
|
||||
@override
|
||||
void providerDidFail(
|
||||
ProviderBase provider,
|
||||
Object error,
|
||||
StackTrace stackTrace,
|
||||
ProviderContainer container,
|
||||
) {
|
||||
print('Provider ${provider.name ?? provider.runtimeType} failed: $error');
|
||||
}
|
||||
}
|
||||
496
client/lib/core/routes/app_routes.dart
Normal file
@@ -0,0 +1,496 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../features/auth/screens/splash_screen.dart';
|
||||
import '../../features/auth/screens/login_screen.dart';
|
||||
import '../../features/auth/screens/register_screen.dart';
|
||||
import '../../features/auth/screens/forgot_password_screen.dart';
|
||||
import '../../features/main/screens/main_app_screen.dart';
|
||||
import '../../features/learning/screens/learning_home_screen.dart';
|
||||
import '../../features/vocabulary/screens/vocabulary_home_screen.dart';
|
||||
import '../../features/vocabulary/screens/vocabulary_category_screen.dart';
|
||||
|
||||
import '../../features/vocabulary/screens/vocabulary_book_screen.dart';
|
||||
import '../../features/vocabulary/screens/word_learning_screen.dart';
|
||||
import '../../features/vocabulary/screens/smart_review_screen.dart';
|
||||
import '../../features/vocabulary/screens/vocabulary_test_screen.dart';
|
||||
import '../../features/vocabulary/screens/daily_words_screen.dart';
|
||||
import '../../features/vocabulary/screens/ai_recommendation_screen.dart';
|
||||
import '../../features/vocabulary/screens/word_book_screen.dart';
|
||||
import '../../features/vocabulary/screens/study_plan_screen.dart';
|
||||
import '../../features/vocabulary/models/word_model.dart';
|
||||
import '../../features/vocabulary/models/vocabulary_book_model.dart';
|
||||
import '../../features/vocabulary/models/review_models.dart';
|
||||
import '../../features/listening/screens/listening_home_screen.dart';
|
||||
import '../../features/listening/screens/listening_category_screen.dart';
|
||||
import '../../features/listening/screens/listening_exercise_detail_screen.dart';
|
||||
import '../../features/listening/screens/listening_difficulty_screen.dart';
|
||||
import '../../features/listening/screens/listening_stats_screen.dart';
|
||||
import '../../features/listening/models/listening_exercise_model.dart';
|
||||
// 移除静态数据依赖
|
||||
import '../../features/reading/screens/reading_home_screen.dart';
|
||||
import '../../features/writing/screens/writing_home_screen.dart';
|
||||
import '../../features/speaking/screens/speaking_home_screen.dart';
|
||||
import '../../features/comprehensive_test/screens/comprehensive_test_screen.dart';
|
||||
import '../../features/profile/screens/profile_home_screen.dart';
|
||||
import '../../features/profile/screens/profile_edit_screen.dart';
|
||||
import '../../features/profile/screens/settings_screen.dart';
|
||||
import '../../features/profile/screens/help_feedback_screen.dart';
|
||||
import '../../features/ai/pages/ai_main_page.dart';
|
||||
import '../../features/ai/pages/ai_writing_page.dart';
|
||||
import '../../features/ai/pages/ai_speaking_page.dart';
|
||||
import '../../features/home/screens/learning_stats_detail_screen.dart';
|
||||
import '../../features/notification/screens/notification_list_screen.dart';
|
||||
import '../widgets/not_found_screen.dart';
|
||||
|
||||
// 学习模式枚举
|
||||
enum LearningMode {
|
||||
normal,
|
||||
review,
|
||||
test
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// 路由名称常量
|
||||
class Routes {
|
||||
static const String splash = '/splash';
|
||||
static const String login = '/login';
|
||||
static const String register = '/register';
|
||||
static const String forgotPassword = '/forgot-password';
|
||||
static const String home = '/home';
|
||||
static const String learning = '/learning';
|
||||
static const String profile = '/profile';
|
||||
static const String editProfile = '/edit-profile';
|
||||
static const String settings = '/settings';
|
||||
static const String helpFeedback = '/help-feedback';
|
||||
static const String vocabularyHome = '/vocabulary';
|
||||
static const String vocabularyCategory = '/vocabulary/category';
|
||||
static const String vocabularyList = '/vocabulary/list';
|
||||
static const String vocabularyBook = '/vocabulary/book';
|
||||
static const String wordDetail = '/vocabulary/word';
|
||||
static const String vocabularyTest = '/vocabulary/test';
|
||||
static const String wordLearning = '/vocabulary/learning';
|
||||
static const String smartReview = '/vocabulary/smart-review';
|
||||
static const String dailyWords = '/vocabulary/daily-words';
|
||||
static const String aiRecommendation = '/vocabulary/ai-recommendation';
|
||||
static const String wordBook = '/vocabulary/word-book';
|
||||
static const String studyPlan = '/vocabulary/study-plan';
|
||||
static const String listeningHome = '/listening';
|
||||
static const String listeningExercise = '/listening/exercise';
|
||||
static const String listeningCategory = '/listening/category';
|
||||
static const String listeningExerciseDetail = '/listening/exercise-detail';
|
||||
static const String listeningDifficulty = '/listening/difficulty';
|
||||
static const String listeningStats = '/listening/stats';
|
||||
static const String readingHome = '/reading';
|
||||
static const String readingExercise = '/reading/exercise';
|
||||
static const String writingHome = '/writing';
|
||||
static const String writingExercise = '/writing/exercise';
|
||||
static const String speakingHome = '/speaking';
|
||||
static const String speakingExercise = '/speaking/exercise';
|
||||
static const String comprehensiveTest = '/comprehensive-test';
|
||||
static const String ai = '/ai';
|
||||
static const String aiWriting = '/ai/writing';
|
||||
static const String aiSpeaking = '/ai/speaking';
|
||||
static const String learningStatsDetail = '/learning-stats-detail';
|
||||
static const String notifications = '/notifications';
|
||||
}
|
||||
|
||||
/// 应用路由配置
|
||||
class AppRoutes {
|
||||
/// 路由映射表
|
||||
static final Map<String, WidgetBuilder> _routes = {
|
||||
Routes.splash: (context) => const SplashScreen(),
|
||||
Routes.login: (context) => const LoginScreen(),
|
||||
Routes.register: (context) => const RegisterScreen(),
|
||||
Routes.forgotPassword: (context) => const ForgotPasswordScreen(),
|
||||
Routes.home: (context) => const MainAppScreen(),
|
||||
Routes.learning: (context) => const LearningHomeScreen(),
|
||||
Routes.vocabularyHome: (context) => const VocabularyHomeScreen(),
|
||||
// TODO: 这些路由需要参数,暂时注释掉,后续通过onGenerateRoute处理
|
||||
// Routes.vocabularyList: (context) => const VocabularyBookScreen(),
|
||||
// Routes.wordDetail: (context) => const WordLearningScreen(),
|
||||
// Routes.vocabularyTest: (context) => const VocabularyTestScreen(),
|
||||
// Routes.wordLearning: (context) => const SmartReviewScreen(),
|
||||
Routes.dailyWords: (context) => const DailyWordsScreen(),
|
||||
Routes.aiRecommendation: (context) => const AIRecommendationScreen(),
|
||||
Routes.wordBook: (context) => const WordBookScreen(),
|
||||
Routes.studyPlan: (context) => const StudyPlanScreen(),
|
||||
Routes.listeningHome: (context) => const ListeningHomeScreen(),
|
||||
Routes.listeningDifficulty: (context) => const ListeningDifficultyScreen(),
|
||||
Routes.listeningStats: (context) => const ListeningStatsScreen(),
|
||||
Routes.readingHome: (context) => const ReadingHomeScreen(),
|
||||
Routes.writingHome: (context) => const WritingHomeScreen(),
|
||||
Routes.speakingHome: (context) => const SpeakingHomeScreen(),
|
||||
Routes.comprehensiveTest: (context) => const ComprehensiveTestScreen(),
|
||||
Routes.profile: (context) => const ProfileHomeScreen(),
|
||||
Routes.editProfile: (context) => const ProfileEditScreen(),
|
||||
Routes.settings: (context) => const SettingsScreen(),
|
||||
Routes.helpFeedback: (context) => const HelpFeedbackScreen(),
|
||||
Routes.ai: (context) => const AIMainPage(),
|
||||
Routes.aiWriting: (context) => const AIWritingPage(),
|
||||
Routes.aiSpeaking: (context) => const AISpeakingPage(),
|
||||
Routes.learningStatsDetail: (context) => const LearningStatsDetailScreen(),
|
||||
Routes.notifications: (context) => const NotificationListScreen(),
|
||||
// TODO: 添加其他页面路由
|
||||
};
|
||||
|
||||
/// 获取路由映射表
|
||||
static Map<String, WidgetBuilder> get routes => _routes;
|
||||
|
||||
/// 路由生成器
|
||||
static Route<dynamic>? onGenerateRoute(RouteSettings settings) {
|
||||
final String routeName = settings.name ?? '';
|
||||
final arguments = settings.arguments;
|
||||
|
||||
// 处理带参数的词汇学习路由
|
||||
switch (routeName) {
|
||||
case Routes.vocabularyCategory:
|
||||
if (arguments is Map<String, dynamic>) {
|
||||
final category = arguments['category'];
|
||||
if (category != null) {
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => VocabularyCategoryScreen(category: category),
|
||||
settings: settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Routes.vocabularyList:
|
||||
if (arguments is Map<String, dynamic>) {
|
||||
final vocabularyBook = arguments['vocabularyBook'];
|
||||
if (vocabularyBook != null) {
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => VocabularyBookScreen(vocabularyBook: vocabularyBook),
|
||||
settings: settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Routes.wordLearning:
|
||||
if (arguments is Map<String, dynamic>) {
|
||||
final vocabularyBook = arguments['vocabularyBook'];
|
||||
final specificWords = arguments['specificWords'];
|
||||
final mode = arguments['mode'];
|
||||
if (vocabularyBook != null) {
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => WordLearningScreen(
|
||||
vocabularyBook: vocabularyBook,
|
||||
specificWords: specificWords,
|
||||
mode: mode ?? LearningMode.normal,
|
||||
),
|
||||
settings: settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Routes.vocabularyTest:
|
||||
// 词汇测试路由,支持带参数和不带参数的情况
|
||||
if (arguments is Map<String, dynamic>) {
|
||||
final vocabularyBook = arguments['vocabularyBook'];
|
||||
final testType = arguments['testType'];
|
||||
final questionCount = arguments['questionCount'];
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => VocabularyTestScreen(
|
||||
vocabularyBook: vocabularyBook,
|
||||
testType: testType ?? TestType.vocabularyLevel,
|
||||
questionCount: questionCount ?? 20,
|
||||
),
|
||||
settings: settings,
|
||||
);
|
||||
} else {
|
||||
// 没有参数时,使用默认设置
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => const VocabularyTestScreen(
|
||||
testType: TestType.vocabularyLevel,
|
||||
questionCount: 20,
|
||||
),
|
||||
settings: settings,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case Routes.wordDetail:
|
||||
if (arguments is Map<String, dynamic>) {
|
||||
final vocabularyBook = arguments['vocabularyBook'];
|
||||
final reviewMode = arguments['reviewMode'];
|
||||
final dailyTarget = arguments['dailyTarget'];
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => SmartReviewScreen(
|
||||
vocabularyBook: vocabularyBook,
|
||||
reviewMode: reviewMode ?? ReviewMode.adaptive,
|
||||
dailyTarget: dailyTarget ?? 20,
|
||||
),
|
||||
settings: settings,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case Routes.smartReview:
|
||||
// 智能复习路由,支持带参数和不带参数的情况
|
||||
if (arguments is Map<String, dynamic>) {
|
||||
final vocabularyBook = arguments['vocabularyBook'];
|
||||
final reviewMode = arguments['reviewMode'];
|
||||
final dailyTarget = arguments['dailyTarget'];
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => SmartReviewScreen(
|
||||
vocabularyBook: vocabularyBook,
|
||||
reviewMode: reviewMode ?? ReviewMode.adaptive,
|
||||
dailyTarget: dailyTarget ?? 20,
|
||||
),
|
||||
settings: settings,
|
||||
);
|
||||
} else {
|
||||
// 没有参数时,使用默认设置
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => const SmartReviewScreen(
|
||||
reviewMode: ReviewMode.adaptive,
|
||||
dailyTarget: 20,
|
||||
),
|
||||
settings: settings,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
// 听力相关路由
|
||||
case Routes.listeningCategory:
|
||||
if (arguments is Map<String, dynamic>) {
|
||||
final type = arguments['type'] as ListeningExerciseType;
|
||||
final title = arguments['title'] as String;
|
||||
final category = ListeningCategory(
|
||||
id: type.toString(),
|
||||
name: title,
|
||||
description: '${title}练习材料',
|
||||
icon: Icons.headphones,
|
||||
exerciseCount: 0,
|
||||
type: type,
|
||||
);
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => ListeningCategoryScreen(
|
||||
category: category,
|
||||
),
|
||||
settings: settings,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case Routes.listeningExerciseDetail:
|
||||
if (arguments is Map<String, dynamic>) {
|
||||
final exerciseId = arguments['exerciseId'];
|
||||
if (exerciseId != null) {
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => ListeningExerciseDetailScreen(
|
||||
exerciseId: exerciseId,
|
||||
),
|
||||
settings: settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 默认路由处理
|
||||
final WidgetBuilder? builder = _routes[routeName];
|
||||
if (builder != null) {
|
||||
return MaterialPageRoute(
|
||||
builder: builder,
|
||||
settings: settings,
|
||||
);
|
||||
}
|
||||
|
||||
// 未找到路由时的处理
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => const NotFoundScreen(),
|
||||
settings: settings,
|
||||
);
|
||||
}
|
||||
|
||||
/// 路由守卫 - 检查是否需要认证
|
||||
static bool requiresAuth(String routeName) {
|
||||
const publicRoutes = [
|
||||
Routes.splash,
|
||||
Routes.login,
|
||||
Routes.register,
|
||||
Routes.forgotPassword,
|
||||
];
|
||||
|
||||
return !publicRoutes.contains(routeName);
|
||||
}
|
||||
|
||||
/// 获取初始路由
|
||||
static String getInitialRoute(bool isLoggedIn) {
|
||||
return isLoggedIn ? Routes.home : Routes.splash;
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动页面
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SplashScreen> createState() => _SplashScreenState();
|
||||
}
|
||||
|
||||
class _SplashScreenState extends State<SplashScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeApp();
|
||||
}
|
||||
|
||||
Future<void> _initializeApp() async {
|
||||
// 初始化应用配置
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
if (mounted) {
|
||||
// 导航到登录页面,让用户进行认证
|
||||
Navigator.of(context).pushReplacementNamed(Routes.login);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// TODO: 添加应用Logo
|
||||
Icon(
|
||||
Icons.school,
|
||||
size: 100,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'AI英语学习',
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'智能化英语学习平台',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
const CircularProgressIndicator(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 404页面
|
||||
class NotFoundScreen extends StatelessWidget {
|
||||
const NotFoundScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('页面未找到'),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 100,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'404',
|
||||
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'抱歉,您访问的页面不存在',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||
Routes.home,
|
||||
(route) => false,
|
||||
);
|
||||
},
|
||||
child: const Text('返回首页'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 路由导航辅助类
|
||||
class AppNavigator {
|
||||
/// 导航到指定页面
|
||||
static Future<T?> push<T extends Object?>(
|
||||
BuildContext context,
|
||||
String routeName, {
|
||||
Object? arguments,
|
||||
}) {
|
||||
return Navigator.of(context).pushNamed<T>(
|
||||
routeName,
|
||||
arguments: arguments,
|
||||
);
|
||||
}
|
||||
|
||||
/// 替换当前页面
|
||||
static Future<T?> pushReplacement<T extends Object?, TO extends Object?>(
|
||||
BuildContext context,
|
||||
String routeName, {
|
||||
Object? arguments,
|
||||
TO? result,
|
||||
}) {
|
||||
return Navigator.of(context).pushReplacementNamed<T, TO>(
|
||||
routeName,
|
||||
arguments: arguments,
|
||||
result: result,
|
||||
);
|
||||
}
|
||||
|
||||
/// 清空栈并导航到指定页面
|
||||
static Future<T?> pushAndRemoveUntil<T extends Object?>(
|
||||
BuildContext context,
|
||||
String routeName, {
|
||||
Object? arguments,
|
||||
bool Function(Route<dynamic>)? predicate,
|
||||
}) {
|
||||
return Navigator.of(context).pushNamedAndRemoveUntil<T>(
|
||||
routeName,
|
||||
predicate ?? (route) => false,
|
||||
arguments: arguments,
|
||||
);
|
||||
}
|
||||
|
||||
/// 返回上一页
|
||||
static void pop<T extends Object?>(BuildContext context, [T? result]) {
|
||||
Navigator.of(context).pop<T>(result);
|
||||
}
|
||||
|
||||
/// 返回到指定页面
|
||||
static void popUntil(BuildContext context, String routeName) {
|
||||
Navigator.of(context).popUntil(ModalRoute.withName(routeName));
|
||||
}
|
||||
|
||||
/// 检查是否可以返回
|
||||
static bool canPop(BuildContext context) {
|
||||
return Navigator.of(context).canPop();
|
||||
}
|
||||
}
|
||||
284
client/lib/core/services/api_service.dart
Normal file
@@ -0,0 +1,284 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'storage_service.dart';
|
||||
import '../config/environment.dart';
|
||||
|
||||
class ApiResponse {
|
||||
final dynamic data;
|
||||
final int statusCode;
|
||||
final String? message;
|
||||
|
||||
ApiResponse({
|
||||
required this.data,
|
||||
required this.statusCode,
|
||||
this.message,
|
||||
});
|
||||
}
|
||||
|
||||
class ApiService {
|
||||
late final Dio _dio;
|
||||
final StorageService _storageService;
|
||||
|
||||
ApiService({required StorageService storageService})
|
||||
: _storageService = storageService {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: EnvironmentConfig.baseUrl,
|
||||
connectTimeout: Duration(milliseconds: EnvironmentConfig.connectTimeout),
|
||||
receiveTimeout: Duration(milliseconds: EnvironmentConfig.receiveTimeout),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
));
|
||||
|
||||
_setupInterceptors();
|
||||
}
|
||||
|
||||
void _setupInterceptors() {
|
||||
// 请求拦截器
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
// 添加认证token
|
||||
final token = await _storageService.getToken();
|
||||
if (token != null) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
print('API Request: ${options.method} ${options.uri}');
|
||||
print('Headers: ${options.headers}');
|
||||
if (options.data != null) {
|
||||
print('Data: ${options.data}');
|
||||
}
|
||||
}
|
||||
|
||||
handler.next(options);
|
||||
},
|
||||
onResponse: (response, handler) {
|
||||
if (kDebugMode) {
|
||||
print('API Response: ${response.statusCode} ${response.requestOptions.uri}');
|
||||
}
|
||||
handler.next(response);
|
||||
},
|
||||
onError: (error, handler) {
|
||||
if (kDebugMode) {
|
||||
print('API Error: ${error.message}');
|
||||
print('Response: ${error.response?.data}');
|
||||
}
|
||||
handler.next(error);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
// GET请求
|
||||
Future<ApiResponse> get(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParams,
|
||||
Options? options,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
path,
|
||||
queryParameters: queryParams,
|
||||
options: options,
|
||||
);
|
||||
return ApiResponse(
|
||||
data: response.data,
|
||||
statusCode: response.statusCode ?? 200,
|
||||
message: response.statusMessage,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// POST请求
|
||||
Future<ApiResponse> post(
|
||||
String path,
|
||||
dynamic data, {
|
||||
Map<String, dynamic>? queryParams,
|
||||
Options? options,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParams,
|
||||
options: options,
|
||||
);
|
||||
return ApiResponse(
|
||||
data: response.data,
|
||||
statusCode: response.statusCode ?? 200,
|
||||
message: response.statusMessage,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT请求
|
||||
Future<ApiResponse> put(
|
||||
String path,
|
||||
dynamic data, {
|
||||
Map<String, dynamic>? queryParams,
|
||||
Options? options,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.put(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParams,
|
||||
options: options,
|
||||
);
|
||||
return ApiResponse(
|
||||
data: response.data,
|
||||
statusCode: response.statusCode ?? 200,
|
||||
message: response.statusMessage,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH请求
|
||||
Future<ApiResponse> patch(
|
||||
String path,
|
||||
dynamic data, {
|
||||
Map<String, dynamic>? queryParams,
|
||||
Options? options,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.patch(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParams,
|
||||
options: options,
|
||||
);
|
||||
return ApiResponse(
|
||||
data: response.data,
|
||||
statusCode: response.statusCode ?? 200,
|
||||
message: response.statusMessage,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE请求
|
||||
Future<ApiResponse> delete(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParams,
|
||||
Options? options,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.delete(
|
||||
path,
|
||||
queryParameters: queryParams,
|
||||
options: options,
|
||||
);
|
||||
return ApiResponse(
|
||||
data: response.data,
|
||||
statusCode: response.statusCode ?? 200,
|
||||
message: response.statusMessage,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
Future<ApiResponse> uploadFile(
|
||||
String path,
|
||||
String filePath, {
|
||||
String? fileName,
|
||||
Map<String, dynamic>? data,
|
||||
ProgressCallback? onSendProgress,
|
||||
}) async {
|
||||
try {
|
||||
final formData = FormData.fromMap({
|
||||
'file': await MultipartFile.fromFile(
|
||||
filePath,
|
||||
filename: fileName,
|
||||
),
|
||||
...?data,
|
||||
});
|
||||
|
||||
final response = await _dio.post(
|
||||
path,
|
||||
data: formData,
|
||||
options: Options(
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
),
|
||||
onSendProgress: onSendProgress,
|
||||
);
|
||||
|
||||
return ApiResponse(
|
||||
data: response.data,
|
||||
statusCode: response.statusCode ?? 200,
|
||||
message: response.statusMessage,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
Future<void> downloadFile(
|
||||
String url,
|
||||
String savePath, {
|
||||
ProgressCallback? onReceiveProgress,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
try {
|
||||
await _dio.download(
|
||||
url,
|
||||
savePath,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 错误处理
|
||||
Exception _handleError(DioException error) {
|
||||
switch (error.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return Exception('网络连接超时,请检查网络设置');
|
||||
case DioExceptionType.badResponse:
|
||||
final statusCode = error.response?.statusCode;
|
||||
final message = error.response?.data?['message'] ?? '请求失败';
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
return Exception('请求参数错误: $message');
|
||||
case 401:
|
||||
return Exception('认证失败,请重新登录');
|
||||
case 403:
|
||||
return Exception('权限不足: $message');
|
||||
case 404:
|
||||
return Exception('请求的资源不存在');
|
||||
case 500:
|
||||
return Exception('服务器内部错误,请稍后重试');
|
||||
default:
|
||||
return Exception('请求失败($statusCode): $message');
|
||||
}
|
||||
case DioExceptionType.cancel:
|
||||
return Exception('请求已取消');
|
||||
case DioExceptionType.connectionError:
|
||||
return Exception('网络连接失败,请检查网络设置');
|
||||
case DioExceptionType.unknown:
|
||||
default:
|
||||
return Exception('未知错误: ${error.message}');
|
||||
}
|
||||
}
|
||||
|
||||
// 取消所有请求
|
||||
void cancelRequests() {
|
||||
_dio.close();
|
||||
}
|
||||
}
|
||||
378
client/lib/core/services/audio_service.dart
Normal file
@@ -0,0 +1,378 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
// 音频录制状态
|
||||
enum RecordingState {
|
||||
idle,
|
||||
recording,
|
||||
paused,
|
||||
stopped,
|
||||
}
|
||||
|
||||
// 音频播放状态
|
||||
enum PlaybackState {
|
||||
idle,
|
||||
playing,
|
||||
paused,
|
||||
stopped,
|
||||
}
|
||||
|
||||
class AudioService {
|
||||
// 录制相关
|
||||
RecordingState _recordingState = RecordingState.idle;
|
||||
String? _currentRecordingPath;
|
||||
DateTime? _recordingStartTime;
|
||||
|
||||
// 播放相关
|
||||
PlaybackState _playbackState = PlaybackState.idle;
|
||||
String? _currentPlayingPath;
|
||||
|
||||
// 回调函数
|
||||
Function(RecordingState)? onRecordingStateChanged;
|
||||
Function(PlaybackState)? onPlaybackStateChanged;
|
||||
Function(Duration)? onRecordingProgress;
|
||||
Function(Duration)? onPlaybackProgress;
|
||||
Function(String)? onRecordingComplete;
|
||||
Function()? onPlaybackComplete;
|
||||
|
||||
// Getters
|
||||
RecordingState get recordingState => _recordingState;
|
||||
PlaybackState get playbackState => _playbackState;
|
||||
String? get currentRecordingPath => _currentRecordingPath;
|
||||
String? get currentPlayingPath => _currentPlayingPath;
|
||||
bool get isRecording => _recordingState == RecordingState.recording;
|
||||
bool get isPlaying => _playbackState == PlaybackState.playing;
|
||||
|
||||
// 初始化音频服务
|
||||
Future<void> initialize() async {
|
||||
// 请求麦克风权限
|
||||
await _requestPermissions();
|
||||
}
|
||||
|
||||
// 请求权限
|
||||
Future<bool> _requestPermissions() async {
|
||||
// Web平台不支持某些权限,需要特殊处理
|
||||
if (kIsWeb) {
|
||||
// Web平台只需要麦克风权限,且通过浏览器API处理
|
||||
if (kDebugMode) {
|
||||
print('Web平台:跳过权限请求');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
final microphoneStatus = await Permission.microphone.request();
|
||||
|
||||
if (microphoneStatus != PermissionStatus.granted) {
|
||||
throw Exception('需要麦克风权限才能录音');
|
||||
}
|
||||
|
||||
// 存储权限在某些平台可能不需要
|
||||
try {
|
||||
final storageStatus = await Permission.storage.request();
|
||||
if (storageStatus != PermissionStatus.granted) {
|
||||
if (kDebugMode) {
|
||||
print('存储权限未授予,但继续执行');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 某些平台不支持存储权限,忽略错误
|
||||
if (kDebugMode) {
|
||||
print('存储权限请求失败(可能不支持): $e');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('权限请求失败: $e');
|
||||
}
|
||||
// 在某些平台上,权限请求可能失败,但仍然可以继续
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 开始录音
|
||||
Future<void> startRecording({String? fileName}) async {
|
||||
try {
|
||||
if (_recordingState == RecordingState.recording) {
|
||||
throw Exception('已经在录音中');
|
||||
}
|
||||
|
||||
await _requestPermissions();
|
||||
|
||||
// 生成录音文件路径
|
||||
if (kIsWeb) {
|
||||
// Web平台使用内存存储或IndexedDB
|
||||
fileName ??= 'recording_${DateTime.now().millisecondsSinceEpoch}.webm';
|
||||
_currentRecordingPath = '/recordings/$fileName';
|
||||
} else {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final recordingsDir = Directory('${directory.path}/recordings');
|
||||
if (!await recordingsDir.exists()) {
|
||||
await recordingsDir.create(recursive: true);
|
||||
}
|
||||
|
||||
fileName ??= 'recording_${DateTime.now().millisecondsSinceEpoch}.m4a';
|
||||
_currentRecordingPath = '${recordingsDir.path}/$fileName';
|
||||
}
|
||||
|
||||
// 这里应该使用实际的录音插件,比如 record 或 flutter_sound
|
||||
// 由于没有实际的录音插件,这里只是模拟
|
||||
_recordingStartTime = DateTime.now();
|
||||
_setRecordingState(RecordingState.recording);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('开始录音: $_currentRecordingPath');
|
||||
}
|
||||
|
||||
// 模拟录音进度更新
|
||||
_startRecordingProgressTimer();
|
||||
|
||||
} catch (e) {
|
||||
throw Exception('开始录音失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 停止录音
|
||||
Future<String?> stopRecording() async {
|
||||
try {
|
||||
if (_recordingState != RecordingState.recording) {
|
||||
throw Exception('当前没有在录音');
|
||||
}
|
||||
|
||||
// 这里应该调用实际录音插件的停止方法
|
||||
_setRecordingState(RecordingState.stopped);
|
||||
|
||||
final recordingPath = _currentRecordingPath;
|
||||
|
||||
if (kDebugMode) {
|
||||
print('录音完成: $recordingPath');
|
||||
}
|
||||
|
||||
// 通知录音完成
|
||||
if (recordingPath != null && onRecordingComplete != null) {
|
||||
onRecordingComplete!(recordingPath);
|
||||
}
|
||||
|
||||
return recordingPath;
|
||||
} catch (e) {
|
||||
throw Exception('停止录音失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停录音
|
||||
Future<void> pauseRecording() async {
|
||||
try {
|
||||
if (_recordingState != RecordingState.recording) {
|
||||
throw Exception('当前没有在录音');
|
||||
}
|
||||
|
||||
// 这里应该调用实际录音插件的暂停方法
|
||||
_setRecordingState(RecordingState.paused);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('录音已暂停');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('暂停录音失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复录音
|
||||
Future<void> resumeRecording() async {
|
||||
try {
|
||||
if (_recordingState != RecordingState.paused) {
|
||||
throw Exception('录音没有暂停');
|
||||
}
|
||||
|
||||
// 这里应该调用实际录音插件的恢复方法
|
||||
_setRecordingState(RecordingState.recording);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('录音已恢复');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('恢复录音失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 播放音频
|
||||
Future<void> playAudio(String audioPath) async {
|
||||
try {
|
||||
if (_playbackState == PlaybackState.playing) {
|
||||
await stopPlayback();
|
||||
}
|
||||
|
||||
_currentPlayingPath = audioPath;
|
||||
|
||||
// 这里应该使用实际的音频播放插件,比如 audioplayers 或 just_audio
|
||||
// 由于没有实际的播放插件,这里只是模拟
|
||||
_setPlaybackState(PlaybackState.playing);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('开始播放: $audioPath');
|
||||
}
|
||||
|
||||
// 模拟播放进度和完成
|
||||
_startPlaybackProgressTimer();
|
||||
|
||||
} catch (e) {
|
||||
throw Exception('播放音频失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停播放
|
||||
Future<void> pausePlayback() async {
|
||||
try {
|
||||
if (_playbackState != PlaybackState.playing) {
|
||||
throw Exception('当前没有在播放');
|
||||
}
|
||||
|
||||
// 这里应该调用实际播放插件的暂停方法
|
||||
_setPlaybackState(PlaybackState.paused);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('播放已暂停');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('暂停播放失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复播放
|
||||
Future<void> resumePlayback() async {
|
||||
try {
|
||||
if (_playbackState != PlaybackState.paused) {
|
||||
throw Exception('播放没有暂停');
|
||||
}
|
||||
|
||||
// 这里应该调用实际播放插件的恢复方法
|
||||
_setPlaybackState(PlaybackState.playing);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('播放已恢复');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('恢复播放失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 停止播放
|
||||
Future<void> stopPlayback() async {
|
||||
try {
|
||||
// 这里应该调用实际播放插件的停止方法
|
||||
_setPlaybackState(PlaybackState.stopped);
|
||||
_currentPlayingPath = null;
|
||||
|
||||
if (kDebugMode) {
|
||||
print('播放已停止');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('停止播放失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// 获取音频文件时长
|
||||
Future<Duration?> getAudioDuration(String audioPath) async {
|
||||
try {
|
||||
// 这里应该使用实际的音频插件获取时长
|
||||
// 模拟返回时长
|
||||
return const Duration(seconds: 30);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('获取音频时长失败: ${e.toString()}');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除录音文件
|
||||
Future<bool> deleteRecording(String filePath) async {
|
||||
try {
|
||||
final file = File(filePath);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('删除录音文件失败: ${e.toString()}');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有录音文件
|
||||
Future<List<String>> getAllRecordings() async {
|
||||
try {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final recordingsDir = Directory('${directory.path}/recordings');
|
||||
|
||||
if (!await recordingsDir.exists()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final files = await recordingsDir.list().toList();
|
||||
return files
|
||||
.where((file) => file is File && file.path.endsWith('.m4a'))
|
||||
.map((file) => file.path)
|
||||
.toList();
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('获取录音文件列表失败: ${e.toString()}');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 私有方法:设置录音状态
|
||||
void _setRecordingState(RecordingState state) {
|
||||
_recordingState = state;
|
||||
onRecordingStateChanged?.call(state);
|
||||
}
|
||||
|
||||
// 私有方法:设置播放状态
|
||||
void _setPlaybackState(PlaybackState state) {
|
||||
_playbackState = state;
|
||||
onPlaybackStateChanged?.call(state);
|
||||
}
|
||||
|
||||
// 私有方法:录音进度计时器
|
||||
void _startRecordingProgressTimer() {
|
||||
// 这里应该实现实际的进度更新逻辑
|
||||
// 模拟进度更新
|
||||
}
|
||||
|
||||
// 私有方法:播放进度计时器
|
||||
void _startPlaybackProgressTimer() {
|
||||
// 这里应该实现实际的播放进度更新逻辑
|
||||
// 模拟播放完成
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
_setPlaybackState(PlaybackState.stopped);
|
||||
onPlaybackComplete?.call();
|
||||
});
|
||||
}
|
||||
|
||||
// 释放资源
|
||||
void dispose() {
|
||||
// 停止所有操作
|
||||
if (_recordingState == RecordingState.recording) {
|
||||
stopRecording();
|
||||
}
|
||||
if (_playbackState == PlaybackState.playing) {
|
||||
stopPlayback();
|
||||
}
|
||||
|
||||
// 清理回调
|
||||
onRecordingStateChanged = null;
|
||||
onPlaybackStateChanged = null;
|
||||
onRecordingProgress = null;
|
||||
onPlaybackProgress = null;
|
||||
onRecordingComplete = null;
|
||||
onPlaybackComplete = null;
|
||||
}
|
||||
}
|
||||