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

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>