diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..8432d47 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "mcp__chrome-devtools__list_pages" + ] + } +} diff --git a/package-lock.json b/package-lock.json index 87b2abc..fb62619 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "element-plus": "^2.11.8", "vue": "^3.5.22", "vue-echarts": "^8.0.1", - "vue-router": "^4.6.4" + "vue-router": "^4.6.4", + "xlsx": "^0.18.5" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.1", @@ -1744,6 +1745,15 @@ } } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ansis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", @@ -1881,6 +1891,28 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1916,6 +1948,18 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2233,6 +2277,15 @@ "node": ">= 6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2801,6 +2854,18 @@ "node": ">=0.10.0" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/superjson": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz", @@ -3120,6 +3185,24 @@ "vue": "^3.5.0" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/wsl-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", @@ -3136,6 +3219,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 462e53a..76692e6 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "element-plus": "^2.11.8", "vue": "^3.5.22", "vue-echarts": "^8.0.1", - "vue-router": "^4.6.4" + "vue-router": "^4.6.4", + "xlsx": "^0.18.5" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.1", diff --git a/src/api/login.js b/src/api/login.js new file mode 100644 index 0000000..3b667c8 --- /dev/null +++ b/src/api/login.js @@ -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') + } +} \ No newline at end of file diff --git a/src/api/ticket.js b/src/api/ticket.js index a016f39..fa01fd3 100644 --- a/src/api/ticket.js +++ b/src/api/ticket.js @@ -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}`) } -} \ No newline at end of file +} + +export const exportApi = { + exportdata(range) { + return adminRequest.post('/getExportData', { range }) + } +} diff --git a/src/assets/main.css b/src/assets/main.css index 32fc0a3..7d5dc37 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -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; - } -} \ No newline at end of file +.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); +} diff --git a/src/components/ticket/TicketFilter.vue b/src/components/ticket/TicketFilter.vue index d8eb496..cb643e4 100644 --- a/src/components/ticket/TicketFilter.vue +++ b/src/components/ticket/TicketFilter.vue @@ -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%; } +} diff --git a/src/components/ticket/TicketForm.vue b/src/components/ticket/TicketForm.vue index dddb746..524dbd1 100644 --- a/src/components/ticket/TicketForm.vue +++ b/src/components/ticket/TicketForm.vue @@ -13,6 +13,16 @@ + + + + + +
@@ -21,16 +31,17 @@ - + - + @@ -70,7 +81,7 @@ diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 0079b97..d7a349c 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -1,272 +1,269 @@ diff --git a/src/utils/format.js b/src/utils/format.js index f2acd40..1201bec 100644 --- a/src/utils/format.js +++ b/src/utils/format.js @@ -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') diff --git a/src/views/AccountRegistrationView.vue b/src/views/AccountRegistrationView.vue index b5fcfb3..86b1db5 100644 --- a/src/views/AccountRegistrationView.vue +++ b/src/views/AccountRegistrationView.vue @@ -1,95 +1,123 @@