Initial commit: 电动车租赁管理系统管理端

This commit is contained in:
notyclaw 2026-03-27 20:27:45 +08:00
commit 6f5d2b8d13
42 changed files with 6543 additions and 0 deletions

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
node_modules/

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

33
browser-server.js Normal file
View File

@ -0,0 +1,33 @@
const { chromium } = require('playwright');
let browser = null;
let page = null;
async function init() {
browser = await chromium.launch({
headless: true,
args: ['--disable-setuid-sandbox', '--no-sandbox']
});
page = await browser.newPage();
await page.setViewportSize({ width: 1920, height: 1080 });
// 登录
await page.goto('http://localhost:5173');
await page.waitForTimeout(3000);
await page.fill('input[placeholder="请输入用户名"]', 'admin');
await page.fill('input[placeholder="请输入密码"]', 'admin');
await page.click('button[type="submit"]');
await page.waitForTimeout(3000);
console.log('登录完成');
}
async function screenshot(route, filename) {
if (!page) await init();
await page.goto('http://localhost:5173/#/' + route);
await page.waitForTimeout(3000);
await page.screenshot({ path: '/Users/notyclaw/Desktop/' + filename, fullPage: true });
console.log(filename + ' 完成');
}
module.exports = { init, screenshot };

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>租车系统管理后台</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

26
login.cjs Normal file
View File

@ -0,0 +1,26 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('http://localhost:5173');
// 等待Vue应用渲染
await page.waitForSelector('.el-input', { timeout: 15000 }).catch(() => console.log('没找到el-input'));
await page.waitForTimeout(3000);
// 尝试输入
try {
await page.fill('input[placeholder="请输入用户名"]', 'admin');
await page.fill('input[placeholder="请输入密码"]', 'admin');
await page.click('button[type="submit"]');
await page.waitForTimeout(3000);
await page.screenshot({ path: '/Users/notyclaw/Desktop/admin_login.png', fullPage: true });
console.log('登录后截图');
} catch(e) {
console.log('登录失败:', e.message);
}
await browser.close();
})();

2736
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "e-scooter-rental-web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@rollup/rollup-linux-x64-gnu": "^4.60.0",
"axios": "^1.13.6",
"echarts": "^6.0.0",
"element-plus": "^2.13.3",
"file-saver": "^2.0.5",
"html2canvas": "^1.4.1",
"jspdf": "^4.2.0",
"vue": "^3.5.25",
"vue-echarts": "^8.0.1",
"vue-jsonp": "^2.1.0",
"vue-router": "^5.0.3",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.2",
"playwright": "^1.58.2",
"vite": "^7.3.1"
}
}

178
public/article-cover.html Normal file
View File

@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>外卖骑手选车攻略</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
width: 1200px;
height: 800px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
}
/* 背景装饰 */
.bg-decoration {
position: absolute;
width: 100%;
height: 100%;
}
.circle {
position: absolute;
border-radius: 50%;
opacity: 0.1;
}
.circle1 {
width: 400px;
height: 400px;
background: #e94560;
top: -100px;
right: -100px;
}
.circle2 {
width: 300px;
height: 300px;
background: #00d9ff;
bottom: -50px;
left: -50px;
}
.circle3 {
width: 200px;
height: 200px;
background: #ffd700;
top: 50%;
left: 10%;
opacity: 0.05;
}
/* 主内容 */
.content {
text-align: center;
z-index: 10;
padding: 40px;
}
.badge {
display: inline-block;
background: linear-gradient(90deg, #e94560, #ff6b6b);
color: white;
padding: 8px 24px;
border-radius: 30px;
font-size: 18px;
font-weight: 600;
margin-bottom: 30px;
letter-spacing: 4px;
}
.title {
font-size: 72px;
font-weight: 800;
color: white;
margin-bottom: 20px;
text-shadow: 0 4px 20px rgba(0,0,0,0.3);
line-height: 1.2;
}
.title span {
background: linear-gradient(90deg, #ffd700, #ffaa00);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 28px;
color: rgba(255,255,255,0.7);
margin-bottom: 40px;
letter-spacing: 8px;
}
.tags {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.tag {
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
color: rgba(255,255,255,0.8);
padding: 10px 20px;
border-radius: 8px;
font-size: 16px;
}
/* 底部信息 */
.footer {
position: absolute;
bottom: 40px;
display: flex;
align-items: center;
gap: 20px;
z-index: 10;
}
.logo {
width: 60px;
height: 60px;
background: linear-gradient(135deg, #e94560, #ff6b6b);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
}
.account-name {
color: rgba(255,255,255,0.6);
font-size: 18px;
}
/* 装饰图标 */
.icons {
position: absolute;
width: 100%;
height: 100%;
z-index: 1;
}
.icon {
position: absolute;
font-size: 60px;
opacity: 0.15;
}
.icon1 { top: 15%; right: 20%; transform: rotate(15deg); }
.icon2 { bottom: 20%; right: 15%; transform: rotate(-10deg); }
.icon3 { top: 30%; left: 10%; transform: rotate(-5deg); }
</style>
</head>
<body>
<div class="bg-decoration">
<div class="circle circle1"></div>
<div class="circle circle2"></div>
<div class="circle circle3"></div>
</div>
<div class="icons">
<div class="icon icon1">🛵</div>
<div class="icon icon2">🚴</div>
<div class="icon icon3"></div>
</div>
<div class="content">
<div class="badge">2026最新版</div>
<h1 class="title">外卖骑手<br><span>选车攻略</span></h1>
<p class="subtitle">跑单三年经验总结</p>
<div class="tags">
<span class="tag">🚴 租车攻略</span>
<span class="tag">💰 性价比</span>
<span class="tag">⚠️ 避坑指南</span>
<span class="tag">🛠 保养技巧</span>
</div>
</div>
<div class="footer">
<div class="logo">🦞</div>
<span class="account-name">飞达租赁</span>
</div>
</body>
</html>

View File

@ -0,0 +1,227 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>黑骑士电动车测评</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
width: 1200px;
height: 800px;
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 40%, #16213e 100%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
}
/* 背景特效 */
.bg-effects {
position: absolute;
width: 100%;
height: 100%;
}
.glow {
position: absolute;
border-radius: 50%;
filter: blur(100px);
}
.glow-red {
width: 600px;
height: 600px;
background: #ff3333;
top: -200px;
right: -150px;
opacity: 0.25;
}
.glow-blue {
width: 400px;
height: 400px;
background: #00aaff;
bottom: -100px;
left: -100px;
opacity: 0.2;
}
.glow-gold {
width: 250px;
height: 250px;
background: #ffd700;
top: 45%;
left: 15%;
opacity: 0.12;
}
/* 装饰元素 */
.decor {
position: absolute;
font-size: 60px;
opacity: 0.12;
}
.decor1 { top: 8%; right: 12%; transform: rotate(-20deg); }
.decor2 { bottom: 12%; left: 8%; transform: rotate(15deg); }
.decor3 { top: 35%; left: 5%; transform: rotate(-10deg); font-size: 40px; }
/* 主内容 */
.content {
text-align: center;
z-index: 10;
padding: 40px;
}
.tag {
display: inline-block;
background: linear-gradient(90deg, #ff3333, #ff6633);
color: white;
padding: 12px 35px;
border-radius: 45px;
font-size: 22px;
font-weight: 700;
margin-bottom: 35px;
letter-spacing: 5px;
box-shadow: 0 15px 40px rgba(255, 51, 51, 0.35);
}
.main-title {
font-size: 90px;
font-weight: 950;
color: white;
margin-bottom: 20px;
text-shadow: 0 10px 40px rgba(0,0,0,0.6);
line-height: 1.05;
}
.main-title span {
background: linear-gradient(90deg, #ff3333, #ffd700);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 34px;
color: rgba(255,255,255,0.55);
margin-bottom: 45px;
letter-spacing: 10px;
}
.divider {
width: 220px;
height: 4px;
background: linear-gradient(90deg, transparent, #ff3333, transparent);
margin: 0 auto 35px;
}
.features {
display: flex;
gap: 25px;
justify-content: center;
flex-wrap: wrap;
}
.feature {
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.12);
color: rgba(255,255,255,0.9);
padding: 14px 28px;
border-radius: 35px;
font-size: 18px;
font-weight: 500;
}
.feature.hot {
background: rgba(255, 51, 51, 0.15);
border-color: rgba(255, 51, 51, 0.4);
}
/* 车型标签 */
.models-row {
display: flex;
gap: 20px;
justify-content: center;
margin-top: 30px;
}
.model-badge {
background: rgba(0,0,0,0.3);
border: 1px solid rgba(255,215,0,0.3);
border-radius: 12px;
padding: 12px 20px;
}
.model-name {
color: #ffd700;
font-size: 20px;
font-weight: 700;
}
.model-price {
color: rgba(255,255,255,0.5);
font-size: 13px;
margin-top: 3px;
}
/* 底部 */
.footer {
position: absolute;
bottom: 35px;
display: flex;
align-items: center;
gap: 18px;
z-index: 10;
}
.logo {
width: 55px;
height: 55px;
background: linear-gradient(135deg, #ff3333, #ff6633);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
box-shadow: 0 8px 25px rgba(255, 51, 51, 0.35);
}
.account-name {
color: rgba(255,255,255,0.55);
font-size: 16px;
letter-spacing: 2px;
}
</style>
</head>
<body>
<div class="bg-effects">
<div class="glow glow-red"></div>
<div class="glow glow-blue"></div>
<div class="glow glow-gold"></div>
</div>
<div class="decor decor1">🛵</div>
<div class="decor decor2"></div>
<div class="decor decor3">🚴</div>
<div class="content">
<div class="tag">🚴 外卖神车</div>
<h1 class="main-title">黑骑士<br><span>深度测评</span></h1>
<p class="subtitle">2026外卖骑手必看选车指南</p>
<div class="divider"></div>
<div class="features">
<span class="feature hot">🔥 续航300公里</span>
<span class="feature">💡 6万流明大灯</span>
<span class="feature">📱 4G智能中控</span>
<span class="feature">🛡️ CBS联动刹车</span>
</div>
<div class="models-row">
<div class="model-badge">
<div class="model-name">B3 PLUS</div>
<div class="model-price">性价比之王 ¥1599起</div>
</div>
<div class="model-badge">
<div class="model-name">E10</div>
<div class="model-price">网红爆款 续航王者</div>
</div>
<div class="model-badge">
<div class="model-name">B5P</div>
<div class="model-price">旗舰商务 舒适拉满</div>
</div>
</div>
</div>
<div class="footer">
<div class="logo">🦞</div>
<span class="account-name">飞达租赁</span>
</div>
</body>
</html>

107
public/daily-cover.html Normal file
View File

@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>电动车封面</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 800px;
height: 400px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
position: relative;
overflow: hidden;
}
body::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(233,69,96,0.1) 0%, transparent 50%);
animation: rotate 20s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.content {
position: relative;
z-index: 1;
text-align: center;
color: white;
}
.logo {
width: 80px;
height: 80px;
background: linear-gradient(135deg, #e94560, #ff6b6b);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 30px;
font-size: 40px;
box-shadow: 0 10px 30px rgba(233,69,96,0.4);
}
h1 {
font-size: 48px;
font-weight: 800;
margin-bottom: 20px;
background: linear-gradient(90deg, #fff, #e94560);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
font-size: 24px;
color: rgba(255,255,255,0.8);
margin-bottom: 30px;
}
.tags {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.tag {
background: rgba(255,255,255,0.15);
padding: 8px 20px;
border-radius: 25px;
font-size: 16px;
border: 1px solid rgba(255,255,255,0.2);
}
.date {
position: absolute;
bottom: 30px;
right: 30px;
font-size: 18px;
color: rgba(255,255,255,0.6);
}
</style>
</head>
<body>
<div class="content">
<div class="logo">🚗</div>
<h1>2026电动车选购指南</h1>
<p class="subtitle">买对不买贵这5点一定要记住</p>
<div class="tags">
<span class="tag">🔋 续航</span>
<span class="tag">💰 性价比</span>
<span class="tag">🛡️ 避坑</span>
<span class="tag">⚡ 新国标</span>
</div>
</div>
<div class="date">2026年3月8日</div>
</body>
</html>

12
public/logo-simple.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>租车系统 Logo</title>
</head>
<body style="margin:0;padding:40px;background:#fff;text-align:center;font-family:微软雅黑;">
<div style="font-size:100px;">🦞</div>
<div style="font-size:48px;font-weight:bold;margin:20px 0;color:#333;">租车系统</div>
<div style="font-size:18px;color:#666;">Electric Scooter Rental</div>
</body>
</html>

54
public/logo.html Normal file
View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>租车系统 Logo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: 'Microsoft YaHei', sans-serif;
}
.logo-container {
text-align: center;
padding: 40px;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.logo-icon {
font-size: 80px;
margin-bottom: 20px;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.logo-text {
font-size: 36px;
font-weight: bold;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.logo-sub {
font-size: 14px;
color: #999;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="logo-container">
<div class="logo-icon">🦞</div>
<div class="logo-text">租车系统</div>
<div class="logo-sub">Electric Scooter Rental</div>
</div>
</body>
</html>

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

26
screenshot-rental.cjs Normal file
View File

@ -0,0 +1,26 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
console.log('打开页面...');
await page.goto('http://localhost:5173', { waitUntil: 'networkidle' });
console.log('填写账号...');
await page.type('input[placeholder="请输入用户名"]', 'admin', { delay: 50 });
await page.type('input[placeholder="请输入密码"]', 'admin', { delay: 50 });
console.log('点击登录...');
await page.click('button[type="submit"]');
console.log('等待跳转...');
await page.waitForTimeout(3000);
console.log('截取首页...');
await page.screenshot({ path: '/tmp/admin_home.png', fullPage: true });
console.log('完成!');
await browser.close();
process.exit(0);
})();

35
screenshot-rental2.cjs Normal file
View File

@ -0,0 +1,35 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
console.log('打开登录页...');
await page.goto('http://localhost:5173/login', { waitUntil: 'networkidle', timeout: 15000 });
console.log('等待页面加载...');
await page.waitForTimeout(2000);
// 打印页面内容用于调试
const html = await page.content();
console.log('页面标题:', await page.title());
console.log('填写用户名...');
await page.fill('input[placeholder="请输入用户名"]', 'admin', { timeout: 5000 });
console.log('填写密码...');
await page.fill('input[placeholder="请输入密码"]', 'admin', { timeout: 5000 });
console.log('点击登录...');
await page.click('button:has-text("登录")');
console.log('等待跳转...');
await page.waitForTimeout(3000);
console.log('截取首页...');
await page.screenshot({ path: '/tmp/admin_home.png', fullPage: true });
console.log('完成!截图保存在 /tmp/admin_home.png');
await browser.close();
process.exit(0);
})();

32
screenshot.cjs Normal file
View File

@ -0,0 +1,32 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
console.log('打开页面...');
await page.goto('http://localhost:5173', { waitUntil: 'networkidle' });
console.log('填写账号...');
await page.type('input[placeholder="请输入用户名"]', 'admin', { delay: 50 });
await page.type('input[placeholder="请输入密码"]', 'admin', { delay: 50 });
console.log('点击登录...');
await page.click('button[type="submit"]');
console.log('等待跳转...');
await page.waitForTimeout(3000);
console.log('截取首页...');
await page.screenshot({ path: '/Users/notyclaw/Desktop/admin_home.png', fullPage: true });
// 点击财务管理
console.log('点击财务管理...');
await page.click('text=财务管理');
await page.waitForTimeout(2000);
await page.screenshot({ path: '/Users/notyclaw/Desktop/finance.png', fullPage: true });
console.log('完成!');
await browser.close();
process.exit(0);
})();

13
src/App.vue Normal file
View File

@ -0,0 +1,13 @@
<template>
<router-view />
</template>
<script setup>
</script>
<style>
#app {
width: 100%;
height: 100%;
}
</style>

1
src/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,43 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

17
src/main.js Normal file
View File

@ -0,0 +1,17 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(ElementPlus)
app.use(router)
app.mount('#app')

38
src/router/index.js Normal file
View File

@ -0,0 +1,38 @@
import { createRouter, createWebHistory } from 'vue-router'
import Dashboard from '../views/Dashboard.vue'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue')
},
{
path: '/',
component: () => import('../views/Layout.vue'),
redirect: '/',
meta: { requiresAuth: true },
children: [
{ path: '/', name: 'Dashboard', component: Dashboard },
{ path: '/finance', name: 'Finance', component: () => import('../views/Finance.vue') },
{ path: '/stores', name: 'Stores', component: () => import('../views/Stores.vue') },
{ path: '/applications', name: 'Applications', component: () => import('../views/Applications.vue') },
{ path: '/disputes', name: 'Disputes', component: () => import('../views/Disputes.vue') },
{ path: '/complaints', name: 'Complaints', component: () => import('../views/Complaints.vue') },
{ path: '/payments', name: 'Payments', component: () => import('../views/Payments.vue') }
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
// 暂时禁用登录验证
next()
})
export default router

240
src/style.css Normal file
View File

@ -0,0 +1,240 @@
:root {
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
line-height: 1.6;
font-weight: 400;
color-scheme: light;
color: #333;
background-color: #f5f7fa;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* 主题色 - 清新绿 */
--primary-color: #10b981;
--primary-light: #34d399;
--primary-dark: #059669;
--secondary-color: #06b6d4;
--accent-color: #f59e0b;
--bg-card: #ffffff;
--bg-sidebar: #1f2937;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-light: #9ca3af;
--border-color: #e5e7eb;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow-md: 0 4px 6px rgba(0,0,0,0.07);
--shadow-lg: 0 10px 15px rgba(0,0,0,0.1);
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
--transition: all 0.3s ease;
}
/* 强制 Element Plus 选中文字显示 */
.el-select__wrapper.is-focused {
box-shadow: 0 0 0 1px #10b981 inset !important;
}
.el-select__wrapper .el-select__selected-item {
color: #333 !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.el-select__wrapper .el-select__placeholder {
color: #999 !important;
display: block !important;
}
.el-tag.el-select__input-is-focused {
background-color: #f0f0f0 !important;
color: #333 !important;
}
* {
box-sizing: border-box;
}
a {
font-weight: 500;
color: var(--primary-color);
text-decoration: none;
transition: var(--transition);
}
a:hover {
color: var(--primary-dark);
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
}
/* 卡片样式 */
.card {
background: var(--bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
padding: 1.5rem;
transition: var(--transition);
}
.card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
#app {
width: 100%;
min-height: 100vh;
}
/* 按钮样式 */
button {
border-radius: var(--radius-md);
border: none;
padding: 0.75rem 1.5rem;
font-size: 0.95em;
font-weight: 500;
font-family: inherit;
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
color: white;
cursor: pointer;
transition: var(--transition);
box-shadow: var(--shadow-sm);
}
button:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
background: linear-gradient(135deg, var(--primary-light), var(--primary-color));
}
button:active {
transform: translateY(0);
}
/* 输入框样式 */
input, select, textarea {
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
padding: 0.6rem 1rem;
font-size: 0.95em;
font-family: inherit;
transition: var(--transition);
background: white;
color: #333;
}
input::placeholder, textarea::placeholder {
color: #999;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
}
/* 表格样式 */
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
background: white;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
th {
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
color: white;
font-weight: 600;
padding: 1rem;
text-align: left;
}
td {
padding: 0.875rem 1rem;
border-bottom: 1px solid var(--border-color);
}
tr:hover td {
background: #f9fafb;
}
/* 滚动条美化 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--primary-light);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--primary-color);
}
/* 动画 */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}
.fade-in {
animation: fadeIn 0.4s ease-out;
}
.slide-in {
animation: slideIn 0.3s ease-out;
}
/* 状态标签 */
.status-tag {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.85em;
font-weight: 500;
}
.status-active {
background: #d1fae5;
color: #065f46;
}
.status-inactive {
background: #fee2e2;
color: #991b1b;
}
.status-pending {
background: #fef3c7;
color: #92400e;
}

25
src/utils/index.js Normal file
View File

@ -0,0 +1,25 @@
// 工具函数
import * as XLSX from 'xlsx'
import { saveAs } from 'file-saver'
// 导出 Excel
export const exportExcel = (data, filename, sheetName = 'Sheet1') => {
const worksheet = XLSX.utils.json_to_sheet(data)
const workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName)
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' })
const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
saveAs(blob, `${filename}.xlsx`)
}
// 格式化日期
export const formatDate = (date) => {
if (!date) return '-'
return new Date(date).toLocaleDateString('zh-CN')
}
// 格式化金额
export const formatMoney = (amount) => {
if (!amount && amount !== 0) return '-'
return `¥${Number(amount).toLocaleString()}`
}

127
src/views/Applications.vue Normal file
View File

@ -0,0 +1,127 @@
<template>
<div class="page">
<el-card class="search-card">
<el-form :inline="true" :model="searchForm">
<el-form-item label="申请类型">
<el-select v-model="searchForm.type" clearable>
<el-option label="注册申请" value="注册申请" />
<el-option label="活动申请" value="活动申请" />
<el-option label="促销活动" value="促销活动" />
<el-option label="设备申请" value="设备申请" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="审批状态">
<el-select v-model="searchForm.status" clearable>
<el-option label="待审批" value="待审批" />
<el-option label="已通过" value="已通过" />
<el-option label="已拒绝" value="已拒绝" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<el-table :data="tableData" border stripe>
<el-table-column prop="appId" label="申请编号" width="150" />
<el-table-column prop="storeName" label="门店" />
<el-table-column prop="type" label="类型" width="100" />
<el-table-column prop="title" label="标题" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="申请时间" width="120">
<template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button size="small" @click="handleView(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" title="申请详情" width="500px">
<el-descriptions :column="1" border>
<el-descriptions-item label="申请编号">{{ viewData.appId }}</el-descriptions-item>
<el-descriptions-item label="门店">{{ viewData.storeName }}</el-descriptions-item>
<el-descriptions-item label="类型">{{ viewData.type }}</el-descriptions-item>
<el-descriptions-item label="标题">{{ viewData.title }}</el-descriptions-item>
<el-descriptions-item label="内容">{{ viewData.content }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusType(viewData.status)">{{ viewData.status }}</el-tag>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<div v-if="viewData.status === '待审批'">
<el-button type="danger" @click="handleReject">拒绝</el-button>
<el-button type="success" @click="handleApprove">通过</el-button>
</div>
<el-button v-else @click="dialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog v-model="rejectDialogVisible" title="拒绝原因" width="400px">
<el-input v-model="rejectReason" type="textarea" rows="4" placeholder="请输入拒绝理由" />
<template #footer>
<el-button @click="rejectDialogVisible = false">取消</el-button>
<el-button type="danger" @click="confirmReject">确认</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
const searchForm = ref({ type: '', status: '' })
const tableData = ref([])
const dialogVisible = ref(false)
const rejectDialogVisible = ref(false)
const viewData = ref({})
const rejectReason = ref('')
const fetchData = async () => {
const res = await axios.get('http://localhost:3000/api/applications')
if (res.data.success) tableData.value = res.data.data
}
const handleSearch = () => {
let data = [...tableData.value]
if (searchForm.value.type) data = data.filter(a => a.type === searchForm.value.type)
if (searchForm.value.status) data = data.filter(a => a.status === searchForm.value.status)
tableData.value = data
}
const handleReset = () => { searchForm.value = { type: '', status: '' }; fetchData() }
const handleView = (row) => { viewData.value = row; dialogVisible.value = true }
const handleApprove = async () => {
await axios.put(`http://localhost:3000/api/applications/${viewData.value._id}`, { status: '已通过' })
ElMessage.success('已通过')
dialogVisible.value = false
fetchData()
}
const handleReject = () => { rejectDialogVisible.value = true }
const confirmReject = async () => {
if (!rejectReason.value.trim()) { ElMessage.warning('请填写拒绝理由'); return }
await axios.put(`http://localhost:3000/api/applications/${viewData.value._id}`, { status: '已拒绝', rejectReason: rejectReason.value })
ElMessage.info('已拒绝')
rejectDialogVisible.value = false
rejectReason.value = ''
dialogVisible.value = false
fetchData()
}
const getStatusType = (status) => ({ '待审批': 'warning', '已通过': 'success', '已拒绝': 'danger' }[status] || 'info')
const formatDate = (d) => d ? new Date(d).toLocaleDateString('zh-CN') : '-'
onMounted(() => { fetchData() })
</script>
<style scoped>
.search-card { margin-bottom: 20px; }
</style>

178
src/views/Approvals.vue Normal file
View File

@ -0,0 +1,178 @@
<template>
<div class="page">
<el-card class="search-card">
<el-form :inline="true" :model="searchForm">
<el-form-item label="审批类型">
<select v-model="searchForm.type" style="width: 150px; height: 36px; border: 1px solid #dcdfe6; border-radius: 4px; padding: 0 10px;">
<option value="">全部</option>
<option value="订单退款">订单退款</option>
<option value="车辆报废">车辆报废</option>
<option value="押金退还">押金退还</option>
<option value="价格修改">价格修改</option>
<option value="其他">其他</option>
</select>
</el-form-item>
<el-form-item label="状态">
<select v-model="searchForm.status" style="width: 150px; height: 36px; border: 1px solid #dcdfe6; border-radius: 4px; padding: 0 10px;">
<option value="">全部</option>
<option value="待审批">待审批</option>
<option value="已通过">已通过</option>
<option value="已拒绝">已拒绝</option>
</select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<div class="toolbar">
<el-button type="primary" @click="handleAdd">+ 创建审批</el-button>
</div>
<el-card>
<el-table :data="tableData" border stripe>
<el-table-column prop="approvalId" label="审批编号" width="140" />
<el-table-column prop="type" label="类型" width="100" />
<el-table-column prop="title" label="标题" />
<el-table-column prop="applicant" label="申请人" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="approver" label="审批人" width="100" />
<el-table-column prop="createdAt" label="创建时间" width="120">
<template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button v-if="row.status === '待审批'" size="small" type="success" @click="handleApprove(row)">通过</el-button>
<el-button v-if="row.status === '待审批'" size="small" type="danger" @click="handleReject(row)">拒绝</el-button>
<el-button size="small" @click="handleEdit(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
<el-form-item label="类型" prop="type">
<el-select v-model="form.type">
<el-option label="订单退款" value="订单退款" />
<el-option label="车辆报废" value="车辆报废" />
<el-option label="押金退还" value="押金退还" />
<el-option label="价格修改" value="价格修改" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="内容">
<el-input v-model="form.content" type="textarea" rows="3" />
</el-form-item>
<el-form-item label="申请人">
<el-input v-model="form.applicant" />
</el-form-item>
<el-form-item label="审批人">
<el-input v-model="form.approver" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" rows="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
const searchForm = ref({ type: '', status: '' })
const tableData = ref([])
const dialogVisible = ref(false)
const dialogTitle = ref('创建审批')
const formRef = ref(null)
const form = ref({ _id: '', type: '', title: '', content: '', applicant: '', approver: '', remark: '', status: '待审批' })
const rules = { type: [{ required: true, message: '请选择类型', trigger: 'change' }], title: [{ required: true, message: '请输入标题', trigger: 'blur' }] }
const fetchData = async () => {
const res = await axios.get('http://localhost:3000/api/approvals')
if (res.data.success) tableData.value = res.data.data
}
const handleSearch = () => {
let data = [...tableData.value]
if (searchForm.value.type) data = data.filter(a => a.type === searchForm.value.type)
if (searchForm.value.status) data = data.filter(a => a.status === searchForm.value.status)
tableData.value = data
}
const handleReset = () => { searchForm.value = { type: '', status: '' }; fetchData() }
const handleAdd = () => { dialogTitle.value = '创建审批'; form.value = { _id: '', type: '', title: '', content: '', applicant: '', approver: '', remark: '', status: '待审批' }; dialogVisible.value = true }
const handleEdit = (row) => { dialogTitle.value = '查看审批'; form.value = { ...row }; dialogVisible.value = true }
const handleApprove = async (row) => {
await axios.put(`http://localhost:3000/api/approvals/${row._id}`, { status: '已通过', approver: '管理员' })
ElMessage.success('审批通过'); fetchData()
}
const handleReject = async (row) => {
await axios.put(`http://localhost:3000/api/approvals/${row._id}`, { status: '已拒绝', approver: '管理员' })
ElMessage.info('已拒绝'); fetchData()
}
const handleSubmit = async () => {
await formRef.value.validate()
if (form.value._id) {
await axios.put(`http://localhost:3000/api/approvals/${form.value._id}`, form.value)
ElMessage.success('修改成功')
} else {
await axios.post('http://localhost:3000/api/approvals', form.value)
ElMessage.success('创建成功')
}
dialogVisible.value = false; fetchData()
}
const getStatusType = (status) => ({ '待审批': 'warning', '已通过': 'success', '已拒绝': 'danger' }[status] || 'info')
const formatDate = (d) => d ? new Date(d).toLocaleDateString('zh-CN') : '-'
onMounted(() => { fetchData() })
</script>
<style scoped>
.search-card {
margin-bottom: 20px;
}
.search-card :deep(.el-select) {
min-width: 200px;
}
.search-card :deep(.el-select .el-input) {
width: 200px;
}
.search-card :deep(.el-select-dropdown__item) {
padding: 8px 16px;
}
.search-card :deep(.el-select__wrapper) {
background-color: #fff !important;
min-height: 40px !important;
width: 200px !important;
}
.search-card :deep(.el-select__placeholder) {
color: #999 !important;
display: block !important;
}
.search-card :deep(.el-select__selected-item) {
color: #333 !important;
display: block !important;
line-height: 24px !important;
}
.search-card :deep(.el-tag) {
background-color: #f0f0f0 !important;
color: #333 !important;
height: 28px !important;
line-height: 28px !important;
}
.toolbar { margin-bottom: 20px; }
</style>

144
src/views/Complaints.vue Normal file
View File

@ -0,0 +1,144 @@
<template>
<div class="page">
<el-card class="search-card">
<el-form :inline="true" :model="searchForm">
<el-form-item label="投诉类型">
<el-select v-model="searchForm.type" clearable>
<el-option label="门店服务态度差" value="门店服务态度差" />
<el-option label="车辆配置/质量问题" value="车辆配置/质量问题" />
<el-option label="费用/分成问题" value="费用/分成问题" />
<el-option label="订单分配不公" value="订单分配不公" />
<el-option label="门店管理问题" value="门店管理问题" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" clearable>
<el-option label="待处理" value="待处理" />
<el-option label="处理中" value="处理中" />
<el-option label="已解决" value="已解决" />
<el-option label="已关闭" value="已关闭" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<el-table :data="tableData" border stripe>
<el-table-column prop="complaintId" label="投诉编号" width="140" />
<el-table-column label="客户" width="100">
<template #default="{ row }">{{ row.customer?.name || '-' }}</template>
</el-table-column>
<el-table-column prop="type" label="类型" width="100" />
<el-table-column prop="content" label="内容" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="handler" label="处理人" width="100" />
<el-table-column prop="createdAt" label="创建时间" width="120">
<template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template #default="{ row }">
<el-button size="small" type="primary" @click="handleEdit(row)">处理</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
<el-form-item label="类型" prop="type">
<el-select v-model="form.type">
<el-option label="门店服务态度差" value="门店服务态度差" />
<el-option label="车辆配置/质量问题" value="车辆配置/质量问题" />
<el-option label="费用/分成问题" value="费用/分成问题" />
<el-option label="订单分配不公" value="订单分配不公" />
<el-option label="门店管理问题" value="门店管理问题" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="内容" prop="content">
<el-input v-model="form.content" type="textarea" rows="3" />
</el-form-item>
<el-form-item label="处理人">
<el-input v-model="form.handler" />
</el-form-item>
<el-form-item label="回复">
<el-input v-model="form.response" type="textarea" rows="2" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="form.status">
<el-option label="待处理" value="待处理" />
<el-option label="处理中" value="处理中" />
<el-option label="已解决" value="已解决" />
<el-option label="已关闭" value="已关闭" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
const searchForm = ref({ type: '', status: '' })
const tableData = ref([])
const dialogVisible = ref(false)
const dialogTitle = ref('添加投诉')
const formRef = ref(null)
const form = ref({ _id: '', type: '', content: '', handler: '', response: '', status: '待处理' })
const rules = { type: [{ required: true, message: '请选择类型', trigger: 'change' }], content: [{ required: true, message: '请输入内容', trigger: 'blur' }] }
const fetchData = async () => {
const res = await axios.get('http://localhost:3000/api/complaints')
if (res.data.success) tableData.value = res.data.data
}
const handleSearch = () => {
let data = [...tableData.value]
if (searchForm.value.type) data = data.filter(c => c.type === searchForm.value.type)
if (searchForm.value.status) data = data.filter(c => c.status === searchForm.value.status)
tableData.value = data
}
const handleReset = () => { searchForm.value = { type: '', status: '' }; fetchData() }
const handleAdd = () => { dialogTitle.value = '添加投诉'; form.value = { _id: '', type: '', content: '', handler: '', response: '', status: '待处理' }; dialogVisible.value = true }
const handleEdit = (row) => { dialogTitle.value = '处理投诉'; form.value = { ...row }; dialogVisible.value = true }
const handleSubmit = async () => {
await formRef.value.validate()
if (form.value._id) {
await axios.put(`http://localhost:3000/api/complaints/${form.value._id}`, form.value)
ElMessage.success('处理成功')
} else {
await axios.post('http://localhost:3000/api/complaints', form.value)
ElMessage.success('添加成功')
}
dialogVisible.value = false; fetchData()
}
const handleDelete = async (row) => {
await ElMessageBox.confirm('确定删除?', '提示', { type: 'warning' })
await axios.delete(`http://localhost:3000/api/complaints/${row._id}`)
ElMessage.success('删除成功'); fetchData()
}
const getStatusType = (status) => ({ '待处理': 'danger', '处理中': 'warning', '已解决': 'success', '已关闭': 'info' }[status] || 'info')
const formatDate = (d) => d ? new Date(d).toLocaleDateString('zh-CN') : '-'
onMounted(() => { fetchData() })
</script>
<style scoped>
.search-card { margin-bottom: 20px; }
.toolbar { margin-bottom: 20px; }
</style>

153
src/views/Conflicts.vue Normal file
View File

@ -0,0 +1,153 @@
<template>
<div class="page">
<el-card class="search-card">
<el-form :inline="true" :model="searchForm">
<el-form-item label="矛盾类型">
<el-select v-model="searchForm.type" clearable>
<el-option label="骑手门店" value="骑手门店" />
<el-option label="门店公司" value="门店公司" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" clearable>
<el-option label="待处理" value="待处理" />
<el-option label="处理中" value="处理中" />
<el-option label="已解决" value="已解决" />
<el-option label="已关闭" value="已关闭" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<div class="toolbar">
<el-button type="primary" @click="handleAdd">+ 登记矛盾</el-button>
</div>
<el-card>
<el-table :data="tableData" border stripe>
<el-table-column prop="conflictId" label="矛盾编号" width="140" />
<el-table-column prop="type" label="类型" width="100">
<template #default="{ row }">
<el-tag :type="row.type === '骑手门店' ? 'warning' : 'success'">{{ row.type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" />
<el-table-column label="当事方" width="180">
<template #default="{ row }">{{ row.partyA }} {{ row.partyB }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="handler" label="处理人" width="100" />
<el-table-column prop="createdAt" label="创建时间" width="120">
<template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template #default="{ row }">
<el-button size="small" type="primary" @click="handleEdit(row)">处理</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="550px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
<el-form-item label="类型" prop="type">
<el-select v-model="form.type">
<el-option label="骑手门店" value="骑手门店" />
<el-option label="门店公司" value="门店公司" />
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入矛盾标题" />
</el-form-item>
<el-form-item label="当事方A">
<el-input v-model="form.partyA" placeholder="骑手姓名或门店名称" />
</el-form-item>
<el-form-item label="当事方B">
<el-input v-model="form.partyB" placeholder="门店名称或公司" />
</el-form-item>
<el-form-item label="详情">
<el-input v-model="form.content" type="textarea" rows="3" placeholder="矛盾详情..." />
</el-form-item>
<el-form-item label="处理人">
<el-input v-model="form.handler" />
</el-form-item>
<el-form-item label="处理结果">
<el-input v-model="form.result" type="textarea" rows="2" placeholder="处理结果..." />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="form.status">
<el-option label="待处理" value="待处理" />
<el-option label="处理中" value="处理中" />
<el-option label="已解决" value="已解决" />
<el-option label="已关闭" value="已关闭" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
const searchForm = ref({ type: '', status: '' })
const tableData = ref([])
const dialogVisible = ref(false)
const dialogTitle = ref('登记矛盾')
const formRef = ref(null)
const form = ref({ _id: '', type: '', title: '', partyA: '', partyB: '', content: '', handler: '', result: '', status: '待处理' })
const rules = { type: [{ required: true, message: '请选择类型', trigger: 'change' }], title: [{ required: true, message: '请输入标题', trigger: 'blur' }] }
const fetchData = async () => {
const res = await axios.get('http://localhost:3000/api/conflicts')
if (res.data.success) tableData.value = res.data.data
}
const handleSearch = () => {
let data = [...tableData.value]
if (searchForm.value.type) data = data.filter(c => c.type === searchForm.value.type)
if (searchForm.value.status) data = data.filter(c => c.status === searchForm.value.status)
tableData.value = data
}
const handleReset = () => { searchForm.value = { type: '', status: '' }; fetchData() }
const handleAdd = () => { dialogTitle.value = '登记矛盾'; form.value = { _id: '', type: '', title: '', partyA: '', partyB: '', content: '', handler: '', result: '', status: '待处理' }; dialogVisible.value = true }
const handleEdit = (row) => { dialogTitle.value = '处理矛盾'; form.value = { ...row }; dialogVisible.value = true }
const handleSubmit = async () => {
await formRef.value.validate()
if (form.value._id) {
await axios.put(`http://localhost:3000/api/conflicts/${form.value._id}`, form.value)
ElMessage.success('修改成功')
} else {
await axios.post('http://localhost:3000/api/conflicts', form.value)
ElMessage.success('登记成功')
}
dialogVisible.value = false; fetchData()
}
const handleDelete = async (row) => {
await ElMessageBox.confirm('确定删除?', '提示', { type: 'warning' })
await axios.delete(`http://localhost:3000/api/conflicts/${row._id}`)
ElMessage.success('删除成功'); fetchData()
}
const getStatusType = (status) => ({ '待处理': 'danger', '处理中': 'warning', '已解决': 'success', '已关闭': 'info' }[status] || 'info')
const formatDate = (d) => d ? new Date(d).toLocaleDateString('zh-CN') : '-'
onMounted(() => { fetchData() })
</script>
<style scoped>
.search-card { margin-bottom: 20px; }
.toolbar { margin-bottom: 20px; }
</style>

187
src/views/Customers.vue Normal file
View File

@ -0,0 +1,187 @@
<template>
<div class="customers-page">
<el-card class="search-card">
<el-form :inline="true" :model="searchForm">
<el-form-item label="客户姓名">
<el-input v-model="searchForm.name" placeholder="请输入姓名" clearable />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="searchForm.phone" placeholder="请输入手机号" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<div class="toolbar">
<el-button type="primary" @click="handleAdd">+ 添加客户</el-button>
<el-button @click="fetchCustomers">🔄 刷新</el-button>
</div>
<el-card>
<el-table :data="tableData" border stripe>
<el-table-column prop="customerId" label="客户编号" width="140" />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="phone" label="手机号" width="120" />
<el-table-column prop="idCard" label="身份证号" width="180" />
<el-table-column prop="address" label="地址" />
<el-table-column prop="creditScore" label="信用分" width="80" />
<el-table-column prop="creditLevel" label="信用等级" width="100">
<template #default="{ row }">
<el-tag :type="getCreditType(row.creditLevel)">{{ row.creditLevel }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="150">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 添加/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="form.idCard" placeholder="请输入身份证号" />
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="form.address" placeholder="请输入地址" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="信用等级" prop="creditLevel">
<el-select v-model="form.creditLevel" placeholder="请选择信用等级">
<el-option label="优秀" value="优秀" />
<el-option label="良好" value="良好" />
<el-option label="一般" value="一般" />
<el-option label="较差" value="较差" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="notes">
<el-input v-model="form.notes" type="textarea" rows="3" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
const searchForm = ref({ name: '', phone: '' })
const tableData = ref([])
const dialogVisible = ref(false)
const dialogTitle = ref('添加客户')
const formRef = ref(null)
const form = ref({
_id: '',
customerId: '',
name: '',
phone: '',
idCard: '',
address: '',
email: '',
creditScore: 100,
creditLevel: '优秀',
notes: ''
})
const rules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }]
}
const fetchCustomers = async () => {
try {
const res = await axios.get('http://localhost:3000/api/customers')
if (res.data.success) {
tableData.value = res.data.data
}
} catch (error) {
ElMessage.error('获取客户列表失败')
}
}
const handleSearch = () => {
let data = tableData.value
if (searchForm.value.name) {
data = data.filter(c => c.name.includes(searchForm.value.name))
}
if (searchForm.value.phone) {
data = data.filter(c => c.phone.includes(searchForm.value.phone))
}
tableData.value = data
}
const handleReset = () => {
searchForm.value = { name: '', phone: '' }
fetchCustomers()
}
const handleAdd = () => {
dialogTitle.value = '添加客户'
form.value = {
_id: '', customerId: '', name: '', phone: '', idCard: '',
address: '', email: '', creditScore: 100, creditLevel: '优秀', notes: ''
}
dialogVisible.value = true
}
const handleEdit = (row) => {
dialogTitle.value = '编辑客户'
form.value = { ...row }
dialogVisible.value = true
}
const handleSubmit = async () => {
try {
await formRef.value.validate()
if (form.value._id) {
const res = await axios.put(`http://localhost:3000/api/customers/${form.value._id}`, form.value)
if (res.data.success) { ElMessage.success('修改成功') }
} else {
const res = await axios.post('http://localhost:3000/api/customers', form.value)
if (res.data.success) { ElMessage.success('添加成功') }
}
dialogVisible.value = false
fetchCustomers()
} catch { ElMessage.error('操作失败') }
}
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除这个客户吗?', '提示', { type: 'warning' })
const res = await axios.delete(`http://localhost:3000/api/customers/${row._id}`)
if (res.data.success) { ElMessage.success('删除成功'); fetchCustomers() }
} catch {}
}
const getCreditType = (level) => {
const types = { '优秀': 'success', '良好': 'primary', '一般': 'warning', '较差': 'danger' }
return types[level] || 'info'
}
onMounted(() => { fetchCustomers() })
</script>
<style scoped>
.search-card { margin-bottom: 20px; }
.toolbar { margin-bottom: 20px; display: flex; gap: 10px; }
</style>

280
src/views/Dashboard.vue Normal file
View File

@ -0,0 +1,280 @@
<template>
<div class="dashboard">
<!-- 车辆统计卡片 -->
<el-row :gutter="20" class="stats-row">
<el-col :span="6">
<div class="stat-card">
<div class="stat-icon" style="background: #e6f7ff;">
<span style="color: #1890ff; font-size: 24px;">📦</span>
</div>
<div class="stat-content">
<div class="stat-value">{{ stats.totalVehicles }}</div>
<div class="stat-label">车辆总数</div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<div class="stat-icon" style="background: #f6ffed;">
<span style="color: #52c41a; font-size: 24px;">🚗</span>
</div>
<div class="stat-content">
<div class="stat-value">{{ stats.rentedVehicles }}</div>
<div class="stat-label">出租中</div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<div class="stat-icon" style="background: #fff7e6;">
<span style="color: #fa8c16; font-size: 24px;"></span>
</div>
<div class="stat-content">
<div class="stat-value">{{ stats.availableVehicles }}</div>
<div class="stat-label">空闲车辆</div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<div class="stat-icon" style="background: #fff1f0;">
<span style="color: #f5222d; font-size: 24px;">🔧</span>
</div>
<div class="stat-content">
<div class="stat-value">{{ stats.maintenanceVehicles }}</div>
<div class="stat-label">维修中</div>
</div>
</div>
</el-col>
</el-row>
<!-- 财务统计 - 收支明细模式 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="6">
<el-card class="finance-card income">
<template #header><span>💰 总收入</span></template>
<div class="finance-value">¥{{ financeStats.totalIncome.toLocaleString() }}</div>
<div class="finance-detail">收入 {{ financeStats.incomeCount }} </div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="finance-card expense">
<template #header><span>💸 总支出</span></template>
<div class="finance-value">¥{{ financeStats.totalExpense.toLocaleString() }}</div>
<div class="finance-detail">支出 {{ financeStats.expenseCount }} </div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="finance-card balance" :class="{ positive: financeStats.balance >= 0, negative: financeStats.balance < 0 }">
<template #header><span>📊 结余</span></template>
<div class="finance-value">¥{{ financeStats.balance.toLocaleString() }}</div>
<div class="finance-detail">{{ financeStats.balance >= 0 ? '盈利' : '亏损' }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="finance-card">
<template #header><span>📋 收支记录</span></template>
<div class="finance-value">{{ financeStats.totalCount }}</div>
<div class="finance-detail">总收入 {{ financeStats.totalCount }} </div>
</el-card>
</el-col>
</el-row>
<!-- 图表 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="12">
<el-card>
<template #header><span>📊 收支分布</span></template>
<div ref="pieChartRef" style="width: 100%; height: 300px;"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<template #header><span>📈 收支对比</span></template>
<div ref="barChartRef" style="width: 100%; height: 300px;"></div>
</el-card>
</el-col>
</el-row>
<!-- 最近收支明细 -->
<el-card style="margin-top: 20px;">
<template #header>
<span>📋 最近收支明细</span>
</template>
<el-table :data="recentPayments" stripe size="small">
<el-table-column label="类型" width="80">
<template #default="{ row }">
<el-tag :type="row.type === 'income' ? 'success' : 'danger'" size="small">
{{ row.type === 'income' ? '收入' : '支出' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="party" label="对方" width="120" />
<el-table-column prop="amount" label="金额" width="100">
<template #default="{ row }">
<span :style="{ color: row.type === 'income' ? '#52c41a' : '#f5222d', fontWeight: 'bold' }">
{{ row.type === 'income' ? '+' : '-' }}¥{{ row.amount }}
</span>
</template>
</el-table-column>
<el-table-column prop="method" label="方式" width="80" />
<el-table-column prop="category" label="分类" width="100" />
<el-table-column prop="remark" label="备注" />
<el-table-column prop="createdAt" label="时间" width="170">
<template #default="{ row }">
{{ new Date(row.createdAt).toLocaleString() }}
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 快捷操作 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="24">
<el-card>
<template #header><span> 快捷操作</span></template>
<el-button type="primary" @click="$router.push('/finance')">💰 财务管理</el-button>
<el-button type="success" @click="$router.push('/vehicles')">🚗 车辆管理</el-button>
<el-button type="warning" @click="$router.push('/orders')">📋 订单管理</el-button>
<el-button type="info" @click="$router.push('/customers')">👤 客户管理</el-button>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
const stats = ref({
totalVehicles: 0,
rentedVehicles: 0,
availableVehicles: 0,
maintenanceVehicles: 0,
todayIncome: 0,
monthIncome: 0,
totalIncome: 0,
pendingPayment: 0
})
const financeStats = ref({
totalIncome: 0,
totalExpense: 0,
balance: 0,
incomeCount: 0,
expenseCount: 0,
totalCount: 0
})
const recentPayments = ref([])
const pieChartRef = ref(null)
const barChartRef = ref(null)
const loadData = async () => {
try {
//
const vehicleRes = await fetch('http://localhost:3000/api/vehicles')
const vehicleData = await vehicleRes.json()
if (vehicleData.success) {
const vehicles = vehicleData.data
stats.value.totalVehicles = vehicles.length
stats.value.availableVehicles = vehicles.filter(v => v.status === '空闲').length
stats.value.rentedVehicles = vehicles.filter(v => v.status === '出租中' || v.status === '在租').length
stats.value.maintenanceVehicles = vehicles.filter(v => v.status === '维修中').length
}
//
const financeRes = await fetch('http://localhost:3000/api/finance')
const financeData = await financeRes.json()
if (financeData.success) {
const list = financeData.data.list || []
const summary = financeData.data.summary || {}
financeStats.value.totalIncome = summary.totalIncome || 0
financeStats.value.totalExpense = summary.totalExpense || 0
financeStats.value.balance = summary.balance || 0
financeStats.value.totalCount = list.length
financeStats.value.incomeCount = list.filter(p => p.type === 'income').length
financeStats.value.expenseCount = list.filter(p => p.type === 'expense').length
// 5
recentPayments.value = list.slice(0, 5)
}
nextTick(() => {
initCharts()
})
} catch (e) {
console.error('加载失败:', e)
}
}
const initCharts = () => {
if (pieChartRef.value) {
const pieChart = echarts.init(pieChartRef.value)
pieChart.setOption({
tooltip: { trigger: 'item' },
series: [{
type: 'pie',
radius: ['40%', '70%'],
data: [
{ value: financeStats.value.totalIncome, name: '收入', itemStyle: { color: '#52c41a' } },
{ value: financeStats.value.totalExpense, name: '支出', itemStyle: { color: '#f5222d' } }
]
}]
})
}
if (barChartRef.value) {
const barChart = echarts.init(barChartRef.value)
barChart.setOption({
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: ['收入', '支出', '结余'] },
yAxis: { type: 'value' },
series: [{
type: 'bar',
data: [
financeStats.value.totalIncome,
financeStats.value.totalExpense,
financeStats.value.balance
],
itemStyle: {
color: (params) => ['#52c41a', '#f5222d', financeStats.value.balance >= 0 ? '#1890ff' : '#f5222d'][params.dataIndex]
}
}]
})
}
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.dashboard { padding: 20px; }
.stats-row { margin-bottom: 20px; }
.stat-card {
display: flex;
align-items: center;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.stat-icon { margin-right: 15px; }
.stat-value { font-size: 28px; font-weight: bold; color: #333; }
.stat-label { color: #999; font-size: 14px; }
.finance-card { text-align: center; padding: 10px; }
.finance-card.income { border-top: 3px solid #52c41a; }
.finance-card.expense { border-top: 3px solid #f5222d; }
.finance-card.balance { border-top: 3px solid #1890ff; }
.finance-card.balance.negative { border-top-color: #f5222d; }
.finance-value { font-size: 24px; font-weight: bold; margin: 10px 0; }
.finance-card.income .finance-value { color: #52c41a; }
.finance-card.expense .finance-value { color: #f5222d; }
.finance-card.balance .finance-value { color: #1890ff; }
.finance-card.balance.negative .finance-value { color: #f5222d; }
.finance-detail { color: #999; font-size: 12px; }
</style>

110
src/views/Disputes.vue Normal file
View File

@ -0,0 +1,110 @@
<template>
<div class="page">
<el-card class="search-card">
<el-form :inline="true" :model="searchForm">
<el-form-item label="纠纷类型">
<el-select v-model="searchForm.type" clearable>
<el-option label="订单纠纷" value="订单纠纷" />
<el-option label="区域纠纷" value="区域纠纷" />
<el-option label="费用纠纷" value="费用纠纷" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="处理状态">
<el-select v-model="searchForm.status" clearable>
<el-option label="待处理" value="待处理" />
<el-option label="处理中" value="处理中" />
<el-option label="已解决" value="已解决" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<el-table :data="tableData" border stripe>
<el-table-column prop="disputeId" label="纠纷编号" width="150" />
<el-table-column label="当事门店" width="180">
<template #default="{ row }">{{ row.storeAName }} {{ row.storeBName }}</template>
</el-table-column>
<el-table-column prop="type" label="类型" width="100" />
<el-table-column prop="title" label="标题" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="120">
<template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button size="small" @click="handleView(row)">处理</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" title="纠纷处理" width="600px">
<el-descriptions :column="1" border>
<el-descriptions-item label="纠纷编号">{{ viewData.disputeId }}</el-descriptions-item>
<el-descriptions-item label="当事门店">{{ viewData.storeAName }} {{ viewData.storeBName }}</el-descriptions-item>
<el-descriptions-item label="类型">{{ viewData.type }}</el-descriptions-item>
<el-descriptions-item label="标题">{{ viewData.title }}</el-descriptions-item>
<el-descriptions-item label="详情">{{ viewData.content }}</el-descriptions-item>
<el-descriptions-item label="处理结果">{{ viewData.result || '待处理' }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusType(viewData.status)">{{ viewData.status }}</el-tag>
</el-descriptions-item>
</el-descriptions>
<div v-if="viewData.status !== '已解决'" style="margin-top: 20px;">
<el-input v-model="result" type="textarea" rows="3" placeholder="填写处理结果..." />
</div>
<template #footer>
<el-button @click="dialogVisible = false">关闭</el-button>
<el-button v-if="viewData.status !== '已解决'" type="success" @click="handleResolve">标记已解决</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
const searchForm = ref({ type: '', status: '' })
const tableData = ref([])
const dialogVisible = ref(false)
const viewData = ref({})
const result = ref('')
const fetchData = async () => {
const res = await axios.get('http://localhost:3000/api/disputes')
if (res.data.success) tableData.value = res.data.data
}
const handleSearch = () => {
let data = [...tableData.value]
if (searchForm.value.type) data = data.filter(d => d.type === searchForm.value.type)
if (searchForm.value.status) data = data.filter(d => d.status === searchForm.value.status)
tableData.value = data
}
const handleReset = () => { searchForm.value = { type: '', status: '' }; fetchData() }
const handleView = (row) => { viewData.value = row; result.value = row.result || ''; dialogVisible.value = true }
const handleResolve = async () => {
await axios.put(`http://localhost:3000/api/disputes/${viewData.value._id}`, { status: '已解决', result: result.value })
ElMessage.success('已标记为解决')
dialogVisible.value = false
fetchData()
}
const getStatusType = (status) => ({ '待处理': 'danger', '处理中': 'warning', '已解决': 'success' }[status] || 'info')
const formatDate = (d) => d ? new Date(d).toLocaleDateString('zh-CN') : '-'
onMounted(() => { fetchData() })
</script>
<style scoped>
.search-card { margin-bottom: 20px; }
</style>

160
src/views/Finance.vue Normal file
View File

@ -0,0 +1,160 @@
<template>
<div class="finance-page">
<!-- 收支统计卡片 -->
<el-row :gutter="20">
<el-col :span="8">
<el-card class="stat-card">
<div class="stat-icon" style="background: #f6ffed;">💰</div>
<div class="stat-value" style="color: #52c41a;">¥{{ stats.totalIncome.toLocaleString() }}</div>
<div class="stat-label">总收入</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card class="stat-card">
<div class="stat-icon" style="background: #fff1f0;">💸</div>
<div class="stat-value" style="color: #f5222d;">¥{{ stats.totalExpense.toLocaleString() }}</div>
<div class="stat-label">总支出</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card class="stat-card">
<div class="stat-icon" style="background: #e6f7ff;">📊</div>
<div class="stat-value" style="color: #1890ff;">¥{{ stats.balance.toLocaleString() }}</div>
<div class="stat-label">结余</div>
</el-card>
</el-col>
</el-row>
<!-- 图表 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="12">
<el-card>
<template #header><span>📊 收支分布</span></template>
<div ref="pieChartRef" style="width: 100%; height: 300px;"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<template #header><span>📈 月度趋势</span></template>
<div ref="barChartRef" style="width: 100%; height: 300px;"></div>
</el-card>
</el-col>
</el-row>
<!-- 收支明细 -->
<el-card style="margin-top: 20px;">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>📋 收支明细</span>
<el-button type="primary" size="small" @click="refresh">🔄 刷新</el-button>
</div>
</template>
<el-table :data="payments" border stripe>
<el-table-column label="类型" width="80">
<template #default="{ row }">
<el-tag :type="row.type === 'income' ? 'success' : 'danger'">
{{ row.type === 'income' ? '收入' : '支出' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="party" label="对方" width="150" />
<el-table-column prop="amount" label="金额" width="100">
<template #default="{ row }">
<span :style="{ color: row.type === 'income' ? '#52c41a' : '#f5222d', fontWeight: 'bold' }">
{{ row.type === 'income' ? '+' : '-' }}¥{{ row.amount }}
</span>
</template>
</el-table-column>
<el-table-column prop="method" label="方式" width="100" />
<el-table-column prop="category" label="分类" width="120" />
<el-table-column prop="remark" label="备注" />
<el-table-column prop="createdAt" label="时间" width="170">
<template #default="{ row }">
{{ new Date(row.createdAt).toLocaleString() }}
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { ElMessage } from 'element-plus'
const payments = ref([])
const stats = ref({
totalIncome: 0,
totalExpense: 0,
balance: 0
})
const pieChartRef = ref(null)
const barChartRef = ref(null)
const refresh = async () => {
try {
const res = await fetch('http://localhost:3000/api/finance')
const data = await res.json()
if (data.success) {
payments.value = data.data.list
stats.value = data.data.summary
ElMessage.success('刷新成功')
nextTick(() => {
initCharts()
})
}
} catch (e) {
ElMessage.error('加载失败: ' + e.message)
}
}
const initCharts = () => {
if (pieChartRef.value) {
const pieChart = echarts.init(pieChartRef.value)
pieChart.setOption({
tooltip: { trigger: 'item' },
series: [{
type: 'pie',
radius: ['40%', '70%'],
data: [
{ value: stats.value.totalIncome, name: '收入', itemStyle: { color: '#52c41a' } },
{ value: stats.value.totalExpense, name: '支出', itemStyle: { color: '#f5222d' } }
]
}]
})
}
if (barChartRef.value) {
const barChart = echarts.init(barChartRef.value)
barChart.setOption({
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: ['收入', '支出', '结余'] },
yAxis: { type: 'value' },
series: [{
type: 'bar',
data: [
stats.value.totalIncome,
stats.value.totalExpense,
stats.value.balance
],
itemStyle: {
color: (params) => ['#52c41a', '#f5222d', '#1890ff'][params.dataIndex]
}
}]
})
}
}
onMounted(() => {
refresh()
})
</script>
<style scoped>
.finance-page { padding: 20px; }
.stat-card { text-align: center; padding: 20px; }
.stat-icon { font-size: 32px; margin-bottom: 10px; }
.stat-value { font-size: 28px; font-weight: bold; }
.stat-label { color: #666; margin-top: 5px; }
</style>

222
src/views/Layout.vue Normal file
View File

@ -0,0 +1,222 @@
<template>
<el-container class="layout-container">
<!-- 左侧菜单 -->
<el-aside width="220px">
<div class="logo">
<span>🦞 租车系统</span>
</div>
<el-menu
:default-active="activeMenu"
class="side-menu"
background-color="#1a1a2e"
text-color="#fff"
active-text-color="#409EFF"
router
>
<el-menu-item index="/">
<el-icon><DataBoard /></el-icon>
<span>数据看板</span>
</el-menu-item>
<el-menu-item index="/finance">
<el-icon><Money /></el-icon>
<span>财务管理</span>
</el-menu-item>
<el-sub-menu index="/stores-group">
<template #title>
<el-icon><Shop /></el-icon>
<span>门店管理</span>
</template>
<el-menu-item index="/stores">审批</el-menu-item>
</el-sub-menu>
<el-menu-item index="/disputes">
<el-icon><ChatDotRound /></el-icon>
<span>纠纷协调</span>
</el-menu-item>
<el-menu-item index="/complaints">
<el-icon><Warning /></el-icon>
<span>客户投诉</span>
</el-menu-item>
<el-menu-item index="/payments">
<el-icon><Wallet /></el-icon>
<span>打款</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<!-- 顶部导航 -->
<el-header>
<div class="header-left">
<span class="page-title">{{ pageTitle }}</span>
</div>
<div class="header-right">
<el-badge :value="notificationCount" :hidden="notificationCount === 0">
<el-button :icon="Bell" circle @click="showNotifications" />
</el-badge>
<el-dropdown @command="handleCommand">
<span class="user-info">
👤 管理员
<el-icon><arrow-down /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<!-- 主内容区 -->
<el-main>
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import { DataBoard, Money, Bell, Shop, Service, Wallet, ArrowDown, Warning, ChatDotRound, Document } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const route = useRoute()
const activeMenu = computed(() => route.path)
const notificationCount = ref(2)
const pageTitle = computed(() => {
const titles = {
'/': '数据看板',
'/finance': '财务管理',
'/stores': '门店审批',
'/applications': '门店申请',
'/disputes': '纠纷协调',
'/complaints': '客户投诉',
'/payments': '打款'
}
return titles[route.path] || '租车系统'
})
const showNotifications = () => {
ElMessage.info('您有 2 条订单逾期提醒')
}
const handleCommand = (command) => {
if (command === 'logout') {
localStorage.removeItem('isLoggedIn')
window.location.href = '/login'
}
}
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', sans-serif;
}
.layout-container {
height: 100vh;
}
.el-aside {
background-color: #1a1a2e;
}
.logo {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
font-weight: bold;
border-bottom: 1px solid #2a2a4e;
}
.side-menu {
border-right: none;
}
.side-menu .el-menu-item {
height: 50px;
line-height: 50px;
}
.side-menu .el-menu-item.is-active {
background-color: #409EFF !important;
color: #fff !important;
}
.el-header {
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
}
.header-left {
font-size: 18px;
font-weight: 600;
color: #333;
}
.header-right {
color: #666;
display: flex;
align-items: center;
gap: 12px;
}
.user-info {
cursor: pointer;
padding: 8px 12px;
border-radius: 4px;
display: flex;
align-items: center;
gap: 4px;
transition: background 0.3s;
}
.user-info:hover {
background: #f5f7fa;
}
.el-main {
background-color: #f5f7fa;
padding: 20px;
}
/* 手机端响应式 */
@media (max-width: 768px) {
.el-aside {
width: 60px !important;
}
.logo span {
display: none;
}
.side-menu .el-menu-item span {
display: none;
}
.side-menu .el-menu-item {
padding: 0 10px;
}
.el-header {
padding: 0 10px;
}
.page-title {
font-size: 14px;
}
.el-main {
padding: 10px;
}
}
</style>

79
src/views/Login.vue Normal file
View File

@ -0,0 +1,79 @@
<template>
<div class="login-container">
<el-card class="login-card">
<template #header>
<div class="card-header">
<span class="logo">🦞 租车系统</span>
</div>
</template>
<el-form :model="loginForm" :rules="rules" ref="formRef" label-width="80px">
<el-form-item label="用户名" prop="username">
<el-input v-model="loginForm.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码" show-password />
</el-form-item>
<el-form-item>
<el-button type="primary" style="width: 100%;" @click="handleLogin">登录</el-button>
</el-form-item>
</el-form>
<div class="tips">演示账号: admin / admin</div>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
const router = useRouter()
const formRef = ref(null)
const loginForm = ref({ username: '', password: '' })
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
}
const handleLogin = () => {
formRef.value.validate((valid) => {
if (valid) {
if (loginForm.value.username === 'admin' && loginForm.value.password === 'admin') {
ElMessage.success('登录成功!')
localStorage.setItem('isLoggedIn', 'true')
router.push('/')
} else {
ElMessage.error('用户名或密码错误')
}
}
})
}
</script>
<style scoped>
.login-container {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
}
.login-card {
width: 400px;
}
.card-header {
text-align: center;
}
.logo {
font-size: 24px;
font-weight: bold;
color: #333;
}
.tips {
text-align: center;
color: #999;
font-size: 12px;
margin-top: 10px;
}
</style>

230
src/views/Orders.vue Normal file
View File

@ -0,0 +1,230 @@
<template>
<div class="orders-page">
<el-card class="search-card">
<el-form :inline="true" :model="searchForm">
<el-form-item label="订单编号">
<el-input v-model="searchForm.orderNumber" placeholder="请输入订单编号" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="待支付" value="待支付" />
<el-option label="进行中" value="进行中" />
<el-option label="已完成" value="已完成" />
<el-option label="逾期" value="逾期" />
<el-option label="已取消" value="已取消" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<div class="toolbar">
<el-button type="primary" @click="handleAdd">+ 创建订单</el-button>
<el-button @click="fetchOrders">🔄 刷新</el-button>
</div>
<el-card>
<el-table :data="tableData" border stripe>
<el-table-column prop="orderNumber" label="订单编号" width="120" />
<el-table-column label="客户" width="120">
<template #default="{ row }">{{ row.customer?.name || '-' }}</template>
</el-table-column>
<el-table-column label="车辆" width="120">
<template #default="{ row }">{{ row.vehicle?.model || '-' }}</template>
</el-table-column>
<el-table-column prop="startDate" label="开始日期" width="120">
<template #default="{ row }">{{ formatDate(row.startDate) }}</template>
</el-table-column>
<el-table-column prop="endDate" label="结束日期" width="120">
<template #default="{ row }">{{ formatDate(row.endDate) }}</template>
</el-table-column>
<el-table-column prop="rentalFee" label="租金" width="80">
<template #default="{ row }">¥{{ row.rentalFee }}</template>
</el-table-column>
<el-table-column prop="totalAmount" label="总价" width="100">
<template #default="{ row }">¥{{ row.totalAmount }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="150">
<template #default="{ row }">
<el-button-group>
<el-button size="small" type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
</el-button-group>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 添加/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="客户" prop="customer">
<el-select v-model="form.customer" placeholder="请选择客户" filterable>
<el-option v-for="c in customers" :key="c._id" :label="c.name + ' - ' + c.phone" :value="c._id" />
</el-select>
</el-form-item>
<el-form-item label="车辆" prop="vehicle">
<el-select v-model="form.vehicle" placeholder="请选择车辆" filterable>
<el-option v-for="v in vehicles" :key="v._id" :label="v.vehicleId + ' - ' + v.model" :value="v._id" />
</el-select>
</el-form-item>
<el-form-item label="开始日期" prop="startDate">
<el-date-picker v-model="form.startDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="结束日期" prop="endDate">
<el-date-picker v-model="form.endDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="租金" prop="rentalFee">
<el-input-number v-model="form.rentalFee" :min="0" />
</el-form-item>
<el-form-item label="押金" prop="deposit">
<el-input-number v-model="form.deposit" :min="0" />
</el-form-item>
<el-form-item label="总价" prop="totalAmount">
<el-input-number v-model="form.totalAmount" :min="0" />
</el-form-item>
<el-form-item label="已付金额" prop="paidAmount">
<el-input-number v-model="form.paidAmount" :min="0" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option label="待支付" value="待支付" />
<el-option label="进行中" value="进行中" />
<el-option label="已完成" value="已完成" />
<el-option label="逾期" value="逾期" />
<el-option label="已取消" value="已取消" />
</el-select>
</el-form-item>
<el-form-item label="支付方式" prop="paymentMethod">
<el-select v-model="form.paymentMethod" placeholder="请选择支付方式">
<el-option label="微信" value="微信" />
<el-option label="支付宝" value="支付宝" />
<el-option label="现金" value="现金" />
<el-option label="银行卡" value="银行卡" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="notes">
<el-input v-model="form.notes" type="textarea" rows="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
const searchForm = ref({ orderNumber: '', status: '' })
const tableData = ref([])
const customers = ref([])
const vehicles = ref([])
const dialogVisible = ref(false)
const dialogTitle = ref('创建订单')
const formRef = ref(null)
const form = ref({
_id: '', orderNumber: '', customer: '', vehicle: '',
startDate: '', endDate: '', rentalFee: 0, deposit: 0,
totalAmount: 0, paidAmount: 0, status: '待支付', paymentMethod: '', notes: ''
})
const rules = {
customer: [{ required: true, message: '请选择客户', trigger: 'change' }],
vehicle: [{ required: true, message: '请选择车辆', trigger: 'change' }],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }]
}
const fetchOrders = async () => {
try {
const res = await axios.get('http://localhost:3000/api/orders')
if (res.data.success) tableData.value = res.data.data
} catch { ElMessage.error('获取订单列表失败') }
}
const fetchOptions = async () => {
const [cRes, vRes] = await Promise.all([
axios.get('http://localhost:3000/api/customers'),
axios.get('http://localhost:3000/api/vehicles')
])
if (cRes.data.success) customers.value = cRes.data.data
if (vRes.data.success) vehicles.value = vRes.data.data.filter(v => v.status === '空闲')
}
const handleSearch = () => {
let data = tableData.value
if (searchForm.value.orderNumber) data = data.filter(o => o.orderNumber?.includes(searchForm.value.orderNumber))
if (searchForm.value.status) data = data.filter(o => o.status === searchForm.value.status)
tableData.value = data
}
const handleReset = () => { searchForm.value = { orderNumber: '', status: '' }; fetchOrders() }
const handleAdd = async () => {
dialogTitle.value = '创建订单'
form.value = { _id: '', orderNumber: '', customer: '', vehicle: '', startDate: '', endDate: '', rentalFee: 0, deposit: 0, totalAmount: 0, paidAmount: 0, status: '待支付', paymentMethod: '', notes: '' }
await fetchOptions()
dialogVisible.value = true
}
const handleEdit = async (row) => {
dialogTitle.value = '编辑订单'
form.value = { ...row, customer: row.customer?._id || row.customer, vehicle: row.vehicle?._id || row.vehicle }
await fetchOptions()
dialogVisible.value = true
}
const handleView = (row) => {
ElMessage.info(`订单详情: ${row.orderNumber}, 客户: ${row.customer?.name}, 车辆: ${row.vehicle?.model}`)
}
const handleSubmit = async () => {
try {
await formRef.value.validate()
if (form.value._id) {
const res = await axios.put(`http://localhost:3000/api/orders/${form.value._id}`, form.value)
if (res.data.success) ElMessage.success('修改成功')
} else {
const res = await axios.post('http://localhost:3000/api/orders', form.value)
if (res.data.success) ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchOrders()
} catch { ElMessage.error('操作失败') }
}
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除这个订单吗?', '提示', { type: 'warning' })
const res = await axios.delete(`http://localhost:3000/api/orders/${row._id}`)
if (res.data.success) { ElMessage.success('删除成功'); fetchOrders() }
} catch {}
}
const getStatusType = (status) => {
const types = { '待支付': 'warning', '进行中': 'primary', '已完成': 'success', '逾期': 'danger', '已取消': 'info' }
return types[status] || 'info'
}
const formatDate = (date) => { if (!date) return '-'; return new Date(date).toLocaleDateString('zh-CN') }
onMounted(() => { fetchOrders() })
</script>
<style scoped>
.search-card { margin-bottom: 20px; }
.toolbar { margin-bottom: 20px; display: flex; gap: 10px; }
</style>

146
src/views/Payments.vue Normal file
View File

@ -0,0 +1,146 @@
<template>
<div class="page">
<el-card class="search-card">
<el-form :inline="true" :model="searchForm">
<el-form-item label="打款类型">
<el-select v-model="searchForm.type" clearable>
<el-option label="退款" value="退款" />
<el-option label="押金退还" value="押金退还" />
<el-option label="分成" value="分成" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" clearable>
<el-option label="待打款" value="待打款" />
<el-option label="打款中" value="打款中" />
<el-option label="已打款" value="已打款" />
<el-option label="打款失败" value="打款失败" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<div class="toolbar">
<el-button type="primary" @click="handleAdd">+ 创建打款</el-button>
</div>
<el-card>
<el-table :data="tableData" border stripe>
<el-table-column prop="paymentId" label="打款编号" width="140" />
<el-table-column label="客户" width="100">
<template #default="{ row }">{{ row.customer?.name || '-' }}</template>
</el-table-column>
<el-table-column prop="type" label="类型" width="100" />
<el-table-column prop="amount" label="金额" width="100">
<template #default="{ row }">¥{{ row.amount }}</template>
</el-table-column>
<el-table-column prop="method" label="打款方式" width="100" />
<el-table-column prop="account" label="账号" width="150" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="operator" label="操作人" width="100" />
<el-table-column label="操作" width="150">
<template #default="{ row }">
<el-button v-if="row.status === '待打款'" size="small" type="primary" @click="handleProcess(row)">打款</el-button>
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
<el-form-item label="类型" prop="type">
<el-select v-model="form.type">
<el-option label="退款" value="退款" />
<el-option label="押金退还" value="押金退还" />
<el-option label="分成" value="分成" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="金额" prop="amount">
<el-input-number v-model="form.amount" :min="0" />
</el-form-item>
<el-form-item label="打款方式">
<el-select v-model="form.method">
<el-option label="微信" value="微信" />
<el-option label="支付宝" value="支付宝" />
<el-option label="银行卡" value="银行卡" />
<el-option label="现金" value="现金" />
</el-select>
</el-form-item>
<el-form-item label="账号">
<el-input v-model="form.account" placeholder="收款账号" />
</el-form-item>
<el-form-item label="操作人">
<el-input v-model="form.operator" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" rows="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
const searchForm = ref({ type: '', status: '' })
const tableData = ref([])
const dialogVisible = ref(false)
const dialogTitle = ref('创建打款')
const formRef = ref(null)
const form = ref({ _id: '', type: '', amount: 0, method: '', account: '', operator: '', remark: '', status: '待打款' })
const rules = { type: [{ required: true, message: '请选择类型', trigger: 'change' }], amount: [{ required: true, message: '请输入金额', trigger: 'blur' }] }
const fetchData = async () => {
const res = await axios.get('http://localhost:3000/api/payments')
if (res.data.success) tableData.value = res.data.data
}
const handleSearch = () => {
let data = [...tableData.value]
if (searchForm.value.type) data = data.filter(p => p.type === searchForm.value.type)
if (searchForm.value.status) data = data.filter(p => p.status === searchForm.value.status)
tableData.value = data
}
const handleReset = () => { searchForm.value = { type: '', status: '' }; fetchData() }
const handleAdd = () => { dialogTitle.value = '创建打款'; form.value = { _id: '', type: '', amount: 0, method: '', account: '', operator: '', remark: '', status: '待打款' }; dialogVisible.value = true }
const handleEdit = (row) => { dialogTitle.value = '编辑打款'; form.value = { ...row }; dialogVisible.value = true }
const handleProcess = async (row) => {
await axios.put(`http://localhost:3000/api/payments/${row._id}`, { status: '已打款' })
ElMessage.success('打款成功'); fetchData()
}
const handleSubmit = async () => {
await formRef.value.validate()
if (form.value._id) {
await axios.put(`http://localhost:3000/api/payments/${form.value._id}`, form.value)
ElMessage.success('修改成功')
} else {
await axios.post('http://localhost:3000/api/payments', form.value)
ElMessage.success('创建成功')
}
dialogVisible.value = false; fetchData()
}
const getStatusType = (status) => ({ '待打款': 'warning', '打款中': 'primary', '已打款': 'success', '打款失败': 'danger' }[status] || 'info')
onMounted(() => { fetchData() })
</script>
<style scoped>
.search-card { margin-bottom: 20px; }
.toolbar { margin-bottom: 20px; }
</style>

185
src/views/Stores.vue Normal file
View File

@ -0,0 +1,185 @@
<template>
<div class="page">
<el-card class="search-card">
<el-form :inline="true" :model="searchForm">
<el-form-item label="门店名称">
<el-input v-model="searchForm.name" placeholder="请输入门店名称" clearable />
</el-form-item>
<el-form-item label="审批状态">
<el-select v-model="searchForm.approvalStatus" placeholder="请选择状态" clearable>
<el-option label="待审批" value="待审批" />
<el-option label="已通过" value="已通过" />
<el-option label="已拒绝" value="已拒绝" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<el-table :data="tableData" border stripe @row-click="handleRowClick">
<el-table-column prop="storeId" label="门店编号" width="140" />
<el-table-column prop="name" label="门店名称" />
<el-table-column prop="address" label="地址" />
<el-table-column prop="phone" label="电话" width="120" />
<el-table-column prop="manager" label="负责人" width="100" />
<el-table-column prop="approvalStatus" label="审批状态" width="100">
<template #default="{ row }">
<el-tag :type="getApprovalType(row.approvalStatus)">{{ row.approvalStatus || '待审批' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button size="small" @click="handleRowClick(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" title="门店详情" width="600px" @close="dialogVisible = false">
<el-descriptions :column="1" border>
<el-descriptions-item label="门店编号">{{ viewData.storeId }}</el-descriptions-item>
<el-descriptions-item label="门店名称">{{ viewData.name }}</el-descriptions-item>
<el-descriptions-item label="地址">{{ viewData.address }}</el-descriptions-item>
<el-descriptions-item label="电话">{{ viewData.phone }}</el-descriptions-item>
<el-descriptions-item label="负责人">{{ viewData.manager }}</el-descriptions-item>
<el-descriptions-item label="审批状态">
<el-tag :type="getApprovalType(viewData.approvalStatus)">{{ viewData.approvalStatus || '待审批' }}</el-tag>
</el-descriptions-item>
</el-descriptions>
<div class="store-images">
<h4>门店证件由门店上传</h4>
<div class="image-list">
<div class="image-box">
<span class="image-label">门店照片</span>
<el-image
v-if="viewData.images?.find(i => i.type === '门店照片')"
:src="viewData.images.find(i => i.type === '门店照片').url"
:preview-src-list="[viewData.images.find(i => i.type === '门店照片').url]"
fit="cover"
style="width: 150px; height: 150px; border-radius: 4px;"
/>
<div v-else class="image-placeholder">暂无照片</div>
</div>
<div class="image-box">
<span class="image-label">营业执照</span>
<el-image
v-if="viewData.images?.find(i => i.type === '营业执照')"
:src="viewData.images.find(i => i.type === '营业执照').url"
:preview-src-list="[viewData.images.find(i => i.type === '营业执照').url]"
fit="cover"
style="width: 150px; height: 150px; border-radius: 4px;"
/>
<div v-else class="image-placeholder">暂无照片</div>
</div>
<div class="image-box">
<span class="image-label">身份证正面</span>
<el-image
v-if="viewData.images?.find(i => i.type === '身份证正面')"
:src="viewData.images.find(i => i.type === '身份证正面').url"
:preview-src-list="[viewData.images.find(i => i.type === '身份证正面').url]"
fit="cover"
style="width: 150px; height: 150px; border-radius: 4px;"
/>
<div v-else class="image-placeholder">暂无照片</div>
</div>
<div class="image-box">
<span class="image-label">身份证反面</span>
<el-image
v-if="viewData.images?.find(i => i.type === '身份证反面')"
:src="viewData.images.find(i => i.type === '身份证反面').url"
:preview-src-list="[viewData.images.find(i => i.type === '身份证反面').url]"
fit="cover"
style="width: 150px; height: 150px; border-radius: 4px;"
/>
<div v-else class="image-placeholder">暂无照片</div>
</div>
</div>
</div>
<template #footer>
<div v-if="!viewData.approvalStatus || viewData.approvalStatus === '待审批'">
<el-button type="danger" @click="handleReject">拒绝</el-button>
<el-button type="success" @click="handleApprove">通过</el-button>
</div>
<el-button v-else @click="dialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 拒绝理由弹窗 -->
<el-dialog v-model="showRejectDialog" title="拒绝原因" width="400px">
<el-input v-model="rejectReason" type="textarea" rows="4" placeholder="请输入拒绝理由" />
<template #footer>
<el-button @click="showRejectDialog = false">取消</el-button>
<el-button type="danger" @click="confirmReject">确认拒绝</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
const searchForm = ref({ name: '', approvalStatus: '' })
const tableData = ref([])
const dialogVisible = ref(false)
const viewData = ref({})
const fetchData = async () => {
const res = await axios.get('http://localhost:3000/api/stores')
if (res.data.success) tableData.value = res.data.data
}
const handleSearch = () => {
let data = [...tableData.value]
if (searchForm.value.name) data = data.filter(s => s.name.includes(searchForm.value.name))
if (searchForm.value.approvalStatus) data = data.filter(s => (s.approvalStatus || '待审批') === searchForm.value.approvalStatus)
tableData.value = data
}
const handleReset = () => { searchForm.value = { name: '', approvalStatus: '' }; fetchData() }
const handleRowClick = (row) => {
viewData.value = { ...row };
dialogVisible.value = true
}
const rejectReason = ref('')
const showRejectDialog = ref(false)
const handleApprove = async () => {
await axios.put(`http://localhost:3000/api/stores/${viewData.value._id}`, { approvalStatus: '已通过' })
ElMessage.success('已通过审批')
dialogVisible.value = false
fetchData()
}
const confirmReject = async () => {
if (!rejectReason.value.trim()) {
ElMessage.warning('请填写拒绝理由')
return
}
await axios.put(`http://localhost:3000/api/stores/${viewData.value._id}`, { approvalStatus: '已拒绝', rejectReason: rejectReason.value })
ElMessage.info('已拒绝')
showRejectDialog.value = false
rejectReason.value = ''
dialogVisible.value = false
fetchData()
}
const handleReject = () => {
showRejectDialog.value = true
}
const getApprovalType = (status) => ({ '待审批': 'warning', '已通过': 'success', '已拒绝': 'danger' }[status || '待审批'] || 'info')
onMounted(() => { fetchData() })
</script>
<style scoped>
.search-card { margin-bottom: 20px; }
.store-images { margin-top: 20px; }
.store-images h4 { margin-bottom: 15px; }
.image-list { display: flex; gap: 20px; flex-wrap: wrap; }
.image-box { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.image-label { font-size: 14px; color: #666; }
.image-placeholder { width: 150px; height: 150px; display: flex; align-items: center; justify-content: center; background: #f5f7fa; border-radius: 4px; color: #999; font-size: 14px; }
</style>

136
src/views/Users.vue Normal file
View File

@ -0,0 +1,136 @@
<template>
<div class="users-page">
<el-card class="search-card">
<el-form :inline="true" :model="searchForm">
<el-form-item label="用户名">
<el-input v-model="searchForm.username" placeholder="请输入用户名" clearable />
</el-form-item>
<el-form-item label="角色">
<el-select v-model="searchForm.role" placeholder="请选择角色" clearable>
<el-option label="管理员" value="admin" />
<el-option label="员工" value="staff" />
<el-option label="只读" value="readonly" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<div class="toolbar">
<el-button type="primary" @click="handleAdd">+ 添加用户</el-button>
</div>
<el-card>
<el-table :data="tableData" border stripe>
<el-table-column prop="username" label="用户名" width="120" />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="role" label="角色" width="100">
<template #default="{ row }">
<el-tag :type="getRoleType(row.role)">{{ getRoleName(row.role) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === '正常' ? 'success' : 'danger'">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="lastLogin" label="最后登录" width="120">
<template #default="{ row }">{{ row.lastLogin || '-' }}</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="150">
<template #default="{ row }">
<el-button size="small" type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 添加/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="450px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="角色" prop="role">
<el-select v-model="form.role" placeholder="请选择角色">
<el-option label="管理员" value="admin" />
<el-option label="员工" value="staff" />
<el-option label="只读" value="readonly" />
</el-select>
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio label="正常">正常</el-radio>
<el-radio label="禁用">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const searchForm = ref({ username: '', role: '' })
const tableData = ref([
{ _id: '1', username: 'admin', name: '管理员', role: 'admin', phone: '13800138000', email: 'admin@example.com', status: '正常', lastLogin: '2026-03-06' },
{ _id: '2', username: 'staff1', name: '张三', role: 'staff', phone: '13800138001', email: 'zhangsan@example.com', status: '正常', lastLogin: '2026-03-05' },
{ _id: '3', username: 'readonly1', name: '李四', role: 'readonly', phone: '13800138002', email: 'lisi@example.com', status: '正常', lastLogin: null }
])
const dialogVisible = ref(false)
const dialogTitle = ref('添加用户')
const formRef = ref(null)
const form = ref({ _id: '', username: '', password: '', name: '', role: 'staff', phone: '', email: '', status: '正常' })
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }]
}
const handleSearch = () => {
let data = tableData.value
if (searchForm.value.username) data = data.filter(u => u.username.includes(searchForm.value.username))
if (searchForm.value.role) data = data.filter(u => u.role === searchForm.value.role)
tableData.value = data
}
const handleReset = () => { searchForm.value = { username: '', role: '' }; tableData.value = [
{ _id: '1', username: 'admin', name: '管理员', role: 'admin', phone: '13800138000', email: 'admin@example.com', status: '正常', lastLogin: '2026-03-06' },
{ _id: '2', username: 'staff1', name: '张三', role: 'staff', phone: '13800138001', email: 'zhangsan@example.com', status: '正常', lastLogin: '2026-03-05' },
{ _id: '3', username: 'readonly1', name: '李四', role: 'readonly', phone: '13800138002', email: 'lisi@example.com', status: '正常', lastLogin: null }
]}
const handleAdd = () => { dialogTitle.value = '添加用户'; form.value = { _id: '', username: '', password: '', name: '', role: 'staff', phone: '', email: '', status: '正常' }; dialogVisible.value = true }
const handleEdit = (row) => { dialogTitle.value = '编辑用户'; form.value = { ...row, password: '******' }; dialogVisible.value = true }
const handleSubmit = () => { formRef.value.validate((valid) => { if (valid) { ElMessage.success(form.value._id ? '修改成功' : '添加成功'); dialogVisible.value = false } }) }
const handleDelete = async (row) => { await ElMessageBox.confirm('确定要删除这个用户吗?', '提示', { type: 'warning' }); ElMessage.success('删除成功') }
const getRoleType = (role) => ({ admin: 'danger', staff: 'primary', readonly: 'info' }[role] || 'info')
const getRoleName = (role) => ({ admin: '管理员', staff: '员工', readonly: '只读' }[role] || role)
</script>
<style scoped>
.search-card { margin-bottom: 20px; }
.toolbar { margin-bottom: 20px; display: flex; gap: 10px; }
</style>

207
src/views/Vehicles.vue Normal file
View File

@ -0,0 +1,207 @@
<template>
<div class="vehicles-page">
<!-- 搜索栏 -->
<el-card class="search-card">
<el-form :inline="true" :model="searchForm">
<el-form-item label="车辆编号">
<el-input v-model="searchForm.vehicleId" placeholder="请输入车辆编号" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="空闲" value="空闲" />
<el-option label="出租中" value="出租中" />
<el-option label="维修中" value="维修中" />
<el-option label="已报废" value="已报废" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 操作栏 -->
<div class="toolbar">
<el-button type="primary" @click="handleAdd">+ 添加车辆</el-button>
<el-button @click="fetchVehicles">🔄 刷新</el-button>
<el-button type="success" @click="handleExport">📥 导出Excel</el-button>
</div>
<!-- 表格 -->
<el-card>
<el-table :data="tableData" border stripe>
<el-table-column prop="vehicleId" label="车辆编号" width="120" />
<el-table-column prop="model" label="车型" width="120" />
<el-table-column prop="color" label="颜色" width="80" />
<el-table-column prop="batteryType" label="电池类型" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="purchaseDate" label="购买日期" width="120">
<template #default="{ row }">{{ formatDate(row.purchaseDate) }}</template>
</el-table-column>
<el-table-column prop="purchasePrice" label="购买价格" width="100">
<template #default="{ row }">¥{{ row.purchasePrice }}</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="150">
<template #default="{ row }">
<el-button-group>
<el-button size="small" type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
</el-button-group>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 添加/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="车辆编号" prop="vehicleId">
<el-input v-model="form.vehicleId" placeholder="请输入车辆编号" />
</el-form-item>
<el-form-item label="车型" prop="model">
<el-input v-model="form.model" placeholder="请输入车型" />
</el-form-item>
<el-form-item label="颜色" prop="color">
<el-input v-model="form.color" placeholder="请输入颜色" />
</el-form-item>
<el-form-item label="电池类型" prop="batteryType">
<el-select v-model="form.batteryType" placeholder="请选择电池类型">
<el-option label="锂电池" value="锂电池" />
<el-option label="铅酸电池" value="铅酸电池" />
</el-select>
</el-form-item>
<el-form-item label="电池容量" prop="batteryCapacity">
<el-input-number v-model="form.batteryCapacity" :min="1" :max="100" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option label="空闲" value="空闲" />
<el-option label="出租中" value="出租中" />
<el-option label="维修中" value="维修中" />
<el-option label="已报废" value="已报废" />
</el-select>
</el-form-item>
<el-form-item label="购买日期" prop="purchaseDate">
<el-date-picker v-model="form.purchaseDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="购买价格" prop="purchasePrice">
<el-input-number v-model="form.purchasePrice" :min="0" :max="100000" />
</el-form-item>
<el-form-item label="供应商" prop="purchaseSupplier">
<el-input v-model="form.purchaseSupplier" placeholder="请输入供应商" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
import { exportExcel, formatDate } from '../utils'
const searchForm = ref({ vehicleId: '', status: '' })
const tableData = ref([])
const dialogVisible = ref(false)
const dialogTitle = ref('添加车辆')
const formRef = ref(null)
const form = ref({
_id: '', vehicleId: '', model: '', color: '',
batteryType: '', batteryCapacity: 20, status: '空闲',
purchaseDate: '', purchasePrice: 0, purchaseSupplier: ''
})
const rules = {
vehicleId: [{ required: true, message: '请输入车辆编号', trigger: 'blur' }],
model: [{ required: true, message: '请输入车型', trigger: 'blur' }]
}
const fetchVehicles = async () => {
try {
const res = await axios.get('http://localhost:3000/api/vehicles')
if (res.data.success) tableData.value = res.data.data
} catch { ElMessage.error('获取车辆列表失败') }
}
const handleSearch = () => {
let data = [...tableData.value]
if (searchForm.value.vehicleId) data = data.filter(v => v.vehicleId.includes(searchForm.value.vehicleId))
if (searchForm.value.status) data = data.filter(v => v.status === searchForm.value.status)
tableData.value = data
}
const handleReset = () => { searchForm.value = { vehicleId: '', status: '' }; fetchVehicles() }
const handleAdd = () => {
dialogTitle.value = '添加车辆'
form.value = { _id: '', vehicleId: '', model: '', color: '', batteryType: '', batteryCapacity: 20, status: '空闲', purchaseDate: '', purchasePrice: 0, purchaseSupplier: '' }
dialogVisible.value = true
}
const handleEdit = (row) => {
dialogTitle.value = '编辑车辆'
form.value = { ...row }
dialogVisible.value = true
}
const handleSubmit = async () => {
try {
await formRef.value.validate()
if (form.value._id) {
const res = await axios.put(`http://localhost:3000/api/vehicles/${form.value._id}`, form.value)
if (res.data.success) ElMessage.success('修改成功')
} else {
const res = await axios.post('http://localhost:3000/api/vehicles', form.value)
if (res.data.success) ElMessage.success('添加成功')
}
dialogVisible.value = false
fetchVehicles()
} catch { ElMessage.error('操作失败') }
}
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除这辆车吗?', '提示', { type: 'warning' })
const res = await axios.delete(`http://localhost:3000/api/vehicles/${row._id}`)
if (res.data.success) { ElMessage.success('删除成功'); fetchVehicles() }
} catch {}
}
const handleExport = () => {
const data = tableData.value.map(v => ({
车辆编号: v.vehicleId,
车型: v.model,
颜色: v.color,
电池类型: v.batteryType,
电池容量: v.batteryCapacity,
状态: v.status,
购买日期: formatDate(v.purchaseDate),
购买价格: v.purchasePrice,
供应商: v.purchaseSupplier
}))
exportExcel(data, '车辆列表', '车辆')
ElMessage.success('导出成功')
}
const getStatusType = (status) => {
const types = { '空闲': 'success', '出租中': 'warning', '维修中': 'info', '已报废': 'danger' }
return types[status] || 'info'
}
onMounted(() => { fetchVehicles() })
</script>
<style scoped>
.search-card { margin-bottom: 20px; }
.toolbar { margin-bottom: 20px; display: flex; gap: 10px; }
</style>

41
test.cjs Normal file
View File

@ -0,0 +1,41 @@
const { chromium } = require('playwright');
(async () => {
console.log('启动浏览器...');
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
console.log('打开页面...');
await page.goto('http://localhost:5173', { timeout: 10000 });
console.log('等待页面加载...');
await page.waitForLoadState('domcontentloaded');
await page.waitForTimeout(3000);
// 检查是否有登录表单
const usernameInput = await page.$('input[placeholder="请输入用户名"]');
console.log('找到用户名输入框:', !!usernameInput);
if (usernameInput) {
console.log('填写账号...');
await usernameInput.fill('admin');
const passwordInput = await page.$('input[placeholder="请输入密码"]');
await passwordInput.fill('admin');
console.log('点击登录...');
await page.click('button[type="submit"]');
await page.waitForTimeout(3000);
console.log('截图...');
await page.screenshot({ path: '/Users/notyclaw/Desktop/admin_home.png', fullPage: true });
console.log('首页截好了');
}
await browser.close();
console.log('完成');
})().catch(e => {
console.error('错误:', e.message);
process.exit(1);
});

30
test2.cjs Normal file
View File

@ -0,0 +1,30 @@
const { chromium } = require('playwright');
(async () => {
console.log('启动浏览器...');
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
console.log('打开页面...');
await page.goto('http://localhost:5173', { timeout: 15000 });
console.log('等待更久...');
await page.waitForTimeout(5000);
// 获取页面内容检查
const content = await page.content();
console.log('页面长度:', content.length);
// 尝试查找任何input
const inputs = await page.$$('input');
console.log('找到input数量:', inputs.length);
// 截个图看看
await page.screenshot({ path: '/Users/notyclaw/Desktop/debug.png' });
console.log('截图完成');
await browser.close();
})().catch(e => {
console.error('错误:', e.message);
process.exit(1);
});

7
vite.config.js Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})