add
This commit is contained in:
31
android/app/src/main/AndroidManifest.xml
Normal file
31
android/app/src/main/AndroidManifest.xml
Normal 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>
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
29
android/app/src/main/java/com/carmaintenance/MainActivity.kt
Normal file
29
android/app/src/main/java/com/carmaintenance/MainActivity.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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: 实现工作人员仪表板
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
115
android/app/src/main/java/com/carmaintenance/ui/theme/Type.kt
Normal file
115
android/app/src/main/java/com/carmaintenance/ui/theme/Type.kt
Normal 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
|
||||
)
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
3
android/app/src/main/res/values/strings.xml
Normal file
3
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">车管家</string>
|
||||
</resources>
|
||||
4
android/app/src/main/res/values/themes.xml
Normal file
4
android/app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.CarMaintenance" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
4
android/app/src/main/res/xml/backup_rules.xml
Normal file
4
android/app/src/main/res/xml/backup_rules.xml
Normal 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>
|
||||
6
android/app/src/main/res/xml/data_extraction_rules.xml
Normal file
6
android/app/src/main/res/xml/data_extraction_rules.xml
Normal 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>
|
||||
Reference in New Issue
Block a user