最新修改
This commit is contained in:
13
src/api/login.js
Normal file
13
src/api/login.js
Normal 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')
|
||||
}
|
||||
}
|
||||
@@ -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 = {
|
||||
getAllUsers() {
|
||||
return adminRequest.get('/getAllUsers')
|
||||
@@ -86,10 +100,16 @@ export const accountApi = {
|
||||
},
|
||||
|
||||
updateAccountStatus(id, status) {
|
||||
return adminRequest.put(`/account/${id}/status`, { status })
|
||||
return adminRequest.put(`/updateAccountStatus/${id}/status`, { status })
|
||||
},
|
||||
|
||||
deleteAccount(id) {
|
||||
return adminRequest.delete(`/account/${id}`)
|
||||
return adminRequest.delete(`/deleteAccount/${id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const exportApi = {
|
||||
exportdata(range) {
|
||||
return adminRequest.post('/getExportData', { range })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,120 @@
|
||||
/* 重置基础样式 */
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f5f7fa;
|
||||
/* 给背景一个淡灰色,更有后台质感 */
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
/* ============================================
|
||||
Global Design System — Refined Minimal
|
||||
============================================ */
|
||||
:root {
|
||||
--color-bg: #fafbfc;
|
||||
--color-surface: #ffffff;
|
||||
--color-border: #e9eaed;
|
||||
--color-border-light: #f0f1f3;
|
||||
|
||||
--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 {
|
||||
/* 移除原有的 grid 和 center 布局 */
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
/* 恢复块级布局 */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
width: 100%; height: 100%;
|
||||
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;
|
||||
}
|
||||
|
||||
/* 如果你希望内容有一个最大宽度并居中,可以这样写 */
|
||||
.ticket-management {
|
||||
max-width: 1440px;
|
||||
/* 常见的管理后台舒适宽度 */
|
||||
margin: 0 auto;
|
||||
/* 水平居中 */
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
#app { width: 100%; margin: 0; padding: 0; display: block; }
|
||||
|
||||
a { text-decoration: none; color: var(--color-primary); }
|
||||
|
||||
/* Element Plus overrides */
|
||||
.el-button--primary {
|
||||
--el-button-bg-color: var(--color-primary);
|
||||
--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;
|
||||
}
|
||||
|
||||
/* 链接样式微调 */
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #409eff;
|
||||
transition: 0.3s;
|
||||
.el-button { border-radius: var(--radius-sm); font-weight: 500; }
|
||||
|
||||
.el-input .el-input__wrapper {
|
||||
border-radius: var(--radius-sm);
|
||||
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;
|
||||
}
|
||||
|
||||
/* 针对移动端的小适配 */
|
||||
@media (max-width: 768px) {
|
||||
.ticket-management {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
.el-select .el-input__wrapper { border-radius: var(--radius-sm); }
|
||||
|
||||
.el-table {
|
||||
--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);
|
||||
}
|
||||
|
||||
@@ -145,108 +145,41 @@ const setSort = (order) => {
|
||||
.filter-card {
|
||||
border: none !important;
|
||||
background: transparent;
|
||||
/* 核心修改:禁止用户选中文字,防止点击时变蓝 */
|
||||
-webkit-user-select: none;
|
||||
/* Safari */
|
||||
-moz-user-select: none;
|
||||
/* Firefox */
|
||||
-ms-user-select: none;
|
||||
/* IE/Edge */
|
||||
user-select: none;
|
||||
/* 标准语法 */
|
||||
}
|
||||
:deep(.el-input__inner) { user-select: text !important; }
|
||||
|
||||
/* 恢复输入框的可选中状态,否则无法输入文字 */
|
||||
:deep(.el-input__inner) {
|
||||
-webkit-user-select: text !important;
|
||||
user-select: text !important;
|
||||
}
|
||||
.search-container { display: flex; justify-content: space-between; align-items: flex-start; gap: 32px; }
|
||||
.filter-main { flex: 1; display: flex; flex-direction: column; }
|
||||
.input-group-row { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; }
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 32px;
|
||||
}
|
||||
.divider-wrapper { color: var(--text-muted); font-size: 16px; font-weight: 300; height: 32px; display: flex; align-items: center; margin-bottom: 18px; }
|
||||
|
||||
.filter-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
:deep(.el-form-item) { margin-bottom: 18px; }
|
||||
:deep(.el-form-item__label) { color: var(--text-secondary); font-weight: 500; }
|
||||
|
||||
.input-group-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.wide-select { width: 180px; }
|
||||
.wide-search-input { width: 320px; }
|
||||
|
||||
.divider-wrapper {
|
||||
color: #dcdfe6;
|
||||
font-size: 18px;
|
||||
font-weight: 200;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.filter-main :deep(.el-input__wrapper),
|
||||
.filter-main :deep(.el-button) { border-radius: 20px !important; }
|
||||
:deep(.el-select-dropdown) { border-radius: 12px !important; overflow: hidden; }
|
||||
.filter-actions :deep(.el-button) { border-radius: 20px !important; }
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.filter-actions { display: flex; gap: 10px; margin-top: 4px; flex-shrink: 0; }
|
||||
|
||||
/* 宽度控制 */
|
||||
.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) {
|
||||
background-color: #f5f7fa;
|
||||
background: var(--color-bg);
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
color: var(--text-primary);
|
||||
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>
|
||||
|
||||
@@ -13,6 +13,16 @@
|
||||
</template>
|
||||
</el-input>
|
||||
</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 class="form-section people-grid">
|
||||
@@ -21,16 +31,17 @@
|
||||
<el-form-item label="客户单位" prop="company_id" class="client-item">
|
||||
<el-select v-model="form.company_id" placeholder="请选择单位" style="width: 100%" filterable
|
||||
@change="handleCustomerChange">
|
||||
<el-option v-for="item in companiesWithOperators.filter(c => c.id !== 0)" :key="item.id" :label="item.company_name"
|
||||
:value="item.id" />
|
||||
<el-option v-for="item in companiesWithOperators.filter(c => c.id !== 0)" :key="item.id"
|
||||
:label="item.company_name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="运营人员" prop="operator_id" class="client-item">
|
||||
<el-select v-model="form.operator_id" placeholder="请选择人员" style="width: 100%" filterable
|
||||
: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"
|
||||
:value="item.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" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
@@ -70,7 +81,7 @@
|
||||
|
||||
<script setup>
|
||||
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"]);
|
||||
|
||||
@@ -90,6 +101,7 @@ const submitting = ref(false);
|
||||
// 核心表单对象:customer_id 已包含在内
|
||||
const form = reactive({
|
||||
website: "",
|
||||
datanum: "",
|
||||
company_id: null,
|
||||
operator_id: null,
|
||||
user_id: null,
|
||||
@@ -170,6 +182,7 @@ const resetForm = () => {
|
||||
if (formRef.value) formRef.value.resetFields();
|
||||
Object.assign(form, {
|
||||
website: "",
|
||||
datanum: "",
|
||||
company_id: null,
|
||||
operator_id: null,
|
||||
user_id: null,
|
||||
@@ -186,7 +199,7 @@ const handleSubmit = async () => {
|
||||
await formRef.value.validate();
|
||||
submitting.value = true;
|
||||
|
||||
// 复制提交数据,如果是新增,确保 staff_id 为空
|
||||
|
||||
const submitData = { ...form };
|
||||
|
||||
// 核心修复:如果状态是”已完结”,确保 lock_time 不会被意外修改。
|
||||
@@ -198,9 +211,6 @@ const handleSubmit = async () => {
|
||||
|
||||
delete submitData.create_time;
|
||||
delete submitData.update_time;
|
||||
if (!props.isEditing) {
|
||||
submitData.user_id = null;
|
||||
}
|
||||
|
||||
emit("submit", submitData);
|
||||
} catch (e) {
|
||||
@@ -212,30 +222,26 @@ const handleSubmit = async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 保持你原本的样式不变 */
|
||||
:deep(.modern-dialog) {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.section-title::before {
|
||||
content: "";
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
background: #409eff;
|
||||
width: 3px;
|
||||
height: 12px;
|
||||
background: var(--color-primary);
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -243,7 +249,7 @@ const handleSubmit = async () => {
|
||||
.people-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.column-full {
|
||||
@@ -251,9 +257,13 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
.modern-radio-group {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@selection-change="$emit('selection-change', $event)"
|
||||
>
|
||||
<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 }">
|
||||
<el-link
|
||||
type="primary"
|
||||
@@ -100,29 +100,33 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="锁定状态" width="90" align="center">
|
||||
<el-table-column label="锁定" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="row.lock_time && row.lock_time !== '0'"
|
||||
type="success"
|
||||
size="small"
|
||||
effect="light"
|
||||
>已锁定</el-tag
|
||||
<span
|
||||
class="lock-badge"
|
||||
:class="{ locked: row.lock_time && row.lock_time !== '0' }"
|
||||
>
|
||||
<el-tag v-else type="info" size="small" effect="plain">未锁定</el-tag>
|
||||
{{ row.lock_time && row.lock_time !== '0' ? '已锁定' : '未锁定' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="时间节点" width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="time-wrapper">
|
||||
<span>创建:{{ formatDateTime(row.create_time) }}</span>
|
||||
<span class="update-label">更新:{{ formatDateTime(row.update_time) }}</span>
|
||||
<span class="time-row">
|
||||
<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>
|
||||
</template>
|
||||
</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 }">
|
||||
<el-button link type="primary" @click="$emit('edit', row)">编辑</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
@@ -167,10 +171,10 @@ const statusLabel = (status) => statusMap[status] || "未知";
|
||||
|
||||
const getStatusStyle = (status) => {
|
||||
const styles = {
|
||||
0: { color: "#909399", background: "#f4f4f5" },
|
||||
1: { color: "#409EFF", background: "#ecf5ff" },
|
||||
2: { color: "#67C23A", background: "#f0f9eb" },
|
||||
3: { color: "#e6a23c", background: "#fdf6ec" },
|
||||
0: { color: "#6b7280", background: "#f3f4f6" },
|
||||
1: { color: "#4f46e5", background: "#eef0ff" },
|
||||
2: { color: "#16a34a", background: "#ecfdf3" },
|
||||
3: { color: "#d97706", background: "#fffbeb" },
|
||||
};
|
||||
return styles[status] || styles[0];
|
||||
};
|
||||
@@ -178,14 +182,14 @@ const getStatusStyle = (status) => {
|
||||
// 客户徽章颜色映射(同一客户保持同一颜色)
|
||||
const companyColorMap = new Map();
|
||||
const COMPANY_PALETTE = [
|
||||
{ color: "#409EFF", background: "#ecf5ff" },
|
||||
{ color: "#67C23A", background: "#f0f9eb" },
|
||||
{ color: "#E6A23C", background: "#fdf6ec" },
|
||||
{ color: "#F56C6C", background: "#fef0f0" },
|
||||
{ color: "#909399", background: "#f4f4f5" },
|
||||
{ color: "#9C27B0", background: "#f3e5f5" },
|
||||
{ color: "#00BCD4", background: "#e0f7fa" },
|
||||
{ color: "#FF9800", background: "#fff3e0" },
|
||||
{ color: "#4f46e5", background: "#eef0ff" },
|
||||
{ color: "#16a34a", background: "#ecfdf3" },
|
||||
{ color: "#d97706", background: "#fffbeb" },
|
||||
{ color: "#dc2626", background: "#fef2f2" },
|
||||
{ color: "#6b7280", background: "#f3f4f6" },
|
||||
{ color: "#7c3aed", background: "#f5f0ff" },
|
||||
{ color: "#0891b2", background: "#ecfeff" },
|
||||
{ color: "#ea580c", background: "#fff7ed" },
|
||||
];
|
||||
let nextColorIndex = 0;
|
||||
|
||||
@@ -200,118 +204,81 @@ const getCompanyStyle = (name) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.progress-container {
|
||||
padding-right: 10px;
|
||||
.progress-container { padding-right: 10px; }
|
||||
:deep(.el-progress__text) { font-size: 11px !important; min-width: 28px; }
|
||||
|
||||
.compact-card {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 调整进度条内部文字大小 */
|
||||
:deep(.el-progress__text) {
|
||||
font-size: 11px !important;
|
||||
min-width: 28px;
|
||||
:deep(.el-table) {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.staff-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.staff-wrapper { display: flex; flex-direction: column; gap: 4px; }
|
||||
|
||||
.staff-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex; align-items: center; height: 20px;
|
||||
font-size: 11px; border-radius: 4px; overflow: hidden;
|
||||
}
|
||||
|
||||
.staff-role {
|
||||
width: 34px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
background: #909399;
|
||||
color: #fff;
|
||||
width: 30px; height: 100%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 9px; font-weight: 600; color: #fff;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.staff-name {
|
||||
padding-left: 6px;
|
||||
color: inherit;
|
||||
}
|
||||
.staff-name { padding-left: 6px; color: inherit; }
|
||||
|
||||
.staff-item.client {
|
||||
background: #f4f4f5;
|
||||
color: #909399;
|
||||
border: 1px solid #e9e9eb;
|
||||
background: #f5f5f7; color: #6b6d7a;
|
||||
border: 1px solid #ededf0;
|
||||
}
|
||||
.staff-item.client .staff-role { background: #8b8d97; }
|
||||
|
||||
.staff-item.internal {
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
border: 1px solid #d9ecff;
|
||||
background: #f5f5f7; color: #6b6d7a;
|
||||
border: 1px solid #ededf0;
|
||||
}
|
||||
.staff-item.internal .staff-role { background: #8b8d97; }
|
||||
|
||||
.staff-item.internal .staff-role {
|
||||
background: #409eff;
|
||||
.data-num-text { font-family: var(--font-mono); font-weight: 600; color: var(--color-primary); }
|
||||
.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 {
|
||||
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 {
|
||||
.lock-badge {
|
||||
display: inline-block;
|
||||
padding: 0 8px;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
padding: 1px 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
border-radius: 11px;
|
||||
font-weight: 500;
|
||||
color: #9ca3af;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
.lock-badge.locked {
|
||||
color: #16a34a;
|
||||
background: #ecfdf3;
|
||||
}
|
||||
|
||||
.company-badge {
|
||||
max-width: 110px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.time-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 11px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.update-label {
|
||||
color: #909399;
|
||||
}
|
||||
.time-wrapper { display: flex; flex-direction: column; font-size: 11px; color: var(--text-secondary); }
|
||||
.update-label { color: var(--text-muted); font-family: var(--font-mono); font-size: 10px; }
|
||||
|
||||
:deep(.table-header) {
|
||||
background-color: #fafafa !important;
|
||||
background: var(--color-bg) !important;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,272 +1,269 @@
|
||||
<template>
|
||||
<div class="full-screen-layout">
|
||||
<el-container class="full-screen-container">
|
||||
<el-aside width="200px" class="siderbar">
|
||||
<div class="sidebar-header">
|
||||
<!-- 修复:将 logo 区域包装成一个正确的外部链接 -->
|
||||
<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 class="shell">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div class="brand-mark">
|
||||
<img src="@/assets/logo.jpg" alt="logo" class="brand-img" />
|
||||
</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>
|
||||
<el-menu-item index="/">
|
||||
<el-icon>
|
||||
<icon-menu />
|
||||
</el-icon>
|
||||
<span>工单管理</span>
|
||||
</el-menu-item>
|
||||
<nav class="nav">
|
||||
<router-link to="/" class="nav-item" active-class="active" exact-active-class="active">
|
||||
<span class="nav-dot"></span>
|
||||
<span class="nav-label">工单管理</span>
|
||||
</router-link>
|
||||
<router-link to="/stats" class="nav-item" active-class="active">
|
||||
<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">
|
||||
<el-icon>
|
||||
<document />
|
||||
</el-icon>
|
||||
<span>数据统计</span>
|
||||
</el-menu-item>
|
||||
<div class="sidebar-foot">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-text">运行中</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<el-menu-item index="/companyAoperators">
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
<span>客户管理</span>
|
||||
</el-menu-item>
|
||||
|
||||
<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 class="main">
|
||||
<header class="topbar">
|
||||
<span class="page-title">{{ route.meta?.title || '工单管理' }}</span>
|
||||
<el-dropdown trigger="click">
|
||||
<div class="user-chip">
|
||||
<el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" />
|
||||
<span class="user-name">{{ user.name }}</span>
|
||||
</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">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
<main class="content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, onMounted } from "vue";
|
||||
import { Menu as IconMenu, Document, User } from "@element-plus/icons-vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { resetRouter } from "@/router";
|
||||
import { userApi } from "@/api/ticket";
|
||||
|
||||
const router = useRouter();
|
||||
const user = reactive({
|
||||
name: "加载中...",
|
||||
avatar: "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png",
|
||||
});
|
||||
const route = useRoute();
|
||||
const user = reactive({ name: "..." });
|
||||
|
||||
onMounted(() => {
|
||||
const savedUser =
|
||||
localStorage.getItem("user_info") || sessionStorage.getItem("user_info");
|
||||
if (savedUser) {
|
||||
const data = JSON.parse(savedUser);
|
||||
user.name = data.username || "管理员";
|
||||
}
|
||||
const saved = localStorage.getItem("user_info") || sessionStorage.getItem("user_info");
|
||||
if (saved) user.name = JSON.parse(saved).username || "用户";
|
||||
});
|
||||
|
||||
const handleLogout = async () => {
|
||||
// 1. 调用后端接口销毁 session
|
||||
try {
|
||||
await userApi.logout();
|
||||
} catch (e) {
|
||||
// 即使后端接口异常,也要完成前端的清理
|
||||
}
|
||||
|
||||
try { await userApi.logout(); } catch (e) { }
|
||||
localStorage.removeItem("user_info");
|
||||
sessionStorage.removeItem("user_info");
|
||||
|
||||
resetRouter();
|
||||
router.push({ name: "login" });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* --- 基础布局保持不变 --- */
|
||||
.full-screen-layout,
|
||||
.full-screen-container {
|
||||
.shell {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: #ebedf0;
|
||||
}
|
||||
|
||||
.siderbar {
|
||||
background-color: #001529;
|
||||
color: white;
|
||||
/* 禁止用户选中文字 */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
/* 兼容 Safari */
|
||||
-moz-user-select: none;
|
||||
/* 兼容 Firefox */
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
height: 80px;
|
||||
/* ---- Sidebar ---- */
|
||||
.sidebar {
|
||||
width: 224px;
|
||||
flex-shrink: 0;
|
||||
background: #f3f4f7;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.07);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* 修复:为新添加的链接添加样式 */
|
||||
.logo-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
.sidebar-brand {
|
||||
padding: 22px 20px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
.brand-mark {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
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 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
.brand-img { width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
.brand-text { display: flex; flex-direction: column; gap: 2px; }
|
||||
|
||||
.brand-name {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1c1c28;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.system-name {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
.brand-ver {
|
||||
font-size: 10px;
|
||||
color: #9a9ba7;
|
||||
letter-spacing: 0.03em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.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;
|
||||
/* ---- Navigation ---- */
|
||||
.nav {
|
||||
flex: 1;
|
||||
padding: 14px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 20px;
|
||||
height: 60px;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.user-profile {
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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;
|
||||
outline: none;
|
||||
/* 保持隐藏默认框 */
|
||||
height: 48px;
|
||||
/* 稍微缩小高度,让点击范围更精致 */
|
||||
padding: 0 12px;
|
||||
/* 左右加点间距 */
|
||||
border-radius: 6px;
|
||||
/* 加个圆角 */
|
||||
transition: all 0.2s;
|
||||
/* 让颜色变化更顺滑 */
|
||||
padding: 4px 14px 4px 4px;
|
||||
border-radius: 24px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
/* 鼠标悬停时的效果 */
|
||||
.user-profile:hover {
|
||||
background-color: #f0f2f5;
|
||||
.user-chip:hover { background: rgba(91, 95, 227, 0.04); }
|
||||
|
||||
.user-chip :deep(.el-avatar) {
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
}
|
||||
|
||||
/* 关键:鼠标点击那一瞬间的效果 */
|
||||
.user-profile:active {
|
||||
background-color: #e6e8eb;
|
||||
/* 背景变深 */
|
||||
transform: scale(0.98);
|
||||
/* 轻轻缩放,增加“按下去”的质感 */
|
||||
.user-name {
|
||||
font-size: 13px;
|
||||
color: #1c1c28;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1;
|
||||
/* 消除默认行高导致的下偏 */
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
height: calc(100vh - 60px);
|
||||
/* ---- Content ---- */
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px 28px;
|
||||
}
|
||||
|
||||
/* 过渡动画 */
|
||||
.fade-transform-enter-active,
|
||||
.fade-transform-leave-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fade-transform-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
.fade-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
}
|
||||
/* ---- Transitions ---- */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active { transition: opacity 0.15s, transform 0.15s; }
|
||||
.fade-enter-from { opacity: 0; transform: translateY(4px); }
|
||||
.fade-leave-to { opacity: 0; transform: translateY(-4px); }
|
||||
</style>
|
||||
|
||||
@@ -19,7 +19,7 @@ export function formatDate(time) {
|
||||
|
||||
const timestamp = Number(time)
|
||||
if (!isNaN(timestamp) && timestamp > 0) {
|
||||
const date = new Date(timestamp * 1000)
|
||||
const date = new Date(timestamp > 9999999999 ? timestamp : timestamp * 1000)
|
||||
if (!isNaN(date.getTime())) {
|
||||
const y = date.getFullYear()
|
||||
const m = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
|
||||
@@ -1,95 +1,123 @@
|
||||
<template>
|
||||
<div class="account-container">
|
||||
<h2>账户管理</h2>
|
||||
|
||||
<div class="action-bar">
|
||||
<div class="search-group">
|
||||
<el-input v-model="searchQuery" placeholder="搜索用户名/真实姓名" style="width: 250px" clearable />
|
||||
<div class="account-root">
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card" :class="{ active: activeRole === s.role }" v-for="s in stats" :key="s.key"
|
||||
:style="{ '--border': s.color }" @click="activeRole = activeRole === s.role ? null : s.role">
|
||||
<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>
|
||||
<el-button type="primary" :icon="Plus" @click="openRegisterDialog">注册用户</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="filteredUsers" 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="70" align="center" /> -->
|
||||
<el-table-column prop="username" label="用户名" min-width="120" />
|
||||
<el-table-column prop="real_name" label="真实姓名" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.real_name || '-' }}
|
||||
</template>
|
||||
</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)">
|
||||
删除
|
||||
<!-- 主面板 -->
|
||||
<div class="main-card">
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<span class="header-title">用户列表</span>
|
||||
<span class="header-count">{{ filteredUsers.length }} 人</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-input v-model="searchQuery" placeholder="搜索用户名或真实姓名" style="width: 240px" size="default" clearable
|
||||
:prefix-icon="Search" />
|
||||
<el-button type="primary" :icon="Plus" @click="openRegisterDialog">
|
||||
注册用户
|
||||
</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>
|
||||
</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
|
||||
v-model="registerDialogVisible"
|
||||
title="注册新用户"
|
||||
width="450px"
|
||||
destroy-on-close
|
||||
@close="resetRegisterForm"
|
||||
>
|
||||
<el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" label-width="80px">
|
||||
<el-dialog v-model="registerDialogVisible" title="注册新用户" width="420px" destroy-on-close @close="resetRegisterForm"
|
||||
class="register-dialog">
|
||||
<el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" label-position="top">
|
||||
<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 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 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 label="角色" prop="role">
|
||||
<el-select v-model="registerForm.role" placeholder="请选择角色" style="width: 100%">
|
||||
<el-option label="管理员" value="admin" />
|
||||
<el-option label="员工" value="user" />
|
||||
<el-option label="客户" value="client" />
|
||||
<div class="role-group">
|
||||
<button v-for="opt in roleOptions" :key="opt.value" type="button" class="role-btn"
|
||||
:class="{ selected: registerForm.role === opt.value }" :style="{ '--accent': opt.color }"
|
||||
@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-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="registerDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleRegister">确定注册</el-button>
|
||||
<div class="dialog-actions">
|
||||
<el-button size="large" @click="registerDialogVisible = false">取消</el-button>
|
||||
<el-button size="large" type="primary" :loading="submitting" @click="handleRegister">确定注册</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -97,14 +125,15 @@
|
||||
|
||||
<script setup>
|
||||
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 { accountApi } from "@/api/ticket";
|
||||
import { formatDate } from "@/utils/format";
|
||||
import { accountApi, ticketApi } from "@/api/ticket";
|
||||
|
||||
const loading = ref(false);
|
||||
const users = ref([]);
|
||||
const companyList = ref([]);
|
||||
const searchQuery = ref("");
|
||||
const activeRole = ref(null);
|
||||
|
||||
const registerDialogVisible = ref(false);
|
||||
const registerFormRef = ref(null);
|
||||
@@ -115,19 +144,17 @@ const registerForm = reactive({
|
||||
real_name: "",
|
||||
password: "",
|
||||
role: "user",
|
||||
company_id: null,
|
||||
});
|
||||
|
||||
const registerRules = {
|
||||
username: [
|
||||
{ required: true, message: "请输入用户名", trigger: "blur" },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: "请输入密码", trigger: "blur" },
|
||||
{ min: 6, message: "密码至少6位", trigger: "blur" },
|
||||
],
|
||||
role: [
|
||||
{ required: true, message: "请选择角色", trigger: "change" },
|
||||
],
|
||||
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
|
||||
// password: [
|
||||
// { required: true, message: "请输入密码", trigger: "blur" },
|
||||
// { min: 6, message: "密码至少6位", trigger: "blur" },
|
||||
// ],
|
||||
role: [{ required: true, message: "请选择角色", trigger: "change" }],
|
||||
company_id: [{ required: true, message: "请选择所属公司", trigger: "change" }],
|
||||
};
|
||||
|
||||
const roleLabel = (role) => {
|
||||
@@ -135,23 +162,42 @@ const roleLabel = (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 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(() => {
|
||||
let list = users.value;
|
||||
|
||||
// 角色筛选
|
||||
if (activeRole.value) {
|
||||
list = list.filter(u => u.role === activeRole.value);
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (searchQuery.value) {
|
||||
const q = searchQuery.value.toLowerCase();
|
||||
list = list.filter(
|
||||
(u) =>
|
||||
u.username.toLowerCase().includes(q) ||
|
||||
(u.real_name && u.real_name.toLowerCase().includes(q))
|
||||
);
|
||||
list = list.filter(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));
|
||||
});
|
||||
|
||||
@@ -159,11 +205,8 @@ const fetchUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await accountApi.getAllUsers();
|
||||
if (res.code === 200) {
|
||||
users.value = res.data || [];
|
||||
} else {
|
||||
ElMessage.error(res.msg || "获取用户列表失败");
|
||||
}
|
||||
if (res.code === 200) users.value = res.data || [];
|
||||
else ElMessage.error(res.msg || "获取用户列表失败");
|
||||
} catch (error) {
|
||||
ElMessage.error("获取用户数据失败");
|
||||
} 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 = () => {
|
||||
if (companyList.value.length === 0) fetchCompanyList();
|
||||
registerDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const resetRegisterForm = () => {
|
||||
if (registerFormRef.value) registerFormRef.value.resetFields();
|
||||
registerForm.username = "";
|
||||
registerForm.real_name = "";
|
||||
registerForm.password = "";
|
||||
registerForm.role = "user";
|
||||
Object.assign(registerForm, { username: "", real_name: "", password: "", role: "user", company_id: null });
|
||||
};
|
||||
|
||||
const handleRegister = async () => {
|
||||
@@ -197,9 +248,7 @@ const handleRegister = async () => {
|
||||
ElMessage.error(res.msg || "注册失败");
|
||||
}
|
||||
} catch (e) {
|
||||
if (e?.name !== "ValidationError") {
|
||||
ElMessage.error("注册请求异常");
|
||||
}
|
||||
if (e?.name !== "ValidationError") ElMessage.error("注册请求异常");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
@@ -208,9 +257,7 @@ const handleRegister = async () => {
|
||||
const handleToggleStatus = (row) => {
|
||||
const newStatus = row.status === 1 ? 0 : 1;
|
||||
const action = newStatus === 0 ? "禁用" : "启用";
|
||||
ElMessageBox.confirm(`确定${action}用户【${row.username}】吗?`, "提示", {
|
||||
type: "warning",
|
||||
})
|
||||
ElMessageBox.confirm(`确定${action}用户【${row.username}】吗?`, "提示", { type: "warning" })
|
||||
.then(async () => {
|
||||
const res = await accountApi.updateAccountStatus(row.id, newStatus);
|
||||
if (res.code === 200) {
|
||||
@@ -220,50 +267,318 @@ const handleToggleStatus = (row) => {
|
||||
ElMessage.error(res.msg || `${action}失败`);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => { });
|
||||
};
|
||||
|
||||
const handleDeleteAccount = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定删除用户【${row.username}】吗?此操作不可逆。`,
|
||||
"警告",
|
||||
{ type: "error", confirmButtonClass: "el-button--danger" }
|
||||
)
|
||||
ElMessageBox.confirm(`确定删除用户【${row.username}】吗?此操作不可逆。`, "警告", {
|
||||
type: "error",
|
||||
confirmButtonClass: "el-button--danger",
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await accountApi.deleteAccount(row.id);
|
||||
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);
|
||||
ElMessage.success("删除成功");
|
||||
} else {
|
||||
ElMessage.error(res.msg || "删除失败");
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => { });
|
||||
};
|
||||
|
||||
onMounted(fetchUsers);
|
||||
onMounted(() => { fetchUsers(); fetchCompanyList(); });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.account-container {
|
||||
padding: 30px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
.account-root {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 25px;
|
||||
color: #303133;
|
||||
/* ---- 统计卡片 ---- */
|
||||
.stats-row {
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.role-btn {
|
||||
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>
|
||||
|
||||
@@ -1,131 +1,137 @@
|
||||
<template>
|
||||
<div class="client-view-container">
|
||||
<header class="client-header">
|
||||
<div class="brand">
|
||||
<div class="logo-box">
|
||||
<el-icon>
|
||||
<DataLine />
|
||||
</el-icon>
|
||||
<div class="cv-wrap">
|
||||
<!-- Header -->
|
||||
<header class="cv-header">
|
||||
<div class="cv-brand">
|
||||
<div class="cv-logo">
|
||||
<el-icon><DataLine /></el-icon>
|
||||
</div>
|
||||
<div class="brand-text">
|
||||
<span class="title">{{ currentUser.company_name || "数据交付监控台" }}</span>
|
||||
<span class="version">Real-time Monitor v2.0</span>
|
||||
<div>
|
||||
<div class="cv-title">方舟星河</div>
|
||||
<div class="cv-subtitle">采集数据平台 · Monitor v2.0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<el-space :size="20">
|
||||
<div class="user-badge">
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
<span>{{ currentUser.username }}</span>
|
||||
</div>
|
||||
<el-button type="danger" plain size="small" @click="handleLogout" :icon="SwitchButton">退出</el-button>
|
||||
</el-space>
|
||||
<div class="cv-user">
|
||||
<div class="cv-avatar">{{ currentUser.username?.charAt(0) || 'U' }}</div>
|
||||
<span class="cv-username">{{ currentUser.username }}</span>
|
||||
<el-button class="cv-logout-btn" @click="handleLogout" :icon="SwitchButton" size="small">退出</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="client-stats">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6" v-for="stat in summaryStats" :key="stat.label">
|
||||
<div class="stat-card-premium" :class="{ 'is-active': activeStatus === stat.status }"
|
||||
:style="{ '--theme-color': stat.color }" @click="handleStatFilter(stat.status)">
|
||||
<div class="stat-info">
|
||||
<span class="s-label">{{ stat.label }}</span>
|
||||
<span class="s-value">{{ stat.value }}</span>
|
||||
</div>
|
||||
<div class="stat-icon-wrapper">
|
||||
<el-icon>
|
||||
<component :is="stat.icon" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div v-if="activeStatus === stat.status" class="active-dot"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- Stats -->
|
||||
<section class="cv-stats">
|
||||
<div
|
||||
v-for="stat in summaryStats"
|
||||
:key="stat.label"
|
||||
class="cv-stat-card"
|
||||
:class="{ 'is-active': activeStatus === stat.status }"
|
||||
:style="{ '--c': stat.color }"
|
||||
@click="handleStatFilter(stat.status)"
|
||||
>
|
||||
<div class="cv-stat-icon">
|
||||
<el-icon><component :is="stat.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="cv-stat-body">
|
||||
<div class="cv-stat-val">{{ stat.value }}</div>
|
||||
<div class="cv-stat-label">{{ stat.label }}</div>
|
||||
</div>
|
||||
<div class="cv-stat-bar"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<main class="data-section">
|
||||
<div class="section-header">
|
||||
<div class="header-left">
|
||||
<span class="indicator"></span>
|
||||
<span class="section-title">实时交付看板</span>
|
||||
<el-tag v-if="activeStatus" closable size="small" type="info" class="filter-tag" @close="activeStatus = null">
|
||||
当前筛选: {{ activeStatus }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button type="primary" link @click="loadData" :loading="loading">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon> 同步最新数据
|
||||
</el-button>
|
||||
<!-- Table Panel -->
|
||||
<main class="cv-panel">
|
||||
<div class="cv-panel-header">
|
||||
<div class="cv-panel-title">
|
||||
<span class="cv-title-dot"></span>
|
||||
实时交付看板
|
||||
<el-tag
|
||||
v-if="activeStatus"
|
||||
closable
|
||||
size="small"
|
||||
class="cv-filter-tag"
|
||||
@close="activeStatus = null"
|
||||
>{{ activeStatus }}</el-tag>
|
||||
</div>
|
||||
<el-button type="primary" link :loading="loading" @click="loadData" :icon="Refresh" size="small">
|
||||
同步数据
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="paginatedTickets" v-loading="loading" class="custom-client-table">
|
||||
<el-table-column prop="id" label="单号" width="90" align="center">
|
||||
<el-table
|
||||
: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 }">
|
||||
<span class="id-tag">#{{ row.id }}</span>
|
||||
<span class="cv-id">#{{ row.id }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="website" label="网站地址" min-width="260">
|
||||
<el-table-column label="网站地址" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<div class="project-cell">
|
||||
<el-icon class="project-icon">
|
||||
<Monitor />
|
||||
</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 class="cv-site-cell">
|
||||
<div class="cv-site-favicon">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
</div>
|
||||
<el-progress :percentage="Number(scope.row.progress || 0)"
|
||||
:status="Number(scope.row.progress) === 100 ? 'success' : ''" :stroke-width="10" :show-text="false"
|
||||
striped striped-flow />
|
||||
<span class="cv-site-name" :title="row.website">{{ row.website }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="datanum" label="总数据量" width="160" align="center">
|
||||
<template #default="scope">
|
||||
<div class="data-val-box">
|
||||
<span class="data-num">{{ formatNumber(scope.row.datanum) }}</span>
|
||||
<el-table-column label="交付进度" width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="cv-progress-cell">
|
||||
<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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="交付状态" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<div :class="['status-dot-box', getStatusClass(scope.row.is_complete)]">
|
||||
<span class="dot"></span>
|
||||
<span class="text">{{ scope.row.is_complete }}</span>
|
||||
</div>
|
||||
<el-table-column label="数据量" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="cv-datanum">{{ formatNumber(row.datanum) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="更新日期" width="140" align="center">
|
||||
<template #default="scope">
|
||||
<span class="date-text">{{
|
||||
formatDate(
|
||||
scope.row.lock_time || scope.row.update_time || scope.row.create_time
|
||||
)
|
||||
}}</span>
|
||||
<el-table-column label="状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="['cv-status-badge', getStatusClass(row.status)]">
|
||||
<i class="cv-status-dot"></i>{{ statusLabel(row.status) }}
|
||||
</span>
|
||||
</template>
|
||||
</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>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<footer class="pagination-footer">
|
||||
<el-pagination v-model:current-page="page" v-model:page-size="limit" :total="total"
|
||||
layout="total, prev, pager, next" background @current-change="handlePageChange" />
|
||||
</footer>
|
||||
<div class="cv-footer">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="limit"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
background
|
||||
small
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -133,17 +139,12 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { resetRouter } from "@/router";
|
||||
import {
|
||||
DataLine,
|
||||
SwitchButton,
|
||||
User,
|
||||
Refresh,
|
||||
Files,
|
||||
Timer,
|
||||
CircleCheck,
|
||||
Monitor,
|
||||
DataLine, SwitchButton, User, Refresh,
|
||||
Files, Timer, CircleCheck, Monitor,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { ticketApi, userApi } from "@/api/ticket";
|
||||
import { clientApi, userApi } from "@/api/ticket";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { formatDate } from "@/utils/format";
|
||||
|
||||
@@ -152,387 +153,217 @@ const loading = ref(false);
|
||||
const tickets = ref([]);
|
||||
const page = ref(1);
|
||||
const limit = ref(15);
|
||||
const activeStatus = ref(null);
|
||||
|
||||
// --- 筛选状态控制 ---
|
||||
const activeStatus = ref(null); // null 代表显示全部,其余为具体状态字符串
|
||||
const statusMap = { 0: "未分配", 1: "进行中", 2: "已完成", 3: "已完结" };
|
||||
const statusLabel = (s) => statusMap[Number(s)] || "未知";
|
||||
|
||||
// --- 1. 用户信息 ---
|
||||
const currentUser = computed(() => {
|
||||
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 () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const customerId = currentUser.value.customer_id;
|
||||
const res = await ticketApi.getTickets({
|
||||
page: 1,
|
||||
limit: 1000, // 客户侧通常数据量不大,取1000条前端分页
|
||||
customer_id: customerId,
|
||||
});
|
||||
|
||||
if (res.code === 200 || res.code === true) {
|
||||
tickets.value = res.data.data;
|
||||
}
|
||||
} catch (e) {
|
||||
const res = await clientApi.getTickets();
|
||||
if (res.code === 200) tickets.value = res.data || [];
|
||||
} catch {
|
||||
ElMessage.error("数据同步失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// --- 3. 核心:双重过滤逻辑 (权限过滤 + 卡片点击过滤) ---
|
||||
const baseTickets = computed(() => tickets.value.filter((t) => Number(t.status) !== 3));
|
||||
const filteredTickets = computed(() => {
|
||||
const allowed = ["未分配", "进行中", "已完成"];
|
||||
// 第一步:过滤掉“已完结”等不需展示给客户的状态
|
||||
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;
|
||||
if (!activeStatus.value) return baseTickets.value;
|
||||
return baseTickets.value.filter((t) => statusLabel(t.status) === activeStatus.value);
|
||||
});
|
||||
|
||||
const paginatedTickets = computed(() => {
|
||||
const start = (page.value - 1) * limit.value;
|
||||
return filteredTickets.value.slice(start, start + limit.value);
|
||||
});
|
||||
|
||||
const total = computed(() => filteredTickets.value.length);
|
||||
|
||||
// --- 4. 统计卡片数据定义 ---
|
||||
const summaryStats = computed(() => {
|
||||
// 基础权限过滤后的原始数组
|
||||
const baseData = tickets.value.filter((t) =>
|
||||
["未分配", "进行中", "已完成"].includes(t.is_complete)
|
||||
);
|
||||
|
||||
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 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: "未分配" },
|
||||
{ label: "正在采集", value: baseTickets.value.filter((t) => Number(t.status) === 1).length, color: "#f59e0b", icon: Timer, status: "进行中" },
|
||||
{ label: "已就绪", value: baseTickets.value.filter((t) => Number(t.status) === 2).length, color: "#10b981", icon: CircleCheck, status: "已完成" },
|
||||
]);
|
||||
|
||||
const handleStatFilter = (status) => { activeStatus.value = status; page.value = 1; };
|
||||
const formatNumber = (val) => {
|
||||
if (!val || val === "0") return "0";
|
||||
const num = Number(val);
|
||||
return isNaN(num) ? val : num.toLocaleString();
|
||||
if (!val || val === "0") return "—";
|
||||
const n = Number(val);
|
||||
return isNaN(n) ? val : n.toLocaleString();
|
||||
};
|
||||
|
||||
const getStatusClass = (status) => {
|
||||
const map = { 未分配: "is-waiting", 进行中: "is-running", 已完成: "is-success" };
|
||||
return map[status] || "";
|
||||
};
|
||||
|
||||
const handlePageChange = (p) => {
|
||||
page.value = p;
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
};
|
||||
|
||||
const getStatusClass = (s) => ({ 0: "s-wait", 1: "s-run", 2: "s-done" })[Number(s)] || "";
|
||||
const handlePageChange = (p) => { page.value = p; window.scrollTo({ top: 0, behavior: "smooth" }); };
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await userApi.logout();
|
||||
} catch (e) {}
|
||||
|
||||
localStorage.removeItem("user_info");
|
||||
sessionStorage.removeItem("user_info");
|
||||
router.push("/login");
|
||||
try { await userApi.logout(); } catch {}
|
||||
resetRouter();
|
||||
router.push({ name: "login" });
|
||||
};
|
||||
|
||||
onMounted(loadData);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.client-view-container {
|
||||
padding: 24px 40px;
|
||||
background-color: #f6f8fb;
|
||||
min-height: 100vh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
/* ── tokens ── */
|
||||
.cv-wrap {
|
||||
--cv-th-bg: #f8f9fb;
|
||||
--cv-radius: 14px;
|
||||
padding: 8px 4px 24px;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.client-header {
|
||||
/* ── Header ── */
|
||||
.cv-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
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;
|
||||
}
|
||||
|
||||
.brand {
|
||||
.cv-stat-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--cv-radius);
|
||||
padding: 20px 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.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;
|
||||
gap: 16px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.03);
|
||||
border-left: 4px solid var(--theme-color);
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
cursor: pointer;
|
||||
transition: transform 0.18s, box-shadow 0.18s, border-color 0.18s;
|
||||
}
|
||||
|
||||
.stat-card-premium:hover {
|
||||
.cv-stat-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.08);
|
||||
}
|
||||
|
||||
.stat-card-premium.is-active {
|
||||
background: #f0f7ff;
|
||||
border-left-width: 8px;
|
||||
box-shadow: inset 0 0 0 1px rgba(64, 158, 255, 0.1), 0 8px 20px rgba(0, 0, 0, 0.1);
|
||||
.cv-stat-card.is-active {
|
||||
border-color: var(--c);
|
||||
background: color-mix(in srgb, var(--c) 5%, #fff);
|
||||
box-shadow: 0 6px 20px color-mix(in srgb, var(--c) 18%, transparent);
|
||||
}
|
||||
|
||||
.active-dot {
|
||||
.cv-stat-bar {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--theme-color);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 8px var(--theme-color);
|
||||
animation: pulse 2s infinite;
|
||||
bottom: 0; left: 0; right: 0;
|
||||
height: 3px;
|
||||
background: var(--c);
|
||||
opacity: 0;
|
||||
transition: opacity 0.18s;
|
||||
border-radius: 0 0 var(--cv-radius) var(--cv-radius);
|
||||
}
|
||||
.cv-stat-card.is-active .cv-stat-bar,
|
||||
.cv-stat-card:hover .cv-stat-bar { opacity: 1; }
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
.cv-stat-icon {
|
||||
width: 46px; height: 46px; border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--c) 10%, transparent);
|
||||
color: var(--c);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.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 {
|
||||
font-size: 13px;
|
||||
color: #86868b;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
/* ── Panel ── */
|
||||
.cv-panel {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--cv-radius);
|
||||
padding: 24px 28px 20px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
.cv-panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
.cv-panel-title {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
font-size: 14px; font-weight: 600; color: var(--text-primary);
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
background: #409eff;
|
||||
.cv-title-dot {
|
||||
width: 3px; height: 16px;
|
||||
background: linear-gradient(180deg, #6366f1, #a78bfa);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.cv-filter-tag { border-radius: 6px; }
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
/* ── Table cells ── */
|
||||
.cv-table { width: 100%; }
|
||||
.cv-id { font-family: var(--font-mono); font-size: 12px; color: var(--text-muted); }
|
||||
|
||||
.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 {
|
||||
margin-left: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.cv-progress-cell { display: flex; align-items: center; gap: 10px; }
|
||||
.cv-progress-cell .el-progress { flex: 1; }
|
||||
.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 {
|
||||
color: #909399;
|
||||
font-family: "Consolas", monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
.cv-datanum { font-family: var(--font-mono); font-size: 13px; font-weight: 600; color: var(--text-primary); }
|
||||
.cv-date { font-size: 12px; color: var(--text-muted); }
|
||||
|
||||
.project-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
/* ── Status badge ── */
|
||||
.cv-status-badge {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
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 {
|
||||
color: #c0c4cc;
|
||||
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;
|
||||
}
|
||||
/* ── Footer ── */
|
||||
.cv-footer { margin-top: 24px; padding-bottom: 4px; display: flex; justify-content: center; }
|
||||
</style>
|
||||
|
||||
@@ -1,54 +1,58 @@
|
||||
<template>
|
||||
<div class="customers-container">
|
||||
<h2>运营人员管理</h2>
|
||||
<div class="customers-root">
|
||||
<!-- 主面板 -->
|
||||
<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">
|
||||
<div class="search-group">
|
||||
<el-input v-model="searchQuery" placeholder="搜索客户单位" style="width: 250px" clearable />
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<el-button type="primary" :icon="Plus" @click="customerDialogVisible = true">
|
||||
新增客户单位
|
||||
</el-button>
|
||||
<el-button type="success" :icon="User" @click="handleAddOperator">
|
||||
新增运营人员
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="filteredData" v-loading="loading" header-cell-class-name="table-head" row-class-name="table-row">
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无客户数据" />
|
||||
</template>
|
||||
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="company_name" label="客户单位名称" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span class="company-name">{{ row.company_name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<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>
|
||||
|
||||
<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" />
|
||||
|
||||
<OperatorEditDialog v-model="operatorDialogVisible" :company-list="companyList"
|
||||
@success="fetchCompanyAndOperators" />
|
||||
|
||||
<OperatorListDialog v-model="employeeDetailVisible" :company-name="activeCompanyName"
|
||||
:operators="currentCompanyOperators" @refresh="fetchCompanyAndOperators" />
|
||||
</div>
|
||||
@@ -56,7 +60,7 @@
|
||||
|
||||
<script setup>
|
||||
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 { customerApi, ticketApi } from "@/api/ticket";
|
||||
|
||||
@@ -66,7 +70,6 @@ import OperatorListDialog from "@/components/operators/OperatorListDialog.vue";
|
||||
|
||||
const loading = ref(false);
|
||||
const companyList = ref([]);
|
||||
const employeeList = ref([]);
|
||||
const searchQuery = ref("");
|
||||
const companyAndOperators = ref([]);
|
||||
|
||||
@@ -75,19 +78,23 @@ const operatorDialogVisible = ref(false);
|
||||
const employeeDetailVisible = ref(false);
|
||||
|
||||
|
||||
const activeCompanyName = ref(""); // 当前选中客户单位的名称
|
||||
const currentCompanyOperators = ref([]); // 当前选中客户单位的员工列表
|
||||
const activeCompanyName = 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 () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await ticketApi.getOperatorsNew();
|
||||
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 }))
|
||||
}
|
||||
// console.log('获取数据', companyAndOperators.value);
|
||||
} catch (error) {
|
||||
ElMessage.error("获取客户数据失败,请检查网络");
|
||||
} finally {
|
||||
@@ -132,28 +139,88 @@ onMounted(fetchCompanyAndOperators);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.customers-container {
|
||||
padding: 30px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
.customers-root {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 25px;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
/* 圆角 */
|
||||
.header-right :deep(.el-input__wrapper) {
|
||||
border-radius: 20px !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.button-group .el-button {
|
||||
margin-left: 10px;
|
||||
.header-left {
|
||||
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>
|
||||
|
||||
@@ -1,110 +1,97 @@
|
||||
<template>
|
||||
<div class="ark-wrapper">
|
||||
<div class="grid-bg"></div>
|
||||
<div class="scan-line"></div>
|
||||
<div class="login-root">
|
||||
<div class="login-card">
|
||||
<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">
|
||||
<transition name="ark-fade" appear>
|
||||
<div class="ark-card">
|
||||
<div class="corner-decorator top-left"></div>
|
||||
<div class="corner-decorator bottom-right"></div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
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">
|
||||
<div class="brand-icon">
|
||||
<el-icon>
|
||||
<DataBoard />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="brand-text">
|
||||
<h1 class="title">方舟工单</h1>
|
||||
<span class="sub-text">ARK TICKET SYSTEM v4.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
placeholder="访问密钥"
|
||||
size="large"
|
||||
show-password
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Lock /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<div class="system-monitor">
|
||||
<span class="label">STATUS:</span>
|
||||
<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 class="form-row">
|
||||
<el-checkbox v-model="rememberMe">保持登录</el-checkbox>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="submit-btn"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登 录
|
||||
</el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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 { userApi } from "@/api/ticket";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const loginForm = reactive({ username: "", password: "" });
|
||||
const form = reactive({ username: "", password: "" });
|
||||
const rememberMe = 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" }],
|
||||
password: [{ required: true, message: "请输入密钥", trigger: "blur" }],
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return;
|
||||
if (!formRef.value) return;
|
||||
try {
|
||||
await loginFormRef.value.validate();
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
|
||||
const res = await userApi.login(loginForm.username, loginForm.password);
|
||||
|
||||
const res = await userApi.login(form.username, form.password);
|
||||
if (res.code === 200) {
|
||||
// ThinkPHP 会自动通过 Set-Cookie 下发 session,浏览器自动接管,无需手动存 token
|
||||
const userInfo = res.data;
|
||||
const userInfoStr = JSON.stringify(userInfo);
|
||||
|
||||
// 用户信息存前端,仅用于显示用户名和角色路由判断
|
||||
const userInfo = JSON.stringify(res.data);
|
||||
const storage = rememberMe.value ? localStorage : sessionStorage;
|
||||
storage.setItem("user_info", userInfoStr);
|
||||
|
||||
ElMessage({
|
||||
message: "身份验证通过,方舟系统加载中...",
|
||||
type: "success",
|
||||
duration: 300,
|
||||
});
|
||||
|
||||
// 触发路由守卫,自动处理跳转
|
||||
storage.setItem("user_info", userInfo);
|
||||
ElMessage({ message: "登录成功", type: "success", duration: 300 });
|
||||
await router.push(route.query.redirect || "/");
|
||||
} else {
|
||||
ElMessage.error(res.message || "登录失败,请检查账号和密码");
|
||||
ElMessage.error(res.message || "登录失败");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name !== "ValidationError") {
|
||||
@@ -117,243 +104,104 @@ const handleLogin = async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 1. 全局背景:工业网格风格 */
|
||||
.ark-wrapper {
|
||||
position: relative;
|
||||
.login-root {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #0b111a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(165deg, #f6f7fa 0%, #edeff4 40%, #f2f3f7 100%);
|
||||
}
|
||||
|
||||
.grid-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: linear-gradient(rgba(0, 242, 255, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 242, 255, 0.05) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
mask-image: radial-gradient(circle at center, black, transparent 80%);
|
||||
.login-card {
|
||||
width: 380px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-radius: 16px;
|
||||
padding: 44px 38px 38px;
|
||||
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 {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, rgba(0, 242, 255, 0.2), transparent);
|
||||
top: 0;
|
||||
animation: scan 4s linear infinite;
|
||||
.card-head {
|
||||
text-align: center;
|
||||
margin-bottom: 34px;
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0% {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 110%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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%);
|
||||
.logo {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 14px;
|
||||
object-fit: cover;
|
||||
margin-bottom: 16px;
|
||||
box-shadow:
|
||||
0 0 0 4px rgba(91, 95, 227, 0.06),
|
||||
0 4px 12px rgba(91, 95, 227, 0.12);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
margin: 0 0 6px;
|
||||
font-size: 21px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 0;
|
||||
letter-spacing: 2px;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
color: #00f2ff;
|
||||
font-size: 10px;
|
||||
opacity: 0.7;
|
||||
.login-form :deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 3. 监控条 */
|
||||
.system-monitor {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 5px 12px;
|
||||
margin-bottom: 25px;
|
||||
border-left: 3px solid #00f2ff;
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
.login-form :deep(.el-input__wrapper) {
|
||||
border-radius: 10px;
|
||||
padding: 3px 12px;
|
||||
background: #f8f9fb;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05) inset;
|
||||
transition: box-shadow 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.system-monitor .label {
|
||||
color: #64748b;
|
||||
margin-right: 10px;
|
||||
.login-form :deep(.el-input__wrapper:hover) {
|
||||
background: #f4f5f8;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08) inset;
|
||||
}
|
||||
|
||||
.system-monitor .value {
|
||||
color: #22c55e;
|
||||
text-shadow: 0 0 5px #22c55e;
|
||||
.login-form :deep(.el-input__wrapper.is-focus) {
|
||||
background: #fcfcfd;
|
||||
box-shadow: 0 0 0 2px rgba(91, 95, 227, 0.12), 0 0 0 1px rgba(91, 95, 227, 0.3) inset;
|
||||
}
|
||||
|
||||
/* 4. 输入框定制 */
|
||||
.input-prefix {
|
||||
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;
|
||||
.form-row {
|
||||
margin: -2px 0 24px;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__inner) {
|
||||
background: transparent;
|
||||
border-color: #334155;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
margin-top: 30px;
|
||||
text-align: right;
|
||||
border-top: 1px dashed rgba(0, 242, 255, 0.1);
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.code {
|
||||
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;
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.25em;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
<el-avatar :size="28">{{ staff.username.slice(0, 1) }}</el-avatar>
|
||||
<span class="staff-name">{{ staff.username }}</span>
|
||||
</div>
|
||||
<el-tag type="success" effect="dark" round>
|
||||
<span class="count-badge">
|
||||
已锁定 {{ staff.lockedTickets.length }} 个站点
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="staff.lockedTickets" stripe border size="small" class="inner-table">
|
||||
@@ -41,34 +41,112 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<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) }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { ticketApi } from "@/api/ticket";
|
||||
import { ticketApi, exportApi } from "@/api/ticket";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { formatDate } from "@/utils/format";
|
||||
import * as XLSX from "xlsx";
|
||||
|
||||
const loading = ref(true);
|
||||
const staffUserList = ref([]);
|
||||
const allTickets = ref([]);
|
||||
const activeCollapse = ref("");
|
||||
const showOnlyThisMonth = ref(false);
|
||||
const exportRange = ref("week");
|
||||
|
||||
const statusMap = { 0: "未分配", 1: "进行中", 2: "已完成", 3: "已完结" };
|
||||
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 () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -144,12 +222,21 @@ const staffWithLockedTickets = computed(() => {
|
||||
|
||||
<style scoped>
|
||||
.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: 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 {
|
||||
@@ -158,16 +245,17 @@ const staffWithLockedTickets = computed(() => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header>span {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: #1c1c28;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header > span {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.collapse-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -179,12 +267,27 @@ const staffWithLockedTickets = computed(() => {
|
||||
.staff-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.staff-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
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 {
|
||||
@@ -192,12 +295,104 @@ const staffWithLockedTickets = computed(() => {
|
||||
margin: 0 16px 16px 16px;
|
||||
}
|
||||
|
||||
:deep(.inner-table) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.el-collapse) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__header) {
|
||||
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) {
|
||||
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>
|
||||
|
||||
@@ -376,152 +376,96 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 样式部分保持不变 */
|
||||
.ticket-management {
|
||||
padding: 20px;
|
||||
background-color: #f8fafc;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.ticket-management { padding: 0; min-height: 0; background: none; }
|
||||
|
||||
.stats-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(150px, 1fr));
|
||||
gap: 16px;
|
||||
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;
|
||||
grid-template-columns: repeat(5, minmax(140px, 1fr));
|
||||
gap: 14px;
|
||||
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;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
margin-bottom: 12px;
|
||||
background: #fdf6ec;
|
||||
border: 1px solid #faecd8;
|
||||
border-radius: 8px;
|
||||
transition: box-shadow 0.2s, transform 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.batch-info {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
.total { border-left-color: #4f46e5; }
|
||||
.pending { border-left-color: #6b7280; }
|
||||
.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 {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
.stat-icon {
|
||||
font-size: 20px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.stats-container {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
.total .stat-icon { color: #4f46e5; background: rgba(79, 70, 229, 0.12); }
|
||||
.pending .stat-icon { color: #4b5563; background: rgba(75, 85, 99, 0.1); }
|
||||
.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) {
|
||||
.stats-container {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
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>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
牛马身份: {{ realStaffInfo.name }}
|
||||
牛马身份: {{ realStaffInfo.username }}
|
||||
</el-tag>
|
||||
<el-tag v-else type="danger" round size="small"
|
||||
>未录入员工表,无权接单</el-tag
|
||||
@@ -71,11 +71,10 @@
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<el-table
|
||||
:data="paginatedTickets"
|
||||
v-loading="loading"
|
||||
border
|
||||
style="width: 100%; margin-top: 20px"
|
||||
class="custom-table"
|
||||
>
|
||||
<template #empty>
|
||||
@@ -203,7 +202,7 @@
|
||||
</template>
|
||||
</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">
|
||||
<template #default="scope">
|
||||
@@ -274,7 +273,7 @@
|
||||
<el-table-column label="操作" width="190" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="scope.row.is_complete === '未分配'"
|
||||
v-if="Number(scope.row.status) === 0"
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="!realStaffInfo"
|
||||
@@ -283,8 +282,8 @@
|
||||
>
|
||||
<template
|
||||
v-if="
|
||||
scope.row.is_complete === '进行中' &&
|
||||
String(scope.row.staff_id) === String(realStaffInfo?.id)
|
||||
Number(scope.row.status) === 1 &&
|
||||
String(scope.row.user_id) === String(realStaffInfo?.id)
|
||||
"
|
||||
>
|
||||
<el-button type="success" size="small" @click="handleFinish(scope.row)"
|
||||
@@ -310,6 +309,7 @@
|
||||
background
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -339,6 +339,7 @@ const editValue = ref("");
|
||||
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(15);
|
||||
const emptyText = computed(() => `暂无${currentStatus.value}的工单`);
|
||||
|
||||
// js计算时间
|
||||
const welcomeMessage = computed(() => {
|
||||
@@ -386,7 +387,7 @@ const fetchTickets = async () => {
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
params.staff_id = realStaffInfo.value.id;
|
||||
params.user_id = realStaffInfo.value.id;
|
||||
}
|
||||
|
||||
const res = await ticketApi.getTickets(params);
|
||||
@@ -457,8 +458,8 @@ const handleAccept = (row) => {
|
||||
ElMessageBox.confirm(`确认接手工单 #${row.id}?`).then(async () => {
|
||||
try {
|
||||
const res = await ticketApi.updateTicket(row.id, {
|
||||
staff_id: realStaffInfo.value.id,
|
||||
is_complete: "进行中",
|
||||
user_id: realStaffInfo.value.id,
|
||||
status: 1,
|
||||
progress: 0,
|
||||
});
|
||||
if (res.code === 200 || res.code === true) {
|
||||
@@ -478,9 +479,9 @@ const handleFinish = (row) => {
|
||||
const currentTimestamp = Math.floor(Date.now() / 1000);
|
||||
// 关键修改:在提交时增加 progress: 100
|
||||
const res = await ticketApi.updateTicket(row.id, {
|
||||
is_complete: "已完成",
|
||||
status: 2,
|
||||
lock_time: String(currentTimestamp),
|
||||
progress: 100, // 默认改为100
|
||||
progress: 100,
|
||||
});
|
||||
|
||||
if (res.code === 200 || res.code === true) {
|
||||
@@ -502,8 +503,8 @@ const handleCancel = (row) => {
|
||||
).then(async () => {
|
||||
try {
|
||||
const res = await ticketApi.updateTicket(row.id, {
|
||||
staff_id: null,
|
||||
is_complete: "未分配",
|
||||
user_id: null,
|
||||
status: 0,
|
||||
progress: 0,
|
||||
});
|
||||
if (res.code === 200 || res.code === true) {
|
||||
@@ -534,9 +535,9 @@ const handleLogout = () => {
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await ticketApi.getStaffs();
|
||||
const res = await ticketApi.getUserStaffs();
|
||||
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;
|
||||
}
|
||||
} catch (e) {}
|
||||
@@ -545,226 +546,357 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 容器布局 */
|
||||
.header-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
/* ============================================
|
||||
ENERGY WORKBENCH — Gamified Employee Desk
|
||||
============================================ */
|
||||
.user-view-container {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
/* 这里统一控制欢迎语、副标题、标签之间的垂直间距 */
|
||||
/* ---- Header Card (Gradient Hero) ---- */
|
||||
.user-header-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
||||
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 {
|
||||
margin: 0;
|
||||
color: #1a1a1a;
|
||||
color: #fff;
|
||||
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 {
|
||||
margin: 4px 0 0 0;
|
||||
/* 仅顶部保留微小间距 */
|
||||
color: #8c8c8c;
|
||||
margin: 4px 0 0;
|
||||
color: rgba(255,255,255,0.75);
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
/* 标签组位置微调 */
|
||||
.identity-badges {
|
||||
margin-top: 4px;
|
||||
/* 让标签组靠近上方的文字 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 针对 el-tag 内部图标和文字的微调 */
|
||||
:deep(.el-tag) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-style: dashed;
|
||||
/* 既然是牛马,边框可以搞成虚线更有自嘲感 */
|
||||
}
|
||||
|
||||
.user-view-container {
|
||||
padding: 24px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.user-header-card {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
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;
|
||||
.identity-badges { margin-top: 8px; display: flex; align-items: center; position: relative; z-index: 1; }
|
||||
:deep(.identity-badges .el-tag) {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 5px !important;
|
||||
background: rgba(255,255,255,0.18) !important;
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255,255,255,0.25) !important;
|
||||
color: #fff !important;
|
||||
font-weight: 600 !important;
|
||||
padding: 4px 14px !important;
|
||||
border-radius: 20px !important;
|
||||
}
|
||||
:deep(.identity-badges .el-tag .el-icon) { color: #fbbf24 !important; }
|
||||
|
||||
.header-action { position: relative; z-index: 1; }
|
||||
.header-action :deep(.el-button) {
|
||||
background: rgba(255,255,255,0.15) !important;
|
||||
border: 1px solid rgba(255,255,255,0.25) !important;
|
||||
color: #fff !important;
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: 24px !important;
|
||||
padding: 8px 18px !important;
|
||||
}
|
||||
.header-action :deep(.el-button:hover) {
|
||||
background: rgba(255,255,255,0.25) !important;
|
||||
}
|
||||
|
||||
/* ---- Tab Switcher ---- */
|
||||
.status-tabs-wrapper {
|
||||
background: white;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
border-radius: 14px;
|
||||
padding: 5px;
|
||||
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) {
|
||||
border: none !important;
|
||||
border-radius: 6px !important;
|
||||
margin: 0 4px;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 10px !important;
|
||||
margin: 0 1px;
|
||||
padding: 10px 18px;
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: #6b6d7a;
|
||||
background: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.custom-table {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
|
||||
.custom-radio-group :deep(.el-radio-button__inner:hover) {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.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;
|
||||
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: #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;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pop-header {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: #434343;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
color: #1c1c28;
|
||||
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 {
|
||||
margin-top: 12px;
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 1. 强行修正进度条内部容器的对齐方式 */
|
||||
: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;
|
||||
.pop-footer :deep(.el-button--primary) {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2) !important;
|
||||
border: none !important;
|
||||
border-radius: 10px !important;
|
||||
padding: 7px 20px !important;
|
||||
font-weight: 600 !important;
|
||||
box-shadow: 0 2px 8px rgba(102,126,234,0.3);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.pop-footer :deep(.el-button--primary:hover) {
|
||||
box-shadow: 0 4px 16px rgba(102,126,234,0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 2. 针对 100% 时出现的那个 svg 图标进行位移修正 */
|
||||
:deep(.el-progress-bar__innerText svg),
|
||||
:deep(.el-progress-bar__innerText .el-icon) {
|
||||
margin: 0 !important;
|
||||
/* 关键:利用相对定位向上平移 1px,抵消视觉上的下沉 */
|
||||
position: relative !important;
|
||||
top: -1px !important;
|
||||
/* 稍微调整图标大小,让它在 10px 的进度条里更协调 */
|
||||
width: 0.8em;
|
||||
height: 0.8em;
|
||||
/* Progress radio buttons in popover */
|
||||
:deep(.popover-content .el-radio-button__inner) {
|
||||
border-radius: 8px !important;
|
||||
padding: 6px 12px !important;
|
||||
}
|
||||
:deep(.popover-content .el-radio-button__inner:hover) {
|
||||
color: #667eea !important;
|
||||
}
|
||||
:deep(.popover-content .is-active .el-radio-button__inner) {
|
||||
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. 如果还是觉得整体偏,可以给进度条容器加一个微调 */
|
||||
:deep(.el-progress) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* ---- Action Buttons Glow ---- */
|
||||
:deep(.el-table .el-button--primary) { transition: all 0.2s; }
|
||||
:deep(.el-table .el-button--primary:hover) { box-shadow: 0 0 12px var(--glow-indigo); }
|
||||
: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>
|
||||
|
||||
Reference in New Issue
Block a user