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:
300
frontend/admin/dashboard.html
Normal file
300
frontend/admin/dashboard.html
Normal 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
377
frontend/css/common.css
Normal 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
286
frontend/css/dashboard.css
Normal 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
152
frontend/css/login.css
Normal 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;
|
||||
}
|
||||
}
|
||||
361
frontend/customer/dashboard.html
Normal file
361
frontend/customer/dashboard.html
Normal 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>
|
||||
468
frontend/js/admin-dashboard.js
Normal file
468
frontend/js/admin-dashboard.js
Normal 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
194
frontend/js/api.js
Normal 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
52
frontend/js/config.js
Normal 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
102
frontend/js/login.js
Normal 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
61
frontend/login.html
Normal 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>
|
||||
270
frontend/staff/dashboard.html
Normal file
270
frontend/staff/dashboard.html
Normal 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>
|
||||
Reference in New Issue
Block a user