Initial commit: 电动车租赁管理系统管理端
This commit is contained in:
commit
6f5d2b8d13
|
|
@ -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/
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
|
|
@ -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).
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 |
|
|
@ -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);
|
||||
})();
|
||||
|
|
@ -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);
|
||||
})();
|
||||
|
|
@ -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);
|
||||
})();
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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()}`
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
})
|
||||
Loading…
Reference in New Issue