Initial commit: Car Maintenance Management System

Author: Yang Lu

School: Liaoning Institute of Science and Technology

Major: Computer Science and Technology

Class: BZ246

Tech Stack:

- Backend: Spring Boot 2.7.18 + JPA + MySQL

- Frontend: HTML5 + CSS3 + JavaScript

Features:

- User Management (Admin/Staff/Customer roles)

- Vehicle Archive Management

- Service Order Management

- Parts Inventory Management

- Online Appointment Service

- Data Statistics and Analysis

Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
wangziqi
2026-01-07 14:28:50 +08:00
commit cfae122685
45 changed files with 5447 additions and 0 deletions

View File

@@ -0,0 +1,300 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理员仪表板 - 车管家4S店车辆维保管理系统</title>
<link rel="stylesheet" href="../css/common.css">
<link rel="stylesheet" href="../css/dashboard.css">
</head>
<body>
<div class="dashboard-container">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<h2>车管家系统</h2>
<p>管理员控制台</p>
</div>
<div class="sidebar-menu">
<div class="menu-item active" onclick="showSection('overview')">
<span class="menu-icon">📊</span>
<span>系统概览</span>
</div>
<div class="menu-item" onclick="showSection('users')">
<span class="menu-icon">👥</span>
<span>用户管理</span>
</div>
<div class="menu-item" onclick="showSection('vehicles')">
<span class="menu-icon">🚗</span>
<span>车辆管理</span>
</div>
<div class="menu-item" onclick="showSection('orders')">
<span class="menu-icon">📋</span>
<span>工单管理</span>
</div>
<div class="menu-item" onclick="showSection('parts')">
<span class="menu-icon">🔧</span>
<span>配件管理</span>
</div>
<div class="menu-item" onclick="showSection('appointments')">
<span class="menu-icon">📅</span>
<span>预约管理</span>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="top-nav">
<div class="top-nav-left">
<h1>管理员仪表板</h1>
</div>
<div class="top-nav-right">
<div class="user-info">
<div class="user-avatar" id="userAvatar">A</div>
<span class="user-name" id="userName">管理员</span>
</div>
<button class="btn-logout" onclick="utils.logout()">退出登录</button>
</div>
</div>
<!-- 系统概览 -->
<div id="overview-section" class="section">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon blue">👥</div>
<div class="stat-info">
<h3 id="totalUsers">0</h3>
<p>用户总数</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon green">🚗</div>
<div class="stat-info">
<h3 id="totalVehicles">0</h3>
<p>车辆总数</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon orange">📋</div>
<div class="stat-info">
<h3 id="totalOrders">0</h3>
<p>工单总数</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon red">🔧</div>
<div class="stat-info">
<h3 id="lowStockParts">0</h3>
<p>库存预警</p>
</div>
</div>
</div>
<div class="content-card">
<div class="content-header">
<h2>最近工单</h2>
</div>
<div class="content-body">
<table class="table" id="recentOrdersTable">
<thead>
<tr>
<th>工单编号</th>
<th>服务类型</th>
<th>车牌号</th>
<th>状态</th>
<th>创建时间</th>
</tr>
</thead>
<tbody id="recentOrdersBody">
<tr><td colspan="5" class="empty-state">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 用户管理 -->
<div id="users-section" class="section" style="display: none;">
<div class="toolbar">
<div class="toolbar-left">
<button class="btn btn-primary" onclick="showAddUserModal()">添加用户</button>
</div>
<div class="search-box">
<input type="text" id="searchUser" placeholder="搜索用户..." onkeyup="searchUsers()">
<button>🔍</button>
</div>
</div>
<div class="content-card">
<div class="content-body">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>真实姓名</th>
<th>手机号</th>
<th>角色</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="usersTableBody">
<tr><td colspan="7" class="empty-state">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 车辆管理 -->
<div id="vehicles-section" class="section" style="display: none;">
<div class="toolbar">
<div class="toolbar-left">
<button class="btn btn-primary" onclick="showAddVehicleModal()">添加车辆</button>
</div>
<div class="search-box">
<input type="text" id="searchVehicle" placeholder="搜索车辆..." onkeyup="searchVehicles()">
<button>🔍</button>
</div>
</div>
<div class="content-card">
<div class="content-body">
<table class="table">
<thead>
<tr>
<th>车牌号</th>
<th>品牌型号</th>
<th>颜色</th>
<th>里程数</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="vehiclesTableBody">
<tr><td colspan="6" class="empty-state">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 工单管理 -->
<div id="orders-section" class="section" style="display: none;">
<div class="toolbar">
<div class="toolbar-left">
<button class="btn btn-primary" onclick="showAddOrderModal()">创建工单</button>
<select id="orderStatusFilter" onchange="filterOrders()">
<option value="">全部状态</option>
<option value="pending">待处理</option>
<option value="appointed">已预约</option>
<option value="in_progress">进行中</option>
<option value="completed">已完成</option>
<option value="cancelled">已取消</option>
</select>
</div>
</div>
<div class="content-card">
<div class="content-body">
<table class="table">
<thead>
<tr>
<th>工单编号</th>
<th>服务类型</th>
<th>车牌号</th>
<th>总费用</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="ordersTableBody">
<tr><td colspan="7" class="empty-state">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 配件管理 -->
<div id="parts-section" class="section" style="display: none;">
<div class="toolbar">
<div class="toolbar-left">
<button class="btn btn-primary" onclick="showAddPartModal()">添加配件</button>
<button class="btn btn-warning" onclick="showLowStockParts()">库存预警</button>
</div>
<div class="search-box">
<input type="text" id="searchPart" placeholder="搜索配件..." onkeyup="searchParts()">
<button>🔍</button>
</div>
</div>
<div class="content-card">
<div class="content-body">
<table class="table">
<thead>
<tr>
<th>配件编号</th>
<th>配件名称</th>
<th>类别</th>
<th>库存数量</th>
<th>单价</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="partsTableBody">
<tr><td colspan="7" class="empty-state">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 预约管理 -->
<div id="appointments-section" class="section" style="display: none;">
<div class="toolbar">
<div class="toolbar-left">
<select id="appointmentStatusFilter" onchange="filterAppointments()">
<option value="">全部状态</option>
<option value="pending">待确认</option>
<option value="confirmed">已确认</option>
<option value="completed">已完成</option>
<option value="cancelled">已取消</option>
</select>
</div>
</div>
<div class="content-card">
<div class="content-body">
<table class="table">
<thead>
<tr>
<th>预约ID</th>
<th>服务类型</th>
<th>车牌号</th>
<th>预约时间</th>
<th>联系电话</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="appointmentsTableBody">
<tr><td colspan="7" class="empty-state">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script src="../js/config.js"></script>
<script src="../js/api.js"></script>
<script src="../js/admin-dashboard.js"></script>
</body>
</html>

377
frontend/css/common.css Normal file
View File

@@ -0,0 +1,377 @@
/* 通用样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
font-size: 14px;
color: #333;
background-color: #f5f5f5;
line-height: 1.6;
}
/* 容器 */
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* 卡片样式 */
.card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 20px;
margin-bottom: 20px;
}
.card-header {
border-bottom: 2px solid #1890ff;
padding-bottom: 10px;
margin-bottom: 20px;
}
.card-header h2 {
font-size: 18px;
font-weight: 600;
color: #1890ff;
}
.card-body {
padding: 10px 0;
}
/* 按钮样式 */
.btn {
display: inline-block;
padding: 8px 16px;
font-size: 14px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
text-align: center;
}
.btn-primary {
background-color: #1890ff;
color: #fff;
}
.btn-primary:hover {
background-color: #40a9ff;
}
.btn-success {
background-color: #52c41a;
color: #fff;
}
.btn-success:hover {
background-color: #73d13d;
}
.btn-danger {
background-color: #f5222d;
color: #fff;
}
.btn-danger:hover {
background-color: #ff4d4f;
}
.btn-warning {
background-color: #faad14;
color: #fff;
}
.btn-warning:hover {
background-color: #ffc53d;
}
.btn-info {
background-color: #13c2c2;
color: #fff;
}
.btn-info:hover {
background-color: #36cfc9;
}
.btn-secondary {
background-color: #d9d9d9;
color: #333;
}
.btn-secondary:hover {
background-color: #bfbfbf;
}
/* 表单样式 */
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #555;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #1890ff;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.form-row {
display: flex;
gap: 15px;
}
.form-row .form-group {
flex: 1;
}
/* 表格样式 */
.table {
width: 100%;
border-collapse: collapse;
background: #fff;
}
.table th,
.table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e8e8e8;
}
.table th {
background-color: #fafafa;
font-weight: 600;
color: #333;
}
.table tr:hover {
background-color: #f5f5f5;
}
.table-actions {
display: flex;
gap: 8px;
}
.table-actions button {
padding: 4px 12px;
font-size: 12px;
}
/* 状态标签 */
.badge {
display: inline-block;
padding: 2px 8px;
font-size: 12px;
border-radius: 4px;
font-weight: 500;
}
.badge-success {
background-color: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
.badge-warning {
background-color: #fffbe6;
color: #faad14;
border: 1px solid #ffe58f;
}
.badge-danger {
background-color: #fff1f0;
color: #f5222d;
border: 1px solid #ffa39e;
}
.badge-info {
background-color: #e6f7ff;
color: #1890ff;
border: 1px solid #91d5ff;
}
.badge-secondary {
background-color: #fafafa;
color: #595959;
border: 1px solid #d9d9d9;
}
/* 模态框 */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
overflow-y: auto;
}
.modal.active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: #fff;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.modal-header {
padding: 16px 20px;
border-bottom: 1px solid #e8e8e8;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
font-size: 16px;
font-weight: 600;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
}
.modal-close:hover {
color: #333;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 12px 20px;
border-top: 1px solid #e8e8e8;
text-align: right;
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 警告框 */
.alert {
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 15px;
}
.alert-success {
background-color: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
.alert-error {
background-color: #fff1f0;
color: #f5222d;
border: 1px solid #ffa39e;
}
.alert-warning {
background-color: #fffbe6;
color: #faad14;
border: 1px solid #ffe58f;
}
.alert-info {
background-color: #e6f7ff;
color: #1890ff;
border: 1px solid #91d5ff;
}
/* 加载动画 */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
.empty-state img {
width: 120px;
margin-bottom: 20px;
opacity: 0.5;
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.form-row {
flex-direction: column;
}
.table {
font-size: 12px;
}
.table th,
.table td {
padding: 8px;
}
}

286
frontend/css/dashboard.css Normal file
View File

@@ -0,0 +1,286 @@
/* 仪表板通用样式 */
.dashboard-container {
display: flex;
min-height: 100vh;
background-color: #f0f2f5;
}
/* 侧边栏 */
.sidebar {
width: 250px;
background: linear-gradient(180deg, #1890ff 0%, #0050b3 100%);
color: #fff;
position: fixed;
height: 100vh;
overflow-y: auto;
box-shadow: 2px 0 8px rgba(0,0,0,0.1);
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-header h2 {
font-size: 18px;
margin-bottom: 5px;
}
.sidebar-header p {
font-size: 12px;
opacity: 0.8;
}
.sidebar-menu {
padding: 20px 0;
}
.menu-item {
padding: 12px 20px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 10px;
color: rgba(255, 255, 255, 0.85);
text-decoration: none;
}
.menu-item:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
}
.menu-item.active {
background-color: rgba(255, 255, 255, 0.2);
color: #fff;
border-left: 3px solid #fff;
}
.menu-icon {
font-size: 18px;
}
/* 主内容区 */
.main-content {
flex: 1;
margin-left: 250px;
padding: 20px;
}
/* 顶部导航 */
.top-nav {
background: #fff;
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.top-nav-left h1 {
font-size: 20px;
color: #333;
}
.top-nav-right {
display: flex;
align-items: center;
gap: 20px;
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
}
.user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
}
.user-name {
font-size: 14px;
color: #333;
}
.btn-logout {
padding: 6px 16px;
background-color: #ff4d4f;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.btn-logout:hover {
background-color: #ff7875;
}
/* 统计卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
display: flex;
align-items: center;
gap: 15px;
transition: transform 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #fff;
}
.stat-icon.blue {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.stat-icon.green {
background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
}
.stat-icon.orange {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
.stat-icon.red {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.stat-info h3 {
font-size: 28px;
font-weight: 600;
margin-bottom: 5px;
color: #333;
}
.stat-info p {
font-size: 13px;
color: #666;
}
/* 工具栏 */
.toolbar {
background: #fff;
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.toolbar-left {
display: flex;
gap: 10px;
}
.search-box {
position: relative;
}
.search-box input {
padding: 8px 35px 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
width: 250px;
}
.search-box button {
position: absolute;
right: 0;
top: 0;
padding: 8px 12px;
background: none;
border: none;
cursor: pointer;
color: #666;
}
/* 内容卡片 */
.content-card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
}
.content-header {
padding: 15px 20px;
border-bottom: 1px solid #e8e8e8;
display: flex;
justify-content: space-between;
align-items: center;
}
.content-header h2 {
font-size: 16px;
font-weight: 600;
}
.content-body {
padding: 20px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.sidebar {
width: 60px;
}
.sidebar-header h2,
.sidebar-header p,
.menu-item span {
display: none;
}
.main-content {
margin-left: 60px;
}
.stats-grid {
grid-template-columns: 1fr;
}
.toolbar {
flex-direction: column;
gap: 10px;
}
.search-box input {
width: 100%;
}
}

152
frontend/css/login.css Normal file
View File

@@ -0,0 +1,152 @@
/* 登录页面样式 */
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
width: 100%;
max-width: 450px;
padding: 20px;
}
.login-box {
background: #fff;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.login-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
padding: 30px 20px;
text-align: center;
}
.login-header h1 {
font-size: 24px;
margin-bottom: 8px;
font-weight: 600;
}
.login-header p {
font-size: 12px;
opacity: 0.9;
font-weight: 300;
}
.login-form {
padding: 30px;
}
.login-form .form-group {
margin-bottom: 20px;
}
.login-form .form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.login-form input,
.login-form select {
width: 100%;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.login-form input:focus,
.login-form select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn-login {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
margin-top: 10px;
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
}
.btn-login:active {
transform: translateY(0);
}
.login-footer {
margin-top: 20px;
text-align: center;
display: flex;
justify-content: space-between;
}
.login-footer a {
color: #667eea;
text-decoration: none;
font-size: 13px;
transition: color 0.3s;
}
.login-footer a:hover {
color: #764ba2;
text-decoration: underline;
}
.demo-accounts {
background: #f8f9fa;
padding: 15px 30px;
border-top: 1px solid #e9ecef;
}
.demo-accounts p {
font-size: 12px;
color: #666;
margin-bottom: 8px;
font-weight: 600;
}
.demo-accounts ul {
list-style: none;
}
.demo-accounts li {
font-size: 11px;
color: #888;
padding: 3px 0;
}
/* 响应式设计 */
@media (max-width: 480px) {
.login-container {
padding: 10px;
}
.login-header h1 {
font-size: 20px;
}
.login-form {
padding: 20px;
}
}

View File

@@ -0,0 +1,361 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客户中心 - 车管家4S店车辆维保管理系统</title>
<link rel="stylesheet" href="../css/common.css">
<link rel="stylesheet" href="../css/dashboard.css">
</head>
<body>
<div class="dashboard-container">
<div class="sidebar">
<div class="sidebar-header">
<h2>车管家系统</h2>
<p>客户中心</p>
</div>
<div class="sidebar-menu">
<div class="menu-item active" onclick="showSection('myvehicles')">
<span class="menu-icon">🚗</span>
<span>我的车辆</span>
</div>
<div class="menu-item" onclick="showSection('myorders')">
<span class="menu-icon">📋</span>
<span>维保记录</span>
</div>
<div class="menu-item" onclick="showSection('appointments')">
<span class="menu-icon">📅</span>
<span>我的预约</span>
</div>
<div class="menu-item" onclick="showSection('newappointment')">
<span class="menu-icon"></span>
<span>在线预约</span>
</div>
</div>
</div>
<div class="main-content">
<div class="top-nav">
<div class="top-nav-left">
<h1>客户中心</h1>
</div>
<div class="top-nav-right">
<div class="user-info">
<div class="user-avatar" id="userAvatar">C</div>
<span class="user-name" id="userName">客户</span>
</div>
<button class="btn-logout" onclick="utils.logout()">退出登录</button>
</div>
</div>
<div id="myvehicles-section" class="section">
<div class="content-card">
<div class="content-header">
<h2>我的车辆</h2>
</div>
<div class="content-body">
<div id="vehiclesGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:20px;">
<p class="empty-state">加载中...</p>
</div>
</div>
</div>
</div>
<div id="myorders-section" class="section" style="display: none;">
<div class="content-card">
<div class="content-header">
<h2>维保记录</h2>
</div>
<div class="content-body">
<table class="table">
<thead>
<tr>
<th>工单编号</th>
<th>服务类型</th>
<th>车牌号</th>
<th>费用</th>
<th>状态</th>
<th>创建时间</th>
</tr>
</thead>
<tbody id="ordersBody">
<tr><td colspan="6" class="empty-state">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="appointments-section" class="section" style="display: none;">
<div class="content-card">
<div class="content-header">
<h2>我的预约</h2>
</div>
<div class="content-body">
<table class="table">
<thead>
<tr>
<th>服务类型</th>
<th>车牌号</th>
<th>预约时间</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="appointmentsBody">
<tr><td colspan="5" class="empty-state">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="newappointment-section" class="section" style="display: none;">
<div class="content-card">
<div class="content-header">
<h2>在线预约服务</h2>
</div>
<div class="content-body">
<form id="appointmentForm" style="max-width:600px;">
<div class="form-group">
<label>选择车辆</label>
<select id="vehicleSelect" required>
<option value="">请选择车辆</option>
</select>
</div>
<div class="form-group">
<label>服务类型</label>
<select id="serviceType" required>
<option value="maintenance">保养维护</option>
<option value="repair">维修服务</option>
<option value="beauty">美容服务</option>
<option value="insurance">保险代理</option>
</select>
</div>
<div class="form-group">
<label>预约时间</label>
<input type="datetime-local" id="appointmentTime" required>
</div>
<div class="form-group">
<label>联系电话</label>
<input type="tel" id="contactPhone" required>
</div>
<div class="form-group">
<label>预约说明</label>
<textarea id="description" placeholder="请描述您的需求"></textarea>
</div>
<button type="submit" class="btn btn-primary">提交预约</button>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="../js/config.js"></script>
<script src="../js/api.js"></script>
<script>
if (!utils.checkAuth() || !utils.hasRole('customer')) {
window.location.href = '../login.html';
}
let currentCustomerId = null;
window.addEventListener('DOMContentLoaded', async () => {
const user = utils.getCurrentUser();
if (user) {
document.getElementById('userName').textContent = user.realName;
document.getElementById('userAvatar').textContent = user.realName.charAt(0);
document.getElementById('contactPhone').value = user.phone;
await loadCustomerData(user.userId);
}
});
async function loadCustomerData(userId) {
try {
const usersRes = await api.get(API_ENDPOINTS.USERS);
const customers = usersRes.data?.filter(u => u.role === 'customer') || [];
const currentUser = customers.find(c => c.userId === userId);
if (currentUser) {
currentCustomerId = currentUser.userId;
loadVehicles();
loadOrders();
loadAppointments();
loadVehicleOptions();
}
} catch (error) {
console.error('加载数据失败:', error);
}
}
async function loadVehicles() {
try {
const response = await api.get(API_ENDPOINTS.VEHICLES);
if (response.code === 200 && response.data) {
const myVehicles = response.data.filter(v => v.customerId === currentCustomerId);
displayVehicles(myVehicles);
}
} catch (error) {
console.error('加载车辆失败:', error);
}
}
function displayVehicles(vehicles) {
const grid = document.getElementById('vehiclesGrid');
if (vehicles.length === 0) {
grid.innerHTML = '<p class="empty-state">暂无车辆</p>';
return;
}
grid.innerHTML = vehicles.map(v => `
<div class="card">
<h3 style="color:#1890ff;">${v.licensePlate}</h3>
<p><strong>品牌:</strong> ${v.brand} ${v.model}</p>
<p><strong>颜色:</strong> ${v.color || '-'}</p>
<p><strong>里程:</strong> ${v.mileage || 0} 公里</p>
<p><strong>上次保养:</strong> ${utils.formatDate(v.lastMaintenanceDate)}</p>
<p><strong>下次保养:</strong> ${utils.formatDate(v.nextMaintenanceDate)}</p>
<p><strong>状态:</strong> ${utils.getStatusBadge(v.status)}</p>
</div>
`).join('');
}
async function loadOrders() {
try {
const [ordersRes, vehiclesRes] = await Promise.all([
api.get(API_ENDPOINTS.ORDERS),
api.get(API_ENDPOINTS.VEHICLES)
]);
if (ordersRes.code === 200 && ordersRes.data) {
const myOrders = ordersRes.data.filter(o => o.customerId === currentCustomerId);
const vehicles = vehiclesRes.data || [];
const tbody = document.getElementById('ordersBody');
if (myOrders.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">暂无维保记录</td></tr>';
return;
}
tbody.innerHTML = myOrders.map(o => {
const vehicle = vehicles.find(v => v.vehicleId === o.vehicleId);
return `
<tr>
<td>${o.orderNo}</td>
<td>${utils.getServiceTypeText(o.serviceType)}</td>
<td>${vehicle ? vehicle.licensePlate : '-'}</td>
<td>¥${o.totalCost || 0}</td>
<td>${utils.getStatusBadge(o.status)}</td>
<td>${utils.formatDateTime(o.createTime)}</td>
</tr>
`;
}).join('');
}
} catch (error) {
console.error('加载工单失败:', error);
}
}
async function loadAppointments() {
try {
const [appointmentsRes, vehiclesRes] = await Promise.all([
api.get(API_ENDPOINTS.APPOINTMENTS),
api.get(API_ENDPOINTS.VEHICLES)
]);
if (appointmentsRes.code === 200 && appointmentsRes.data) {
const myAppointments = appointmentsRes.data.filter(a => a.customerId === currentCustomerId);
const vehicles = vehiclesRes.data || [];
const tbody = document.getElementById('appointmentsBody');
if (myAppointments.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">暂无预约记录</td></tr>';
return;
}
tbody.innerHTML = myAppointments.map(a => {
const vehicle = vehicles.find(v => v.vehicleId === a.vehicleId);
return `
<tr>
<td>${utils.getServiceTypeText(a.serviceType)}</td>
<td>${vehicle ? vehicle.licensePlate : '-'}</td>
<td>${utils.formatDateTime(a.appointmentTime)}</td>
<td>${utils.getStatusBadge(a.status)}</td>
<td>
${a.status === 'pending' ? `<button class="btn btn-danger" onclick="cancelAppointment(${a.appointmentId})">取消</button>` : '-'}
</td>
</tr>
`;
}).join('');
}
} catch (error) {
console.error('加载预约失败:', error);
}
}
async function loadVehicleOptions() {
try {
const response = await api.get(API_ENDPOINTS.VEHICLES);
if (response.code === 200 && response.data) {
const myVehicles = response.data.filter(v => v.customerId === currentCustomerId);
const select = document.getElementById('vehicleSelect');
select.innerHTML = '<option value="">请选择车辆</option>' +
myVehicles.map(v => `<option value="${v.vehicleId}">${v.licensePlate} - ${v.brand} ${v.model}</option>`).join('');
}
} catch (error) {
console.error('加载车辆选项失败:', error);
}
}
document.getElementById('appointmentForm').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
customerId: currentCustomerId,
vehicleId: parseInt(document.getElementById('vehicleSelect').value),
serviceType: document.getElementById('serviceType').value,
appointmentTime: document.getElementById('appointmentTime').value,
contactPhone: document.getElementById('contactPhone').value,
description: document.getElementById('description').value
};
try {
const response = await api.post(API_ENDPOINTS.APPOINTMENTS, data);
if (response.code === 200) {
utils.showSuccess('预约成功!');
document.getElementById('appointmentForm').reset();
loadAppointments();
} else {
utils.showError(response.message);
}
} catch (error) {
utils.showError('预约失败');
}
});
async function cancelAppointment(id) {
if (utils.confirm('确定要取消此预约吗?')) {
try {
const response = await api.put(API_ENDPOINTS.CANCEL_APPOINTMENT(id));
if (response.code === 200) {
utils.showSuccess('预约已取消');
loadAppointments();
} else {
utils.showError(response.message);
}
} catch (error) {
utils.showError('操作失败');
}
}
}
function showSection(name) {
document.querySelectorAll('.section').forEach(s => s.style.display = 'none');
document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('active'));
event.currentTarget.classList.add('active');
document.getElementById(name + '-section').style.display = 'block';
}
</script>
</body>
</html>

View File

@@ -0,0 +1,468 @@
// 管理员仪表板JavaScript
// 检查登录状态和权限
if (!utils.checkAuth() || !utils.hasRole('admin')) {
window.location.href = '../login.html';
}
// 页面加载时初始化
window.addEventListener('DOMContentLoaded', () => {
initializeDashboard();
loadOverviewData();
});
// 初始化仪表板
function initializeDashboard() {
const user = utils.getCurrentUser();
if (user) {
document.getElementById('userName').textContent = user.realName;
document.getElementById('userAvatar').textContent = user.realName.charAt(0);
}
}
// 切换显示区域
function showSection(sectionName) {
// 隐藏所有区域
const sections = document.querySelectorAll('.section');
sections.forEach(section => section.style.display = 'none');
// 移除所有菜单项的活动状态
const menuItems = document.querySelectorAll('.menu-item');
menuItems.forEach(item => item.classList.remove('active'));
// 显示选中的区域
document.getElementById(`${sectionName}-section`).style.display = 'block';
// 设置对应菜单项为活动状态
event.currentTarget.classList.add('active');
// 加载对应数据
switch(sectionName) {
case 'overview':
loadOverviewData();
break;
case 'users':
loadUsers();
break;
case 'vehicles':
loadVehicles();
break;
case 'orders':
loadOrders();
break;
case 'parts':
loadParts();
break;
case 'appointments':
loadAppointments();
break;
}
}
// 加载概览数据
async function loadOverviewData() {
try {
// 加载统计数据
const [usersRes, vehiclesRes, ordersRes, partsRes] = await Promise.all([
api.get(API_ENDPOINTS.USERS),
api.get(API_ENDPOINTS.VEHICLES),
api.get(API_ENDPOINTS.ORDERS),
api.get(API_ENDPOINTS.PARTS_LOW_STOCK)
]);
document.getElementById('totalUsers').textContent = usersRes.data?.length || 0;
document.getElementById('totalVehicles').textContent = vehiclesRes.data?.length || 0;
document.getElementById('totalOrders').textContent = ordersRes.data?.length || 0;
document.getElementById('lowStockParts').textContent = partsRes.data?.length || 0;
// 加载最近工单
if (ordersRes.data && ordersRes.data.length > 0) {
const recentOrders = ordersRes.data.slice(0, 5);
displayRecentOrders(recentOrders);
}
} catch (error) {
console.error('加载概览数据失败:', error);
}
}
// 显示最近工单
async function displayRecentOrders(orders) {
const tbody = document.getElementById('recentOrdersBody');
if (orders.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">暂无数据</td></tr>';
return;
}
// 获取所有车辆信息
const vehiclesRes = await api.get(API_ENDPOINTS.VEHICLES);
const vehicles = vehiclesRes.data || [];
tbody.innerHTML = orders.map(order => {
const vehicle = vehicles.find(v => v.vehicleId === order.vehicleId);
return `
<tr>
<td>${order.orderNo}</td>
<td>${utils.getServiceTypeText(order.serviceType)}</td>
<td>${vehicle ? vehicle.licensePlate : '-'}</td>
<td>${utils.getStatusBadge(order.status)}</td>
<td>${utils.formatDateTime(order.createTime)}</td>
</tr>
`;
}).join('');
}
// 加载用户列表
async function loadUsers() {
try {
const response = await api.get(API_ENDPOINTS.USERS);
if (response.code === 200 && response.data) {
displayUsers(response.data);
}
} catch (error) {
console.error('加载用户列表失败:', error);
utils.showError('加载用户列表失败');
}
}
// 显示用户列表
function displayUsers(users) {
const tbody = document.getElementById('usersTableBody');
if (users.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-state">暂无数据</td></tr>';
return;
}
tbody.innerHTML = users.map(user => `
<tr>
<td>${user.userId}</td>
<td>${user.username}</td>
<td>${user.realName}</td>
<td>${user.phone}</td>
<td>${utils.getRoleText(user.role)}</td>
<td>${user.status === 1 ? '<span class="badge badge-success">启用</span>' : '<span class="badge badge-secondary">禁用</span>'}</td>
<td class="table-actions">
<button class="btn btn-info" onclick="viewUser(${user.userId})">查看</button>
<button class="btn btn-warning" onclick="editUser(${user.userId})">编辑</button>
<button class="btn btn-danger" onclick="deleteUser(${user.userId})">删除</button>
</td>
</tr>
`).join('');
}
// 加载车辆列表
async function loadVehicles() {
try {
const response = await api.get(API_ENDPOINTS.VEHICLES);
if (response.code === 200 && response.data) {
displayVehicles(response.data);
}
} catch (error) {
console.error('加载车辆列表失败:', error);
utils.showError('加载车辆列表失败');
}
}
// 显示车辆列表
function displayVehicles(vehicles) {
const tbody = document.getElementById('vehiclesTableBody');
if (vehicles.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">暂无数据</td></tr>';
return;
}
tbody.innerHTML = vehicles.map(vehicle => `
<tr>
<td>${vehicle.licensePlate}</td>
<td>${vehicle.brand} ${vehicle.model}</td>
<td>${vehicle.color || '-'}</td>
<td>${vehicle.mileage || 0} 公里</td>
<td>${utils.getStatusBadge(vehicle.status)}</td>
<td class="table-actions">
<button class="btn btn-info" onclick="viewVehicle(${vehicle.vehicleId})">查看</button>
<button class="btn btn-warning" onclick="editVehicle(${vehicle.vehicleId})">编辑</button>
<button class="btn btn-danger" onclick="deleteVehicle(${vehicle.vehicleId})">删除</button>
</td>
</tr>
`).join('');
}
// 加载工单列表
async function loadOrders(status = '') {
try {
const url = status ? API_ENDPOINTS.ORDERS_BY_STATUS(status) : API_ENDPOINTS.ORDERS;
const response = await api.get(url);
if (response.code === 200 && response.data) {
displayOrders(response.data);
}
} catch (error) {
console.error('加载工单列表失败:', error);
utils.showError('加载工单列表失败');
}
}
// 显示工单列表
async function displayOrders(orders) {
const tbody = document.getElementById('ordersTableBody');
if (orders.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-state">暂无数据</td></tr>';
return;
}
const vehiclesRes = await api.get(API_ENDPOINTS.VEHICLES);
const vehicles = vehiclesRes.data || [];
tbody.innerHTML = orders.map(order => {
const vehicle = vehicles.find(v => v.vehicleId === order.vehicleId);
return `
<tr>
<td>${order.orderNo}</td>
<td>${utils.getServiceTypeText(order.serviceType)}</td>
<td>${vehicle ? vehicle.licensePlate : '-'}</td>
<td>¥${order.totalCost || 0}</td>
<td>${utils.getStatusBadge(order.status)}</td>
<td>${utils.formatDateTime(order.createTime)}</td>
<td class="table-actions">
<button class="btn btn-info" onclick="viewOrder(${order.orderId})">查看</button>
<button class="btn btn-warning" onclick="editOrder(${order.orderId})">编辑</button>
<button class="btn btn-danger" onclick="deleteOrder(${order.orderId})">删除</button>
</td>
</tr>
`;
}).join('');
}
// 加载配件列表
async function loadParts() {
try {
const response = await api.get(API_ENDPOINTS.PARTS);
if (response.code === 200 && response.data) {
displayParts(response.data);
}
} catch (error) {
console.error('加载配件列表失败:', error);
utils.showError('加载配件列表失败');
}
}
// 显示配件列表
function displayParts(parts) {
const tbody = document.getElementById('partsTableBody');
if (parts.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-state">暂无数据</td></tr>';
return;
}
tbody.innerHTML = parts.map(part => {
const isLowStock = part.stockQuantity <= part.minStock;
return `
<tr ${isLowStock ? 'style="background-color: #fff1f0;"' : ''}>
<td>${part.partNo}</td>
<td>${part.partName}</td>
<td>${part.category || '-'}</td>
<td>${part.stockQuantity} ${part.unit}${isLowStock ? ' <span class="badge badge-danger">预警</span>' : ''}</td>
<td>¥${part.unitPrice}</td>
<td>${part.status === 1 ? '<span class="badge badge-success">正常</span>' : '<span class="badge badge-secondary">停用</span>'}</td>
<td class="table-actions">
<button class="btn btn-info" onclick="viewPart(${part.partId})">查看</button>
<button class="btn btn-warning" onclick="editPart(${part.partId})">编辑</button>
<button class="btn btn-danger" onclick="deletePart(${part.partId})">删除</button>
</td>
</tr>
`;
}).join('');
}
// 加载预约列表
async function loadAppointments(status = '') {
try {
const url = status ? API_ENDPOINTS.APPOINTMENTS_BY_STATUS(status) : API_ENDPOINTS.APPOINTMENTS;
const response = await api.get(url);
if (response.code === 200 && response.data) {
displayAppointments(response.data);
}
} catch (error) {
console.error('加载预约列表失败:', error);
utils.showError('加载预约列表失败');
}
}
// 显示预约列表
async function displayAppointments(appointments) {
const tbody = document.getElementById('appointmentsTableBody');
if (appointments.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-state">暂无数据</td></tr>';
return;
}
const vehiclesRes = await api.get(API_ENDPOINTS.VEHICLES);
const vehicles = vehiclesRes.data || [];
tbody.innerHTML = appointments.map(appointment => {
const vehicle = vehicles.find(v => v.vehicleId === appointment.vehicleId);
return `
<tr>
<td>${appointment.appointmentId}</td>
<td>${utils.getServiceTypeText(appointment.serviceType)}</td>
<td>${vehicle ? vehicle.licensePlate : '-'}</td>
<td>${utils.formatDateTime(appointment.appointmentTime)}</td>
<td>${appointment.contactPhone}</td>
<td>${utils.getStatusBadge(appointment.status)}</td>
<td class="table-actions">
<button class="btn btn-success" onclick="confirmAppointment(${appointment.appointmentId})">确认</button>
<button class="btn btn-danger" onclick="cancelAppointment(${appointment.appointmentId})">取消</button>
</td>
</tr>
`;
}).join('');
}
// 过滤工单
function filterOrders() {
const status = document.getElementById('orderStatusFilter').value;
loadOrders(status);
}
// 过滤预约
function filterAppointments() {
const status = document.getElementById('appointmentStatusFilter').value;
loadAppointments(status);
}
// 搜索用户
function searchUsers() {
const keyword = document.getElementById('searchUser').value.toLowerCase();
// 实现搜索逻辑
}
// 搜索车辆
function searchVehicles() {
const keyword = document.getElementById('searchVehicle').value.toLowerCase();
// 实现搜索逻辑
}
// 搜索配件
function searchParts() {
const keyword = document.getElementById('searchPart').value.toLowerCase();
// 实现搜索逻辑
}
// 显示低库存配件
async function showLowStockParts() {
try {
const response = await api.get(API_ENDPOINTS.PARTS_LOW_STOCK);
if (response.code === 200 && response.data) {
displayParts(response.data);
utils.showSuccess(`找到 ${response.data.length} 个库存预警配件`);
}
} catch (error) {
console.error('加载库存预警失败:', error);
utils.showError('加载库存预警失败');
}
}
// 占位函数 - 实际项目中需要实现完整功能
function showAddUserModal() { alert('添加用户功能'); }
function viewUser(id) { alert('查看用户: ' + id); }
function editUser(id) { alert('编辑用户: ' + id); }
async function deleteUser(id) {
if (utils.confirm('确定要删除此用户吗?')) {
try {
const response = await api.delete(API_ENDPOINTS.USER_BY_ID(id));
if (response.code === 200) {
utils.showSuccess('删除成功');
loadUsers();
} else {
utils.showError(response.message);
}
} catch (error) {
utils.showError('删除失败');
}
}
}
function showAddVehicleModal() { alert('添加车辆功能'); }
function viewVehicle(id) { alert('查看车辆: ' + id); }
function editVehicle(id) { alert('编辑车辆: ' + id); }
async function deleteVehicle(id) {
if (utils.confirm('确定要删除此车辆吗?')) {
try {
const response = await api.delete(API_ENDPOINTS.VEHICLE_BY_ID(id));
if (response.code === 200) {
utils.showSuccess('删除成功');
loadVehicles();
} else {
utils.showError(response.message);
}
} catch (error) {
utils.showError('删除失败');
}
}
}
function showAddOrderModal() { alert('创建工单功能'); }
function viewOrder(id) { alert('查看工单: ' + id); }
function editOrder(id) { alert('编辑工单: ' + id); }
async function deleteOrder(id) {
if (utils.confirm('确定要删除此工单吗?')) {
try {
const response = await api.delete(API_ENDPOINTS.ORDER_BY_ID(id));
if (response.code === 200) {
utils.showSuccess('删除成功');
loadOrders();
} else {
utils.showError(response.message);
}
} catch (error) {
utils.showError('删除失败');
}
}
}
function showAddPartModal() { alert('添加配件功能'); }
function viewPart(id) { alert('查看配件: ' + id); }
function editPart(id) { alert('编辑配件: ' + id); }
async function deletePart(id) {
if (utils.confirm('确定要删除此配件吗?')) {
try {
const response = await api.delete(API_ENDPOINTS.PART_BY_ID(id));
if (response.code === 200) {
utils.showSuccess('删除成功');
loadParts();
} else {
utils.showError(response.message);
}
} catch (error) {
utils.showError('删除失败');
}
}
}
async function confirmAppointment(id) {
try {
const response = await api.put(API_ENDPOINTS.APPOINTMENT_BY_ID(id), { status: 'confirmed' });
if (response.code === 200) {
utils.showSuccess('预约已确认');
loadAppointments();
} else {
utils.showError(response.message);
}
} catch (error) {
utils.showError('操作失败');
}
}
async function cancelAppointment(id) {
if (utils.confirm('确定要取消此预约吗?')) {
try {
const response = await api.put(API_ENDPOINTS.CANCEL_APPOINTMENT(id));
if (response.code === 200) {
utils.showSuccess('预约已取消');
loadAppointments();
} else {
utils.showError(response.message);
}
} catch (error) {
utils.showError('操作失败');
}
}
}

194
frontend/js/api.js Normal file
View File

@@ -0,0 +1,194 @@
// API请求工具类
class API {
constructor() {
this.baseURL = API_CONFIG.BASE_URL;
this.timeout = API_CONFIG.TIMEOUT;
}
// 获取请求头
getHeaders() {
const headers = {
'Content-Type': 'application/json'
};
const token = localStorage.getItem(STORAGE_KEYS.TOKEN);
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return headers;
}
// 通用请求方法
async request(url, options = {}) {
const config = {
method: options.method || 'GET',
headers: this.getHeaders(),
...options
};
if (options.body && typeof options.body === 'object') {
config.body = JSON.stringify(options.body);
}
try {
const response = await fetch(this.baseURL + url, config);
const data = await response.json();
if (data.code === 401) {
this.handleUnauthorized();
throw new Error('未授权,请重新登录');
}
return data;
} catch (error) {
console.error('API请求错误:', error);
throw error;
}
}
// GET请求
async get(url) {
return this.request(url, { method: 'GET' });
}
// POST请求
async post(url, body) {
return this.request(url, { method: 'POST', body });
}
// PUT请求
async put(url, body) {
return this.request(url, { method: 'PUT', body });
}
// DELETE请求
async delete(url) {
return this.request(url, { method: 'DELETE' });
}
// 处理未授权情况
handleUnauthorized() {
localStorage.removeItem(STORAGE_KEYS.TOKEN);
localStorage.removeItem(STORAGE_KEYS.USER_INFO);
window.location.href = 'login.html';
}
}
// 创建API实例
const api = new API();
// 工具函数
const utils = {
// 显示提示消息
showMessage(message, type = 'info') {
alert(message);
},
// 显示成功消息
showSuccess(message) {
this.showMessage(message, 'success');
},
// 显示错误消息
showError(message) {
this.showMessage(message, 'error');
},
// 确认对话框
confirm(message) {
return window.confirm(message);
},
// 格式化日期
formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN');
},
// 格式化日期时间
formatDateTime(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleString('zh-CN');
},
// 获取当前用户信息
getCurrentUser() {
const userStr = localStorage.getItem(STORAGE_KEYS.USER_INFO);
return userStr ? JSON.parse(userStr) : null;
},
// 检查用户角色
hasRole(role) {
const user = this.getCurrentUser();
return user && user.role === role;
},
// 退出登录
logout() {
if (this.confirm('确定要退出登录吗?')) {
localStorage.removeItem(STORAGE_KEYS.TOKEN);
localStorage.removeItem(STORAGE_KEYS.USER_INFO);
window.location.href = 'login.html';
}
},
// 检查登录状态
checkAuth() {
const token = localStorage.getItem(STORAGE_KEYS.TOKEN);
if (!token) {
window.location.href = 'login.html';
return false;
}
return true;
},
// 获取状态标签HTML
getStatusBadge(status, type) {
const badges = {
// 工单状态
pending: '<span class="badge badge-info">待处理</span>',
appointed: '<span class="badge badge-info">已预约</span>',
in_progress: '<span class="badge badge-warning">进行中</span>',
completed: '<span class="badge badge-success">已完成</span>',
cancelled: '<span class="badge badge-secondary">已取消</span>',
// 支付状态
unpaid: '<span class="badge badge-danger">未支付</span>',
paid: '<span class="badge badge-success">已支付</span>',
refunded: '<span class="badge badge-secondary">已退款</span>',
// 预约状态
confirmed: '<span class="badge badge-success">已确认</span>',
// 车辆状态
normal: '<span class="badge badge-success">正常</span>',
in_service: '<span class="badge badge-warning">维修中</span>'
};
return badges[status] || `<span class="badge badge-secondary">${status}</span>`;
},
// 获取服务类型文本
getServiceTypeText(type) {
const types = {
maintenance: '保养维护',
repair: '维修服务',
beauty: '美容服务',
insurance: '保险代理'
};
return types[type] || type;
},
// 获取用户角色文本
getRoleText(role) {
const roles = {
admin: '管理员',
staff: '工作人员',
customer: '客户'
};
return roles[role] || role;
}
};

52
frontend/js/config.js Normal file
View File

@@ -0,0 +1,52 @@
// API配置
const API_CONFIG = {
BASE_URL: 'http://localhost:8080/api',
TIMEOUT: 30000
};
// API端点
const API_ENDPOINTS = {
// 认证相关
LOGIN: '/auth/login',
LOGOUT: '/auth/logout',
REGISTER: '/auth/register',
// 用户管理
USERS: '/users',
USER_BY_ID: (id) => `/users/${id}`,
USERS_BY_ROLE: (role) => `/users/role/${role}`,
CHANGE_PASSWORD: (id) => `/users/${id}/password`,
// 车辆管理
VEHICLES: '/vehicles',
VEHICLE_BY_ID: (id) => `/vehicles/${id}`,
VEHICLES_BY_CUSTOMER: (customerId) => `/vehicles/customer/${customerId}`,
VEHICLE_BY_PLATE: (plate) => `/vehicles/plate/${plate}`,
// 工单管理
ORDERS: '/orders',
ORDER_BY_ID: (id) => `/orders/${id}`,
ORDERS_BY_CUSTOMER: (customerId) => `/orders/customer/${customerId}`,
ORDERS_BY_VEHICLE: (vehicleId) => `/orders/vehicle/${vehicleId}`,
ORDERS_BY_STATUS: (status) => `/orders/status/${status}`,
// 配件管理
PARTS: '/parts',
PART_BY_ID: (id) => `/parts/${id}`,
PARTS_BY_CATEGORY: (category) => `/parts/category/${category}`,
PARTS_LOW_STOCK: '/parts/low-stock',
// 预约管理
APPOINTMENTS: '/appointments',
APPOINTMENT_BY_ID: (id) => `/appointments/${id}`,
APPOINTMENTS_BY_CUSTOMER: (customerId) => `/appointments/customer/${customerId}`,
APPOINTMENTS_BY_STATUS: (status) => `/appointments/status/${status}`,
CANCEL_APPOINTMENT: (id) => `/appointments/${id}/cancel`
};
// 本地存储键名
const STORAGE_KEYS = {
TOKEN: 'car_maintenance_token',
USER_INFO: 'car_maintenance_user',
REMEMBER_ME: 'car_maintenance_remember'
};

102
frontend/js/login.js Normal file
View File

@@ -0,0 +1,102 @@
// 登录页面JavaScript
// 页面加载时检查是否已登录
window.addEventListener('DOMContentLoaded', () => {
const token = localStorage.getItem(STORAGE_KEYS.TOKEN);
if (token) {
const user = utils.getCurrentUser();
if (user) {
redirectToDashboard(user.role);
}
}
// 回车键登录
document.getElementById('password').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleLogin();
}
});
});
// 处理登录
async function handleLogin() {
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value.trim();
const role = document.getElementById('role').value;
// 验证输入
if (!username) {
utils.showError('请输入用户名');
return;
}
if (!password) {
utils.showError('请输入密码');
return;
}
// 禁用登录按钮
const loginBtn = document.querySelector('.btn-login');
const originalText = loginBtn.textContent;
loginBtn.disabled = true;
loginBtn.textContent = '登录中...';
try {
// 调用登录API
const response = await api.post(API_ENDPOINTS.LOGIN, {
username: username,
password: password
});
if (response.code === 200) {
const { token, userInfo } = response.data;
// 验证角色
if (userInfo.role !== role) {
utils.showError('登录角色不匹配,请选择正确的角色');
loginBtn.disabled = false;
loginBtn.textContent = originalText;
return;
}
// 保存登录信息
localStorage.setItem(STORAGE_KEYS.TOKEN, token);
localStorage.setItem(STORAGE_KEYS.USER_INFO, JSON.stringify(userInfo));
utils.showSuccess('登录成功!');
// 延迟跳转以显示成功消息
setTimeout(() => {
redirectToDashboard(userInfo.role);
}, 500);
} else {
utils.showError(response.message || '登录失败');
loginBtn.disabled = false;
loginBtn.textContent = originalText;
}
} catch (error) {
console.error('登录错误:', error);
utils.showError('登录失败,请检查网络连接');
loginBtn.disabled = false;
loginBtn.textContent = originalText;
}
}
// 根据角色跳转到对应的仪表板
function redirectToDashboard(role) {
switch (role) {
case 'admin':
window.location.href = 'admin/dashboard.html';
break;
case 'staff':
window.location.href = 'staff/dashboard.html';
break;
case 'customer':
window.location.href = 'customer/dashboard.html';
break;
default:
utils.showError('未知的用户角色');
}
}

61
frontend/login.html Normal file
View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>车管家4S店车辆维保管理系统 - 登录</title>
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/login.css">
</head>
<body>
<div class="login-container">
<div class="login-box">
<div class="login-header">
<h1>车管家4S店车辆维保管理系统</h1>
<p>Car Maintenance Management System</p>
</div>
<div class="login-form">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" placeholder="请输入用户名" autocomplete="off">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" placeholder="请输入密码">
</div>
<div class="form-group">
<label for="role">登录角色</label>
<select id="role" name="role">
<option value="admin">管理员</option>
<option value="staff">工作人员</option>
<option value="customer">客户</option>
</select>
</div>
<button class="btn-login" onclick="handleLogin()">登录</button>
<div class="login-footer">
<a href="#" onclick="alert('请联系管理员重置密码')">忘记密码?</a>
<a href="#" onclick="alert('客户可自助注册,工作人员请联系管理员')">注册账号</a>
</div>
</div>
<div class="demo-accounts">
<p>演示账号:</p>
<ul>
<li>管理员: admin / 123456</li>
<li>工作人员: staff001 / 123456</li>
<li>客户: customer001 / 123456</li>
</ul>
</div>
</div>
</div>
<script src="js/config.js"></script>
<script src="js/api.js"></script>
<script src="js/login.js"></script>
</body>
</html>

View File

@@ -0,0 +1,270 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>工作人员仪表板 - 车管家4S店车辆维保管理系统</title>
<link rel="stylesheet" href="../css/common.css">
<link rel="stylesheet" href="../css/dashboard.css">
</head>
<body>
<div class="dashboard-container">
<div class="sidebar">
<div class="sidebar-header">
<h2>车管家系统</h2>
<p>工作人员控制台</p>
</div>
<div class="sidebar-menu">
<div class="menu-item active" onclick="showSection('overview')">
<span class="menu-icon">📊</span>
<span>工作概览</span>
</div>
<div class="menu-item" onclick="showSection('myorders')">
<span class="menu-icon">📋</span>
<span>我的工单</span>
</div>
<div class="menu-item" onclick="showSection('vehicles')">
<span class="menu-icon">🚗</span>
<span>车辆查询</span>
</div>
<div class="menu-item" onclick="showSection('parts')">
<span class="menu-icon">🔧</span>
<span>配件查询</span>
</div>
</div>
</div>
<div class="main-content">
<div class="top-nav">
<div class="top-nav-left">
<h1>工作人员仪表板</h1>
</div>
<div class="top-nav-right">
<div class="user-info">
<div class="user-avatar" id="userAvatar">S</div>
<span class="user-name" id="userName">工作人员</span>
</div>
<button class="btn-logout" onclick="utils.logout()">退出登录</button>
</div>
</div>
<div id="overview-section" class="section">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon blue">📋</div>
<div class="stat-info">
<h3 id="myOrdersCount">0</h3>
<p>我的工单</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon orange"></div>
<div class="stat-info">
<h3 id="inProgressCount">0</h3>
<p>进行中</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon green"></div>
<div class="stat-info">
<h3 id="completedCount">0</h3>
<p>已完成</p>
</div>
</div>
</div>
<div class="content-card">
<div class="content-header">
<h2>待处理工单</h2>
</div>
<div class="content-body">
<table class="table">
<thead>
<tr>
<th>工单编号</th>
<th>服务类型</th>
<th>车牌号</th>
<th>状态</th>
<th>预约时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="pendingOrdersBody">
<tr><td colspan="6" class="empty-state">暂无待处理工单</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="myorders-section" class="section" style="display: none;">
<div class="content-card">
<div class="content-header">
<h2>我的工单列表</h2>
</div>
<div class="content-body">
<table class="table">
<thead>
<tr>
<th>工单编号</th>
<th>服务类型</th>
<th>车牌号</th>
<th>客户姓名</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="myOrdersBody">
<tr><td colspan="7" class="empty-state">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="vehicles-section" class="section" style="display: none;">
<div class="toolbar">
<div class="search-box">
<input type="text" id="searchVehicle" placeholder="输入车牌号查询...">
<button onclick="searchVehicle()">🔍</button>
</div>
</div>
<div class="content-card">
<div class="content-body">
<div id="vehicleResult"></div>
</div>
</div>
</div>
<div id="parts-section" class="section" style="display: none;">
<div class="toolbar">
<div class="search-box">
<input type="text" id="searchPart" placeholder="搜索配件...">
<button onclick="searchParts()">🔍</button>
</div>
</div>
<div class="content-card">
<div class="content-body">
<table class="table">
<thead>
<tr>
<th>配件编号</th>
<th>配件名称</th>
<th>类别</th>
<th>库存数量</th>
<th>单价</th>
</tr>
</thead>
<tbody id="partsBody">
<tr><td colspan="5" class="empty-state">请输入关键词搜索配件</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script src="../js/config.js"></script>
<script src="../js/api.js"></script>
<script>
if (!utils.checkAuth() || !utils.hasRole('staff')) {
window.location.href = '../login.html';
}
window.addEventListener('DOMContentLoaded', () => {
const user = utils.getCurrentUser();
if (user) {
document.getElementById('userName').textContent = user.realName;
document.getElementById('userAvatar').textContent = user.realName.charAt(0);
loadStaffData(user.userId);
}
});
function showSection(name) {
document.querySelectorAll('.section').forEach(s => s.style.display = 'none');
document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('active'));
event.currentTarget.classList.add('active');
document.getElementById(name + '-section').style.display = 'block';
}
async function loadStaffData(staffId) {
try {
const response = await api.get(API_ENDPOINTS.ORDERS);
if (response.code === 200 && response.data) {
const myOrders = response.data.filter(o => o.staffId === staffId);
const inProgress = myOrders.filter(o => o.status === 'in_progress');
const completed = myOrders.filter(o => o.status === 'completed');
document.getElementById('myOrdersCount').textContent = myOrders.length;
document.getElementById('inProgressCount').textContent = inProgress.length;
document.getElementById('completedCount').textContent = completed.length;
}
} catch (error) {
console.error('加载数据失败:', error);
}
}
async function searchVehicle() {
const plate = document.getElementById('searchVehicle').value.trim();
if (!plate) {
utils.showError('请输入车牌号');
return;
}
try {
const response = await api.get(API_ENDPOINTS.VEHICLE_BY_PLATE(plate));
if (response.code === 200 && response.data) {
const v = response.data;
document.getElementById('vehicleResult').innerHTML = `
<div class="card">
<h3>车辆信息</h3>
<p><strong>车牌号:</strong> ${v.licensePlate}</p>
<p><strong>品牌型号:</strong> ${v.brand} ${v.model}</p>
<p><strong>颜色:</strong> ${v.color || '-'}</p>
<p><strong>车架号:</strong> ${v.vin || '-'}</p>
<p><strong>里程数:</strong> ${v.mileage || 0} 公里</p>
<p><strong>状态:</strong> ${utils.getStatusBadge(v.status)}</p>
</div>
`;
} else {
document.getElementById('vehicleResult').innerHTML = '<p class="empty-state">未找到该车辆</p>';
}
} catch (error) {
utils.showError('查询失败');
}
}
async function searchParts() {
try {
const response = await api.get(API_ENDPOINTS.PARTS);
if (response.code === 200 && response.data) {
const tbody = document.getElementById('partsBody');
const keyword = document.getElementById('searchPart').value.toLowerCase();
const filtered = response.data.filter(p =>
p.partName.toLowerCase().includes(keyword) ||
p.partNo.toLowerCase().includes(keyword)
);
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">未找到配件</td></tr>';
return;
}
tbody.innerHTML = filtered.map(p => `
<tr>
<td>${p.partNo}</td>
<td>${p.partName}</td>
<td>${p.category || '-'}</td>
<td>${p.stockQuantity} ${p.unit}</td>
<td>¥${p.unitPrice}</td>
</tr>
`).join('');
}
} catch (error) {
utils.showError('搜索失败');
}
}
</script>
</body>
</html>