This commit is contained in:
wangziqi
2026-01-09 11:03:54 +08:00
parent abe03cbfef
commit 6295efc386
458 changed files with 63962 additions and 55 deletions

View File

@@ -4,12 +4,13 @@
<application
android:allowBackup="true"
android:label="Car Maintenance"
android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
android:roundIcon="@drawable/ic_launcher"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.CarMaintenance">
<activity android:name=".CustomerAppointmentActivity" />
<activity android:name=".RoleHomeActivity" />
<activity
android:name=".LoginActivity"

View File

@@ -7,4 +7,19 @@ import retrofit2.http.POST;
public interface ApiService {
@POST("auth/login")
Call<ApiResult<LoginResponse>> login(@Body LoginRequest request);
@retrofit2.http.GET("customers/user/{userId}")
Call<ApiResult<Customer>> getCustomerByUserId(@retrofit2.http.Path("userId") int userId);
@retrofit2.http.GET("vehicles/customer/{customerId}")
Call<ApiResult<java.util.List<Vehicle>>> getVehiclesByCustomerId(@retrofit2.http.Path("customerId") int customerId);
@retrofit2.http.GET("appointments/customer/{customerId}")
Call<ApiResult<java.util.List<Appointment>>> getAppointmentsByCustomerId(@retrofit2.http.Path("customerId") int customerId);
@retrofit2.http.POST("appointments")
Call<ApiResult<Appointment>> createAppointment(@retrofit2.http.Body AppointmentCreateRequest request);
@retrofit2.http.PUT("appointments/{id}/cancel")
Call<ApiResult<Appointment>> cancelAppointment(@retrofit2.http.Path("id") int id);
}

View File

@@ -0,0 +1,44 @@
package com.carmaintenance;
public class Appointment {
private Integer appointmentId;
private Integer customerId;
private Integer vehicleId;
private String serviceType;
private String appointmentTime;
private String contactPhone;
private String description;
private String status;
public Integer getAppointmentId() {
return appointmentId;
}
public Integer getCustomerId() {
return customerId;
}
public Integer getVehicleId() {
return vehicleId;
}
public String getServiceType() {
return serviceType;
}
public String getAppointmentTime() {
return appointmentTime;
}
public String getContactPhone() {
return contactPhone;
}
public String getDescription() {
return description;
}
public String getStatus() {
return status;
}
}

View File

@@ -0,0 +1,79 @@
package com.carmaintenance;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class AppointmentAdapter extends RecyclerView.Adapter<AppointmentAdapter.ViewHolder> {
public interface OnCancelListener {
void onCancel(Appointment appointment);
}
private final List<Appointment> items = new ArrayList<>();
private final Map<Integer, Vehicle> vehicleMap;
private final OnCancelListener cancelListener;
public AppointmentAdapter(Map<Integer, Vehicle> vehicleMap, OnCancelListener cancelListener) {
this.vehicleMap = vehicleMap;
this.cancelListener = cancelListener;
}
public void setItems(List<Appointment> appointments) {
items.clear();
if (appointments != null) {
items.addAll(appointments);
}
notifyDataSetChanged();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_appointment, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Appointment appointment = items.get(position);
Vehicle vehicle = vehicleMap.get(appointment.getVehicleId());
String plate = vehicle != null ? vehicle.getLicensePlate() : "-";
holder.summary.setText(ServiceTypeMapper.toDisplay(appointment.getServiceType()) + " - " + plate);
holder.detail.setText(holder.itemView.getContext().getString(R.string.appointment_time_label, appointment.getAppointmentTime()));
holder.status.setText(ServiceTypeMapper.statusDisplay(appointment.getStatus()));
boolean canCancel = "pending".equalsIgnoreCase(appointment.getStatus());
holder.cancelButton.setVisibility(canCancel ? View.VISIBLE : View.GONE);
holder.cancelButton.setOnClickListener(v -> cancelListener.onCancel(appointment));
}
@Override
public int getItemCount() {
return items.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
final TextView summary;
final TextView detail;
final TextView status;
final Button cancelButton;
ViewHolder(@NonNull View itemView) {
super(itemView);
summary = itemView.findViewById(R.id.appointmentSummary);
detail = itemView.findViewById(R.id.appointmentDetail);
status = itemView.findViewById(R.id.appointmentStatus);
cancelButton = itemView.findViewById(R.id.cancelAppointmentButton);
}
}
}

View File

@@ -0,0 +1,20 @@
package com.carmaintenance;
public class AppointmentCreateRequest {
private final Integer customerId;
private final Integer vehicleId;
private final String serviceType;
private final String appointmentTime;
private final String contactPhone;
private final String description;
public AppointmentCreateRequest(Integer customerId, Integer vehicleId, String serviceType,
String appointmentTime, String contactPhone, String description) {
this.customerId = customerId;
this.vehicleId = vehicleId;
this.serviceType = serviceType;
this.appointmentTime = appointmentTime;
this.contactPhone = contactPhone;
this.description = description;
}
}

View File

@@ -0,0 +1,24 @@
package com.carmaintenance;
public class Customer {
private Integer customerId;
private Integer userId;
private String realName;
private String phone;
public Integer getCustomerId() {
return customerId;
}
public Integer getUserId() {
return userId;
}
public String getRealName() {
return realName;
}
public String getPhone() {
return phone;
}
}

View File

@@ -0,0 +1,306 @@
package com.carmaintenance;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.ArrayAdapter;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TimePicker;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.RecyclerView;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class CustomerAppointmentActivity extends AppCompatActivity {
private Spinner vehicleSpinner;
private Spinner serviceTypeSpinner;
private EditText appointmentTimeInput;
private EditText contactPhoneInput;
private EditText descriptionInput;
private ProgressBar progressBar;
private AppointmentAdapter adapter;
private final Calendar appointmentCalendar = Calendar.getInstance();
private final SimpleDateFormat appointmentFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.CHINA);
private final List<Vehicle> vehicles = new ArrayList<>();
private final Map<Integer, Vehicle> vehicleMap = new HashMap<>();
private Integer customerId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_customer_appointment);
vehicleSpinner = findViewById(R.id.vehicleSpinner);
serviceTypeSpinner = findViewById(R.id.serviceTypeSpinner);
appointmentTimeInput = findViewById(R.id.appointmentTimeInput);
contactPhoneInput = findViewById(R.id.contactPhoneInput);
descriptionInput = findViewById(R.id.descriptionInput);
progressBar = findViewById(R.id.appointmentProgress);
Button submitButton = findViewById(R.id.submitAppointmentButton);
RecyclerView recyclerView = findViewById(R.id.appointmentRecycler);
adapter = new AppointmentAdapter(vehicleMap, this::cancelAppointment);
recyclerView.setAdapter(adapter);
serviceTypeSpinner.setAdapter(new ArrayAdapter<>(
this,
android.R.layout.simple_spinner_dropdown_item,
ServiceTypeMapperOptions.getServiceTypes()
));
appointmentTimeInput.setOnClickListener(v -> openDatePicker());
appointmentTimeInput.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
openDatePicker();
}
});
submitButton.setOnClickListener(v -> submitAppointment());
loadCustomerContext();
}
private void loadCustomerContext() {
setLoading(true);
int userId = new UserSession(this).getUserId();
if (userId == 0) {
toast(R.string.error_missing_user);
setLoading(false);
return;
}
ApiClient.getService().getCustomerByUserId(userId).enqueue(new Callback<ApiResult<Customer>>() {
@Override
public void onResponse(Call<ApiResult<Customer>> call, Response<ApiResult<Customer>> response) {
if (!response.isSuccessful() || response.body() == null || response.body().getData() == null) {
toast(R.string.error_load_customer);
setLoading(false);
return;
}
customerId = response.body().getData().getCustomerId();
loadVehiclesAndAppointments();
}
@Override
public void onFailure(Call<ApiResult<Customer>> call, Throwable t) {
toast(getString(R.string.error_network) + t.getMessage());
setLoading(false);
}
});
}
private void loadVehiclesAndAppointments() {
if (customerId == null) {
setLoading(false);
return;
}
ApiClient.getService().getVehiclesByCustomerId(customerId).enqueue(new Callback<ApiResult<List<Vehicle>>>() {
@Override
public void onResponse(Call<ApiResult<List<Vehicle>>> call, Response<ApiResult<List<Vehicle>>> response) {
vehicles.clear();
vehicleMap.clear();
if (response.isSuccessful() && response.body() != null && response.body().getData() != null) {
vehicles.addAll(response.body().getData());
}
for (Vehicle v : vehicles) {
if (v.getVehicleId() != null) {
vehicleMap.put(v.getVehicleId(), v);
}
}
List<String> labels = new ArrayList<>();
labels.add(getString(R.string.select_vehicle));
for (Vehicle v : vehicles) {
labels.add(v.getLicensePlate() + " - " + v.getBrand() + " " + v.getModel());
}
vehicleSpinner.setAdapter(new ArrayAdapter<>(
CustomerAppointmentActivity.this,
android.R.layout.simple_spinner_dropdown_item,
labels
));
loadAppointments();
}
@Override
public void onFailure(Call<ApiResult<List<Vehicle>>> call, Throwable t) {
toast(getString(R.string.error_network) + t.getMessage());
setLoading(false);
}
});
}
private void loadAppointments() {
if (customerId == null) {
setLoading(false);
return;
}
ApiClient.getService().getAppointmentsByCustomerId(customerId).enqueue(new Callback<ApiResult<List<Appointment>>>() {
@Override
public void onResponse(Call<ApiResult<List<Appointment>>> call, Response<ApiResult<List<Appointment>>> response) {
setLoading(false);
if (!response.isSuccessful() || response.body() == null) {
toast(R.string.error_load_appointments);
return;
}
adapter.setItems(response.body().getData());
}
@Override
public void onFailure(Call<ApiResult<List<Appointment>>> call, Throwable t) {
setLoading(false);
toast(getString(R.string.error_network) + t.getMessage());
}
});
}
private void submitAppointment() {
if (customerId == null) {
toast(R.string.error_load_customer);
return;
}
if (vehicleSpinner.getSelectedItemPosition() == 0) {
toast(R.string.error_select_vehicle);
return;
}
String appointmentTime = appointmentTimeInput.getText() != null ? appointmentTimeInput.getText().toString().trim() : "";
String contactPhone = contactPhoneInput.getText() != null ? contactPhoneInput.getText().toString().trim() : "";
String description = descriptionInput.getText() != null ? descriptionInput.getText().toString().trim() : "";
if (TextUtils.isEmpty(appointmentTime)) {
toast(R.string.error_select_time);
return;
}
if (TextUtils.isEmpty(contactPhone)) {
toast(R.string.error_contact_phone);
return;
}
Vehicle selectedVehicle = vehicles.get(vehicleSpinner.getSelectedItemPosition() - 1);
String serviceType = ServiceTypeMapperOptions.getServiceTypeValue(serviceTypeSpinner.getSelectedItemPosition());
setLoading(true);
AppointmentCreateRequest request = new AppointmentCreateRequest(
customerId,
selectedVehicle.getVehicleId(),
serviceType,
appointmentTime,
contactPhone,
description
);
ApiClient.getService().createAppointment(request).enqueue(new Callback<ApiResult<Appointment>>() {
@Override
public void onResponse(Call<ApiResult<Appointment>> call, Response<ApiResult<Appointment>> response) {
setLoading(false);
if (!response.isSuccessful() || response.body() == null || response.body().getCode() != 200) {
toast(R.string.error_submit_appointment);
return;
}
appointmentTimeInput.setText("");
contactPhoneInput.setText("");
descriptionInput.setText("");
toast(R.string.success_submit_appointment);
loadAppointments();
}
@Override
public void onFailure(Call<ApiResult<Appointment>> call, Throwable t) {
setLoading(false);
toast(getString(R.string.error_network) + t.getMessage());
}
});
}
private void cancelAppointment(Appointment appointment) {
if (appointment.getAppointmentId() == null) {
return;
}
setLoading(true);
ApiClient.getService().cancelAppointment(appointment.getAppointmentId()).enqueue(new Callback<ApiResult<Appointment>>() {
@Override
public void onResponse(Call<ApiResult<Appointment>> call, Response<ApiResult<Appointment>> response) {
setLoading(false);
if (!response.isSuccessful() || response.body() == null || response.body().getCode() != 200) {
toast(R.string.error_cancel_appointment);
return;
}
toast(R.string.success_cancel_appointment);
loadAppointments();
}
@Override
public void onFailure(Call<ApiResult<Appointment>> call, Throwable t) {
setLoading(false);
toast(getString(R.string.error_network) + t.getMessage());
}
});
}
private void setLoading(boolean loading) {
progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
private void openDatePicker() {
DatePickerDialog dialog = new DatePickerDialog(
this,
(DatePicker view, int year, int month, int dayOfMonth) -> {
appointmentCalendar.set(Calendar.YEAR, year);
appointmentCalendar.set(Calendar.MONTH, month);
appointmentCalendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
openTimePicker();
},
appointmentCalendar.get(Calendar.YEAR),
appointmentCalendar.get(Calendar.MONTH),
appointmentCalendar.get(Calendar.DAY_OF_MONTH)
);
dialog.show();
}
private void openTimePicker() {
TimePickerDialog dialog = new TimePickerDialog(
this,
(TimePicker view, int hourOfDay, int minute) -> {
appointmentCalendar.set(Calendar.HOUR_OF_DAY, hourOfDay);
appointmentCalendar.set(Calendar.MINUTE, minute);
appointmentCalendar.set(Calendar.SECOND, 0);
appointmentTimeInput.setText(appointmentFormat.format(appointmentCalendar.getTime()));
},
appointmentCalendar.get(Calendar.HOUR_OF_DAY),
appointmentCalendar.get(Calendar.MINUTE),
true
);
dialog.show();
}
private void toast(int resId) {
Toast.makeText(this, resId, Toast.LENGTH_SHORT).show();
}
private void toast(String text) {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
}
}

View File

@@ -41,7 +41,7 @@ public class LoginActivity extends AppCompatActivity {
String password = textOf(passwordInput);
if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) {
showError("Username and password are required.");
showError(getString(R.string.error_required));
return;
}
@@ -54,13 +54,13 @@ public class LoginActivity extends AppCompatActivity {
setLoading(false);
if (!response.isSuccessful() || response.body() == null) {
showError("Login failed. Please try again.");
showError(getString(R.string.error_login_failed));
return;
}
ApiResult<LoginResponse> result = response.body();
if (result.getCode() != 200 || result.getData() == null) {
showError(result.getMessage() != null ? result.getMessage() : "Login failed.");
showError(result.getMessage() != null ? result.getMessage() : getString(R.string.error_login_failed));
return;
}
@@ -69,6 +69,10 @@ public class LoginActivity extends AppCompatActivity {
String role = data.getUserInfo() != null ? data.getUserInfo().getRole() : "unknown";
String displayName = data.getUserInfo() != null ? data.getUserInfo().getUsername() : username;
int userId = data.getUserInfo() != null && data.getUserInfo().getUserId() != null
? data.getUserInfo().getUserId()
: 0;
new UserSession(LoginActivity.this).save(userId, displayName, role);
Intent intent = new Intent(LoginActivity.this, RoleHomeActivity.class);
intent.putExtra("role", role);
@@ -80,7 +84,7 @@ public class LoginActivity extends AppCompatActivity {
@Override
public void onFailure(Call<ApiResult<LoginResponse>> call, Throwable t) {
setLoading(false);
showError("Network error: " + t.getMessage());
showError(getString(R.string.error_network) + t.getMessage());
}
});
}

View File

@@ -28,24 +28,36 @@ public class RoleHomeActivity extends AppCompatActivity {
username = "user";
}
userInfo.setText("Welcome " + username + " (" + role + ")");
userInfo.setText(getString(R.string.welcome_format, username, role));
if ("admin".equalsIgnoreCase(role)) {
primaryAction.setText("Manage Users");
secondaryAction.setText("Manage Orders");
primaryAction.setText(R.string.role_admin_primary);
secondaryAction.setText(R.string.role_admin_secondary);
primaryAction.setOnClickListener(v -> {});
secondaryAction.setOnClickListener(v -> {});
} else if ("staff".equalsIgnoreCase(role)) {
primaryAction.setText("Search Vehicles");
secondaryAction.setText("Parts Lookup");
primaryAction.setText(R.string.role_staff_primary);
secondaryAction.setText(R.string.role_staff_secondary);
primaryAction.setOnClickListener(v -> {});
secondaryAction.setOnClickListener(v -> {});
} else {
primaryAction.setText("My Vehicles");
secondaryAction.setText("My Appointments");
primaryAction.setText(R.string.role_customer_primary);
secondaryAction.setText(R.string.role_customer_secondary);
primaryAction.setOnClickListener(v -> openCustomerAppointment());
secondaryAction.setOnClickListener(v -> openCustomerAppointment());
}
logoutButton.setOnClickListener(v -> {
new TokenStore(RoleHomeActivity.this).clear();
new UserSession(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);
});
}
private void openCustomerAppointment() {
Intent intent = new Intent(RoleHomeActivity.this, CustomerAppointmentActivity.class);
startActivity(intent);
}
}

View File

@@ -0,0 +1,39 @@
package com.carmaintenance;
public class ServiceTypeMapper {
public static String toDisplay(String serviceType) {
if (serviceType == null) {
return "未知服务";
}
switch (serviceType) {
case "maintenance":
return "保养";
case "repair":
return "维修";
case "beauty":
return "美容";
case "insurance":
return "保险";
default:
return serviceType;
}
}
public static String statusDisplay(String status) {
if (status == null) {
return "状态:未知";
}
switch (status) {
case "pending":
return "状态:待确认";
case "confirmed":
return "状态:已确认";
case "completed":
return "状态:已完成";
case "cancelled":
return "状态:已取消";
default:
return "状态:" + status;
}
}
}

View File

@@ -0,0 +1,30 @@
package com.carmaintenance;
import java.util.ArrayList;
import java.util.List;
public class ServiceTypeMapperOptions {
public static List<String> getServiceTypes() {
List<String> list = new ArrayList<>();
list.add("保养");
list.add("维修");
list.add("美容");
list.add("保险");
return list;
}
public static String getServiceTypeValue(int position) {
switch (position) {
case 0:
return "maintenance";
case 1:
return "repair";
case 2:
return "beauty";
case 3:
return "insurance";
default:
return "maintenance";
}
}
}

View File

@@ -0,0 +1,45 @@
package com.carmaintenance;
import android.content.Context;
import android.content.SharedPreferences;
public class UserSession {
private static final String PREFS_NAME = "auth_prefs";
private static final String KEY_USER_ID = "user_id";
private static final String KEY_USERNAME = "username";
private static final String KEY_ROLE = "role";
private final SharedPreferences preferences;
public UserSession(Context context) {
this.preferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
}
public void save(int userId, String username, String role) {
preferences.edit()
.putInt(KEY_USER_ID, userId)
.putString(KEY_USERNAME, username)
.putString(KEY_ROLE, role)
.apply();
}
public int getUserId() {
return preferences.getInt(KEY_USER_ID, 0);
}
public String getUsername() {
return preferences.getString(KEY_USERNAME, "");
}
public String getRole() {
return preferences.getString(KEY_ROLE, "");
}
public void clear() {
preferences.edit()
.remove(KEY_USER_ID)
.remove(KEY_USERNAME)
.remove(KEY_ROLE)
.apply();
}
}

View File

@@ -0,0 +1,24 @@
package com.carmaintenance;
public class Vehicle {
private Integer vehicleId;
private String licensePlate;
private String brand;
private String model;
public Integer getVehicleId() {
return vehicleId;
}
public String getLicensePlate() {
return licensePlate;
}
public String getBrand() {
return brand;
}
public String getModel() {
return model;
}
}

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/appointment_title"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/appointment_form_title"
android:textStyle="bold" />
<Spinner
android:id="@+id/vehicleSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
<Spinner
android:id="@+id/serviceTypeSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
<EditText
android:id="@+id/appointmentTimeInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/appointment_time_hint"
android:inputType="none"
android:focusable="false"
android:clickable="true" />
<EditText
android:id="@+id/contactPhoneInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/contact_phone"
android:inputType="phone" />
<EditText
android:id="@+id/descriptionInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/appointment_description"
android:minLines="2"
android:inputType="textMultiLine" />
<Button
android:id="@+id/submitAppointmentButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/submit_appointment" />
<ProgressBar
android:id="@+id/appointmentProgress"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="12dp"
android:visibility="gone" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/appointment_list_title"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/appointmentRecycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
</ScrollView>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?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"
@@ -10,7 +10,7 @@
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Car Maintenance"
android:text="@string/login_title"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?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"
@@ -10,7 +10,7 @@
android:id="@+id/welcomeTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Role Home"
android:text="@string/welcome_title"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
@@ -32,7 +32,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Primary Action"
android:text="@string/primary_action"
app:layout_constraintTop_toBottomOf="@id/userInfo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
@@ -42,7 +42,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Secondary Action"
android:text="@string/secondary_action"
app:layout_constraintTop_toBottomOf="@id/primaryAction"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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="wrap_content"
android:layout_marginBottom="8dp"
app:cardElevation="2dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/appointmentSummary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold" />
<TextView
android:id="@+id/appointmentDetail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp" />
<TextView
android:id="@+id/appointmentStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp" />
<Button
android:id="@+id/cancelAppointmentButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/cancel_appointment" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -1,8 +1,42 @@
<resources>
<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>
<string name="app_name">车管家</string>
<string name="login">登录</string>
<string name="username">用户名</string>
<string name="password">密码</string>
<string name="role_home">角色主页</string>
<string name="logout">退出登录</string>
<string name="welcome_title">角色主页</string>
<string name="login_title">车管家</string>
<string name="primary_action">主要功能</string>
<string name="secondary_action">次要功能</string>
<string name="error_required">请输入用户名和密码</string>
<string name="error_login_failed">登录失败,请重试</string>
<string name="error_network">网络错误:</string>
<string name="role_admin_primary">用户管理</string>
<string name="role_admin_secondary">工单管理</string>
<string name="role_staff_primary">车辆查询</string>
<string name="role_staff_secondary">配件查询</string>
<string name="role_customer_primary">我的车辆</string>
<string name="role_customer_secondary">在线预约</string>
<string name="welcome_format">欢迎 %1$s%2$s</string>
<string name="appointment_title">在线预约</string>
<string name="appointment_form_title">提交预约</string>
<string name="appointment_list_title">我的预约</string>
<string name="appointment_time_hint">请选择预约时间</string>
<string name="contact_phone">联系电话</string>
<string name="appointment_description">预约说明</string>
<string name="submit_appointment">提交预约</string>
<string name="cancel_appointment">取消预约</string>
<string name="select_vehicle">请选择车辆</string>
<string name="appointment_time_label">预约时间:%1$s</string>
<string name="error_missing_user">用户信息缺失,请重新登录</string>
<string name="error_load_customer">获取客户信息失败</string>
<string name="error_load_appointments">加载预约失败</string>
<string name="error_select_vehicle">请选择车辆</string>
<string name="error_select_time">请选择预约时间</string>
<string name="error_contact_phone">请输入联系电话</string>
<string name="error_submit_appointment">预约提交失败</string>
<string name="success_submit_appointment">预约成功</string>
<string name="error_cancel_appointment">取消预约失败</string>
<string name="success_cancel_appointment">预约已取消</string>
</resources>