This commit is contained in:
sjk
2025-11-17 13:39:05 +08:00
commit d4cfe2b9de
479 changed files with 109324 additions and 0 deletions

93
.gitignore vendored Normal file
View 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
View 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
View 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. **访问应用**
- 后端APIhttp://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
View 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
View File

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

45
client/.metadata Normal file
View 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
View 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;"]

View 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
View 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

View File

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

14
client/android/.gitignore vendored Normal file
View 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

View 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 = "../.."
}

View 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>

View 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>

View File

@@ -0,0 +1,5 @@
package com.aienglish.learning
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View 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>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View 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>

View File

@@ -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>

View 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>

View 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)
}

View 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

View 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

View 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")

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

85
client/build_prod.bat Normal file
View 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

View 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
View 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

View 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>

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View 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 */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -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>

View File

@@ -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>

View 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)
}
}

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View 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.

View 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>

View 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>

View 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>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View 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.
}
}

View 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 用于 Web10.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;
}
}

View 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;
}

View 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',
);
}
}

View 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,
});
}

View 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,
};
}
}

View 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);
}

View 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(),
};

View 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');
}
}
}

View 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,
);
}
}

View 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';
}

View 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;
}
}

View 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');
}
}

View 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();
}
}

View 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();
}
}

View 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;
}
}

Some files were not shown because too many files have changed in this diff Show More