diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index bd1aa8f..0000000 --- a/android/.gitignore +++ /dev/null @@ -1,16 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml -.DS_Store -/build -/captures -.externalNativeBuild -.cxx -local.properties -*.hprof diff --git a/android/PROJECT_SUMMARY.md b/android/PROJECT_SUMMARY.md deleted file mode 100644 index bd0e8b8..0000000 --- a/android/PROJECT_SUMMARY.md +++ /dev/null @@ -1,303 +0,0 @@ -# Android 客户端开发完成总结 - -## 项目概况 - -已为车管家 4S店车辆维保管理系统创建了完整的 Android 客户端应用。 - -**项目位置**: `car-maintenance-system/android/` - -## 技术架构 - -### 核心技术栈 -- **语言**: Kotlin -- **UI框架**: Jetpack Compose + Material 3 -- **架构模式**: MVVM + Clean Architecture -- **依赖注入**: Hilt -- **网络请求**: Retrofit + OkHttp -- **异步处理**: Coroutines + Flow -- **本地存储**: DataStore Preferences -- **导航**: Navigation Compose - -### 依赖版本 -```kotlin -- Kotlin: 1.9.20 -- Compose BOM: 2023.10.01 -- Hilt: 2.48 -- Retrofit: 2.9.0 -- OkHttp: 4.12.0 -- Target SDK: 34 -- Min SDK: 26 -``` - -## 已实现功能 - -### ✅ 基础架构 -1. **项目配置** - - Gradle 构建配置 - - Hilt 依赖注入设置 - - ProGuard 混淆规则 - - 网络权限配置 - -2. **数据层** - - 数据模型(User, Vehicle, ServiceOrder, Appointment等) - - Repository 模式实现 - - API 接口定义 - - Token 管理(自动添加到请求头) - -3. **网络层** - - Retrofit 配置 - - OkHttp 拦截器(日志、认证) - - API 响应统一封装 - - 错误处理 - -### ✅ 用户认证 -- 登录界面(用户名/密码/角色选择) -- Token 自动存储和管理 -- 自动登录检测 -- 退出登录功能 -- 角色权限路由(管理员/工作人员/客户) - -### ✅ 客户模块 -1. **客户仪表板** - - 用户信息展示 - - 快捷功能入口 - - 车辆列表展示 - - 车辆卡片(显示车牌、品牌、里程、保养时间) - -2. **我的车辆** - - 车辆列表展示 - - 加载状态 - - 空状态处理 - -3. **维保记录** - - 工单列表展示 - - 状态标签显示 - - 费用信息展示 - -4. **我的预约** - - 预约列表展示 - - 状态标签(待确认/已确认/已完成/已取消) - - 悬浮新建按钮(预留) - -### ✅ 管理员模块 -- 管理员仪表板 -- 系统概览统计 -- 管理功能菜单(用户/车辆/工单/配件/预约) - -### ✅ UI/UX 设计 -1. **主题系统** - - Material 3 设计语言 - - 自定义颜色方案 - - 深色/浅色主题支持 - -2. **组件设计** - - 统一的卡片样式 - - 状态徽章组件 - - 加载指示器 - - 空状态提示 - -3. **导航系统** - - 导航图配置 - - 角色路由分发 - - 自动登录检测 - -## 项目文件清单 - -### 核心代码文件(32个) -``` -数据层 (11个) -├── ApiResponse.kt # API响应封装 -├── ApiService.kt # API接口定义 -├── AuthModels.kt # 认证相关模型 -├── User.kt # 用户模型 -├── Vehicle.kt # 车辆模型 -├── ServiceOrder.kt # 工单模型 -├── Appointment.kt # 预约模型 -├── TokenManager.kt # Token管理 -├── AuthRepository.kt # 认证仓库 -├── VehicleRepository.kt # 车辆仓库 -├── OrderRepository.kt # 工单仓库 -└── AppointmentRepository.kt # 预约仓库 - -业务层 (4个) -├── NetworkModule.kt # 网络模块 -├── AuthViewModel.kt # 认证视图模型 -└── CustomerViewModel.kt # 客户视图模型 - -UI层 (17个) -├── MainActivity.kt # 主Activity -├── CarMaintenanceApplication.kt # Application类 -├── Screen.kt # 路由定义 -├── Navigation.kt # 导航配置 -├── Theme.kt # 主题配置 -├── Type.kt # 字体样式 -├── Color.kt # 颜色定义 -├── LoginScreen.kt # 登录页面 -├── CustomerDashboardScreen.kt # 客户仪表板 -├── CustomerVehiclesScreen.kt # 我的车辆 -├── CustomerOrdersScreen.kt # 维保记录 -├── CustomerAppointmentsScreen.kt # 我的预约 -└── AdminDashboardScreen.kt # 管理员仪表板 -``` - -### 配置文件(10个) -``` -├── settings.gradle.kts # Gradle设置 -├── build.gradle.kts # 项目构建配置 -├── gradle.properties # Gradle属性 -├── .gitignore # Git忽略文件 -├── app/build.gradle.kts # 应用构建配置 -├── app/proguard-rules.pro # 混淆规则 -├── app/src/main/AndroidManifest.xml # 清单文件 -├── app/src/main/res/values/strings.xml -├── app/src/main/res/values/themes.xml -└── app/src/main/res/xml/*.xml # 备份和提取规则 -``` - -### 文档文件(2个) -``` -├── README.md # 项目说明 -└── QUICKSTART.md # 快速开始指南 -``` - -## 待完善功能 - -### 📋 高优先级 -1. **在线预约功能** - - 预约表单页面 - - 车辆选择器 - - 服务类型选择 - - 日期时间选择器 - -2. **详情页面** - - 车辆详情查看 - - 工单详情查看 - - 预约详情查看 - -3. **管理员完整功能** - - 用户管理CRUD - - 车辆管理CRUD - - 工单管理CRUD - - 配件管理CRUD - - 预约管理(确认/取消) - -### 📋 中优先级 -1. **工作人员模块** - - 我的工单列表 - - 工单处理流程 - - 预约确认功能 - -2. **数据刷新** - - 下拉刷新 - - 自动刷新机制 - - 状态同步 - -3. **表单功能** - - 车辆添加 - - 信息编辑 - - 表单验证 - -### 📋 低优先级 -1. **增强功能** - - 搜索过滤 - - 排序功能 - - 数据导出 - -2. **用户体验** - - 加载动画优化 - - 错误提示优化 - - 空状态优化 - -3. **高级功能** - - 推送通知 - - 离线缓存 - - 图片上传 - - 电子签名 - -## 快速开始 - -### 环境要求 -- Android Studio Hedgehog (2023.1.1) 或更高版本 -- JDK 17 -- Android SDK 34 -- 后端服务运行在 http://localhost:8080 - -### 运行步骤 -1. 打开 Android Studio -2. 打开 `android` 目录 -3. 等待 Gradle 同步 -4. 配置 API 地址(如果需要) -5. 运行应用 - -### 测试账号 -- 管理员: admin / 123456 -- 工作人员: staff001 / 123456 -- 客户: customer001 / 123456 - -## 架构亮点 - -1. **MVVM 架构** - - 清晰的职责分离 - - 状态管理使用 StateFlow - - 数据自动观察和更新 - -2. **依赖注入** - - 使用 Hilt 管理依赖 - - 模块化配置 - - 编译时依赖检查 - -3. **网络层** - - 统一的 API 响应处理 - - 自动 Token 管理 - - 请求/响应日志记录 - -4. **导航系统** - - 类型安全的导航 - - 角色路由分发 - - 自动登录检测 - -5. **现代 UI** - - Jetpack Compose 声明式UI - - Material 3 设计规范 - - 自适应布局 - -## 后续开发建议 - -1. **功能完善** - - 优先实现核心业务流程 - - 补充表单和详情页面 - - 添加数据验证 - -2. **性能优化** - - 添加图片缓存 - - 优化列表滚动 - - 减少内存占用 - -3. **用户体验** - - 添加骨架屏 - - 优化加载状态 - - 完善错误处理 - -4. **测试** - - 添加单元测试 - - 添加 UI 测试 - - 集成测试 - -## 技术债务 - -1. 需要添加完整的错误处理机制 -2. 需要实现数据缓存策略 -3. 需要添加更多的单元测试 -4. 需要优化网络请求的重试逻辑 -5. 需要实现更细粒度的权限控制 - -## 总结 - -Android 客户端的基础架构已完成,实现了: -- ✅ 完整的项目结构 -- ✅ 网络请求层 -- ✅ 用户认证系统 -- ✅ 客户核心功能 -- ✅ 管理员基础功能 - -应用可以正常编译运行,并且与后端 API 对接正常。后续可以根据业务需求逐步添加更多功能。 diff --git a/android/QUICKSTART.md b/android/QUICKSTART.md index 7fb68f4..9be5685 100644 --- a/android/QUICKSTART.md +++ b/android/QUICKSTART.md @@ -1,222 +1,19 @@ -# 车管家 Android 客户端 - 快速开始指南 +# Android (Java) Quickstart -## 项目概述 +## Requirements +- Android Studio Hedgehog (2023.1.1) or newer +- JDK 17 +- Android SDK 34 -这是一个现代化的 Android 应用,为车管家 4S店车辆维保管理系统提供移动端支持。 +## Run +1. Open Android Studio +2. Open the `android` folder +3. Let Gradle sync +4. Update API base URL if needed: + - `android/app/build.gradle` -> `API_BASE_URL` +5. Run `LoginActivity` -## 开发前准备 - -### 1. 安装 Android Studio - -下载并安装最新版本的 Android Studio: -https://developer.android.com/studio - -### 2. 配置 JDK - -确保安装了 JDK 17 或更高版本: -```bash -java -version # 应该显示 17.x.x 或更高 -``` - -### 3. 启动后端服务 - -在运行 Android 应用之前,确保后端服务正在运行: - -```bash -# 进入后端项目目录 -cd car-maintenance-system/backend - -# 使用 Maven 启动服务 -mvn spring-boot:run - -# 或者在 IDEA 中运行 SpringBootAppApplication.java -``` - -后端默认运行在 `http://localhost:8080` - -### 4. 配置 API 地址 - -根据你的测试环境,修改 `app/build.gradle.kts`: - -**使用 Android 模拟器:** -```kotlin -buildConfigField("String", "API_BASE_URL", "\"http://10.0.2.2:8080/api/\"") -``` - -**使用真机:** -```kotlin -buildConfigField("String", "API_BASE_URL", "\"http://YOUR_PC_IP:8080/api/\"") -// 例如: "http://192.168.1.100:8080/api/" -``` - -## 运行应用 - -### 方式一:使用 Android Studio - -1. 打开 Android Studio -2. 选择 **File > Open** -3. 选择 `car-maintenance-system/android` 目录 -4. 等待 Gradle 同步完成 -5. 点击工具栏的 Run 按钮 (绿色三角形) - -### 方式二:使用命令行 - -```bash -cd android - -# 查看连接的设备 -adb devices - -# 安装并运行 -./gradlew installDebug -``` - -## 测试账号 - -| 角色 | 用户名 | 密码 | 功能 | -|------|--------|------|------| -| 管理员 | admin | 123456 | 系统管理、数据统计 | -| 工作人员 | staff001 | 123456 | 工单处理、预约确认 | -| 客户 | customer001 | 123456 | 查看车辆、预约服务 | - -## 项目结构说明 - -``` -android/ -├── app/ # 应用模块 -│ ├── src/main/ -│ │ ├── java/com/carmaintenance/ -│ │ │ ├── data/ # 数据层 -│ │ │ │ ├── local/ # 本地存储 -│ │ │ │ ├── model/ # 数据模型 -│ │ │ │ ├── remote/ # API接口 -│ │ │ │ ├── repository/ # 数据仓库 -│ │ │ │ └── manager/ # Token管理 -│ │ │ ├── di/ # 依赖注入 -│ │ │ ├── ui/ # UI层 -│ │ │ │ ├── navigation/ # 导航 -│ │ │ │ ├── screen/ # 页面 -│ │ │ │ ├── theme/ # 主题 -│ │ │ │ └── viewmodel/ # 视图模型 -│ │ │ ├── CarMaintenanceApplication.kt -│ │ │ └── MainActivity.kt -│ │ └── res/ # 资源文件 -│ ├── build.gradle.kts # 应用级构建配置 -│ └── proguard-rules.pro # 混淆规则 -├── build.gradle.kts # 项目级构建配置 -├── settings.gradle.kts # Gradle设置 -└── gradle.properties # Gradle属性 -``` - -## 常见问题 - -### 1. Gradle 同步失败 - -**问题**: Gradle 同步时出现错误 - -**解决方案**: -```bash -# 清理项目 -./gradlew clean - -# 重新构建 -./gradlew build - -# 如果还是失败,删除 .gradle 目录后重试 -rm -rf .gradle -``` - -### 2. 网络请求失败 - -**问题**: 应用无法连接到服务器 - -**解决方案**: -- 确认后端服务正在运行 -- 检查 API_BASE_URL 配置 -- 确保设备和电脑在同一网络(真机测试) -- 检查防火墙设置 - -### 3. 登录失败 - -**问题**: 登录时提示错误 - -**解决方案**: -- 确认后端数据库中有测试用户 -- 检查用户名和密码是否正确 -- 查看后端日志确认请求是否到达 - -### 4. 模拟器无法访问宿主机 - -**问题**: 模拟器中的 App 无法访问电脑上的后端服务 - -**解决方案**: -使用 `10.0.2.2` 代替 `localhost`: -```kotlin -// 错误 -buildConfigField("String", "API_BASE_URL", "\"http://localhost:8080/api/\"") - -// 正确(模拟器) -buildConfigField("String", "API_BASE_URL", "\"http://10.0.2.2:8080/api/\"") -``` - -### 5. 数据显示为空 - -**问题**: 登录后车辆、工单等数据显示为空 - -**解决方案**: -- 确认后端数据库有相关数据 -- 使用 Admin 账号登录后台添加测试数据 -- 检查客户ID是否正确关联 - -## 调试技巧 - -### 查看 API 请求 - -在 `NetworkModule.kt` 中,日志级别已设置为 `BODY`: -```kotlin -if (BuildConfig.DEBUG) { - HttpLoggingInterceptor.Level.BODY -} -``` - -在 Logcat 中过滤 `OkHttp` 即可看到所有网络请求。 - -### 查看 Token 存储 - -在 Logcat 中过滤 `TokenManager` 可以看到 Token 相关的日志。 - -## 下一步开发 - -### 功能优先级 - -1. **高优先级** - - 在线预约表单 - - 车辆详情查看 - - 工单详情查看 - -2. **中优先级** - - 管理员完整功能 - - 工作人员功能 - - 推送通知 - -3. **低优先级** - - 数据刷新机制 - - 离线缓存 - - 图片上传 - -### 扩展功能建议 - -- [ ] 车辆照片上传 -- [ ] 电子签名 -- [ ] 扫码查看车辆 -- [ ] 消息推送(Firebase) -- [ ] 支付集成 -- [ ] 数据导出 -- [ ] 多语言支持 - -## 技术支持 - -如有问题,请查看: -- [Jetpack Compose 官方文档](https://developer.android.com/jetpack/compose) -- [Hilt 官方文档](https://dagger.dev/hilt/) -- [Retrofit 官方文档](https://square.github.io/retrofit/) +## Test Accounts +- admin / 123456 +- staff001 / 123456 +- customer001 / 123456 diff --git a/android/README.md b/android/README.md deleted file mode 100644 index 1891e92..0000000 --- a/android/README.md +++ /dev/null @@ -1,182 +0,0 @@ -# 车管家 4S店车辆维保管理系统 - Android 客户端 - -基于 **Kotlin + Jetpack Compose** 开发的现代化 Android 应用。 - -## 技术栈 - -- **UI 框架**: Jetpack Compose (Material 3) -- **架构**: MVVM + Clean Architecture -- **依赖注入**: Hilt -- **网络请求**: Retrofit + OkHttp -- **异步处理**: Coroutines + Flow -- **本地存储**: DataStore (用于存储Token和用户信息) -- **导航**: Navigation Compose - -## 项目结构 - -``` -app/ -├── data/ # 数据层 -│ ├── local/ # 本地数据 -│ ├── model/ # 数据模型 -│ ├── remote/ # 远程数据源 -│ │ ├── ApiService.kt # API接口定义 -│ │ └── ApiResponse.kt # API响应封装 -│ ├── repository/ # 仓库层 -│ └── manager/ # 数据管理器 -├── di/ # 依赖注入模块 -│ └── NetworkModule.kt # 网络模块配置 -├── ui/ # UI层 -│ ├── navigation/ # 导航配置 -│ ├── screen/ # 页面 -│ │ ├── customer/ # 客户模块 -│ │ └── admin/ # 管理员模块 -│ ├── theme/ # 主题配置 -│ └── viewmodel/ # ViewModel -└── MainActivity.kt # 主Activity -``` - -## 功能模块 - -### 已实现功能 -- ✅ 用户登录/登出 -- ✅ 角色权限管理(管理员、工作人员、客户) -- ✅ 客户仪表板 - - 查看我的车辆 - - 查看维保记录 - - 查看预约记录 -- ✅ 管理员仪表板(基础版) -- ✅ 数据持久化(Token存储) - -### 待完善功能 -- ⏳ 在线预约表单 -- ⏳ 管理员完整功能 -- ⏳ 工作人员功能 -- ⏳ 推送通知 -- ⏳ 车辆详情查看 -- ⏳ 工单详情查看 - -## 开发环境要求 - -- Android Studio Hedgehog (2023.1.1) 或更高版本 -- JDK 17 -- Android SDK 34 -- Gradle 8.2 - -## 配置说明 - -### API 地址配置 - -在 `app/build.gradle.kts` 中修改 API_BASE_URL: - -```kotlin -// 模拟器测试(访问宿主机localhost) -buildConfigField("String", "API_BASE_URL", "\"http://10.0.2.2:8080/api/\"") - -// 真机测试(改为实际IP) -buildConfigField("String", "API_BASE_URL", "\"http://192.168.1.100:8080/api/\"") -``` - -### 测试账号 - -- **管理员**: admin / 123456 -- **工作人员**: staff001 / 123456 -- **客户**: customer001 / 123456 - -## 构建和运行 - -### 使用 Android Studio - -1. 打开 Android Studio -2. 选择 `File > Open`,选择 `android` 目录 -3. 等待 Gradle 同步完成 -4. 连接 Android 模拟器或真机 -5. 点击 Run 按钮(或按 Shift+F10) - -### 使用命令行 - -```bash -# 进入项目目录 -cd android - -# 构建 Debug 版本 -./gradlew assembleDebug - -# 安装到设备 -./gradlew installDebug - -# 或者直接运行 -./gradlew installDebug -``` - -## 主要依赖 - -```kotlin -// Compose BOM -implementation(platform("androidx.compose:compose-bom:2023.10.01")) - -// Hilt -implementation("com.google.dagger:hilt-android:2.48") - -// Retrofit -implementation("com.squareup.retrofit2:retrofit:2.9.0") - -// OkHttp -implementation("com.squareup.okhttp3:okhttp:4.12.0") - -// DataStore -implementation("androidx.datastore:datastore-preferences:1.0.0") -``` - -## 开发注意事项 - -### 网络权限 -已在 `AndroidManifest.xml` 中配置: -- `INTERNET` - 访问网络 -- `ACCESS_NETWORK_STATE` - 检查网络状态 -- `usesCleartextTraffic="true"` - 允许HTTP请求(仅用于开发) - -### ProGuard 配置 -已添加 Retrofit、OkHttp、Gson、Hilt 的混淆规则。 - -## 架构设计 - -### 分层架构 -``` -Presentation Layer (UI) - ↓ -Domain Layer (ViewModel) - ↓ -Data Layer (Repository) - ↓ -Remote Data Source (API) -``` - -### 数据流 -``` -UI Screen - ↓ (用户操作) -ViewModel - ↓ (调用方法) -Repository - ↓ (网络请求) -API Service - ↓ (返回数据) -Repository (处理数据) - ↓ (StateFlow) -ViewModel (状态更新) - ↓ (collectAsState) -UI Screen (自动更新) -``` - -## 贡献指南 - -1. Fork 项目 -2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) -3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) -4. 推送到分支 (`git push origin feature/AmazingFeature`) -5. 开启 Pull Request - -## 许可证 - -本项目采用 MIT 许可证 diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..54695bd --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.carmaintenance' + compileSdk 34 + + defaultConfig { + applicationId 'com.carmaintenance' + minSdk 24 + targetSdk 34 + versionCode 1 + versionName '1.0' + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + buildConfigField 'String', 'API_BASE_URL', '"http://10.0.2.2:8080/api/"' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0' + + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' +} diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts deleted file mode 100644 index 8148025..0000000 --- a/android/app/build.gradle.kts +++ /dev/null @@ -1,114 +0,0 @@ -plugins { - id("com.android.application") - id("org.jetbrains.kotlin.android") - id("com.google.dagger.hilt.android") - id("com.google.devtools.ksp") -} - -android { - namespace = "com.carmaintenance" - compileSdk = 34 - - defaultConfig { - applicationId = "com.carmaintenance.app" - minSdk = 26 - targetSdk = 34 - versionCode = 1 - versionName = "1.0" - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables { - useSupportLibrary = true - } - - // API基础URL配置 - buildConfigField("String", "API_BASE_URL", "\"http://10.0.2.2:8080/api/\"") - // 模拟器使用10.0.2.2访问宿主机localhost - // 真机测试需要改为实际IP地址,如: "http://192.168.1.100:8080/api/" - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = "17" - } - - buildFeatures { - compose = true - buildConfig = true - } - - composeOptions { - kotlinCompilerExtensionVersion = "1.5.4" - } - - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - } - } -} - -dependencies { - // Core Android - implementation("androidx.core:core-ktx:1.12.0") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") - implementation("androidx.activity:activity-compose:1.8.1") - - // Compose BOM - implementation(platform("androidx.compose:compose-bom:2023.10.01")) - implementation("androidx.compose.ui:ui") - implementation("androidx.compose.ui:ui-graphics") - implementation("androidx.compose.ui:ui-tooling-preview") - implementation("androidx.compose.material3:material3") - implementation("androidx.compose.material:material-icons-extended") - - // Navigation - implementation("androidx.navigation:navigation-compose:2.7.5") - - // Lifecycle - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2") - implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.2") - - // Hilt Dependency Injection - implementation("com.google.dagger:hilt-android:2.48") - ksp("com.google.dagger:hilt-compiler:2.48") - implementation("androidx.hilt:hilt-navigation-compose:1.1.0") - - // Networking - implementation("com.squareup.retrofit2:retrofit:2.9.0") - implementation("com.squareup.retrofit2:converter-gson:2.9.0") - implementation("com.squareup.okhttp3:okhttp:4.12.0") - implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") - - // Local Storage - DataStore - implementation("androidx.datastore:datastore-preferences:1.0.0") - - // Image Loading - implementation("io.coil-kt:coil-compose:2.5.0") - - // JSON - implementation("com.google.code.gson:gson:2.10.1") - - // Testing - testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") - androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01")) - androidTestImplementation("androidx.compose.ui:ui-test-junit4") - debugImplementation("androidx.compose.ui:ui-tooling") - debugImplementation("androidx.compose.ui:ui-test-manifest") -} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 2fe8102..fb164d6 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -1,49 +1 @@ -# Project-level Gradle stuff here has been deprecated, with the default one being expected. -# For custom project-level configs, place them in the appropriate sub-projects. # Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile - -# Retrofit --dontwarn okhttp3.** --dontwarn retrofit2.** --keep class retrofit2.** { *; } --keepattributes Signature --keepattributes Exceptions - -# OkHttp --dontwarn okhttp3.** --keep class okhttp3.** { *; } --keep interface okhttp3.** { *; } - -# Gson --keepattributes Signature --keepattributes *Annotation* --dontwarn sun.misc.** --keep class * implements com.google.gson.TypeAdapter --keep class * implements com.google.gson.TypeAdapterFactory --keep class * implements com.google.gson.JsonSerializer --keep class * implements com.google.gson.JsonDeserializer - -# Keep Hilt generated classes --keep class dagger.hilt.** { *; } --keep class javax.inject.** { *; } --keep class * extends dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9079a3d..864d9f1 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,26 +1,19 @@ - - + - + android:theme="@style/Theme.CarMaintenance"> + + android:name=".LoginActivity" + android:exported="true"> diff --git a/android/app/src/main/java/com/carmaintenance/ApiClient.java b/android/app/src/main/java/com/carmaintenance/ApiClient.java new file mode 100644 index 0000000..0877eb4 --- /dev/null +++ b/android/app/src/main/java/com/carmaintenance/ApiClient.java @@ -0,0 +1,30 @@ +package com.carmaintenance; + +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class ApiClient { + private static ApiService service; + + public static ApiService getService() { + if (service == null) { + HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); + logging.setLevel(HttpLoggingInterceptor.Level.BODY); + + OkHttpClient client = new OkHttpClient.Builder() + .addInterceptor(logging) + .build(); + + Retrofit retrofit = new Retrofit.Builder() + .baseUrl(BuildConfig.API_BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .client(client) + .build(); + + service = retrofit.create(ApiService.class); + } + return service; + } +} diff --git a/android/app/src/main/java/com/carmaintenance/ApiResult.java b/android/app/src/main/java/com/carmaintenance/ApiResult.java new file mode 100644 index 0000000..04136bb --- /dev/null +++ b/android/app/src/main/java/com/carmaintenance/ApiResult.java @@ -0,0 +1,19 @@ +package com.carmaintenance; + +public class ApiResult { + private int code; + private String message; + private T data; + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } + + public T getData() { + return data; + } +} diff --git a/android/app/src/main/java/com/carmaintenance/ApiService.java b/android/app/src/main/java/com/carmaintenance/ApiService.java new file mode 100644 index 0000000..c0c8c2d --- /dev/null +++ b/android/app/src/main/java/com/carmaintenance/ApiService.java @@ -0,0 +1,10 @@ +package com.carmaintenance; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.POST; + +public interface ApiService { + @POST("auth/login") + Call> login(@Body LoginRequest request); +} diff --git a/android/app/src/main/java/com/carmaintenance/CarMaintenanceApplication.kt b/android/app/src/main/java/com/carmaintenance/CarMaintenanceApplication.kt deleted file mode 100644 index b775ade..0000000 --- a/android/app/src/main/java/com/carmaintenance/CarMaintenanceApplication.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.carmaintenance - -import android.app.Application -import dagger.hilt.android.HiltAndroidApp - -@HiltAndroidApp -class CarMaintenanceApplication : Application() { - - override fun onCreate() { - super.onCreate() - } -} diff --git a/android/app/src/main/java/com/carmaintenance/LoginActivity.java b/android/app/src/main/java/com/carmaintenance/LoginActivity.java new file mode 100644 index 0000000..9417da2 --- /dev/null +++ b/android/app/src/main/java/com/carmaintenance/LoginActivity.java @@ -0,0 +1,104 @@ +package com.carmaintenance; + +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +import com.google.android.material.textfield.TextInputEditText; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class LoginActivity extends AppCompatActivity { + private TextInputEditText usernameInput; + private TextInputEditText passwordInput; + private ProgressBar loginProgress; + private TextView loginError; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + + usernameInput = findViewById(R.id.usernameInput); + passwordInput = findViewById(R.id.passwordInput); + loginProgress = findViewById(R.id.loginProgress); + loginError = findViewById(R.id.loginError); + Button loginButton = findViewById(R.id.loginButton); + + loginButton.setOnClickListener(v -> attemptLogin()); + } + + private void attemptLogin() { + String username = textOf(usernameInput); + String password = textOf(passwordInput); + + if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) { + showError("Username and password are required."); + return; + } + + setLoading(true); + LoginRequest request = new LoginRequest(username, password); + + ApiClient.getService().login(request).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + setLoading(false); + + if (!response.isSuccessful() || response.body() == null) { + showError("Login failed. Please try again."); + return; + } + + ApiResult result = response.body(); + if (result.getCode() != 200 || result.getData() == null) { + showError(result.getMessage() != null ? result.getMessage() : "Login failed."); + return; + } + + LoginResponse data = result.getData(); + new TokenStore(LoginActivity.this).saveToken(data.getToken()); + + String role = data.getUserInfo() != null ? data.getUserInfo().getRole() : "unknown"; + String displayName = data.getUserInfo() != null ? data.getUserInfo().getUsername() : username; + + Intent intent = new Intent(LoginActivity.this, RoleHomeActivity.class); + intent.putExtra("role", role); + intent.putExtra("username", displayName); + startActivity(intent); + finish(); + } + + @Override + public void onFailure(Call> call, Throwable t) { + setLoading(false); + showError("Network error: " + t.getMessage()); + } + }); + } + + private String textOf(TextInputEditText input) { + if (input.getText() == null) { + return ""; + } + return input.getText().toString().trim(); + } + + private void setLoading(boolean loading) { + loginProgress.setVisibility(loading ? View.VISIBLE : View.GONE); + loginError.setVisibility(View.GONE); + } + + private void showError(String message) { + loginError.setText(message); + loginError.setVisibility(View.VISIBLE); + } +} diff --git a/android/app/src/main/java/com/carmaintenance/LoginRequest.java b/android/app/src/main/java/com/carmaintenance/LoginRequest.java new file mode 100644 index 0000000..44c7a10 --- /dev/null +++ b/android/app/src/main/java/com/carmaintenance/LoginRequest.java @@ -0,0 +1,11 @@ +package com.carmaintenance; + +public class LoginRequest { + private final String username; + private final String password; + + public LoginRequest(String username, String password) { + this.username = username; + this.password = password; + } +} diff --git a/android/app/src/main/java/com/carmaintenance/LoginResponse.java b/android/app/src/main/java/com/carmaintenance/LoginResponse.java new file mode 100644 index 0000000..c915b91 --- /dev/null +++ b/android/app/src/main/java/com/carmaintenance/LoginResponse.java @@ -0,0 +1,47 @@ +package com.carmaintenance; + +public class LoginResponse { + private String token; + private UserInfo userInfo; + + public String getToken() { + return token; + } + + public UserInfo getUserInfo() { + return userInfo; + } + + public static class UserInfo { + private Integer userId; + private String username; + private String realName; + private String phone; + private String email; + private String role; + + public Integer getUserId() { + return userId; + } + + public String getUsername() { + return username; + } + + public String getRealName() { + return realName; + } + + public String getPhone() { + return phone; + } + + public String getEmail() { + return email; + } + + public String getRole() { + return role; + } + } +} diff --git a/android/app/src/main/java/com/carmaintenance/MainActivity.kt b/android/app/src/main/java/com/carmaintenance/MainActivity.kt deleted file mode 100644 index 5abe35a..0000000 --- a/android/app/src/main/java/com/carmaintenance/MainActivity.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.carmaintenance - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.ui.Modifier -import com.carmaintenance.ui.navigation.CarMaintenanceNavGraph -import com.carmaintenance.ui.theme.CarMaintenanceTheme -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - CarMaintenanceTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - CarMaintenanceNavGraph() - } - } - } - } -} diff --git a/android/app/src/main/java/com/carmaintenance/RoleHomeActivity.java b/android/app/src/main/java/com/carmaintenance/RoleHomeActivity.java new file mode 100644 index 0000000..9b876f1 --- /dev/null +++ b/android/app/src/main/java/com/carmaintenance/RoleHomeActivity.java @@ -0,0 +1,51 @@ +package com.carmaintenance; + +import android.content.Intent; +import android.os.Bundle; +import android.widget.Button; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +public class RoleHomeActivity extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_role_home); + + TextView userInfo = findViewById(R.id.userInfo); + Button primaryAction = findViewById(R.id.primaryAction); + Button secondaryAction = findViewById(R.id.secondaryAction); + Button logoutButton = findViewById(R.id.logoutButton); + + String role = getIntent().getStringExtra("role"); + String username = getIntent().getStringExtra("username"); + + if (role == null) { + role = "unknown"; + } + if (username == null) { + username = "user"; + } + + userInfo.setText("Welcome " + username + " (" + role + ")"); + + if ("admin".equalsIgnoreCase(role)) { + primaryAction.setText("Manage Users"); + secondaryAction.setText("Manage Orders"); + } else if ("staff".equalsIgnoreCase(role)) { + primaryAction.setText("Search Vehicles"); + secondaryAction.setText("Parts Lookup"); + } else { + primaryAction.setText("My Vehicles"); + secondaryAction.setText("My Appointments"); + } + + logoutButton.setOnClickListener(v -> { + new TokenStore(RoleHomeActivity.this).clear(); + Intent intent = new Intent(RoleHomeActivity.this, LoginActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + }); + } +} diff --git a/android/app/src/main/java/com/carmaintenance/TokenStore.java b/android/app/src/main/java/com/carmaintenance/TokenStore.java new file mode 100644 index 0000000..d404d99 --- /dev/null +++ b/android/app/src/main/java/com/carmaintenance/TokenStore.java @@ -0,0 +1,27 @@ +package com.carmaintenance; + +import android.content.Context; +import android.content.SharedPreferences; + +public class TokenStore { + private static final String PREFS_NAME = "auth_prefs"; + private static final String KEY_TOKEN = "token"; + + private final SharedPreferences preferences; + + public TokenStore(Context context) { + this.preferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } + + public void saveToken(String token) { + preferences.edit().putString(KEY_TOKEN, token).apply(); + } + + public String getToken() { + return preferences.getString(KEY_TOKEN, null); + } + + public void clear() { + preferences.edit().remove(KEY_TOKEN).apply(); + } +} diff --git a/android/app/src/main/java/com/carmaintenance/data/interceptor/AuthInterceptor.kt b/android/app/src/main/java/com/carmaintenance/data/interceptor/AuthInterceptor.kt deleted file mode 100644 index 3a68920..0000000 --- a/android/app/src/main/java/com/carmaintenance/data/interceptor/AuthInterceptor.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.carmaintenance.data.interceptor - -import android.content.Context -import com.carmaintenance.data.manager.TokenManager -import kotlinx.coroutines.runBlocking -import okhttp3.Interceptor -import okhttp3.Response -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class AuthInterceptor @Inject constructor( - private val tokenManager: TokenManager -) : Interceptor { - - override fun intercept(chain: Interceptor.Chain): Response { - val originalRequest = chain.request() - - // 获取Token - val token = runBlocking { tokenManager.getToken() } - - val newRequest = if (token != null) { - originalRequest.newBuilder() - .addHeader("Authorization", "Bearer $token") - .build() - } else { - originalRequest - } - - return chain.proceed(newRequest) - } -} diff --git a/android/app/src/main/java/com/carmaintenance/data/manager/TokenManager.kt b/android/app/src/main/java/com/carmaintenance/data/manager/TokenManager.kt deleted file mode 100644 index 5a55754..0000000 --- a/android/app/src/main/java/com/carmaintenance/data/manager/TokenManager.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.carmaintenance.data.manager - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.first -import javax.inject.Inject -import javax.inject.Singleton - -private val Context.dataStore: DataStore by preferencesDataStore(name = "user_prefs") - -@Singleton -class TokenManager @Inject constructor( - @ApplicationContext private val context: Context -) { - companion object { - private val TOKEN_KEY = stringPreferencesKey("auth_token") - private val USER_KEY = stringPreferencesKey("user_info") - } - - suspend fun saveToken(token: String) { - context.dataStore.edit { preferences -> - preferences[TOKEN_KEY] = token - } - } - - suspend fun getToken(): String? { - return context.dataStore.data.map { preferences -> - preferences[TOKEN_KEY] - }.first() - } - - suspend fun clearToken() { - context.dataStore.edit { preferences -> - preferences.remove(TOKEN_KEY) - preferences.remove(USER_KEY) - } - } - - fun getTokenFlow(): Flow { - return context.dataStore.data.map { preferences -> - preferences[TOKEN_KEY] - } - } - - suspend fun saveUserInfo(userInfoJson: String) { - context.dataStore.edit { preferences -> - preferences[USER_KEY] = userInfoJson - } - } - - suspend fun getUserInfo(): String? { - return context.dataStore.data.map { preferences -> - preferences[USER_KEY] - }.first() - } - - fun getUserInfoFlow(): Flow { - return context.dataStore.data.map { preferences -> - preferences[USER_KEY] - } - } -} diff --git a/android/app/src/main/java/com/carmaintenance/data/model/Appointment.kt b/android/app/src/main/java/com/carmaintenance/data/model/Appointment.kt deleted file mode 100644 index fbfab3c..0000000 --- a/android/app/src/main/java/com/carmaintenance/data/model/Appointment.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.carmaintenance.data.model - -import com.google.gson.annotations.SerializedName - -/** - * 预约实体 - */ -data class Appointment( - @SerializedName("appointmentId") - val appointmentId: Int, - @SerializedName("customerId") - val customerId: Int, - @SerializedName("vehicleId") - val vehicleId: Int, - @SerializedName("serviceType") - val serviceType: String, - @SerializedName("appointmentTime") - val appointmentTime: String, - @SerializedName("contactPhone") - val contactPhone: String, - @SerializedName("description") - val description: String? = null, - @SerializedName("status") - val status: String, - @SerializedName("createTime") - val createTime: String -) { - enum class Status(val displayName: String) { - PENDING("待确认"), - CONFIRMED("已确认"), - COMPLETED("已完成"), - CANCELLED("已取消"); - - companion object { - fun fromValue(value: String): Status { - return values().find { it.name.lowercase() == value } ?: PENDING - } - } - } - - val statusEnum: Status - get() = Status.fromValue(status) -} diff --git a/android/app/src/main/java/com/carmaintenance/data/model/AuthModels.kt b/android/app/src/main/java/com/carmaintenance/data/model/AuthModels.kt deleted file mode 100644 index d6623df..0000000 --- a/android/app/src/main/java/com/carmaintenance/data/model/AuthModels.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.carmaintenance.data.model - -import com.google.gson.annotations.SerializedName - -/** - * 登录请求 - */ -data class LoginRequest( - @SerializedName("username") - val username: String, - @SerializedName("password") - val password: String -) - -/** - * 登录响应数据 - */ -data class LoginResponseData( - @SerializedName("token") - val token: String, - @SerializedName("userInfo") - val userInfo: User -) diff --git a/android/app/src/main/java/com/carmaintenance/data/model/ServiceOrder.kt b/android/app/src/main/java/com/carmaintenance/data/model/ServiceOrder.kt deleted file mode 100644 index 5217c22..0000000 --- a/android/app/src/main/java/com/carmaintenance/data/model/ServiceOrder.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.carmaintenance.data.model - -import com.google.gson.annotations.SerializedName - -/** - * 维保工单实体 - */ -data class ServiceOrder( - @SerializedName("orderId") - val orderId: Int, - @SerializedName("orderNo") - val orderNo: String, - @SerializedName("vehicleId") - val vehicleId: Int, - @SerializedName("customerId") - val customerId: Int, - @SerializedName("serviceType") - val serviceType: String, - @SerializedName("appointmentTime") - val appointmentTime: String? = null, - @SerializedName("arrivalTime") - val arrivalTime: String? = null, - @SerializedName("startTime") - val startTime: String? = null, - @SerializedName("completeTime") - val completeTime: String? = null, - @SerializedName("staffId") - val staffId: Int? = null, - @SerializedName("currentMileage") - val currentMileage: Double? = null, - @SerializedName("faultDescription") - val faultDescription: String? = null, - @SerializedName("diagnosisResult") - val diagnosisResult: String? = null, - @SerializedName("serviceItems") - val serviceItems: String? = null, - @SerializedName("partsCost") - val partsCost: Double? = 0.0, - @SerializedName("laborCost") - val laborCost: Double? = 0.0, - @SerializedName("totalCost") - val totalCost: Double? = 0.0, - @SerializedName("status") - val status: String, - @SerializedName("paymentStatus") - val paymentStatus: String, - @SerializedName("remark") - val remark: String? = null, - @SerializedName("createTime") - val createTime: String -) { - enum class ServiceType(val displayName: String) { - MAINTENANCE("保养维护"), - REPAIR("维修服务"), - BEAUTY("美容服务"), - INSURANCE("保险代理"); - - companion object { - fun fromValue(value: String): ServiceType { - return values().find { it.name.lowercase() == value } ?: MAINTENANCE - } - } - } - - enum class Status(val displayName: String) { - PENDING("待处理"), - APPOINTED("已预约"), - IN_PROGRESS("进行中"), - COMPLETED("已完成"), - CANCELLED("已取消"); - - companion object { - fun fromValue(value: String): Status { - return values().find { it.name.lowercase() == value } ?: PENDING - } - } - } - - val serviceTypeEnum: ServiceType - get() = ServiceType.fromValue(serviceType) - - val statusEnum: Status - get() = Status.fromValue(status) -} diff --git a/android/app/src/main/java/com/carmaintenance/data/model/User.kt b/android/app/src/main/java/com/carmaintenance/data/model/User.kt deleted file mode 100644 index 1d969fe..0000000 --- a/android/app/src/main/java/com/carmaintenance/data/model/User.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.carmaintenance.data.model - -import com.google.gson.annotations.SerializedName - -/** - * 用户实体 - */ -data class User( - @SerializedName("userId") - val userId: Int, - @SerializedName("username") - val username: String, - @SerializedName("realName") - val realName: String, - @SerializedName("phone") - val phone: String, - @SerializedName("email") - val email: String? = null, - @SerializedName("role") - val role: String, - @SerializedName("status") - val status: Int -) { - enum class Role(val value: String) { - ADMIN("admin"), - STAFF("staff"), - CUSTOMER("customer"); - - companion object { - fun fromValue(value: String): Role { - return values().find { it.value == value } ?: CUSTOMER - } - } - } - - val roleEnum: Role - get() = Role.fromValue(role) - - val isActive: Boolean - get() = status == 1 -} diff --git a/android/app/src/main/java/com/carmaintenance/data/model/Vehicle.kt b/android/app/src/main/java/com/carmaintenance/data/model/Vehicle.kt deleted file mode 100644 index dd8ed6c..0000000 --- a/android/app/src/main/java/com/carmaintenance/data/model/Vehicle.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.carmaintenance.data.model - -import com.google.gson.annotations.SerializedName - -/** - * 车辆实体 - */ -data class Vehicle( - @SerializedName("vehicleId") - val vehicleId: Int, - @SerializedName("customerId") - val customerId: Int, - @SerializedName("licensePlate") - val licensePlate: String, - @SerializedName("brand") - val brand: String, - @SerializedName("model") - val model: String, - @SerializedName("color") - val color: String? = null, - @SerializedName("vin") - val vin: String? = null, - @SerializedName("engineNo") - val engineNo: String? = null, - @SerializedName("purchaseDate") - val purchaseDate: String? = null, - @SerializedName("mileage") - val mileage: Double? = 0.0, - @SerializedName("lastMaintenanceDate") - val lastMaintenanceDate: String? = null, - @SerializedName("nextMaintenanceDate") - val nextMaintenanceDate: String? = null, - @SerializedName("status") - val status: String = "normal" -) { - val displayName: String - get() = "$licensePlate - $brand $model" -} diff --git a/android/app/src/main/java/com/carmaintenance/data/remote/ApiResponse.kt b/android/app/src/main/java/com/carmaintenance/data/remote/ApiResponse.kt deleted file mode 100644 index 2a70693..0000000 --- a/android/app/src/main/java/com/carmaintenance/data/remote/ApiResponse.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.carmaintenance.data.remote - -import com.google.gson.annotations.SerializedName - -/** - * 通用API响应封装 - */ -data class ApiResponse( - @SerializedName("code") - val code: Int, - @SerializedName("message") - val message: String? = null, - @SerializedName("data") - val data: T? = null -) { - val isSuccess: Boolean - get() = code == 200 -} diff --git a/android/app/src/main/java/com/carmaintenance/data/remote/ApiService.kt b/android/app/src/main/java/com/carmaintenance/data/remote/ApiService.kt deleted file mode 100644 index cd0fba9..0000000 --- a/android/app/src/main/java/com/carmaintenance/data/remote/ApiService.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.carmaintenance.data.remote - -import com.carmaintenance.data.model.* -import retrofit2.http.* - -/** - * API服务接口 - */ -interface ApiService { - - // ==================== 认证相关 ==================== - @POST("auth/login") - suspend fun login(@Body request: LoginRequest): ApiResponse - - @POST("auth/logout") - suspend fun logout(): ApiResponse - - // ==================== 用户相关 ==================== - @GET("users") - suspend fun getUsers(): ApiResponse> - - @GET("users/{id}") - suspend fun getUser(@Path("id") id: Int): ApiResponse - - // ==================== 车辆相关 ==================== - @GET("vehicles") - suspend fun getVehicles(): ApiResponse> - - @GET("vehicles/{id}") - suspend fun getVehicle(@Path("id") id: Int): ApiResponse - - @GET("vehicles/customer/{customerId}") - suspend fun getVehiclesByCustomer(@Path("customerId") customerId: Int): ApiResponse> - - // ==================== 工单相关 ==================== - @GET("orders") - suspend fun getOrders(): ApiResponse> - - @GET("orders/{id}") - suspend fun getOrder(@Path("id") id: Int): ApiResponse - - @GET("orders/customer/{customerId}") - suspend fun getOrdersByCustomer(@Path("customerId") customerId: Int): ApiResponse> - - @PUT("orders/{id}") - suspend fun updateOrder( - @Path("id") id: Int, - @Body order: ServiceOrder - ): ApiResponse - - // ==================== 预约相关 ==================== - @GET("appointments") - suspend fun getAppointments(): ApiResponse> - - @GET("appointments/{id}") - suspend fun getAppointment(@Path("id") id: Int): ApiResponse - - @GET("appointments/customer/{customerId}") - suspend fun getAppointmentsByCustomer(@Path("customerId") customerId: Int): ApiResponse> - - @POST("appointments") - suspend fun createAppointment(@Body appointment: Appointment): ApiResponse - - @PUT("appointments/{id}/cancel") - suspend fun cancelAppointment(@Path("id") id: Int): ApiResponse - - // ==================== 客户相关 ==================== - @GET("customers") - suspend fun getCustomers(): ApiResponse> - - @GET("customers/user/{userId}") - suspend fun getCustomerByUserId(@Path("userId") userId: Int): ApiResponse -} - -/** - * 客户实体 - */ -data class Customer( - @SerializedName("customerId") - val customerId: Int, - @SerializedName("userId") - val userId: Int, - @SerializedName("customerNo") - val customerNo: String, - @SerializedName("idCard") - val idCard: String? = null, - @SerializedName("address") - val address: String? = null, - @SerializedName("gender") - val gender: String? = null, - @SerializedName("birthDate") - val birthDate: String? = null, - @SerializedName("membershipLevel") - val membershipLevel: String? = null, - @SerializedName("points") - val points: Int = 0 -) diff --git a/android/app/src/main/java/com/carmaintenance/data/repository/AppointmentRepository.kt b/android/app/src/main/java/com/carmaintenance/data/repository/AppointmentRepository.kt deleted file mode 100644 index 58684ce..0000000 --- a/android/app/src/main/java/com/carmaintenance/data/repository/AppointmentRepository.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.carmaintenance.data.repository - -import com.carmaintenance.data.model.Appointment -import com.carmaintenance.data.remote.ApiResponse -import com.carmaintenance.data.remote.ApiService -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class AppointmentRepository @Inject constructor( - private val apiService: ApiService -) { - suspend fun getAppointments(): Result> { - return try { - val response = apiService.getAppointments() - if (response.isSuccess) { - Result.success(response.data ?: emptyList()) - } else { - Result.failure(Exception(response.message ?: "获取预约列表失败")) - } - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun getCustomerAppointments(customerId: Int): Result> { - return try { - val response = apiService.getAppointmentsByCustomer(customerId) - if (response.isSuccess) { - Result.success(response.data ?: emptyList()) - } else { - Result.failure(Exception(response.message ?: "获取预约列表失败")) - } - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun createAppointment(appointment: Appointment): Result { - return try { - val response = apiService.createAppointment(appointment) - if (response.isSuccess && response.data != null) { - Result.success(response.data) - } else { - Result.failure(Exception(response.message ?: "创建预约失败")) - } - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun cancelAppointment(appointmentId: Int): Result { - return try { - val response = apiService.cancelAppointment(appointmentId) - if (response.isSuccess) { - Result.success(Unit) - } else { - Result.failure(Exception(response.message ?: "取消预约失败")) - } - } catch (e: Exception) { - Result.failure(e) - } - } -} diff --git a/android/app/src/main/java/com/carmaintenance/data/repository/AuthRepository.kt b/android/app/src/main/java/com/carmaintenance/data/repository/AuthRepository.kt deleted file mode 100644 index f4d801f..0000000 --- a/android/app/src/main/java/com/carmaintenance/data/repository/AuthRepository.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.carmaintenance.data.repository - -import com.carmaintenance.data.manager.TokenManager -import com.carmaintenance.data.model.LoginRequest -import com.carmaintenance.data.model.User -import com.carmaintenance.data.remote.ApiResponse -import com.carmaintenance.data.remote.ApiService -import com.google.gson.Gson -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class AuthRepository @Inject constructor( - private val apiService: ApiService, - private val tokenManager: TokenManager -) { - suspend fun login(username: String, password: String): Result { - return try { - val response = apiService.login(LoginRequest(username, password)) - if (response.isSuccess && response.data != null) { - // 保存Token - tokenManager.saveToken(response.data.token) - // 保存用户信息 - val userJson = Gson().toJson(response.data.userInfo) - tokenManager.saveUserInfo(userJson) - Result.success(response.data.userInfo) - } else { - Result.failure(Exception(response.message ?: "登录失败")) - } - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun logout(): Result { - return try { - apiService.logout() - tokenManager.clearToken() - Result.success(Unit) - } catch (e: Exception) { - tokenManager.clearToken() - Result.success(Unit) - } - } - - suspend fun getCurrentUser(): User? { - return try { - val userJson = tokenManager.getUserInfo() - if (userJson != null) { - Gson().fromJson(userJson, User::class.java) - } else { - null - } - } catch (e: Exception) { - null - } - } - - fun getCurrentUserFlow() = tokenManager.getUserInfoFlow() -} diff --git a/android/app/src/main/java/com/carmaintenance/data/repository/OrderRepository.kt b/android/app/src/main/java/com/carmaintenance/data/repository/OrderRepository.kt deleted file mode 100644 index d223d10..0000000 --- a/android/app/src/main/java/com/carmaintenance/data/repository/OrderRepository.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.carmaintenance.data.repository - -import com.carmaintenance.data.model.ServiceOrder -import com.carmaintenance.data.remote.ApiResponse -import com.carmaintenance.data.remote.ApiService -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class OrderRepository @Inject constructor( - private val apiService: ApiService -) { - suspend fun getOrders(): Result> { - return try { - val response = apiService.getOrders() - if (response.isSuccess) { - Result.success(response.data ?: emptyList()) - } else { - Result.failure(Exception(response.message ?: "获取工单列表失败")) - } - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun getCustomerOrders(customerId: Int): Result> { - return try { - val response = apiService.getOrdersByCustomer(customerId) - if (response.isSuccess) { - Result.success(response.data ?: emptyList()) - } else { - Result.failure(Exception(response.message ?: "获取工单列表失败")) - } - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun updateOrder(orderId: Int, order: ServiceOrder): Result { - return try { - val response = apiService.updateOrder(orderId, order) - if (response.isSuccess && response.data != null) { - Result.success(response.data) - } else { - Result.failure(Exception(response.message ?: "更新工单失败")) - } - } catch (e: Exception) { - Result.failure(e) - } - } -} diff --git a/android/app/src/main/java/com/carmaintenance/data/repository/VehicleRepository.kt b/android/app/src/main/java/com/carmaintenance/data/repository/VehicleRepository.kt deleted file mode 100644 index b626797..0000000 --- a/android/app/src/main/java/com/carmaintenance/data/repository/VehicleRepository.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.carmaintenance.data.repository - -import com.carmaintenance.data.model.Vehicle -import com.carmaintenance.data.remote.ApiResponse -import com.carmaintenance.data.remote.ApiService -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class VehicleRepository @Inject constructor( - private val apiService: ApiService -) { - suspend fun getVehicles(): Result> { - return try { - val response = apiService.getVehicles() - if (response.isSuccess) { - Result.success(response.data ?: emptyList()) - } else { - Result.failure(Exception(response.message ?: "获取车辆列表失败")) - } - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun getCustomerVehicles(customerId: Int): Result> { - return try { - val response = apiService.getVehiclesByCustomer(customerId) - if (response.isSuccess) { - Result.success(response.data ?: emptyList()) - } else { - Result.failure(Exception(response.message ?: "获取车辆列表失败")) - } - } catch (e: Exception) { - Result.failure(e) - } - } -} diff --git a/android/app/src/main/java/com/carmaintenance/di/NetworkModule.kt b/android/app/src/main/java/com/carmaintenance/di/NetworkModule.kt deleted file mode 100644 index a0c0adb..0000000 --- a/android/app/src/main/java/com/carmaintenance/di/NetworkModule.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.carmaintenance.di - -import com.carmaintenance.BuildConfig -import com.carmaintenance.data.remote.ApiService -import com.carmaintenance.data.interceptor.AuthInterceptor -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import java.util.concurrent.TimeUnit -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object NetworkModule { - - @Provides - @Singleton - fun provideLoggingInterceptor(): HttpLoggingInterceptor { - return HttpLoggingInterceptor().apply { - level = if (BuildConfig.DEBUG) { - HttpLoggingInterceptor.Level.BODY - } else { - HttpLoggingInterceptor.Level.NONE - } - } - } - - @Provides - @Singleton - fun provideOkHttpClient( - loggingInterceptor: HttpLoggingInterceptor, - authInterceptor: AuthInterceptor - ): OkHttpClient { - return OkHttpClient.Builder() - .addInterceptor(authInterceptor) - .addInterceptor(loggingInterceptor) - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .build() - } - - @Provides - @Singleton - fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit { - return Retrofit.Builder() - .baseUrl(BuildConfig.API_BASE_URL) - .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create()) - .build() - } - - @Provides - @Singleton - fun provideApiService(retrofit: Retrofit): ApiService { - return retrofit.create(ApiService::class.java) - } -} diff --git a/android/app/src/main/java/com/carmaintenance/ui/navigation/Navigation.kt b/android/app/src/main/java/com/carmaintenance/ui/navigation/Navigation.kt deleted file mode 100644 index 206447b..0000000 --- a/android/app/src/main/java/com/carmaintenance/ui/navigation/Navigation.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.carmaintenance.ui.navigation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavHostController -import androidx.navigation.NavType -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.navArgument -import com.carmaintenance.ui.screen.LoginScreen -import com.carmaintenance.ui.screen.admin.AdminDashboardScreen -import com.carmaintenance.ui.screen.customer.CustomerDashboardScreen -import com.carmaintenance.ui.screen.customer.CustomerAppointmentsScreen -import com.carmaintenance.ui.screen.customer.CustomerOrdersScreen -import com.carmaintenance.ui.screen.customer.CustomerVehiclesScreen -import com.carmaintenance.ui.viewmodel.AuthViewModel -import com.carmaintenance.ui.viewmodel.AuthUiState - -@Composable -fun CarMaintenanceNavGraph( - navController: NavHostController, - authViewModel: AuthViewModel = hiltViewModel() -) { - val uiState by authViewModel.uiState.collectAsState() - - LaunchedEffect(uiState) { - when (uiState) { - is AuthUiState.LoggedIn -> { - val user = (uiState as AuthUiState.LoggedIn).user - val route = when (user.role) { - "admin" -> Screen.AdminDashboard.route - "staff" -> Screen.StaffDashboard.route - "customer" -> Screen.CustomerDashboard.route - else -> Screen.Login.route - } - navController.navigate(route) { - popUpTo(Screen.Login.route) { inclusive = true } - } - } - is AuthUiState.NotLoggedIn -> { - navController.navigate(Screen.Login.route) { - popUpTo(0) { inclusive = true } - } - } - else -> {} - } - } - - NavHost( - navController = navController, - startDestination = Screen.Login.route - ) { - composable(Screen.Login.route) { - LoginScreen( - onLoginSuccess = { - // 导航由LaunchedEffect处理 - } - ) - } - - composable(Screen.CustomerDashboard.route) { - CustomerDashboardScreen( - onNavigateBack = { authViewModel.logout() } - ) - } - - composable(Screen.CustomerVehicles.route) { - CustomerVehiclesScreen( - onNavigateBack = { navController.popBackStack() } - ) - } - - composable(Screen.CustomerOrders.route) { - CustomerOrdersScreen( - onNavigateBack = { navController.popBackStack() } - ) - } - - composable(Screen.CustomerAppointments.route) { - CustomerAppointmentsScreen( - onNavigateBack = { navController.popBackStack() } - ) - } - - composable(Screen.AdminDashboard.route) { - AdminDashboardScreen( - onNavigateBack = { authViewModel.logout() } - ) - } - - composable(Screen.StaffDashboard.route) { - // TODO: 实现工作人员仪表板 - } - } -} diff --git a/android/app/src/main/java/com/carmaintenance/ui/navigation/Screen.kt b/android/app/src/main/java/com/carmaintenance/ui/navigation/Screen.kt deleted file mode 100644 index fb1000d..0000000 --- a/android/app/src/main/java/com/carmaintenance/ui/navigation/Screen.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.carmaintenance.ui.navigation - -sealed class Screen(val route: String) { - object Login : Screen("login") - object CustomerDashboard : Screen("customer_dashboard") - object CustomerVehicles : Screen("customer_vehicles") - object CustomerOrders : Screen("customer_orders") - object CustomerAppointments : Screen("customer_appointments") - object AdminDashboard : Screen("admin_dashboard") - object StaffDashboard : Screen("staff_dashboard") -} diff --git a/android/app/src/main/java/com/carmaintenance/ui/screen/LoginScreen.kt b/android/app/src/main/java/com/carmaintenance/ui/screen/LoginScreen.kt deleted file mode 100644 index 8a1333f..0000000 --- a/android/app/src/main/java/com/carmaintenance/ui/screen/LoginScreen.kt +++ /dev/null @@ -1,198 +0,0 @@ -package com.carmaintenance.ui.screen - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Visibility -import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.carmaintenance.ui.viewmodel.AuthViewModel - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun LoginScreen( - onLoginSuccess: () -> Unit, - viewModel: AuthViewModel = hiltViewModel() -) { - var username by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } - var passwordVisible by remember { mutableStateOf(false) } - var selectedRole by remember { mutableStateOf("customer") } - val uiState by viewModel.uiState.collectAsState() - - LaunchedEffect(uiState) { - if (uiState is AuthUiState.LoggedIn) { - onLoginSuccess() - } - } - - Scaffold( - topBar = { - TopAppBar( - title = { Text("车管家系统") }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primary, - titleContentColor = MaterialTheme.colorScheme.onPrimary - ) - ) - } - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "欢迎登录", - style = MaterialTheme.typography.headlineLarge, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(bottom = 48.dp) - ) - - // 用户名输入 - OutlinedTextField( - value = username, - onValueChange = { username = it }, - label = { Text("用户名") }, - singleLine = true, - modifier = Modifier.fillMaxWidth(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - // 密码输入 - OutlinedTextField( - value = password, - onValueChange = { password = it }, - label = { Text("密码") }, - singleLine = true, - modifier = Modifier.fillMaxWidth(), - visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), - trailingIcon = { - IconButton(onClick = { passwordVisible = !passwordVisible }) { - Icon( - imageVector = if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, - contentDescription = if (passwordVisible) "隐藏密码" else "显示密码" - ) - } - } - ) - - Spacer(modifier = Modifier.height(16.dp)) - - // 角色选择 - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "选择角色", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(bottom = 8.dp) - ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - FilterChip( - selected = selectedRole == "admin", - onClick = { selectedRole = "admin" }, - label = { Text("管理员") }, - modifier = Modifier.padding(end = 8.dp) - ) - FilterChip( - selected = selectedRole == "staff", - onClick = { selectedRole = "staff" }, - label = { Text("工作人员") }, - modifier = Modifier.padding(end = 8.dp) - ) - FilterChip( - selected = selectedRole == "customer", - onClick = { selectedRole = "customer" }, - label = { Text("客户") } - ) - } - } - } - - Spacer(modifier = Modifier.height(32.dp)) - - // 登录按钮 - Button( - onClick = { - viewModel.login(username, password) - }, - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - enabled = username.isNotBlank() && password.isNotBlank() && uiState !is AuthUiState.Loading - ) { - if (uiState is AuthUiState.Loading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary - ) - } else { - Text("登录", style = MaterialTheme.typography.titleMedium) - } - } - - // 错误信息 - if (uiState is AuthUiState.Error) { - Spacer(modifier = Modifier.height(16.dp)) - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ), - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = (uiState as AuthUiState.Error).message, - color = MaterialTheme.colorScheme.onErrorContainer, - modifier = Modifier.padding(16.dp) - ) - } - } - - Spacer(modifier = Modifier.height(24.dp)) - - // 测试账号提示 - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer - ), - modifier = Modifier.fillMaxWidth() - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "测试账号", - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onSecondaryContainer, - modifier = Modifier.padding(bottom = 8.dp) - ) - Text( - text = "管理员: admin / 123456\n工作人员: staff001 / 123456\n客户: customer001 / 123456", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSecondaryContainer - ) - } - } - } - } -} diff --git a/android/app/src/main/java/com/carmaintenance/ui/screen/admin/AdminDashboardScreen.kt b/android/app/src/main/java/com/carmaintenance/ui/screen/admin/AdminDashboardScreen.kt deleted file mode 100644 index ab6272c..0000000 --- a/android/app/src/main/java/com/carmaintenance/ui/screen/admin/AdminDashboardScreen.kt +++ /dev/null @@ -1,166 +0,0 @@ -package com.carmaintenance.ui.screen.admin - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.carmaintenance.ui.viewmodel.CustomerViewModel - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AdminDashboardScreen( - onNavigateBack: () -> Unit, - viewModel: CustomerViewModel = hiltViewModel() -) { - val vehicles by viewModel.vehicles.collectAsState() - val isLoading by viewModel.isLoadingVehicles.collectAsState() - - Scaffold( - topBar = { - TopAppBar( - title = { Text("管理员控制台") }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "退出") - } - } - ) - } - ) { paddingValues -> - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - item { - Text( - text = "系统概览", - style = MaterialTheme.typography.titleLarge - ) - } - - item { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - StatCard(title = "用户总数", value = "3", modifier = Modifier.weight(1f)) - StatCard(title = "车辆总数", value = "${vehicles.size}", modifier = Modifier.weight(1f)) - } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - StatCard(title = "工单总数", value = "0", modifier = Modifier.weight(1f)) - StatCard(title = "库存预警", value = "0", modifier = Modifier.weight(1f)) - } - } - - item { - Text( - text = "管理功能", - style = MaterialTheme.typography.titleLarge - ) - } - - item { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - AdminMenuItem( - icon = "👥", - title = "用户管理", - subtitle = "管理系统用户" - ) - Divider() - AdminMenuItem( - icon = "🚗", - title = "车辆管理", - subtitle = "管理车辆档案" - ) - Divider() - AdminMenuItem( - icon = "📋", - title = "工单管理", - subtitle = "管理维保工单" - ) - Divider() - AdminMenuItem( - icon = "📦", - title = "配件管理", - subtitle = "管理配件库存" - ) - Divider() - AdminMenuItem( - icon = "📅", - title = "预约管理", - subtitle = "管理客户预约" - ) - } - } - } - } - } -} - -@Composable -fun StatCard(title: String, value: String, modifier: Modifier = Modifier) { - Card( - modifier = modifier - ) { - Column( - modifier = Modifier.padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = value, - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = title, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) - ) - } - } -} - -@Composable -fun AdminMenuItem(icon: String, title: String, subtitle: String) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = icon, - style = MaterialTheme.typography.headlineMedium - ) - Spacer(modifier = Modifier.width(16.dp)) - Column { - Text( - text = title, - style = MaterialTheme.typography.titleMedium - ) - Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) - ) - } - } -} diff --git a/android/app/src/main/java/com/carmaintenance/ui/screen/customer/CustomerAppointmentsScreen.kt b/android/app/src/main/java/com/carmaintenance/ui/screen/customer/CustomerAppointmentsScreen.kt deleted file mode 100644 index 74104e3..0000000 --- a/android/app/src/main/java/com/carmaintenance/ui/screen/customer/CustomerAppointmentsScreen.kt +++ /dev/null @@ -1,123 +0,0 @@ -package com.carmaintenance.ui.screen.customer - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.carmaintenance.ui.viewmodel.CustomerViewModel - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun CustomerAppointmentsScreen( - onNavigateBack: () -> Unit, - viewModel: CustomerViewModel = hiltViewModel() -) { - val appointments by viewModel.appointments.collectAsState() - val isLoading by viewModel.isLoadingAppointments.collectAsState() - - Scaffold( - topBar = { - TopAppBar( - title = { Text("我的预约") }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回") - } - } - ) - }, - floatingActionButton = { - FloatingActionButton( - onClick = { /* TODO: 打开预约表单 */ } - ) { - Icon(Icons.Default.Add, contentDescription = "新建预约") - } - } - ) { paddingValues -> - if (isLoading) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } else if (appointments.isEmpty()) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - Text("暂无预约记录") - } - } else { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - items(appointments) { appointment -> - AppointmentCard(appointment = appointment) - } - } - } - } -} - -@Composable -fun AppointmentCard(appointment: com.carmaintenance.data.model.Appointment) { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = appointment.serviceTypeEnum.displayName, - style = MaterialTheme.typography.titleMedium - ) - Surface( - color = when (appointment.statusEnum) { - com.carmaintenance.data.model.Appointment.Status.PENDING -> MaterialTheme.colorScheme.tertiaryContainer - com.carmaintenance.data.model.Appointment.Status.CONFIRMED -> MaterialTheme.colorScheme.primaryContainer - com.carmaintenance.data.model.Appointment.Status.COMPLETED -> MaterialTheme.colorScheme.secondaryContainer - com.carmaintenance.data.model.Appointment.Status.CANCELLED -> MaterialTheme.colorScheme.errorContainer - }, - shape = MaterialTheme.shapes.small - ) { - Text( - text = appointment.statusEnum.displayName, - style = MaterialTheme.typography.labelSmall, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) - ) - } - } - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "预约时间: ${appointment.appointmentTime}", - style = MaterialTheme.typography.bodyMedium - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "联系电话: ${appointment.contactPhone}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) - ) - } - } -} diff --git a/android/app/src/main/java/com/carmaintenance/ui/screen/customer/CustomerDashboardScreen.kt b/android/app/src/main/java/com/carmaintenance/ui/screen/customer/CustomerDashboardScreen.kt deleted file mode 100644 index d14ca34..0000000 --- a/android/app/src/main/java/com/carmaintenance/ui/screen/customer/CustomerDashboardScreen.kt +++ /dev/null @@ -1,311 +0,0 @@ -package com.carmaintenance.ui.screen.customer - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.carmaintenance.data.model.Vehicle -import com.carmaintenance.ui.viewmodel.AuthViewModel -import com.carmaintenance.ui.viewmodel.CustomerViewModel - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun CustomerDashboardScreen( - onNavigateBack: () -> Unit, - viewModel: CustomerViewModel = hiltViewModel(), - authViewModel: AuthViewModel = hiltViewModel() -) { - val user by authViewModel.currentUser.collectAsState() - val vehicles by viewModel.vehicles.collectAsState() - val isLoading by viewModel.isLoadingVehicles.collectAsState() - - LaunchedEffect(user) { - user?.let { viewModel.loadCustomerData(it.userId) } - } - - Scaffold( - topBar = { - TopAppBar( - title = { Text("客户中心") }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.Default.ExitToApp, contentDescription = "退出") - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primary, - titleContentColor = MaterialTheme.colorScheme.onPrimary, - navigationIconContentColor = MaterialTheme.colorScheme.onPrimary - ) - ) - } - ) { paddingValues -> - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - item { - // 用户信息卡片 - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ) - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Person, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(16.dp)) - Column { - Text( - text = user?.realName ?: "客户", - style = MaterialTheme.typography.titleLarge - ) - Text( - text = "欢迎回来!", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) - ) - } - } - } - } - } - - item { - Text( - text = "快捷功能", - style = MaterialTheme.typography.titleLarge - ) - } - - item { - // 功能菜单 - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - FeatureCard( - icon = Icons.Default.DirectionsCar, - title = "我的车辆", - subtitle = "${vehicles.size} 辆", - modifier = Modifier.weight(1f) - ) - FeatureCard( - icon = Icons.Default.Receipt, - title = "维保记录", - subtitle = "查看详情", - modifier = Modifier.weight(1f) - ) - } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - FeatureCard( - icon = Icons.Default.Event, - title = "我的预约", - subtitle = "在线预约", - modifier = Modifier.weight(1f) - ) - FeatureCard( - icon = Icons.Default.Notifications, - title = "消息通知", - subtitle = "暂无消息", - modifier = Modifier.weight(1f) - ) - } - } - - item { - Text( - text = "我的车辆", - style = MaterialTheme.typography.titleLarge - ) - } - - if (isLoading) { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(32.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } - } else if (vehicles.isEmpty()) { - item { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - imageVector = Icons.Default.DirectionsCar, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = "暂无车辆", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) - ) - } - } - } - } else { - items(vehicles) { vehicle -> - VehicleCard(vehicle = vehicle) - } - } - } - } -} - -@Composable -fun FeatureCard( - icon: ImageVector, - title: String, - subtitle: String, - modifier: Modifier = Modifier -) { - Card( - modifier = modifier.height(100.dp), - onClick = { /* TODO: 导航 */ } - ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(12.dp), - verticalArrangement = Arrangement.Center - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = title, - style = MaterialTheme.typography.titleSmall - ) - Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) - ) - } - } -} - -@Composable -fun VehicleCard(vehicle: Vehicle) { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text( - text = vehicle.licensePlate, - style = MaterialTheme.typography.titleLarge - ) - Text( - text = "${vehicle.brand} ${vehicle.model}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) - ) - } - Surface( - color = MaterialTheme.colorScheme.primaryContainer, - shape = MaterialTheme.shapes.small - ) { - Text( - text = "正常", - style = MaterialTheme.typography.labelSmall, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) - ) - } - } - Spacer(modifier = Modifier.height(12.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - InfoItem( - icon = Icons.Default.Palette, - label = "颜色", - value = vehicle.color ?: "-" - ) - InfoItem( - icon = Icons.Default.Speed, - label = "里程", - value = "${vehicle.mileage?.toInt() ?: 0} km" - ) - InfoItem( - icon = Icons.Default.DateRange, - label = "保养", - value = vehicle.lastMaintenanceDate ?: "-" - ) - } - } - } -} - -@Composable -fun InfoItem(icon: ImageVector, label: String, value: String) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = label, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) - ) - Text( - text = value, - style = MaterialTheme.typography.bodySmall - ) - } -} diff --git a/android/app/src/main/java/com/carmaintenance/ui/screen/customer/CustomerOrdersScreen.kt b/android/app/src/main/java/com/carmaintenance/ui/screen/customer/CustomerOrdersScreen.kt deleted file mode 100644 index 22777fd..0000000 --- a/android/app/src/main/java/com/carmaintenance/ui/screen/customer/CustomerOrdersScreen.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.carmaintenance.ui.screen.customer - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.carmaintenance.ui.viewmodel.CustomerViewModel - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun CustomerOrdersScreen( - onNavigateBack: () -> Unit, - viewModel: CustomerViewModel = hiltViewModel() -) { - val orders by viewModel.orders.collectAsState() - val isLoading by viewModel.isLoadingOrders.collectAsState() - - Scaffold( - topBar = { - TopAppBar( - title = { Text("维保记录") }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回") - } - } - ) - } - ) { paddingValues -> - if (isLoading) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } else if (orders.isEmpty()) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - Text("暂无维保记录") - } - } else { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - items(orders) { order -> - OrderCard(order = order) - } - } - } - } -} - -@Composable -fun OrderCard(order: com.carmaintenance.data.model.ServiceOrder) { - Card( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = order.orderNo, - style = MaterialTheme.typography.titleMedium - ) - Surface( - color = MaterialTheme.colorScheme.primaryContainer, - shape = MaterialTheme.shapes.small - ) { - Text( - text = order.statusEnum.displayName, - style = MaterialTheme.typography.labelSmall, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) - ) - } - } - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = order.serviceTypeEnum.displayName, - style = MaterialTheme.typography.bodyMedium - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "费用: ¥${order.totalCost ?: 0.0}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = order.createTime, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) - ) - } - } -} diff --git a/android/app/src/main/java/com/carmaintenance/ui/screen/customer/CustomerVehiclesScreen.kt b/android/app/src/main/java/com/carmaintenance/ui/screen/customer/CustomerVehiclesScreen.kt deleted file mode 100644 index 10f5be8..0000000 --- a/android/app/src/main/java/com/carmaintenance/ui/screen/customer/CustomerVehiclesScreen.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.carmaintenance.ui.screen.customer - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.carmaintenance.ui.viewmodel.CustomerViewModel - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun CustomerVehiclesScreen( - onNavigateBack: () -> Unit, - viewModel: CustomerViewModel = hiltViewModel() -) { - val vehicles by viewModel.vehicles.collectAsState() - val isLoading by viewModel.isLoadingVehicles.collectAsState() - - Scaffold( - topBar = { - TopAppBar( - title = { Text("我的车辆") }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回") - } - } - ) - } - ) { paddingValues -> - if (isLoading) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } else { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - items(vehicles) { vehicle -> - VehicleCard(vehicle = vehicle) - } - } - } - } -} diff --git a/android/app/src/main/java/com/carmaintenance/ui/theme/Color.kt b/android/app/src/main/java/com/carmaintenance/ui/theme/Color.kt deleted file mode 100644 index 4509a79..0000000 --- a/android/app/src/main/java/com/carmaintenance/ui/theme/Color.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.carmaintenance.ui.theme - -import androidx.compose.ui.graphics.Color - -// 主色调 -val Primary = Color(0xFF1976D2) -val PrimaryDark = Color(0xFF1565C0) -val PrimaryLight = Color(0xFF42A5F5) - -// 辅助色 -val Secondary = Color(0xFF03DAC6) -val SecondaryVariant = Color(0xFF018786) - -// 背景色 -val Background = Color(0xFFFAFAFA) -val Surface = Color(0xFFFFFFFF) - -// 状态色 -val Success = Color(0xFF4CAF50) -val Warning = Color(0xFFFFC107) -val Error = Color(0xFFF44336) -val Info = Color(0xFF2196F3) - -// 文字颜色 -val TextPrimary = Color(0xFF212121) -val TextSecondary = Color(0xFF757575) diff --git a/android/app/src/main/java/com/carmaintenance/ui/theme/Theme.kt b/android/app/src/main/java/com/carmaintenance/ui/theme/Theme.kt deleted file mode 100644 index d1c5d4b..0000000 --- a/android/app/src/main/java/com/carmaintenance/ui/theme/Theme.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.carmaintenance.ui.theme - -import android.app.Activity -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.core.view.WindowCompat - -private val DarkColorScheme = darkColorScheme( - primary = Primary, - secondary = Secondary, - error = Error -) - -private val LightColorScheme = lightColorScheme( - primary = Primary, - primaryContainer = PrimaryLight, - secondary = Secondary, - secondaryContainer = SecondaryVariant, - background = Background, - surface = Surface, - error = Error, - onPrimary = androidx.compose.ui.graphics.Color.White, - onSecondary = androidx.compose.ui.graphics.Color.Black, - onBackground = TextPrimary, - onSurface = TextPrimary -) - -@Composable -fun CarMaintenanceTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - dynamicColor: Boolean = false, - content: @Composable () -> Unit -) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme - } - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) -} diff --git a/android/app/src/main/java/com/carmaintenance/ui/theme/Type.kt b/android/app/src/main/java/com/carmaintenance/ui/theme/Type.kt deleted file mode 100644 index ae98531..0000000 --- a/android/app/src/main/java/com/carmaintenance/ui/theme/Type.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.carmaintenance.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -val Typography = Typography( - displayLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Bold, - fontSize = 57.sp, - lineHeight = 64.sp, - letterSpacing = 0.sp - ), - displayMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Bold, - fontSize = 45.sp, - lineHeight = 52.sp, - letterSpacing = 0.sp - ), - displaySmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Bold, - fontSize = 36.sp, - lineHeight = 44.sp, - letterSpacing = 0.sp - ), - headlineLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Bold, - fontSize = 32.sp, - lineHeight = 40.sp, - letterSpacing = 0.sp - ), - headlineMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Bold, - fontSize = 28.sp, - lineHeight = 36.sp, - letterSpacing = 0.sp - ), - headlineSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - lineHeight = 32.sp, - letterSpacing = 0.sp - ), - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.SemiBold, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - titleMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.SemiBold, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), - titleSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ), - bodyMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.25.sp - ), - bodySmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.4.sp - ), - labelLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - labelMedium = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) -) diff --git a/android/app/src/main/java/com/carmaintenance/ui/viewmodel/AuthViewModel.kt b/android/app/src/main/java/com/carmaintenance/ui/viewmodel/AuthViewModel.kt deleted file mode 100644 index 1ec73c9..0000000 --- a/android/app/src/main/java/com/carmaintenance/ui/viewmodel/AuthViewModel.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.carmaintenance.ui.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.carmaintenance.data.model.User -import com.carmaintenance.data.repository.AuthRepository -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class AuthViewModel @Inject constructor( - private val authRepository: AuthRepository -) : ViewModel() { - - private val _uiState = MutableStateFlow(AuthUiState.Initial) - val uiState: StateFlow = _uiState.asStateFlow() - - private val _isLoggedIn = MutableStateFlow(false) - val isLoggedIn: StateFlow = _isLoggedIn.asStateFlow() - - private val _currentUser = MutableStateFlow(null) - val currentUser: StateFlow = _currentUser.asStateFlow() - - init { - checkLoginStatus() - } - - private fun checkLoginStatus() { - viewModelScope.launch { - val user = authRepository.getCurrentUser() - if (user != null) { - _currentUser.value = user - _isLoggedIn.value = true - _uiState.value = AuthUiState.LoggedIn(user) - } else { - _isLoggedIn.value = false - _uiState.value = AuthUiState.NotLoggedIn - } - } - } - - fun login(username: String, password: String) { - if (username.isBlank() || password.isBlank()) { - _uiState.value = AuthUiState.Error("用户名和密码不能为空") - return - } - - viewModelScope.launch { - _uiState.value = AuthUiState.Loading - val result = authRepository.login(username, password) - result.onSuccess { user -> - _currentUser.value = user - _isLoggedIn.value = true - _uiState.value = AuthUiState.LoggedIn(user) - }.onFailure { exception -> - _uiState.value = AuthUiState.Error(exception.message ?: "登录失败") - } - } - } - - fun logout() { - viewModelScope.launch { - authRepository.logout() - _currentUser.value = null - _isLoggedIn.value = false - _uiState.value = AuthUiState.NotLoggedIn - } - } -} - -sealed class AuthUiState { - object Initial : AuthUiState() - object Loading : AuthUiState() - object NotLoggedIn : AuthUiState() - data class LoggedIn(val user: User) : AuthUiState() - data class Error(val message: String) : AuthUiState() -} diff --git a/android/app/src/main/java/com/carmaintenance/ui/viewmodel/CustomerViewModel.kt b/android/app/src/main/java/com/carmaintenance/ui/viewmodel/CustomerViewModel.kt deleted file mode 100644 index e21ffe1..0000000 --- a/android/app/src/main/java/com/carmaintenance/ui/viewmodel/CustomerViewModel.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.carmaintenance.ui.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.carmaintenance.data.model.Appointment -import com.carmaintenance.data.model.ServiceOrder -import com.carmaintenance.data.model.Vehicle -import com.carmaintenance.data.repository.AppointmentRepository -import com.carmaintenance.data.repository.OrderRepository -import com.carmaintenance.data.repository.VehicleRepository -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class CustomerViewModel @Inject constructor( - private val vehicleRepository: VehicleRepository, - private val orderRepository: OrderRepository, - private val appointmentRepository: AppointmentRepository -) : ViewModel() { - - // 车辆相关 - private val _vehicles = MutableStateFlow>(emptyList()) - val vehicles: StateFlow> = _vehicles.asStateFlow() - - private val _isLoadingVehicles = MutableStateFlow(false) - val isLoadingVehicles: StateFlow = _isLoadingVehicles.asStateFlow() - - // 工单相关 - private val _orders = MutableStateFlow>(emptyList()) - val orders: StateFlow> = _orders.asStateFlow() - - private val _isLoadingOrders = MutableStateFlow(false) - val isLoadingOrders: StateFlow = _isLoadingOrders.asStateFlow() - - // 预约相关 - private val _appointments = MutableStateFlow>(emptyList()) - val appointments: StateFlow> = _appointments.asStateFlow() - - private val _isLoadingAppointments = MutableStateFlow(false) - val isLoadingAppointments: StateFlow = _isLoadingAppointments.asStateFlow() - - private val _message = MutableStateFlow(null) - val message: StateFlow = _message.asStateFlow() - - fun loadCustomerData(customerId: Int) { - loadVehicles(customerId) - loadOrders(customerId) - loadAppointments(customerId) - } - - fun loadVehicles(customerId: Int) { - viewModelScope.launch { - _isLoadingVehicles.value = true - vehicleRepository.getCustomerVehicles(customerId) - .onSuccess { _vehicles.value = it } - .onFailure { _message.value = it.message } - _isLoadingVehicles.value = false - } - } - - fun loadOrders(customerId: Int) { - viewModelScope.launch { - _isLoadingOrders.value = true - orderRepository.getCustomerOrders(customerId) - .onSuccess { _orders.value = it } - .onFailure { _message.value = it.message } - _isLoadingOrders.value = false - } - } - - fun loadAppointments(customerId: Int) { - viewModelScope.launch { - _isLoadingAppointments.value = true - appointmentRepository.getCustomerAppointments(customerId) - .onSuccess { _appointments.value = it } - .onFailure { _message.value = it.message } - _isLoadingAppointments.value = false - } - } - - fun createAppointment(appointment: Appointment) { - viewModelScope.launch { - appointmentRepository.createAppointment(appointment) - .onSuccess { - _message.value = "预约创建成功" - if (appointment.customerId > 0) { - loadAppointments(appointment.customerId) - } - } - .onFailure { _message.value = it.message ?: "预约创建失败" } - } - } - - fun cancelAppointment(appointmentId: Int, customerId: Int) { - viewModelScope.launch { - appointmentRepository.cancelAppointment(appointmentId) - .onSuccess { - _message.value = "预约已取消" - loadAppointments(customerId) - } - .onFailure { _message.value = it.message ?: "取消预约失败" } - } - } - - fun clearMessage() { - _message.value = null - } -} diff --git a/android/app/src/main/res/drawable/ic_launcher.xml b/android/app/src/main/res/drawable/ic_launcher.xml new file mode 100644 index 0000000..4832576 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1,4 @@ + + + + diff --git a/android/app/src/main/res/layout/activity_login.xml b/android/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..d417351 --- /dev/null +++ b/android/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + +