upd
This commit is contained in:
@@ -1,26 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<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:label="Car Maintenance"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:roundIcon="@drawable/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.CarMaintenance"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
android:theme="@style/Theme.CarMaintenance">
|
||||
<activity android:name=".RoleHomeActivity" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.CarMaintenance">
|
||||
android:name=".LoginActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
30
android/app/src/main/java/com/carmaintenance/ApiClient.java
Normal file
30
android/app/src/main/java/com/carmaintenance/ApiClient.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.carmaintenance;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
public class ApiClient {
|
||||
private static ApiService service;
|
||||
|
||||
public static ApiService getService() {
|
||||
if (service == null) {
|
||||
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
|
||||
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.addInterceptor(logging)
|
||||
.build();
|
||||
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl(BuildConfig.API_BASE_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.client(client)
|
||||
.build();
|
||||
|
||||
service = retrofit.create(ApiService.class);
|
||||
}
|
||||
return service;
|
||||
}
|
||||
}
|
||||
19
android/app/src/main/java/com/carmaintenance/ApiResult.java
Normal file
19
android/app/src/main/java/com/carmaintenance/ApiResult.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.carmaintenance;
|
||||
|
||||
public class ApiResult<T> {
|
||||
private int code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public T getData() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
10
android/app/src/main/java/com/carmaintenance/ApiService.java
Normal file
10
android/app/src/main/java/com/carmaintenance/ApiService.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package com.carmaintenance;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.POST;
|
||||
|
||||
public interface ApiService {
|
||||
@POST("auth/login")
|
||||
Call<ApiResult<LoginResponse>> login(@Body LoginRequest request);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.carmaintenance
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class CarMaintenanceApplication : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
}
|
||||
}
|
||||
104
android/app/src/main/java/com/carmaintenance/LoginActivity.java
Normal file
104
android/app/src/main/java/com/carmaintenance/LoginActivity.java
Normal file
@@ -0,0 +1,104 @@
|
||||
package com.carmaintenance;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class LoginActivity extends AppCompatActivity {
|
||||
private TextInputEditText usernameInput;
|
||||
private TextInputEditText passwordInput;
|
||||
private ProgressBar loginProgress;
|
||||
private TextView loginError;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_login);
|
||||
|
||||
usernameInput = findViewById(R.id.usernameInput);
|
||||
passwordInput = findViewById(R.id.passwordInput);
|
||||
loginProgress = findViewById(R.id.loginProgress);
|
||||
loginError = findViewById(R.id.loginError);
|
||||
Button loginButton = findViewById(R.id.loginButton);
|
||||
|
||||
loginButton.setOnClickListener(v -> attemptLogin());
|
||||
}
|
||||
|
||||
private void attemptLogin() {
|
||||
String username = textOf(usernameInput);
|
||||
String password = textOf(passwordInput);
|
||||
|
||||
if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) {
|
||||
showError("Username and password are required.");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
LoginRequest request = new LoginRequest(username, password);
|
||||
|
||||
ApiClient.getService().login(request).enqueue(new Callback<ApiResult<LoginResponse>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResult<LoginResponse>> call, Response<ApiResult<LoginResponse>> response) {
|
||||
setLoading(false);
|
||||
|
||||
if (!response.isSuccessful() || response.body() == null) {
|
||||
showError("Login failed. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
ApiResult<LoginResponse> result = response.body();
|
||||
if (result.getCode() != 200 || result.getData() == null) {
|
||||
showError(result.getMessage() != null ? result.getMessage() : "Login failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
LoginResponse data = result.getData();
|
||||
new TokenStore(LoginActivity.this).saveToken(data.getToken());
|
||||
|
||||
String role = data.getUserInfo() != null ? data.getUserInfo().getRole() : "unknown";
|
||||
String displayName = data.getUserInfo() != null ? data.getUserInfo().getUsername() : username;
|
||||
|
||||
Intent intent = new Intent(LoginActivity.this, RoleHomeActivity.class);
|
||||
intent.putExtra("role", role);
|
||||
intent.putExtra("username", displayName);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResult<LoginResponse>> call, Throwable t) {
|
||||
setLoading(false);
|
||||
showError("Network error: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String textOf(TextInputEditText input) {
|
||||
if (input.getText() == null) {
|
||||
return "";
|
||||
}
|
||||
return input.getText().toString().trim();
|
||||
}
|
||||
|
||||
private void setLoading(boolean loading) {
|
||||
loginProgress.setVisibility(loading ? View.VISIBLE : View.GONE);
|
||||
loginError.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void showError(String message) {
|
||||
loginError.setText(message);
|
||||
loginError.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.carmaintenance;
|
||||
|
||||
public class LoginRequest {
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
public LoginRequest(String username, String password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.carmaintenance;
|
||||
|
||||
public class LoginResponse {
|
||||
private String token;
|
||||
private UserInfo userInfo;
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public UserInfo getUserInfo() {
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
public static class UserInfo {
|
||||
private Integer userId;
|
||||
private String username;
|
||||
private String realName;
|
||||
private String phone;
|
||||
private String email;
|
||||
private String role;
|
||||
|
||||
public Integer getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getRealName() {
|
||||
return realName;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.carmaintenance
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.carmaintenance.ui.navigation.CarMaintenanceNavGraph
|
||||
import com.carmaintenance.ui.theme.CarMaintenanceTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
CarMaintenanceTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CarMaintenanceNavGraph()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.carmaintenance;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
public class RoleHomeActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_role_home);
|
||||
|
||||
TextView userInfo = findViewById(R.id.userInfo);
|
||||
Button primaryAction = findViewById(R.id.primaryAction);
|
||||
Button secondaryAction = findViewById(R.id.secondaryAction);
|
||||
Button logoutButton = findViewById(R.id.logoutButton);
|
||||
|
||||
String role = getIntent().getStringExtra("role");
|
||||
String username = getIntent().getStringExtra("username");
|
||||
|
||||
if (role == null) {
|
||||
role = "unknown";
|
||||
}
|
||||
if (username == null) {
|
||||
username = "user";
|
||||
}
|
||||
|
||||
userInfo.setText("Welcome " + username + " (" + role + ")");
|
||||
|
||||
if ("admin".equalsIgnoreCase(role)) {
|
||||
primaryAction.setText("Manage Users");
|
||||
secondaryAction.setText("Manage Orders");
|
||||
} else if ("staff".equalsIgnoreCase(role)) {
|
||||
primaryAction.setText("Search Vehicles");
|
||||
secondaryAction.setText("Parts Lookup");
|
||||
} else {
|
||||
primaryAction.setText("My Vehicles");
|
||||
secondaryAction.setText("My Appointments");
|
||||
}
|
||||
|
||||
logoutButton.setOnClickListener(v -> {
|
||||
new TokenStore(RoleHomeActivity.this).clear();
|
||||
Intent intent = new Intent(RoleHomeActivity.this, LoginActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(intent);
|
||||
});
|
||||
}
|
||||
}
|
||||
27
android/app/src/main/java/com/carmaintenance/TokenStore.java
Normal file
27
android/app/src/main/java/com/carmaintenance/TokenStore.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.carmaintenance;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public class TokenStore {
|
||||
private static final String PREFS_NAME = "auth_prefs";
|
||||
private static final String KEY_TOKEN = "token";
|
||||
|
||||
private final SharedPreferences preferences;
|
||||
|
||||
public TokenStore(Context context) {
|
||||
this.preferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public void saveToken(String token) {
|
||||
preferences.edit().putString(KEY_TOKEN, token).apply();
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return preferences.getString(KEY_TOKEN, null);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
preferences.edit().remove(KEY_TOKEN).apply();
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.carmaintenance.data.interceptor
|
||||
|
||||
import android.content.Context
|
||||
import com.carmaintenance.data.manager.TokenManager
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AuthInterceptor @Inject constructor(
|
||||
private val tokenManager: TokenManager
|
||||
) : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
// 获取Token
|
||||
val token = runBlocking { tokenManager.getToken() }
|
||||
|
||||
val newRequest = if (token != null) {
|
||||
originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer $token")
|
||||
.build()
|
||||
} else {
|
||||
originalRequest
|
||||
}
|
||||
|
||||
return chain.proceed(newRequest)
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.carmaintenance.data.manager
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private val Context.dataStore: DataStore<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]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package com.carmaintenance.data.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* 预约实体
|
||||
*/
|
||||
data class Appointment(
|
||||
@SerializedName("appointmentId")
|
||||
val appointmentId: Int,
|
||||
@SerializedName("customerId")
|
||||
val customerId: Int,
|
||||
@SerializedName("vehicleId")
|
||||
val vehicleId: Int,
|
||||
@SerializedName("serviceType")
|
||||
val serviceType: String,
|
||||
@SerializedName("appointmentTime")
|
||||
val appointmentTime: String,
|
||||
@SerializedName("contactPhone")
|
||||
val contactPhone: String,
|
||||
@SerializedName("description")
|
||||
val description: String? = null,
|
||||
@SerializedName("status")
|
||||
val status: String,
|
||||
@SerializedName("createTime")
|
||||
val createTime: String
|
||||
) {
|
||||
enum class Status(val displayName: String) {
|
||||
PENDING("待确认"),
|
||||
CONFIRMED("已确认"),
|
||||
COMPLETED("已完成"),
|
||||
CANCELLED("已取消");
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: String): Status {
|
||||
return values().find { it.name.lowercase() == value } ?: PENDING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val statusEnum: Status
|
||||
get() = Status.fromValue(status)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.carmaintenance.data.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* 登录请求
|
||||
*/
|
||||
data class LoginRequest(
|
||||
@SerializedName("username")
|
||||
val username: String,
|
||||
@SerializedName("password")
|
||||
val password: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 登录响应数据
|
||||
*/
|
||||
data class LoginResponseData(
|
||||
@SerializedName("token")
|
||||
val token: String,
|
||||
@SerializedName("userInfo")
|
||||
val userInfo: User
|
||||
)
|
||||
@@ -1,84 +0,0 @@
|
||||
package com.carmaintenance.data.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* 维保工单实体
|
||||
*/
|
||||
data class ServiceOrder(
|
||||
@SerializedName("orderId")
|
||||
val orderId: Int,
|
||||
@SerializedName("orderNo")
|
||||
val orderNo: String,
|
||||
@SerializedName("vehicleId")
|
||||
val vehicleId: Int,
|
||||
@SerializedName("customerId")
|
||||
val customerId: Int,
|
||||
@SerializedName("serviceType")
|
||||
val serviceType: String,
|
||||
@SerializedName("appointmentTime")
|
||||
val appointmentTime: String? = null,
|
||||
@SerializedName("arrivalTime")
|
||||
val arrivalTime: String? = null,
|
||||
@SerializedName("startTime")
|
||||
val startTime: String? = null,
|
||||
@SerializedName("completeTime")
|
||||
val completeTime: String? = null,
|
||||
@SerializedName("staffId")
|
||||
val staffId: Int? = null,
|
||||
@SerializedName("currentMileage")
|
||||
val currentMileage: Double? = null,
|
||||
@SerializedName("faultDescription")
|
||||
val faultDescription: String? = null,
|
||||
@SerializedName("diagnosisResult")
|
||||
val diagnosisResult: String? = null,
|
||||
@SerializedName("serviceItems")
|
||||
val serviceItems: String? = null,
|
||||
@SerializedName("partsCost")
|
||||
val partsCost: Double? = 0.0,
|
||||
@SerializedName("laborCost")
|
||||
val laborCost: Double? = 0.0,
|
||||
@SerializedName("totalCost")
|
||||
val totalCost: Double? = 0.0,
|
||||
@SerializedName("status")
|
||||
val status: String,
|
||||
@SerializedName("paymentStatus")
|
||||
val paymentStatus: String,
|
||||
@SerializedName("remark")
|
||||
val remark: String? = null,
|
||||
@SerializedName("createTime")
|
||||
val createTime: String
|
||||
) {
|
||||
enum class ServiceType(val displayName: String) {
|
||||
MAINTENANCE("保养维护"),
|
||||
REPAIR("维修服务"),
|
||||
BEAUTY("美容服务"),
|
||||
INSURANCE("保险代理");
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: String): ServiceType {
|
||||
return values().find { it.name.lowercase() == value } ?: MAINTENANCE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Status(val displayName: String) {
|
||||
PENDING("待处理"),
|
||||
APPOINTED("已预约"),
|
||||
IN_PROGRESS("进行中"),
|
||||
COMPLETED("已完成"),
|
||||
CANCELLED("已取消");
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: String): Status {
|
||||
return values().find { it.name.lowercase() == value } ?: PENDING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val serviceTypeEnum: ServiceType
|
||||
get() = ServiceType.fromValue(serviceType)
|
||||
|
||||
val statusEnum: Status
|
||||
get() = Status.fromValue(status)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.carmaintenance.data.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* 用户实体
|
||||
*/
|
||||
data class User(
|
||||
@SerializedName("userId")
|
||||
val userId: Int,
|
||||
@SerializedName("username")
|
||||
val username: String,
|
||||
@SerializedName("realName")
|
||||
val realName: String,
|
||||
@SerializedName("phone")
|
||||
val phone: String,
|
||||
@SerializedName("email")
|
||||
val email: String? = null,
|
||||
@SerializedName("role")
|
||||
val role: String,
|
||||
@SerializedName("status")
|
||||
val status: Int
|
||||
) {
|
||||
enum class Role(val value: String) {
|
||||
ADMIN("admin"),
|
||||
STAFF("staff"),
|
||||
CUSTOMER("customer");
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: String): Role {
|
||||
return values().find { it.value == value } ?: CUSTOMER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val roleEnum: Role
|
||||
get() = Role.fromValue(role)
|
||||
|
||||
val isActive: Boolean
|
||||
get() = status == 1
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.carmaintenance.data.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* 车辆实体
|
||||
*/
|
||||
data class Vehicle(
|
||||
@SerializedName("vehicleId")
|
||||
val vehicleId: Int,
|
||||
@SerializedName("customerId")
|
||||
val customerId: Int,
|
||||
@SerializedName("licensePlate")
|
||||
val licensePlate: String,
|
||||
@SerializedName("brand")
|
||||
val brand: String,
|
||||
@SerializedName("model")
|
||||
val model: String,
|
||||
@SerializedName("color")
|
||||
val color: String? = null,
|
||||
@SerializedName("vin")
|
||||
val vin: String? = null,
|
||||
@SerializedName("engineNo")
|
||||
val engineNo: String? = null,
|
||||
@SerializedName("purchaseDate")
|
||||
val purchaseDate: String? = null,
|
||||
@SerializedName("mileage")
|
||||
val mileage: Double? = 0.0,
|
||||
@SerializedName("lastMaintenanceDate")
|
||||
val lastMaintenanceDate: String? = null,
|
||||
@SerializedName("nextMaintenanceDate")
|
||||
val nextMaintenanceDate: String? = null,
|
||||
@SerializedName("status")
|
||||
val status: String = "normal"
|
||||
) {
|
||||
val displayName: String
|
||||
get() = "$licensePlate - $brand $model"
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package com.carmaintenance.data.remote
|
||||
|
||||
import com.carmaintenance.data.model.*
|
||||
import retrofit2.http.*
|
||||
|
||||
/**
|
||||
* API服务接口
|
||||
*/
|
||||
interface ApiService {
|
||||
|
||||
// ==================== 认证相关 ====================
|
||||
@POST("auth/login")
|
||||
suspend fun login(@Body request: LoginRequest): ApiResponse<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
|
||||
)
|
||||
@@ -1,64 +0,0 @@
|
||||
package com.carmaintenance.data.repository
|
||||
|
||||
import com.carmaintenance.data.model.Appointment
|
||||
import com.carmaintenance.data.remote.ApiResponse
|
||||
import com.carmaintenance.data.remote.ApiService
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AppointmentRepository @Inject constructor(
|
||||
private val apiService: ApiService
|
||||
) {
|
||||
suspend fun getAppointments(): Result<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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package com.carmaintenance.data.repository
|
||||
|
||||
import com.carmaintenance.data.manager.TokenManager
|
||||
import com.carmaintenance.data.model.LoginRequest
|
||||
import com.carmaintenance.data.model.User
|
||||
import com.carmaintenance.data.remote.ApiResponse
|
||||
import com.carmaintenance.data.remote.ApiService
|
||||
import com.google.gson.Gson
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AuthRepository @Inject constructor(
|
||||
private val apiService: ApiService,
|
||||
private val tokenManager: TokenManager
|
||||
) {
|
||||
suspend fun login(username: String, password: String): Result<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()
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package com.carmaintenance.data.repository
|
||||
|
||||
import com.carmaintenance.data.model.ServiceOrder
|
||||
import com.carmaintenance.data.remote.ApiResponse
|
||||
import com.carmaintenance.data.remote.ApiService
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class OrderRepository @Inject constructor(
|
||||
private val apiService: ApiService
|
||||
) {
|
||||
suspend fun getOrders(): Result<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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.carmaintenance.data.repository
|
||||
|
||||
import com.carmaintenance.data.model.Vehicle
|
||||
import com.carmaintenance.data.remote.ApiResponse
|
||||
import com.carmaintenance.data.remote.ApiService
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class VehicleRepository @Inject constructor(
|
||||
private val apiService: ApiService
|
||||
) {
|
||||
suspend fun getVehicles(): Result<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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.carmaintenance.di
|
||||
|
||||
import com.carmaintenance.BuildConfig
|
||||
import com.carmaintenance.data.remote.ApiService
|
||||
import com.carmaintenance.data.interceptor.AuthInterceptor
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLoggingInterceptor(): HttpLoggingInterceptor {
|
||||
return HttpLoggingInterceptor().apply {
|
||||
level = if (BuildConfig.DEBUG) {
|
||||
HttpLoggingInterceptor.Level.BODY
|
||||
} else {
|
||||
HttpLoggingInterceptor.Level.NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(
|
||||
loggingInterceptor: HttpLoggingInterceptor,
|
||||
authInterceptor: AuthInterceptor
|
||||
): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(authInterceptor)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BuildConfig.API_BASE_URL)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideApiService(retrofit: Retrofit): ApiService {
|
||||
return retrofit.create(ApiService::class.java)
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package com.carmaintenance.ui.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import com.carmaintenance.ui.screen.LoginScreen
|
||||
import com.carmaintenance.ui.screen.admin.AdminDashboardScreen
|
||||
import com.carmaintenance.ui.screen.customer.CustomerDashboardScreen
|
||||
import com.carmaintenance.ui.screen.customer.CustomerAppointmentsScreen
|
||||
import com.carmaintenance.ui.screen.customer.CustomerOrdersScreen
|
||||
import com.carmaintenance.ui.screen.customer.CustomerVehiclesScreen
|
||||
import com.carmaintenance.ui.viewmodel.AuthViewModel
|
||||
import com.carmaintenance.ui.viewmodel.AuthUiState
|
||||
|
||||
@Composable
|
||||
fun CarMaintenanceNavGraph(
|
||||
navController: NavHostController,
|
||||
authViewModel: AuthViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState by authViewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(uiState) {
|
||||
when (uiState) {
|
||||
is AuthUiState.LoggedIn -> {
|
||||
val user = (uiState as AuthUiState.LoggedIn).user
|
||||
val route = when (user.role) {
|
||||
"admin" -> Screen.AdminDashboard.route
|
||||
"staff" -> Screen.StaffDashboard.route
|
||||
"customer" -> Screen.CustomerDashboard.route
|
||||
else -> Screen.Login.route
|
||||
}
|
||||
navController.navigate(route) {
|
||||
popUpTo(Screen.Login.route) { inclusive = true }
|
||||
}
|
||||
}
|
||||
is AuthUiState.NotLoggedIn -> {
|
||||
navController.navigate(Screen.Login.route) {
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screen.Login.route
|
||||
) {
|
||||
composable(Screen.Login.route) {
|
||||
LoginScreen(
|
||||
onLoginSuccess = {
|
||||
// 导航由LaunchedEffect处理
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable(Screen.CustomerDashboard.route) {
|
||||
CustomerDashboardScreen(
|
||||
onNavigateBack = { authViewModel.logout() }
|
||||
)
|
||||
}
|
||||
|
||||
composable(Screen.CustomerVehicles.route) {
|
||||
CustomerVehiclesScreen(
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
composable(Screen.CustomerOrders.route) {
|
||||
CustomerOrdersScreen(
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
composable(Screen.CustomerAppointments.route) {
|
||||
CustomerAppointmentsScreen(
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
composable(Screen.AdminDashboard.route) {
|
||||
AdminDashboardScreen(
|
||||
onNavigateBack = { authViewModel.logout() }
|
||||
)
|
||||
}
|
||||
|
||||
composable(Screen.StaffDashboard.route) {
|
||||
// TODO: 实现工作人员仪表板
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.carmaintenance.ui.navigation
|
||||
|
||||
sealed class Screen(val route: String) {
|
||||
object Login : Screen("login")
|
||||
object CustomerDashboard : Screen("customer_dashboard")
|
||||
object CustomerVehicles : Screen("customer_vehicles")
|
||||
object CustomerOrders : Screen("customer_orders")
|
||||
object CustomerAppointments : Screen("customer_appointments")
|
||||
object AdminDashboard : Screen("admin_dashboard")
|
||||
object StaffDashboard : Screen("staff_dashboard")
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
package com.carmaintenance.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.carmaintenance.ui.viewmodel.AuthViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
onLoginSuccess: () -> Unit,
|
||||
viewModel: AuthViewModel = hiltViewModel()
|
||||
) {
|
||||
var username by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
var selectedRole by remember { mutableStateOf("customer") }
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(uiState) {
|
||||
if (uiState is AuthUiState.LoggedIn) {
|
||||
onLoginSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("车管家系统") },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "欢迎登录",
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(bottom = 48.dp)
|
||||
)
|
||||
|
||||
// 用户名输入
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("用户名") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 密码输入
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("密码") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(
|
||||
imageVector = if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
|
||||
contentDescription = if (passwordVisible) "隐藏密码" else "显示密码"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 角色选择
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "选择角色",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
FilterChip(
|
||||
selected = selectedRole == "admin",
|
||||
onClick = { selectedRole = "admin" },
|
||||
label = { Text("管理员") },
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
FilterChip(
|
||||
selected = selectedRole == "staff",
|
||||
onClick = { selectedRole = "staff" },
|
||||
label = { Text("工作人员") },
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
FilterChip(
|
||||
selected = selectedRole == "customer",
|
||||
onClick = { selectedRole = "customer" },
|
||||
label = { Text("客户") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// 登录按钮
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.login(username, password)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
enabled = username.isNotBlank() && password.isNotBlank() && uiState !is AuthUiState.Loading
|
||||
) {
|
||||
if (uiState is AuthUiState.Loading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
} else {
|
||||
Text("登录", style = MaterialTheme.typography.titleMedium)
|
||||
}
|
||||
}
|
||||
|
||||
// 错误信息
|
||||
if (uiState is AuthUiState.Error) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = (uiState as AuthUiState.Error).message,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 测试账号提示
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "测试账号",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
Text(
|
||||
text = "管理员: admin / 123456\n工作人员: staff001 / 123456\n客户: customer001 / 123456",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
package com.carmaintenance.ui.screen.admin
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.carmaintenance.ui.viewmodel.CustomerViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AdminDashboardScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: CustomerViewModel = hiltViewModel()
|
||||
) {
|
||||
val vehicles by viewModel.vehicles.collectAsState()
|
||||
val isLoading by viewModel.isLoadingVehicles.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("管理员控制台") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "退出")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = "系统概览",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
StatCard(title = "用户总数", value = "3", modifier = Modifier.weight(1f))
|
||||
StatCard(title = "车辆总数", value = "${vehicles.size}", modifier = Modifier.weight(1f))
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
StatCard(title = "工单总数", value = "0", modifier = Modifier.weight(1f))
|
||||
StatCard(title = "库存预警", value = "0", modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "管理功能",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
AdminMenuItem(
|
||||
icon = "👥",
|
||||
title = "用户管理",
|
||||
subtitle = "管理系统用户"
|
||||
)
|
||||
Divider()
|
||||
AdminMenuItem(
|
||||
icon = "🚗",
|
||||
title = "车辆管理",
|
||||
subtitle = "管理车辆档案"
|
||||
)
|
||||
Divider()
|
||||
AdminMenuItem(
|
||||
icon = "📋",
|
||||
title = "工单管理",
|
||||
subtitle = "管理维保工单"
|
||||
)
|
||||
Divider()
|
||||
AdminMenuItem(
|
||||
icon = "📦",
|
||||
title = "配件管理",
|
||||
subtitle = "管理配件库存"
|
||||
)
|
||||
Divider()
|
||||
AdminMenuItem(
|
||||
icon = "📅",
|
||||
title = "预约管理",
|
||||
subtitle = "管理客户预约"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatCard(title: String, value: String, modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AdminMenuItem(icon: String, title: String, subtitle: String) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = icon,
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package com.carmaintenance.ui.screen.customer
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.carmaintenance.ui.viewmodel.CustomerViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CustomerAppointmentsScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: CustomerViewModel = hiltViewModel()
|
||||
) {
|
||||
val appointments by viewModel.appointments.collectAsState()
|
||||
val isLoading by viewModel.isLoadingAppointments.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("我的预约") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回")
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = { /* TODO: 打开预约表单 */ }
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = "新建预约")
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
if (isLoading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else if (appointments.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("暂无预约记录")
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(appointments) { appointment ->
|
||||
AppointmentCard(appointment = appointment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppointmentCard(appointment: com.carmaintenance.data.model.Appointment) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = appointment.serviceTypeEnum.displayName,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Surface(
|
||||
color = when (appointment.statusEnum) {
|
||||
com.carmaintenance.data.model.Appointment.Status.PENDING -> MaterialTheme.colorScheme.tertiaryContainer
|
||||
com.carmaintenance.data.model.Appointment.Status.CONFIRMED -> MaterialTheme.colorScheme.primaryContainer
|
||||
com.carmaintenance.data.model.Appointment.Status.COMPLETED -> MaterialTheme.colorScheme.secondaryContainer
|
||||
com.carmaintenance.data.model.Appointment.Status.CANCELLED -> MaterialTheme.colorScheme.errorContainer
|
||||
},
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Text(
|
||||
text = appointment.statusEnum.displayName,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "预约时间: ${appointment.appointmentTime}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "联系电话: ${appointment.contactPhone}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
package com.carmaintenance.ui.screen.customer
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.carmaintenance.data.model.Vehicle
|
||||
import com.carmaintenance.ui.viewmodel.AuthViewModel
|
||||
import com.carmaintenance.ui.viewmodel.CustomerViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CustomerDashboardScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: CustomerViewModel = hiltViewModel(),
|
||||
authViewModel: AuthViewModel = hiltViewModel()
|
||||
) {
|
||||
val user by authViewModel.currentUser.collectAsState()
|
||||
val vehicles by viewModel.vehicles.collectAsState()
|
||||
val isLoading by viewModel.isLoadingVehicles.collectAsState()
|
||||
|
||||
LaunchedEffect(user) {
|
||||
user?.let { viewModel.loadCustomerData(it.userId) }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("客户中心") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ExitToApp, contentDescription = "退出")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item {
|
||||
// 用户信息卡片
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Person,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = user?.realName ?: "客户",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Text(
|
||||
text = "欢迎回来!",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "快捷功能",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
// 功能菜单
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
FeatureCard(
|
||||
icon = Icons.Default.DirectionsCar,
|
||||
title = "我的车辆",
|
||||
subtitle = "${vehicles.size} 辆",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
FeatureCard(
|
||||
icon = Icons.Default.Receipt,
|
||||
title = "维保记录",
|
||||
subtitle = "查看详情",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
FeatureCard(
|
||||
icon = Icons.Default.Event,
|
||||
title = "我的预约",
|
||||
subtitle = "在线预约",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
FeatureCard(
|
||||
icon = Icons.Default.Notifications,
|
||||
title = "消息通知",
|
||||
subtitle = "暂无消息",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = "我的车辆",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
} else if (vehicles.isEmpty()) {
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.DirectionsCar,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "暂无车辆",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items(vehicles) { vehicle ->
|
||||
VehicleCard(vehicle = vehicle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FeatureCard(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.height(100.dp),
|
||||
onClick = { /* TODO: 导航 */ }
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(12.dp),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VehicleCard(vehicle: Vehicle) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = vehicle.licensePlate,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Text(
|
||||
text = "${vehicle.brand} ${vehicle.model}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Text(
|
||||
text = "正常",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
InfoItem(
|
||||
icon = Icons.Default.Palette,
|
||||
label = "颜色",
|
||||
value = vehicle.color ?: "-"
|
||||
)
|
||||
InfoItem(
|
||||
icon = Icons.Default.Speed,
|
||||
label = "里程",
|
||||
value = "${vehicle.mileage?.toInt() ?: 0} km"
|
||||
)
|
||||
InfoItem(
|
||||
icon = Icons.Default.DateRange,
|
||||
label = "保养",
|
||||
value = vehicle.lastMaintenanceDate ?: "-"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoItem(icon: ImageVector, label: String, value: String) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package com.carmaintenance.ui.screen.customer
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.carmaintenance.ui.viewmodel.CustomerViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CustomerOrdersScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: CustomerViewModel = hiltViewModel()
|
||||
) {
|
||||
val orders by viewModel.orders.collectAsState()
|
||||
val isLoading by viewModel.isLoadingOrders.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("维保记录") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
if (isLoading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else if (orders.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("暂无维保记录")
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(orders) { order ->
|
||||
OrderCard(order = order)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OrderCard(order: com.carmaintenance.data.model.ServiceOrder) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = order.orderNo,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Text(
|
||||
text = order.statusEnum.displayName,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = order.serviceTypeEnum.displayName,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "费用: ¥${order.totalCost ?: 0.0}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = order.createTime,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package com.carmaintenance.ui.screen.customer
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.carmaintenance.ui.viewmodel.CustomerViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CustomerVehiclesScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: CustomerViewModel = hiltViewModel()
|
||||
) {
|
||||
val vehicles by viewModel.vehicles.collectAsState()
|
||||
val isLoading by viewModel.isLoadingVehicles.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("我的车辆") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
if (isLoading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(vehicles) { vehicle ->
|
||||
VehicleCard(vehicle = vehicle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.carmaintenance.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
// 主色调
|
||||
val Primary = Color(0xFF1976D2)
|
||||
val PrimaryDark = Color(0xFF1565C0)
|
||||
val PrimaryLight = Color(0xFF42A5F5)
|
||||
|
||||
// 辅助色
|
||||
val Secondary = Color(0xFF03DAC6)
|
||||
val SecondaryVariant = Color(0xFF018786)
|
||||
|
||||
// 背景色
|
||||
val Background = Color(0xFFFAFAFA)
|
||||
val Surface = Color(0xFFFFFFFF)
|
||||
|
||||
// 状态色
|
||||
val Success = Color(0xFF4CAF50)
|
||||
val Warning = Color(0xFFFFC107)
|
||||
val Error = Color(0xFFF44336)
|
||||
val Info = Color(0xFF2196F3)
|
||||
|
||||
// 文字颜色
|
||||
val TextPrimary = Color(0xFF212121)
|
||||
val TextSecondary = Color(0xFF757575)
|
||||
@@ -1,68 +0,0 @@
|
||||
package com.carmaintenance.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Primary,
|
||||
secondary = Secondary,
|
||||
error = Error
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Primary,
|
||||
primaryContainer = PrimaryLight,
|
||||
secondary = Secondary,
|
||||
secondaryContainer = SecondaryVariant,
|
||||
background = Background,
|
||||
surface = Surface,
|
||||
error = Error,
|
||||
onPrimary = androidx.compose.ui.graphics.Color.White,
|
||||
onSecondary = androidx.compose.ui.graphics.Color.Black,
|
||||
onBackground = TextPrimary,
|
||||
onSurface = TextPrimary
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun CarMaintenanceTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = false,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
window.statusBarColor = colorScheme.primary.toArgb()
|
||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package com.carmaintenance.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
val Typography = Typography(
|
||||
displayLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 57.sp,
|
||||
lineHeight = 64.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
displayMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 45.sp,
|
||||
lineHeight = 52.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
displaySmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 36.sp,
|
||||
lineHeight = 44.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 40.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 36.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 32.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.15.sp
|
||||
),
|
||||
titleSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp
|
||||
),
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.25.sp
|
||||
),
|
||||
bodySmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.4.sp
|
||||
),
|
||||
labelLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp
|
||||
),
|
||||
labelMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
)
|
||||
@@ -1,81 +0,0 @@
|
||||
package com.carmaintenance.ui.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.carmaintenance.data.model.User
|
||||
import com.carmaintenance.data.repository.AuthRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AuthViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow<AuthUiState>(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()
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package com.carmaintenance.ui.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.carmaintenance.data.model.Appointment
|
||||
import com.carmaintenance.data.model.ServiceOrder
|
||||
import com.carmaintenance.data.model.Vehicle
|
||||
import com.carmaintenance.data.repository.AppointmentRepository
|
||||
import com.carmaintenance.data.repository.OrderRepository
|
||||
import com.carmaintenance.data.repository.VehicleRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class CustomerViewModel @Inject constructor(
|
||||
private val vehicleRepository: VehicleRepository,
|
||||
private val orderRepository: OrderRepository,
|
||||
private val appointmentRepository: AppointmentRepository
|
||||
) : ViewModel() {
|
||||
|
||||
// 车辆相关
|
||||
private val _vehicles = MutableStateFlow<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
|
||||
}
|
||||
}
|
||||
4
android/app/src/main/res/drawable/ic_launcher.xml
Normal file
4
android/app/src/main/res/drawable/ic_launcher.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="@color/purple_500" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
86
android/app/src/main/res/layout/activity_login.xml
Normal file
86
android/app/src/main/res/layout/activity_login.xml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Car Maintenance"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/usernameLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/usernameInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/username"
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/passwordLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/usernameLayout"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/passwordInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/password"
|
||||
android:inputType="textPassword" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/loginButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/login"
|
||||
app:layout_constraintTop_toBottomOf="@id/passwordLayout"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loginProgress"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/loginButton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loginError"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:textColor="@android:color/holo_red_dark"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/loginProgress"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
60
android/app/src/main/res/layout/activity_role_home.xml
Normal file
60
android/app/src/main/res/layout/activity_role_home.xml
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Role Home"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/userInfo"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintTop_toBottomOf="@id/welcomeTitle"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/primaryAction"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="Primary Action"
|
||||
app:layout_constraintTop_toBottomOf="@id/userInfo"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/secondaryAction"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="Secondary Action"
|
||||
app:layout_constraintTop_toBottomOf="@id/primaryAction"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/logoutButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/logout"
|
||||
app:layout_constraintTop_toBottomOf="@id/secondaryAction"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
6
android/app/src/main/res/values/colors.xml
Normal file
6
android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<color name="purple_500">#6200EE</color>
|
||||
<color name="purple_700">#3700B3</color>
|
||||
<color name="teal_200">#03DAC5</color>
|
||||
<color name="white">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -1,3 +1,8 @@
|
||||
<resources>
|
||||
<string name="app_name">车管家</string>
|
||||
<string name="app_name">Car Maintenance</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="username">Username</string>
|
||||
<string name="password">Password</string>
|
||||
<string name="role_home">Role Home</string>
|
||||
<string name="logout">Logout</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.CarMaintenance" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.CarMaintenance" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/white</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<include domain="sharedpref" path="user_prefs.xml"/>
|
||||
</full-backup-content>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?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