This commit is contained in:
wangziqi
2026-01-08 13:23:09 +08:00
parent 177cfd9b9d
commit 35098f3028
57 changed files with 4725 additions and 55 deletions

5
.claude/plugins.json Normal file
View File

@@ -0,0 +1,5 @@
{
"version": 1,
"lastUpdated": 1767835571272,
"plugins": []
}

View File

@@ -0,0 +1,13 @@
{
"permissions": {
"allow": [
"mcp__Docx_MCP__create_document",
"mcp__Docx_MCP__add_heading",
"mcp__Docx_MCP__add_paragraph",
"Bash(python:*)",
"Bash(python3:*)",
"Bash(py generate_word.py:*)",
"Bash(where:*)"
]
}
}

16
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
*.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

303
android/PROJECT_SUMMARY.md Normal file
View File

@@ -0,0 +1,303 @@
# 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 对接正常。后续可以根据业务需求逐步添加更多功能。

222
android/QUICKSTART.md Normal file
View File

@@ -0,0 +1,222 @@
# 车管家 Android 客户端 - 快速开始指南
## 项目概述
这是一个现代化的 Android 应用,为车管家 4S店车辆维保管理系统提供移动端支持。
## 开发前准备
### 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/)

182
android/README.md Normal file
View File

@@ -0,0 +1,182 @@
# 车管家 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 许可证

View File

@@ -0,0 +1,114 @@
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")
}

49
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,49 @@
# 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

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".CarMaintenanceApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CarMaintenance"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.CarMaintenance">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,12 @@
package com.carmaintenance
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class CarMaintenanceApplication : Application() {
override fun onCreate() {
super.onCreate()
}
}

View File

@@ -0,0 +1,29 @@
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()
}
}
}
}
}

View File

@@ -0,0 +1,32 @@
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)
}
}

View File

@@ -0,0 +1,69 @@
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<Preferences> 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<String?> {
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<String?> {
return context.dataStore.data.map { preferences ->
preferences[USER_KEY]
}
}
}

View File

@@ -0,0 +1,43 @@
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)
}

View File

@@ -0,0 +1,23 @@
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
)

View File

@@ -0,0 +1,84 @@
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)
}

View File

@@ -0,0 +1,41 @@
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
}

View File

@@ -0,0 +1,38 @@
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"
}

View File

@@ -0,0 +1,18 @@
package com.carmaintenance.data.remote
import com.google.gson.annotations.SerializedName
/**
* 通用API响应封装
*/
data class ApiResponse<T>(
@SerializedName("code")
val code: Int,
@SerializedName("message")
val message: String? = null,
@SerializedName("data")
val data: T? = null
) {
val isSuccess: Boolean
get() = code == 200
}

View File

@@ -0,0 +1,97 @@
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<LoginResponseData>
@POST("auth/logout")
suspend fun logout(): ApiResponse<Void>
// ==================== 用户相关 ====================
@GET("users")
suspend fun getUsers(): ApiResponse<List<User>>
@GET("users/{id}")
suspend fun getUser(@Path("id") id: Int): ApiResponse<User>
// ==================== 车辆相关 ====================
@GET("vehicles")
suspend fun getVehicles(): ApiResponse<List<Vehicle>>
@GET("vehicles/{id}")
suspend fun getVehicle(@Path("id") id: Int): ApiResponse<Vehicle>
@GET("vehicles/customer/{customerId}")
suspend fun getVehiclesByCustomer(@Path("customerId") customerId: Int): ApiResponse<List<Vehicle>>
// ==================== 工单相关 ====================
@GET("orders")
suspend fun getOrders(): ApiResponse<List<ServiceOrder>>
@GET("orders/{id}")
suspend fun getOrder(@Path("id") id: Int): ApiResponse<ServiceOrder>
@GET("orders/customer/{customerId}")
suspend fun getOrdersByCustomer(@Path("customerId") customerId: Int): ApiResponse<List<ServiceOrder>>
@PUT("orders/{id}")
suspend fun updateOrder(
@Path("id") id: Int,
@Body order: ServiceOrder
): ApiResponse<ServiceOrder>
// ==================== 预约相关 ====================
@GET("appointments")
suspend fun getAppointments(): ApiResponse<List<Appointment>>
@GET("appointments/{id}")
suspend fun getAppointment(@Path("id") id: Int): ApiResponse<Appointment>
@GET("appointments/customer/{customerId}")
suspend fun getAppointmentsByCustomer(@Path("customerId") customerId: Int): ApiResponse<List<Appointment>>
@POST("appointments")
suspend fun createAppointment(@Body appointment: Appointment): ApiResponse<Appointment>
@PUT("appointments/{id}/cancel")
suspend fun cancelAppointment(@Path("id") id: Int): ApiResponse<Void>
// ==================== 客户相关 ====================
@GET("customers")
suspend fun getCustomers(): ApiResponse<List<Customer>>
@GET("customers/user/{userId}")
suspend fun getCustomerByUserId(@Path("userId") userId: Int): ApiResponse<Customer>
}
/**
* 客户实体
*/
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
)

View File

@@ -0,0 +1,64 @@
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<List<Appointment>> {
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<List<Appointment>> {
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<Appointment> {
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<Unit> {
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)
}
}
}

View File

@@ -0,0 +1,60 @@
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<User> {
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<Unit> {
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()
}

View File

@@ -0,0 +1,51 @@
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<List<ServiceOrder>> {
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<List<ServiceOrder>> {
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<ServiceOrder> {
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)
}
}
}

View File

@@ -0,0 +1,38 @@
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<List<Vehicle>> {
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<List<Vehicle>> {
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)
}
}
}

View File

@@ -0,0 +1,63 @@
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)
}
}

View File

@@ -0,0 +1,98 @@
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: 实现工作人员仪表板
}
}
}

View File

@@ -0,0 +1,11 @@
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")
}

View File

@@ -0,0 +1,198 @@
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
)
}
}
}
}
}

View File

@@ -0,0 +1,166 @@
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)
)
}
}
}

View File

@@ -0,0 +1,123 @@
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)
)
}
}
}

View File

@@ -0,0 +1,311 @@
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
)
}
}

View File

@@ -0,0 +1,117 @@
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)
)
}
}
}

View File

@@ -0,0 +1,60 @@
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)
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
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)

View File

@@ -0,0 +1,68 @@
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
)
}

View File

@@ -0,0 +1,115 @@
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
)
)

View File

@@ -0,0 +1,81 @@
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>(AuthUiState.Initial)
val uiState: StateFlow<AuthUiState> = _uiState.asStateFlow()
private val _isLoggedIn = MutableStateFlow(false)
val isLoggedIn: StateFlow<Boolean> = _isLoggedIn.asStateFlow()
private val _currentUser = MutableStateFlow<User?>(null)
val currentUser: StateFlow<User?> = _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()
}

View File

@@ -0,0 +1,112 @@
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<List<Vehicle>>(emptyList())
val vehicles: StateFlow<List<Vehicle>> = _vehicles.asStateFlow()
private val _isLoadingVehicles = MutableStateFlow(false)
val isLoadingVehicles: StateFlow<Boolean> = _isLoadingVehicles.asStateFlow()
// 工单相关
private val _orders = MutableStateFlow<List<ServiceOrder>>(emptyList())
val orders: StateFlow<List<ServiceOrder>> = _orders.asStateFlow()
private val _isLoadingOrders = MutableStateFlow(false)
val isLoadingOrders: StateFlow<Boolean> = _isLoadingOrders.asStateFlow()
// 预约相关
private val _appointments = MutableStateFlow<List<Appointment>>(emptyList())
val appointments: StateFlow<List<Appointment>> = _appointments.asStateFlow()
private val _isLoadingAppointments = MutableStateFlow(false)
val isLoadingAppointments: StateFlow<Boolean> = _isLoadingAppointments.asStateFlow()
private val _message = MutableStateFlow<String?>(null)
val message: StateFlow<String?> = _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
}
}

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">车管家</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.CarMaintenance" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<include domain="sharedpref" path="user_prefs.xml"/>
</full-backup-content>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<include domain="sharedpref" path="user_prefs.xml"/>
</cloud-backup>
</data-extraction-rules>

7
android/build.gradle.kts Normal file
View File

@@ -0,0 +1,7 @@
// Top-level build file
plugins {
id("com.android.application") version "8.2.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
id("com.google.dagger.hilt.android") version "2.48" apply false
id("com.google.devtools.ksp") version "1.9.20-1.0.14" apply false
}

View File

@@ -0,0 +1,6 @@
# Gradle properties
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
android.enableJetifier=true
kotlin.code.style=official
android.nonTransitiveRClass=false

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-milestone-1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
android/gradlew vendored Normal file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
android/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,17 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "CarMaintenance"
include(":app")

View File

@@ -0,0 +1,54 @@
package com.carmaintenance.controller;
import com.carmaintenance.dto.Result;
import com.carmaintenance.entity.Customer;
import com.carmaintenance.repository.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 客户管理控制器
*/
@RestController
@RequestMapping("/customers")
@CrossOrigin
public class CustomerController {
@Autowired
private CustomerRepository customerRepository;
/**
* 获取所有客户
*/
@GetMapping
public Result<List<Customer>> getAllCustomers() {
List<Customer> customers = customerRepository.findAll();
return Result.success(customers);
}
/**
* 根据ID获取客户
*/
@GetMapping("/{id}")
public Result<Customer> getCustomerById(@PathVariable Integer id) {
Customer customer = customerRepository.findById(id).orElse(null);
if (customer == null) {
return Result.notFound("客户不存在");
}
return Result.success(customer);
}
/**
* 根据用户ID获取客户信息
*/
@GetMapping("/user/{userId}")
public Result<Customer> getCustomerByUserId(@PathVariable Integer userId) {
Customer customer = customerRepository.findByUserId(userId).orElse(null);
if (customer == null) {
return Result.notFound("客户信息不存在");
}
return Result.success(customer);
}
}

View File

@@ -2,10 +2,13 @@ package com.carmaintenance.controller;
import com.carmaintenance.dto.Result;
import com.carmaintenance.entity.ServiceOrder;
import com.carmaintenance.entity.Vehicle;
import com.carmaintenance.repository.ServiceOrderRepository;
import com.carmaintenance.repository.VehicleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@@ -21,6 +24,9 @@ public class ServiceOrderController {
@Autowired
private ServiceOrderRepository serviceOrderRepository;
@Autowired
private VehicleRepository vehicleRepository;
/**
* 获取所有工单
*/
@@ -98,6 +104,9 @@ public class ServiceOrderController {
return Result.notFound("工单不存在");
}
// 记录原状态,用于后续判断是否需要更新车辆保养时间
ServiceOrder.OrderStatus oldStatus = existingOrder.getStatus();
if (order.getStaffId() != null) existingOrder.setStaffId(order.getStaffId());
if (order.getArrivalTime() != null) existingOrder.setArrivalTime(order.getArrivalTime());
if (order.getStartTime() != null) existingOrder.setStartTime(order.getStartTime());
@@ -114,6 +123,31 @@ public class ServiceOrderController {
if (order.getRemark() != null) existingOrder.setRemark(order.getRemark());
ServiceOrder updatedOrder = serviceOrderRepository.save(existingOrder);
// 如果工单状态变更为"已完成",且服务类型为"保养维护",则更新车辆的上次保养时间
if (oldStatus != ServiceOrder.OrderStatus.completed
&& updatedOrder.getStatus() == ServiceOrder.OrderStatus.completed
&& updatedOrder.getServiceType() == ServiceOrder.ServiceType.maintenance) {
Vehicle vehicle = vehicleRepository.findById(updatedOrder.getVehicleId()).orElse(null);
if (vehicle != null) {
// 使用完成时间作为上次保养时间,如果没有完成时间则使用当前时间
LocalDate maintenanceDate = updatedOrder.getCompleteTime() != null
? updatedOrder.getCompleteTime().toLocalDate()
: LocalDate.now();
vehicle.setLastMaintenanceDate(maintenanceDate);
// 更新车辆里程(如果工单中有记录当前里程)
if (updatedOrder.getCurrentMileage() != null
&& updatedOrder.getCurrentMileage().signum() > 0) {
vehicle.setMileage(updatedOrder.getCurrentMileage());
}
vehicleRepository.save(vehicle);
}
}
return Result.success("更新成功", updatedOrder);
}

View File

@@ -489,6 +489,95 @@
</div>
</div>
<!-- 配件编辑模态框 -->
<div class="modal fade" id="partModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="partModalTitle">编辑配件</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="partForm">
<input type="hidden" id="partId">
<div class="row">
<div class="col-6 mb-3">
<label class="form-label">配件编号</label>
<input type="text" class="form-control" id="partPartNo" required>
</div>
<div class="col-6 mb-3">
<label class="form-label">配件名称</label>
<input type="text" class="form-control" id="partPartName" required>
</div>
</div>
<div class="row">
<div class="col-6 mb-3">
<label class="form-label">类别</label>
<input type="text" class="form-control" id="partCategory" placeholder="如:机油滤清器">
</div>
<div class="col-6 mb-3">
<label class="form-label">品牌</label>
<input type="text" class="form-control" id="partBrand">
</div>
</div>
<div class="row">
<div class="col-6 mb-3">
<label class="form-label">型号</label>
<input type="text" class="form-control" id="partModel">
</div>
<div class="col-6 mb-3">
<label class="form-label">单位</label>
<select class="form-select" id="partUnit">
<option value="个"></option>
<option value="套"></option>
<option value="桶"></option>
<option value="瓶"></option>
<option value="对"></option>
<option value="件"></option>
</select>
</div>
</div>
<div class="row">
<div class="col-6 mb-3">
<label class="form-label">单价(元)</label>
<input type="number" step="0.01" class="form-control" id="partUnitPrice" required>
</div>
<div class="col-6 mb-3">
<label class="form-label">库存数量</label>
<input type="number" class="form-control" id="partStockQuantity" required>
</div>
</div>
<div class="row">
<div class="col-6 mb-3">
<label class="form-label">
最小库存
<span class="text-danger" title="低于此值将触发库存预警">*</span>
</label>
<input type="number" class="form-control" id="partMinStock" required>
</div>
<div class="col-6 mb-3">
<label class="form-label">供应商</label>
<input type="text" class="form-control" id="partSupplier">
</div>
</div>
<div class="mb-3">
<label class="form-label">仓库位置</label>
<input type="text" class="form-control" id="partWarehouseLocation" placeholder="如A区01架02层">
</div>
<div class="mb-3">
<label class="form-label">备注</label>
<textarea class="form-control" id="partRemark" rows="2"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="savePart()">保存</button>
</div>
</div>
</div>
</div>
<!-- Toast 通知 -->
<div class="toast-container position-fixed top-0 end-0 p-3">
<div id="liveToast" class="toast" role="alert">

View File

@@ -484,19 +484,42 @@ function filterOrders(status) {
// 查看配件详情
async function viewPart(id) {
Utils.loading(true);
try {
const response = await http.get(API.PART(id));
if (response.code === 200 && response.data) {
const part = response.data;
// 显示配件详情
Utils.showToast('配件名称: ' + part.partName + '\n库存: ' + part.stockQuantity + part.unit, 'info');
}
} catch (error) {
Utils.showToast('加载配件信息失败', 'error');
} finally {
Utils.loading(false);
const part = allPartsData.find(p => p.partId === id);
if (!part) {
Utils.showToast('配件不存在', 'error');
return;
}
// 填充表单
document.getElementById('partId').value = part.partId;
document.getElementById('partPartNo').value = part.partNo;
document.getElementById('partPartName').value = part.partName;
document.getElementById('partCategory').value = part.category || '';
document.getElementById('partBrand').value = part.brand || '';
document.getElementById('partModel').value = part.model || '';
document.getElementById('partUnit').value = part.unit || '个';
document.getElementById('partUnitPrice').value = part.unitPrice;
document.getElementById('partStockQuantity').value = part.stockQuantity;
document.getElementById('partMinStock').value = part.minStock;
document.getElementById('partSupplier').value = part.supplier || '';
document.getElementById('partWarehouseLocation').value = part.warehouseLocation || '';
document.getElementById('partRemark').value = part.remark || '';
// 禁用表单(只读模式)
document.getElementById('partForm').querySelectorAll('input, select, textarea').forEach(el => {
el.disabled = true;
});
document.getElementById('partModalTitle').textContent = '查看配件详情';
const modal = new bootstrap.Modal(document.getElementById('partModal'));
modal.show();
// 启用表单控件(在关闭时)
document.getElementById('partModal').addEventListener('hidden.bs.modal', function() {
document.getElementById('partForm').querySelectorAll('input, select, textarea').forEach(el => {
el.disabled = false;
});
}, { once: true });
}
// 编辑配件
@@ -507,33 +530,81 @@ async function editPart(id) {
return;
}
// 简单实现使用prompt编辑
const newStock = prompt('请输入新的库存数量:', part.stockQuantity);
if (newStock !== null && !isNaN(newStock)) {
Utils.loading(true);
try {
const response = await http.put(API.PART(id), {
...part,
stockQuantity: parseInt(newStock)
});
// 填充表单
document.getElementById('partId').value = part.partId;
document.getElementById('partPartNo').value = part.partNo;
document.getElementById('partPartName').value = part.partName;
document.getElementById('partCategory').value = part.category || '';
document.getElementById('partBrand').value = part.brand || '';
document.getElementById('partModel').value = part.model || '';
document.getElementById('partUnit').value = part.unit || '个';
document.getElementById('partUnitPrice').value = part.unitPrice;
document.getElementById('partStockQuantity').value = part.stockQuantity;
document.getElementById('partMinStock').value = part.minStock;
document.getElementById('partSupplier').value = part.supplier || '';
document.getElementById('partWarehouseLocation').value = part.warehouseLocation || '';
document.getElementById('partRemark').value = part.remark || '';
if (response.code === 200) {
Utils.showToast('更新成功', 'success');
loadAllParts();
} else {
Utils.showToast(response.message || '更新失败', 'error');
}
} catch (error) {
Utils.showToast('更新失败', 'error');
} finally {
Utils.loading(false);
document.getElementById('partModalTitle').textContent = '编辑配件';
const modal = new bootstrap.Modal(document.getElementById('partModal'));
modal.show();
}
// 保存配件
async function savePart() {
const partId = document.getElementById('partId').value;
const partData = {
partNo: document.getElementById('partPartNo').value,
partName: document.getElementById('partPartName').value,
category: document.getElementById('partCategory').value,
brand: document.getElementById('partBrand').value,
model: document.getElementById('partModel').value,
unit: document.getElementById('partUnit').value,
unitPrice: parseFloat(document.getElementById('partUnitPrice').value),
stockQuantity: parseInt(document.getElementById('partStockQuantity').value),
minStock: parseInt(document.getElementById('partMinStock').value),
supplier: document.getElementById('partSupplier').value,
warehouseLocation: document.getElementById('partWarehouseLocation').value,
remark: document.getElementById('partRemark').value
};
Utils.loading(true);
try {
let response;
if (partId) {
// 更新
response = await http.put(API.PART(partId), partData);
} else {
// 新增
response = await http.post(API.PARTS, partData);
}
if (response.code === 200) {
Utils.showToast(partId ? '更新成功' : '添加成功', 'success');
const modal = bootstrap.Modal.getInstance(document.getElementById('partModal'));
modal.hide();
loadAllParts();
} else {
Utils.showToast(response.message || '保存失败', 'error');
}
} catch (error) {
console.error('保存配件失败:', error);
Utils.showToast('保存失败', 'error');
} finally {
Utils.loading(false);
}
}
// 添加配件
function showAddPartModal() {
Utils.showToast('添加配件功能开发中...', 'info');
// 清空表单
document.getElementById('partForm').reset();
document.getElementById('partId').value = '';
document.getElementById('partUnit').value = '个';
document.getElementById('partModalTitle').textContent = '添加配件';
const modal = new bootstrap.Modal(document.getElementById('partModal'));
modal.show();
}
// ==================== 数据加载函数 ====================
@@ -578,6 +649,9 @@ async function loadAllData() {
// 加载预约数据
await loadAppointments();
// 加载管理员首页统计数据和最近工单
await loadAdminStatsData();
console.log('所有数据加载完成');
} catch (error) {
console.error('加载数据失败:', error);
@@ -587,6 +661,83 @@ async function loadAllData() {
}
}
// 加载管理员首页数据(统计和最近工单)
async function loadAdminStatsData() {
try {
// 加载统计数据
const [usersRes, vehiclesRes, ordersRes, partsRes] = await Promise.all([
http.get(API.USERS),
http.get(API.VEHICLES),
http.get(API.ORDERS),
http.get(API.PARTS_LOW_STOCK)
]);
if (usersRes.code === 200) {
updateStat('totalUsers', usersRes.data?.length || 0);
}
if (vehiclesRes.code === 200) {
updateStat('totalVehicles', vehiclesRes.data?.length || 0);
}
if (ordersRes.code === 200) {
updateStat('totalOrders', ordersRes.data?.length || 0);
// 显示最近工单前5条
const recentOrders = ordersRes.data.slice(0, 5);
await displayRecentOrders(recentOrders);
}
if (partsRes.code === 200) {
updateStat('lowStockParts', partsRes.data?.length || 0);
}
} catch (error) {
console.error('加载统计数据失败:', error);
}
}
// 显示最近工单
async function displayRecentOrders(orders) {
const tbody = document.getElementById('recentOrdersTableBody');
if (!tbody) return;
if (orders.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted py-4">暂无数据</td></tr>';
return;
}
// 获取所有车辆信息
const vehiclesRes = await http.get(API.VEHICLES);
const vehicles = vehiclesRes.data || [];
tbody.innerHTML = orders.map(o => {
const vehicle = vehicles.find(v => v.vehicleId === o.vehicleId);
return `
<tr>
<td>${o.orderNo}</td>
<td>${Utils.getServiceTypeText(o.serviceType)}</td>
<td>${vehicle ? vehicle.licensePlate : '-'}</td>
<td>${Utils.formatMoney(o.totalCost)}</td>
<td>${Utils.getStatusBadge(o.status)}</td>
<td>${Utils.formatDateTime(o.createTime)}</td>
</tr>
`;
}).join('');
}
// 更新统计数字
function updateStat(id, value) {
const el = document.getElementById(id);
if (el) {
el.textContent = value;
// 添加动画效果
el.style.transition = 'all 0.3s';
el.style.transform = 'scale(1.2)';
setTimeout(() => {
el.style.transform = 'scale(1)';
}, 300);
}
}
// 加载所有配件
async function loadAllParts() {
Utils.loading(true);

View File

@@ -275,17 +275,21 @@ async function loadCustomerData() {
if (!user) return;
try {
// 获取所有用户找到自己的customer_id
const usersRes = await http.get(API.USERS);
if (usersRes.code === 200 && usersRes.data) {
const customers = usersRes.data.filter(u => u.role === 'customer');
const currentUser = customers.find(c => c.userId === user.userId);
// 获取用户信息、客户信息、车辆信息
const [usersRes, customersRes] = await Promise.all([
http.get(API.USERS),
http.get(API.CUSTOMERS)
]);
if (currentUser) {
// 加载车辆、工单、预约数据
loadCustomerVehicles(currentUser.userId);
loadCustomerOrders(currentUser.userId);
loadCustomerAppointments(currentUser.userId);
if (usersRes.code === 200 && customersRes.code === 200) {
// 找到当前用户对应的客户记录
const currentCustomer = customersRes.data.find(c => c.userId === user.userId);
if (currentCustomer) {
// 加载车辆、工单、预约数据使用customer_id而不是user_id
loadCustomerVehicles(currentCustomer.customerId);
loadCustomerOrders(currentCustomer.customerId);
loadCustomerAppointments(currentCustomer.customerId);
}
}
} catch (error) {
@@ -294,13 +298,13 @@ async function loadCustomerData() {
}
// 加载客户车辆
async function loadCustomerVehicles(userId) {
async function loadCustomerVehicles(customerId) {
try {
const response = await http.get(API.VEHICLES);
// 使用专门的API端点获取客户的车辆而不是获取所有车辆再过滤
const response = await http.get(API.VEHICLE_CUSTOMER(customerId));
if (response.code === 200 && response.data) {
const myVehicles = response.data.filter(v => v.customerId === userId);
displayVehicles(myVehicles);
populateVehicleSelect(myVehicles);
displayVehicles(response.data);
populateVehicleSelect(response.data);
}
} catch (error) {
console.error('加载车辆失败:', error);
@@ -352,10 +356,10 @@ function populateVehicleSelect(vehicles) {
}
// 加载客户工单
async function loadCustomerOrders(userId) {
async function loadCustomerOrders(customerId) {
try {
const [ordersRes, vehiclesRes] = await Promise.all([
http.get(API.ORDER_CUSTOMER(userId)),
http.get(API.ORDER_CUSTOMER(customerId)),
http.get(API.VEHICLES)
]);
@@ -394,10 +398,10 @@ function displayOrders(orders, vehicles) {
}
// 加载客户预约
async function loadCustomerAppointments(userId) {
async function loadCustomerAppointments(customerId) {
try {
const [appointmentsRes, vehiclesRes] = await Promise.all([
http.get(API.APPOINTMENT_CUSTOMER(userId)),
http.get(API.APPOINTMENT_CUSTOMER(customerId)),
http.get(API.VEHICLES)
]);
@@ -445,10 +449,16 @@ async function cancelAppointment(id) {
const response = await http.put(API.APPOINTMENT_CANCEL(id), {});
if (response.code === 200) {
Utils.showToast('预约已取消', 'success');
// 重新加载预约列表
// 重新加载预约列表 - 需要获取customer_id
const user = Utils.getCurrentUser();
if (user) {
loadCustomerAppointments(user.userId);
const customersRes = await http.get(API.CUSTOMERS);
if (customersRes.code === 200) {
const currentCustomer = customersRes.data.find(c => c.userId === user.userId);
if (currentCustomer) {
loadCustomerAppointments(currentCustomer.customerId);
}
}
}
} else {
Utils.showToast(response.message || '取消失败', 'error');
@@ -490,8 +500,23 @@ async function submitAppointment() {
Utils.loading(true);
try {
// 获取customer_id
const customersRes = await http.get(API.CUSTOMERS);
if (customersRes.code !== 200) {
Utils.showToast('获取客户信息失败', 'error');
Utils.loading(false);
return;
}
const currentCustomer = customersRes.data.find(c => c.userId === user.userId);
if (!currentCustomer) {
Utils.showToast('未找到客户信息', 'error');
Utils.loading(false);
return;
}
const response = await http.post(API.APPOINTMENTS, {
customerId: user.userId,
customerId: currentCustomer.customerId,
vehicleId: parseInt(vehicleId),
serviceType: serviceType,
appointmentTime: appointmentTime,
@@ -504,7 +529,7 @@ async function submitAppointment() {
// 重置表单
document.getElementById('appointmentForm').reset();
// 重新加载预约列表
loadCustomerAppointments(user.userId);
loadCustomerAppointments(currentCustomer.customerId);
} else {
Utils.showToast(response.message || '预约失败', 'error');
}

View File

@@ -17,6 +17,10 @@ const API = {
USERS_ROLE: (role) => `/users/role/${role}`,
CHANGE_PASSWORD: (id) => `/users/${id}/password`,
// 客户
CUSTOMERS: '/customers',
CUSTOMER: (id) => `/customers/${id}`,
// 车辆
VEHICLES: '/vehicles',
VEHICLE: (id) => `/vehicles/${id}`,

135
generate_word.py Normal file
View File

@@ -0,0 +1,135 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
将实习报告Markdown转换为Word文档
"""
from docx import Document
from docx.shared import Pt, RGBColor, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
import re
def set_cell_border(cell):
"""设置表格边框"""
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
tcPr = cell._element.get_or_add_tcPr()
tcBorders = OxmlElement('w:tcBorders')
for border_name in ['top', 'left', 'bottom', 'right']:
border = OxmlElement(f'w:{border_name}')
border.set(qn('w:val'), 'single')
border.set(qn('w:sz'), '4')
border.set(qn('w:space'), '0')
border.set(qn('w:color'), '000000')
tcBorders.append(border)
tcPr.append(tcBorders)
def add_paragraph_with_format(doc, text, bold=False, font_size=12, alignment=WD_ALIGN_PARAGRAPH.LEFT):
"""添加带格式的段落"""
para = doc.add_paragraph()
para.alignment = alignment
run = para.add_run(text)
run.font.size = Pt(font_size)
run.font.name = '宋体'
run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
if bold:
run.bold = True
return para
def add_heading(doc, text, level=1):
"""添加标题"""
heading = doc.add_heading(text, level=level)
for run in heading.runs:
run.font.size = Pt(16 - level * 2)
run.font.name = '黑体'
run._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体')
return heading
def parse_markdown_to_docx(md_file, docx_file):
"""解析Markdown文件并转换为Word文档"""
# 创建Word文档
doc = Document()
# 设置页面边距
sections = doc.sections
for section in sections:
section.top_margin = Inches(1)
section.bottom_margin = Inches(1)
section.left_margin = Inches(1.25)
section.right_margin = Inches(1.25)
# 读取Markdown文件
with open(md_file, 'r', encoding='utf-8') as f:
content = f.read()
# 分割成行
lines = content.split('\n')
i = 0
while i < len(lines):
line = lines[i].strip()
# 跳过空行
if not line:
i += 1
continue
# 处理标题
if line.startswith('#'):
level = line.count('#')
title_text = line.lstrip('#').strip()
if level <= 6:
add_heading(doc, title_text, min(level, 3))
else:
add_paragraph_with_format(doc, title_text, font_size=14, bold=True)
i += 1
continue
# 处理水平线
if line.startswith('---'):
doc.add_paragraph('_' * 50)
i += 1
continue
# 处理粗体文本
if line.startswith('**') and line.endswith('**'):
text = line.replace('**', '').strip()
add_paragraph_with_format(doc, text, bold=True)
i += 1
continue
# 处理列表
if line.startswith('- ') or line.startswith(''):
text = line.lstrip('').strip()
add_paragraph_with_format(doc, ' ' + text)
i += 1
continue
# 处理编号列表
if re.match(r'^\d+[、.]', line):
add_paragraph_with_format(doc, line)
i += 1
continue
# 处理普通段落(可能是多行)
para_text = line
i += 1
while i < len(lines) and lines[i].strip() and not lines[i].startswith('#') and not lines[i].startswith('-') and not lines[i].startswith('**'):
para_text += ' ' + lines[i].strip()
i += 1
add_paragraph_with_format(doc, para_text)
# 保存文档
doc.save(docx_file)
print(f"Word文档已生成: {docx_file}")
if __name__ == '__main__':
md_file = '杨璐-实习报告.md'
docx_file = '杨璐-实习报告.docx'
parse_markdown_to_docx(md_file, docx_file)

BIN
杨璐-实训报告.docx Normal file

Binary file not shown.

596
罗爱-实训报告.md Normal file
View File

@@ -0,0 +1,596 @@
# 辽宁科技学院
# 实习报告
姓名: 罗爱 学号: 74133230127
系部: 电信学院 专业:计算机科学与技术
班级: 计BZ231 指导教师: 王宇婷
实习名称:校企合作训练项目 实习时间2024.11.25-2025.1.10
实习单位: 智慧南楼-机房
辽宁科技学院教务处制
# 1、课程实践的目的、意义
实习内容为Web应用开发CSS+JavaScript、JavaWeb应用开发、移动应用开发。计划在校内实施相关项目学习由校方老师和企业老师指导具体实习任务。
# (一)获取知识目标
教学目标1掌握Android应用开发的一般流程。
教学目标2了解JavaWeb开发方法掌握相应Web前端及JavaWeb项目设计方法能够熟练使用JavaWeb开发的方法与技巧。
# (二)分析问题目标
教学目标3了解项目调试的一般方法能够自行解决项目调试过程中遇到的常见问题掌握项目调试过程中一般问题的解决方法。
# (三)解决问题目标
教学目标4整理所做工作的文档、图表依照报告格式要求撰写课程设计报告。实习报告的内容完整格式整齐、图表正确、杜绝抄袭。
# (四)思政目标
着眼于大学生世界观、价值观的塑造,使学生明白科技创新的重要性;使学生明确作为社会主义事业建设者和接班人所肩负的责任和使命。
# 2、课程实践的内容
# 1、绪论
# 1.1 研究目的与意义
信息化管理模式是将行业中的工作流程由人工服务逐渐转换为使用计算机技术的信息化管理服务。这种管理模式发展迅速使用起来非常简单容易用户甚至不用掌握相关的专业知识根据教程指导即可正常使用相关的系统因而被越来越多的用户所使用。由于相关行业管理信息化这就使得管理工作不在受到时间和区域的限制随时随地即可完成相关的工作任务和结果。就目前而言管理信息化在现代社会中非常流行使用比较广泛。早在20世纪70年代末就出现了早期的电子商务相关的公司企业使用计算机建立专用的内部网络通过内部网络完成相应的采购、销售等活动加快相关的企业之间的交易速度。
同时研究 SpringBoot 汽车租赁管理系统也有很多意义。从企业运营角度能有效提高管理效率。它可以自动化处理汽车租赁的各个流程,如车辆预订、调度和归还等,减少人工操作的繁琐环节和可能出现的错误,实现业务流程的精准管理。在提升客户体验方面意义重大。通过系统,客户能够方便地在线查询车辆信息、预订车辆、了解
租赁价格等,还可以及时收到租赁状态的通知,使得租车过程更加透明、便捷,有助于提高客户满意度和忠诚度。对于数据管理系统能够集中存储和管理汽车信息、客户信息、租赁记录等大量数据。可以方便地进行数据统计和分析,帮助企业更好地了解市场需求、车辆利用率等情况,从而优化车辆配置、调整价格策略等商业决策。从技术层面 SpringBoot 框架具有开发效率高、易于维护和部署的特点。基于此框架开发汽车租赁管理系统可以降低开发成本,并且使系统能够更灵活地适应业务的变化和拓展。
当下许多行业采用互联网技术将工作流程信息化、数字化提高了相关人员的服务质量和效率节约了相关行业的人力、财力、物力等资源与此同时人们获取外界的相关信息主要依赖于主流的信息化技术和工具。人们对生活的需求也在不断的发生着变化为了应对用户的多样化需求许多相关的第三产业应运而生管理信息化也逐渐的流行起来比如电子商务行业。本人通过查询大量学习资料了解基本的开发系统的基本背景和关键任务学习与掌握Java语言、web技术、AJAX技
术、HTML语言等开发技术设计系统功能模块以及MySQL数据库的相关语法和工具创建和存储数据表格反映和关联表格之间相互存在的关系由此对汽车租赁
系统进行研发和实现。
# 1.2国内外研究现状
大概在20世纪90年代我国才开始研发汽车租赁系统与一些发达国家相比系统研发起步比较晚。当时的计算机技术刚开始发展起来国家经济力量比较薄弱各地区的经济发展水平不平衡再加上相关的网络应用技术不太先进我国也使用了一段较长的时间对网络信息化管理进行探索。近些年因为国家非常重视和支持第三产业的发展以及人们的日常生活需求越来越离不开信息管理技术的使用所以我国的信息管理系统行业发展速度非常快并且相关的体制法规也正在不断地被完善和改进。新时代背景下根据人们的相关需求不断地促进着相关产业的产生与发展一系列电子产品、应用软件、信息管理系统等新时代的产物逐渐出现在人们的视野中并且在近几年发展迅速日渐趋于成熟。
与国内相比国外汽车租赁系统领域发展较早。国外的计算机技术发展比较成熟所以系统相关的设计也比较完善。19世纪60年代左右国外就开始研发汽车租赁系统并且不久之后迅速将其投入市场进行使用。美国、英国等一些发达国家快速发展计算机技术促进了汽车租赁系统管理行业信息化建设。而后随着相关的技术
不断地发展,覆盖面非常广泛,应用领域比较多,促进着汽车租赁系统等相关的信息管理系统不断地发展和完善,并且其所设计的系统功能结构也比较合理、全面。相对而言,国外系统的研发在相关领域上还是占据着较大的优势。因此,我们需要吸收国外系统开发领域中的较好的技术精华,发展我国的信息化管理系统,使得其面向大众,能够更好的、更全面的服务于相关的工作人员。
目前汽车租赁管理国内外研究依然还存在不足比如在技术层面Spring Boot助力搭建系统基础架构但性能优化有欠缺。面对租车高峰如国内“五一”、十一”假期出行潮系统易现卡顿、延迟应对高并发的缓存策略、异步处理研究不足无法保障流畅体验。物联网与租赁结合停留于车辆位置追踪未能深挖传感器数据依车辆健康状况提前维护、精准调度区块链用于确保租赁数据可信、防篡改及智能合约执行的探索也尚浅。客户细分需求也兼顾不够。面向新手司机缺简化操作引导、
驾驶辅助推荐;商务客户期望快速结算、定制发票等专属服务,现有研究未充分落
实。增值服务整合零散,租车与保险、维修联动生硬,客户难一站式便捷选购,国际业务还受地域法规差异制约,未能构建通用增值服务模式。数据分析利用低效,海量租赁数据多停留在表面统计,未深挖关联信息,不知哪些客户易流失、何时是最佳营
销时机。跨国租赁场景下,各地数据因格式、法规不同难以协同,无法依国际旅游淡
旺季在全球调配资源,阻碍租赁企业国际化拓展,限制整体行业突破。
# 2、系统分析
# 2.1需求分析
# 2.1.1、功能需求分析
功能需求分析是软件系统开发过程中的一个关键环节。它主要是对软件系统应该具备的功能进行详细的剖析、定义和描述。具体而言,是从用户的角度出发,明确用户期望软件系统能够完成的各项任务和操作。
在汽车租赁管理系统中,用户管理功能需求分析是支持用户注册、登录,信息的录入与修改,如个人用户的姓名、身份证号、联系方式,企业用户的营业执照、法人信息等,同时具备用户信用评级功能,便于风险管控。详细记录车辆品牌、型号、车架号、购置日期、年检情况、保险信息等,实时更新车辆状态,包括可租、维修中、预订、已租,方便调度安排。实现线上预订、选车,灵活设定租赁起止时间,自动生成电子合同,租赁期间可实时追踪订单,结束后精准结算,涵盖租金、押金、超时或损坏赔偿,支持多种支付方式。收集用户租赁行为、车辆使用频率、不同区域需求等
数据,深度分析以优化车辆投放、调整租赁价格策略,提升运营效率。通过设置不同角色权限,如管理员、普通员工、租客,保障数据安全,定期备份数据,确保系统稳定运行,及时排除故障。
功能需求分析为后续的系统设计、开发、测试提供了清晰的方向和具体的目标,确保开发出来的系统能够满足用户在实际业务场景中的各种功能要求。
# 2.1.2、非功能需求分析
非功能需求聚焦于性能需求、易用性需求、可拓展需求、可靠性及安全性等多个层面。在性能需求上看系统要确保快速响应查询车辆、生成订单等日常操作的响应时间控制在1-2秒内即便在旅游旺季、节假日等租车高峰也能稳定运行本系统承载高并发访问同时保证数据处理精准避免错记、漏记租金或车辆状态。
对于用户信息安全该系统保护用户隐私与数据安全。采用SSL加密技术防止数据传输泄露对用户密码、身份证号等敏感信息加密存储设置严格的访问权限不同岗位员工如财务、调度员权限分明防止越权操作还需定期备份数据防范硬件故障、黑客攻击导致的数据丢失。当然无论是管理员还是普通用户界面设计都应简洁直观。管理员操作流程简化如车辆信息录入可批量处理用户端租车步骤不超
三步,且提供操作引导、错误提示,方便新手快速上手,还可适配移动端,满足随时
随地租车需求。
考虑到汽车租赁市场动态变化,系统结构具备广泛的扩展性。未来若增加新车型、新租赁模式,或拓展业务区域,系统能轻松接纳,通过插件、模块扩展等方式,快速融入新功能,无需推倒重来。需保证系统全年无故障运行时间达到 $99\%$ 以上,建立故障预警机制,提前发现潜在问题,一旦出现故障,能迅速切换至备用系统或快速恢复,将对业务的影响降到最低。
# 2.2可行性分析
# 2.2.1、技术可行性
汽车租赁管理系统选用 SpringBoot 框架它已在行业内广泛应用拥有庞大的开发者社区。这意味着开发过程中遇到问题能迅速从海量的技术文档、论坛讨论及开源代码示例中获取解决方案像利用其自动配置特性可快速搭建系统基础架构减少繁琐的初始配置工作为高效开发汽车租赁管理系统提供坚实支撑。前端技术如Vue.js 或 React 与 SpringBoot 后端配合娴熟。前端负责打造交互友好的用户界面,通过 Ajax 等技术与后端无缝通信,后端专注业务逻辑处理、数据存储与检索。例如,
用户在前端进行车辆查询操作请求瞬间传至后端SpringBoot依据数据库查询指令快速反馈精准结果确保租车客户及管理人员操作流畅这种成熟的前后端分工协作模式提升了系统开发的整体效率与质量。
# 2.2.2、经济可行性
汽车租赁管理系统作为一种商业应用软件,其经济可行性是指评估该项目是否具有经济效益,能否为企业带来利润,以及长期运营的成本效益比。可以从成本角度以及效益产出方面角度分别分析。
从成本角度来看,虽然开发初期需要投入一定资金,但并非不可承受。人力成本方面,如今开发人才市场丰富,企业可按需招聘组建团队或部分外包,降低开支;软件工具及必要硬件采购成本逐年降低,像基于云服务的服务器租赁模式,能依业务量灵活调整配置,避免高额硬件购置与维护费。
在效益产出上收益显著。首先大幅提升运营效率自动化车辆、订单、客户管理流程使原本繁琐的人工操作精简降低人力需求节省人工成本例如原本5人负责的车辆调度借助系统2人就能高效完成。其次能有效增加业务量便捷的线上租车、智能推荐等功能吸引更多客户拓展客源带来租金收入提升据行业数
据,使用类似系统的企业业务量平均增长 $20\%$ 。最后,优化资源配置,精准的数据分析助力合理投放车辆、安排维护,减少闲置浪费,降低运营成本,以一家中型租赁公司为例,车辆闲置率从 $25\%$ 降至 $15\%$ ,节省大量资金。
通过对上述各项的综合分析,可以判断汽车租赁管理系统项目在经济上可行,经济可行性分析不仅是启动项目的前提,也是持续改进和调整方向的重要依据。
# 2.2.3、社会可行性
现代社会人们的出行需求越来越多样化,汽车租赁管理系统提供了多种租赁方式,使汽车租赁管理系统具有社会可行性。于用户而言,它极大地提升了租车体验的便捷性与满意度。如今人们生活节奏快,出行需求瞬息万变,借助该系统,租客能随时随地在线选车、预订,实时了解车辆状态与租赁价格,一站式完成合同签订与支付,整个过程高效透明,贴合现代人对高效服务的追求,适应社会快节奏生活模式。
从企业运营角度,一方面,为租赁公司创造了公平有序的市场竞争环境。通过系统精准管理车辆、优化调度,避免资源浪费,小型企业得以凭借精细化运营与大型企业同台竞争,促进市场多元化发展;另一方面,系统带来的高效管理促使企业提升服务质量,如快速响应客户需求、精准匹配车辆等,间接推动行业整体服务水平迈向新
高度,满足社会对高品质出行服务的期望。
在社会资源利用方面,它助力城市交通资源优化配置。依据大数据分析合理投放车辆,减少热门区域车辆拥堵,提高道路通行效率;同时引导闲置车辆流向租赁市场,盘活社会闲置资源,实现物尽其用,与当下倡导的绿色、可持续发展理念高度契合,对缓解交通压力、节能减排有着积极意义。
# 2.2.4、操作可行性
系统界面设计简单明了,操作流程贴合员工日常工作习惯,采用简洁直观的布局,车辆管理模块中,车辆入库、出库、维修保养记录的录入界面,字段清晰、引导明确,员工只需简单培训就能上手。菜单导航设计合理,各功能模块一目了然,无论是查询客户订单,还是统计车辆租赁数据,都能迅速找到对应入口,最大程度降低学习成本,减少因操作复杂带来的抵触情绪,确保员工能够快速适应新系统,实现工作的平稳过渡。
对于一线租赁业务人员,重点培训客户信息录入、合同签订流程、车辆预订与交付操作等直接关乎业务开展的模块,通过模拟真实租赁场景,让他们在实践中熟悉系统;对于后台管理人员,着重讲解车辆库存管理、财务报表生成、数据分析挖掘等功
能,利用案例教学与实际操作相结合的方式,使他们精准掌握系统核心管理能力。培训方式上,除传统的课堂讲授,还可制作线上教程视频、开发模拟操作软件,供员工随时随地学习,多管齐下提升员工操作技能熟练度。
系统的兼容性与稳定性影响着日常运营的流畅性。一方面,要兼容各类硬件设备,无论是租赁门店的台式电脑、移动办公的笔记本,还是员工用于现场车辆检查的手持终端,都应确保系统正常运行,数据实时同步,避免因设备差异造成使用障碍;另一方面,稳定的软件性能至关重要,在租赁高峰时段,面对大量客户咨询、订单涌入,系统不能出现卡顿、报错或数据丢失情况,能可靠地处理多任务并行,如同时进行车辆调配、租金结算、客户反馈处理等,以坚实的技术保障为租赁业务保驾护航。
此系统便捷的线上预订功能是吸引客户的重要因素,客户通过手机端或电脑端进入系统,能迅速浏览车辆信息、选择租赁时段、提交预订申请,操作步骤精简,支付流程安全顺畅;后续的服务跟进,如订单状态实时查询、还车提醒等功能,让客户全程掌握租赁动态,提升满意度。若客户遇到问题,系统内置的在线客服功能及时响应,人工客服或智能客服能迅速协助解决,使客户感受到系统的高效与贴心,为企业树立良好口碑,间接促进汽车租赁业务的繁荣发展。
# 2.2.5、法律可行性
只要汽车租赁管理系统在开发、运营各环节严守法律红线,充分落实各项法规要求,便能具备坚实的法律可行性,助力汽车租赁行业健康发展。
从数据隐私保护层面看,该系统涉及大量客户信息,包括姓名、身份证号、联系方式、驾驶执照信息等,以及车辆的详细资料。系统开发必须严格遵循相关隐私法规,如《中华人民共和国个人信息保护法》,确保数据收集、存储、传输、使用全过程的安全性。需采取加密技术防止信息泄露,明确数据访问权限,仅授权人员可接触特定数据,并且在客户信息使用场景上严格限定范围,未经客户许可不得用于商业推广等其他目的,切实保障客户隐私权益。
在合同管理方面,系统内置的电子合同签订功能要符合《中华人民共和国民法典》中关于电子合同的规定。电子合同需保证签约主体身份真实可鉴,运用数字证书、电子签名技术,确保合同签订的法律效力等同于纸质合同,杜绝合同篡改风险,为租赁双方权利义务的界定提供坚实法律依据,一旦出现纠纷,合同内容能有效支撑法律诉求。
就系统运营资质而言,开发运营汽车租赁管理系统的企业,若涉及第三方支付集
成用于租金收缴等业务,必须依法取得相应支付业务许可证,遵循支付结算相关法规,保障资金交易安全、合规。同时,系统所依托的软件著作权归属应明晰,避免知识产权纠纷,开发方应依据法律及时申请软件著作权登记,维护自身创新成果权益也为后续系统升级、商业合作筑牢法律根基。
# 3、系统使用相关技术
# 3.1后端开发技术
后端开发技术采用了 SpringBoot 框架以及 Java 语言。SpringBoot 框架是核心后端技术之一。它简化了基于 Spring 的应用开发通过自动配置等功能能快速搭建系统的基础架构。例如在处理车辆信息管理、用户认证、租赁订单处理等业务逻辑时SpringBoot 可以高效地整合各种组件,减少繁琐的配置工作。
Java语言作为SpringBoot的主要编程语言Java提供了面向对象编程的强大功能。开发人员可以利用Java的类、对象、接口等概念来构建清晰的系统模块如创建车辆类来存储车辆的各种属性包括品牌、型号、车架号等信息并且通过方法来实现车辆状态的更新、查询等操作。
# 3.2数据库技术
该系统采用了关系型数据库以及非关系型数据库。关系型数据库用于存储结构化数据如车辆基本信息表包含车辆ID、品牌、型号、购置日期等字段、用户信息表用户ID、姓名、联系方式、驾照信息等、租赁订单表订单ID、车辆ID、用户ID、租赁开始时间、租赁结束时间等。这些数据库通过SQL进行数据的操作如插入、查询、更新和删除操作以确保系统数据的持久性和一致性。
非关系型数据库在某些场景下也会使用,例如存储一些非结构化或半结构化的数据,如车辆的历史维修记录、用户的租车偏好等信息,它以文档形式存储数据,具有灵活的数据模型,适合处理复杂多变的数据类型。
# 3.3前端开发技术
本系统前端开发技术采用 HTML/CSS/JavaScriptHTML 用于定义网页的结构如创建车辆列表展示页面的布局包括标题、表格等元素CSS 用于控制网页的样式如设置车辆列表的字体、颜色、边框等样式使页面更加美观JavaScript 则用于实现网页的交互功能,例如,当用户点击车辆预订按钮时,通过 JavaScript 发送请求到后端服务器进行预订操作,并在页面上显示预订结果的反馈信息。
另外该系统添加了前端框架它可以提高前端开发的效率和用户体验。以Vue.js
为例,它采用组件化开发方式,能够方便地构建复杂的前端界面。在汽车租赁管理系统中,可以创建车辆信息组件、用户登录组件、租赁订单组件等,每个组件都有自己独立的逻辑和样式,并且可以在不同页面中复用,使得前端代码更加模块化、易于维护。
# 3.4 B/S 模式
B/S结构亦被称为浏览器/服务器Browser/Server结构是当今网络应用中广泛采用的一种架构模式。在此模式下用户通过浏览器Browser作为客户端界面直接访问网络上的服务器Server以实现数据的动态交互和服务的即时提供。B/S结构的核心优势在于其卓越的跨平台性。由于客户端主要依赖于通用的浏览器软件用户能够轻松地在不同操作系统和硬件平台上无缝访问服务器这一特性使得B/S结构在实际应用中更具灵活性和便利性。实现数据共享和服务交互。易于维护和升级B/S结构将所有的业务逻辑和数据处理都集中在B/S结构中客户端的主要职责是负责数据的显示和用户交互而实际的业务逻辑和数据处理则集中在服务器端。这样的设计使得在系统需要升级或维护时只需专注于修改服务器端的程序从而显著简化了维护和升级的流程。此外B/S结构还具备出色的扩展性。由于客户端
仅依赖浏览器这一通用工具系统能够灵活地实现功能的扩展和升级无需对客户端进行大规模的改动或更新。因此可以轻松地增加新的用户或新的服务而不需要对客户端进行大规模的修改。用户操作简便用户只需通过浏览器即可访问系统无需安装额外的客户端软件降低了用户的使用门槛。安全性虽然B/S结构在安全性方面存在一定的挑战但通过合理的设计和安全措施如HTTPS协议、防火墙等可以保障数据的安全传输和存储。
# 3.5 IDEA 开发环境
软件开发使用的编程语言有许多种而每种编程语言需要通过与其相对应的开发平台进行编译和运行。IDEA 平台都是目前比较常用的开发环境。IDEA 平台是开源的,具有功能强大、可扩展性强等特点,可以应用于 C/S 模式软件的开发,但是它所占据的内存容量比较大,运行较慢,并且其并未提供 Tomcat 服务器,运行过程中需要将代码发布到 Tomcat 服务器中,测试使用的时间较长,故而不太适用于 B/S 模式软件的开发。
IDEA 平台是建立在 IDEA 平台的基础之上,增加了许多的应用插件,比如 Tomcat 插件、mail 组件等。IDEA 平台增加了 Tomcat 插件,代码编写完成或者更新
完成时,程序员无需将代码发布到 Tomcat 服务器中可以直接通过调试实现程序的运行。IDEA 平台增加了 Mail 组件该组件可以为本程序提供标准的邮件方法便于开发人员完成与邮件功能相关的编译工作。IDEA 平台占据的内存空间较小同时其也具有较高的可扩展性编程人员可以根据需要添加和使用相关的插件。可以支持主流的开源产品和相关的开发框架被广泛运用到相关的移动系统、web 应用系统等开发中。相比于 IDEA 平台,本系统比较适合使用 IDEA 平台进行编程和开发。
# 4、系统设计
本系统主要通过使用Java语言编码设计系统功能MySQL数据库管理数据AJAX技术设计简洁的、友好的网址页面然后在IDEA开发平台中编写相关的Java代码文件接着通过连接语言完成与数据库的搭建工作再通过平台提供的Tomcat插件完成信息的交互最后在浏览器中打开系统网址便可使用本系统。本系统的使用角色可以被分为用户和管理员用户具有注册、查看信息、留言信息等功能管理员具有修改用户信息发布新闻等功能。
开发该系统需要提前设计数据库。汽车租赁系统当中的数据库是相关数据的集合,存储在一起的这些数据也是按照一定的组织方式进行的。目前,数据库能够服务
于多种应用程序,则是源于它存储方式最佳,具备数据冗余率低的优势。虽然数据库为程序提供信息存储服务,但它与程序之间也可以保持较高的独立性。总而言之,数据库经历了很长一段时间的发展,从最初的不为人知,到现在的人尽皆知,其相关技术也越发成熟,同时也拥有着坚实的理论基础。
本系统使用MySQL数据库管理与系统相关的数据信息。逻辑设计阶段是将上一个阶段中的概念数据模型转换为方便数据库进行存储的关系模型即基本表的形式方便开发人员后期对数据模型进行优化和管理。逻辑设计阶段是整个数据库设计设计的关键与系统有关的信息将会在这一阶段中被存储在数据库中当用户使用本系统进行相关的功能操作时与之有关的数据信息所在的基本表会发生相应的更新变化。数据库的逻辑设计阶段主要任务是将与系统相关的数据信息设计成为方便数据库存储和管理的基本表格的形式。
4.1 项目 1: JavaScript+CSS 项目
4.1.1、页面布局
首页整体布局简洁明了,上方设置导航栏,包含“首页”“公告”“留言板”
“汽车租赁”“网页介绍”“联系我们”“个人中心”等主要菜单选项。导航栏下方
是轮播图区域,展示热门车型、优惠活动等吸引眼球的内容。再往下是车辆推荐板块,以卡片形式呈现部分热门或特色车辆,展示车辆图片、品牌、型号以及简短的惠租车标语,方便用户快速浏览了解。
车辆展示页主体部分划分成左右结构。左侧是车辆筛选栏,设置车型、品牌、价格区间、可租日期等筛选条件,用户能通过下拉菜单、输入框、日期选择器等交互组件精准筛选心仪车辆。右侧是车辆列表展示区,以表格或卡片形式罗列车辆,每辆车展示图片、品牌、型号、座位数、日租金、车辆状态(可租/已租等)等关键信息,并且每个车辆展示区域都带有“查看详情”“立即预订”等操作按钮,方便用户进一步了解或直接预订。
车辆详情页上方是大尺寸的车辆高清图片展示区,下方依次为车辆详细信息板块,涵盖车辆基本属性(如车架号、购置时间等)、配置详情(如内饰、安全配置等)、租赁规则(如押金、最长租赁时长等)。再往下是用户评价区,展示过往租客的评价内容与评分,方便新用户参考。最下方是“预订”按钮,点击后弹出预订窗口,引导用户填写租赁起止时间、联系人信息等进行预订操作。
我的订单页顶部设置订单状态筛选标签,如“待支付”“已支付”“已完成”
“已取消”等,方便用户快速定位不同状态的订单。主体部分是订单列表,以表格形式呈现,包含订单编号、车辆信息、租赁起止时间、租金总额、订单状态等字段,每条订单记录旁设有“查看详情”“取消订单”(在符合取消条件下)“支付租金”(针对待支付订单)等功能按钮,便于用户对订单进行相应操作。
用户中心页左侧是功能导航栏,涵盖“个人信息修改”“我的收藏”“租车记录”“账户安全”等选项。右侧根据左侧导航栏选项切换相应内容,比如选择“个人信息修改”,右侧就呈现姓名、联系方式、密码等信息的修改表单;选择“我的收藏”,则展示用户收藏的心仪车辆列表等内容,方便用户管理个人相关事务。
登录/注册页布局简洁大方,登录区域有用户名、密码输入框以及“登录”“忘记密码”按钮;注册区域则包含用户名、密码、确认密码、手机号、验证码等输入框,下方还有“注册协议”勾选框以及“注册”按钮,引导新用户完成注册流程,整体页面注重输入框的提示信息展示,帮助用户准确填写信息。
下图代码为首页页面:
![image](https://cdn-mineru.openxlab.org.cn/result/2026-01-08/e4d88765-b6c4-4c8b-8af6-48f02a33acc1/5414d94f8298517e604cd04ba95b4de409f11f97360dec0dcaa16db027460fde.jpg)
图1首页界面
以下为首页页面关键代码:
![image](https://cdn-mineru.openxlab.org.cn/result/2026-01-08/e4d88765-b6c4-4c8b-8af6-48f02a33acc1/cddcc6631f951732bf3e7314a510cbf78408df985af60d33c39f3ef8cadd996f.jpg)
图2首页界面关键代码
# 4.1.2、页面设计
该系统页面设计通过合理的布局、清晰的导航栏能让用户便捷地找到信息。汽车租赁的页面设计,用户可以通过分类导航快速定位车辆,了解到车辆的当前状态,流畅的租车流程设计也会提升满意度。独特且风格统一的页面设计能够强化品牌在用户心中的印象。汽车租赁页面采用红色调为主的页面设计,很容易让人联想到品牌形象。当前页面通过对内容进行有序组织,将重要内容放在显眼位置,搭配合适的字体
和颜色突出关键信息,能够让用户更快理解内容。此页面也设计合理的预订表单和流程引导,可使用户能够方便快捷地完成车辆预订,减少操作步骤和时间成本,提高租赁效率。同时设置在线留言、常见问题解答等功能,及时解决用户在租赁过程中遇到的疑问和问题,保障租赁流程的顺利进行。
![image](https://cdn-mineru.openxlab.org.cn/result/2026-01-08/e4d88765-b6c4-4c8b-8af6-48f02a33acc1/1c86495895a898505aa72e9d24c8551b2b1b8ff05a20632b9bfb2ee6d4224ef1.jpg)
图3汽车租赁界面
# 4.1.3、表单验证
该系统处于登录界面时该系统通过输入用户名、手机号、邮箱等唯一标识和密码,系统与数据库中存储的用户信息进行比对验证,确认用户身份是否合法,防止非法访问。可提供勾选框让用户选择,若勾选,下次打开系统时能自动登录,一般通过在客户端存储加密后的相关凭证实现,方便用户操作。登录错误提示:当用户名或密
码输入错误时,给出相应提示“账号或密码不正确”。
以下为登录页面:
![image](https://cdn-mineru.openxlab.org.cn/result/2026-01-08/e4d88765-b6c4-4c8b-8af6-48f02a33acc1/ce0c8f4f962d8db42601adc1f2e327b66ad36a4a10d865e01eea311c50fcc35c.jpg)
图4用户登录界面
![image](https://cdn-mineru.openxlab.org.cn/result/2026-01-08/e4d88765-b6c4-4c8b-8af6-48f02a33acc1/fcc271036085a147ce0422094da2d90088003678fca498c06e96255aa48045a8.jpg)
图5登录失败界面
以下为登陆成功的关键代码:
```javascript
http.request(data_role + '/login', 'get', data, function(res) {
layer.msg('登录成功', {
time: 2000,
icon: 6
```
# 4.2项目2后端功能模块设计
# 4.2.1、登录和注册模块
汽车租赁管理系统登录模块,通过简单直观的页面设计易于用户理解跟操作,通过红色的背景板以及白色的提示框吸引用户眼球,用户手动输入账号与密码即可登录该网址。注册模块包含:账户密码、用户姓名、手机号、身份证号、性别、电子邮箱等引导用户进行注册。登陆跟注册模块不仅提升了用户体验,高效的表单填写过程减少用户负担,提升满意度。此外注册模块也能增加安全性,结合密码强度检查、二次验证码、生物识别等多种手段,构筑坚实防线对抗恶意攻击。登录后展现个性化的主页,基于用户偏好推荐相关内容,提升互动感。忘记密码选项也能提供便利的找回途径,通过邮箱或手机快速恢复访问,减少用户流失。社交媒体集成:允许通过社交媒体账户一键登录,吸引更多年轻用户群。
下图为注册页面。
![image](https://cdn-mineru.openxlab.org.cn/result/2026-01-08/e4d88765-b6c4-4c8b-8af6-48f02a33acc1/8c2b0b3b2375aee648602ba39bf199c4d02b1eba2b19f4f6fd979a109ad7e851.jpg)
图6用户注册界面
# 4.2.2、主调模块
汽车租赁模块是系统的一个主要模块。它主要负责车辆信息的维护和管理,包括车辆的基本信息(如品牌、型号、车牌号、车辆颜色等)录入、车辆状态(可租、维修中、已租等)更新、车辆配置信息管理等。
汽车租赁模块关联其他模块,和订单管理模块相互配合。订单的生成和执行会改变车辆的状态,例如当一个订单完成租赁手续后,车辆管理模块需要将车辆状态从“已租”更新为“可租”。它也和用户管理模块有关,因为车辆的租赁情况需要记录对应的用户信息。
汽车租赁模块提高车辆管理效率,方便管理员实时掌控车辆状态,如可租、维
修、已租等精准安排车辆调度与维护减少闲置、避免超租。用户线上便捷预订随时选车、订租期查看详情到店快速提车还有租赁记录、积分优惠提升满意度与忠诚度。同时降低运营成本自动化流程减少人力电子合同节省纸张智能提醒规避逾期罚款提高效益。同时增强风险管控严格审核客户资质结合GPS追踪确保按规用车、按时还车降低违约、丢车风险。
下图为汽车租赁页面:
![image](https://cdn-mineru.openxlab.org.cn/result/2026-01-08/e4d88765-b6c4-4c8b-8af6-48f02a33acc1/a272530324f90cc9a19c9afb16d2aa7bd45f80cc5ab916d00c03c03fd66977a3.jpg)
图7汽车租赁界面
```txt
以下为汽车租赁页面的关键代码:
<ul class="post-meta">
<li v-if="detail.qicheUuidNumber">汽车编号:
{ detail.qicheUuidNumber}
</li>
<li v-if="detail.qicheTypes">汽车类型:
{ detail.qicheValue}
```
```html
</li>
<li v-if="detail.qicheKucunNumber">汽车库存数量:
{ detail.qicheKucunNumber}
</li>
<li v-if="detail.qicheOldMoney">租赁原价/天:
{ detail.qicheOldMoney}
</li>
<li v-if="detail.qicheNewMoney">现价/天:
{ detail.qicheNewMoney}
</li>
<li v-if="detail.qicheClicknum">热度:
{ detail.qicheClicknum}
</li>
</ul>
```
# 4.2.3、分功能模块
该系统留言界面设计为一个分功能模块,它提供一个用户友好的留言输入框,可支持多行文本输入,方便用户详细地表达想法。同时,设置留言主题输入框,让用户可以概括留言内容。展示留言区域,采用列表形式或者卡片形式展示已有的留言,每条留言包括留言主题、留言内容摘要、留言时间和留言人(如果用户已登录并允许显示用户名)。
通过设计数据库表来存储留言信息,包括留言主题、留言内容、留言时间、留言
人。当接收到前端发送的留言信息后,将其存储到数据库中,同时记录留言的时间戳,确保留言按照时间顺序存储。管理员可以检索某个特定用户的所有留言,或者查看某一时间段内的留言。对留言内容进行过滤,防止恶意留言或者包含敏感信息的留言发布。
下图为留言板的页面设计截图:
![image](https://cdn-mineru.openxlab.org.cn/result/2026-01-08/e4d88765-b6c4-4c8b-8af6-48f02a33acc1/3a1d5caec6afb9455c7985a1584b8e8389ccc53560ce683755ca822bf32c35e7.jpg)
图8留言板界面
![image](https://cdn-mineru.openxlab.org.cn/result/2026-01-08/e4d88765-b6c4-4c8b-8af6-48f02a33acc1/16318a92dd2b0abaa29b213ac8e3344b98c1aab6a2f2871e13782bebde839dfe.jpg)
图9留言板界面
以下为留言板界面的关键代码:
```html
<div v-for="item,index) in dataList" v-bind:key="index" href="javascript:void(0);" class="forum-item"> <h3 style="padding:0px 50px;"留言标题{{item.liuyanName}}></h3> <h6 style="padding:0px 50px;"昵称{{item.yonghuName}}></h6> <div class="content" style="padding:0px 50px;"
```
留言内容:
```handlebars
<div>
&nbsp;&nbsp;&nbsp;&nbsp;<span v-html="myFilters(item.liuyanText)"></span>
</div>
<div style="float: right;">
{{item.insertTime}}
</div>
```
# 4.3项目3Android应用开发
# 4.3.1、UI界面设计
```txt
UI界面设计主体是简洁的登录表单包含用户名输入框、密码输入框、“登录”按钮输入框下方适时显示错误提示下方还设置“忘记密码”“注册新用户”等引导链接。注册表单内容更丰富包含用户名、密码输入框每个框都有对应格式提示底部是“提交注册”按钮页面有全面的错误提示区域来反馈不符合要求的情况。
```
下图为 UI 登录页面:
![image](https://cdn-mineru.openxlab.org.cn/result/2026-01-08/e4d88765-b6c4-4c8b-8af6-48f02a33acc1/b72df9d10c05a063afc64480ff1b6d94f96f923a345a93f65a9afffccfdda5b9.jpg)
图10 UI登陆界面
以下为 UI 登陆界面的关键代码:
```kotlin
if (email.isEmpty() || password.isEmpty() || password.isEmpty()) {
toast("输入完整信息")
return@set OnClickListener
} lifecycleScope.launch(Dispatchers.Default) {
getDoa().getUser(email).let {
if (it.isEmpty())
lifecycleScope.launch {
user = it[0]
toast("登录成功")
}
}
}
```
图11 UI登陆界面关键代码
# 4.3.2、Activity 的跳转
Activity 跳转可以代表不同的功能模块,如下图在汽车租赁管理系统 APP 中,一个 Activity 用于展示汽车列表供用户选择(租车界面),另一个 Activity 用于显示租车订单详情。这样的分离使得每个模块可以独立开发和维护,便于代码的组织和管理。当需要对租车流程进行修改或者优化时,只需要在对应的 Activity 中进行修改,
不会影响到其他不相关的功能模块。用户从出租页面返回退租列表页面,系统能正确地恢复车辆列表页面的状态,包括之前的滚动位置、筛选条件等,提供了连贯的操作感受。
通过 Intent 意图来控制 Activity 跳转的方式,如启动新的 Activity 或者在栈顶替换现有 Activity 等。这使得开发者能够根据应用的需求创建多样化的导航模式。在用户完成租车操作后,可以选择将用户引导至一个新的 Activity展示租车成功的信息以及后续服务内容或者在用户登录后直接替换当前的登录 Activity 为应用的主界面 Activity让用户感觉流程连贯、操作自然。
Android系统通过任务栈来管理Activity的顺序。这种机制使得应用能够模拟用户在实际使用场景中的操作流程方便用户在不同的界面之间进行切换和返回。用户在汽车租赁APP中从主界面进入车辆详情页再进入租车下单页系统会将这些Activity依次放入任务栈。用户可以通过系统返回键按照相反的顺序返回之前浏览过的页面就像在网页浏览器中浏览网页一样这符合用户的使用习惯。
下图为出租页面以及退租界面:
![image](https://cdn-mineru.openxlab.org.cn/result/2026-01-08/e4d88765-b6c4-4c8b-8af6-48f02a33acc1/f655aac5ff7d65bac819eee07efbf059e843e7205cd0705c6f37ee7d416c1e22.jpg)
图12出租界面
![image](https://cdn-mineru.openxlab.org.cn/result/2026-01-08/e4d88765-b6c4-4c8b-8af6-48f02a33acc1/4f19f134619ceaccc4ae37ad2c25b521154ceaa308c202b0d4312fb19887a013.jpg)
图13退租界面
# 4.3.3、菜单的使用
此系统通过使用“汽车”菜单和“已租”菜单。方便用户操作,用户可以快速浏览并找到他们想要的功能。用户不用去记忆复杂的快捷键,通过菜单就能轻松找到“保存”“撤销”等功能。
使用菜单可以提升用户体验,一个设计良好的菜单能够引导用户,让用户对软件
功能有清晰的认识。当软件需要增加新功能时,可以方便地在菜单中添加对应的选
项。开发人员可以简单地在菜单结构中插入新的菜单项来扩展软件功能,而不会对软
件整体架构造成太大混乱,菜单的存在使得软件功能的组织更加有条理。
车辆名称:丰田凯美瑞
车座数5
押金Y4000元
价格每日250元
![image](https://cdn-mineru.openxlab.org.cn/result/2026-01-08/e4d88765-b6c4-4c8b-8af6-48f02a33acc1/08d4ac4af9334977ef60de92ffb9a1d0550bd9a833855ab6e52b8e12117cbbdd.jpg)
汽车
![image](https://cdn-mineru.openxlab.org.cn/result/2026-01-08/e4d88765-b6c4-4c8b-8af6-48f02a33acc1/c9c712d31fe946c92dfbf2986bffc933f6fa7f0ff0c6e44c2d5829335624da51.jpg)
已租
图14菜单界面
# 4.3.4、对话框的使用
确认对话框用于让用户确认某个重要的操作,如删除租赁信息、取消订单等可能
会对数据产生重大影响的操作。确定界面简洁,聚焦重点,确定对话框通常只包含最
关键的选项,如“确定”和“取消”,避免了给用户过多复杂的选择,让用户能够集
中精力思考当前的关键问题。它在屏幕上以弹出的形式出现不会对APP的主界面
布局产生过多干扰。当对话框弹出时,主界面会被虚化或者部分遮挡,将用户的注意
力吸引到对话框的内容上,等用户操作完成后,主界面又可以正常使用如下图所示,
当管理员想要删除一辆汽车的退租信息时,弹出一个确认对话框,询问“确定退租
吗”。
![image](https://cdn-mineru.openxlab.org.cn/result/2026-01-08/e4d88765-b6c4-4c8b-8af6-48f02a33acc1/dbdbd3b72e73ffe8509d6b719b8ae3e4c4936d9a732868b3e781ec813c7a3fc0.jpg)
图15确定退租界面
已下为确定退租的关键代码:
AlertDialog.BuilderrequireContext())
.setTitle("退租")
.setMessage("确定退租吗?")
.setPositiveButton("确定")
5、系统测试与运行
5.1 测试阶段
5.1.1、单元测试
单元测试是对该APP的最小可测试单元进行的测试。这些最小单元可以是一个
函数、一个方法或者一个类。单元测试主要检查这些单元是否按照预期的方式工作,
独立于APP的其他组件。测试租车价格计算模块输入租车天数和车型验证计算
出的价格是否正确。其目的是尽早发现代码中的错误,提高软件质量和稳定性。
# 5.1.2、集成测试
集成测试是在单元测试的基础上进行的测试。它将各个经过单元测试的模块组合在一起,测试这些模块之间的接口和交互是否正确。在汽车租赁系统中,测试租车下单模块和库存管理模块之间的交互。当租车下单时,库存管理模块能否正确减少可租车辆数量;或者测试用户管理模块和订单管理模块之间的信息传递是否准确,像用户信息是否能准确关联到对应的租赁订单上。通过集成测试,可以发现模块集成后可能出现的问题,如接口不匹配、数据传递错误等。
# 5.1.3、安全性测试
安全性测试是用于评估该APP保护数据和抵御潜在安全威胁的能力。用户需要检查用户名和密码的验证过程是否存在漏洞比如是否能被暴力破解是否会泄露用户登录凭证。管理员尝试用常见密码去登录系统看是否有安全机制阻止。确保在APP和服务器之间传输的用户信息和租赁业务数据是加密的。管理员通过抓包工具查看数据是否以密文形式传输。普通用户不能访问系统管理后台功能租车用户不能修改其他用户的订单等权限是否严格限制。
# 5.2 运行阶段
此系统根据系统开发所选用的技术框架 SpringBoot准备好相应的服务器环境安装配置好数据库 MySQL、Web 服务器,确保系统能正常部署并启动运行。日常也应该进行监控与维护,实时关注系统运行状态,通过日志文件查看是否有报错信息产生,对于出现的错误及时排查修复,如数据库连接异常、程序运行时出现的空指针等问题。同时应该定期备份数据库,防止数据丢失,备份频率可根据业务量等情况确定,如每天备份一次或每周备份多次等。根据业务发展和用户反馈,适时对系统进行功能优化和升级,确保系统持续满足汽车租赁业务的管理需求。
# 3、课程实践总结
汽车租赁管理实训课程落下帷幕,这段充实的实践经历为我打开了一扇通往专业技能提升与职场认知的大门。通过模拟运营汽车租赁管理系统的核心业务流程,全方位锤炼了自己的知识运用、技术实操与团队协作能力,同时也在发现问题、解决问题中积累了宝贵经验,明确了未来的成长方向。
实训前,虽对汽车租赁概念有初步了解,但仅停留在表面。此次深入其中,才真切掌握其复杂而精细的业务逻辑。从车辆采购源头,明白了依据市场需求、预算考量
选择车型,与供应商洽谈采购合同细节,确保车辆按时、高质量交付;在车辆入库环节,学会严谨登记车辆基本信息、购置日期、初始车况等,为后续租赁管理筑牢根基。
客户管理板块知识大幅扩充,知晓精准识别客户群体,针对个人租户、企业客户制定差异化营销策略。不仅收集姓名、联系方式常规信息,更注重挖掘租车用途、消费习惯,以提供个性化服务,促进客户忠诚度养成。租赁订单处理流程中,厘清不同租赁时长、车型、淡旺季定价策略,理解合同条款拟定要点,从保险责任界定到逾期归还违约责任,每一处细节都关乎公司与客户双方权益保障。
技术实现是将汽车租赁业务蓝图落地的关键画笔。数据库搭建方面,运用具体数据库软件名称,依据车辆、客户、订单、员工等实体关系,精心设计表结构。车辆表中,字段涵盖车牌号、车架号、品牌型号、购置价格、保养记录等,通过主键唯一标识,外键关联订单表与客户表,确保数据一致性与完整性,经反复调试优化,实现数据高效存储、查询与更新。
编程实践中,前端语言名称结合后端语言名称打造系统前后台。前端界面聚焦用户体验,设计简洁租车预订页面,客户输入租车起止时间、车型偏好等信息后,实时
反馈可选车辆列表、预估费用;后台则承载繁重业务逻辑,订单生成瞬间,自动触发库存检查、费用计算、合同生成模块协同工作,利用函数封装、类继承等编程技巧,让代码条理清晰、易于维护,在一次次报错修复中,代码驾驭能力稳步提升。
实践并非坦途,诸多问题纷至沓来。性能瓶颈首当其冲,伴随模拟业务量增长,系统响应迟缓,订单查询耗时过长。经排查,发现数据库查询语句缺乏优化,索引设置不合理,通过重构查询逻辑、按需添加索引,结合缓存技术暂存高频访问数据,大幅提速。
用户体验瑕疵也不容忽视,部分界面操作繁琐,引导提示不明,致新用户上手困难。重新设计交互流程,引入分步式引导、信息实时校验,以简洁图标、醒目标签优化视觉呈现,让系统易用性显著改善。再者,数据安全隐患暗藏,传输未加密易泄露用户隐私,紧急强化加密算法,对敏感数据存储加密处理,设置严格用户权限,仅授权人员可访问关键信息,为系统筑牢安全防线。
本系统是以B/S模式为网络结构模式在IDEA开发环境中首先使用Java语言设计系统功能使用MySQL数据库存储数据信息然后使用连接语言实现前端Java语言与后台MySQL数据库的交互再通过平台提供的Tomcat插件将系统发布到
Tomcat服务器上最后用户可以选择浏览器打开网址使用本系统。本系统使用性能稳定可靠在功能设计上基本上达到预期的设计目标并且根据系统测试结果可以得知本系统现在是可以正常的被投入使用。
如今是信息化的社会,随着大数据技术、人工智能、深度学习等新一代科学技术力量的出现,大大加快了各行业信息化建设的进程。我们应该努力学习新一代科学技术以及相关知识,不断提高自己的专业能力水平,设计和实现出一款能够顺应时代变化的,功能强大的信息管理系统。
可成长的路上没有终点,冷静审视,我深知自身不足。系统高扩展性设计关乎未来能否承载业务爆发式增长,智能化运营决策支持功能植入更是能为企业精准导航,而我在这些关键领域仅初窥门径。
再看智能化运营决策支持功能植入,这无疑是企业在激烈市场竞争中的“智能导航仪”。借助大数据分析、人工智能算法,它能精准预测市场需求,提前布局热门车型储备,优化租赁价格策略,还能依客户信用精准风控。可反观自己,目前仅仅是略知皮毛,尚未掌握核心技术要领,想要自如运用,还需付出大量心力深入钻研。
由于本人的专业能力和时间有限,本系统可能存在一定的局限性,比如系统处理
能力、用户信息安全等方面可能存在不足。本人将通过学习目前比较主流的计算机技术和新型科技知识,并且将其积极的应用到系统的设计过程中,增强系统的可维护性,提高系统安全性,提升系统的实用性,让系统更加人性化、智能化,在用户使用本系统时,使得系统能够更快的响应用户,更好的服务用户。
本人签字:
年月日
# 评阅教师评阅意见
评阅成绩:
评阅教师:
年月日