最新修改

This commit is contained in:
2026-05-26 14:08:13 +08:00
parent 93bd773c51
commit f54c58c366
18 changed files with 2180 additions and 1720 deletions

View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"mcp__chrome-devtools__list_pages"
]
}
}

106
package-lock.json generated
View File

@@ -14,7 +14,8 @@
"element-plus": "^2.11.8", "element-plus": "^2.11.8",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-echarts": "^8.0.1", "vue-echarts": "^8.0.1",
"vue-router": "^4.6.4" "vue-router": "^4.6.4",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
@@ -1744,6 +1745,15 @@
} }
} }
}, },
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/ansis": { "node_modules/ansis": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz",
@@ -1881,6 +1891,28 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -1916,6 +1948,18 @@
"url": "https://github.com/sponsors/mesqueeb" "url": "https://github.com/sponsors/mesqueeb"
} }
}, },
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -2233,6 +2277,15 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2801,6 +2854,18 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/superjson": { "node_modules/superjson": {
"version": "2.2.5", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz", "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz",
@@ -3120,6 +3185,24 @@
"vue": "^3.5.0" "vue": "^3.5.0"
} }
}, },
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/wsl-utils": { "node_modules/wsl-utils": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
@@ -3136,6 +3219,27 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@@ -18,7 +18,8 @@
"element-plus": "^2.11.8", "element-plus": "^2.11.8",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-echarts": "^8.0.1", "vue-echarts": "^8.0.1",
"vue-router": "^4.6.4" "vue-router": "^4.6.4",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",

13
src/api/login.js Normal file
View File

@@ -0,0 +1,13 @@
import { adminRequest, indexRequest } from '@/utils/request'
// ==================== 前台接口(/api/index) ====================
export const userApi = {
login(username, password) {
return indexRequest.post('/login', { username, password })
},
logout() {
return indexRequest.post('/logout')
}
}

View File

@@ -76,6 +76,20 @@ export const customerApi = {
} }
} }
// ==================== 客户接口(/api/index) ====================
export const clientApi = {
// 获取当前客户关联的工单列表
getTickets(params) {
return indexRequest.get('/client/tickets', { params })
},
// 获取当前客户的工单统计
getStats() {
return indexRequest.get('/client/stats')
}
}
export const accountApi = { export const accountApi = {
getAllUsers() { getAllUsers() {
return adminRequest.get('/getAllUsers') return adminRequest.get('/getAllUsers')
@@ -86,10 +100,16 @@ export const accountApi = {
}, },
updateAccountStatus(id, status) { updateAccountStatus(id, status) {
return adminRequest.put(`/account/${id}/status`, { status }) return adminRequest.put(`/updateAccountStatus/${id}/status`, { status })
}, },
deleteAccount(id) { deleteAccount(id) {
return adminRequest.delete(`/account/${id}`) return adminRequest.delete(`/deleteAccount/${id}`)
} }
} }
export const exportApi = {
exportdata(range) {
return adminRequest.post('/getExportData', { range })
}
}

View File

@@ -1,44 +1,120 @@
/* 重置基础样式 */ /* ============================================
html, Global Design System — Refined Minimal
body { ============================================ */
margin: 0; :root {
padding: 0; --color-bg: #fafbfc;
width: 100%; --color-surface: #ffffff;
height: 100%; --color-border: #e9eaed;
background-color: #f5f7fa; --color-border-light: #f0f1f3;
/* 给背景一个淡灰色,更有后台质感 */
font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif; --color-primary: #5b5fe3;
--color-primary-soft: #f0f0ff;
--color-primary-hover:#4a4ed1;
--color-success: #34c759;
--color-success-soft: #edfaf1;
--color-warning: #f59e0b;
--color-warning-soft: #fffcf0;
--color-danger: #ef4444;
--color-danger-soft: #fef2f2;
--text-primary: #1c1c28;
--text-secondary: #6b6d7a;
--text-muted: #9a9ba7;
--text-inverse: #ffffff;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--shadow-xs: 0 1px 2px rgba(0,0,0,0.03);
--shadow-sm: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02);
--shadow-md: 0 4px 12px rgba(0,0,0,0.04), 0 2px 4px rgba(0,0,0,0.02);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.03);
--font-sans: "PingFang SC", "Microsoft YaHei", -apple-system, "Helvetica Neue", sans-serif;
--font-mono: "SF Mono", "JetBrains Mono", "Consolas", monospace;
--el-color-primary: var(--color-primary);
--el-color-primary-light-3: rgba(91,95,227,0.3);
--el-color-primary-light-5: rgba(91,95,227,0.15);
--el-color-primary-light-7: rgba(91,95,227,0.06);
--el-color-primary-light-9: rgba(91,95,227,0.02);
--el-color-success: var(--color-success);
--el-color-warning: var(--color-warning);
--el-color-danger: var(--color-danger);
} }
#app { *, *::before, *::after { box-sizing: border-box; }
/* 移除原有的 grid 和 center 布局 */
width: 100%; html, body {
margin: 0; margin: 0; padding: 0;
padding: 0; width: 100%; height: 100%;
display: block; background: var(--color-bg);
/* 恢复块级布局 */ color: var(--text-primary);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 14px;
line-height: 1.5;
} }
/* 如果你希望内容有一个最大宽度并居中,可以这样写 */ #app { width: 100%; margin: 0; padding: 0; display: block; }
.ticket-management {
max-width: 1440px; a { text-decoration: none; color: var(--color-primary); }
/* 常见的管理后台舒适宽度 */
margin: 0 auto; /* Element Plus overrides */
/* 水平居中 */ .el-button--primary {
padding: 20px; --el-button-bg-color: var(--color-primary);
box-sizing: border-box; --el-button-border-color: var(--color-primary);
--el-button-hover-bg-color: var(--color-primary-hover);
--el-button-hover-border-color: var(--color-primary-hover);
font-weight: 500;
letter-spacing: 0.01em;
} }
/* 链接样式微调 */ .el-button { border-radius: var(--radius-sm); font-weight: 500; }
a {
text-decoration: none; .el-input .el-input__wrapper {
color: #409eff; border-radius: var(--radius-sm);
transition: 0.3s; box-shadow: 0 0 0 1px var(--color-border) inset;
transition: box-shadow 0.2s;
}
.el-input .el-input__wrapper:hover { box-shadow: 0 0 0 1px var(--color-primary) inset; }
.el-input .el-input__wrapper.is-focus {
box-shadow: 0 0 0 2px rgba(91,95,227,0.15), 0 0 0 1px var(--color-primary) inset;
} }
/* 针对移动端的小适配 */ .el-select .el-input__wrapper { border-radius: var(--radius-sm); }
@media (max-width: 768px) {
.ticket-management { .el-table {
padding: 10px; --el-table-border-color: var(--color-border-light);
} border-radius: var(--radius-md);
} overflow: hidden;
}
.el-dialog {
--el-dialog-bg-color: var(--color-surface);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-lg);
}
.el-dialog__header { border-bottom: 1px solid var(--color-border-light); padding: 20px 24px; }
.el-dialog__body { padding: 24px; }
.el-dialog__footer { padding: 16px 24px; border-top: 1px solid var(--color-border-light); }
.el-message-box {
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
}
.el-tag {
--el-tag-border-radius: 6px;
border: none;
font-weight: 500;
}
.el-pagination {
--el-pagination-button-bg-color: var(--color-surface);
--el-pagination-hover-color: var(--color-primary);
}

View File

@@ -145,108 +145,41 @@ const setSort = (order) => {
.filter-card { .filter-card {
border: none !important; border: none !important;
background: transparent; background: transparent;
/* 核心修改:禁止用户选中文字,防止点击时变蓝 */
-webkit-user-select: none;
/* Safari */
-moz-user-select: none;
/* Firefox */
-ms-user-select: none;
/* IE/Edge */
user-select: none; user-select: none;
/* 标准语法 */
} }
:deep(.el-input__inner) { user-select: text !important; }
/* 恢复输入框的可选中状态,否则无法输入文字 */ .search-container { display: flex; justify-content: space-between; align-items: flex-start; gap: 32px; }
:deep(.el-input__inner) { .filter-main { flex: 1; display: flex; flex-direction: column; }
-webkit-user-select: text !important; .input-group-row { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; }
user-select: text !important;
}
.search-container { .divider-wrapper { color: var(--text-muted); font-size: 16px; font-weight: 300; height: 32px; display: flex; align-items: center; margin-bottom: 18px; }
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 32px;
}
.filter-main { :deep(.el-form-item) { margin-bottom: 18px; }
flex: 1; :deep(.el-form-item__label) { color: var(--text-secondary); font-weight: 500; }
display: flex;
flex-direction: column;
}
.input-group-row { .wide-select { width: 180px; }
display: flex; .wide-search-input { width: 320px; }
flex-wrap: wrap;
align-items: center;
gap: 16px;
}
.divider-wrapper { .filter-main :deep(.el-input__wrapper),
color: #dcdfe6; .filter-main :deep(.el-button) { border-radius: 20px !important; }
font-size: 18px; :deep(.el-select-dropdown) { border-radius: 12px !important; overflow: hidden; }
font-weight: 200; .filter-actions :deep(.el-button) { border-radius: 20px !important; }
height: 32px;
display: flex;
align-items: center;
margin-bottom: 18px;
}
:deep(.el-form-item) { .filter-actions { display: flex; gap: 10px; margin-top: 4px; flex-shrink: 0; }
margin-bottom: 18px;
}
/* 宽度控制 */
.wide-select {
width: 180px;
}
.wide-search-input {
width: 320px;
}
.filter-actions {
display: flex;
gap: 10px;
margin-top: 4px;
flex-shrink: 0;
}
.search-btn {
font-weight: bold;
}
@media (max-width: 1400px) {
.wide-search-input {
width: 260px;
}
}
@media (max-width: 768px) {
.search-container {
flex-direction: column;
}
.input-group-row {
flex-direction: column;
align-items: flex-start;
}
.divider-wrapper {
display: none;
}
.wide-select,
.wide-search-input {
width: 100%;
}
}
/* 运营人员分组标题样式 */
:deep(.el-select-group__title) { :deep(.el-select-group__title) {
background-color: #f5f7fa; background: var(--color-bg);
font-weight: 600; font-weight: 600;
color: #303133; color: var(--text-primary);
padding-left: 12px; padding-left: 12px;
} }
@media (max-width: 1400px) { .wide-search-input { width: 260px; } }
@media (max-width: 768px) {
.search-container { flex-direction: column; }
.input-group-row { flex-direction: column; align-items: flex-start; }
.divider-wrapper { display: none; }
.wide-select, .wide-search-input { width: 100%; }
}
</style> </style>

View File

@@ -13,6 +13,16 @@
</template> </template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item label="数据量" prop="datanum">
<el-input v-model="form.datanum" placeholder="输入数据量" clearable>
<template #prefix>
<el-icon>
<DataAnalysis />
</el-icon>
</template>
</el-input>
</el-form-item>
</div> </div>
<div class="form-section people-grid"> <div class="form-section people-grid">
@@ -21,16 +31,17 @@
<el-form-item label="客户单位" prop="company_id" class="client-item"> <el-form-item label="客户单位" prop="company_id" class="client-item">
<el-select v-model="form.company_id" placeholder="请选择单位" style="width: 100%" filterable <el-select v-model="form.company_id" placeholder="请选择单位" style="width: 100%" filterable
@change="handleCustomerChange"> @change="handleCustomerChange">
<el-option v-for="item in companiesWithOperators.filter(c => c.id !== 0)" :key="item.id" :label="item.company_name" <el-option v-for="item in companiesWithOperators.filter(c => c.id !== 0)" :key="item.id"
:value="item.id" /> :label="item.company_name" :value="item.id" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="运营人员" prop="operator_id" class="client-item"> <el-form-item label="运营人员" prop="operator_id" class="client-item">
<el-select v-model="form.operator_id" placeholder="请选择人员" style="width: 100%" filterable <el-select v-model="form.operator_id" placeholder="请选择人员" style="width: 100%" filterable
:disabled="!form.company_id"> :disabled="!form.company_id">
<el-option v-for="item in (companiesWithOperators.find(c => c.id === form.company_id)?.companyoperators || []).filter(op => op.id !== 0)" :key="item.id" :label="item.operator_name" <el-option
:value="item.id" /> v-for="item in (companiesWithOperators.find(c => c.id === form.company_id)?.companyoperators || []).filter(op => op.id !== 0)"
:key="item.id" :label="item.operator_name" :value="item.id" />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -70,7 +81,7 @@
<script setup> <script setup>
import { ref, reactive, watch, computed, nextTick } from "vue"; import { ref, reactive, watch, computed, nextTick } from "vue";
import { Link } from "@element-plus/icons-vue"; import { Link, DataAnalysis } from "@element-plus/icons-vue";
const emit = defineEmits(["update:modelValue", "submit"]); const emit = defineEmits(["update:modelValue", "submit"]);
@@ -90,6 +101,7 @@ const submitting = ref(false);
// 核心表单对象:customer_id 已包含在内 // 核心表单对象:customer_id 已包含在内
const form = reactive({ const form = reactive({
website: "", website: "",
datanum: "",
company_id: null, company_id: null,
operator_id: null, operator_id: null,
user_id: null, user_id: null,
@@ -170,6 +182,7 @@ const resetForm = () => {
if (formRef.value) formRef.value.resetFields(); if (formRef.value) formRef.value.resetFields();
Object.assign(form, { Object.assign(form, {
website: "", website: "",
datanum: "",
company_id: null, company_id: null,
operator_id: null, operator_id: null,
user_id: null, user_id: null,
@@ -186,7 +199,7 @@ const handleSubmit = async () => {
await formRef.value.validate(); await formRef.value.validate();
submitting.value = true; submitting.value = true;
// 复制提交数据,如果是新增,确保 staff_id 为空
const submitData = { ...form }; const submitData = { ...form };
// 核心修复:如果状态是”已完结”,确保 lock_time 不会被意外修改。 // 核心修复:如果状态是”已完结”,确保 lock_time 不会被意外修改。
@@ -198,9 +211,6 @@ const handleSubmit = async () => {
delete submitData.create_time; delete submitData.create_time;
delete submitData.update_time; delete submitData.update_time;
if (!props.isEditing) {
submitData.user_id = null;
}
emit("submit", submitData); emit("submit", submitData);
} catch (e) { } catch (e) {
@@ -212,30 +222,26 @@ const handleSubmit = async () => {
</script> </script>
<style scoped> <style scoped>
/* 保持你原本的样式不变 */
:deep(.modern-dialog) {
border-radius: 12px;
overflow: hidden;
}
.form-section { .form-section {
margin-bottom: 24px; margin-bottom: 20px;
} }
.section-title { .section-title {
font-size: 14px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: #1e293b; color: var(--text-muted);
margin-bottom: 12px; margin-bottom: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
text-transform: uppercase;
letter-spacing: 0.5px;
} }
.section-title::before { .section-title::before {
content: ""; content: "";
width: 4px; width: 3px;
height: 14px; height: 12px;
background: #409eff; background: var(--color-primary);
margin-right: 8px; margin-right: 8px;
border-radius: 2px; border-radius: 2px;
} }
@@ -243,7 +249,7 @@ const handleSubmit = async () => {
.people-grid { .people-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 20px; gap: 16px;
} }
.column-full { .column-full {
@@ -251,9 +257,13 @@ const handleSubmit = async () => {
} }
.modern-radio-group { .modern-radio-group {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
border-radius: 8px;
overflow: hidden; overflow: hidden;
border-radius: var(--radius-sm);
}
:deep(.el-form-item__label) {
color: var(--text-secondary);
font-weight: 500;
} }
.submit-btn { .submit-btn {

View File

@@ -9,7 +9,7 @@
@selection-change="$emit('selection-change', $event)" @selection-change="$emit('selection-change', $event)"
> >
<el-table-column type="selection" width="50" /> <el-table-column type="selection" width="50" />
<el-table-column prop="website" label="网站地址" min-width="180"> <el-table-column label="网站地址" min-width="180">
<template #default="{ row }"> <template #default="{ row }">
<el-link <el-link
type="primary" type="primary"
@@ -100,29 +100,33 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="锁定状态" width="90" align="center"> <el-table-column label="锁定" width="80" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag <span
v-if="row.lock_time && row.lock_time !== '0'" class="lock-badge"
type="success" :class="{ locked: row.lock_time && row.lock_time !== '0' }"
size="small"
effect="light"
>已锁定</el-tag
> >
<el-tag v-else type="info" size="small" effect="plain">未锁定</el-tag> {{ row.lock_time && row.lock_time !== '0' ? '已锁定' : '未锁定' }}
</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="时间节点" width="160"> <el-table-column label="时间节点" width="160">
<template #default="{ row }"> <template #default="{ row }">
<div class="time-wrapper"> <div class="time-wrapper">
<span>创建:{{ formatDateTime(row.create_time) }}</span> <span class="time-row">
<span class="update-label">更新:{{ formatDateTime(row.update_time) }}</span> <span class="time-label">创建</span>
{{ formatDateTime(row.create_time) }}
</span>
<span class="time-row is-update">
<span class="time-label">更新</span>
{{ formatDateTime(row.update_time) }}
</span>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="120" fixed="right" align="right"> <el-table-column label="操作" width="120" fixed="right" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" @click="$emit('edit', row)">编辑</el-button> <el-button link type="primary" @click="$emit('edit', row)">编辑</el-button>
<el-divider direction="vertical" /> <el-divider direction="vertical" />
@@ -167,10 +171,10 @@ const statusLabel = (status) => statusMap[status] || "未知";
const getStatusStyle = (status) => { const getStatusStyle = (status) => {
const styles = { const styles = {
0: { color: "#909399", background: "#f4f4f5" }, 0: { color: "#6b7280", background: "#f3f4f6" },
1: { color: "#409EFF", background: "#ecf5ff" }, 1: { color: "#4f46e5", background: "#eef0ff" },
2: { color: "#67C23A", background: "#f0f9eb" }, 2: { color: "#16a34a", background: "#ecfdf3" },
3: { color: "#e6a23c", background: "#fdf6ec" }, 3: { color: "#d97706", background: "#fffbeb" },
}; };
return styles[status] || styles[0]; return styles[status] || styles[0];
}; };
@@ -178,14 +182,14 @@ const getStatusStyle = (status) => {
// 客户徽章颜色映射(同一客户保持同一颜色) // 客户徽章颜色映射(同一客户保持同一颜色)
const companyColorMap = new Map(); const companyColorMap = new Map();
const COMPANY_PALETTE = [ const COMPANY_PALETTE = [
{ color: "#409EFF", background: "#ecf5ff" }, { color: "#4f46e5", background: "#eef0ff" },
{ color: "#67C23A", background: "#f0f9eb" }, { color: "#16a34a", background: "#ecfdf3" },
{ color: "#E6A23C", background: "#fdf6ec" }, { color: "#d97706", background: "#fffbeb" },
{ color: "#F56C6C", background: "#fef0f0" }, { color: "#dc2626", background: "#fef2f2" },
{ color: "#909399", background: "#f4f4f5" }, { color: "#6b7280", background: "#f3f4f6" },
{ color: "#9C27B0", background: "#f3e5f5" }, { color: "#7c3aed", background: "#f5f0ff" },
{ color: "#00BCD4", background: "#e0f7fa" }, { color: "#0891b2", background: "#ecfeff" },
{ color: "#FF9800", background: "#fff3e0" }, { color: "#ea580c", background: "#fff7ed" },
]; ];
let nextColorIndex = 0; let nextColorIndex = 0;
@@ -200,118 +204,81 @@ const getCompanyStyle = (name) => {
</script> </script>
<style scoped> <style scoped>
.progress-container { .progress-container { padding-right: 10px; }
padding-right: 10px; :deep(.el-progress__text) { font-size: 11px !important; min-width: 28px; }
.compact-card {
background: transparent;
border: none;
} }
/* 调整进度条内部文字大小 */ :deep(.el-table) {
:deep(.el-progress__text) { border-radius: 10px;
font-size: 11px !important; overflow: hidden;
min-width: 28px;
} }
.staff-wrapper { .staff-wrapper { display: flex; flex-direction: column; gap: 4px; }
display: flex;
flex-direction: column;
gap: 4px;
}
.staff-item { .staff-item {
display: flex; display: flex; align-items: center; height: 20px;
align-items: center; font-size: 11px; border-radius: 4px; overflow: hidden;
height: 22px;
font-size: 12px;
border-radius: 4px;
overflow: hidden;
} }
.staff-role { .staff-role {
width: 34px; width: 30px; height: 100%;
height: 100%; display: flex; align-items: center; justify-content: center;
display: flex; font-size: 9px; font-weight: 600; color: #fff;
align-items: center; letter-spacing: 0.5px;
justify-content: center;
font-size: 10px;
background: #909399;
color: #fff;
} }
.staff-name { .staff-name { padding-left: 6px; color: inherit; }
padding-left: 6px;
color: inherit;
}
.staff-item.client { .staff-item.client {
background: #f4f4f5; background: #f5f5f7; color: #6b6d7a;
color: #909399; border: 1px solid #ededf0;
border: 1px solid #e9e9eb;
} }
.staff-item.client .staff-role { background: #8b8d97; }
.staff-item.internal { .staff-item.internal {
background: #ecf5ff; background: #f5f5f7; color: #6b6d7a;
color: #409eff; border: 1px solid #ededf0;
border: 1px solid #d9ecff;
} }
.staff-item.internal .staff-role { background: #8b8d97; }
.staff-item.internal .staff-role { .data-num-text { font-family: var(--font-mono); font-weight: 600; color: var(--color-primary); }
background: #409eff; .remark-text { font-size: 12px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; }
.empty-text { color: var(--text-muted); font-size: 11px; }
.status-badge, .company-badge {
display: inline-block; padding: 0 8px;
height: 20px; line-height: 20px;
font-size: 10px; font-weight: 600;
border-radius: 10px;
} }
.company-badge { max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.data-num-text { .lock-badge {
font-family: "Monaco", monospace;
font-weight: bold;
color: #409eff;
}
.remark-text {
font-size: 12px;
color: #606266;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
.empty-text {
color: #c0c4cc;
font-size: 12px;
}
.compact-card {
border-radius: 8px;
border: 1px solid #ebeef5;
}
.status-badge,
.company-badge {
display: inline-block; display: inline-block;
padding: 0 8px; padding: 1px 10px;
height: 22px; border-radius: 10px;
line-height: 22px;
font-size: 11px; font-size: 11px;
border-radius: 11px; font-weight: 500;
color: #9ca3af;
background: #f3f4f6;
}
.lock-badge.locked {
color: #16a34a;
background: #ecfdf3;
} }
.company-badge { .time-wrapper { display: flex; flex-direction: column; font-size: 11px; color: var(--text-secondary); }
max-width: 110px; .update-label { color: var(--text-muted); font-family: var(--font-mono); font-size: 10px; }
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.time-wrapper {
display: flex;
flex-direction: column;
font-size: 11px;
color: #606266;
}
.update-label {
color: #909399;
}
:deep(.table-header) { :deep(.table-header) {
background-color: #fafafa !important; background: var(--color-bg) !important;
font-weight: 600; font-weight: 600;
color: var(--text-secondary);
font-size: 11px;
letter-spacing: 0.3px;
} }
</style> </style>

View File

@@ -1,272 +1,269 @@
<template> <template>
<div class="full-screen-layout"> <div class="shell">
<el-container class="full-screen-container"> <aside class="sidebar">
<el-aside width="200px" class="siderbar"> <div class="sidebar-brand">
<div class="sidebar-header"> <div class="brand-mark">
<!-- 修复:将 logo 区域包装成一个正确的外部链接 --> <img src="@/assets/logo.jpg" alt="logo" class="brand-img" />
<a href="https://www.example.com" target="_blank" rel="noopener noreferrer" class="logo-link">
<div class="logo-container">
<img src="@/assets/logo.jpg" alt="方舟网络" class="logo-image" />
<div class="text-content">
<h2 class="company-name">方舟网络</h2>
<p class="system-name">工单管理系统</p>
</div>
</div>
</a>
</div> </div>
<div class="brand-text">
<span class="brand-name">方舟工单</span>
<span class="brand-ver">v4.0</span>
</div>
</div>
<el-menu default-active="/" class="sidebar-menu" router> <nav class="nav">
<el-menu-item index="/"> <router-link to="/" class="nav-item" active-class="active" exact-active-class="active">
<el-icon> <span class="nav-dot"></span>
<icon-menu /> <span class="nav-label">工单管理</span>
</el-icon> </router-link>
<span>工单管理</span> <router-link to="/stats" class="nav-item" active-class="active">
</el-menu-item> <span class="nav-dot"></span>
<span class="nav-label">数据统计</span>
</router-link>
<router-link to="/companyAoperators" class="nav-item" active-class="active">
<span class="nav-dot"></span>
<span class="nav-label">客户管理</span>
</router-link>
<router-link to="/accountRegistration" class="nav-item" active-class="active">
<span class="nav-dot"></span>
<span class="nav-label">账户注册</span>
</router-link>
</nav>
<el-menu-item index="/stats"> <div class="sidebar-foot">
<el-icon> <span class="status-dot"></span>
<document /> <span class="status-text">运行中</span>
</el-icon> </div>
<span>数据统计</span> </aside>
</el-menu-item>
<el-menu-item index="/companyAoperators"> <div class="main">
<el-icon> <header class="topbar">
<User /> <span class="page-title">{{ route.meta?.title || '工单管理' }}</span>
</el-icon> <el-dropdown trigger="click">
<span>客户管理</span> <div class="user-chip">
</el-menu-item> <el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" />
<span class="user-name">{{ user.name }}</span>
<el-menu-item index="/accountRegistration">
<el-icon>
<Plus />
</el-icon>
<span>账户注册</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container class="main-container">
<el-header class="main-header">
<div class="header-content">
<div class="user-info">
<el-dropdown trigger="click">
<div class="user-profile">
<el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" />
<span class="username">{{ user.name }}</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleLogout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div> </div>
</el-header> <template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleLogout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</header>
<el-main class="main-content"> <main class="content">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<transition name="fade-transform" mode="out-in"> <transition name="fade" mode="out-in">
<component :is="Component" /> <component :is="Component" />
</transition> </transition>
</router-view> </router-view>
</el-main> </main>
</el-container> </div>
</el-container>
</div> </div>
</template> </template>
<script setup> <script setup>
import { reactive, onMounted } from "vue"; import { reactive, onMounted } from "vue";
import { Menu as IconMenu, Document, User } from "@element-plus/icons-vue"; import { useRouter, useRoute } from "vue-router";
import { useRouter } from "vue-router";
import { resetRouter } from "@/router"; import { resetRouter } from "@/router";
import { userApi } from "@/api/ticket"; import { userApi } from "@/api/ticket";
const router = useRouter(); const router = useRouter();
const user = reactive({ const route = useRoute();
name: "加载中...", const user = reactive({ name: "..." });
avatar: "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png",
});
onMounted(() => { onMounted(() => {
const savedUser = const saved = localStorage.getItem("user_info") || sessionStorage.getItem("user_info");
localStorage.getItem("user_info") || sessionStorage.getItem("user_info"); if (saved) user.name = JSON.parse(saved).username || "用户";
if (savedUser) {
const data = JSON.parse(savedUser);
user.name = data.username || "管理员";
}
}); });
const handleLogout = async () => { const handleLogout = async () => {
// 1. 调用后端接口销毁 session try { await userApi.logout(); } catch (e) { }
try {
await userApi.logout();
} catch (e) {
// 即使后端接口异常,也要完成前端的清理
}
localStorage.removeItem("user_info"); localStorage.removeItem("user_info");
sessionStorage.removeItem("user_info"); sessionStorage.removeItem("user_info");
resetRouter(); resetRouter();
router.push({ name: "login" }); router.push({ name: "login" });
}; };
</script> </script>
<style scoped> <style scoped>
/* --- 基础布局保持不变 --- */ .shell {
.full-screen-layout, display: flex;
.full-screen-container {
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
margin: 0;
padding: 0;
overflow: hidden; overflow: hidden;
background: #ebedf0;
} }
.siderbar { /* ---- Sidebar ---- */
background-color: #001529; .sidebar {
color: white; width: 224px;
/* 禁止用户选中文字 */ flex-shrink: 0;
user-select: none; background: #f3f4f7;
-webkit-user-select: none; border-right: 1px solid rgba(0, 0, 0, 0.07);
/* 兼容 Safari */
-moz-user-select: none;
/* 兼容 Firefox */
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
height: 80px;
display: flex; display: flex;
align-items: center; flex-direction: column;
user-select: none;
} }
/* 修复:为新添加的链接添加样式 */ .sidebar-brand {
.logo-link { padding: 22px 20px 18px;
text-decoration: none;
color: inherit;
display: block;
}
.logo-container {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
} }
.logo-image { .brand-mark {
width: 40px; width: 38px;
height: 40px; height: 38px;
border-radius: 6px; border-radius: 10px;
object-fit: cover; overflow: hidden;
flex-shrink: 0;
box-shadow:
0 0 0 3px rgba(91, 95, 227, 0.06),
0 2px 8px rgba(91, 95, 227, 0.1);
} }
.company-name { .brand-img { width: 100%; height: 100%; object-fit: cover; }
margin: 0;
font-size: 16px; .brand-text { display: flex; flex-direction: column; gap: 2px; }
font-weight: 600;
color: #ffffff; .brand-name {
font-size: 15px;
font-weight: 700;
color: #1c1c28;
letter-spacing: 0.02em;
} }
.system-name { .brand-ver {
margin: 0; font-size: 10px;
font-size: 12px; color: #9a9ba7;
color: rgba(255, 255, 255, 0.7); letter-spacing: 0.03em;
font-weight: 500;
} }
.sidebar-menu { /* ---- Navigation ---- */
border: none; .nav {
background-color: transparent; flex: 1;
} padding: 14px 12px;
.sidebar-menu .el-menu-item {
color: rgba(255, 255, 255, 0.8);
}
.sidebar-menu .el-menu-item:hover {
background-color: rgba(24, 144, 255, 0.2);
color: #ffffff;
}
.sidebar-menu .el-menu-item.is-active {
background-color: #1890ff;
color: #ffffff;
}
/* --- 右侧 Header 布局优化 --- */
.main-header {
background-color: #fff;
border-bottom: 1px solid #e6e6e6;
display: flex; display: flex;
align-items: center; flex-direction: column;
justify-content: flex-end; gap: 1px;
padding: 0 20px;
height: 60px;
} }
.user-profile { .nav-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 10px 14px;
margin: 1px 0;
border-radius: 10px;
color: #6b6d7a;
text-decoration: none;
font-size: 13.5px;
font-weight: 500;
transition: all 0.15s;
}
.nav-item:hover {
background: rgba(91, 95, 227, 0.06);
color: #1c1c28;
}
.nav-item.active {
background: rgba(91, 95, 227, 0.1);
color: #4338ca;
font-weight: 600;
}
.nav-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: currentColor;
opacity: 0.3;
flex-shrink: 0;
transition: opacity 0.15s;
}
.nav-item.active .nav-dot { opacity: 1; }
.nav-label { letter-spacing: 0.02em; }
/* Sidebar footer */
.sidebar-foot {
padding: 14px 20px;
border-top: 1px solid rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #787a85;
}
.status-dot {
width: 5px; height: 5px;
border-radius: 50%;
background: #34c759;
box-shadow: 0 0 4px rgba(52, 199, 89, 0.3);
}
/* ---- Main Area ---- */
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.topbar {
height: 56px;
flex-shrink: 0;
background: #f3f4f7;
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 28px;
}
.page-title {
font-size: 15px;
font-weight: 600;
color: #1c1c28;
letter-spacing: 0.02em;
}
.user-chip {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer; cursor: pointer;
outline: none; padding: 4px 14px 4px 4px;
/* 保持隐藏默认框 */ border-radius: 24px;
height: 48px; transition: background 0.15s;
/* 稍微缩小高度,让点击范围更精致 */
padding: 0 12px;
/* 左右加点间距 */
border-radius: 6px;
/* 加个圆角 */
transition: all 0.2s;
/* 让颜色变化更顺滑 */
} }
/* 鼠标悬停时的效果 */ .user-chip:hover { background: rgba(91, 95, 227, 0.04); }
.user-profile:hover {
background-color: #f0f2f5; .user-chip :deep(.el-avatar) {
width: 28px !important;
height: 28px !important;
} }
/* 关键:鼠标点击那一瞬间的效果 */ .user-name {
.user-profile:active { font-size: 13px;
background-color: #e6e8eb; color: #1c1c28;
/* 背景变深 */ font-weight: 500;
transform: scale(0.98);
/* 轻轻缩放,增加“按下去”的质感 */
} }
.username { /* ---- Content ---- */
font-size: 14px; .content {
color: #333; flex: 1;
line-height: 1;
/* 消除默认行高导致的下偏 */
display: inline-block;
}
.main-content {
padding: 20px;
background-color: #f5f7fa;
height: calc(100vh - 60px);
overflow-y: auto; overflow-y: auto;
padding: 24px 28px;
} }
/* 过渡动画 */ /* ---- Transitions ---- */
.fade-transform-enter-active, .fade-enter-active,
.fade-transform-leave-active { .fade-leave-active { transition: opacity 0.15s, transform 0.15s; }
transition: all 0.3s; .fade-enter-from { opacity: 0; transform: translateY(4px); }
} .fade-leave-to { opacity: 0; transform: translateY(-4px); }
.fade-transform-enter-from {
opacity: 0;
transform: translateX(-10px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(10px);
}
</style> </style>

View File

@@ -19,7 +19,7 @@ export function formatDate(time) {
const timestamp = Number(time) const timestamp = Number(time)
if (!isNaN(timestamp) && timestamp > 0) { if (!isNaN(timestamp) && timestamp > 0) {
const date = new Date(timestamp * 1000) const date = new Date(timestamp > 9999999999 ? timestamp : timestamp * 1000)
if (!isNaN(date.getTime())) { if (!isNaN(date.getTime())) {
const y = date.getFullYear() const y = date.getFullYear()
const m = (date.getMonth() + 1).toString().padStart(2, '0') const m = (date.getMonth() + 1).toString().padStart(2, '0')

View File

@@ -1,95 +1,123 @@
<template> <template>
<div class="account-container"> <div class="account-root">
<h2>账户管理</h2> <!-- 统计卡片 -->
<div class="stats-row">
<div class="action-bar"> <div class="stat-card" :class="{ active: activeRole === s.role }" v-for="s in stats" :key="s.key"
<div class="search-group"> :style="{ '--border': s.color }" @click="activeRole = activeRole === s.role ? null : s.role">
<el-input v-model="searchQuery" placeholder="搜索用户名/真实姓名" style="width: 250px" clearable /> <div class="stat-left">
<div class="stat-icon-wrap" :style="{ '--bg': s.color }">
<el-icon size="16">
<component :is="s.icon" />
</el-icon>
</div>
<span class="stat-label">{{ s.label }}</span>
</div>
<span class="stat-num">{{ s.value }}</span>
</div> </div>
<el-button type="primary" :icon="Plus" @click="openRegisterDialog">注册用户</el-button>
</div> </div>
<el-table :data="filteredUsers" border style="width: 100%; margin-top: 20px" v-loading="loading"> <!-- 主面板 -->
<template #empty> <div class="main-card">
<el-empty :image-size="100" description="暂无用户数据" /> <div class="card-header">
</template> <div class="header-left">
<span class="header-title">用户列表</span>
<!-- <el-table-column prop="id" label="ID" width="70" align="center" /> --> <span class="header-count">{{ filteredUsers.length }} 人</span>
<el-table-column prop="username" label="用户名" min-width="120" /> </div>
<el-table-column prop="real_name" label="真实姓名" min-width="120"> <div class="header-right">
<template #default="{ row }"> <el-input v-model="searchQuery" placeholder="搜索用户名或真实姓名" style="width: 240px" size="default" clearable
{{ row.real_name || '-' }} :prefix-icon="Search" />
</template> <el-button type="primary" :icon="Plus" @click="openRegisterDialog">
</el-table-column> 注册用户
<el-table-column label="角色" width="120" align="center">
<template #default="{ row }">
<el-tag :type="roleTagType(row.role)" size="small" effect="plain">
{{ roleLabel(row.role) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small" effect="plain">
{{ row.status === 1 ? '正常' : '已禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" width="160" align="center">
<template #default="{ row }">
{{ formatDate(row.create_time) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="scope">
<el-button
size="small"
:type="scope.row.status === 1 ? 'warning' : 'success'"
plain
@click="handleToggleStatus(scope.row)"
>
{{ scope.row.status === 1 ? '禁用' : '启用' }}
</el-button>
<el-button size="small" type="danger" plain @click="handleDeleteAccount(scope.row)">
删除
</el-button> </el-button>
</div>
</div>
<el-table :data="filteredUsers" v-loading="loading" header-cell-class-name="table-head"
row-class-name="table-row">
<template #empty>
<el-empty :image-size="80" description="暂无用户数据" />
</template> </template>
</el-table-column>
</el-table> <el-table-column label="用户名" min-width="140">
<template #default="{ row }">
<div class="user-cell">
<div class="user-avatar" :style="{ background: avatarColor(row.username) }">
{{ row.username.slice(0, 1).toUpperCase() }}
</div>
<span class="user-name">{{ row.username }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="真实姓名" min-width="120">
<template #default="{ row }">
<span :class="{ 'text-muted': !row.real_name }">{{ row.real_name || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="角色" width="100" align="center">
<template #default="{ row }">
<span class="role-tag" :class="row.role">{{ roleLabel(row.role) }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<span class="status-badge" :class="{ on: row.status === 1 }">
{{ row.status === 1 ? '正常' : '已禁用' }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="140" fixed="right" align="center">
<template #default="scope">
<el-button size="small" :type="scope.row.status === 1 ? 'warning' : 'success'"
@click="handleToggleStatus(scope.row)">
{{ scope.row.status === 1 ? '禁用' : '启用' }}
</el-button>
<el-button size="small" type="danger" plain @click="handleDeleteAccount(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 注册弹窗 --> <!-- 注册弹窗 -->
<el-dialog <el-dialog v-model="registerDialogVisible" title="注册新用户" width="420px" destroy-on-close @close="resetRegisterForm"
v-model="registerDialogVisible" class="register-dialog">
title="注册新用户" <el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" label-position="top">
width="450px"
destroy-on-close
@close="resetRegisterForm"
>
<el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" label-width="80px">
<el-form-item label="用户名" prop="username"> <el-form-item label="用户名" prop="username">
<el-input v-model="registerForm.username" placeholder="登录用" clearable /> <el-input v-model="registerForm.username" placeholder="用于登录" size="large" />
</el-form-item> </el-form-item>
<el-form-item label="真实姓名" prop="real_name"> <el-form-item label="真实姓名" prop="real_name">
<el-input v-model="registerForm.real_name" placeholder="选填" clearable /> <el-input v-model="registerForm.real_name" placeholder="选填" size="large" />
</el-form-item> </el-form-item>
<el-form-item label="密码" prop="password"> <el-form-item label="密码" prop="password">
<el-input v-model="registerForm.password" type="password" placeholder="请输入密码" show-password /> <el-input v-model="registerForm.password" type="password" placeholder="选填,如果不填,则默认密码为123456" size="large"
show-password />
</el-form-item> </el-form-item>
<el-form-item label="角色" prop="role"> <el-form-item label="角色" prop="role">
<el-select v-model="registerForm.role" placeholder="请选择角色" style="width: 100%"> <div class="role-group">
<el-option label="管理员" value="admin" /> <button v-for="opt in roleOptions" :key="opt.value" type="button" class="role-btn"
<el-option label="员工" value="user" /> :class="{ selected: registerForm.role === opt.value }" :style="{ '--accent': opt.color }"
<el-option label="客户" value="client" /> @click="registerForm.role = opt.value">
<el-icon size="16">
<component :is="opt.icon" />
</el-icon>
<span>{{ opt.label }}</span>
</button>
</div>
</el-form-item>
<el-form-item label="所属公司">
<el-select v-model="registerForm.company_id" placeholder="请选择所属公司" size="large" style="width: 100%">
<el-option v-for="c in companyList" :key="c.company_id" :label="c.company_name" :value="c.company_id" />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="registerDialogVisible = false">取消</el-button> <div class="dialog-actions">
<el-button type="primary" :loading="submitting" @click="handleRegister">确定注册</el-button> <el-button size="large" @click="registerDialogVisible = false">取消</el-button>
<el-button size="large" type="primary" :loading="submitting" @click="handleRegister">确定注册</el-button>
</div>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
@@ -97,14 +125,15 @@
<script setup> <script setup>
import { ref, reactive, computed, onMounted } from "vue"; import { ref, reactive, computed, onMounted } from "vue";
import { Plus } from "@element-plus/icons-vue"; import { Plus, Search, User, Monitor, Star } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { accountApi } from "@/api/ticket"; import { accountApi, ticketApi } from "@/api/ticket";
import { formatDate } from "@/utils/format";
const loading = ref(false); const loading = ref(false);
const users = ref([]); const users = ref([]);
const companyList = ref([]);
const searchQuery = ref(""); const searchQuery = ref("");
const activeRole = ref(null);
const registerDialogVisible = ref(false); const registerDialogVisible = ref(false);
const registerFormRef = ref(null); const registerFormRef = ref(null);
@@ -115,19 +144,17 @@ const registerForm = reactive({
real_name: "", real_name: "",
password: "", password: "",
role: "user", role: "user",
company_id: null,
}); });
const registerRules = { const registerRules = {
username: [ username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
{ required: true, message: "请输入用户名", trigger: "blur" }, // password: [
], // { required: true, message: "请输入密码", trigger: "blur" },
password: [ // { min: 6, message: "密码至少6位", trigger: "blur" },
{ required: true, message: "请输入密码", trigger: "blur" }, // ],
{ min: 6, message: "密码至少6位", trigger: "blur" }, role: [{ required: true, message: "请选择角色", trigger: "change" }],
], company_id: [{ required: true, message: "请选择所属公司", trigger: "change" }],
role: [
{ required: true, message: "请选择角色", trigger: "change" },
],
}; };
const roleLabel = (role) => { const roleLabel = (role) => {
@@ -135,23 +162,42 @@ const roleLabel = (role) => {
return map[role] || role; return map[role] || role;
}; };
const roleTagType = (role) => {
const map = { admin: "danger", user: "warning", client: "info" };
return map[role] || "info";
};
const rolePriority = { admin: 0, user: 1, client: 2 }; const rolePriority = { admin: 0, user: 1, client: 2 };
const roleOptions = [
{ value: "admin", label: "管理员", color: "#ef4444", icon: Star },
{ value: "user", label: "员工", color: "#f59e0b", icon: User },
{ value: "client", label: "客户", color: "#34c759", icon: Monitor },
];
const stats = computed(() => [
{ key: "total", role: null, label: "总用户", value: users.value.length, icon: User, color: "#5b5fe3" },
{ key: "admin", role: "admin", label: "管理员", value: users.value.filter(u => u.role === "admin").length, icon: Star, color: "#ef4444" },
{ key: "user", role: "user", label: "员工", value: users.value.filter(u => u.role === "user").length, icon: User, color: "#f59e0b" },
{ key: "client", role: "client", label: "客户", value: users.value.filter(u => u.role === "client").length, icon: Monitor, color: "#34c759" },
]);
const avatarColor = (name) => {
const colors = ["#5b5fe3", "#ef4444", "#f59e0b", "#34c759", "#0ea5e9", "#8b5cf6", "#ec4899", "#14b8a6"];
let hash = 0;
for (let i = 0; i < (name || "").length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
return colors[Math.abs(hash) % colors.length];
};
const filteredUsers = computed(() => { const filteredUsers = computed(() => {
let list = users.value; let list = users.value;
// 角色筛选
if (activeRole.value) {
list = list.filter(u => u.role === activeRole.value);
}
// 关键词搜索
if (searchQuery.value) { if (searchQuery.value) {
const q = searchQuery.value.toLowerCase(); const q = searchQuery.value.toLowerCase();
list = list.filter( list = list.filter(u => u.username.toLowerCase().includes(q) || (u.real_name && u.real_name.toLowerCase().includes(q)));
(u) =>
u.username.toLowerCase().includes(q) ||
(u.real_name && u.real_name.toLowerCase().includes(q))
);
} }
return [...list].sort((a, b) => (rolePriority[a.role] ?? 99) - (rolePriority[b.role] ?? 99)); return [...list].sort((a, b) => (rolePriority[a.role] ?? 99) - (rolePriority[b.role] ?? 99));
}); });
@@ -159,11 +205,8 @@ const fetchUsers = async () => {
loading.value = true; loading.value = true;
try { try {
const res = await accountApi.getAllUsers(); const res = await accountApi.getAllUsers();
if (res.code === 200) { if (res.code === 200) users.value = res.data || [];
users.value = res.data || []; else ElMessage.error(res.msg || "获取用户列表失败");
} else {
ElMessage.error(res.msg || "获取用户列表失败");
}
} catch (error) { } catch (error) {
ElMessage.error("获取用户数据失败"); ElMessage.error("获取用户数据失败");
} finally { } finally {
@@ -171,16 +214,24 @@ const fetchUsers = async () => {
} }
}; };
const fetchCompanyList = async () => {
try {
const res = await ticketApi.getOperatorsNew();
if (res.code === 200) {
companyList.value = (res.data || []).map(item => ({ company_id: item.id, company_name: item.company_name }));
console.log("公司列表:", companyList.value);
}
} catch (e) { /* 静默失败 */ }
};
const openRegisterDialog = () => { const openRegisterDialog = () => {
if (companyList.value.length === 0) fetchCompanyList();
registerDialogVisible.value = true; registerDialogVisible.value = true;
}; };
const resetRegisterForm = () => { const resetRegisterForm = () => {
if (registerFormRef.value) registerFormRef.value.resetFields(); if (registerFormRef.value) registerFormRef.value.resetFields();
registerForm.username = ""; Object.assign(registerForm, { username: "", real_name: "", password: "", role: "user", company_id: null });
registerForm.real_name = "";
registerForm.password = "";
registerForm.role = "user";
}; };
const handleRegister = async () => { const handleRegister = async () => {
@@ -197,9 +248,7 @@ const handleRegister = async () => {
ElMessage.error(res.msg || "注册失败"); ElMessage.error(res.msg || "注册失败");
} }
} catch (e) { } catch (e) {
if (e?.name !== "ValidationError") { if (e?.name !== "ValidationError") ElMessage.error("注册请求异常");
ElMessage.error("注册请求异常");
}
} finally { } finally {
submitting.value = false; submitting.value = false;
} }
@@ -208,9 +257,7 @@ const handleRegister = async () => {
const handleToggleStatus = (row) => { const handleToggleStatus = (row) => {
const newStatus = row.status === 1 ? 0 : 1; const newStatus = row.status === 1 ? 0 : 1;
const action = newStatus === 0 ? "禁用" : "启用"; const action = newStatus === 0 ? "禁用" : "启用";
ElMessageBox.confirm(`确定${action}用户【${row.username}】吗?`, "提示", { ElMessageBox.confirm(`确定${action}用户【${row.username}】吗?`, "提示", { type: "warning" })
type: "warning",
})
.then(async () => { .then(async () => {
const res = await accountApi.updateAccountStatus(row.id, newStatus); const res = await accountApi.updateAccountStatus(row.id, newStatus);
if (res.code === 200) { if (res.code === 200) {
@@ -220,50 +267,318 @@ const handleToggleStatus = (row) => {
ElMessage.error(res.msg || `${action}失败`); ElMessage.error(res.msg || `${action}失败`);
} }
}) })
.catch(() => {}); .catch(() => { });
}; };
const handleDeleteAccount = (row) => { const handleDeleteAccount = (row) => {
ElMessageBox.confirm( ElMessageBox.confirm(`确定删除用户【${row.username}】吗?此操作不可逆。`, "警告", {
`确定删除用户【${row.username}】吗?此操作不可逆。`, type: "error",
"警告", confirmButtonClass: "el-button--danger",
{ type: "error", confirmButtonClass: "el-button--danger" } })
)
.then(async () => { .then(async () => {
const res = await accountApi.deleteAccount(row.id); const res = await accountApi.deleteAccount(row.id);
if (res.code === 200) { if (res.code === 200) {
const idx = users.value.findIndex((u) => u.id === row.id); const idx = users.value.findIndex(u => u.id === row.id);
if (idx > -1) users.value.splice(idx, 1); if (idx > -1) users.value.splice(idx, 1);
ElMessage.success("删除成功"); ElMessage.success("删除成功");
} else { } else {
ElMessage.error(res.msg || "删除失败"); ElMessage.error(res.msg || "删除失败");
} }
}) })
.catch(() => {}); .catch(() => { });
}; };
onMounted(fetchUsers); onMounted(() => { fetchUsers(); fetchCompanyList(); });
</script> </script>
<style scoped> <style scoped>
.account-container { .account-root {
padding: 30px; padding: 0;
background-color: #f5f7fa;
min-height: 100vh;
} }
h2 { /* ---- 统计卡片 ---- */
margin-bottom: 25px; .stats-row {
color: #303133; display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 18px;
}
.stat-card {
background: var(--color-surface);
border: 1px solid var(--color-border-light);
border-left: 3px solid var(--border);
border-radius: var(--radius-md);
padding: 16px 18px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: var(--shadow-xs);
user-select: none;
cursor: pointer;
transition: box-shadow 0.2s, transform 0.2s, border-color 0.2s;
}
.stat-card:hover {
box-shadow: var(--shadow-sm);
transform: translateY(-1px);
border-color: var(--border);
}
.stat-card.active {
background: color-mix(in srgb, var(--border) 6%, #fff);
border-color: var(--border);
box-shadow: var(--shadow-sm);
}
.stat-left {
display: flex;
align-items: center;
gap: 10px;
}
.stat-icon-wrap {
width: 36px;
height: 36px;
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--bg) 12%, transparent);
color: var(--bg);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.stat-label {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
}
.stat-num {
font-size: 26px;
font-weight: 700;
color: var(--text-primary);
font-family: var(--font-mono);
}
/* ---- 主面板 ---- */
.main-card {
background: var(--color-surface);
border: 1px solid var(--color-border-light);
border-radius: var(--radius-md);
padding: 0;
box-shadow: var(--shadow-xs);
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 20px;
border-bottom: 1px solid var(--color-border-light);
}
.card-header :deep(.el-input__wrapper) {
border-radius: 20px !important;
}
.card-header :deep(.el-button) {
border-radius: 20px !important;
}
.header-left {
display: flex;
align-items: baseline;
gap: 10px;
}
.header-title {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
}
.header-count {
font-size: 12px;
color: var(--text-muted);
}
.header-right {
display: flex;
align-items: center;
gap: 10px;
}
/* ---- 表格 ---- */
:deep(.table-head) {
background: var(--color-bg) !important;
color: var(--text-secondary);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.3px;
text-transform: none;
}
:deep(.table-row) {
transition: background 0.15s;
}
:deep(.el-table__body tr:hover > td) {
background: #fafbfd !important;
}
/* 用户列 */
.user-cell {
display: flex;
align-items: center;
gap: 10px;
padding: 2px 0;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 13px;
font-weight: 600;
flex-shrink: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.user-name {
font-weight: 600;
font-size: 13px;
color: var(--text-primary);
}
.text-muted {
color: var(--text-muted);
}
/* 角色标签 */
.role-tag {
display: inline-block;
padding: 2px 10px;
border-radius: 10px;
font-size: 11px;
font-weight: 600; font-weight: 600;
} }
.action-bar { .role-tag.admin {
color: #dc2626;
background: #fef2f2;
}
.role-tag.user {
color: #d97706;
background: #fffbeb;
}
.role-tag.client {
color: #16a34a;
background: #f0fdf4;
}
/* 状态徽章 */
.status-badge {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 500;
color: #9ca3af;
}
.status-badge::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: #d1d5db;
flex-shrink: 0;
}
.status-badge.on {
color: #16a34a;
}
.status-badge.on::before {
background: #16a34a;
}
/* ---- 注册弹窗 ---- */
.register-dialog :deep(.el-dialog__header) {
padding: 20px 24px 0;
}
.register-dialog :deep(.el-dialog__body) {
padding: 20px 24px 4px;
}
.register-dialog :deep(.el-dialog__footer) {
padding: 0 24px 20px;
}
.register-dialog :deep(.el-form-item) {
margin-bottom: 20px;
}
.register-dialog :deep(.el-form-item__label) {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
padding-bottom: 4px;
}
/* 角色选择按钮 */
.role-group {
display: flex; display: flex;
justify-content: space-between; gap: 10px;
background: #fff; }
padding: 15px 20px;
border-radius: 8px; .role-btn {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8px;
padding: 16px 12px;
border: 2px solid var(--color-border-light);
border-radius: 12px;
background: #f8f9fb;
color: var(--text-muted);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
white-space: nowrap;
}
.role-btn:hover {
border-color: var(--accent);
color: var(--accent);
background: color-mix(in srgb, var(--accent) 4%, #f8f9fb);
}
.role-btn.selected {
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 12%, #fff);
color: var(--accent);
font-weight: 600;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent);
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
} }
</style> </style>

View File

@@ -1,131 +1,137 @@
<template> <template>
<div class="client-view-container"> <div class="cv-wrap">
<header class="client-header"> <!-- Header -->
<div class="brand"> <header class="cv-header">
<div class="logo-box"> <div class="cv-brand">
<el-icon> <div class="cv-logo">
<DataLine /> <el-icon><DataLine /></el-icon>
</el-icon>
</div> </div>
<div class="brand-text"> <div>
<span class="title">{{ currentUser.company_name || "数据交付监控台" }}</span> <div class="cv-title">方舟星河</div>
<span class="version">Real-time Monitor v2.0</span> <div class="cv-subtitle">采集数据平台 · Monitor v2.0</div>
</div> </div>
</div> </div>
<div class="user-info"> <div class="cv-user">
<el-space :size="20"> <div class="cv-avatar">{{ currentUser.username?.charAt(0) || 'U' }}</div>
<div class="user-badge"> <span class="cv-username">{{ currentUser.username }}</span>
<el-icon> <el-button class="cv-logout-btn" @click="handleLogout" :icon="SwitchButton" size="small">退出</el-button>
<User />
</el-icon>
<span>{{ currentUser.username }}</span>
</div>
<el-button type="danger" plain size="small" @click="handleLogout" :icon="SwitchButton">退出</el-button>
</el-space>
</div> </div>
</header> </header>
<section class="client-stats"> <!-- Stats -->
<el-row :gutter="20"> <section class="cv-stats">
<el-col :span="6" v-for="stat in summaryStats" :key="stat.label"> <div
<div class="stat-card-premium" :class="{ 'is-active': activeStatus === stat.status }" v-for="stat in summaryStats"
:style="{ '--theme-color': stat.color }" @click="handleStatFilter(stat.status)"> :key="stat.label"
<div class="stat-info"> class="cv-stat-card"
<span class="s-label">{{ stat.label }}</span> :class="{ 'is-active': activeStatus === stat.status }"
<span class="s-value">{{ stat.value }}</span> :style="{ '--c': stat.color }"
</div> @click="handleStatFilter(stat.status)"
<div class="stat-icon-wrapper"> >
<el-icon> <div class="cv-stat-icon">
<component :is="stat.icon" /> <el-icon><component :is="stat.icon" /></el-icon>
</el-icon> </div>
</div> <div class="cv-stat-body">
<div v-if="activeStatus === stat.status" class="active-dot"></div> <div class="cv-stat-val">{{ stat.value }}</div>
</div> <div class="cv-stat-label">{{ stat.label }}</div>
</el-col> </div>
</el-row> <div class="cv-stat-bar"></div>
</div>
</section> </section>
<main class="data-section"> <!-- Table Panel -->
<div class="section-header"> <main class="cv-panel">
<div class="header-left"> <div class="cv-panel-header">
<span class="indicator"></span> <div class="cv-panel-title">
<span class="section-title">实时交付看板</span> <span class="cv-title-dot"></span>
<el-tag v-if="activeStatus" closable size="small" type="info" class="filter-tag" @close="activeStatus = null"> 实时交付看板
当前筛选: {{ activeStatus }} <el-tag
</el-tag> v-if="activeStatus"
</div> closable
<div class="header-right"> size="small"
<el-button type="primary" link @click="loadData" :loading="loading"> class="cv-filter-tag"
<el-icon> @close="activeStatus = null"
<Refresh /> >{{ activeStatus }}</el-tag>
</el-icon>&nbsp;同步最新数据
</el-button>
</div> </div>
<el-button type="primary" link :loading="loading" @click="loadData" :icon="Refresh" size="small">
同步数据
</el-button>
</div> </div>
<el-table :data="paginatedTickets" v-loading="loading" class="custom-client-table"> <el-table
<el-table-column prop="id" label="单号" width="90" align="center"> :data="paginatedTickets"
v-loading="loading"
class="cv-table"
:header-cell-style="{ background: 'var(--cv-th-bg)', color: 'var(--text-muted)', fontWeight: '500', fontSize: '12px' }"
:row-style="{ height: '52px' }"
>
<el-table-column label="单号" width="80" align="center">
<template #default="{ row }"> <template #default="{ row }">
<span class="id-tag">#{{ row.id }}</span> <span class="cv-id">#{{ row.id }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="website" label="网站地址" min-width="260"> <el-table-column label="网站地址" min-width="240">
<template #default="{ row }"> <template #default="{ row }">
<div class="project-cell"> <div class="cv-site-cell">
<el-icon class="project-icon"> <div class="cv-site-favicon">
<Monitor /> <el-icon><Monitor /></el-icon>
</el-icon>
<span class="project-name" :title="row.website">{{ row.website }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="交付进度" width="260">
<template #default="scope">
<div class="progress-box">
<div class="progress-labels">
<span class="percent-val">{{ scope.row.progress || 0 }}%</span>
</div> </div>
<el-progress :percentage="Number(scope.row.progress || 0)" <span class="cv-site-name" :title="row.website">{{ row.website }}</span>
:status="Number(scope.row.progress) === 100 ? 'success' : ''" :stroke-width="10" :show-text="false"
striped striped-flow />
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="datanum" label="总数据量" width="160" align="center"> <el-table-column label="交付进度" width="220">
<template #default="scope"> <template #default="{ row }">
<div class="data-val-box"> <div class="cv-progress-cell">
<span class="data-num">{{ formatNumber(scope.row.datanum) }}</span> <el-progress
:percentage="Number(row.progress || 0)"
:status="Number(row.progress) === 100 ? 'success' : ''"
:stroke-width="6"
:show-text="false"
striped
striped-flow
/>
<span class="cv-progress-pct" :class="{ 'is-done': Number(row.progress) === 100 }">
{{ row.progress || 0 }}%
</span>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="交付状态" width="120" align="center"> <el-table-column label="数据量" width="130" align="right">
<template #default="scope"> <template #default="{ row }">
<div :class="['status-dot-box', getStatusClass(scope.row.is_complete)]"> <span class="cv-datanum">{{ formatNumber(row.datanum) }}</span>
<span class="dot"></span>
<span class="text">{{ scope.row.is_complete }}</span>
</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="更新日期" width="140" align="center"> <el-table-column label="状态" width="110" align="center">
<template #default="scope"> <template #default="{ row }">
<span class="date-text">{{ <span :class="['cv-status-badge', getStatusClass(row.status)]">
formatDate( <i class="cv-status-dot"></i>{{ statusLabel(row.status) }}
scope.row.lock_time || scope.row.update_time || scope.row.create_time </span>
) </template>
}}</span> </el-table-column>
<el-table-column label="更新时间" width="120" align="center">
<template #default="{ row }">
<span class="cv-date">{{ formatDate(row.lock_time || row.update_time || row.create_time) }}</span>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<footer class="pagination-footer"> <div class="cv-footer">
<el-pagination v-model:current-page="page" v-model:page-size="limit" :total="total" <el-pagination
layout="total, prev, pager, next" background @current-change="handlePageChange" /> v-model:current-page="page"
</footer> v-model:page-size="limit"
:total="total"
layout="total, prev, pager, next"
background
small
@current-change="handlePageChange"
/>
</div>
</main> </main>
</div> </div>
</template> </template>
@@ -133,17 +139,12 @@
<script setup> <script setup>
import { ref, computed, onMounted } from "vue"; import { ref, computed, onMounted } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { resetRouter } from "@/router";
import { import {
DataLine, DataLine, SwitchButton, User, Refresh,
SwitchButton, Files, Timer, CircleCheck, Monitor,
User,
Refresh,
Files,
Timer,
CircleCheck,
Monitor,
} from "@element-plus/icons-vue"; } from "@element-plus/icons-vue";
import { ticketApi, userApi } from "@/api/ticket"; import { clientApi, userApi } from "@/api/ticket";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { formatDate } from "@/utils/format"; import { formatDate } from "@/utils/format";
@@ -152,387 +153,217 @@ const loading = ref(false);
const tickets = ref([]); const tickets = ref([]);
const page = ref(1); const page = ref(1);
const limit = ref(15); const limit = ref(15);
const activeStatus = ref(null);
// --- 筛选状态控制 --- const statusMap = { 0: "未分配", 1: "进行中", 2: "已完成", 3: "已完结" };
const activeStatus = ref(null); // null 代表显示全部,其余为具体状态字符串 const statusLabel = (s) => statusMap[Number(s)] || "未知";
// --- 1. 用户信息 ---
const currentUser = computed(() => { const currentUser = computed(() => {
const info = localStorage.getItem("user_info") || sessionStorage.getItem("user_info"); const info = localStorage.getItem("user_info") || sessionStorage.getItem("user_info");
return info ? JSON.parse(info) : { username: "外部客户", customer_id: null }; return info ? JSON.parse(info) : { username: "访客" };
}); });
// --- 2. 数据加载 ---
const loadData = async () => { const loadData = async () => {
loading.value = true; loading.value = true;
try { try {
const customerId = currentUser.value.customer_id; const res = await clientApi.getTickets();
const res = await ticketApi.getTickets({ if (res.code === 200) tickets.value = res.data || [];
page: 1, } catch {
limit: 1000, // 客户侧通常数据量不大,取1000条前端分页
customer_id: customerId,
});
if (res.code === 200 || res.code === true) {
tickets.value = res.data.data;
}
} catch (e) {
ElMessage.error("数据同步失败"); ElMessage.error("数据同步失败");
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; };
// --- 3. 核心:双重过滤逻辑 (权限过滤 + 卡片点击过滤) --- const baseTickets = computed(() => tickets.value.filter((t) => Number(t.status) !== 3));
const filteredTickets = computed(() => { const filteredTickets = computed(() => {
const allowed = ["未分配", "进行中", "已完成"]; if (!activeStatus.value) return baseTickets.value;
// 第一步:过滤掉“已完结”等不需展示给客户的状态 return baseTickets.value.filter((t) => statusLabel(t.status) === activeStatus.value);
let result = tickets.value.filter((t) => allowed.includes(t.is_complete));
// 第二步:根据卡片点击的状态进行筛选
if (activeStatus.value) {
result = result.filter((t) => t.is_complete === activeStatus.value);
}
return result;
}); });
const paginatedTickets = computed(() => { const paginatedTickets = computed(() => {
const start = (page.value - 1) * limit.value; const start = (page.value - 1) * limit.value;
return filteredTickets.value.slice(start, start + limit.value); return filteredTickets.value.slice(start, start + limit.value);
}); });
const total = computed(() => filteredTickets.value.length); const total = computed(() => filteredTickets.value.length);
// --- 4. 统计卡片数据定义 --- const summaryStats = computed(() => [
const summaryStats = computed(() => { { label: "采集项目总数", value: baseTickets.value.length, color: "#6366f1", icon: Monitor, status: null },
// 基础权限过滤后的原始数组 { label: "等待排期", value: baseTickets.value.filter((t) => Number(t.status) === 0).length, color: "#94a3b8", icon: Files, status: "未分配" },
const baseData = tickets.value.filter((t) => { label: "正在采集", value: baseTickets.value.filter((t) => Number(t.status) === 1).length, color: "#f59e0b", icon: Timer, status: "进行中" },
["未分配", "进行中", "已完成"].includes(t.is_complete) { label: "已就绪", value: baseTickets.value.filter((t) => Number(t.status) === 2).length, color: "#10b981", icon: CircleCheck, status: "已完成" },
); ]);
return [
{
label: "项目总数",
value: baseData.length,
color: "#409EFF",
icon: Monitor,
status: null,
},
{
label: "等待排期",
value: baseData.filter((t) => t.is_complete === "未分配").length,
color: "#A8ABB2",
icon: Files,
status: "未分配",
},
{
label: "正在采集",
value: baseData.filter((t) => t.is_complete === "进行中").length,
color: "#E6A23C",
icon: Timer,
status: "进行中",
},
{
label: "已就绪",
value: baseData.filter((t) => t.is_complete === "已完成").length,
color: "#67C23A",
icon: CircleCheck,
status: "已完成",
},
];
});
// --- 5. 交互方法 ---
const handleStatFilter = (status) => {
activeStatus.value = status;
page.value = 1; // 切换筛选时重置到第一页
};
const handleStatFilter = (status) => { activeStatus.value = status; page.value = 1; };
const formatNumber = (val) => { const formatNumber = (val) => {
if (!val || val === "0") return "0"; if (!val || val === "0") return "—";
const num = Number(val); const n = Number(val);
return isNaN(num) ? val : num.toLocaleString(); return isNaN(n) ? val : n.toLocaleString();
}; };
const getStatusClass = (s) => ({ 0: "s-wait", 1: "s-run", 2: "s-done" })[Number(s)] || "";
const getStatusClass = (status) => { const handlePageChange = (p) => { page.value = p; window.scrollTo({ top: 0, behavior: "smooth" }); };
const map = { 未分配: "is-waiting", 进行中: "is-running", 已完成: "is-success" };
return map[status] || "";
};
const handlePageChange = (p) => {
page.value = p;
window.scrollTo({ top: 0, behavior: "smooth" });
};
const handleLogout = async () => { const handleLogout = async () => {
try { try { await userApi.logout(); } catch {}
await userApi.logout(); resetRouter();
} catch (e) {} router.push({ name: "login" });
localStorage.removeItem("user_info");
sessionStorage.removeItem("user_info");
router.push("/login");
}; };
onMounted(loadData); onMounted(loadData);
</script> </script>
<style scoped> <style scoped>
.client-view-container { /* ── tokens ── */
padding: 24px 40px; .cv-wrap {
background-color: #f6f8fb; --cv-th-bg: #f8f9fb;
min-height: 100vh; --cv-radius: 14px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; padding: 8px 4px 24px;
} }
/* 头部样式 */ /* ── Header ── */
.client-header { .cv-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 28px;
padding: 0 2px;
}
.cv-brand { display: flex; align-items: center; gap: 14px; }
.cv-logo {
width: 42px; height: 42px;
border-radius: 12px;
background: linear-gradient(135deg, #6366f1, #818cf8);
display: flex; align-items: center; justify-content: center;
color: #fff; font-size: 20px;
box-shadow: 0 4px 14px rgba(99,102,241,.32);
flex-shrink: 0;
}
.cv-title { font-size: 17px; font-weight: 700; color: var(--text-primary); line-height: 1.3; letter-spacing: -0.2px; }
.cv-subtitle { font-size: 11px; color: var(--text-muted); margin-top: 2px; letter-spacing: 0.2px; }
.cv-user { display: flex; align-items: center; gap: 12px; }
.cv-avatar {
width: 32px; height: 32px; border-radius: 50%;
background: linear-gradient(135deg, #6366f1, #a78bfa);
color: #fff; font-size: 13px; font-weight: 600;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 2px 8px rgba(99,102,241,.25);
}
.cv-username { font-size: 13px; color: var(--text-secondary); }
.cv-logout-btn { border-radius: 8px !important; }
/* ── Stats ── */
.cv-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px; margin-bottom: 24px;
} }
.cv-stat-card {
.brand { background: var(--color-surface);
border: 1px solid var(--color-border-light);
border-radius: var(--cv-radius);
padding: 20px 22px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 16px;
} cursor: pointer;
.logo-box {
background: linear-gradient(135deg, #409eff, #007aff);
width: 38px;
height: 38px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 20px;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.35);
}
.title {
font-size: 19px;
font-weight: 700;
color: #1d1d1f;
}
.version {
font-size: 10px;
color: #a1a1a6;
margin-left: 8px;
letter-spacing: 0.5px;
}
.user-badge {
display: flex;
align-items: center;
gap: 8px;
background: #fff;
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
color: #606266;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
/* 统计卡片交互样式 */
.stat-card-premium {
background: #fff;
padding: 22px;
border-radius: 12px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.03); transition: transform 0.18s, box-shadow 0.18s, border-color 0.18s;
border-left: 4px solid var(--theme-color);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
cursor: pointer;
} }
.cv-stat-card:hover {
.stat-card-premium:hover {
transform: translateY(-3px); transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08); box-shadow: 0 8px 24px rgba(0,0,0,.08);
} }
.cv-stat-card.is-active {
.stat-card-premium.is-active { border-color: var(--c);
background: #f0f7ff; background: color-mix(in srgb, var(--c) 5%, #fff);
border-left-width: 8px; box-shadow: 0 6px 20px color-mix(in srgb, var(--c) 18%, transparent);
box-shadow: inset 0 0 0 1px rgba(64, 158, 255, 0.1), 0 8px 20px rgba(0, 0, 0, 0.1);
} }
.cv-stat-bar {
.active-dot {
position: absolute; position: absolute;
top: 10px; bottom: 0; left: 0; right: 0;
right: 10px; height: 3px;
width: 6px; background: var(--c);
height: 6px; opacity: 0;
background: var(--theme-color); transition: opacity 0.18s;
border-radius: 50%; border-radius: 0 0 var(--cv-radius) var(--cv-radius);
box-shadow: 0 0 8px var(--theme-color);
animation: pulse 2s infinite;
} }
.cv-stat-card.is-active .cv-stat-bar,
.cv-stat-card:hover .cv-stat-bar { opacity: 1; }
@keyframes pulse { .cv-stat-icon {
0% { width: 46px; height: 46px; border-radius: 12px;
opacity: 1; background: color-mix(in srgb, var(--c) 10%, transparent);
} color: var(--c);
display: flex; align-items: center; justify-content: center;
50% { font-size: 22px;
opacity: 0.3; flex-shrink: 0;
}
100% {
opacity: 1;
}
} }
.cv-stat-val { font-size: 28px; font-weight: 700; color: var(--text-primary); line-height: 1.1; letter-spacing: -0.5px; }
.cv-stat-label { font-size: 11px; color: var(--text-muted); margin-top: 4px; letter-spacing: 0.1px; }
.s-label { /* ── Panel ── */
font-size: 13px; .cv-panel {
color: #86868b; background: var(--color-surface);
display: block; border: 1px solid var(--color-border-light);
margin-bottom: 6px; border-radius: var(--cv-radius);
padding: 24px 28px 20px;
} }
.cv-panel-header {
.s-value {
font-size: 28px;
font-weight: 800;
color: #1d1d1f;
}
.stat-icon-wrapper {
font-size: 36px;
color: var(--theme-color);
opacity: 0.1;
}
/* 列表区域 */
.data-section {
background: #fff;
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02);
}
.section-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px; margin-bottom: 20px;
} }
.cv-panel-title {
.header-left { display: flex; align-items: center; gap: 10px;
display: flex; font-size: 14px; font-weight: 600; color: var(--text-primary);
align-items: center;
gap: 10px;
} }
.cv-title-dot {
.indicator { width: 3px; height: 16px;
width: 3px; background: linear-gradient(180deg, #6366f1, #a78bfa);
height: 16px;
background: #409eff;
border-radius: 2px; border-radius: 2px;
} }
.cv-filter-tag { border-radius: 6px; }
.section-title { /* ── Table cells ── */
font-size: 16px; .cv-table { width: 100%; }
font-weight: 600; .cv-id { font-family: var(--font-mono); font-size: 12px; color: var(--text-muted); }
color: #303133;
.cv-site-cell { display: flex; align-items: center; gap: 10px; }
.cv-site-favicon {
width: 28px; height: 28px; border-radius: 7px;
background: var(--cv-th-bg);
display: flex; align-items: center; justify-content: center;
color: var(--text-muted); font-size: 13px; flex-shrink: 0;
}
.cv-site-name {
font-size: 13px; font-weight: 500; color: var(--text-primary);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 220px;
} }
.filter-tag { .cv-progress-cell { display: flex; align-items: center; gap: 10px; }
margin-left: 10px; .cv-progress-cell .el-progress { flex: 1; }
border-radius: 4px; .cv-progress-pct { font-size: 11px; font-weight: 600; color: var(--text-muted); width: 32px; text-align: right; flex-shrink: 0; }
} .cv-progress-pct.is-done { color: #10b981; }
.id-tag { .cv-datanum { font-family: var(--font-mono); font-size: 13px; font-weight: 600; color: var(--text-primary); }
color: #909399; .cv-date { font-size: 12px; color: var(--text-muted); }
font-family: "Consolas", monospace;
font-size: 12px;
}
.project-cell { /* ── Status badge ── */
display: flex; .cv-status-badge {
align-items: center; display: inline-flex; align-items: center; gap: 5px;
gap: 8px; padding: 4px 11px; border-radius: 20px;
font-size: 11px; font-weight: 500;
} }
.cv-status-dot {
width: 5px; height: 5px; border-radius: 50%;
background: currentColor; display: inline-block;
}
.s-wait { background: #f1f5f9; color: #94a3b8; }
.s-run { background: #fffbeb; color: #f59e0b; }
.s-done { background: #ecfdf5; color: #10b981; }
.project-icon { /* ── Footer ── */
color: #c0c4cc; .cv-footer { margin-top: 24px; padding-bottom: 4px; display: flex; justify-content: center; }
font-size: 16px;
}
.project-name {
font-weight: 500;
color: #2c3e50;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 240px;
}
.percent-val {
font-size: 11px;
font-weight: 700;
color: #409eff;
}
.data-num {
font-size: 16px;
font-weight: 700;
color: #2c3e50;
}
.status-dot-box {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 500;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.is-waiting {
color: #909399;
}
.is-waiting .dot {
background: #909399;
}
.is-running {
color: #e6a23c;
}
.is-running .dot {
background: #e6a23c;
box-shadow: 0 0 6px rgba(230, 162, 60, 0.4);
}
.is-success {
color: #67c23a;
}
.is-success .dot {
background: #67c23a;
}
.pagination-footer {
margin-top: 24px;
display: flex;
justify-content: center;
}
:deep(.el-table) {
--el-table-header-bg-color: #f8f9fb;
}
</style> </style>

View File

@@ -1,54 +1,58 @@
<template> <template>
<div class="customers-container"> <div class="customers-root">
<h2>运营人员管理</h2> <!-- 主面板 -->
<div class="main-card">
<div class="card-header">
<div class="header-left">
<span class="header-title">客户单位</span>
<span class="header-count">{{ filteredData.length }} 个</span>
</div>
<div class="header-right">
<el-input v-model="searchQuery" placeholder="搜索客户单位" style="width: 200px" size="default" clearable
:prefix-icon="Search" />
<el-button type="primary" :icon="Plus" @click="customerDialogVisible = true">
新增客户单位
</el-button>
<el-button type="success" :icon="User" @click="handleAddOperator">
新增运营人员
</el-button>
</div>
</div>
<div class="action-bar"> <el-table :data="filteredData" v-loading="loading" header-cell-class-name="table-head" row-class-name="table-row">
<div class="search-group"> <template #empty>
<el-input v-model="searchQuery" placeholder="搜索客户单位" style="width: 250px" clearable /> <el-empty :image-size="80" description="暂无客户数据" />
</div> </template>
<div class="button-group">
<el-button type="primary" :icon="Plus" @click="customerDialogVisible = true"> <el-table-column prop="id" label="ID" width="80" align="center" />
新增客户单位 <el-table-column prop="company_name" label="客户单位名称" min-width="200">
</el-button> <template #default="{ row }">
<el-button type="success" :icon="User" @click="handleAddOperator"> <span class="company-name">{{ row.company_name }}</span>
新增运营人员 </template>
</el-button> </el-table-column>
</div>
<el-table-column label="运营人员数量" width="140" align="center">
<template #default="scope">
<b class="operator-count">{{ scope.row.companyoperators.length }}</b> 人
</template>
</el-table-column>
<el-table-column label="操作" width="140" fixed="right" align="center">
<template #default="scope">
<el-button size="small" type="primary" link @click="showCompanyAndOperatorsDetails(scope.row)">
查看
</el-button>
<el-button size="small" type="danger" link @click="handleDeleteCompany(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div> </div>
<el-table :data="companyAndOperators" border style="width: 100%; margin-top: 20px" v-loading="loading">
<template #empty>
<el-empty :image-size="100" description="暂无客户数据" />
</template>
<el-table-column prop="id" label="客户ID" width="100" />
<el-table-column prop="company_name" label="客户单位名称" />
<el-table-column label="员工管理" width="250" align="center">
<template #default="scope">
<span style="margin-right: 15px">
共 <b style="color: #409eff">{{ scope.row.companyoperators.length }}</b> 人
</span>
<el-button size="small" type="primary" link :icon="View" @click="showCompanyAndOperatorsDetails(scope.row)">
查看详情
</el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="scope">
<el-button size="small" type="danger" plain @click="handleDeleteCompany(scope.row)">
删除单位
</el-button>
</template>
</el-table-column>
</el-table>
<CompanyEditDialog v-model="customerDialogVisible" @success="fetchCompanyAndOperators" /> <CompanyEditDialog v-model="customerDialogVisible" @success="fetchCompanyAndOperators" />
<OperatorEditDialog v-model="operatorDialogVisible" :company-list="companyList" <OperatorEditDialog v-model="operatorDialogVisible" :company-list="companyList"
@success="fetchCompanyAndOperators" /> @success="fetchCompanyAndOperators" />
<OperatorListDialog v-model="employeeDetailVisible" :company-name="activeCompanyName" <OperatorListDialog v-model="employeeDetailVisible" :company-name="activeCompanyName"
:operators="currentCompanyOperators" @refresh="fetchCompanyAndOperators" /> :operators="currentCompanyOperators" @refresh="fetchCompanyAndOperators" />
</div> </div>
@@ -56,7 +60,7 @@
<script setup> <script setup>
import { ref, computed, onMounted } from "vue"; import { ref, computed, onMounted } from "vue";
import { Plus, User, View } from "@element-plus/icons-vue"; import { Plus, Search, User, View } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { customerApi, ticketApi } from "@/api/ticket"; import { customerApi, ticketApi } from "@/api/ticket";
@@ -66,7 +70,6 @@ import OperatorListDialog from "@/components/operators/OperatorListDialog.vue";
const loading = ref(false); const loading = ref(false);
const companyList = ref([]); const companyList = ref([]);
const employeeList = ref([]);
const searchQuery = ref(""); const searchQuery = ref("");
const companyAndOperators = ref([]); const companyAndOperators = ref([]);
@@ -75,19 +78,23 @@ const operatorDialogVisible = ref(false);
const employeeDetailVisible = ref(false); const employeeDetailVisible = ref(false);
const activeCompanyName = ref(""); // 当前选中客户单位的名称 const activeCompanyName = ref("");
const currentCompanyOperators = ref([]); // 当前选中客户单位的员工列表 const currentCompanyOperators = ref([]);
const filteredData = computed(() => {
if (!searchQuery.value) return companyAndOperators.value;
const q = searchQuery.value.toLowerCase();
return companyAndOperators.value.filter(c => c.company_name.toLowerCase().includes(q));
});
const fetchCompanyAndOperators = async () => { const fetchCompanyAndOperators = async () => {
loading.value = true; loading.value = true;
try { try {
const response = await ticketApi.getOperatorsNew(); const response = await ticketApi.getOperatorsNew();
if (response.code === 200) { if (response.code === 200) {
companyAndOperators.value = response.data companyAndOperators.value = response.data.filter(item => item.id !== 0)
companyList.value = response.data.map(item => ({ company_id: item.id, company_name: item.company_name })) companyList.value = response.data.map(item => ({ company_id: item.id, company_name: item.company_name }))
} }
// console.log('获取数据', companyAndOperators.value);
} catch (error) { } catch (error) {
ElMessage.error("获取客户数据失败,请检查网络"); ElMessage.error("获取客户数据失败,请检查网络");
} finally { } finally {
@@ -132,28 +139,88 @@ onMounted(fetchCompanyAndOperators);
</script> </script>
<style scoped> <style scoped>
.customers-container { .customers-root {
padding: 30px; padding: 0;
background-color: #f5f7fa;
min-height: 100vh;
} }
h2 { /* 圆角 */
margin-bottom: 25px; .header-right :deep(.el-input__wrapper) {
color: #303133; border-radius: 20px !important;
font-weight: 600;
} }
.action-bar { .header-right :deep(.el-button) {
border-radius: 20px !important;
}
/* ---- 主面板 ---- */
.main-card {
background: #f5f6f8;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 12px;
padding: 0;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.04),
0 4px 16px rgba(0, 0, 0, 0.035);
overflow: hidden;
}
.card-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
background: #fff; align-items: center;
padding: 15px 20px; padding: 16px 20px;
border-radius: 8px; border-bottom: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
} }
.button-group .el-button { .header-left {
margin-left: 10px; display: flex;
align-items: baseline;
gap: 10px;
}
.header-title {
font-size: 15px;
font-weight: 600;
color: #1c1c28;
}
.header-count {
font-size: 12px;
color: #9a9ba7;
}
.header-right {
display: flex;
align-items: center;
gap: 14px;
}
/* ---- 表格 ---- */
:deep(.table-head) {
background: #f0f1f4 !important;
color: #6b6d7a;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.3px;
}
:deep(.table-row) {
transition: background 0.15s;
}
:deep(.el-table__body tr:hover > td) {
background: rgba(0, 0, 0, 0.015) !important;
}
.company-name {
font-weight: 500;
color: #1c1c28;
}
.operator-count {
font-size: 15px;
color: #4f46e5;
font-weight: 700;
font-family: var(--font-mono);
} }
</style> </style>

View File

@@ -1,110 +1,97 @@
<template> <template>
<div class="ark-wrapper"> <div class="login-root">
<div class="grid-bg"></div> <div class="login-card">
<div class="scan-line"></div> <div class="card-head">
<img src="@/assets/logo.jpg" alt="" class="logo" />
<h1 class="title">方舟工单</h1>
<p class="desc">登录以继续使用系统</p>
</div>
<div class="login-container"> <el-form
<transition name="ark-fade" appear> ref="formRef"
<div class="ark-card"> :model="form"
<div class="corner-decorator top-left"></div> :rules="rules"
<div class="corner-decorator bottom-right"></div> class="login-form"
@keyup.enter="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="form.username"
placeholder="员工编号"
size="large"
clearable
>
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
<div class="card-header"> <el-form-item prop="password">
<div class="brand-icon"> <el-input
<el-icon> v-model="form.password"
<DataBoard /> type="password"
</el-icon> placeholder="访问密钥"
</div> size="large"
<div class="brand-text"> show-password
<h1 class="title">方舟工单</h1> >
<span class="sub-text">ARK TICKET SYSTEM v4.0</span> <template #prefix>
</div> <el-icon><Lock /></el-icon>
</div> </template>
</el-input>
</el-form-item>
<div class="system-monitor"> <div class="form-row">
<span class="label">STATUS:</span> <el-checkbox v-model="rememberMe">保持登录</el-checkbox>
<span class="value online">ENCRYPTED CONNECTION</span>
</div>
<el-form :model="loginForm" :rules="loginRules" ref="loginFormRef" class="ark-form">
<el-form-item prop="username">
<div class="input-prefix">ID</div>
<el-input v-model="loginForm.username" placeholder="员工编号" class="ark-input" />
</el-form-item>
<el-form-item prop="password">
<div class="input-prefix">KEY</div>
<el-input v-model="loginForm.password" type="password" placeholder="访问密钥" show-password class="ark-input"
@keyup.enter="handleLogin" />
</el-form-item>
<div class="options-bar">
<el-checkbox v-model="rememberMe" @keyup.enter="handleLogin">保持认证状态</el-checkbox>
</div>
<!--loading为true时候,会转圈圈-->
<el-button type="primary" class="ark-btn" @click="handleLogin" :loading="loading">
<div class="btn-content">
<span class="main-btn-text">INITIALIZE / 启动</span>
</div>
</el-button>
</el-form>
<div class="card-footer">
<span class="code">SECURE PROTOCOL 801-ARK</span>
</div>
</div> </div>
</transition>
<el-button
type="primary"
size="large"
class="submit-btn"
:loading="loading"
@click="handleLogin"
>
登 录
</el-button>
</el-form>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive } from "vue"; import { ref, reactive } from "vue";
import { DataBoard } from "@element-plus/icons-vue"; import { User, Lock } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { userApi } from "@/api/ticket"; import { userApi } from "@/api/ticket";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const loginForm = reactive({ username: "", password: "" }); const form = reactive({ username: "", password: "" });
const rememberMe = ref(false); const rememberMe = ref(false);
const loading = ref(false); const loading = ref(false);
const loginFormRef = ref(null); const formRef = ref(null);
const loginRules = reactive({ const rules = {
username: [{ required: true, message: "请输入员工编号", trigger: "blur" }], username: [{ required: true, message: "请输入员工编号", trigger: "blur" }],
password: [{ required: true, message: "请输入密钥", trigger: "blur" }], password: [{ required: true, message: "请输入密钥", trigger: "blur" }],
}); };
const handleLogin = async () => { const handleLogin = async () => {
if (!loginFormRef.value) return; if (!formRef.value) return;
try { try {
await loginFormRef.value.validate(); await formRef.value.validate();
loading.value = true; loading.value = true;
const res = await userApi.login(form.username, form.password);
const res = await userApi.login(loginForm.username, loginForm.password);
if (res.code === 200) { if (res.code === 200) {
// ThinkPHP 会自动通过 Set-Cookie 下发 session,浏览器自动接管,无需手动存 token const userInfo = JSON.stringify(res.data);
const userInfo = res.data;
const userInfoStr = JSON.stringify(userInfo);
// 用户信息存前端,仅用于显示用户名和角色路由判断
const storage = rememberMe.value ? localStorage : sessionStorage; const storage = rememberMe.value ? localStorage : sessionStorage;
storage.setItem("user_info", userInfoStr); storage.setItem("user_info", userInfo);
ElMessage({ message: "登录成功", type: "success", duration: 300 });
ElMessage({
message: "身份验证通过,方舟系统加载中...",
type: "success",
duration: 300,
});
// 触发路由守卫,自动处理跳转
await router.push(route.query.redirect || "/"); await router.push(route.query.redirect || "/");
} else { } else {
ElMessage.error(res.message || "登录失败,请检查账号和密码"); ElMessage.error(res.message || "登录失败");
} }
} catch (error) { } catch (error) {
if (error.name !== "ValidationError") { if (error.name !== "ValidationError") {
@@ -117,243 +104,104 @@ const handleLogin = async () => {
</script> </script>
<style scoped> <style scoped>
/* 1. 全局背景:工业网格风格 */ .login-root {
.ark-wrapper {
position: relative;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background-color: #0b111a;
display: flex; display: flex;
align-items: center;
justify-content: center; justify-content: center;
align-items: center; background: linear-gradient(165deg, #f6f7fa 0%, #edeff4 40%, #f2f3f7 100%);
overflow: hidden;
} }
.grid-bg { .login-card {
position: absolute; width: 380px;
inset: 0; background: #fcfcfd;
background-image: linear-gradient(rgba(0, 242, 255, 0.05) 1px, transparent 1px), border: 1px solid rgba(0, 0, 0, 0.04);
linear-gradient(90deg, rgba(0, 242, 255, 0.05) 1px, transparent 1px); border-radius: 16px;
background-size: 40px 40px; padding: 44px 38px 38px;
mask-image: radial-gradient(circle at center, black, transparent 80%); box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.02),
0 2px 6px rgba(0, 0, 0, 0.02),
0 12px 40px rgba(0, 0, 0, 0.04),
0 24px 80px rgba(0, 0, 0, 0.03);
user-select: none;
} }
.scan-line { .card-head {
position: absolute; text-align: center;
width: 100%; margin-bottom: 34px;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(0, 242, 255, 0.2), transparent);
top: 0;
animation: scan 4s linear infinite;
} }
@keyframes scan { .logo {
0% { width: 52px;
top: -10%; height: 52px;
} border-radius: 14px;
object-fit: cover;
100% { margin-bottom: 16px;
top: 110%; box-shadow:
} 0 0 0 4px rgba(91, 95, 227, 0.06),
} 0 4px 12px rgba(91, 95, 227, 0.12);
/* 2. 方舟卡片 */
.ark-card {
position: relative;
width: 420px;
background: rgba(13, 22, 34, 0.85);
border: 1px solid rgba(0, 242, 255, 0.3);
padding: 40px;
box-shadow: 0 0 40px rgba(0, 242, 255, 0.1), inset 0 0 20px rgba(0, 242, 255, 0.05);
backdrop-filter: blur(10px);
}
/* 装饰性角标 */
.corner-decorator {
position: absolute;
width: 20px;
height: 20px;
border-color: #00f2ff;
border-style: solid;
}
.top-left {
top: -2px;
left: -2px;
border-width: 3px 0 0 3px;
}
.bottom-right {
bottom: -2px;
right: -2px;
border-width: 0 3px 3px 0;
}
.card-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 30px;
}
.brand-icon {
background: rgba(0, 242, 255, 0.1);
padding: 10px;
border: 1px solid #00f2ff;
display: flex;
color: #00f2ff;
clip-path: polygon(20% 0%, 100% 0%, 100% 80%, 80% 100%, 0% 100%, 0% 20%);
} }
.title { .title {
color: #fff; margin: 0 0 6px;
font-size: 24px; font-size: 21px;
font-weight: 700;
letter-spacing: 0.02em;
color: var(--text-primary);
}
.desc {
margin: 0; margin: 0;
letter-spacing: 2px; font-size: 13px;
font-weight: 400;
color: var(--text-muted);
letter-spacing: 0.01em;
} }
.sub-text { .login-form :deep(.el-form-item) {
color: #00f2ff; margin-bottom: 20px;
font-size: 10px;
opacity: 0.7;
} }
/* 3. 监控条 */ .login-form :deep(.el-input__wrapper) {
.system-monitor { border-radius: 10px;
background: rgba(0, 0, 0, 0.4); padding: 3px 12px;
padding: 5px 12px; background: #f8f9fb;
margin-bottom: 25px; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05) inset;
border-left: 3px solid #00f2ff; transition: box-shadow 0.2s, background 0.2s;
font-family: monospace;
font-size: 11px;
} }
.system-monitor .label { .login-form :deep(.el-input__wrapper:hover) {
color: #64748b; background: #f4f5f8;
margin-right: 10px; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08) inset;
} }
.system-monitor .value { .login-form :deep(.el-input__wrapper.is-focus) {
color: #22c55e; background: #fcfcfd;
text-shadow: 0 0 5px #22c55e; box-shadow: 0 0 0 2px rgba(91, 95, 227, 0.12), 0 0 0 1px rgba(91, 95, 227, 0.3) inset;
} }
/* 4. 输入框定制 */ .form-row {
.input-prefix { margin: -2px 0 24px;
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
z-index: 10;
font-size: 10px;
color: #00f2ff;
font-weight: bold;
opacity: 0.6;
}
:deep(.ark-input .el-input__wrapper) {
background: rgba(0, 242, 255, 0.03) !important;
box-shadow: none !important;
border: 1px solid rgba(0, 242, 255, 0.2);
border-radius: 0;
/* 工业风通常使用直角 */
height: 50px;
padding-left: 45px;
}
:deep(.ark-input .is-focus) {
border-color: #00f2ff !important;
background: rgba(0, 242, 255, 0.08) !important;
}
/* 5. 启动按钮 */
.ark-btn {
width: 100%;
height: 50px;
background: transparent !important;
border: 1px solid #00f2ff !important;
color: #00f2ff !important;
border-radius: 0;
position: relative;
transition: all 0.3s;
font-weight: bold;
}
.ark-btn:hover {
background: #00f2ff !important;
color: #000 !important;
box-shadow: 0 0 20px rgba(0, 242, 255, 0.4);
}
.options-bar {
margin: 15px 0 25px;
} }
:deep(.el-checkbox__label) { :deep(.el-checkbox__label) {
color: #64748b; font-size: 13px;
font-size: 12px; color: var(--text-muted);
font-weight: 400;
} }
:deep(.el-checkbox__inner) { :deep(.el-checkbox__inner) {
background: transparent; border-radius: 4px;
border-color: #334155;
} }
.card-footer { .submit-btn {
margin-top: 30px; width: 100%;
text-align: right; height: 48px;
border-top: 1px dashed rgba(0, 242, 255, 0.1); border-radius: 10px;
padding-top: 15px; font-size: 15px;
} font-weight: 600;
letter-spacing: 0.25em;
.code { transition: all 0.2s;
font-family: monospace;
font-size: 10px;
color: #334155;
}
/* 入场动画 */
.ark-fade-enter-active {
transition: all 0.6s ease-out;
}
.ark-fade-enter-from {
opacity: 0;
transform: scale(1.1);
filter: brightness(2);
}
/* 1. 核心修复:禁止卡片内非交互元素的文本选中 */
.ark-card,
.card-header,
.brand-text,
.title,
.sub-text,
.system-monitor,
.options-bar,
.card-footer,
.code,
.input-prefix {
user-select: none;
/* 禁止选中文字 */
-webkit-user-select: none;
/* 兼容 Safari */
-moz-user-select: none;
/* 兼容 Firefox */
-ms-user-select: none;
/* 兼容 IE */
cursor: default;
/* 强制鼠标指针为标准箭头,不变成 I 型 */
}
/* 2. 确保输入框内部依然可以正常输入和选中(不受父级影响) */
:deep(.el-input__inner) {
user-select: text !important;
cursor: text !important;
}
/* 3. 确保复选框的 Label 也不会被选中(防止点击勾选时变蓝) */
:deep(.el-checkbox) {
user-select: none;
} }
</style> </style>

View File

@@ -20,9 +20,9 @@
<el-avatar :size="28">{{ staff.username.slice(0, 1) }}</el-avatar> <el-avatar :size="28">{{ staff.username.slice(0, 1) }}</el-avatar>
<span class="staff-name">{{ staff.username }}</span> <span class="staff-name">{{ staff.username }}</span>
</div> </div>
<el-tag type="success" effect="dark" round> <span class="count-badge">
已锁定 {{ staff.lockedTickets.length }} 个站点 已锁定 {{ staff.lockedTickets.length }} 个站点
</el-tag> </span>
</div> </div>
</template> </template>
<el-table :data="staff.lockedTickets" stripe border size="small" class="inner-table"> <el-table :data="staff.lockedTickets" stripe border size="small" class="inner-table">
@@ -41,34 +41,112 @@
</el-table-column> </el-table-column>
<el-table-column label="状态" width="100" align="center"> <el-table-column label="状态" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="Number(row.status) === 2 ? 'success' : 'info'" size="small"> <span class="status-badge" :class="'s' + Number(row.status)">
{{ statusLabel(row.status) }} {{ statusLabel(row.status) }}
</el-tag> </span>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-collapse-item> </el-collapse-item>
</el-collapse> </el-collapse>
</div> </div>
<!-- 数据记录导出 -->
<div class="export-bar">
<span class="export-label">数据记录导出</span>
<div class="export-actions">
<el-radio-group v-model="exportRange" size="small">
<el-radio value="week">本周</el-radio>
<el-radio value="month">本月</el-radio>
</el-radio-group>
<el-button size="small" @click="handleExport">导出</el-button>
</div>
</div>
</el-card> </el-card>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, computed } from "vue"; import { ref, onMounted, computed } from "vue";
import { ticketApi } from "@/api/ticket"; import { ticketApi, exportApi } from "@/api/ticket";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { formatDate } from "@/utils/format"; import { formatDate } from "@/utils/format";
import * as XLSX from "xlsx";
const loading = ref(true); const loading = ref(true);
const staffUserList = ref([]); const staffUserList = ref([]);
const allTickets = ref([]); const allTickets = ref([]);
const activeCollapse = ref(""); const activeCollapse = ref("");
const showOnlyThisMonth = ref(false); const showOnlyThisMonth = ref(false);
const exportRange = ref("week");
const statusMap = { 0: "未分配", 1: "进行中", 2: "已完成", 3: "已完结" }; const statusMap = { 0: "未分配", 1: "进行中", 2: "已完成", 3: "已完结" };
const statusLabel = (status) => statusMap[Number(status)] || "未知"; const statusLabel = (status) => statusMap[Number(status)] || "未知";
const handleExport = () => {
// 使用接口获取需要导出的数据
exportApi.exportdata(exportRange.value).then((res) => {
if (res.code === 200) {
const data = res.data || [];
exportToExcel(data, exportRange.value === "week" ? "本周" : "本月", exportRange.value);
} else {
ElMessage.error("导出失败,请稍后再试");
}
}).catch(() => {
ElMessage.error("导出失败,请稍后再试");
});
};
const exportToExcel = (data, label,range) => {
data.sort((a, b) => {
const timeA = new Date(a.create_time).getTime();
const timeB = new Date(b.create_time).getTime();
if (timeA !== timeB) return timeA - timeB;
return a.username.localeCompare(b.username, "zh-CN");
});
const excelData = data.map((item) => ({
域名: item.website,
数据量: item.datanum || 0,
运营人员: item.operator_name,
采集人员: item.username,
备注: item.remark,
开始时间: item.create_time,
结束时间: item.lock_time,
}));
// 生成工作表
const worksheet = XLSX.utils.json_to_sheet(excelData);
// 自动调整列宽
const objectKeys = Object.keys(excelData[0] || {});
const max_widths = objectKeys.map((key) => {
let maxLen = key.replace(/[^\x00-\xff]/g, "00").length;
excelData.forEach((row) => {
const val = row[key] != null ? String(row[key]) : "";
const valLen = val.replace(/[^\x00-\xff]/g, "00").length;
if (valLen > maxLen) maxLen = valLen;
});
return { wch: maxLen + 3 };
});
worksheet["!cols"] = max_widths;
// 创建工作簿并导出
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "工单导出");
const now = new Date();
const month = now.getMonth() + 1;
const weekOfMonth = Math.ceil(now.getDate() / 7);
if (range === "week") {
XLSX.writeFile(workbook, `工单数据导出_${month}月第${weekOfMonth}周.xlsx`);
} else if (range === "month") {
XLSX.writeFile(workbook, `工单数据导出_${month}月.xlsx`);
}
ElMessage.success(`已导出${label}记录`);
}
onMounted(async () => { onMounted(async () => {
loading.value = true; loading.value = true;
try { try {
@@ -144,12 +222,21 @@ const staffWithLockedTickets = computed(() => {
<style scoped> <style scoped>
.stats-view { .stats-view {
padding: 20px; padding: 0;
} }
.box-card { :deep(.el-card) {
background: #f5f6f8;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 12px; border-radius: 12px;
border: 1px solid #e4e7ed; box-shadow:
0 1px 3px rgba(0, 0, 0, 0.04),
0 4px 16px rgba(0, 0, 0, 0.035);
}
:deep(.el-card__header) {
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
padding: 18px 22px;
} }
.card-header { .card-header {
@@ -158,16 +245,17 @@ const staffWithLockedTickets = computed(() => {
align-items: center; align-items: center;
} }
.card-header>span {
font-weight: 600;
font-size: 15px;
color: #1c1c28;
}
.header-actions { .header-actions {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.card-header > span {
font-weight: bold;
font-size: 16px;
}
.collapse-title { .collapse-title {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -179,12 +267,27 @@ const staffWithLockedTickets = computed(() => {
.staff-info { .staff-info {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 10px;
} }
.staff-name { .staff-name {
font-size: 15px; font-size: 14px;
font-weight: 500; font-weight: 600;
color: #1c1c28;
}
.count-badge {
display: inline-flex;
align-items: center;
padding: 1px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
color: #15803d;
background: rgba(22, 163, 74, 0.1);
border: 1px solid rgba(22, 163, 74, 0.2);
white-space: nowrap;
line-height: 1.4;
} }
.inner-table { .inner-table {
@@ -192,12 +295,104 @@ const staffWithLockedTickets = computed(() => {
margin: 0 16px 16px 16px; margin: 0 16px 16px 16px;
} }
:deep(.inner-table) {
border-radius: 8px;
overflow: hidden;
}
:deep(.el-collapse) {
border: none;
}
:deep(.el-collapse-item__header) { :deep(.el-collapse-item__header) {
padding: 0 16px; padding: 0 16px;
height: 60px; height: 52px;
background: transparent;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
color: #1c1c28;
font-weight: 500;
font-size: 14px;
}
:deep(.el-collapse-item__header:hover) {
background: rgba(0, 0, 0, 0.015);
}
:deep(.el-collapse-item__wrap) {
background: none;
border: none;
} }
:deep(.el-collapse-item__content) { :deep(.el-collapse-item__content) {
padding-bottom: 0; padding-bottom: 0;
color: var(--text-primary);
}
:deep(.el-table) {
--el-table-bg-color: #fafbfc;
--el-table-header-bg-color: #f0f1f4;
--el-table-border-color: rgba(0, 0, 0, 0.04);
border-radius: 8px;
overflow: hidden;
}
:deep(.el-table th) {
font-size: 11px;
font-weight: 600;
color: #6b6d7a;
letter-spacing: 0.3px;
}
/* 状态徽章 */
.status-badge {
display: inline-block;
padding: 2px 10px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
}
.s0 {
color: #6b7280;
background: #f3f4f6;
}
.s1 {
color: #4f46e5;
background: #eef0ff;
}
.s2 {
color: #16a34a;
background: #ecfdf3;
}
.s3 {
color: #d97706;
background: #fffbeb;
}
/* 导出栏 */
.export-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding: 14px 16px;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 8px;
}
.export-label {
font-size: 13px;
font-weight: 600;
color: #1c1c28;
}
.export-actions {
display: flex;
align-items: center;
gap: 16px;
} }
</style> </style>

View File

@@ -376,152 +376,96 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
/* 样式部分保持不变 */ .ticket-management { padding: 0; min-height: 0; background: none; }
.ticket-management {
padding: 20px;
background-color: #f8fafc;
min-height: 100vh;
}
.stats-container { .stats-container {
display: grid; display: grid;
grid-template-columns: repeat(5, minmax(150px, 1fr)); grid-template-columns: repeat(5, minmax(140px, 1fr));
gap: 16px; gap: 14px;
margin-bottom: 24px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s ease;
border-bottom: 3px solid transparent;
/* 禁止用户选中文字 */
user-select: none;
-webkit-user-select: none;
/* 兼容 Safari */
-moz-user-select: none;
/* 兼容 Firefox */
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.total {
border-bottom-color: #409eff;
}
.total .stat-icon {
color: #409eff;
background: #ecf5ff;
}
.pending {
border-bottom-color: #909399;
}
.pending .stat-icon {
color: #909399;
background: #f4f4f5;
}
.ongoing {
border-bottom-color: #e6a23c;
}
.ongoing .stat-icon {
color: #e6a23c;
background: #fdf6ec;
}
.success {
border-bottom-color: #67c23a;
}
.success .stat-icon {
color: #67c23a;
background: #f0f9eb;
}
.finished {
border-bottom-color: #009688;
}
.finished .stat-icon {
color: #009688;
background: #e6f6f5;
}
.stat-label {
font-size: 13px;
color: #64748b;
margin-bottom: 8px;
font-weight: 500;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: #1e293b;
}
.stat-icon {
font-size: 24px;
padding: 12px;
border-radius: 10px;
}
.main-content {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px; margin-bottom: 20px;
} }
.batch-bar { .stat-card {
background: #f5f6f8;
padding: 18px 18px;
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-left: 4px solid transparent;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.05),
0 3px 10px rgba(0, 0, 0, 0.04);
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
gap: 12px; transition: box-shadow 0.2s, transform 0.2s;
padding: 10px 16px; user-select: none;
margin-bottom: 12px;
background: #fdf6ec;
border: 1px solid #faecd8;
border-radius: 8px;
} }
.batch-info { .total { border-left-color: #4f46e5; }
font-size: 13px; .pending { border-left-color: #6b7280; }
color: #606266; .ongoing { border-left-color: #d97706; }
.success { border-left-color: #16a34a; }
.finished { border-left-color: #0284c7; }
.stat-card:hover {
transform: translateY(-2px);
box-shadow:
0 3px 6px rgba(0, 0, 0, 0.07),
0 10px 28px rgba(0, 0, 0, 0.07);
} }
.pagination-container { .stat-icon {
margin-top: 20px; font-size: 20px;
display: flex; padding: 10px;
justify-content: flex-end; border-radius: 10px;
} }
@media (max-width: 1400px) { .total .stat-icon { color: #4f46e5; background: rgba(79, 70, 229, 0.12); }
.stats-container { .pending .stat-icon { color: #4b5563; background: rgba(75, 85, 99, 0.1); }
grid-template-columns: repeat(3, 1fr); .ongoing .stat-icon { color: #d97706; background: rgba(217, 119, 6, 0.12); }
} .success .stat-icon { color: #16a34a; background: rgba(22, 163, 74, 0.12); }
.finished .stat-icon { color: #0284c7; background: rgba(2, 132, 199, 0.12); }
.stat-label {
font-size: 12px;
color: #5b5d68;
margin-bottom: 5px;
font-weight: 600;
letter-spacing: 0.02em;
} }
@media (max-width: 768px) { .stat-value {
.stats-container { font-size: 28px;
grid-template-columns: 1fr 1fr; font-weight: 700;
} color: #1c1c28;
font-family: var(--font-mono);
line-height: 1;
} }
.main-content {
background: #f5f6f8;
padding: 22px 24px;
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.05),
0 6px 20px rgba(0, 0, 0, 0.05);
}
.action-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; }
.batch-bar {
display: flex; align-items: center; gap: 12px;
padding: 10px 16px; margin-bottom: 14px;
background: rgba(217, 119, 6, 0.1);
border: 1px solid rgba(217, 119, 6, 0.3);
border-radius: 10px;
}
.batch-info { font-size: 13px; color: #5b5d68; }
.batch-info strong { color: #92400e; font-weight: 600; }
.pagination-container { margin-top: 18px; display: flex; justify-content: flex-end; }
@media (max-width: 1400px) { .stats-container { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 768px) { .stats-container { grid-template-columns: 1fr 1fr; } }
</style> </style>

View File

@@ -20,7 +20,7 @@
<el-icon> <el-icon>
<User /> <User />
</el-icon> </el-icon>
牛马身份: {{ realStaffInfo.name }} 牛马身份: {{ realStaffInfo.username }}
</el-tag> </el-tag>
<el-tag v-else type="danger" round size="small" <el-tag v-else type="danger" round size="small"
>未录入员工表,无权接单</el-tag >未录入员工表,无权接单</el-tag
@@ -71,11 +71,10 @@
</el-radio-group> </el-radio-group>
</div> </div>
<div class="table-card">
<el-table <el-table
:data="paginatedTickets" :data="paginatedTickets"
v-loading="loading" v-loading="loading"
border
style="width: 100%; margin-top: 20px"
class="custom-table" class="custom-table"
> >
<template #empty> <template #empty>
@@ -203,7 +202,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="operator" label="运营" width="120" align="center" /> <el-table-column prop="operator_name" label="运营" width="120" align="center" />
<el-table-column label="备注" min-width="150" align="center"> <el-table-column label="备注" min-width="150" align="center">
<template #default="scope"> <template #default="scope">
@@ -274,7 +273,7 @@
<el-table-column label="操作" width="190" fixed="right" align="center"> <el-table-column label="操作" width="190" fixed="right" align="center">
<template #default="scope"> <template #default="scope">
<el-button <el-button
v-if="scope.row.is_complete === '未分配'" v-if="Number(scope.row.status) === 0"
type="primary" type="primary"
size="small" size="small"
:disabled="!realStaffInfo" :disabled="!realStaffInfo"
@@ -283,8 +282,8 @@
> >
<template <template
v-if=" v-if="
scope.row.is_complete === '进行中' && Number(scope.row.status) === 1 &&
String(scope.row.staff_id) === String(realStaffInfo?.id) String(scope.row.user_id) === String(realStaffInfo?.id)
" "
> >
<el-button type="success" size="small" @click="handleFinish(scope.row)" <el-button type="success" size="small" @click="handleFinish(scope.row)"
@@ -310,6 +309,7 @@
background background
/> />
</div> </div>
</div>
</div> </div>
</template> </template>
@@ -339,6 +339,7 @@ const editValue = ref("");
const currentPage = ref(1); const currentPage = ref(1);
const pageSize = ref(15); const pageSize = ref(15);
const emptyText = computed(() => `暂无${currentStatus.value}的工单`);
// js计算时间 // js计算时间
const welcomeMessage = computed(() => { const welcomeMessage = computed(() => {
@@ -386,7 +387,7 @@ const fetchTickets = async () => {
loading.value = false; loading.value = false;
return; return;
} }
params.staff_id = realStaffInfo.value.id; params.user_id = realStaffInfo.value.id;
} }
const res = await ticketApi.getTickets(params); const res = await ticketApi.getTickets(params);
@@ -457,8 +458,8 @@ const handleAccept = (row) => {
ElMessageBox.confirm(`确认接手工单 #${row.id}?`).then(async () => { ElMessageBox.confirm(`确认接手工单 #${row.id}?`).then(async () => {
try { try {
const res = await ticketApi.updateTicket(row.id, { const res = await ticketApi.updateTicket(row.id, {
staff_id: realStaffInfo.value.id, user_id: realStaffInfo.value.id,
is_complete: "进行中", status: 1,
progress: 0, progress: 0,
}); });
if (res.code === 200 || res.code === true) { if (res.code === 200 || res.code === true) {
@@ -478,9 +479,9 @@ const handleFinish = (row) => {
const currentTimestamp = Math.floor(Date.now() / 1000); const currentTimestamp = Math.floor(Date.now() / 1000);
// 关键修改:在提交时增加 progress: 100 // 关键修改:在提交时增加 progress: 100
const res = await ticketApi.updateTicket(row.id, { const res = await ticketApi.updateTicket(row.id, {
is_complete: "已完成", status: 2,
lock_time: String(currentTimestamp), lock_time: String(currentTimestamp),
progress: 100, // 默认改为100 progress: 100,
}); });
if (res.code === 200 || res.code === true) { if (res.code === 200 || res.code === true) {
@@ -502,8 +503,8 @@ const handleCancel = (row) => {
).then(async () => { ).then(async () => {
try { try {
const res = await ticketApi.updateTicket(row.id, { const res = await ticketApi.updateTicket(row.id, {
staff_id: null, user_id: null,
is_complete: "未分配", status: 0,
progress: 0, progress: 0,
}); });
if (res.code === 200 || res.code === true) { if (res.code === 200 || res.code === true) {
@@ -534,9 +535,9 @@ const handleLogout = () => {
onMounted(async () => { onMounted(async () => {
try { try {
const res = await ticketApi.getStaffs(); const res = await ticketApi.getUserStaffs();
if (res.code === 200 || res.code === true) { if (res.code === 200 || res.code === true) {
const match = (res.data || []).find((s) => s.name === currentUser.value.username); const match = (res.data || []).find((s) => s.username === currentUser.value.username);
if (match) realStaffInfo.value = match; if (match) realStaffInfo.value = match;
} }
} catch (e) {} } catch (e) {}
@@ -545,226 +546,357 @@ onMounted(async () => {
</script> </script>
<style scoped> <style scoped>
/* 容器布局 */ /* ============================================
.header-main { ENERGY WORKBENCH — Gamified Employee Desk
display: flex; ============================================ */
flex-direction: column; .user-view-container {
justify-content: center; padding: 12px 12px 0;
min-height: 0;
background: none;
--glow-indigo: rgba(99, 102, 241, 0.35);
--glow-amber: rgba(245, 158, 11, 0.3);
--glow-green: rgba(34, 197, 94, 0.3);
--glow-rose: rgba(244, 63, 94, 0.25);
} }
.welcome-container { /* ---- Header Card (Gradient Hero) ---- */
display: flex; .user-header-card {
flex-direction: column; background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
gap: 8px; border: none;
/* 这里统一控制欢迎语、副标题、标签之间的垂直间距 */ border-radius: 16px;
padding: 28px 32px;
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 20px;
box-shadow:
0 4px 16px rgba(102, 126, 234, 0.2),
0 8px 32px rgba(118, 75, 162, 0.15);
position: relative;
overflow: hidden;
} }
/* 清除标题默认的垂直 margin,防止撑开间距 */ .user-header-card::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(circle at 20% 50%, rgba(255,255,255,0.08) 0%, transparent 50%),
radial-gradient(circle at 80% 50%, rgba(255,255,255,0.04) 0%, transparent 50%);
pointer-events: none;
}
.header-main { display: flex; flex-direction: column; justify-content: center; position: relative; z-index: 1; }
.welcome-container { display: flex; flex-direction: column; gap: 8px; }
.greeting { .greeting {
margin: 0; margin: 0;
color: #1a1a1a; color: #fff;
font-size: 22px; font-size: 22px;
line-height: 1.2; line-height: 1.3;
font-weight: 700;
letter-spacing: -0.02em;
text-shadow: 0 1px 3px rgba(0,0,0,0.1);
} }
/* 副标题紧贴标题 */
.sub-text { .sub-text {
margin: 4px 0 0 0; margin: 4px 0 0;
/* 仅顶部保留微小间距 */ color: rgba(255,255,255,0.75);
color: #8c8c8c;
font-size: 13px; font-size: 13px;
font-weight: 400;
letter-spacing: 0.01em;
} }
/* 标签组位置微调 */ .identity-badges { margin-top: 8px; display: flex; align-items: center; position: relative; z-index: 1; }
.identity-badges { :deep(.identity-badges .el-tag) {
margin-top: 4px; display: inline-flex !important;
/* 让标签组靠近上方的文字 */ align-items: center !important;
display: flex; gap: 5px !important;
align-items: center; background: rgba(255,255,255,0.18) !important;
} backdrop-filter: blur(8px);
border: 1px solid rgba(255,255,255,0.25) !important;
/* 针对 el-tag 内部图标和文字的微调 */ color: #fff !important;
:deep(.el-tag) { font-weight: 600 !important;
display: inline-flex; padding: 4px 14px !important;
align-items: center; border-radius: 20px !important;
border-style: dashed; }
/* 既然是牛马,边框可以搞成虚线更有自嘲感 */ :deep(.identity-badges .el-tag .el-icon) { color: #fbbf24 !important; }
}
.header-action { position: relative; z-index: 1; }
.user-view-container { .header-action :deep(.el-button) {
padding: 24px; background: rgba(255,255,255,0.15) !important;
background-color: #f0f2f5; border: 1px solid rgba(255,255,255,0.25) !important;
min-height: 100vh; color: #fff !important;
} backdrop-filter: blur(8px);
border-radius: 24px !important;
.user-header-card { padding: 8px 18px !important;
background: white; }
padding: 24px; .header-action :deep(.el-button:hover) {
border-radius: 12px; background: rgba(255,255,255,0.25) !important;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.greeting {
margin: 0 0 8px 0;
color: #1a1a1a;
font-size: 24px;
} }
/* ---- Tab Switcher ---- */
.status-tabs-wrapper { .status-tabs-wrapper {
background: white; background: #fff;
padding: 8px; border: 1px solid rgba(0,0,0,0.06);
border-radius: 8px; border-radius: 14px;
padding: 5px;
display: inline-block; display: inline-block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); margin-bottom: 18px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
} }
.custom-radio-group :deep(.el-radio-button__inner) { .custom-radio-group :deep(.el-radio-button__inner) {
border: none !important; border: none !important;
border-radius: 6px !important; border-radius: 10px !important;
margin: 0 4px; margin: 0 1px;
padding: 10px 20px; padding: 10px 18px;
display: flex; display: flex; align-items: center; gap: 6px;
align-items: center; font-weight: 500;
gap: 6px; font-size: 13px;
color: #6b6d7a;
background: none;
transition: all 0.2s;
} }
.custom-table { .custom-radio-group :deep(.el-radio-button__inner:hover) {
border-radius: 12px; color: #667eea;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
} }
.cell-content-card { .custom-radio-group :deep(.is-active .el-radio-button__inner) {
background: linear-gradient(135deg, #667eea, #764ba2);
color: #fff;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.35);
}
/* ---- Table Card ---- */
.table-card {
background: #fff;
border: 1px solid rgba(0,0,0,0.06);
border-radius: 16px;
padding: 20px 24px;
box-shadow:
0 2px 8px rgba(0,0,0,0.03),
0 4px 20px rgba(0,0,0,0.04);
}
.custom-table { overflow: hidden; border-radius: var(--radius-md); }
:deep(.el-table__header th) {
background: #f8f9fc;
color: #5b5d68;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.4px;
text-transform: uppercase;
border-bottom: 2px solid #eef0f4 !important;
}
:deep(.el-table__body tr) { transition: all 0.15s; }
:deep(.el-table__body tr:hover > td) {
background: linear-gradient(90deg, rgba(102,126,234,0.03), rgba(118,75,162,0.02)) !important;
}
:deep(.el-table__body tr) { border-bottom: 1px solid #f5f5f8; }
/* Staggered row tints */
:deep(.el-table__body tr:nth-child(odd) td) { background: #fdfdfe; }
:deep(.el-table__body tr:nth-child(even) td) { background: #fafbfd; }
.cell-content-card { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; min-height: 60px; }
.data-display-area { min-height: 24px; display: flex; align-items: center; justify-content: center; width: 100%; }
.display-val { font-weight: 700; color: #667eea; font-size: 15px; font-family: "Courier New", monospace; }
.display-remark {
font-size: 12px; color: #5b5d68; max-width: 180px; line-height: 1.4;
background: #f8f9fc; padding: 4px 12px; border-radius: 6px; text-align: center;
}
.no-data-placeholder { color: #c0c4cc; font-size: 12px; }
.pure-date { color: #6b6d7a; font-weight: 500; font-family: "Courier New", monospace; font-size: 12px; }
.edit-link-btn { font-size: 12px; height: auto; padding: 0; opacity: 0.6; color: #667eea; transition: all 0.15s; }
.edit-link-btn:hover { opacity: 1; color: #4f46e5; font-weight: 600; }
/* ---- Pagination ---- */
.pagination-container {
margin-top: 18px; display: flex; justify-content: flex-end;
padding-top: 16px;
border-top: 1px solid #f0f1f4;
}
/* ---- Progress ---- */
:deep(.el-progress-bar__outer) { background: #eef0f4; border-radius: 10px; overflow: hidden; }
:deep(.el-progress-bar__inner) {
background: linear-gradient(90deg, #667eea, #764ba2) !important;
}
:deep(.el-progress-bar__innerText) {
display: flex !important; align-items: center !important;
justify-content: center !important; height: 100% !important;
line-height: 1 !important; vertical-align: middle !important;
}
:deep(.el-progress-bar__innerText svg),
:deep(.el-progress-bar__innerText .el-icon) {
margin: 0 !important; position: relative !important;
top: -1px !important; width: 0.8em; height: 0.8em;
}
:deep(.el-progress) { display: flex; align-items: center; }
.progress-radio-group { display: flex; justify-content: center; margin: 10px 0; }
.progress-radio-group :deep(.el-radio-button__inner) { padding: 8px 10px !important; }
/* ---- Table Overrides ---- */
:deep(.el-table__row) { height: 100px; }
:deep(.el-table .cell) { overflow: visible !important; }
/* ---- Popover (Inline Edit Flyout) ---- */
:deep(.el-popover) {
border-radius: 16px !important;
border: none !important;
box-shadow:
0 8px 32px rgba(0,0,0,0.1),
0 2px 8px rgba(0,0,0,0.06) !important;
padding: 20px !important;
}
.popover-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; gap: 4px;
justify-content: center;
gap: 6px;
min-height: 60px;
}
.data-display-area {
min-height: 24px;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.display-val {
font-weight: 700;
color: #409eff;
font-size: 15px;
}
.display-remark {
font-size: 12px;
color: #595959;
max-width: 180px;
line-height: 1.4;
background: #f5f7fa;
padding: 4px 10px;
border-radius: 4px;
text-align: center;
}
.no-data-placeholder {
color: #dcdfe6;
font-size: 12px;
}
.pure-date {
color: #606266;
font-weight: 500;
font-family: monospace;
font-size: 14px;
}
.edit-link-btn {
font-size: 12px;
height: auto;
padding: 0;
opacity: 0.7;
}
.edit-link-btn:hover {
opacity: 1;
text-decoration: underline;
}
.pagination-container {
margin-top: 24px;
display: flex;
justify-content: flex-end;
padding: 10px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.progress-radio-group {
display: flex;
justify-content: center;
margin: 10px 0;
}
.progress-radio-group :deep(.el-radio-button__inner) {
padding: 8px 10px !important;
}
:deep(.el-table__row) {
height: 100px;
}
:deep(.el-table .cell) {
overflow: visible !important;
} }
.pop-header { .pop-header {
font-size: 14px; font-size: 13px;
font-weight: 600; font-weight: 700;
margin-bottom: 12px; margin-bottom: 4px;
color: #434343; color: #1c1c28;
text-align: center; text-align: left;
letter-spacing: -0.01em;
display: flex;
align-items: center;
gap: 6px;
}
.pop-header::before {
content: "";
width: 3px; height: 14px;
background: linear-gradient(180deg, #667eea, #764ba2);
border-radius: 2px;
}
:deep(.popover-content .el-input__wrapper) {
border-radius: 10px !important;
box-shadow: 0 0 0 1px rgba(0,0,0,0.08) inset !important;
transition: box-shadow 0.2s;
}
:deep(.popover-content .el-input__wrapper:hover) {
box-shadow: 0 0 0 1px rgba(102,126,234,0.3) inset !important;
}
:deep(.popover-content .el-input__wrapper.is-focus) {
box-shadow: 0 0 0 2px rgba(102,126,234,0.15), 0 0 0 1px #667eea inset !important;
}
:deep(.popover-content .el-textarea__inner) {
border-radius: 10px !important;
box-shadow: 0 0 0 1px rgba(0,0,0,0.08) inset !important;
transition: box-shadow 0.2s;
font-size: 13px;
}
:deep(.popover-content .el-textarea__inner:focus) {
box-shadow: 0 0 0 2px rgba(102,126,234,0.15), 0 0 0 1px #667eea inset !important;
} }
.pop-footer { .pop-footer {
margin-top: 12px; margin-top: 10px;
text-align: right; text-align: right;
} }
.pop-footer :deep(.el-button--primary) {
/* 1. 强行修正进度条内部容器的对齐方式 */ background: linear-gradient(135deg, #667eea, #764ba2) !important;
:deep(.el-progress-bar__innerText) { border: none !important;
display: flex !important; border-radius: 10px !important;
align-items: center !important; padding: 7px 20px !important;
/* 垂直居中 */ font-weight: 600 !important;
justify-content: center !important; box-shadow: 0 2px 8px rgba(102,126,234,0.3);
/* 水平居中 */ transition: all 0.2s;
height: 100% !important; }
line-height: 1 !important; .pop-footer :deep(.el-button--primary:hover) {
/* 移除默认的基线对齐 */ box-shadow: 0 4px 16px rgba(102,126,234,0.4);
vertical-align: middle !important; transform: translateY(-1px);
} }
/* 2. 针对 100% 时出现的那个 svg 图标进行位移修正 */ /* Progress radio buttons in popover */
:deep(.el-progress-bar__innerText svg), :deep(.popover-content .el-radio-button__inner) {
:deep(.el-progress-bar__innerText .el-icon) { border-radius: 8px !important;
margin: 0 !important; padding: 6px 12px !important;
/* 关键:利用相对定位向上平移 1px,抵消视觉上的下沉 */ }
position: relative !important; :deep(.popover-content .el-radio-button__inner:hover) {
top: -1px !important; color: #667eea !important;
/* 稍微调整图标大小,让它在 10px 的进度条里更协调 */ }
width: 0.8em; :deep(.popover-content .is-active .el-radio-button__inner) {
height: 0.8em; background: linear-gradient(135deg, #667eea, #764ba2) !important;
border-color: transparent !important;
color: #fff !important;
box-shadow: 0 2px 6px rgba(102,126,234,0.3);
} }
/* 3. 如果还是觉得整体偏,可以给进度条容器加一个微调 */ /* ---- Action Buttons Glow ---- */
:deep(.el-progress) { :deep(.el-table .el-button--primary) { transition: all 0.2s; }
display: flex; :deep(.el-table .el-button--primary:hover) { box-shadow: 0 0 12px var(--glow-indigo); }
align-items: center; :deep(.el-table .el-button--success:hover) { box-shadow: 0 0 12px var(--glow-green); }
:deep(.el-table .el-button--warning:hover) { box-shadow: 0 0 12px var(--glow-amber); }
/* ---- Logout Dropdown ---- */
:deep(.el-dropdown-menu) {
border-radius: 14px !important;
border: none !important;
box-shadow: 0 8px 32px rgba(0,0,0,0.1), 0 2px 8px rgba(0,0,0,0.06) !important;
padding: 6px !important;
}
:deep(.el-dropdown-menu__item) {
border-radius: 10px !important;
padding: 9px 16px !important;
font-size: 13px !important;
font-weight: 500 !important;
transition: all 0.15s !important;
}
:deep(.el-dropdown-menu__item:hover) {
background: linear-gradient(135deg, rgba(244,63,94,0.06), rgba(244,63,94,0.02)) !important;
color: #e11d48 !important;
}
/* ---- Message Box (Logout Confirm) ---- */
:deep(.el-message-box) {
border-radius: 18px !important;
border: none !important;
box-shadow: 0 12px 48px rgba(0,0,0,0.12), 0 4px 16px rgba(0,0,0,0.06) !important;
padding: 28px !important;
}
:deep(.el-message-box__title) {
font-size: 16px !important;
font-weight: 700 !important;
color: #1c1c28 !important;
}
:deep(.el-message-box__message) {
font-size: 13px !important;
color: #6b6d7a !important;
line-height: 1.6 !important;
}
:deep(.el-message-box__btns) {
padding-top: 12px !important;
}
:deep(.el-message-box__btns .el-button) {
border-radius: 10px !important;
padding: 9px 22px !important;
font-weight: 500 !important;
font-size: 13px !important;
}
:deep(.el-message-box__btns .el-button--primary) {
background: linear-gradient(135deg, #667eea, #764ba2) !important;
border: none !important;
box-shadow: 0 2px 8px rgba(102,126,234,0.3) !important;
}
:deep(.el-message-box__btns .el-button--primary:hover) {
box-shadow: 0 4px 16px rgba(102,126,234,0.4) !important;
} }
</style> </style>