csm修改

master
2358328281@qq.com 6 days ago
parent cc2eb2f3a9
commit d3943a3755

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

534
package-lock.json generated

@ -10,6 +10,8 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@jaames/iro": "^5.5.2", "@jaames/iro": "^5.5.2",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.13.2", "axios": "^1.13.2",
"dayjs": "^1.11.21", "dayjs": "^1.11.21",
"echarts": "^6.0.0", "echarts": "^6.0.0",
@ -1320,6 +1322,12 @@
"win32" "win32"
] ]
}, },
"node_modules/@transloadit/prettier-bytes": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz",
"integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==",
"license": "MIT"
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@ -1327,6 +1335,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@types/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==",
"license": "MIT"
},
"node_modules/@types/lodash": { "node_modules/@types/lodash": {
"version": "4.17.20", "version": "4.17.20",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
@ -1368,6 +1382,61 @@
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==", "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@uppy/companion-client": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-2.2.2.tgz",
"integrity": "sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==",
"license": "MIT",
"dependencies": {
"@uppy/utils": "^4.1.2",
"namespace-emitter": "^2.0.1"
}
},
"node_modules/@uppy/core": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@uppy/core/-/core-2.3.4.tgz",
"integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
"license": "MIT",
"dependencies": {
"@transloadit/prettier-bytes": "0.0.7",
"@uppy/store-default": "^2.1.1",
"@uppy/utils": "^4.1.3",
"lodash.throttle": "^4.1.1",
"mime-match": "^1.0.2",
"namespace-emitter": "^2.0.1",
"nanoid": "^3.1.25",
"preact": "^10.5.13"
}
},
"node_modules/@uppy/store-default": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@uppy/store-default/-/store-default-2.1.1.tgz",
"integrity": "sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==",
"license": "MIT"
},
"node_modules/@uppy/utils": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-4.1.3.tgz",
"integrity": "sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==",
"license": "MIT",
"dependencies": {
"lodash.throttle": "^4.1.1"
}
},
"node_modules/@uppy/xhr-upload": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
"integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
"license": "MIT",
"dependencies": {
"@uppy/companion-client": "^2.2.2",
"@uppy/utils": "^4.1.2",
"nanoid": "^3.1.25"
},
"peerDependencies": {
"@uppy/core": "^2.3.3"
}
},
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
@ -1603,6 +1672,165 @@
} }
} }
}, },
"node_modules/@wangeditor/basic-modules": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
"integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
"license": "MIT",
"dependencies": {
"is-url": "^1.2.4"
},
"peerDependencies": {
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"lodash.throttle": "^4.1.1",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/code-highlight": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz",
"integrity": "sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==",
"license": "MIT",
"dependencies": {
"prismjs": "^1.23.0"
},
"peerDependencies": {
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/core": {
"version": "1.1.19",
"resolved": "https://registry.npmjs.org/@wangeditor/core/-/core-1.1.19.tgz",
"integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
"license": "MIT",
"dependencies": {
"@types/event-emitter": "^0.3.3",
"event-emitter": "^0.3.5",
"html-void-elements": "^2.0.0",
"i18next": "^20.4.0",
"scroll-into-view-if-needed": "^2.2.28",
"slate-history": "^0.66.0"
},
"peerDependencies": {
"@uppy/core": "^2.1.1",
"@uppy/xhr-upload": "^2.0.3",
"dom7": "^3.0.0",
"is-hotkey": "^0.2.0",
"lodash.camelcase": "^4.3.0",
"lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
"lodash.foreach": "^4.5.0",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"lodash.toarray": "^4.4.0",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/editor": {
"version": "5.1.23",
"resolved": "https://registry.npmjs.org/@wangeditor/editor/-/editor-5.1.23.tgz",
"integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==",
"license": "MIT",
"dependencies": {
"@uppy/core": "^2.1.1",
"@uppy/xhr-upload": "^2.0.3",
"@wangeditor/basic-modules": "^1.1.7",
"@wangeditor/code-highlight": "^1.0.3",
"@wangeditor/core": "^1.1.19",
"@wangeditor/list-module": "^1.0.5",
"@wangeditor/table-module": "^1.1.4",
"@wangeditor/upload-image-module": "^1.0.2",
"@wangeditor/video-module": "^1.1.4",
"dom7": "^3.0.0",
"is-hotkey": "^0.2.0",
"lodash.camelcase": "^4.3.0",
"lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
"lodash.foreach": "^4.5.0",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"lodash.toarray": "^4.4.0",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/editor-for-vue": {
"version": "5.1.12",
"resolved": "https://registry.npmjs.org/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz",
"integrity": "sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==",
"license": "MIT",
"peerDependencies": {
"@wangeditor/editor": ">=5.1.0",
"vue": "^3.0.5"
}
},
"node_modules/@wangeditor/list-module": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@wangeditor/list-module/-/list-module-1.0.5.tgz",
"integrity": "sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==",
"license": "MIT",
"peerDependencies": {
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/table-module": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@wangeditor/table-module/-/table-module-1.1.4.tgz",
"integrity": "sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==",
"license": "MIT",
"peerDependencies": {
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/upload-image-module": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz",
"integrity": "sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==",
"license": "MIT",
"peerDependencies": {
"@uppy/core": "^2.0.3",
"@uppy/xhr-upload": "^2.0.3",
"@wangeditor/basic-modules": "1.x",
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"lodash.foreach": "^4.5.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/@wangeditor/video-module": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@wangeditor/video-module/-/video-module-1.1.4.tgz",
"integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==",
"license": "MIT",
"peerDependencies": {
"@uppy/core": "^2.1.4",
"@uppy/xhr-upload": "^2.0.7",
"@wangeditor/core": "1.x",
"dom7": "^3.0.0",
"nanoid": "^3.2.0",
"slate": "^0.72.0",
"snabbdom": "^3.1.0"
}
},
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
@ -2095,6 +2323,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/compute-scroll-into-view": {
"version": "1.0.20",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
"integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
"license": "MIT"
},
"node_modules/copy-anything": { "node_modules/copy-anything": {
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
@ -2270,6 +2504,19 @@
"integrity": "sha512-D80T+tiqkd/8B0xNlbstWDG4x6aqVfO52+OlSUNIdkTvmNw0uQpJLeos2J/2XvpyidAFuTPmpad+tUxLndwj6g==", "integrity": "sha512-D80T+tiqkd/8B0xNlbstWDG4x6aqVfO52+OlSUNIdkTvmNw0uQpJLeos2J/2XvpyidAFuTPmpad+tUxLndwj6g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/d": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
"license": "ISC",
"dependencies": {
"es5-ext": "^0.10.64",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/data-view-buffer": { "node_modules/data-view-buffer": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@ -2470,6 +2717,15 @@
"url": "https://github.com/fb55/entities?sponsor=1" "url": "https://github.com/fb55/entities?sponsor=1"
} }
}, },
"node_modules/dom7": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/dom7/-/dom7-3.0.0.tgz",
"integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
"license": "MIT",
"dependencies": {
"ssr-window": "^3.0.0-alpha.1"
}
},
"node_modules/domelementtype": { "node_modules/domelementtype": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
@ -2707,6 +2963,46 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/es5-ext": {
"version": "0.10.64",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"es6-iterator": "^2.0.3",
"es6-symbol": "^3.1.3",
"esniff": "^2.0.1",
"next-tick": "^1.1.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
"license": "MIT",
"dependencies": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"node_modules/es6-symbol": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
"license": "ISC",
"dependencies": {
"d": "^1.0.2",
"ext": "^1.7.0"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@ -2759,6 +3055,21 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/esniff": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
"license": "ISC",
"dependencies": {
"d": "^1.0.1",
"es5-ext": "^0.10.62",
"event-emitter": "^0.3.5",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/estree-walker": { "node_modules/estree-walker": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
@ -2775,6 +3086,16 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
"license": "MIT",
"dependencies": {
"d": "1",
"es5-ext": "~0.10.14"
}
},
"node_modules/expand-brackets": { "node_modules/expand-brackets": {
"version": "2.1.4", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
@ -2838,6 +3159,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/ext": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
"license": "ISC",
"dependencies": {
"type": "^2.7.2"
}
},
"node_modules/extend-shallow": { "node_modules/extend-shallow": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
@ -3396,6 +3726,16 @@
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/html-void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
"integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/htmlparser2": { "node_modules/htmlparser2": {
"version": "3.10.1", "version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
@ -3418,6 +3758,15 @@
"dev": true, "dev": true,
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/i18next": {
"version": "20.6.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-20.6.1.tgz",
"integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.0"
}
},
"node_modules/image-size": { "node_modules/image-size": {
"version": "0.5.5", "version": "0.5.5",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
@ -3431,6 +3780,16 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.1.4", "version": "5.1.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
@ -3695,6 +4054,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/is-hotkey": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz",
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==",
"license": "MIT"
},
"node_modules/is-map": { "node_modules/is-map": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
@ -3880,6 +4245,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
"license": "MIT"
},
"node_modules/is-weakmap": { "node_modules/is-weakmap": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
@ -4049,6 +4420,49 @@
"lodash-es": "*" "lodash-es": "*"
} }
}, },
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"license": "MIT"
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT"
},
"node_modules/lodash.foreach": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==",
"license": "MIT"
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
},
"node_modules/lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
"license": "MIT"
},
"node_modules/lodash.toarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
"integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==",
"license": "MIT"
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@ -4162,6 +4576,15 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/mime-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/mime-match/-/mime-match-1.0.2.tgz",
"integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==",
"license": "ISC",
"dependencies": {
"wildcard": "^1.1.0"
}
},
"node_modules/mime-types": { "node_modules/mime-types": {
"version": "2.1.35", "version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
@ -4224,6 +4647,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/namespace-emitter": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/namespace-emitter/-/namespace-emitter-2.0.1.tgz",
"integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==",
"license": "MIT"
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@ -4326,6 +4755,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/next-tick": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
"license": "ISC"
},
"node_modules/node-addon-api": { "node_modules/node-addon-api": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
@ -4748,6 +5183,15 @@
"url": "https://opencollective.com/preact" "url": "https://opencollective.com/preact"
} }
}, },
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@ -5531,6 +5975,15 @@
"node": ">=11.0.0" "node": ">=11.0.0"
} }
}, },
"node_modules/scroll-into-view-if-needed": {
"version": "2.2.31",
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
"integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
"license": "MIT",
"dependencies": {
"compute-scroll-into-view": "^1.0.20"
}
},
"node_modules/set-function-length": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@ -5668,6 +6121,56 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/slate": {
"version": "0.72.8",
"resolved": "https://registry.npmjs.org/slate/-/slate-0.72.8.tgz",
"integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
"license": "MIT",
"dependencies": {
"immer": "^9.0.6",
"is-plain-object": "^5.0.0",
"tiny-warning": "^1.0.3"
}
},
"node_modules/slate-history": {
"version": "0.66.0",
"resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.66.0.tgz",
"integrity": "sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==",
"license": "MIT",
"dependencies": {
"is-plain-object": "^5.0.0"
},
"peerDependencies": {
"slate": ">=0.65.3"
}
},
"node_modules/slate-history/node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/slate/node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/snabbdom": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.6.4.tgz",
"integrity": "sha512-VmxEfuw1/Y/eFj5VtMhYnukExpYiPkNzoo3+N3qwAOUDMl8wXgbli5ebR+j0knE3lZ/0eYskLxNcX64uy10N9w==",
"license": "MIT",
"engines": {
"node": ">=12.17.0"
}
},
"node_modules/snapdragon": { "node_modules/snapdragon": {
"version": "0.8.2", "version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@ -5793,13 +6296,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/sortablejs": {
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz",
"integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==",
"license": "MIT",
"peer": true
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -5891,6 +6387,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ssr-window": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-3.0.0.tgz",
"integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==",
"license": "MIT"
},
"node_modules/stable": { "node_modules/stable": {
"version": "0.1.8", "version": "0.1.8",
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
@ -6322,6 +6824,12 @@
"node": ">=16.0.0" "node": ">=16.0.0"
} }
}, },
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
"license": "MIT"
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@ -6470,6 +6978,12 @@
"dev": true, "dev": true,
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/type": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
"license": "ISC"
},
"node_modules/typed-array-buffer": { "node_modules/typed-array-buffer": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@ -6994,6 +7508,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/wildcard": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/wildcard/-/wildcard-1.1.2.tgz",
"integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==",
"license": "MIT"
},
"node_modules/wujie": { "node_modules/wujie": {
"version": "1.0.29", "version": "1.0.29",
"resolved": "https://registry.npmjs.org/wujie/-/wujie-1.0.29.tgz", "resolved": "https://registry.npmjs.org/wujie/-/wujie-1.0.29.tgz",

@ -11,6 +11,8 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@jaames/iro": "^5.5.2", "@jaames/iro": "^5.5.2",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.13.2", "axios": "^1.13.2",
"dayjs": "^1.11.21", "dayjs": "^1.11.21",
"echarts": "^6.0.0", "echarts": "^6.0.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1777452842620" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8518" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M509.9 709.88a20 20 0 0 1-20-20V148.49a20 20 0 0 1 40 0v541.39a20 20 0 0 1-20 20z" fill="currentColor" p-id="8519"></path><path d="M509.9 709.88a20 20 0 0 1-14-5.73L324.43 535.76a20 20 0 0 1 28-28.53l171.48 168.38a20 20 0 0 1-14 34.27z" fill="currentColor" p-id="8520"></path><path d="M509.9 709.88a20 20 0 0 1-14-34.27l171.43-168.38a20 20 0 1 1 28 28.53L523.91 704.15a19.94 19.94 0 0 1-14.01 5.73z" fill="currentColor" p-id="8521"></path><path d="M215.26 925.93a104.23 104.23 0 0 1-104.12-104.09V695.53a20 20 0 0 1 40 0v126.24a64.21 64.21 0 0 0 64.13 64.16h589.16a64.14 64.14 0 0 0 64.16-64.15V695.53a20 20 0 0 1 40 0v126.24A104.12 104.12 0 0 1 804.5 925.93H215.26z" fill="currentColor" p-id="8522"></path></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

@ -0,0 +1,11 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group 36532">
<path id="Vector" d="M20.0903 23H7.90857C6.85829 23 6 22.2165 6 21.2455V13.7925C6.00045 13.5609 6.05026 13.3318 6.14659 13.1181C6.24292 12.9045 6.38386 12.7106 6.56131 12.5475C6.73877 12.3845 6.94924 12.2556 7.18064 12.1681C7.41204 12.0807 7.65981 12.0365 7.90971 12.038H8.38057V10.3015C8.38057 7.28494 10.8994 5 14 5C17.1006 5 19.6194 7.286 19.6194 10.3005V12.0475H20.0903C21.1417 12.0475 22 12.83 22 13.802V21.254C22 22.2175 21.1429 23 20.0903 23ZM13.1977 17.4655V19.418C13.1977 19.6151 13.2822 19.8042 13.4327 19.9436C13.5832 20.083 13.7872 20.1613 14 20.1613C14.2128 20.1613 14.4168 20.083 14.5673 19.9436C14.7178 19.8042 14.8023 19.6151 14.8023 19.418V17.4655C15.2823 17.2135 15.6057 16.736 15.6057 16.1865C15.6057 15.7919 15.4365 15.4135 15.1354 15.1345C14.8343 14.8556 14.4259 14.6988 14 14.6988C13.5741 14.6988 13.1657 14.8556 12.8646 15.1345C12.5635 15.4135 12.3943 15.7919 12.3943 16.1865C12.3943 16.736 12.7177 17.2135 13.1977 17.4655ZM18.0137 10.2475C18.0113 9.26286 17.5873 8.31931 16.8348 7.62394C16.0823 6.92857 15.0628 6.53819 14 6.53847C12.9372 6.53819 11.9177 6.92857 11.1652 7.62394C10.4127 8.31931 9.98871 9.26286 9.98629 10.2475V12.038H18.0137V10.2475Z" fill="url(#paint0_linear_1382_1839)"/>
</g>
<defs>
<linearGradient id="paint0_linear_1382_1839" x1="14" y1="5" x2="14" y2="23" gradientUnits="userSpaceOnUse">
<stop stop-color="#8C9BB3"/>
<stop offset="1" stop-color="#C2CAD7"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1777451652405" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7494" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M888.046933 1024H135.953067A135.816533 135.816533 0 0 1 0 888.046933V135.953067A135.816533 135.816533 0 0 1 135.953067 0H567.978667c22.4256 0 40.0384 17.646933 40.0384 40.0384 0 22.4256-17.6128 40.072533-40.0384 40.072533H135.953067c-30.378667 0-55.978667 25.6-55.978667 55.978667v751.957333c0 30.378667 25.6 55.978667 55.978667 55.978667h751.957333c30.378667 0 55.978667-25.6 55.978667-55.978667V456.021333c0-22.4256 17.646933-40.0384 40.072533-40.0384 22.391467 0 40.0384 17.6128 40.0384 40.0384v432.0256A135.816533 135.816533 0 0 1 888.046933 1024z" fill="currentColor" p-id="7495"></path><path d="M977.578667 297.642667a39.662933 39.662933 0 0 1-40.0384-40.0384v-123.221334l-319.965867 320.034134c-16.042667 16.042667-41.642667 16.042667-55.978667 0-14.336-16.042667-16.042667-41.642667 0-55.978667l307.2-308.770133h-99.191466a39.662933 39.662933 0 0 1-40.072534-40.072534c0-22.391467 17.646933-40.0384 40.072534-40.0384H972.8c31.982933 0 44.817067 7.953067 44.817067 40.0384v208.008534c0 20.821333-17.6128 40.0384-40.0384 40.0384z" fill="currentColor" p-id="7496"></path></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

@ -0,0 +1,11 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group 36531">
<path id="Vector" d="M18.3772 17.718L18.4351 17.7298L18.854 17.7947C19.8029 17.9659 20.6031 18.2539 21.2463 18.6611C21.795 19.0093 22.2305 19.4437 22.5326 19.9512C22.8725 20.5201 22.9976 21.0737 23 21.5069L22.9953 21.6627V22.0699C22.9943 22.2978 22.9099 22.5174 22.7582 22.6875C22.6065 22.8575 22.3978 22.9661 22.1715 22.9929L22.0535 23H5.94287C5.71521 23.0008 5.49502 22.9188 5.32345 22.7691C5.15187 22.6195 5.04064 22.4124 5.01053 22.1867L5.00227 22.0687L5.00345 21.691C4.97749 21.2024 5.09432 20.5768 5.48024 19.9323C5.78591 19.4212 6.2214 18.9869 6.769 18.641C7.33076 18.2858 8.01173 18.0249 8.80363 17.8597L9.1506 17.7936L9.56366 17.7287L9.62031 17.7168L13.6589 20.578C13.7442 20.6382 13.844 20.6748 13.948 20.6842H14.0519C14.1184 20.6787 14.1834 20.662 14.2443 20.6346L14.2938 20.6087L14.341 20.578L18.3772 17.718ZM12.6121 5C12.7974 5 12.9685 5.00708 13.1467 5.02007C15.7679 5.216 17.3045 5.95252 17.9972 6.70675C18.3265 7.088 18.5613 7.54597 18.7053 8.07357C18.7348 8.18334 18.7573 8.28249 18.7738 8.36984L18.7927 8.49141L18.8375 8.78885C18.9001 9.28459 18.9036 9.79331 18.8717 10.3788L18.8576 10.5829L18.8375 10.7954L18.8021 11.0645L18.8883 11.1802L18.9319 11.2522L18.9709 11.3277L19.0346 11.4894C19.0818 11.6334 19.1019 11.7822 19.1019 11.9734C19.0667 12.5931 18.8785 13.1945 18.5543 13.7238L18.4044 13.9528L18.2982 14.0944L18.1742 14.2431L17.714 14.7601C17.1144 15.4471 16.8454 15.9133 16.7474 16.5932L16.7238 16.8033L16.7037 17.057L16.6967 17.2424L16.7321 17.2624C16.7734 17.2837 16.8229 17.3061 16.8796 17.3297L13.9988 19.3728L11.1168 17.3285C11.1722 17.3049 11.223 17.2837 11.2655 17.2612L11.341 17.2199L11.2973 17.2424L11.2914 17.0582L11.2726 16.8033C11.1994 16.0042 10.9409 15.5167 10.2812 14.7589L10.09 14.5465L9.92008 14.3576C9.3876 13.787 9.04141 13.0678 8.92755 12.2956L8.89922 12.0666L8.89332 11.9663C8.89332 11.6783 8.95823 11.4517 9.08215 11.2534L9.1624 11.1377L9.20253 11.0893L9.18601 10.9902C9.10625 10.3838 9.0857 9.77104 9.12464 9.16065L9.15532 8.83252L9.19191 8.57993C9.23321 7.74662 9.66752 7.08328 10.1691 6.61351L10.3072 6.49075L10.3497 6.45416L10.3402 6.41639L10.3237 6.27357V6.12485L10.3449 5.97141C10.4252 5.56774 10.7179 5.22426 11.2277 5.09207C11.3493 5.0602 11.4519 5.04485 11.5853 5.03541L11.734 5.02597L12.2143 5.0059L12.6132 5H12.6121Z" fill="url(#paint0_linear_1401_5011)"/>
</g>
<defs>
<linearGradient id="paint0_linear_1401_5011" x1="14" y1="5" x2="14" y2="23" gradientUnits="userSpaceOnUse">
<stop stop-color="#8C9BB3"/>
<stop offset="1" stop-color="#C2CAD7"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

@ -790,10 +790,6 @@ img {
background-color: var(--brand-color-hover) !important; background-color: var(--brand-color-hover) !important;
} }
body {
color: #fff;
}
.mx-auto { .mx-auto {
margin-left: auto !important; margin-left: auto !important;
margin-right: auto !important; margin-right: auto !important;

@ -1,28 +1,60 @@
<template> <template>
<el-select v-model="currentLang" @change="changeLang" style="width: 120px"> <el-select
<el-option v-for="(item,index) in langData" :value="item.value" :label="item.label" :key="index"></el-option> class="i18n-wrap"
v-model="currentLang"
@change="changeLang"
popper-class="i18n-popper"
>
<el-option
v-for="(item, index) in langData"
:value="item.value"
:label="item.label"
:key="index"
></el-option>
</el-select> </el-select>
</template> </template>
<script setup> <script setup>
import {ref, reactive} from 'vue' import { ref, reactive } from "vue";
import { useI18n } from 'vue-i18n' import { useI18n } from "vue-i18n";
const { locale } = useI18n() const { locale } = useI18n();
const langData = reactive([ const langData = reactive([
{value:'zhCN',label:'中文(简体)'}, { value: "zhCN", label: "中文(简体)" },
{value:'zhTW',label:'中文(繁體)'}, { value: "zhTW", label: "中文(繁體)" },
{value:'enUS',label:'English'}, { value: "enUS", label: "English" },
]) ]);
const currentLang = ref(localStorage.getItem('lang') || 'zhCN') const currentLang = ref(localStorage.getItem("lang") || "zhCN");
const changeLang = (lang) => { const changeLang = (lang) => {
currentLang.value = lang currentLang.value = lang;
locale.value = lang locale.value = lang;
} };
</script> </script>
<style>
<style scoped> .i18n-popper {
--el-color-primary: #2b7afa;
}
</style>
<style lang="scss" scoped>
.i18n-wrap {
:deep(.el-select__wrapper) {
box-shadow: none;
padding: 5px 8px;
background: rgba(255, 255, 255, 0.6);
border-radius: 9999px;
border: 1px solid #ffffff;
outline: none;
font-weight: normal;
font-size: 16px;
color: #1d2129;
.el-select__selected-item.el-select__placeholder {
position: relative;
transform: translateY(0);
width: auto;
}
}
}
</style> </style>

@ -15,12 +15,12 @@
:defaultOpeneds="defaultOpeneds" :defaultOpeneds="defaultOpeneds"
/> />
<div class="collapse-btn" @click="toggleSidebar"> <!-- <div class="collapse-btn" @click="toggleSidebar">
<el-icon :size="14"> <el-icon :size="14">
<component :is="collapsed ? Expand : Fold" /> <component :is="collapsed ? Expand : Fold" />
</el-icon> </el-icon>
<span v-if="!collapsed"></span> <span v-if="!collapsed"></span>
</div> </div> -->
</aside> </aside>
<div class="admin-main"> <div class="admin-main">
@ -37,6 +37,7 @@
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="password">修改密码</el-dropdown-item> <el-dropdown-item command="password">修改密码</el-dropdown-item>
<el-dropdown-item command="theme">更换主题</el-dropdown-item>
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item> <el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@ -48,6 +49,32 @@
<slot /> <slot />
</main> </main>
</div> </div>
<!-- 更换主题弹窗 -->
<el-dialog
v-model="themeDialogVisible"
title="🎨 更换主题"
width="540px"
:close-on-click-modal="false"
align-center
destroy-on-close
>
<div class="theme-dialog">
<div class="theme-grid">
<div
v-for="t in presetThemes"
:key="t.value"
class="theme-card"
:class="{ active: themeStore.themeValue === t.value }"
@click="handlePresetTheme(t.value)"
>
<div class="theme-preview" :style="{ background: t.preview }"></div>
<div class="theme-name">{{ t.name }}</div>
<div class="theme-desc">{{ t.desc }}</div>
</div>
</div>
</div>
</el-dialog>
</div> </div>
</template> </template>
@ -59,12 +86,44 @@ import { Fold, Expand, ArrowDown } from "@element-plus/icons-vue";
import NavBar from "@/components/navBar/NavBar.vue"; import NavBar from "@/components/navBar/NavBar.vue";
import SvgIcon from "@/components/SvgIcon/index.vue"; import SvgIcon from "@/components/SvgIcon/index.vue";
import { useUserStore } from "@/stores/api/user"; import { useUserStore } from "@/stores/api/user";
import { useThemeStore } from "@/stores/theme";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
const themeStore = useThemeStore();
const collapsed = ref(false); const collapsed = ref(false);
//
const themeDialogVisible = ref(false);
// scss
const presetThemes = [
{
value: "blue",
name: "穹宇蓝",
desc: "深邃星空,科技感十足",
preview: "linear-gradient(135deg, #6c7cf3 0%, #5868e6 100%)",
},
{
value: "purple",
name: "星轨紫",
desc: "神秘优雅,梦幻氛围",
preview: "linear-gradient(135deg, #A06CFC 0%, #6B2EDB 100%)",
},
{
value: "green",
name: "青峦绿",
desc: "清新自然,宁静致远",
preview: "linear-gradient(135deg, #34B693 0%, #228F72 100%)",
},
];
//
const handlePresetTheme = (value) => {
themeStore.setTheme(value);
};
// //
const GROUP_ORDER = ["工单管理", "客户管理", "绩效管理", "系统管理"]; const GROUP_ORDER = ["工单管理", "客户管理", "绩效管理", "系统管理"];
@ -132,6 +191,10 @@ const handleCommand = async (cmd) => {
router.push("/admin/password"); router.push("/admin/password");
return; return;
} }
if (cmd === "theme") {
themeDialogVisible.value = true;
return;
}
if (cmd === "logout") { if (cmd === "logout") {
try { try {
await ElMessageBox.confirm("确定要退出登录吗?", "提示", { await ElMessageBox.confirm("确定要退出登录吗?", "提示", {
@ -293,4 +356,56 @@ const handleCommand = async (cmd) => {
} }
} }
} }
.theme-dialog {
padding: 4px 4px 0;
.theme-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.theme-card {
border: 2px solid #e8e8e8;
border-radius: 12px;
padding: 14px;
cursor: pointer;
transition: all 0.25s ease;
text-align: center;
background: #fff;
&:hover {
border-color: var(--brand-color);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
&.active {
border-color: var(--brand-color);
background: var(--brand-color-light);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
}
}
.theme-preview {
height: 56px;
border-radius: 8px;
margin-bottom: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.theme-name {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.theme-desc {
font-size: 12px;
color: #999;
min-height: 18px;
}
}
</style> </style>

@ -1,11 +1,472 @@
export default { export default {
login:{ // Common
title:'Welcome to log in', common: {
name:'Hospital Customer Management', confirm: 'Confirm',
userName:'Please enter your account number.', cancel: 'Cancel',
passWord:'Please enter the password.', save: 'Save',
code:'Please enter the verification code.', submit: 'Submit',
jizhu:'Remember the account number', reset: 'Reset',
denglu:'Log in' query: 'Query',
search: 'Search',
add: 'Add',
edit: 'Edit',
delete: 'Delete',
view: 'View',
close: 'Close',
back: 'Back',
export: 'Export',
download: 'Download',
upload: 'Upload',
more: 'More',
all: 'All',
yes: 'Yes',
no: 'No',
enabled: 'Enabled',
disabled: 'Disabled',
pleaseSelect: 'Please select',
pleaseInput: 'Please input',
success: 'Success',
failed: 'Failed',
operating: 'Action',
remark: 'Remark',
time: 'Time',
today: 'Today',
yesterday: 'Yesterday',
thisWeek: 'This Week',
thisMonth: 'This Month',
thisQuarter: 'This Quarter',
thisYear: 'This Year',
customDate: 'Custom Date',
startDate: 'Start Date',
endDate: 'End Date',
language: 'Language',
},
// Login page
login: {
title: 'Welcome to Log In',
name: '康策智能CSM系统',
headerTitle: 'Kangce Smart CSM System',
userName: 'Please enter your account',
passWord: 'Please enter your password',
code: 'Please enter the verification code',
jizhu: 'Remember me',
denglu: 'Login',
copyright: 'Copyright © Shanghai Kangce Software Co., Ltd.',
hotline: 'Hotline: 021-60713139',
loginSuccess: 'Login Successful',
loginFailed: 'Login Failed',
unknownUserType: 'Unknown user type, cannot log in',
pleaseEnterUserName: 'Please enter your account',
pleaseEnterPassWord: 'Please enter your password',
languageLabel: 'Language:',
},
// Navigation menu
nav: {
workbench: 'Workbench',
submit: 'My Tickets',
points: 'My Points',
quickSubmit: 'Quick Submit',
workorders: 'Service Tickets',
reports: 'Service Reports',
hospitals: 'Hospitals',
customer: 'Customer Service',
managerPoints: 'Manager Points',
account: 'Accounts',
grades: 'Performance',
},
// Page title
title: {
workbench: 'Workbench',
submit: 'My Tickets',
points: 'My Points',
workorders: 'Service Tickets',
reports: 'Service Reports',
hospitals: 'Hospitals',
customer: 'Customer Service',
managerPoints: 'Manager Points',
account: 'Account Management',
grades: 'Performance Management',
changePassword: 'Change Password',
},
// Button text
btn: {
quickSubmit: 'Quick Submit',
view: 'View',
edit: 'Edit',
cancel: 'Cancel',
close: 'Close',
detail: 'Detail',
process: 'Process',
submit: 'Submit Ticket',
save: 'Save Changes',
cancelModal: 'Cancel',
saveChange: 'Save Changes',
query: 'Query',
reset: 'Reset',
export: 'Export',
download: 'Download',
upload: 'Upload',
generate: 'Generate',
add: 'Add',
delete: 'Delete',
addOrder: '+ New Ticket',
addHospital: '+ Add Hospital',
addAccount: '+ Add Account',
quickAddOrder: '+ Quick Submit Ticket',
back: '← Back',
preview: 'View Report',
downloadPdf: 'Download PDF Report',
confirm: 'Confirm',
downloadReport: 'Download Report',
},
// Form labels
label: {
title: 'Issue Title',
description: 'Description',
attachment: 'Attachment',
dept: 'Department',
name: 'Submitter',
priority: 'Priority',
type: 'Service Type',
oldPassword: 'Old Password',
newPassword: 'New Password',
confirmPassword: 'Confirm Password',
hospital: 'Hospital Name',
hospitalName: 'Hospital Name',
contact: 'Contact',
contactPerson: 'Contact Person',
contactDept: 'Contact Department',
contactPosition: 'Position',
contactPhone: 'Phone',
category: 'Customer Category',
signDate: 'Signing Date',
acceptDate: 'Acceptance Date',
years: 'Years',
maintenanceEndDate: 'Maintenance End Date',
manager: 'Account Manager',
acceptanceReport: 'Acceptance Report',
account: 'Account',
accountType: 'Account Type',
password: 'Password',
gender: 'Gender',
role: 'Role',
status: 'Status',
renewal: 'Renewal',
contractSigned: 'Contract Signed',
paymentPaid: 'Payment',
visited: 'Visit Status',
visitResult: 'Visit Result',
createTime: 'Created At',
colorTag: 'Color Tag',
currentPassword: 'Current Password',
},
// Form placeholder
placeholder: {
title: 'Enter issue title',
description: 'Describe the issue in detail...',
dept: 'Enter department',
name: 'Enter submitter name',
type: 'Select service type',
hospital: 'Enter hospital name',
contactPerson: 'Enter contact person',
contactDept: 'Enter contact department',
contactPosition: 'Enter position',
contactPhone: 'Enter phone',
search: 'Please input',
keyword: 'Search title/ticket no.',
account: 'Enter account',
password: 'Enter password',
newPassword: 'Enter new password (at least 6 chars)',
confirmPassword: 'Confirm new password',
currentPassword: 'Enter current password',
remark: 'Enter remark...',
},
// Messages
msg: {
pleaseInputTitle: 'Please enter the issue title',
pleaseInputDescription: 'Please enter the description',
pleaseSelectPriority: 'Please select priority',
pleaseSelectType: 'Please select service type',
pleaseSelectHospital: 'Please select hospital',
pleaseInputDept: 'Please enter department',
pleaseInputName: 'Please enter submitter name',
passwordTooShort: 'New password must be at least 6 characters!',
passwordMismatch: 'Passwords do not match!',
pleaseFillRequired: 'Please fill in required fields!',
submitSuccess: 'Ticket submitted successfully!',
editSuccess: 'Ticket updated successfully!',
cancelSuccess: 'Ticket cancelled successfully!',
closeSuccess: 'Ticket closed successfully!',
passwordChangeSuccess: 'Password changed successfully!',
languageChangeSuccess: 'Language changed successfully!',
themeChangeSuccess: 'Theme changed successfully!',
hospitalAddSuccess: 'Hospital added successfully!',
hospitalEditSuccess: 'Hospital updated successfully!',
accountAddSuccess: 'Account added successfully!',
accountEditSuccess: 'Account updated successfully!',
passwordResetSuccess: 'Password reset successfully!',
reportGenerateSuccess: 'Report generated successfully!',
confirmCancelOrder: 'Are you sure you want to cancel ticket {orderId}? This cannot be undone.',
confirmCloseOrder: 'Are you sure you want to close ticket {orderId}?',
},
// Statistics
stat: {
total: 'Total Submitted',
completed: 'Completed',
pending: 'Pending',
highPriority: 'High Priority',
totalOrders: 'Total',
pendingOrders: 'Pending',
completedOrders: 'Completed',
highPriorityOrders: 'High Priority',
high: 'High Priority',
medium: 'Medium Priority',
low: 'Low Priority',
mediumPriority: 'Medium Priority',
lowPriority: 'Low Priority',
processing: 'Processing',
},
// Table columns
table: {
id: 'Ticket No.',
priority: 'Priority',
title: 'Title',
type: 'Service Type',
status: 'Status',
time: 'Submit Time',
submitter: 'Submitter',
action: 'Action',
hospital: 'Hospital',
handler: 'Handler',
handleTime: 'Handle Time',
handleDesc: 'Handle Description',
registrar: 'Registrar',
dept: 'Department',
category: 'Category',
contact: 'Contact',
phone: 'Phone',
manager: 'Manager',
signDate: 'Sign Date',
usageYears: 'Usage Years',
maintenanceEnd: 'Maintenance End',
acceptanceReport: 'Acceptance Report',
account: 'Account',
name: 'Name',
gender: 'Gender',
role: 'Role',
createTime: 'Created',
hospitalName: 'Hospital Name',
},
// Status
status: {
pending: 'Pending',
processing: 'Processing',
completed: 'Completed',
closed: 'Closed',
active: 'Active',
inactive: 'Inactive',
normal: 'Normal',
warning: 'Warning',
urgent: 'Urgent',
expired: 'Expired',
renewed: 'Renewed',
notRenewed: 'Not Renewed',
paid: 'Paid',
notPaid: 'Unpaid',
},
// Priority
priority: {
high: '🔴 High',
medium: '🟠 Medium',
low: '🟢 Low',
},
// Type
type: {
issue: 'Issue',
consult: 'Consultation',
feature: 'Feature Request',
other: 'Other',
},
// Chart
chart: {
title: 'Ticket Trend',
submit: 'Submitted',
complete: 'Completed',
bar: 'Bar Chart',
line: 'Line Chart',
},
// Date buttons
dateBtn: {
thisMonth: 'This Month',
thisQuarter: 'This Quarter',
thisYear: 'This Year',
customDate: 'Custom Date',
},
// Theme
theme: {
blue: 'Cosmic Blue',
purple: 'Orbital Purple',
green: 'Mountain Green',
title: 'Change Theme',
},
// Language
languageSwitch: {
title: 'Change Language',
zhCN: 'Chinese (Simplified)',
zhTW: 'Chinese (Traditional)',
enUS: 'English',
},
// Modal
modal: {
detail: 'Ticket Detail',
edit: 'Edit Ticket',
submit: 'Quick Submit Ticket',
changePassword: 'Change Password',
changeTheme: 'Change Theme',
changeLanguage: 'Change Language',
addAccount: 'Add Account',
editAccount: 'Edit Account',
resetPassword: 'Reset Password',
addHospital: 'Add Hospital',
editHospital: 'Edit Hospital',
detailHospital: 'Hospital Detail',
generateReport: 'Generate Service Report',
},
// User menu
userMenu: {
changePassword: 'Change Password',
changeTheme: 'Change Theme',
changeLanguage: 'Change Language',
logout: 'Logout',
},
// Customer category
category: {
A: 'Class A (Active)',
B: 'Class B (Inactive)',
C: 'Class C (To Recover)',
AShort: 'Class A',
BShort: 'Class B',
CShort: 'Class C',
},
// Role
role: {
super: 'Super Admin',
manager: 'Account Manager',
},
// Gender
gender: {
male: 'Male',
female: 'Female',
},
// File upload
upload: {
text: 'Click or drag files here to upload',
hint: 'Files up to 50MB supported',
uploaded: 'Uploaded Files',
remove: 'Remove',
rename: 'Rename',
maxSize50M: '(max 50M)',
},
// Points page
points: {
title: '🎁 My Points',
unit: 'pts',
rewards: 'Received 15 service rewards',
detail: 'Points History',
points: 'Points',
pointsTime: 'Time',
pointsSource: 'Source',
pointsOrder: 'Ticket No.',
pointsChange: 'Points Change',
pointsDesc: 'Description',
},
// Report page
report: {
list: 'Service Reports',
generate: 'Generate Report',
view: 'View',
download: 'Download',
remark: 'Remark',
search: 'Search by hospital name',
typeAll: 'All Types',
quarterly: 'Quarterly Report',
annual: 'Annual Report',
period: 'Report Period',
quarter: 'Quarter',
year: 'Annual',
querySuccess: 'Query complete, found {count} report(s).',
},
// Order detail
orderDetail: {
submitTime: 'Submit Time',
statusChange: 'Status Change Log',
operator: 'Operator',
beforeChange: 'Before',
afterChange: 'After',
noAttachment: 'No attachments',
currentStatus: 'Current Status',
dept: 'Department',
name: 'Submitter',
handler: 'Handler',
},
// Change password
password: {
title: 'Change Password',
oldPassword: 'Old Password',
newPassword: 'New Password',
confirmPassword: 'Confirm Password',
submit: 'Submit',
cancel: 'Cancel',
success: 'Password changed',
pleaseInputOld: 'Please enter old password',
pleaseInputNew: 'Please enter new password',
pleaseInputConfirm: 'Please confirm new password',
passwordTooShort: 'Password must be at least 6 characters',
passwordMismatch: 'Passwords do not match',
oldPasswordError: 'Old password is incorrect',
accountNotFound: 'Account does not exist',
},
// Account management
account: {
hospitalAccount: 'Hospital Accounts',
adminAccount: 'Admin Accounts',
list: 'Account List',
addHospital: '+ Add Hospital Account',
addAdmin: '+ Add Admin Account',
enable: 'Enable',
disable: 'Disable',
enableSuccess: 'Account {id} enabled',
disableSuccess: 'Account {id} disabled',
resetPassword: 'Reset Password',
defaultPassword: 'After reset, password will be: 123456',
confirmReset: 'Are you sure you want to reset the password of account {id}?',
resetSuccess: 'Account {id} password has been reset to default: 123456',
pleaseInputName: 'Please enter name!',
},
// Customer service
service: {
title: 'Customer Service',
monthlyView: '📅 Monthly View',
prevMonth: '← Prev Month',
nextMonth: 'Next Month →',
phoneVisit: '📞 Phone Visit',
onSiteInspection: '🔍 On-site Inspection',
training: '📚 Training Records',
gift: '🎁 Gift Records',
expiring: 'Expiring This Month',
searchHospital: 'Search by hospital name',
noRecords: 'No records',
},
// Validation
validation: {
required: 'Required',
},
// Date range
dateRange: {
today: 'Today',
yesterday: 'Yesterday',
thisWeek: 'This Week',
thisMonth: 'This Month',
thisQuarter: 'This Quarter',
}, },
} }

@ -1,11 +1,472 @@
export default { export default {
login:{ // 公共
title:'欢迎登录', common: {
name:'医院客户关系管理系统', confirm: '确认',
userName:'请输入账号', cancel: '取消',
passWord:'请输入密码', save: '保存',
code:'请输入验证码', submit: '提交',
jizhu:'记住账号', reset: '重置',
denglu:'登录' query: '查询',
search: '搜索',
add: '添加',
edit: '编辑',
delete: '删除',
view: '查看',
close: '关闭',
back: '返回',
export: '导出',
download: '下载',
upload: '上传',
more: '更多',
all: '全部',
yes: '是',
no: '否',
enabled: '启用',
disabled: '禁用',
pleaseSelect: '请选择',
pleaseInput: '请输入',
success: '成功',
failed: '失败',
operating: '操作',
remark: '备注',
time: '时间',
today: '今日',
yesterday: '昨日',
thisWeek: '本周',
thisMonth: '本月',
thisQuarter: '本季度',
thisYear: '本年',
customDate: '自定义日期',
startDate: '开始日期',
endDate: '结束日期',
language: '语言',
},
// 登录页
login: {
title: '欢迎登录',
name: '康策智能CSM系统',
headerTitle: '康策智能CSM系统',
userName: '请输入账号',
passWord: '请输入密码',
code: '请输入验证码',
jizhu: '记住账号',
denglu: '登 录',
copyright: '上海康策软件有限公司版权所有',
hotline: '服务热线021-60713139',
loginSuccess: '登录成功',
loginFailed: '登录失败',
unknownUserType: '未知用户类型,无法登录',
pleaseEnterUserName: '请输入账号',
pleaseEnterPassWord: '请输入密码',
languageLabel: '语言:',
},
// 导航菜单
nav: {
workbench: '我的工作台',
submit: '我的工单',
points: '我的积分',
quickSubmit: '快速提交',
workorders: '服务工单',
reports: '服务报告',
hospitals: '医院信息',
customer: '客户服务',
managerPoints: '客户经理积分',
account: '账号管理',
grades: '绩效管理',
},
// 页面标题
title: {
workbench: '我的工作台',
submit: '我的工单',
points: '我的积分',
workorders: '服务工单',
reports: '服务报告',
hospitals: '医院信息',
customer: '客户服务',
managerPoints: '客户经理积分',
account: '账号管理',
grades: '绩效管理',
changePassword: '修改密码',
},
// 按钮文本
btn: {
quickSubmit: '快速提交',
view: '查看',
edit: '编辑',
cancel: '取消',
close: '关闭',
detail: '详情',
process: '处理',
submit: '提交工单',
save: '保存修改',
cancelModal: '取消',
saveChange: '保存修改',
query: '查询',
reset: '重置',
export: '导出',
download: '下载',
upload: '上传',
generate: '生成',
add: '添加',
delete: '删除',
addOrder: '+ 新建工单',
addHospital: '+ 添加医院',
addAccount: '+ 添加账号',
quickAddOrder: '+ 快速提交工单',
back: '← 返回',
preview: '查看报告',
downloadPdf: '下载PDF报告',
confirm: '确认修改',
downloadReport: '下载报告',
},
// 表单标签
label: {
title: '问题名称',
description: '问题描述',
attachment: '附件',
dept: '提出科室',
name: '提出人员',
priority: '优先级',
type: '服务类型',
oldPassword: '原密码',
newPassword: '新密码',
confirmPassword: '确认密码',
hospital: '医院名称',
hospitalName: '医院名称',
contact: '联系人',
contactPerson: '联系人',
contactDept: '联系科室',
contactPosition: '职务',
contactPhone: '联系电话',
category: '客户分类',
signDate: '签约时间',
acceptDate: '验收时间',
years: '年限',
maintenanceEndDate: '维保截止时间',
manager: '客户经理',
acceptanceReport: '验收报告',
account: '账号',
accountType: '账号类型',
password: '密码',
gender: '性别',
role: '角色',
status: '状态',
renewal: '续保完成',
contractSigned: '合同签署',
paymentPaid: '款项支付',
visited: '回访状态',
visitResult: '回访结果',
createTime: '创建时间',
colorTag: '颜色标签',
currentPassword: '当前密码',
},
// 表单 placeholder
placeholder: {
title: '请输入问题名称',
description: '请详细描述问题,并可以编辑器直接贴图',
dept: '请输入提出科室',
name: '请输入提出人员',
type: '请选择服务类型',
hospital: '请输入医院名称',
contactPerson: '请输入联系人',
contactDept: '请输入联系科室',
contactPosition: '请输入职务',
contactPhone: '请输入联系电话',
search: '请输入',
keyword: '搜索标题/工单号',
account: '请输入账号',
password: '请输入密码',
newPassword: '请输入新密码至少6位',
confirmPassword: '请再次输入新密码',
currentPassword: '请输入当前密码',
remark: '请输入备注信息...',
},
// 提示消息
msg: {
pleaseInputTitle: '请输入问题名称',
pleaseInputDescription: '请输入问题描述',
pleaseSelectPriority: '请选择优先级',
pleaseSelectType: '请选择服务类型',
pleaseSelectHospital: '请选择医院',
pleaseInputDept: '请输入提出科室',
pleaseInputName: '请输入提出人员',
passwordTooShort: '新密码至少需要6位',
passwordMismatch: '两次输入的新密码不一致!',
pleaseFillRequired: '请填写必填项!',
submitSuccess: '工单提交成功!',
editSuccess: '工单修改成功!',
cancelSuccess: '工单已成功取消!',
closeSuccess: '工单已成功关闭!',
passwordChangeSuccess: '密码修改成功!',
languageChangeSuccess: '语言切换成功!',
themeChangeSuccess: '主题切换成功!',
hospitalAddSuccess: '医院添加成功!',
hospitalEditSuccess: '医院信息修改成功!',
accountAddSuccess: '账号添加成功!',
accountEditSuccess: '账号更新成功!',
passwordResetSuccess: '密码重置成功!',
reportGenerateSuccess: '报告生成成功!',
confirmCancelOrder: '确定要取消工单 {orderId} 吗?取消后将无法恢复。',
confirmCloseOrder: '确定要关闭工单 {orderId} 吗?',
},
// 统计
stat: {
total: '累计提交工单',
completed: '已完成工单',
pending: '待处理工单',
highPriority: '高优先级工单',
totalOrders: '总工单',
pendingOrders: '待处理',
completedOrders: '已完成',
highPriorityOrders: '高优先级',
high: '高优先级',
medium: '中优先级',
low: '低优先级',
mediumPriority: '中优先级',
lowPriority: '低优先级',
processing: '处理中',
},
// 表格列
table: {
id: '工单号',
priority: '优先级',
title: '问题名称',
type: '服务类型',
status: '状态',
time: '提交时间',
submitter: '提交人',
action: '操作',
hospital: '医院名称',
handler: '处理人',
handleTime: '处理时间',
handleDesc: '处理说明',
registrar: '登记人',
dept: '提出科室',
category: '客户分类',
contact: '联系人',
phone: '联系电话',
manager: '客户经理',
signDate: '签约日期',
usageYears: '客户使用年限',
maintenanceEnd: '维保截止时间',
acceptanceReport: '验收报告',
account: '账号',
name: '姓名',
gender: '性别',
role: '角色',
createTime: '创建时间',
hospitalName: '医院名称',
},
// 状态
status: {
pending: '待处理',
processing: '处理中',
completed: '已完成',
closed: '已关闭',
active: '活跃',
inactive: '不活跃',
normal: '正常',
warning: '预警',
urgent: '紧急',
expired: '已过期',
renewed: '已续保',
notRenewed: '未续保',
paid: '已付款',
notPaid: '未付款',
},
// 优先级
priority: {
high: '🔴 高',
medium: '🟠 中',
low: '🟢 低',
},
// 类型
type: {
issue: '故障问题',
consult: '使用咨询',
feature: '功能需求',
other: '其他',
},
// 图表
chart: {
title: '工单趋势',
submit: '提交工单',
complete: '完成工单',
bar: '柱状图',
line: '折线图',
},
// 日期按钮
dateBtn: {
thisMonth: '本月',
thisQuarter: '本季度',
thisYear: '本年',
customDate: '自定义日期',
},
// 主题
theme: {
blue: '穹宇蓝',
purple: '星轨紫',
green: '青峦绿',
title: '更换主题',
},
// 语言
languageSwitch: {
title: '更换语言',
zhCN: '中文简体',
zhTW: '中文繁体',
enUS: 'English',
},
// 模态框
modal: {
detail: '工单详情',
edit: '编辑工单',
submit: '快速提交工单',
changePassword: '修改密码',
changeTheme: '更换主题',
changeLanguage: '更换语言',
addAccount: '添加账号',
editAccount: '编辑账号',
resetPassword: '重置密码',
addHospital: '添加医院',
editHospital: '编辑医院',
detailHospital: '医院详情',
generateReport: '生成服务报告',
},
// 用户菜单
userMenu: {
changePassword: '修改密码',
changeTheme: '更换主题',
changeLanguage: '更换语言',
logout: '退出登录',
},
// 客户分类
category: {
A: 'A类活跃',
B: 'B类不活跃',
C: 'C类待挽回',
AShort: 'A类',
BShort: 'B类',
CShort: 'C类',
},
// 角色
role: {
super: '超级管理员',
manager: '客户经理',
},
// 性别
gender: {
male: '男',
female: '女',
},
// 文件上传
upload: {
text: '点击或拖拽文件到此处添加',
hint: '支持上传不超过50M的文件',
uploaded: '已上传文件',
remove: '删除',
rename: '重命名',
maxSize50M: '(不超过50M)',
},
// 积分页面
points: {
title: '🎁 我的积分',
unit: '分',
rewards: '已获得15次服务奖励',
detail: '积分明细',
points: '积分',
pointsTime: '时间',
pointsSource: '来源',
pointsOrder: '工单号',
pointsChange: '积分变动',
pointsDesc: '说明',
},
// 报告页面
report: {
list: '服务报告列表',
generate: '生成服务报告',
view: '查看',
download: '下载',
remark: '备注',
search: '输入医院名称搜索',
typeAll: '全部类型',
quarterly: '季度报告',
annual: '年度报告',
period: '报告周期',
quarter: '季度',
year: '年度',
querySuccess: '查询完成,找到 {count} 条报告记录。',
},
// 工单详情
orderDetail: {
submitTime: '提交时间',
statusChange: '状态变更日志',
operator: '操作人',
beforeChange: '变更前',
afterChange: '变更后',
noAttachment: '暂无附件',
currentStatus: '当前状态',
dept: '提出科室',
name: '提出人员',
handler: '负责人员',
},
// 修改密码
password: {
title: '修改密码',
oldPassword: '原密码',
newPassword: '新密码',
confirmPassword: '确认密码',
submit: '提交',
cancel: '取消',
success: '密码已修改',
pleaseInputOld: '请输入原密码',
pleaseInputNew: '请输入新密码',
pleaseInputConfirm: '请再次输入新密码',
passwordTooShort: '密码长度至少 6 位',
passwordMismatch: '两次密码不一致',
oldPasswordError: '原密码错误',
accountNotFound: '账号不存在',
},
// 账号管理
account: {
hospitalAccount: '医院账号',
adminAccount: '管理员账号',
list: '账号列表',
addHospital: '+ 添加医院账号',
addAdmin: '+ 添加管理员账号',
enable: '启用',
disable: '禁用',
enableSuccess: '账号 {id} 已启用',
disableSuccess: '账号 {id} 已禁用',
resetPassword: '重置密码',
defaultPassword: '重置后密码将变为默认密码123456',
confirmReset: '确定要重置账号 {id} 的密码吗?',
resetSuccess: '账号 {id} 的密码已重置为默认密码: 123456',
pleaseInputName: '请输入姓名!',
},
// 客户服务
service: {
title: '客户服务',
monthlyView: '📅 月视图',
prevMonth: '← 上月',
nextMonth: '下月 →',
phoneVisit: '📞 电话回访',
onSiteInspection: '🔍 现场巡检',
training: '📚 培训记录',
gift: '🎁 纪念品记录',
expiring: '本月到期提醒',
searchHospital: '按医院名称搜索',
noRecords: '暂无记录',
},
// 通用验证
validation: {
required: '此项必填',
},
// 通用日期范围
dateRange: {
today: '今日',
yesterday: '昨天',
thisWeek: '本周',
thisMonth: '本月',
thisQuarter: '本季度',
}, },
} }

@ -1,11 +1,472 @@
export default { export default {
login:{ // 公共
title:'歡迎登錄', common: {
name:'醫院客戶關係管理系統', confirm: '確認',
userName:'請輸入賬號', cancel: '取消',
passWord:'請輸入密碼', save: '保存',
code:'請輸入驗證碼', submit: '提交',
jizhu:'記住賬號', reset: '重置',
denglu:'登錄' query: '查詢',
search: '搜索',
add: '添加',
edit: '編輯',
delete: '刪除',
view: '查看',
close: '關閉',
back: '返回',
export: '導出',
download: '下載',
upload: '上傳',
more: '更多',
all: '全部',
yes: '是',
no: '否',
enabled: '啟用',
disabled: '禁用',
pleaseSelect: '請選擇',
pleaseInput: '請輸入',
success: '成功',
failed: '失敗',
operating: '操作',
remark: '備註',
time: '時間',
today: '今日',
yesterday: '昨日',
thisWeek: '本週',
thisMonth: '本月',
thisQuarter: '本季度',
thisYear: '本年',
customDate: '自定義日期',
startDate: '開始日期',
endDate: '結束日期',
language: '語言',
},
// 登录页
login: {
title: '歡迎登錄',
name: '康策智能CSM系統',
headerTitle: '康策智能CSM系統',
userName: '請輸入賬號',
passWord: '請輸入密碼',
code: '請輸入驗證碼',
jizhu: '記住賬號',
denglu: '登 錄',
copyright: '上海康策軟件有限公司版權所有',
hotline: '服務熱線021-60713139',
loginSuccess: '登錄成功',
loginFailed: '登錄失敗',
unknownUserType: '未知用戶類型,無法登錄',
pleaseEnterUserName: '請輸入賬號',
pleaseEnterPassWord: '請輸入密碼',
languageLabel: '語言:',
},
// 导航菜单
nav: {
workbench: '我的工作台',
submit: '我的工單',
points: '我的積分',
quickSubmit: '快速提交',
workorders: '服務工單',
reports: '服務報告',
hospitals: '醫院資訊',
customer: '客戶服務',
managerPoints: '客戶經理積分',
account: '帳號管理',
grades: '績效管理',
},
// 页面标题
title: {
workbench: '我的工作台',
submit: '我的工單',
points: '我的積分',
workorders: '服務工單',
reports: '服務報告',
hospitals: '醫院資訊',
customer: '客戶服務',
managerPoints: '客戶經理積分',
account: '帳號管理',
grades: '績效管理',
changePassword: '修改密碼',
},
// 按钮文本
btn: {
quickSubmit: '快速提交',
view: '查看',
edit: '編輯',
cancel: '取消',
close: '關閉',
detail: '詳情',
process: '處理',
submit: '提交工單',
save: '保存修改',
cancelModal: '取消',
saveChange: '保存修改',
query: '查詢',
reset: '重置',
export: '導出',
download: '下載',
upload: '上傳',
generate: '生成',
add: '添加',
delete: '刪除',
addOrder: '+ 新建工單',
addHospital: '+ 添加醫院',
addAccount: '+ 添加帳號',
quickAddOrder: '+ 快速提交工單',
back: '← 返回',
preview: '查看報告',
downloadPdf: '下載PDF報告',
confirm: '確認修改',
downloadReport: '下載報告',
},
// 表单标签
label: {
title: '問題名稱',
description: '問題描述',
attachment: '附件',
dept: '提出科室',
name: '提出人員',
priority: '優先級',
type: '服務類型',
oldPassword: '原密碼',
newPassword: '新密碼',
confirmPassword: '確認密碼',
hospital: '醫院名稱',
hospitalName: '醫院名稱',
contact: '聯繫人',
contactPerson: '聯繫人',
contactDept: '聯繫科室',
contactPosition: '職務',
contactPhone: '聯繫電話',
category: '客戶分類',
signDate: '簽約時間',
acceptDate: '驗收時間',
years: '年限',
maintenanceEndDate: '維保截止時間',
manager: '客戶經理',
acceptanceReport: '驗收報告',
account: '帳號',
accountType: '帳號類型',
password: '密碼',
gender: '性別',
role: '角色',
status: '狀態',
renewal: '續保完成',
contractSigned: '合同簽署',
paymentPaid: '款項支付',
visited: '回訪狀態',
visitResult: '回訪結果',
createTime: '創建時間',
colorTag: '顏色標籤',
currentPassword: '當前密碼',
},
// 表单 placeholder
placeholder: {
title: '請輸入問題名稱',
description: '請詳細描述問題,並可以編輯器直接貼圖',
dept: '請輸入提出科室',
name: '請輸入提出人員',
type: '請選擇服務類型',
hospital: '請輸入醫院名稱',
contactPerson: '請輸入聯繫人',
contactDept: '請輸入聯繫科室',
contactPosition: '請輸入職務',
contactPhone: '請輸入聯繫電話',
search: '請輸入',
keyword: '搜索標題/工單號',
account: '請輸入帳號',
password: '請輸入密碼',
newPassword: '請輸入新密碼至少6位',
confirmPassword: '請再次輸入新密碼',
currentPassword: '請輸入當前密碼',
remark: '請輸入備註資訊...',
},
// 提示消息
msg: {
pleaseInputTitle: '請輸入問題名稱',
pleaseInputDescription: '請輸入問題描述',
pleaseSelectPriority: '請選擇優先級',
pleaseSelectType: '請選擇服務類型',
pleaseSelectHospital: '請選擇醫院',
pleaseInputDept: '請輸入提出科室',
pleaseInputName: '請輸入提出人員',
passwordTooShort: '新密碼至少需要6位',
passwordMismatch: '兩次輸入的新密碼不一致!',
pleaseFillRequired: '請填寫必填項!',
submitSuccess: '工單提交成功!',
editSuccess: '工單修改成功!',
cancelSuccess: '工單已成功取消!',
closeSuccess: '工單已成功關閉!',
passwordChangeSuccess: '密碼修改成功!',
languageChangeSuccess: '語言切換成功!',
themeChangeSuccess: '主題切換成功!',
hospitalAddSuccess: '醫院添加成功!',
hospitalEditSuccess: '醫院資訊修改成功!',
accountAddSuccess: '帳號添加成功!',
accountEditSuccess: '帳號更新成功!',
passwordResetSuccess: '密碼重設成功!',
reportGenerateSuccess: '報告生成成功!',
confirmCancelOrder: '確定要取消工單 {orderId} 嗎?取消後將無法恢復。',
confirmCloseOrder: '確定要關閉工單 {orderId} 嗎?',
},
// 统计
stat: {
total: '累計提交工單',
completed: '已完成工單',
pending: '待處理工單',
highPriority: '高優先級工單',
totalOrders: '總工單',
pendingOrders: '待處理',
completedOrders: '已完成',
highPriorityOrders: '高優先級',
high: '高優先級',
medium: '中優先級',
low: '低優先級',
mediumPriority: '中優先級',
lowPriority: '低優先級',
processing: '處理中',
},
// 表格列
table: {
id: '工單號',
priority: '優先級',
title: '問題名稱',
type: '服務類型',
status: '狀態',
time: '提交時間',
submitter: '提交人',
action: '操作',
hospital: '醫院名稱',
handler: '處理人',
handleTime: '處理時間',
handleDesc: '處理說明',
registrar: '登記人',
dept: '提出科室',
category: '客戶分類',
contact: '聯繫人',
phone: '聯繫電話',
manager: '客戶經理',
signDate: '簽約日期',
usageYears: '客戶使用年限',
maintenanceEnd: '維保截止時間',
acceptanceReport: '驗收報告',
account: '帳號',
name: '姓名',
gender: '性別',
role: '角色',
createTime: '創建時間',
hospitalName: '醫院名稱',
},
// 状态
status: {
pending: '待處理',
processing: '處理中',
completed: '已完成',
closed: '已關閉',
active: '活躍',
inactive: '不活躍',
normal: '正常',
warning: '預警',
urgent: '緊急',
expired: '已過期',
renewed: '已續保',
notRenewed: '未續保',
paid: '已付款',
notPaid: '未付款',
},
// 优先级
priority: {
high: '🔴 高',
medium: '🟠 中',
low: '🟢 低',
},
// 类型
type: {
issue: '故障問題',
consult: '使用諮詢',
feature: '功能需求',
other: '其他',
},
// 图表
chart: {
title: '工單趨勢',
submit: '提交工單',
complete: '完成工單',
bar: '柱狀圖',
line: '折線圖',
},
// 日期按钮
dateBtn: {
thisMonth: '本月',
thisQuarter: '本季度',
thisYear: '本年',
customDate: '自定義日期',
},
// 主题
theme: {
blue: '穹宇藍',
purple: '星軌紫',
green: '青巒綠',
title: '更換主題',
},
// 语言
languageSwitch: {
title: '更換語言',
zhCN: '中文簡體',
zhTW: '中文繁體',
enUS: 'English',
},
// 模态框
modal: {
detail: '工單詳情',
edit: '編輯工單',
submit: '快速提交工單',
changePassword: '修改密碼',
changeTheme: '更換主題',
changeLanguage: '更換語言',
addAccount: '添加帳號',
editAccount: '編輯帳號',
resetPassword: '重設密碼',
addHospital: '添加醫院',
editHospital: '編輯醫院',
detailHospital: '醫院詳情',
generateReport: '生成服務報告',
},
// 用户菜单
userMenu: {
changePassword: '修改密碼',
changeTheme: '更換主題',
changeLanguage: '更換語言',
logout: '退出登錄',
},
// 客户分类
category: {
A: 'A類活躍',
B: 'B類不活躍',
C: 'C類待挽回',
AShort: 'A類',
BShort: 'B類',
CShort: 'C類',
},
// 角色
role: {
super: '超級管理員',
manager: '客戶經理',
},
// 性别
gender: {
male: '男',
female: '女',
},
// 文件上传
upload: {
text: '點擊或拖拽文件到此處添加',
hint: '支持上傳不超過50M的文件',
uploaded: '已上傳文件',
remove: '刪除',
rename: '重新命名',
maxSize50M: '(不超過50M)',
},
// 积分页面
points: {
title: '🎁 我的積分',
unit: '分',
rewards: '已獲得15次服務獎勵',
detail: '積分明細',
points: '積分',
pointsTime: '時間',
pointsSource: '來源',
pointsOrder: '工單號',
pointsChange: '積分變動',
pointsDesc: '說明',
},
// 报告页面
report: {
list: '服務報告列表',
generate: '生成服務報告',
view: '查看',
download: '下載',
remark: '備註',
search: '輸入醫院名稱搜索',
typeAll: '全部類型',
quarterly: '季度報告',
annual: '年度報告',
period: '報告週期',
quarter: '季度',
year: '年度',
querySuccess: '查詢完成,找到 {count} 條報告記錄。',
},
// 工单详情
orderDetail: {
submitTime: '提交時間',
statusChange: '狀態變更日誌',
operator: '操作人',
beforeChange: '變更前',
afterChange: '變更後',
noAttachment: '暫無附件',
currentStatus: '當前狀態',
dept: '提出科室',
name: '提出人員',
handler: '負責人員',
},
// 修改密码
password: {
title: '修改密碼',
oldPassword: '原密碼',
newPassword: '新密碼',
confirmPassword: '確認密碼',
submit: '提交',
cancel: '取消',
success: '密碼已修改',
pleaseInputOld: '請輸入原密碼',
pleaseInputNew: '請輸入新密碼',
pleaseInputConfirm: '請再次輸入新密碼',
passwordTooShort: '密碼長度至少 6 位',
passwordMismatch: '兩次密碼不一致',
oldPasswordError: '原密碼錯誤',
accountNotFound: '帳號不存在',
},
// 账号管理
account: {
hospitalAccount: '醫院帳號',
adminAccount: '管理員帳號',
list: '帳號列表',
addHospital: '+ 添加醫院帳號',
addAdmin: '+ 添加管理員帳號',
enable: '啟用',
disable: '禁用',
enableSuccess: '帳號 {id} 已啟用',
disableSuccess: '帳號 {id} 已禁用',
resetPassword: '重設密碼',
defaultPassword: '重設後密碼將變為默認密碼123456',
confirmReset: '確定要重設帳號 {id} 的密碼嗎?',
resetSuccess: '帳號 {id} 的密碼已重設為默認密碼: 123456',
pleaseInputName: '請輸入姓名!',
},
// 客户服务
service: {
title: '客戶服務',
monthlyView: '📅 月視圖',
prevMonth: '← 上月',
nextMonth: '下月 →',
phoneVisit: '📞 電話回訪',
onSiteInspection: '🔍 現場巡檢',
training: '📚 培訓記錄',
gift: '🎁 紀念品記錄',
expiring: '本月到期提醒',
searchHospital: '按醫院名稱搜索',
noRecords: '暫無記錄',
},
// 通用验证
validation: {
required: '此項必填',
},
// 通用日期范围
dateRange: {
today: '今日',
yesterday: '昨天',
thisWeek: '本週',
thisMonth: '本月',
thisQuarter: '本季度',
}, },
} }

@ -9,9 +9,9 @@ export const getAdminOrders = (params) => {
export const getAdminOrderDetail = (id) => { export const getAdminOrderDetail = (id) => {
return kcRequest.request(`/admin/orders/${id}`, "get"); return kcRequest.request(`/admin/orders/${id}`, "get");
}; };
// 提交工单 // 提交工单multipart/form-data承载附件 files
export const createAdminOrder = (data) => { export const createAdminOrder = (formData) => {
return kcRequest.request(`/admin/orders`, "post", data); return kcRequest.uploadFile(`/admin/orders`, formData);
}; };
// 处理工单 // 处理工单
export const processAdminOrder = (id, data) => { export const processAdminOrder = (id, data) => {
@ -25,11 +25,18 @@ export const getAdminHospitals = (params) => {
export const getAdminHospitalDetail = (id) => { export const getAdminHospitalDetail = (id) => {
return kcRequest.request(`/admin/hospitals/${id}`, "get"); return kcRequest.request(`/admin/hospitals/${id}`, "get");
}; };
export const createAdminHospital = (data) => { // 新增/编辑医院:使用 multipart/form-data承载表单字段与验收报告 file[]
return kcRequest.request(`/admin/hospitals`, "post", data); export const createAdminHospital = (formData) => {
return kcRequest.uploadFile(`/admin/hospitals`, formData);
}; };
export const updateAdminHospital = (id, data) => { export const updateAdminHospital = (id, formData) => {
return kcRequest.request(`/admin/hospitals/${id}`, "put", data); return kcRequest.uploadFile(`/admin/hospitals/${id}`, formData, true, "PUT");
};
// ============== 通用文件 ==============
// 下载/预览上传的文件医院验收报告等GET返回 Blob
export const getUploadFile = (fileName) => {
return kcRequest.downloadFile(`/upload/${fileName}`);
}; };
// ============== 账号管理 ============== // ============== 账号管理 ==============
@ -48,3 +55,8 @@ export const toggleAdminAccountStatus = (id) => {
export const resetAdminAccountPassword = (id) => { export const resetAdminAccountPassword = (id) => {
return kcRequest.request(`/admin/accounts/${id}/reset-password`, "post"); return kcRequest.request(`/admin/accounts/${id}/reset-password`, "post");
}; };
// 当前登录用户修改自己的密码401 = 旧密码错误,使用 silent 避免误跳登录页)
export const changeMyPassword = (data) => {
return kcRequest.request(`/admin/accounts/change-password`, "put", data, true, "json", true);
};

@ -12,7 +12,9 @@ class Request {
this.instance = axios.create({ this.instance = axios.create({
timeout: timeOut, timeout: timeOut,
baseURL: baseURL:
process.env.NODE_ENV == "development" ? "/api/api" : window.location.origin + "/api", process.env.NODE_ENV == "development"
? "/api/api"
: window.location.origin + "/api",
withCredentials: IS_COOKIES, withCredentials: IS_COOKIES,
}); });
@ -41,28 +43,43 @@ class Request {
(res) => res, (res) => res,
(error) => { (error) => {
const status = error?.response?.status; const status = error?.response?.status;
if (status === 401) { // silent 模式下如修改密码场景401 仅代表旧密码错误),不弹通用提示、不跳登录页
const silent = error?.config?.__silent;
if (
status === 401 &&
!error?.config?.url.endsWith("/login") &&
!error?.config?.url.endsWith("/change-password")
) {
if (!silent) {
ElMessage.error("登录已过期,请重新登录"); ElMessage.error("登录已过期,请重新登录");
useUserStore().clear(); useUserStore().clear();
// 跳到登录页
if (window.location.hash !== "#/login") { if (window.location.hash !== "#/login") {
window.location.hash = "/login"; window.location.hash = "/login";
} }
}
} else if (status === 403) { } else if (status === 403) {
ElMessage.error("无操作权限"); if (!silent) ElMessage.error("无操作权限");
} else if (status >= 500) { } else if (status >= 500) {
ElMessage.error("服务器内部错误"); if (!silent) ElMessage.error("服务器内部错误");
} }
return Promise.reject(error); return Promise.reject(error);
}, },
); );
} }
async request(url, method, params, requireToken = true, headerType = "json") { async request(
url,
method,
params,
requireToken = true,
headerType = "json",
silent = false,
) {
const config = { const config = {
url, url,
method, method,
requireToken, requireToken,
__silent: silent,
}; };
if (headerType === "json") config.jsonHeader = true; if (headerType === "json") config.jsonHeader = true;
if (headerType === "form") config.formHeader = true; if (headerType === "form") config.formHeader = true;
@ -83,8 +100,15 @@ class Request {
const body = res.data; const body = res.data;
if (body && typeof body === "object" && "code" in body) { if (body && typeof body === "object" && "code" in body) {
if (body.code === 200) { if (body.code === 200) {
resolve(body.data); resolve(body.data || {
} else if (body.code === 401) { code: 200
});
} else if (
body.code === 401 &&
// 登录相关接口不弹提示(如登录页本身、修改密码页),其他接口统一处理
!config.url.endsWith("/login") &&
!config.url.endsWith("/change-password")
) {
ElMessage.error(body.message || "未登录"); ElMessage.error(body.message || "未登录");
useUserStore().clear(); useUserStore().clear();
if (window.location.hash !== "#/login") { if (window.location.hash !== "#/login") {
@ -106,9 +130,25 @@ class Request {
}); });
} }
async uploadFile(url, formData, requireToken = true) { // 下载二进制文件,返回 Blob
async downloadFile(url, requireToken = true) {
const config = { const config = {
method: "POST", method: "GET",
url,
requireToken,
responseType: "blob",
};
return new Promise((resolve, reject) => {
this.instance(config)
.then((res) => resolve(res.data))
.catch((err) => reject(err));
});
}
// multipart/form-data 上传method 默认 POST编辑接口需要 PUT 时可传入
async uploadFile(url, formData, requireToken = true, method = "POST") {
const config = {
method,
url, url,
data: formData, data: formData,
requireToken, requireToken,

@ -1,8 +1,8 @@
<template> <template>
<div class="account-edit" v-loading="loading"> <div class="account-edit" v-loading="loading">
<el-page-header :icon="ArrowLeft" content="返回" @back="goBack" class="page-header"> <el-page-header :icon="ArrowLeft" :content="$t('common.back')" @back="goBack" class="page-header">
<template #content> <template #content>
<span class="page-title">{{ isEdit ? "编辑账号" : "新增账号" }}</span> <span class="page-title">{{ isEdit ? $t('modal.editAccount') : $t('modal.addAccount') }}</span>
</template> </template>
</el-page-header> </el-page-header>
@ -14,38 +14,54 @@
label-width="120px" label-width="120px"
style="max-width: 600px" style="max-width: 600px"
> >
<el-form-item v-if="!isEdit" label="用户名" prop="username"> <el-form-item v-if="!isEdit" :label="$t('table.account')" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" /> <el-input v-model="form.username" :placeholder="$t('placeholder.account')" />
</el-form-item> </el-form-item>
<el-form-item v-if="!isEdit" label="密码" prop="password"> <el-form-item v-if="!isEdit" :label="$t('label.password')" prop="password">
<el-input v-model="form.password" type="password" show-password placeholder="请输入密码" /> <el-input v-model="form.password" type="password" show-password :placeholder="$t('placeholder.password')" />
</el-form-item> </el-form-item>
<el-form-item label="姓名" prop="name"> <el-form-item :label="$t('table.name')" prop="name">
<el-input v-model="form.name" /> <el-input v-model="form.name" />
</el-form-item> </el-form-item>
<el-form-item label="性别"> <el-form-item :label="$t('table.gender')">
<el-radio-group v-model="form.gender"> <el-radio-group v-model="form.gender">
<el-radio-button label="男"></el-radio-button> <el-radio-button label="男"></el-radio-button>
<el-radio-button label="女"></el-radio-button> <el-radio-button label="女"></el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="用户类型" prop="userType"> <el-form-item :label="$t('label.accountType')" prop="userType">
<el-radio-group v-model="form.userType"> <el-radio-group v-model="form.userType">
<el-radio-button label="Admin">管理员</el-radio-button> <el-radio-button label="Admin">管理员</el-radio-button>
<el-radio-button label="Hospital">医院端</el-radio-button> <el-radio-button label="Hospital">医院端</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item v-if="form.userType === 'Hospital'" label="所属医院"> <el-form-item v-if="form.userType === 'Hospital'" :label="$t('table.hospital')" prop="hospitalId">
<el-input v-model.number="form.hospitalId" placeholder="请输入医院 ID" /> <el-select
v-model="form.hospitalId"
:placeholder="$t('msg.pleaseSelectHospital')"
filterable
clearable
style="width: 100%"
>
<el-option
v-for="item in hospitalOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="角色"> <el-form-item v-if="form.userType === 'Admin'" :label="$t('table.role')" prop="role">
<el-input v-model="form.role" placeholder="如:超管 / 客服 / 医生" /> <el-select v-model="form.role" :placeholder="$t('table.role')" style="width: 100%">
<el-option label="超级管理员" value="超级管理员" />
<el-option label="客户经理" value="客户经理" />
</el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :loading="submitting" @click="submit"> <el-button type="primary" :loading="submitting" @click="submit">
{{ isEdit ? "保存" : "新增" }} {{ isEdit ? $t('common.save') : $t('common.add') }}
</el-button> </el-button>
<el-button @click="goBack"></el-button> <el-button @click="goBack">{{ $t('common.cancel') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
@ -57,12 +73,27 @@ import { computed, onMounted, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { ArrowLeft } from "@element-plus/icons-vue"; import { ArrowLeft } from "@element-plus/icons-vue";
import { useI18n } from "vue-i18n";
import { import {
getAdminAccounts, getAdminAccounts,
createAdminAccount, createAdminAccount,
updateAdminAccount, updateAdminAccount,
getAdminHospitals,
} from "@/service/modular/admin"; } from "@/service/modular/admin";
const { t: $t } = useI18n();
const hospitalOptions = ref([]);
const loadHospitals = async () => {
try {
const res = await getAdminHospitals();
hospitalOptions.value = res?.list || [];
} catch (e) {
// request
}
};
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const loading = ref(false); const loading = ref(false);
@ -81,17 +112,16 @@ const form = reactive({
}); });
const rules = { const rules = {
username: [{ required: true, message: "请输入用户名", trigger: "blur" }], username: [{ required: true, message: () => $t('placeholder.account'), trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }], password: [{ required: true, message: () => $t('placeholder.password'), trigger: "blur" }],
name: [{ required: true, message: "请输入姓名", trigger: "blur" }], name: [{ required: true, message: () => $t('account.pleaseInputName'), trigger: "blur" }],
userType: [{ required: true, message: "请选择用户类型", trigger: "change" }], userType: [{ required: true, message: () => $t('label.accountType'), trigger: "change" }],
}; };
const loadDetail = async () => { const loadDetail = async () => {
if (!isEdit.value) return; if (!isEdit.value) return;
loading.value = true; loading.value = true;
try { try {
// API +
const res = await getAdminAccounts({ page: 1, pageSize: 1000 }); const res = await getAdminAccounts({ page: 1, pageSize: 1000 });
const found = (res?.list || []).find((it) => String(it.id) === String(route.params.id)); const found = (res?.list || []).find((it) => String(it.id) === String(route.params.id));
if (found) { if (found) {
@ -120,7 +150,7 @@ const submit = async () => {
hospitalId: form.hospitalId, hospitalId: form.hospitalId,
role: form.role, role: form.role,
}); });
ElMessage.success("保存成功"); ElMessage.success($t('msg.accountEditSuccess'));
} else { } else {
await createAdminAccount({ await createAdminAccount({
username: form.username, username: form.username,
@ -131,7 +161,7 @@ const submit = async () => {
hospitalId: form.hospitalId, hospitalId: form.hospitalId,
role: form.role, role: form.role,
}); });
ElMessage.success("新增成功"); ElMessage.success($t('msg.accountAddSuccess'));
} }
router.replace("/admin/accounts"); router.replace("/admin/accounts");
} finally { } finally {
@ -142,7 +172,10 @@ const submit = async () => {
const goBack = () => router.back(); const goBack = () => router.back();
onMounted(loadDetail); onMounted(() => {
loadHospitals();
loadDetail();
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

@ -2,51 +2,51 @@
<div class="account-list"> <div class="account-list">
<el-card shadow="never"> <el-card shadow="never">
<el-form :inline="true" :model="filters" class="filter-form"> <el-form :inline="true" :model="filters" class="filter-form">
<el-form-item label="类型"> <el-form-item :label="$t('label.accountType')">
<el-select v-model="filters.type" placeholder="全部" clearable style="width:140px"> <el-select v-model="filters.type" :placeholder="$t('common.all')" clearable style="width:140px">
<el-option label="管理员" value="Admin" /> <el-option label="管理员" value="Admin" />
<el-option label="医院端" value="Hospital" /> <el-option label="医院端" value="Hospital" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="关键词"> <el-form-item :label="$t('placeholder.keyword')">
<el-input v-model="filters.keyword" placeholder="用户名/姓名" clearable style="width:200px" /> <el-input v-model="filters.keyword" :placeholder="$t('placeholder.keyword')" clearable style="width:200px" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="onSearch"></el-button> <el-button type="primary" @click="onSearch">{{ $t('btn.query') }}</el-button>
<el-button @click="onReset"></el-button> <el-button @click="onReset">{{ $t('btn.reset') }}</el-button>
<el-button type="success" @click="goAdd"></el-button> <el-button type="success" @click="goAdd">{{ $t('common.add') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-table :data="list" v-loading="loading" border stripe> <el-table :data="list" v-loading="loading" border stripe>
<el-table-column prop="id" label="ID" width="80" /> <!-- <el-table-column prop="id" label="ID" width="80" /> -->
<el-table-column prop="userName" label="用户名" width="140" /> <el-table-column :label="$t('table.account')" prop="username" width="140" />
<el-table-column prop="name" label="姓名" width="120" /> <el-table-column :label="$t('table.name')" prop="name" width="120" />
<el-table-column label="类型" width="100"> <el-table-column :label="$t('label.accountType')" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.userType === 'Admin' ? 'danger' : 'primary'" size="small"> <el-tag :type="row.userType === 'Admin' ? 'danger' : 'primary'" size="small">
{{ row.userType === "Admin" ? "管理员" : "医院端" }} {{ row.userType === "Admin" ? "管理员" : "医院端" }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="role" label="角色" width="120" /> <el-table-column :label="$t('table.role')" prop="role" width="120" />
<el-table-column prop="hospitalName" label="所属医院" min-width="160" show-overflow-tooltip /> <el-table-column :label="$t('table.hospital')" prop="hospitalName" min-width="160" show-overflow-tooltip />
<el-table-column label="状态" width="100"> <el-table-column :label="$t('label.status')" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-switch <el-switch
:model-value="row.enabled" :model-value="row.status === '启用' ? true : false"
@change="toggleStatus(row)" @change="toggleStatus(row)"
:loading="row.__loading" :loading="row.__loading"
active-text="启用" :active-text="$t('account.enable')"
inactive-text="禁用" :inactive-text="$t('account.disable')"
inline-prompt inline-prompt
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="200" fixed="right"> <el-table-column :label="$t('table.action')" width="200" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" @click="goEdit(row.id)"></el-button> <el-button link type="primary" @click="goEdit(row.id)">{{ $t('btn.edit') }}</el-button>
<el-button link type="warning" @click="resetPwd(row)"></el-button> <el-button link type="warning" @click="resetPwd(row)">{{ $t('account.resetPassword') }}</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -70,12 +70,14 @@
import { onMounted, reactive, ref } from "vue"; import { onMounted, reactive, ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { useI18n } from "vue-i18n";
import { import {
getAdminAccounts, getAdminAccounts,
toggleAdminAccountStatus, toggleAdminAccountStatus,
resetAdminAccountPassword, resetAdminAccountPassword,
} from "@/service/modular/admin"; } from "@/service/modular/admin";
const { t: $t } = useI18n();
const router = useRouter(); const router = useRouter();
const list = ref([]); const list = ref([]);
const total = ref(0); const total = ref(0);
@ -116,7 +118,7 @@ const toggleStatus = async (row) => {
row.__loading = true; row.__loading = true;
try { try {
await toggleAdminAccountStatus(row.id); await toggleAdminAccountStatus(row.id);
ElMessage.success("已更新状态"); ElMessage.success($t('msg.passwordChangeSuccess'));
loadList(); loadList();
} finally { } finally {
row.__loading = false; row.__loading = false;
@ -126,12 +128,12 @@ const toggleStatus = async (row) => {
const resetPwd = async (row) => { const resetPwd = async (row) => {
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(
`确认重置账号「${row.userName}」的密码为默认密码 123456 吗?`, `${row.username} - ${$t('account.defaultPassword')}`,
"提示", $t('common.confirm'),
{ type: "warning" }, { type: "warning" },
); );
await resetAdminAccountPassword(row.id); await resetAdminAccountPassword(row.id);
ElMessage.success("密码已重置"); ElMessage.success($t('msg.passwordResetSuccess'));
} catch (_) { } catch (_) {
/* canceled */ /* canceled */
} }

@ -1,10 +1,10 @@
<template> <template>
<div class="grades-page"> <div class="grades-page">
<el-alert <el-alert
title="客户分级和积分" :title="$t('title.grades')"
type="info" type="info"
:closable="false" :closable="false"
description="此模块用于管理医院的客户等级A/B/C及其积分规则。当前 API 文档暂未提供管理端积分接口,以下数据为示例 mock待后端接口补充后再对接。" :description="$t('label.hospitalName') + ' / ' + $t('label.category')"
show-icon show-icon
/> />
@ -12,20 +12,20 @@
<el-col v-for="g in gradeList" :key="g.code" :span="8"> <el-col v-for="g in gradeList" :key="g.code" :span="8">
<el-card shadow="never" class="grade-card"> <el-card shadow="never" class="grade-card">
<div class="grade-head" :style="{ background: g.bg }"> <div class="grade-head" :style="{ background: g.bg }">
<span class="code">{{ g.code }} </span> <span class="code">{{ g.code }} {{ $t('category.AShort').replace('A', '') }}</span>
<span class="hint">{{ g.hint }}</span> <span class="hint">{{ g.hint }}</span>
</div> </div>
<div class="grade-body"> <div class="grade-body">
<div class="row-item"> <div class="row-item">
<span class="lbl">折扣</span> <span class="lbl">{{ $t('common.yes') }}</span>
<span class="val">{{ g.discount }}</span> <span class="val">{{ g.discount }}</span>
</div> </div>
<div class="row-item"> <div class="row-item">
<span class="lbl">积分倍率</span> <span class="lbl">{{ $t('points.title') }}</span>
<span class="val">{{ g.pointsRate }}</span> <span class="val">{{ g.pointsRate }}</span>
</div> </div>
<div class="row-item"> <div class="row-item">
<span class="lbl">医院数</span> <span class="lbl">{{ $t('table.hospital') }}</span>
<span class="val">{{ g.hospitalCount }}</span> <span class="val">{{ g.hospitalCount }}</span>
</div> </div>
</div> </div>
@ -35,12 +35,12 @@
<el-card shadow="never" class="rule-card"> <el-card shadow="never" class="rule-card">
<template #header> <template #header>
<span class="title">积分规则mock</span> <span class="title">{{ $t('points.title') }}</span>
</template> </template>
<el-table :data="ruleList" border stripe> <el-table :data="ruleList" border stripe>
<el-table-column prop="action" label="行为" width="200" /> <el-table-column prop="action" :label="$t('common.operating')" width="200" />
<el-table-column prop="points" label="积分" width="120" /> <el-table-column prop="points" :label="$t('points.points')" width="120" />
<el-table-column prop="description" label="说明" /> <el-table-column prop="description" :label="$t('points.pointsDesc')" />
</el-table> </el-table>
</el-card> </el-card>
</div> </div>

@ -1,8 +1,8 @@
<template> <template>
<div class="hospital-detail" v-loading="loading"> <div class="hospital-detail" v-loading="loading">
<el-page-header :icon="ArrowLeft" content="返回" @back="goBack" class="page-header"> <el-page-header :icon="ArrowLeft" :content="$t('common.back')" @back="goBack" class="page-header">
<template #content> <template #content>
<span class="page-title">医院详情</span> <span class="page-title">{{ $t('modal.detailHospital') }}</span>
</template> </template>
</el-page-header> </el-page-header>
@ -10,27 +10,27 @@
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span class="title">{{ detail.name }}</span> <span class="title">{{ detail.name }}</span>
<el-tag size="small">{{ detail.customerCategory }} </el-tag> <el-tag size="small">{{ detail.customerCategory }} {{ $t('category.AShort').replace('A', '') }}</el-tag>
<el-button type="primary" size="small" style="margin-left:auto" @click="goEdit"> <el-button type="primary" size="small" style="margin-left:auto" @click="goEdit">
编辑 {{ $t('btn.edit') }}
</el-button> </el-button>
</div> </div>
</template> </template>
<el-descriptions :column="3" border> <el-descriptions :column="3" border>
<el-descriptions-item label="联系部门">{{ detail.contactDept }}</el-descriptions-item> <el-descriptions-item :label="$t('label.contactDept')">{{ detail.contactDept }}</el-descriptions-item>
<el-descriptions-item label="联系人">{{ detail.contactPerson }}</el-descriptions-item> <el-descriptions-item :label="$t('label.contactPerson')">{{ detail.contactPerson }}</el-descriptions-item>
<el-descriptions-item label="联系人职位">{{ detail.contactPosition }}</el-descriptions-item> <el-descriptions-item :label="$t('label.contactPosition')">{{ detail.contactPosition }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ detail.contactPhone }}</el-descriptions-item> <el-descriptions-item :label="$t('label.contactPhone')">{{ detail.contactPhone }}</el-descriptions-item>
<el-descriptions-item label="签约日期">{{ formatDate(detail.signDate) }}</el-descriptions-item> <el-descriptions-item :label="$t('table.signDate')">{{ formatDate(detail.signDate) }}</el-descriptions-item>
<el-descriptions-item label="验收日期">{{ formatDate(detail.acceptDate) }}</el-descriptions-item> <el-descriptions-item :label="$t('label.acceptDate')">{{ formatDate(detail.acceptDate) }}</el-descriptions-item>
<el-descriptions-item label="合同年限">{{ detail.contractYears }} </el-descriptions-item> <el-descriptions-item :label="$t('label.years')">{{ detail.contractYears }} {{ $t('common.time') }}</el-descriptions-item>
<el-descriptions-item label="维保到期">{{ formatDate(detail.maintenanceEnd) }}</el-descriptions-item> <el-descriptions-item :label="$t('table.maintenanceEnd')">{{ formatDate(detail.maintenanceEnd) }}</el-descriptions-item>
<el-descriptions-item label="负责人 ID">{{ detail.managerId }}</el-descriptions-item> <el-descriptions-item :label="$t('table.manager')">{{ detail.managerId }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
<div class="section"> <div class="section">
<div class="section-title">备注</div> <div class="section-title">{{ $t('common.remark') }}</div>
<div class="html-content">{{ detail.remark || "—" }}</div> <div class="html-content">{{ detail.remark || "—" }}</div>
</div> </div>
</el-card> </el-card>

@ -1,8 +1,13 @@
<template> <template>
<div class="hospital-edit" v-loading="loading"> <div class="hospital-edit" v-loading="loading">
<el-page-header :icon="ArrowLeft" content="返回" @back="goBack" class="page-header"> <el-page-header
:icon="ArrowLeft"
:content="$t('common.back')"
@back="goBack"
class="page-header"
>
<template #content> <template #content>
<span class="page-title">{{ isEdit ? "编辑医院" : "新增医院" }}</span> <span class="page-title">{{ isEdit ? $t('modal.editHospital') : $t('modal.addHospital') }}</span>
</template> </template>
</el-page-header> </el-page-header>
@ -14,69 +19,100 @@
label-width="120px" label-width="120px"
style="max-width: 800px" style="max-width: 800px"
> >
<el-form-item label="医院名称" prop="name"> <el-form-item :label="$t('label.hospitalName')" prop="name">
<el-input v-model="form.name" placeholder="请输入医院名称" /> <el-input v-model="form.name" :placeholder="$t('placeholder.hospital')" />
</el-form-item> </el-form-item>
<el-form-item label="客户类别" prop="customerCategory"> <el-form-item :label="$t('label.category')" prop="customerCategory">
<el-select v-model="form.customerCategory" placeholder="请选择" style="width: 200px"> <el-select
<el-option label="A 类" value="A" /> v-model="form.customerCategory"
<el-option label="B 类" value="B" /> :placeholder="$t('label.category')"
<el-option label="C 类" value="C" /> style="width: 200px"
>
<el-option label="A类" value="A" />
<el-option label="B类" value="B" />
<el-option label="C类" value="C" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="联系部门"> <el-form-item :label="$t('label.contactDept')">
<el-input v-model="form.contactDept" /> <el-input v-model="form.contactDept" />
</el-form-item> </el-form-item>
<el-form-item label="联系人"> <el-form-item :label="$t('label.contactPerson')">
<el-input v-model="form.contactPerson" /> <el-input v-model="form.contactPerson" />
</el-form-item> </el-form-item>
<el-form-item label="联系人职位"> <el-form-item :label="$t('label.contactPosition')">
<el-input v-model="form.contactPosition" /> <el-input v-model="form.contactPosition" />
</el-form-item> </el-form-item>
<el-form-item label="联系电话"> <el-form-item :label="$t('label.contactPhone')">
<el-input v-model="form.contactPhone" /> <el-input v-model="form.contactPhone" />
</el-form-item> </el-form-item>
<el-form-item label="签约日期" prop="signDate"> <el-form-item :label="$t('label.signDate')" prop="signDate">
<el-date-picker <el-date-picker
v-model="form.signDate" v-model="form.signDate"
type="date" type="date"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="请选择" :placeholder="$t('common.pleaseSelect')"
style="width: 200px" style="width: 200px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="验收日期" prop="acceptDate"> <el-form-item :label="$t('label.acceptDate')" prop="acceptDate">
<el-date-picker <el-date-picker
v-model="form.acceptDate" v-model="form.acceptDate"
type="date" type="date"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="请选择" :placeholder="$t('common.pleaseSelect')"
style="width: 200px" style="width: 200px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="合同年限" prop="contractYears"> <el-form-item :label="$t('label.years')" prop="contractYears">
<el-input-number v-model="form.contractYears" :min="1" :max="20" /> <el-input-number v-model="form.contractYears" :min="1" :max="20" />
</el-form-item> </el-form-item>
<el-form-item label="维保到期" prop="maintenanceEnd"> <el-form-item :label="$t('label.maintenanceEndDate')" prop="maintenanceEnd">
<el-date-picker <el-date-picker
v-model="form.maintenanceEnd" v-model="form.maintenanceEnd"
type="date" type="date"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="请选择" :placeholder="$t('common.pleaseSelect')"
style="width: 200px" style="width: 200px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="负责人 ID"> <el-form-item :label="$t('label.manager')">
<el-input-number v-model="form.managerId" :min="0" /> <el-select
v-model="form.managerId"
:placeholder="$t('label.manager')"
filterable
clearable
style="width: 240px"
>
<el-option
v-for="item in managerOptions"
:key="item.id"
:label="`${item.name}`"
:value="item.id"
/>
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="备注"> <el-form-item :label="$t('label.acceptanceReport')">
<el-upload
v-model:file-list="reportFileList"
:auto-upload="false"
:limit="1"
:on-exceed="handleReportExceed"
accept=".pdf"
>
<el-button type="primary" plain>{{ $t('btn.upload') }}</el-button>
<template #tip>
<div class="el-upload__tip">{{ $t('label.acceptanceReport') }} (.pdf)</div>
</template>
</el-upload>
</el-form-item>
<el-form-item :label="$t('common.remark')">
<el-input v-model="form.remark" type="textarea" :rows="3" /> <el-input v-model="form.remark" type="textarea" :rows="3" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :loading="submitting" @click="submit"> <el-button type="primary" :loading="submitting" @click="submit">
{{ isEdit ? "保存" : "新增" }} {{ isEdit ? $t('common.save') : $t('common.add') }}
</el-button> </el-button>
<el-button @click="goBack"></el-button> <el-button @click="goBack">{{ $t('common.cancel') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
@ -88,12 +124,15 @@ import { computed, onMounted, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { ArrowLeft } from "@element-plus/icons-vue"; import { ArrowLeft } from "@element-plus/icons-vue";
import { useI18n } from "vue-i18n";
import { import {
getAdminHospitalDetail, getAdminHospitalDetail,
createAdminHospital, createAdminHospital,
updateAdminHospital, updateAdminHospital,
getAdminAccounts,
} from "@/service/modular/admin"; } from "@/service/modular/admin";
const { t: $t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const loading = ref(false); const loading = ref(false);
@ -118,12 +157,48 @@ const form = reactive({
}); });
const rules = { const rules = {
name: [{ required: true, message: "请输入医院名称", trigger: "blur" }], name: [{ required: true, message: () => $t('placeholder.hospital'), trigger: "blur" }],
customerCategory: [{ required: true, message: "请选择客户类别", trigger: "change" }], customerCategory: [
signDate: [{ required: true, message: "请选择签约日期", trigger: "change" }], { required: true, message: () => $t('label.category'), trigger: "change" },
acceptDate: [{ required: true, message: "请选择验收日期", trigger: "change" }], ],
contractYears: [{ required: true, message: "请输入合同年限", trigger: "blur" }], signDate: [{ required: true, message: () => $t('label.signDate'), trigger: "change" }],
maintenanceEnd: [{ required: true, message: "请选择维保到期日", trigger: "change" }], acceptDate: [
{ required: true, message: () => $t('label.acceptDate'), trigger: "change" },
],
contractYears: [
{ required: true, message: () => $t('label.years'), trigger: "blur" },
],
maintenanceEnd: [
{ required: true, message: () => $t('label.maintenanceEndDate'), trigger: "change" },
],
};
const managerOptions = ref([]);
const reportFileList = ref([]);
const handleReportExceed = (files) => {
reportFileList.value = files.slice(-1);
};
const buildHospitalFormData = () => {
const fd = new FormData();
Object.entries(form).forEach(([key, value]) => {
if (value === undefined || value === null || value === "") return;
fd.append(key, value);
});
const file = (reportFileList.value || []).map((f) => f.raw).filter(Boolean)[0];
if (file) fd.append("files", file);
return fd;
};
const loadManagers = async () => {
try {
const res = await getAdminAccounts({ type: "Admin" });
managerOptions.value = res?.list || [];
} catch (e) {
// request
}
}; };
const loadDetail = async () => { const loadDetail = async () => {
@ -140,14 +215,19 @@ const loadDetail = async () => {
const submit = async () => { const submit = async () => {
await formRef.value.validate(async (valid) => { await formRef.value.validate(async (valid) => {
if (!valid) return; if (!valid) return;
if (!reportFileList.value?.length) {
ElMessage.warning($t('label.acceptanceReport'));
return;
}
submitting.value = true; submitting.value = true;
try { try {
const fd = buildHospitalFormData();
if (isEdit.value) { if (isEdit.value) {
await updateAdminHospital(route.params.id, form); await updateAdminHospital(route.params.id, fd);
ElMessage.success("保存成功"); ElMessage.success($t('msg.hospitalEditSuccess'));
} else { } else {
await createAdminHospital(form); await createAdminHospital(fd);
ElMessage.success("新增成功"); ElMessage.success($t('msg.hospitalAddSuccess'));
} }
router.replace("/admin/hospitals"); router.replace("/admin/hospitals");
} finally { } finally {
@ -158,7 +238,10 @@ const submit = async () => {
const goBack = () => router.back(); const goBack = () => router.back();
onMounted(loadDetail); onMounted(() => {
loadManagers();
loadDetail();
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

@ -2,39 +2,69 @@
<div class="hospital-list"> <div class="hospital-list">
<el-card shadow="never"> <el-card shadow="never">
<el-form :inline="true" :model="filters" class="filter-form"> <el-form :inline="true" :model="filters" class="filter-form">
<el-form-item label="关键词"> <el-form-item :label="$t('placeholder.keyword')">
<el-input v-model="filters.keyword" placeholder="医院名称" clearable style="width:220px" /> <el-input
v-model="filters.keyword"
:placeholder="$t('placeholder.hospital')"
clearable
style="width: 220px"
/>
</el-form-item> </el-form-item>
<el-form-item label="客户类别"> <el-form-item :label="$t('label.category')">
<el-select v-model="filters.category" placeholder="全部" clearable style="width:140px"> <el-select
<el-option label="A 类" value="A" /> v-model="filters.category"
<el-option label="B 类" value="B" /> :placeholder="$t('common.all')"
<el-option label="C 类" value="C" /> clearable
style="width: 140px"
>
<el-option label="A类" value="A" />
<el-option label="B类" value="B" />
<el-option label="C类" value="C" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="onSearch"></el-button> <el-button type="primary" @click="onSearch">{{ $t('btn.query') }}</el-button>
<el-button @click="onReset"></el-button> <el-button @click="onReset">{{ $t('btn.reset') }}</el-button>
<el-button type="success" @click="goAdd"></el-button> <el-button type="success" @click="goAdd">{{ $t('btn.addHospital') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-table :data="list" v-loading="loading" border stripe> <el-table :data="list" v-loading="loading" border stripe>
<el-table-column prop="id" label="ID" width="80" /> <!-- <el-table-column prop="id" label="ID" width="80" /> -->
<el-table-column prop="name" label="医院名称" min-width="180" show-overflow-tooltip /> <el-table-column
<el-table-column prop="customerCategory" label="客户类别" width="100" /> :label="$t('table.hospital')"
<el-table-column prop="contactPerson" label="联系人" width="100" /> prop="name"
<el-table-column prop="contactPhone" label="联系电话" width="140" /> min-width="180"
<el-table-column label="签约日期" width="120"> show-overflow-tooltip
/>
<el-table-column :label="$t('label.category')" prop="customerCategory" width="100" />
<el-table-column :label="$t('table.contact')" prop="contactPerson" width="100" />
<el-table-column :label="$t('table.phone')" prop="contactPhone" width="140" />
<el-table-column :label="$t('table.manager')" prop="managerName" width="140" />
<el-table-column :label="$t('table.usageYears')" prop="contractYears" width="140" />
<el-table-column :label="$t('table.acceptanceReport')" width="140">
<template #default="{ row }">
<el-button
v-if="row.attachmentPath"
link
type="primary"
@click="viewReport(row.attachmentPath)"
>
{{ $t('btn.preview') }}
</el-button>
<span v-else></span>
</template>
</el-table-column>
<el-table-column :label="$t('table.signDate')" width="120">
<template #default="{ row }">{{ formatDate(row.signDate) }}</template> <template #default="{ row }">{{ formatDate(row.signDate) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="维保到期" width="120"> <el-table-column :label="$t('table.maintenanceEnd')" width="120">
<template #default="{ row }">{{ formatDate(row.maintenanceEnd) }}</template> <template #default="{ row }">{{ formatDate(row.maintenanceEnd) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="160" fixed="right"> <el-table-column :label="$t('table.action')" width="160" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" @click="goDetail(row.id)"></el-button> <el-button link type="primary" @click="goDetail(row.id)">{{ $t('btn.detail') }}</el-button>
<el-button link type="primary" @click="goEdit(row.id)"></el-button> <el-button link type="primary" @click="goEdit(row.id)">{{ $t('btn.edit') }}</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -57,9 +87,12 @@
<script setup> <script setup>
import { onMounted, reactive, ref } from "vue"; import { onMounted, reactive, ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { getAdminHospitals } from "@/service/modular/admin"; import { useI18n } from "vue-i18n";
import { getAdminHospitals, getUploadFile } from "@/service/modular/admin";
const { t: $t } = useI18n();
const router = useRouter(); const router = useRouter();
const list = ref([]); const list = ref([]);
const total = ref(0); const total = ref(0);
@ -99,6 +132,22 @@ const goDetail = (id) => router.push(`/admin/hospitals/detail/${id}`);
const goEdit = (id) => router.push(`/admin/hospitals/edit/${id}`); const goEdit = (id) => router.push(`/admin/hospitals/edit/${id}`);
const goAdd = () => router.push("/admin/hospitals/edit"); const goAdd = () => router.push("/admin/hospitals/edit");
const viewReport = async (attachmentPath) => {
if (!attachmentPath) return;
const fileName = String(attachmentPath).split(/[/\\]/).pop();
try {
const blob = await getUploadFile(fileName);
const url = URL.createObjectURL(blob);
const win = window.open(url, "_blank");
if (!win) {
ElMessage.warning($t('msg.pleaseFillRequired'));
}
setTimeout(() => URL.revokeObjectURL(url), 60_000);
} catch (e) {
ElMessage.error(e?.message || $t('msg.failed'));
}
};
onMounted(loadList); onMounted(loadList);
</script> </script>

@ -2,47 +2,47 @@
<div class="hospital-service"> <div class="hospital-service">
<el-card shadow="never"> <el-card shadow="never">
<el-form :inline="true" :model="filters" class="filter-form"> <el-form :inline="true" :model="filters" class="filter-form">
<el-form-item label="服务类型"> <el-form-item :label="$t('label.type')">
<el-select v-model="filters.type" placeholder="全部" clearable style="width:160px"> <el-select v-model="filters.type" :placeholder="$t('common.all')" clearable style="width:160px">
<el-option label="巡检服务" value="巡检服务" /> <el-option label="巡检服务" value="巡检服务" />
<el-option label="技术支持" value="技术支持" /> <el-option label="技术支持" value="技术支持" />
<el-option label="回访服务" value="回访服务" /> <el-option label="回访服务" value="回访服务" />
<el-option label="培训服务" value="培训服务" /> <el-option label="培训服务" value="培训服务" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="负责人"> <el-form-item :label="$t('table.manager')">
<el-input v-model="filters.owner" placeholder="客户经理" clearable style="width:160px" /> <el-input v-model="filters.owner" :placeholder="$t('table.manager')" clearable style="width:160px" />
</el-form-item> </el-form-item>
<el-form-item label="状态"> <el-form-item :label="$t('label.status')">
<el-select v-model="filters.status" placeholder="全部" clearable style="width:140px"> <el-select v-model="filters.status" :placeholder="$t('common.all')" clearable style="width:140px">
<el-option label="待执行" value="待执行" /> <el-option label="待执行" value="待执行" />
<el-option label="进行中" value="进行中" /> <el-option label="进行中" value="进行中" />
<el-option label="已完成" value="已完成" /> <el-option label="已完成" value="已完成" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="onSearch"></el-button> <el-button type="primary" @click="onSearch">{{ $t('btn.query') }}</el-button>
<el-button @click="onReset"></el-button> <el-button @click="onReset">{{ $t('btn.reset') }}</el-button>
<el-button type="success" @click="onAdd"></el-button> <el-button type="success" @click="onAdd">{{ $t('btn.add') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-table :data="list" v-loading="loading" border stripe> <el-table :data="list" v-loading="loading" border stripe>
<el-table-column prop="serviceNo" label="服务编号" width="160" /> <el-table-column prop="serviceNo" label="No." width="160" />
<el-table-column prop="hospitalName" label="医院名称" min-width="160" show-overflow-tooltip /> <el-table-column :label="$t('table.hospital')" prop="hospitalName" min-width="160" show-overflow-tooltip />
<el-table-column prop="type" label="服务类型" width="120" /> <el-table-column :label="$t('label.type')" prop="type" width="120" />
<el-table-column prop="owner" label="负责人" width="120" /> <el-table-column :label="$t('table.manager')" prop="owner" width="120" />
<el-table-column prop="planDate" label="计划日期" width="120" /> <el-table-column :label="$t('common.time')" prop="planDate" width="120" />
<el-table-column label="状态" width="100"> <el-table-column :label="$t('label.status')" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="statusType(row.status)" size="small">{{ row.status }}</el-tag> <el-tag :type="statusType(row.status)" size="small">{{ row.status }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip /> <el-table-column :label="$t('common.remark')" prop="remark" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="160" fixed="right"> <el-table-column :label="$t('table.action')" width="160" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" @click="onView(row)"></el-button> <el-button link type="primary" @click="onView(row)">{{ $t('btn.view') }}</el-button>
<el-button link type="success" @click="onFinish(row)" v-if="row.status !== '已完成'"></el-button> <el-button link type="success" @click="onFinish(row)" v-if="row.status !== '已完成'">{{ $t('status.completed') }}</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -65,8 +65,10 @@
<script setup> <script setup>
import { onMounted, reactive, ref } from "vue"; import { onMounted, reactive, ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
const { t: $t } = useI18n();
// TODO: /api/admin/hospitals/service mock
const MOCK_LIST = [ const MOCK_LIST = [
{ id: 1, serviceNo: "SV-2025-0001", hospitalName: "上海第一人民医院", type: "巡检服务", owner: "张三", planDate: "2025-05-10", status: "已完成", remark: "季度例行巡检" }, { id: 1, serviceNo: "SV-2025-0001", hospitalName: "上海第一人民医院", type: "巡检服务", owner: "张三", planDate: "2025-05-10", status: "已完成", remark: "季度例行巡检" },
{ id: 2, serviceNo: "SV-2025-0002", hospitalName: "北京协和医院", type: "技术支持", owner: "李四", planDate: "2025-05-12", status: "进行中", remark: "系统升级支持" }, { id: 2, serviceNo: "SV-2025-0002", hospitalName: "北京协和医院", type: "技术支持", owner: "李四", planDate: "2025-05-12", status: "进行中", remark: "系统升级支持" },
@ -96,9 +98,12 @@ const loadList = async () => {
const onSearch = () => { page.value = 1; loadList(); }; const onSearch = () => { page.value = 1; loadList(); };
const onReset = () => { filters.type = ""; filters.owner = ""; filters.status = ""; onSearch(); }; const onReset = () => { filters.type = ""; filters.owner = ""; filters.status = ""; onSearch(); };
const onAdd = () => ElMessage.success("新增服务mock"); const onAdd = () => ElMessage.success($t('btn.add'));
const onView = (row) => ElMessage.info(`查看服务:${row.serviceNo}mock`); const onView = (row) => ElMessage.info(`${$t('btn.view')}${row.serviceNo}`);
const onFinish = (row) => { row.status = "已完成"; ElMessage.success("已标记为完成mock"); }; const onFinish = (row) => {
row.status = "已完成";
ElMessage.success($t('msg.editSuccess'));
};
onMounted(loadList); onMounted(loadList);
</script> </script>

@ -1,8 +1,8 @@
<template> <template>
<div class="order-detail" v-loading="loading"> <div class="order-detail" v-loading="loading">
<el-page-header :icon="ArrowLeft" content="返回" @back="goBack" class="page-header"> <el-page-header :icon="ArrowLeft" :content="$t('common.back')" @back="goBack" class="page-header">
<template #content> <template #content>
<span class="page-title">工单详情</span> <span class="page-title">{{ $t('modal.detail') }}</span>
</template> </template>
</el-page-header> </el-page-header>
@ -19,28 +19,28 @@
style="margin-left:auto" style="margin-left:auto"
@click="goProcess" @click="goProcess"
> >
处理工单 {{ $t('btn.process') }}
</el-button> </el-button>
</div> </div>
</template> </template>
<el-descriptions :column="3" border> <el-descriptions :column="3" border>
<el-descriptions-item label="工单号">{{ detail.orderNo }}</el-descriptions-item> <el-descriptions-item :label="$t('table.id')">{{ detail.orderNo }}</el-descriptions-item>
<el-descriptions-item label="医院">{{ detail.hospitalName }}</el-descriptions-item> <el-descriptions-item :label="$t('table.hospital')">{{ detail.hospitalName }}</el-descriptions-item>
<el-descriptions-item label="服务类型">{{ detail.serviceType }}</el-descriptions-item> <el-descriptions-item :label="$t('table.type')">{{ detail.serviceType }}</el-descriptions-item>
<el-descriptions-item label="提交人">{{ detail.submitter }}</el-descriptions-item> <el-descriptions-item :label="$t('table.submitter')">{{ detail.submitter }}</el-descriptions-item>
<el-descriptions-item label="科室">{{ detail.department }}</el-descriptions-item> <el-descriptions-item :label="$t('label.dept')">{{ detail.department }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ detail.createdAt }}</el-descriptions-item> <el-descriptions-item :label="$t('table.time')">{{ detail.createdAt }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
<div class="section"> <div class="section">
<div class="section-title">详细描述</div> <div class="section-title">{{ $t('label.description') }}</div>
<div class="html-content" v-html="detail.description || ''" /> <div class="html-content" v-html="detail.description || ''" />
</div> </div>
<div class="section"> <div class="section">
<div class="section-title">附件 ({{ (detail.attachments || []).length }})</div> <div class="section-title">{{ $t('label.attachment') }} ({{ (detail.attachments || []).length }})</div>
<el-empty v-if="!detail.attachments?.length" description="暂无附件" :image-size="80" /> <el-empty v-if="!detail.attachments?.length" :description="$t('orderDetail.noAttachment')" :image-size="80" />
<div v-else class="files"> <div v-else class="files">
<el-link <el-link
v-for="(f, i) in detail.attachments" v-for="(f, i) in detail.attachments"
@ -56,7 +56,7 @@
</div> </div>
<div class="section"> <div class="section">
<div class="section-title">处理记录</div> <div class="section-title">{{ $t('orderDetail.statusChange') }}</div>
<el-timeline> <el-timeline>
<el-timeline-item <el-timeline-item
v-for="(log, i) in detail.logs || []" v-for="(log, i) in detail.logs || []"

@ -2,10 +2,10 @@
<div class="order-list"> <div class="order-list">
<el-card shadow="never"> <el-card shadow="never">
<el-form :inline="true" :model="filters" class="filter-form"> <el-form :inline="true" :model="filters" class="filter-form">
<el-form-item label="医院"> <el-form-item :label="$t('label.hospitalName')">
<el-select <el-select
v-model="filters.hospitalId" v-model="filters.hospitalId"
placeholder="全部" :placeholder="$t('common.all')"
clearable clearable
filterable filterable
style="width: 200px" style="width: 200px"
@ -18,23 +18,21 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="状态"> <el-form-item :label="$t('table.status')">
<el-select <el-select
v-model="filters.status" v-model="filters.status"
placeholder="全部" :placeholder="$t('common.all')"
clearable clearable
style="width: 140px" style="width: 140px"
> >
<el-option label="待处理" value="待处理" /> <el-option label="待处理" value="待处理" />
<el-option label="处理中" value="处理中" />
<el-option label="已完成" value="已完成" /> <el-option label="已完成" value="已完成" />
<el-option label="已取消" value="已取消" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="优先级"> <el-form-item :label="$t('table.priority')">
<el-select <el-select
v-model="filters.priority" v-model="filters.priority"
placeholder="全部" :placeholder="$t('common.all')"
clearable clearable
style="width: 120px" style="width: 120px"
> >
@ -43,81 +41,144 @@
<el-option label="低" value="低" /> <el-option label="低" value="低" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="服务类型"> <el-form-item :label="$t('table.type')">
<el-input <el-select
v-model="filters.type" v-model="filters.type"
placeholder="请输入" :placeholder="$t('common.all')"
clearable clearable
style="width: 140px" style="width: 120px"
/> >
<el-option label="故障问题" value="故障问题" />
<el-option label="使用咨询" value="使用咨询" />
<el-option label="功能需求" value="功能需求" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="关键词"> <el-form-item :label="$t('placeholder.keyword')">
<el-input <el-input
v-model="filters.keyword" v-model="filters.keyword"
placeholder="搜索标题/描述" :placeholder="$t('placeholder.keyword')"
clearable clearable
style="width: 200px" style="width: 200px"
/> />
</el-form-item> </el-form-item>
<el-form-item label="提交日期"> <el-form-item :label="$t('table.time')">
<el-date-picker <el-date-picker
v-model="dateRange" v-model="dateRange"
type="daterange" type="daterange"
range-separator="" range-separator="-"
start-placeholder="开始" :start-placeholder="$t('common.startDate')"
end-placeholder="结束" :end-placeholder="$t('common.endDate')"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
style="width: 240px" style="width: 240px"
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="onSearch"></el-button> <el-button type="primary" @click="onSearch">{{
<el-button @click="onReset"></el-button> $t("btn.query")
<el-button type="success" @click="openQuick"></el-button> }}</el-button>
<el-button @click="onReset">{{ $t("btn.reset") }}</el-button>
<el-button type="success" @click="openQuick">{{
$t("btn.quickAddOrder")
}}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<!-- 工单总览 -->
<div class="order-stats">
<div class="stat-card stat-total">
<div class="stat-icon">📋</div>
<div class="stat-value">{{ stats.total }}</div>
<div class="stat-label">{{ $t("stat.totalOrders") }}</div>
</div>
<div class="stat-card stat-pending">
<div class="stat-icon"></div>
<div class="stat-value">{{ stats.pending }}</div>
<div class="stat-label">{{ $t("stat.pendingOrders") }}</div>
</div>
<div class="stat-card stat-completed">
<div class="stat-icon"></div>
<div class="stat-value">{{ stats.completed }}</div>
<div class="stat-label">{{ $t("stat.completedOrders") }}</div>
</div>
<div class="stat-card stat-high">
<div class="stat-icon">🔴</div>
<div class="stat-value">{{ stats.highPriority }}</div>
<div class="stat-label">{{ $t("stat.highPriorityOrders") }}</div>
</div>
</div>
<el-table :data="list" v-loading="loading" border stripe> <el-table :data="list" v-loading="loading" border stripe>
<el-table-column prop="orderNo" label="工单号" width="160" /> <el-table-column :label="$t('table.id')" prop="orderNo" width="160" />
<el-table-column <el-table-column
:label="$t('table.hospital')"
prop="hospitalName" prop="hospitalName"
label="医院名称"
min-width="160" min-width="160"
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column <el-table-column
:label="$t('table.title')"
prop="title" prop="title"
label="问题描述"
min-width="200" min-width="200"
show-overflow-tooltip show-overflow-tooltip
/> />
<el-table-column prop="serviceType" label="类型" width="120" /> <el-table-column
<el-table-column label="优先级" width="90"> :label="$t('table.type')"
prop="serviceType"
width="120"
/>
<el-table-column
:label="$t('table.submitter')"
prop="submitter"
min-width="100"
show-overflow-tooltip
/>
<el-table-column
:label="$t('table.dept')"
prop="department"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
:label="$t('table.registrar')"
prop="registrarName"
min-width="100"
show-overflow-tooltip
/>
<el-table-column
:label="$t('table.handler')"
prop="processName"
min-width="100"
show-overflow-tooltip
/>
<el-table-column :label="$t('table.priority')" width="90">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="priorityType(row.priority)" size="small">{{ <el-tag :type="priorityType(row.priority)" size="small">{{
row.priority row.priority
}}</el-tag> }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="状态" width="100"> <el-table-column :label="$t('table.status')" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="statusType(row.status)" size="small">{{ <el-tag :type="statusType(row.status)" size="small">{{
row.status row.status
}}</el-tag> }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="submitTime" label="提交时间" width="170" /> <el-table-column
<el-table-column label="操作" width="120" fixed="right"> :label="$t('table.time')"
prop="createdAt"
width="170"
/>
<el-table-column :label="$t('table.action')" width="120" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" @click="goDetail(row.id)" <el-button link type="primary" @click="goDetail(row.id)">{{
>详情</el-button $t("btn.detail")
> }}</el-button>
<el-button <el-button
link link
type="success" type="success"
v-if="row.status !== '已完成'" v-if="row.status !== '已完成'"
@click="goProcess(row.id)" @click="goProcess(row.id)"
>处理</el-button >{{ $t("btn.process") }}</el-button
> >
</template> </template>
</el-table-column> </el-table-column>
@ -139,7 +200,7 @@
<!-- 快速提交工单 弹窗 --> <!-- 快速提交工单 弹窗 -->
<el-dialog <el-dialog
v-model="quickVisible" v-model="quickVisible"
title="快速提交工单" :title="$t('modal.submit')"
width="640px" width="640px"
:close-on-click-modal="false" :close-on-click-modal="false"
@closed="onQuickClosed" @closed="onQuickClosed"
@ -150,10 +211,10 @@
:rules="quickRules" :rules="quickRules"
label-width="100px" label-width="100px"
> >
<el-form-item label="医院" prop="hospitalId"> <el-form-item :label="$t('label.hospitalName')" prop="hospitalId">
<el-select <el-select
v-model="quickForm.hospitalId" v-model="quickForm.hospitalId"
placeholder="请选择医院" :placeholder="$t('msg.pleaseSelectHospital')"
filterable filterable
style="width: 100%" style="width: 100%"
> >
@ -165,57 +226,124 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="标题" prop="title"> <el-form-item :label="$t('label.title')" prop="title">
<div class="title-row">
<el-input <el-input
v-model="quickForm.title" v-model="quickForm.title"
placeholder="请输入工单标题" :placeholder="$t('placeholder.title')"
maxlength="100" maxlength="100"
show-word-limit show-word-limit
/> />
<div class="color-tag-picker" ref="colorPickerRef">
<div
class="color-tag-preview"
:style="{ background: quickForm.colorTag || '#dcdfe6' }"
:class="{ 'is-empty': !quickForm.colorTag }"
@click.stop="toggleColorPicker"
></div>
<div
v-show="colorPickerVisible"
class="color-tag-dropdown"
@click.stop
>
<div class="color-tag-title">{{ $t("label.colorTag") }}</div>
<div class="color-tag-options">
<div
v-for="c in colorOptions"
:key="c.value"
class="color-tag-option"
:class="{ active: quickForm.colorTag === c.value }"
:style="{ background: c.value }"
:title="c.label"
@click="selectColor(c.value)"
></div>
</div>
</div>
</div>
</div>
</el-form-item> </el-form-item>
<el-form-item label="服务类型" prop="serviceType"> <el-form-item :label="$t('label.type')" prop="serviceType">
<el-input <el-select
v-model="quickForm.serviceType" v-model="quickForm.serviceType"
placeholder="请输入服务类型" :placeholder="$t('placeholder.type')"
/> clearable
>
<el-option label="故障问题" value="故障问题" />
<el-option label="使用咨询" value="使用咨询" />
<el-option label="功能需求" value="功能需求" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="优先级" prop="priority"> <el-form-item :label="$t('label.priority')" prop="priority">
<el-radio-group v-model="quickForm.priority"> <el-radio-group v-model="quickForm.priority">
<el-radio-button label="高"></el-radio-button> <el-radio-button label="高"></el-radio-button>
<el-radio-button label="中"></el-radio-button> <el-radio-button label="中"></el-radio-button>
<el-radio-button label="低"></el-radio-button> <el-radio-button label="低"></el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="科室"> <el-form-item :label="$t('label.name')">
<el-input v-model="quickForm.department" placeholder="请输入科室" /> <el-input
</el-form-item> v-model="quickForm.submitter"
<el-form-item label="颜色标签"> :placeholder="$t('placeholder.name')"
<el-radio-group v-model="quickForm.colorTag"> />
<el-radio-button label=""></el-radio-button>
<el-radio-button label="red"
><span style="color: #F4664A"></span></el-radio-button
>
<el-radio-button label="yellow"
><span style="color: #F6BD16"></span></el-radio-button
>
<el-radio-button label="blue"
><span style="color: #5B8FF9"></span></el-radio-button
>
</el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="详细描述" prop="description"> <el-form-item :label="$t('label.dept')">
<el-input <el-input
v-model="quickForm.department"
:placeholder="$t('placeholder.dept')"
/>
</el-form-item>
<el-form-item :label="$t('label.description')" prop="description">
<div class="rich-editor">
<Toolbar
:editor="editorRef"
:default-config="toolbarConfig"
mode="default"
style="border-bottom: 1px solid #dcdfe6"
/>
<Editor
v-model="quickForm.description" v-model="quickForm.description"
type="textarea" :default-config="editorConfig"
:rows="4" mode="default"
placeholder="请详细描述问题(支持简单 HTML" style="height: 200px; overflow-y: auto"
@on-created="handleEditorCreated"
/> />
</div>
</el-form-item>
<el-form-item :label="$t('label.attachment')">
<div class="attachment-pick">
<el-input
:model-value="quickForm.files[0]?.name || ''"
:placeholder="$t('upload.text')"
readonly
clearable
@click="triggerFilePicker"
@clear="clearFile"
>
<template #append>
<el-button @click.stop="triggerFilePicker">{{
$t("btn.upload")
}}</el-button>
</template>
</el-input>
<input
ref="fileInputRef"
type="file"
class="attachment-hidden-input"
@change="onFileChange"
/>
</div>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="quickVisible = false">取消</el-button> <el-button @click="quickVisible = false">{{
<el-button type="primary" :loading="quickSubmitting" @click="submitQuick" $t("common.cancel")
>提交</el-button }}</el-button>
<el-button
type="primary"
:loading="quickSubmitting"
@click="submitQuick"
>{{ $t("common.submit") }}</el-button
> >
</template> </template>
</el-dialog> </el-dialog>
@ -223,9 +351,12 @@
</template> </template>
<script setup> <script setup>
import { onMounted, reactive, ref } from "vue"; import { onBeforeUnmount, onMounted, reactive, ref, shallowRef } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import "@wangeditor/editor/dist/css/style.css";
import { useI18n } from "vue-i18n";
import { import {
createAdminOrder, createAdminOrder,
getAdminHospitals, getAdminHospitals,
@ -233,6 +364,7 @@ import {
} from "@/service/modular/admin"; } from "@/service/modular/admin";
import { useUserStore } from "@/stores/api/user"; import { useUserStore } from "@/stores/api/user";
const { t: $t } = useI18n();
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
const list = ref([]); const list = ref([]);
@ -253,7 +385,7 @@ const filters = reactive({
const loadHospitals = async () => { const loadHospitals = async () => {
try { try {
const res = await getAdminHospitals({ page: 1, pageSize: 1000 }); const res = await getAdminHospitals({ page: 1, pageSize: 1000 });
hospitals.value = res?.list || []; hospitals.value = res?.list.map || [];
} catch (_) { } catch (_) {
hospitals.value = []; hospitals.value = [];
} }
@ -266,6 +398,28 @@ const statusType = (s) =>
s s
] || "info"; ] || "info";
//
const stats = reactive({
total: 0,
pending: 0,
completed: 0,
highPriority: 0,
});
const parseStats = (res) => {
stats.total = res?.total || 0;
let payload = res?.data;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch (_) {
payload = {};
}
}
stats.completed = Number(payload?.TotalStatus1 ?? 0);
stats.pending = Number(payload?.TotalStatus2 ?? 0);
stats.highPriority = Number(payload?.TotalPriority ?? 0);
};
const loadList = async () => { const loadList = async () => {
loading.value = true; loading.value = true;
try { try {
@ -276,7 +430,11 @@ const loadList = async () => {
} }
const res = await getAdminOrders(params); const res = await getAdminOrders(params);
list.value = res?.list || []; list.value = res?.list || [];
list.value.forEach((item) => {
item.createdAt = item.createdAt.split("T").join(" ");
});
total.value = res?.total || 0; total.value = res?.total || 0;
parseStats(res);
} finally { } finally {
loading.value = false; loading.value = false;
} }
@ -301,6 +459,32 @@ const onReset = () => {
const goDetail = (id) => router.push(`/admin/orders/detail/${id}`); const goDetail = (id) => router.push(`/admin/orders/detail/${id}`);
const goProcess = (id) => router.push(`/admin/orders/process/${id}`); const goProcess = (id) => router.push(`/admin/orders/process/${id}`);
// Hex
const colorOptions = [
{ label: "蓝", value: "#667eea" },
{ label: "绿", value: "#52c41a" },
{ label: "青", value: "#13c2c2" },
{ label: "紫", value: "#722ed1" },
{ label: "橙", value: "#fa8c16" },
{ label: "红", value: "#ff4d4f" },
];
const colorPickerVisible = ref(false);
const colorPickerRef = ref();
const toggleColorPicker = () => {
colorPickerVisible.value = !colorPickerVisible.value;
};
const selectColor = (val) => {
quickForm.colorTag = val;
colorPickerVisible.value = false;
};
const onDocClick = (e) => {
if (!colorPickerVisible.value) return;
const el = colorPickerRef.value;
if (el && !el.contains(e.target)) {
colorPickerVisible.value = false;
}
};
// //
const quickVisible = ref(false); const quickVisible = ref(false);
const quickSubmitting = ref(false); const quickSubmitting = ref(false);
@ -311,21 +495,90 @@ const quickForm = reactive({
serviceType: "", serviceType: "",
priority: "中", priority: "中",
department: "", department: "",
colorTag: "", colorTag: "#667eea",
description: "", description: "",
submitter: "",
files: [],
}); });
const fileInputRef = ref();
const MAX_FILE_SIZE = 50 * 1024 * 1024;
const triggerFilePicker = () => {
fileInputRef.value?.click();
};
const onFileChange = (e) => {
const file = e.target.files?.[0];
e.target.value = "";
if (!file) return;
if (file.size > MAX_FILE_SIZE) return;
quickForm.files = [{ name: file.name, size: file.size, raw: file }];
};
const clearFile = () => {
quickForm.files = [];
};
const quickRules = { const quickRules = {
hospitalId: [{ required: true, message: "请选择医院", trigger: "change" }], hospitalId: [
title: [{ required: true, message: "请输入标题", trigger: "blur" }], {
required: true,
message: () => $t("msg.pleaseSelectHospital"),
trigger: "change",
},
],
title: [
{
required: true,
message: () => $t("msg.pleaseInputTitle"),
trigger: "blur",
},
],
serviceType: [ serviceType: [
{ required: true, message: "请输入服务类型", trigger: "blur" }, {
required: true,
message: () => $t("msg.pleaseSelectType"),
trigger: "blur",
},
],
priority: [
{
required: true,
message: () => $t("msg.pleaseSelectPriority"),
trigger: "change",
},
], ],
priority: [{ required: true, message: "请选择优先级", trigger: "change" }],
description: [ description: [
{ required: true, message: "请输入详细描述", trigger: "blur" }, {
required: true,
validator: (_, value, callback) => {
const text = (value || "")
.replace(/<[^>]*>/g, "")
.replace(/&nbsp;/gi, "")
.trim();
if (!text) return callback(new Error($t("msg.pleaseInputDescription")));
callback();
},
trigger: "change",
},
], ],
}; };
// -
const editorRef = shallowRef();
const toolbarConfig = {
excludeKeys: [
"uploadImage",
"insertVideo",
"uploadVideo",
"group-video",
"emotion",
"group-emoji",
],
};
const editorConfig = {
placeholder: $t("placeholder.description"),
};
const handleEditorCreated = (editor) => {
editorRef.value = editor;
};
const resetQuickForm = () => { const resetQuickForm = () => {
Object.assign(quickForm, { Object.assign(quickForm, {
hospitalId: undefined, hospitalId: undefined,
@ -333,8 +586,10 @@ const resetQuickForm = () => {
serviceType: "", serviceType: "",
priority: "中", priority: "中",
department: "", department: "",
colorTag: "", colorTag: "#667eea",
description: "", description: "",
submitter: "",
files: [],
}); });
quickFormRef.value?.clearValidate(); quickFormRef.value?.clearValidate();
}; };
@ -352,11 +607,19 @@ const submitQuick = async () => {
if (!valid) return; if (!valid) return;
quickSubmitting.value = true; quickSubmitting.value = true;
try { try {
await createAdminOrder({ const formData = new FormData();
...quickForm, const { files, ...rest } = quickForm;
submitter: userStore.userInfo?.userName, for (const [k, v] of Object.entries(rest)) {
}); if (v !== undefined && v !== null && v !== "") {
ElMessage.success("提交成功"); formData.append(k, v);
}
}
formData.append("registrarName", userStore.userInfo?.userName || "");
for (const f of files || []) {
formData.append("files", f.raw, f.name);
}
await createAdminOrder(formData);
ElMessage.success($t("msg.submitSuccess"));
quickVisible.value = false; quickVisible.value = false;
loadList(); loadList();
} finally { } finally {
@ -368,6 +631,14 @@ const submitQuick = async () => {
onMounted(() => { onMounted(() => {
loadHospitals(); loadHospitals();
loadList(); loadList();
document.addEventListener("click", onDocClick);
});
onBeforeUnmount(() => {
document.removeEventListener("click", onDocClick);
const editor = editorRef.value;
if (editor == null) return;
editor.destroy();
}); });
</script> </script>
@ -380,4 +651,178 @@ onMounted(() => {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
.rich-editor {
border: 1px solid #dcdfe6;
border-radius: 4px;
width: 100%;
overflow: hidden;
background: #fff;
}
.rich-editor :deep(.w-e-toolbar) {
background: #fafafa;
}
// +
.title-row {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
.title-row .el-input {
flex: 1;
}
.color-tag-picker {
position: relative;
flex-shrink: 0;
}
.color-tag-preview {
width: 28px;
height: 28px;
border-radius: 6px;
cursor: pointer;
border: 2px solid #fff;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.color-tag-preview:hover {
transform: scale(1.08);
}
.color-tag-preview.is-empty {
background-image: linear-gradient(
135deg,
#f5f5f5 25%,
transparent 25%,
transparent 50%,
#f5f5f5 50%,
#f5f5f5 75%,
transparent 75%
);
background-size: 8px 8px;
box-shadow: 0 0 0 1px #dcdfe6;
}
.color-tag-dropdown {
position: absolute;
top: calc(100% + 8px);
left: 0;
z-index: 10;
background: #fff;
border-radius: 10px;
padding: 12px 14px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
min-width: 180px;
}
.color-tag-title {
font-size: 13px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
}
.color-tag-options {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.color-tag-option {
width: 24px;
height: 24px;
border-radius: 6px;
cursor: pointer;
border: 2px solid transparent;
transition:
transform 0.2s ease,
border-color 0.2s ease;
}
.color-tag-option:hover {
transform: scale(1.12);
}
.color-tag-option.active {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.color-tag-clear {
background: #f5f5f5 !important;
position: relative;
}
.color-tag-clear::before,
.color-tag-clear::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 14px;
height: 2px;
background: #999;
transform-origin: center;
}
.color-tag-clear::before {
transform: translate(-50%, -50%) rotate(45deg);
}
.color-tag-clear::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
//
.attachment-pick {
width: 100%;
}
.attachment-hidden-input {
display: none;
}
//
.order-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.stat-card {
position: relative;
display: flex;
align-items: center;
gap: 14px;
padding: 18px 20px;
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border-left: 4px solid #d9d9d9;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
cursor: default;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
}
.stat-total {
border-left-color: #1890ff;
}
.stat-pending {
border-left-color: #fa8c16;
}
.stat-completed {
border-left-color: #52c41a;
}
.stat-high {
border-left-color: #ff4d4f;
}
.stat-icon {
font-size: 28px;
line-height: 1;
}
.stat-value {
font-size: 26px;
font-weight: 700;
color: #333;
line-height: 1.1;
}
.stat-label {
font-size: 13px;
color: #999;
margin-top: 4px;
}
</style> </style>

@ -1,44 +1,67 @@
<template> <template>
<div class="process" v-loading="loading"> <div class="process" v-loading="loading">
<el-page-header :icon="ArrowLeft" content="返回" @back="goBack" class="page-header"> <el-page-header :icon="ArrowLeft" :content="$t('common.back')" @back="goBack" class="page-header">
<template #content> <template #content>
<span class="page-title">处理工单</span> <span class="page-title">{{ $t('btn.process') }}</span>
</template> </template>
</el-page-header> </el-page-header>
<el-card shadow="never" v-if="detail.id" class="form-card"> <el-card shadow="never" v-if="detail.id" class="form-card">
<el-descriptions :column="3" border> <template #header>
<el-descriptions-item label="工单号">{{ detail.orderNo }}</el-descriptions-item> <span class="card-title">{{ $t('btn.process') }}</span>
<el-descriptions-item label="医院">{{ detail.hospitalName }}</el-descriptions-item> </template>
<el-descriptions-item label="服务类型">{{ detail.serviceType }}</el-descriptions-item>
<el-descriptions-item label="优先级"> <el-form
<el-tag :type="priorityType(detail.priority)" size="small">{{ detail.priority }}</el-tag> ref="formRef"
</el-descriptions-item> :model="form"
<el-descriptions-item label="状态"> :rules="rules"
<el-tag :type="statusType(detail.status)" size="small">{{ detail.status }}</el-tag> label-width="100px"
</el-descriptions-item> style="max-width: 800px"
<el-descriptions-item label="提交时间">{{ detail.createdAt }}</el-descriptions-item> >
</el-descriptions> <!-- 工单号只读 -->
<el-form-item :label="$t('table.id')">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" style="margin-top: 24px; max-width: 800px"> <el-input :model-value="detail.orderNo" readonly />
<el-form-item label="目标状态" prop="status"> </el-form-item>
<el-radio-group v-model="form.status">
<el-radio-button label="处理中">处理中</el-radio-button> <!-- 问题描述只读富文本展示 -->
<el-radio-button label="已完成">已完成</el-radio-button> <el-form-item :label="$t('label.description')">
<el-radio-button label="已取消">已取消</el-radio-button> <div class="readonly-html" v-html="detail.description || ''" />
</el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="处理备注" prop="processRemark">
<el-input <!-- 处理说明wangEditor 富文本编辑器与提交工单页面一致 -->
<el-form-item :label="$t('common.remark')" prop="processRemark">
<div class="rich-editor">
<Toolbar
:editor="editorRef"
:default-config="toolbarConfig"
mode="default"
style="border-bottom: 1px solid #dcdfe6"
/>
<Editor
v-model="form.processRemark" v-model="form.processRemark"
type="textarea" :default-config="editorConfig"
:rows="6" mode="default"
placeholder="请输入处理备注(支持 HTML" style="height: 200px; overflow-y: auto"
@on-created="handleEditorCreated"
/> />
</div>
</el-form-item>
<!-- 新状态只允许"已完成" -->
<el-form-item :label="$t('table.status')" prop="status">
<el-select v-model="form.status" style="width: 240px">
<el-option :label="$t('status.completed')" value="已完成" />
</el-select>
</el-form-item>
<!-- 处理人取当前登录用户名 -->
<el-form-item :label="$t('table.handler')">
<el-input :model-value="handlerName" readonly style="width: 240px" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :loading="submitting" @click="submit"></el-button> <el-button type="primary" :loading="submitting" @click="submit">{{ $t('btn.confirm') }}</el-button>
<el-button @click="goBack"></el-button> <el-button @click="goBack">{{ $t('common.cancel') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
@ -46,28 +69,73 @@
</template> </template>
<script setup> <script setup>
import { onMounted, reactive, ref } from "vue"; import { computed, onBeforeUnmount, onMounted, reactive, ref, shallowRef } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { ArrowLeft } from "@element-plus/icons-vue"; import { ArrowLeft } from "@element-plus/icons-vue";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import "@wangeditor/editor/dist/css/style.css";
import { useI18n } from "vue-i18n";
import { getAdminOrderDetail, processAdminOrder } from "@/service/modular/admin"; import { getAdminOrderDetail, processAdminOrder } from "@/service/modular/admin";
import { useUserStore } from "@/stores/api/user";
const { t: $t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const userStore = useUserStore();
const loading = ref(false); const loading = ref(false);
const submitting = ref(false); const submitting = ref(false);
const detail = ref({}); const detail = ref({});
const formRef = ref(); const formRef = ref();
const form = reactive({ status: "已完成", processRemark: "" });
//
const handlerName = computed(
() => userStore.userInfo?.name || userStore.userInfo?.userName || userStore.userInfo?.account || '管理员',
);
// ""
const form = reactive({
status: "已完成",
processRemark: "",
});
const rules = { const rules = {
status: [{ required: true, message: "请选择目标状态", trigger: "change" }], status: [{ required: true, message: () => $t('table.status'), trigger: "change" }],
processRemark: [{ required: true, message: "请输入处理备注", trigger: "blur" }], processRemark: [
{
required: true,
validator: (_, value, callback) => {
const text = (value || "")
.replace(/<[^>]*>/g, "")
.replace(/&nbsp;/gi, "")
.trim();
if (!text) return callback(new Error($t('placeholder.remark')));
callback();
},
trigger: "change",
},
],
}; };
const priorityType = (p) => (p === "高" ? "danger" : p === "中" ? "warning" : "info"); // wangEditor
const statusType = (s) => const editorRef = shallowRef();
({ 待处理: "warning", 处理中: "primary", 已完成: "success", 已取消: "info" })[s] || "info"; const toolbarConfig = {
excludeKeys: [
"uploadImage",
"insertVideo",
"uploadVideo",
"group-video",
"emotion",
"group-emoji",
],
};
const editorConfig = {
placeholder: $t('placeholder.remark'),
};
const handleEditorCreated = (editor) => {
editorRef.value = editor;
};
const loadDetail = async () => { const loadDetail = async () => {
loading.value = true; loading.value = true;
@ -79,12 +147,16 @@ const loadDetail = async () => {
}; };
const submit = async () => { const submit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => { await formRef.value.validate(async (valid) => {
if (!valid) return; if (!valid) return;
submitting.value = true; submitting.value = true;
try { try {
await processAdminOrder(route.params.id, form); await processAdminOrder(route.params.id, {
ElMessage.success("处理成功"); status: form.status,
processRemark: form.processRemark,
});
ElMessage.success($t('msg.editSuccess'));
router.replace("/admin/orders"); router.replace("/admin/orders");
} finally { } finally {
submitting.value = false; submitting.value = false;
@ -95,6 +167,12 @@ const submit = async () => {
const goBack = () => router.back(); const goBack = () => router.back();
onMounted(loadDetail); onMounted(loadDetail);
onBeforeUnmount(() => {
const editor = editorRef.value;
if (editor == null) return;
editor.destroy();
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -105,4 +183,38 @@ onMounted(loadDetail);
font-weight: 600; font-weight: 600;
} }
} }
.form-card {
.card-title {
font-size: 16px;
font-weight: 600;
}
}
//
.readonly-html {
width: 100%;
min-height: 60px;
padding: 10px 12px;
background: #fafafa;
border: 1px solid #ebeef5;
border-radius: 4px;
line-height: 1.6;
word-break: break-word;
:deep(img) {
max-width: 100%;
}
}
// wangEditor
.rich-editor {
border: 1px solid #dcdfe6;
border-radius: 4px;
width: 100%;
overflow: hidden;
background: #fff;
}
.rich-editor :deep(.w-e-toolbar) {
background: #fafafa;
}
</style> </style>

@ -2,50 +2,50 @@
<div class="order-report"> <div class="order-report">
<el-card shadow="never"> <el-card shadow="never">
<el-form :inline="true" :model="filters" class="filter-form"> <el-form :inline="true" :model="filters" class="filter-form">
<el-form-item label="报告周期"> <el-form-item :label="$t('report.period')">
<el-select v-model="filters.period" placeholder="请选择" style="width:140px"> <el-select v-model="filters.period" :placeholder="$t('common.pleaseSelect')" style="width:140px">
<el-option label="" value="month" /> <el-option label="月" value="month" />
<el-option label="季度" value="quarter" /> <el-option label="季度" value="quarter" />
<el-option label="" value="year" /> <el-option label="年" value="year" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="医院"> <el-form-item :label="$t('label.hospitalName')">
<el-input v-model="filters.hospital" placeholder="医院名称" clearable style="width:180px" /> <el-input v-model="filters.hospital" :placeholder="$t('placeholder.hospital')" clearable style="width:180px" />
</el-form-item> </el-form-item>
<el-form-item label="提交日期"> <el-form-item :label="$t('table.time')">
<el-date-picker <el-date-picker
v-model="dateRange" v-model="dateRange"
type="daterange" type="daterange"
range-separator="" range-separator="-"
start-placeholder="开始" :start-placeholder="$t('common.startDate')"
end-placeholder="结束" :end-placeholder="$t('common.endDate')"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
style="width:240px" style="width:240px"
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="onSearch"></el-button> <el-button type="primary" @click="onSearch">{{ $t('btn.query') }}</el-button>
<el-button @click="onReset"></el-button> <el-button @click="onReset">{{ $t('btn.reset') }}</el-button>
<el-button type="success" @click="onExport"></el-button> <el-button type="success" @click="onExport">{{ $t('btn.export') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-table :data="list" v-loading="loading" border stripe> <el-table :data="list" v-loading="loading" border stripe>
<el-table-column prop="reportNo" label="报告编号" width="160" /> <el-table-column prop="reportNo" label="No." width="160" />
<el-table-column prop="hospitalName" label="医院名称" min-width="160" show-overflow-tooltip /> <el-table-column :label="$t('table.hospital')" prop="hospitalName" min-width="160" show-overflow-tooltip />
<el-table-column prop="period" label="服务周期" width="120" /> <el-table-column :label="$t('report.period')" prop="period" width="120" />
<el-table-column prop="totalOrders" label="工单总数" width="100" /> <el-table-column :label="$t('stat.totalOrders')" prop="totalOrders" width="100" />
<el-table-column prop="completed" label="已完成" width="100" /> <el-table-column :label="$t('stat.completedOrders')" prop="completed" width="100" />
<el-table-column prop="pending" label="待处理" width="100" /> <el-table-column :label="$t('stat.pendingOrders')" prop="pending" width="100" />
<el-table-column prop="satisfaction" label="满意度" width="100"> <el-table-column :label="$t('stat.completed')" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-rate v-model="row.satisfaction" disabled show-score :colors="['#99A9BF', '#F7BA2A', '#FF9900']" /> <el-rate v-model="row.satisfaction" disabled show-score :colors="['#99A9BF', '#F7BA2A', '#FF9900']" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="createdAt" label="生成时间" width="170" /> <el-table-column :label="$t('common.time')" prop="createdAt" width="170" />
<el-table-column label="操作" width="120" fixed="right"> <el-table-column :label="$t('table.action')" width="120" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" @click="onView(row)"></el-button> <el-button link type="primary" @click="onView(row)">{{ $t('btn.view') }}</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -63,15 +63,17 @@
</div> </div>
</el-card> </el-card>
<el-empty v-if="!loading && list.length === 0" description="暂无服务报告" /> <el-empty v-if="!loading && list.length === 0" :description="$t('report.list')" />
</div> </div>
</template> </template>
<script setup> <script setup>
import { onMounted, reactive, ref } from "vue"; import { onMounted, reactive, ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
const { t: $t } = useI18n();
// TODO: /api/admin/orders/report mock
const MOCK_LIST = [ const MOCK_LIST = [
{ id: 1, reportNo: "RP-2025-001", hospitalName: "上海第一人民医院", period: "2025-Q1", totalOrders: 28, completed: 25, pending: 3, satisfaction: 5, createdAt: "2025-04-05 10:30" }, { id: 1, reportNo: "RP-2025-001", hospitalName: "上海第一人民医院", period: "2025-Q1", totalOrders: 28, completed: 25, pending: 3, satisfaction: 5, createdAt: "2025-04-05 10:30" },
{ id: 2, reportNo: "RP-2025-002", hospitalName: "北京协和医院", period: "2025-Q1", totalOrders: 35, completed: 33, pending: 2, satisfaction: 4, createdAt: "2025-04-06 14:12" }, { id: 2, reportNo: "RP-2025-002", hospitalName: "北京协和医院", period: "2025-Q1", totalOrders: 35, completed: 33, pending: 2, satisfaction: 4, createdAt: "2025-04-06 14:12" },
@ -109,8 +111,8 @@ const onReset = () => {
dateRange.value = []; dateRange.value = [];
onSearch(); onSearch();
}; };
const onExport = () => ElMessage.success("已提交导出任务mock"); const onExport = () => ElMessage.success($t('btn.export'));
const onView = (row) => ElMessage.info(`查看报告${row.reportNo}mock`); const onView = (row) => ElMessage.info(`${$t('btn.view')}${row.reportNo}`);
onMounted(loadList); onMounted(loadList);
</script> </script>

@ -1,33 +1,39 @@
<template> <template>
<div class="page"> <div class="page">
<el-card shadow="never"> <el-card shadow="never">
<template #header><span class="title">修改密码</span></template> <template #header
><span class="title">{{ $t("password.title") }}</span></template
>
<el-form <el-form
ref="formRef" ref="formRef"
:model="form" :model="form"
:rules="rules" :rules="rules"
label-width="100px" label-width="120px"
style="max-width: 480px" style="max-width: 480px"
> >
<el-form-item label="原密码" prop="oldPassword"> <el-form-item :label="$t('password.oldPassword')" prop="oldPassword">
<el-input v-model="form.oldPassword" type="password" show-password /> <el-input v-model="form.oldPassword" type="password" show-password />
</el-form-item> </el-form-item>
<el-form-item label="新密码" prop="newPassword"> <el-form-item :label="$t('password.newPassword')" prop="newPassword">
<el-input v-model="form.newPassword" type="password" show-password /> <el-input v-model="form.newPassword" type="password" show-password />
</el-form-item> </el-form-item>
<el-form-item label="确认密码" prop="confirmPassword"> <el-form-item
<el-input v-model="form.confirmPassword" type="password" show-password /> :label="$t('password.confirmPassword')"
prop="confirmPassword"
>
<el-input
v-model="form.confirmPassword"
type="password"
show-password
/>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :loading="submitting" @click="submit"></el-button> <el-button type="primary" :loading="submitting" @click="submit">{{
<el-button @click="onCancel"></el-button> $t("password.submit")
}}</el-button>
<el-button @click="onCancel">{{ $t("password.cancel") }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-alert
title="修改密码接口暂未在 API 文档中提供,此处预留表单,待后端补全"
type="info"
:closable="false"
/>
</el-card> </el-card>
</div> </div>
</template> </template>
@ -36,37 +42,73 @@
import { reactive, ref } from "vue"; import { reactive, ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
import { changeMyPassword } from "@/service/modular/admin";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const formRef = ref(); const formRef = ref();
const submitting = ref(false); const submitting = ref(false);
const form = reactive({ oldPassword: "", newPassword: "", confirmPassword: "" }); const form = reactive({
oldPassword: "",
newPassword: "",
confirmPassword: "",
});
const rules = { const rules = {
oldPassword: [{ required: true, message: "请输入原密码", trigger: "blur" }], oldPassword: [
{
required: true,
message: () => t("password.pleaseInputOld"),
trigger: "blur",
},
],
newPassword: [ newPassword: [
{ required: true, message: "请输入新密码", trigger: "blur" }, {
{ min: 6, message: "密码长度至少 6 位", trigger: "blur" }, required: true,
message: () => t("password.pleaseInputNew"),
trigger: "blur",
},
{ min: 6, message: () => t("password.passwordTooShort"), trigger: "blur" },
], ],
confirmPassword: [ confirmPassword: [
{ required: true, message: "请再次输入新密码", trigger: "blur" },
{ {
validator: (_, val, cb) => (val === form.newPassword ? cb() : cb(new Error("两次密码不一致"))), required: true,
message: () => t("password.pleaseInputConfirm"),
trigger: "blur",
},
{
validator: (_, val, cb) =>
val === form.newPassword
? cb()
: cb(new Error(t("password.passwordMismatch"))),
trigger: "blur", trigger: "blur",
}, },
], ],
}; };
const submit = async () => { const submit = async () => {
await formRef.value.validate((valid) => { await formRef.value.validate(async (valid) => {
if (!valid) return; if (!valid) return;
submitting.value = true; submitting.value = true;
// TODO: try {
const res = await changeMyPassword({
oldPassword: form.oldPassword,
newPassword: form.newPassword,
});
const status = res.code;
console.log(res);
if (status === 200) {
ElMessage.success(t("password.success"));
setTimeout(() => { setTimeout(() => {
router.replace("/login");
}, 1000);
}
} catch (err) {
console.log(err?.message || t("common.failed"));
} finally {
submitting.value = false; submitting.value = false;
ElMessage.success("密码已修改"); }
router.back();
}, 600);
}); });
}; };

@ -2,43 +2,43 @@
<div class="performance-score"> <div class="performance-score">
<el-card shadow="never"> <el-card shadow="never">
<el-form :inline="true" :model="filters" class="filter-form"> <el-form :inline="true" :model="filters" class="filter-form">
<el-form-item label="客户经理"> <el-form-item :label="$t('table.manager')">
<el-input v-model="filters.name" placeholder="姓名" clearable style="width:160px" /> <el-input v-model="filters.name" :placeholder="$t('placeholder.search')" clearable style="width:160px" />
</el-form-item> </el-form-item>
<el-form-item label="统计周期"> <el-form-item :label="$t('common.time')">
<el-date-picker <el-date-picker
v-model="filters.month" v-model="filters.month"
type="month" type="month"
placeholder="选择月份" :placeholder="$t('common.pleaseSelect')"
value-format="YYYY-MM" value-format="YYYY-MM"
style="width:160px" style="width:160px"
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="onSearch"></el-button> <el-button type="primary" @click="onSearch">{{ $t('btn.query') }}</el-button>
<el-button @click="onReset"></el-button> <el-button @click="onReset">{{ $t('btn.reset') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-table :data="list" v-loading="loading" border stripe> <el-table :data="list" v-loading="loading" border stripe>
<el-table-column prop="rank" label="排名" width="80" /> <el-table-column :label="$t('points.points')" prop="rank" width="80" />
<el-table-column prop="name" label="客户经理" width="120" /> <el-table-column :label="$t('table.manager')" prop="name" width="120" />
<el-table-column prop="department" label="所属部门" min-width="140" show-overflow-tooltip /> <el-table-column :label="$t('label.dept')" prop="department" min-width="140" show-overflow-tooltip />
<el-table-column prop="totalScore" label="累计积分" width="120"> <el-table-column :label="$t('points.title')" prop="totalScore" width="120">
<template #default="{ row }"> <template #default="{ row }">
<span class="score-num">{{ row.totalScore }}</span> <span class="score-num">{{ row.totalScore }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="monthScore" label="本月积分" width="120" /> <el-table-column :label="$t('points.detail')" prop="monthScore" width="120" />
<el-table-column prop="serviceCount" label="服务单数" width="100" /> <el-table-column :label="$t('stat.totalOrders')" prop="serviceCount" width="100" />
<el-table-column prop="satisfaction" label="客户满意度" width="160"> <el-table-column :label="$t('stat.completed')" prop="satisfaction" width="160">
<template #default="{ row }"> <template #default="{ row }">
<el-rate v-model="row.satisfaction" disabled show-score :colors="['#99A9BF', '#F7BA2A', '#FF9900']" /> <el-rate v-model="row.satisfaction" disabled show-score :colors="['#99A9BF', '#F7BA2A', '#FF9900']" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="120" fixed="right"> <el-table-column :label="$t('table.action')" width="120" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" @click="onDetail(row)"></el-button> <el-button link type="primary" @click="onDetail(row)">{{ $t('points.detail') }}</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -61,8 +61,10 @@
<script setup> <script setup>
import { onMounted, reactive, ref } from "vue"; import { onMounted, reactive, ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
const { t: $t } = useI18n();
// TODO: /api/admin/performance/score mock
const MOCK_LIST = [ const MOCK_LIST = [
{ id: 1, rank: 1, name: "张三", department: "华东大区·上海组", totalScore: 2860, monthScore: 320, serviceCount: 28, satisfaction: 5 }, { id: 1, rank: 1, name: "张三", department: "华东大区·上海组", totalScore: 2860, monthScore: 320, serviceCount: 28, satisfaction: 5 },
{ id: 2, rank: 2, name: "李四", department: "华北大区·北京组", totalScore: 2540, monthScore: 285, serviceCount: 33, satisfaction: 4 }, { id: 2, rank: 2, name: "李四", department: "华北大区·北京组", totalScore: 2540, monthScore: 285, serviceCount: 33, satisfaction: 4 },
@ -91,7 +93,7 @@ const loadList = async () => {
const onSearch = () => { page.value = 1; loadList(); }; const onSearch = () => { page.value = 1; loadList(); };
const onReset = () => { filters.name = ""; filters.month = ""; onSearch(); }; const onReset = () => { filters.name = ""; filters.month = ""; onSearch(); };
const onDetail = (row) => ElMessage.info(`查看「${row.name}」的积分明细mock`); const onDetail = (row) => ElMessage.info(`${row.name} - ${$t('points.detail')}`);
onMounted(loadList); onMounted(loadList);
</script> </script>

@ -1,8 +1,8 @@
<template> <template>
<div class="order-detail" v-loading="loading"> <div class="order-detail" v-loading="loading">
<el-page-header :icon="ArrowLeft" content="返回" @back="goBack" class="page-header"> <el-page-header :icon="ArrowLeft" :content="$t('common.back')" @back="goBack" class="page-header">
<template #content> <template #content>
<span class="page-title">工单详情</span> <span class="page-title">{{ $t('modal.detail') }}</span>
</template> </template>
</el-page-header> </el-page-header>
@ -16,22 +16,22 @@
</template> </template>
<el-descriptions :column="3" border> <el-descriptions :column="3" border>
<el-descriptions-item label="工单号">{{ detail.orderNo }}</el-descriptions-item> <el-descriptions-item :label="$t('table.id')">{{ detail.orderNo }}</el-descriptions-item>
<el-descriptions-item label="服务类型">{{ detail.serviceType }}</el-descriptions-item> <el-descriptions-item :label="$t('table.type')">{{ detail.serviceType }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ detail.createdAt }}</el-descriptions-item> <el-descriptions-item :label="$t('table.time')">{{ detail.createdAt }}</el-descriptions-item>
<el-descriptions-item label="提交人">{{ detail.submitter }}</el-descriptions-item> <el-descriptions-item :label="$t('table.submitter')">{{ detail.submitter }}</el-descriptions-item>
<el-descriptions-item label="科室">{{ detail.department }}</el-descriptions-item> <el-descriptions-item :label="$t('label.dept')">{{ detail.department }}</el-descriptions-item>
<el-descriptions-item label="颜色标签">{{ detail.colorTag }}</el-descriptions-item> <el-descriptions-item :label="$t('label.colorTag')">{{ detail.colorTag }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
<div class="section"> <div class="section">
<div class="section-title">详细描述</div> <div class="section-title">{{ $t('label.description') }}</div>
<div class="html-content" v-html="detail.description || ''" /> <div class="html-content" v-html="detail.description || ''" />
</div> </div>
<div class="section"> <div class="section">
<div class="section-title">附件 ({{ (detail.attachments || []).length }})</div> <div class="section-title">{{ $t('label.attachment') }} ({{ (detail.attachments || []).length }})</div>
<el-empty v-if="!detail.attachments?.length" description="暂无附件" :image-size="80" /> <el-empty v-if="!detail.attachments?.length" :description="$t('orderDetail.noAttachment')" :image-size="80" />
<div v-else class="files"> <div v-else class="files">
<el-link <el-link
v-for="(f, i) in detail.attachments" v-for="(f, i) in detail.attachments"
@ -47,7 +47,7 @@
</div> </div>
<div class="section"> <div class="section">
<div class="section-title">流转记录</div> <div class="section-title">{{ $t('orderDetail.statusChange') }}</div>
<el-timeline> <el-timeline>
<el-timeline-item <el-timeline-item
v-for="(log, i) in detail.logs || []" v-for="(log, i) in detail.logs || []"

@ -3,51 +3,51 @@
<el-card shadow="never"> <el-card shadow="never">
<!-- 筛选区 --> <!-- 筛选区 -->
<el-form :inline="true" :model="filters" class="filter-form"> <el-form :inline="true" :model="filters" class="filter-form">
<el-form-item label="状态"> <el-form-item :label="$t('table.status')">
<el-select v-model="filters.status" placeholder="全部" clearable style="width:140px"> <el-select v-model="filters.status" :placeholder="$t('common.all')" clearable style="width:140px">
<el-option label="待处理" value="待处理" /> <el-option label="待处理" value="待处理" />
<el-option label="处理中" value="处理中" /> <el-option label="处理中" value="处理中" />
<el-option label="已完成" value="已完成" /> <el-option label="已完成" value="已完成" />
<el-option label="已取消" value="已取消" /> <el-option label="已取消" value="已取消" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="优先级"> <el-form-item :label="$t('table.priority')">
<el-select v-model="filters.priority" placeholder="全部" clearable style="width:120px"> <el-select v-model="filters.priority" :placeholder="$t('common.all')" clearable style="width:120px">
<el-option label="高" value="高" /> <el-option label="高" value="高" />
<el-option label="中" value="中" /> <el-option label="中" value="中" />
<el-option label="低" value="低" /> <el-option label="低" value="低" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="关键词"> <el-form-item :label="$t('placeholder.keyword')">
<el-input v-model="filters.keyword" placeholder="搜索标题/描述" clearable style="width:200px" /> <el-input v-model="filters.keyword" :placeholder="$t('placeholder.keyword')" clearable style="width:200px" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="onSearch"></el-button> <el-button type="primary" @click="onSearch">{{ $t('btn.query') }}</el-button>
<el-button @click="onReset"></el-button> <el-button @click="onReset">{{ $t('btn.reset') }}</el-button>
<el-button type="success" @click="goQuick"></el-button> <el-button type="success" @click="goQuick">{{ $t('btn.quickSubmit') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<!-- 表格 --> <!-- 表格 -->
<el-table :data="list" v-loading="loading" border stripe> <el-table :data="list" v-loading="loading" border stripe>
<el-table-column prop="orderNo" label="工单号" width="160" /> <el-table-column :label="$t('table.id')" prop="orderNo" width="160" />
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip /> <el-table-column :label="$t('table.title')" prop="title" min-width="200" show-overflow-tooltip />
<el-table-column prop="serviceType" label="服务类型" width="120" /> <el-table-column :label="$t('table.type')" prop="serviceType" width="120" />
<el-table-column label="优先级" width="90"> <el-table-column :label="$t('table.priority')" width="90">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="priorityType(row.priority)" size="small">{{ row.priority }}</el-tag> <el-tag :type="priorityType(row.priority)" size="small">{{ row.priority }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="状态" width="100"> <el-table-column :label="$t('table.status')" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="statusType(row.status)" size="small">{{ row.status }}</el-tag> <el-tag :type="statusType(row.status)" size="small">{{ row.status }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="createdAt" label="提交时间" width="170" /> <el-table-column :label="$t('table.time')" prop="createdAt" width="170" />
<el-table-column label="操作" width="120" fixed="right"> <el-table-column :label="$t('table.action')" width="120" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" @click="goDetail(row.id)"></el-button> <el-button link type="primary" @click="goDetail(row.id)">{{ $t('btn.detail') }}</el-button>
<el-button link type="danger" v-if="row.status === '待处理'" @click="cancel(row)"></el-button> <el-button link type="danger" v-if="row.status === '待处理'" @click="cancel(row)">{{ $t('btn.cancel') }}</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -71,8 +71,10 @@
import { onMounted, reactive, ref } from "vue"; import { onMounted, reactive, ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { useI18n } from "vue-i18n";
import { getHospitalOrders, cancelHospitalOrder } from "@/service/modular/hospital"; import { getHospitalOrders, cancelHospitalOrder } from "@/service/modular/hospital";
const { t: $t } = useI18n();
const router = useRouter(); const router = useRouter();
const list = ref([]); const list = ref([]);
const total = ref(0); const total = ref(0);
@ -121,13 +123,13 @@ const goQuick = () => router.push("/hospital/orders/quick");
const cancel = async (row) => { const cancel = async (row) => {
try { try {
await ElMessageBox.confirm(`确定取消工单「${row.title}」吗?`, "提示", { await ElMessageBox.confirm(`${$t('msg.confirmCancelOrder').replace('{orderId}', row.title)}`, $t('common.confirm'), {
type: "warning", type: "warning",
confirmButtonText: "确定", confirmButtonText: $t('common.confirm'),
cancelButtonText: "再想想", cancelButtonText: $t('common.cancel'),
}); });
await cancelHospitalOrder(row.id); await cancelHospitalOrder(row.id);
ElMessage.success("已取消"); ElMessage.success($t('msg.cancelSuccess'));
loadList(); loadList();
} catch (_) { } catch (_) {
/* canceled */ /* canceled */

@ -2,38 +2,38 @@
<div class="quick-submit"> <div class="quick-submit">
<el-card shadow="never"> <el-card shadow="never">
<template #header> <template #header>
<span class="card-title">快速提交工单</span> <span class="card-title">{{ $t('modal.submit') }}</span>
</template> </template>
<el-form <el-form
ref="formRef" ref="formRef"
:model="form" :model="form"
:rules="rules" :rules="rules"
label-width="100px" label-width="120px"
style="max-width: 720px" style="max-width: 720px"
> >
<el-form-item label="标题" prop="title"> <el-form-item :label="$t('label.title')" prop="title">
<el-input v-model="form.title" placeholder="请输入工单标题" maxlength="100" show-word-limit /> <el-input v-model="form.title" :placeholder="$t('placeholder.title')" maxlength="100" show-word-limit />
</el-form-item> </el-form-item>
<el-form-item label="服务类型" prop="serviceType"> <el-form-item :label="$t('label.type')" prop="serviceType">
<el-select v-model="form.serviceType" placeholder="请选择" style="width: 240px"> <el-select v-model="form.serviceType" :placeholder="$t('placeholder.type')" style="width: 240px">
<el-option label="故障报修" value="故障报修" /> <el-option label="故障报修" value="故障报修" />
<el-option label="业务咨询" value="业务咨询" /> <el-option label="业务咨询" value="业务咨询" />
<el-option label="需求变更" value="需求变更" /> <el-option label="需求变更" value="需求变更" />
<el-option label="其他" value="其他" /> <el-option label="其他" value="其他" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="优先级" prop="priority"> <el-form-item :label="$t('label.priority')" prop="priority">
<el-radio-group v-model="form.priority"> <el-radio-group v-model="form.priority">
<el-radio-button label="高"></el-radio-button> <el-radio-button label="高"></el-radio-button>
<el-radio-button label="中"></el-radio-button> <el-radio-button label="中"></el-radio-button>
<el-radio-button label="低"></el-radio-button> <el-radio-button label="低"></el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="科室"> <el-form-item :label="$t('label.dept')">
<el-input v-model="form.department" placeholder="请输入科室" /> <el-input v-model="form.department" :placeholder="$t('placeholder.dept')" />
</el-form-item> </el-form-item>
<el-form-item label="颜色标签"> <el-form-item :label="$t('label.colorTag')">
<el-radio-group v-model="form.colorTag"> <el-radio-group v-model="form.colorTag">
<el-radio-button label=""></el-radio-button> <el-radio-button label=""></el-radio-button>
<el-radio-button label="red"><span style="color:#F4664A"></span></el-radio-button> <el-radio-button label="red"><span style="color:#F4664A"></span></el-radio-button>
@ -41,17 +41,17 @@
<el-radio-button label="blue"><span style="color:#5B8FF9"></span></el-radio-button> <el-radio-button label="blue"><span style="color:#5B8FF9"></span></el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="详细描述" prop="description"> <el-form-item :label="$t('label.description')" prop="description">
<el-input <el-input
v-model="form.description" v-model="form.description"
type="textarea" type="textarea"
:rows="5" :rows="5"
placeholder="请详细描述问题(支持简单 HTML" :placeholder="$t('placeholder.description')"
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :loading="submitting" @click="submit"></el-button> <el-button type="primary" :loading="submitting" @click="submit">{{ $t('common.submit') }}</el-button>
<el-button @click="onCancel"></el-button> <el-button @click="onCancel">{{ $t('common.cancel') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
@ -62,8 +62,10 @@
import { reactive, ref } from "vue"; import { reactive, ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
import { createHospitalOrder } from "@/service/modular/hospital"; import { createHospitalOrder } from "@/service/modular/hospital";
const { t: $t } = useI18n();
const router = useRouter(); const router = useRouter();
const formRef = ref(); const formRef = ref();
const submitting = ref(false); const submitting = ref(false);
@ -78,10 +80,10 @@ const form = reactive({
}); });
const rules = { const rules = {
title: [{ required: true, message: "请输入标题", trigger: "blur" }], title: [{ required: true, message: () => $t('msg.pleaseInputTitle'), trigger: "blur" }],
serviceType: [{ required: true, message: "请选择服务类型", trigger: "change" }], serviceType: [{ required: true, message: () => $t('msg.pleaseSelectType'), trigger: "change" }],
priority: [{ required: true, message: "请选择优先级", trigger: "change" }], priority: [{ required: true, message: () => $t('msg.pleaseSelectPriority'), trigger: "change" }],
description: [{ required: true, message: "请输入详细描述", trigger: "blur" }], description: [{ required: true, message: () => $t('msg.pleaseInputDescription'), trigger: "blur" }],
}; };
const submit = async () => { const submit = async () => {
@ -90,7 +92,7 @@ const submit = async () => {
submitting.value = true; submitting.value = true;
try { try {
await createHospitalOrder(form); await createHospitalOrder(form);
ElMessage.success("提交成功"); ElMessage.success($t('msg.submitSuccess'));
router.replace("/hospital/orders"); router.replace("/hospital/orders");
} finally { } finally {
submitting.value = false; submitting.value = false;

@ -1,33 +1,28 @@
<template> <template>
<div class="page"> <div class="page">
<el-card shadow="never"> <el-card shadow="never">
<template #header><span class="title">修改密码</span></template> <template #header><span class="title">{{ $t('password.title') }}</span></template>
<el-form <el-form
ref="formRef" ref="formRef"
:model="form" :model="form"
:rules="rules" :rules="rules"
label-width="100px" label-width="120px"
style="max-width: 480px" style="max-width: 480px"
> >
<el-form-item label="原密码" prop="oldPassword"> <el-form-item :label="$t('password.oldPassword')" prop="oldPassword">
<el-input v-model="form.oldPassword" type="password" show-password /> <el-input v-model="form.oldPassword" type="password" show-password />
</el-form-item> </el-form-item>
<el-form-item label="新密码" prop="newPassword"> <el-form-item :label="$t('password.newPassword')" prop="newPassword">
<el-input v-model="form.newPassword" type="password" show-password /> <el-input v-model="form.newPassword" type="password" show-password />
</el-form-item> </el-form-item>
<el-form-item label="确认密码" prop="confirmPassword"> <el-form-item :label="$t('password.confirmPassword')" prop="confirmPassword">
<el-input v-model="form.confirmPassword" type="password" show-password /> <el-input v-model="form.confirmPassword" type="password" show-password />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" :loading="submitting" @click="submit"></el-button> <el-button type="primary" :loading="submitting" @click="submit">{{ $t('password.submit') }}</el-button>
<el-button @click="onCancel"></el-button> <el-button @click="onCancel">{{ $t('password.cancel') }}</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-alert
title="修改密码接口暂未在 API 文档中提供,此处预留表单,待后端补全"
type="info"
:closable="false"
/>
</el-card> </el-card>
</div> </div>
</template> </template>
@ -36,22 +31,24 @@
import { reactive, ref } from "vue"; import { reactive, ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const formRef = ref(); const formRef = ref();
const submitting = ref(false); const submitting = ref(false);
const form = reactive({ oldPassword: "", newPassword: "", confirmPassword: "" }); const form = reactive({ oldPassword: "", newPassword: "", confirmPassword: "" });
const rules = { const rules = {
oldPassword: [{ required: true, message: "请输入原密码", trigger: "blur" }], oldPassword: [{ required: true, message: () => t('password.pleaseInputOld'), trigger: "blur" }],
newPassword: [ newPassword: [
{ required: true, message: "请输入新密码", trigger: "blur" }, { required: true, message: () => t('password.pleaseInputNew'), trigger: "blur" },
{ min: 6, message: "密码长度至少 6 位", trigger: "blur" }, { min: 6, message: () => t('password.passwordTooShort'), trigger: "blur" },
], ],
confirmPassword: [ confirmPassword: [
{ required: true, message: "请再次输入新密码", trigger: "blur" }, { required: true, message: () => t('password.pleaseInputConfirm'), trigger: "blur" },
{ {
validator: (_, val, cb) => (val === form.newPassword ? cb() : cb(new Error("两次密码不一致"))), validator: (_, val, cb) => (val === form.newPassword ? cb() : cb(new Error(t('password.passwordMismatch')))),
trigger: "blur", trigger: "blur",
}, },
], ],
@ -61,10 +58,9 @@ const submit = async () => {
await formRef.value.validate((valid) => { await formRef.value.validate((valid) => {
if (!valid) return; if (!valid) return;
submitting.value = true; submitting.value = true;
// TODO:
setTimeout(() => { setTimeout(() => {
submitting.value = false; submitting.value = false;
ElMessage.success("密码已修改"); ElMessage.success(t('password.success'));
router.back(); router.back();
}, 600); }, 600);
}); });

@ -4,30 +4,30 @@
<el-card shadow="never" class="summary-card"> <el-card shadow="never" class="summary-card">
<div class="summary-item"> <div class="summary-item">
<div class="num">{{ summary.totalPoints ?? 0 }}</div> <div class="num">{{ summary.totalPoints ?? 0 }}</div>
<div class="lbl">累计积分</div> <div class="lbl">{{ $t('points.title') }}</div>
</div> </div>
<div class="summary-divider" /> <div class="summary-divider" />
<div class="summary-item"> <div class="summary-item">
<div class="num">{{ summary.rewardCount ?? 0 }}</div> <div class="num">{{ summary.rewardCount ?? 0 }}</div>
<div class="lbl">奖励次数</div> <div class="lbl">{{ $t('points.rewards') }}</div>
</div> </div>
</el-card> </el-card>
</div> </div>
<el-card shadow="never" class="detail-card"> <el-card shadow="never" class="detail-card">
<template #header><span class="title">积分明细</span></template> <template #header><span class="title">{{ $t('points.detail') }}</span></template>
<el-table :data="details" v-loading="loading" border stripe> <el-table :data="details" v-loading="loading" border stripe>
<el-table-column prop="source" label="来源" width="140" /> <el-table-column :label="$t('points.pointsSource')" prop="source" width="140" />
<el-table-column prop="orderNo" label="关联工单" width="160" /> <el-table-column :label="$t('points.pointsOrder')" prop="orderNo" width="160" />
<el-table-column label="积分变更" width="120"> <el-table-column :label="$t('points.pointsChange')" width="120">
<template #default="{ row }"> <template #default="{ row }">
<span :class="['change', row.changeAmount >= 0 ? 'plus' : 'minus']"> <span :class="['change', row.changeAmount >= 0 ? 'plus' : 'minus']">
{{ row.changeAmount >= 0 ? "+" : "" }}{{ row.changeAmount }} {{ row.changeAmount >= 0 ? "+" : "" }}{{ row.changeAmount }}
</span> </span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="description" label="说明" min-width="200" /> <el-table-column :label="$t('points.pointsDesc')" prop="description" min-width="200" />
<el-table-column prop="createdAt" label="时间" width="170" /> <el-table-column :label="$t('points.pointsTime')" prop="createdAt" width="170" />
</el-table> </el-table>
<div class="pagination"> <div class="pagination">

@ -3,19 +3,19 @@
<!-- 时间筛选 --> <!-- 时间筛选 -->
<div class="filter-bar"> <div class="filter-bar">
<el-radio-group v-model="period" size="small" @change="loadStats"> <el-radio-group v-model="period" size="small" @change="loadStats">
<el-radio-button label="day">今日</el-radio-button> <el-radio-button label="day">{{ $t('common.today') }}</el-radio-button>
<el-radio-button label="week">本周</el-radio-button> <el-radio-button label="week">{{ $t('common.thisWeek') }}</el-radio-button>
<el-radio-button label="month">本月</el-radio-button> <el-radio-button label="month">{{ $t('common.thisMonth') }}</el-radio-button>
<el-radio-button label="year">本年</el-radio-button> <el-radio-button label="year">{{ $t('common.thisYear') }}</el-radio-button>
<el-radio-button label="custom">自定义</el-radio-button> <el-radio-button label="custom">{{ $t('common.customDate') }}</el-radio-button>
</el-radio-group> </el-radio-group>
<el-date-picker <el-date-picker
v-if="period === 'custom'" v-if="period === 'custom'"
v-model="dateRange" v-model="dateRange"
type="daterange" type="daterange"
range-separator="" range-separator="-"
start-placeholder="开始日期" :start-placeholder="$t('common.startDate')"
end-placeholder="结束日期" :end-placeholder="$t('common.endDate')"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
size="small" size="small"
style="margin-left: 12px" style="margin-left: 12px"
@ -40,10 +40,10 @@
<el-card class="trend-card" shadow="never"> <el-card class="trend-card" shadow="never">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>工单趋势</span> <span>{{ $t('chart.title') }}</span>
<div class="legend"> <div class="legend">
<span class="legend-item"><i class="dot" style="background:#5B8FF9" />提交工单</span> <span class="legend-item"><i class="dot" style="background:#5B8FF9" />{{ $t('chart.submit') }}</span>
<span class="legend-item"><i class="dot" style="background:#5AD8A6" />完成工单</span> <span class="legend-item"><i class="dot" style="background:#5AD8A6" />{{ $t('chart.complete') }}</span>
</div> </div>
</div> </div>
</template> </template>
@ -65,10 +65,13 @@ import {
LegendComponent, LegendComponent,
TitleComponent, TitleComponent,
} from "echarts/components"; } from "echarts/components";
import { useI18n } from "vue-i18n";
import { getWorkbench } from "@/service/modular/hospital"; import { getWorkbench } from "@/service/modular/hospital";
use([CanvasRenderer, LineChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent]); use([CanvasRenderer, LineChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent]);
const { t: $t } = useI18n();
const period = ref("month"); const period = ref("month");
const dateRange = ref([]); const dateRange = ref([]);
@ -82,28 +85,28 @@ const stats = ref({
const cards = computed(() => [ const cards = computed(() => [
{ {
label: "累计提交工单", label: $t('stat.total'),
value: stats.value.totalOrders, value: stats.value.totalOrders,
icon: Tickets, icon: Tickets,
color: "#5B8FF9", color: "#5B8FF9",
bg: "rgba(91,143,249,0.12)", bg: "rgba(91,143,249,0.12)",
}, },
{ {
label: "已完成工单", label: $t('stat.completed'),
value: stats.value.completedOrders, value: stats.value.completedOrders,
icon: Check, icon: Check,
color: "#5AD8A6", color: "#5AD8A6",
bg: "rgba(90,216,166,0.12)", bg: "rgba(90,216,166,0.12)",
}, },
{ {
label: "待处理工单", label: $t('stat.pending'),
value: stats.value.pendingOrders, value: stats.value.pendingOrders,
icon: Bell, icon: Bell,
color: "#F6BD16", color: "#F6BD16",
bg: "rgba(246,189,22,0.12)", bg: "rgba(246,189,22,0.12)",
}, },
{ {
label: "高优先级工单", label: $t('stat.highPriority'),
value: stats.value.highPriorityOrders, value: stats.value.highPriorityOrders,
icon: Warning, icon: Warning,
color: "#F4664A", color: "#F4664A",
@ -123,7 +126,7 @@ const chartOption = computed(() => {
yAxis: { type: "value", axisLine: { show: false }, splitLine: { lineStyle: { color: "#f0f2f5" } } }, yAxis: { type: "value", axisLine: { show: false }, splitLine: { lineStyle: { color: "#f0f2f5" } } },
series: [ series: [
{ {
name: "提交工单", name: $t('chart.submit'),
type: "line", type: "line",
smooth: true, smooth: true,
symbol: "circle", symbol: "circle",
@ -134,7 +137,7 @@ const chartOption = computed(() => {
areaStyle: { color: "rgba(91,143,249,0.08)" }, areaStyle: { color: "rgba(91,143,249,0.08)" },
}, },
{ {
name: "完成工单", name: $t('chart.complete'),
type: "line", type: "line",
smooth: true, smooth: true,
symbol: "circle", symbol: "circle",

@ -1,18 +1,14 @@
<template> <template>
<div class="login-page"> <div class="login-page">
<div class="login-bg" /> <!-- 右上角语言切换按钮 -->
<div class="login-lang-switch">
<i18nSwitch />
</div>
<div class="login-wrapper"> <div class="login-wrapper">
<div class="login-header"> <div class="login-header">{{ $t("login.headerTitle") }}</div>
<div class="logo">
<img src="@/assets/image/login/KcLogo.png" alt="" />
</div>
<div class="sys-name">康策 CSM 系统</div>
</div>
<div class="login-card"> <div class="login-card">
<div class="login-title">账号登录</div>
<el-form <el-form
ref="formRef" ref="formRef"
:model="form" :model="form"
@ -23,24 +19,31 @@
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input
v-model="form.username" v-model="form.username"
placeholder="请输入账号" :placeholder="$t('login.userName')"
size="large" size="large"
:prefix-icon="User" >
/> <template #prefix>
<SvgIcon name="login-user" :size="28" />
</template>
</el-input>
</el-form-item> </el-form-item>
<el-form-item prop="password"> <el-form-item prop="password">
<el-input <el-input
v-model="form.password" v-model="form.password"
type="password" type="password"
show-password :placeholder="$t('login.passWord')"
placeholder="请输入密码"
size="large" size="large"
:prefix-icon="Lock" >
/> <template #prefix>
<SvgIcon name="login-password" :size="28" />
</template>
</el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-checkbox v-model="remember"></el-checkbox> <el-checkbox v-model="remember">{{
$t("login.jizhu")
}}</el-checkbox>
</el-form-item> </el-form-item>
<el-button <el-button
@ -50,12 +53,13 @@
size="large" size="large"
@click="submit" @click="submit"
> >
{{ $t("login.denglu") }}
</el-button> </el-button>
</el-form> </el-form>
<div class="login-footer"> <div class="login-footer">
上海康策软件有限公司 版权所有 服务热线021-60713139 {{ $t("login.copyright") }} <br />
{{ $t("login.hotline") }}
</div> </div>
</div> </div>
</div> </div>
@ -66,15 +70,17 @@
import { onMounted, reactive, ref } from "vue"; import { onMounted, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { User, Lock } from "@element-plus/icons-vue";
import { adminLogin } from "@/service/modular/login"; import { adminLogin } from "@/service/modular/login";
import { useUserStore } from "@/stores/api/user"; import { useUserStore } from "@/stores/api/user";
import SvgIcon from "@/components/SvgIcon/index.vue";
import i18nSwitch from "@/components/i18nSwitch/index.vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const userStore = useUserStore(); const userStore = useUserStore();
// userType
const HOME_BY_TYPE = { const HOME_BY_TYPE = {
Admin: "/admin/orders", Admin: "/admin/orders",
Hospital: "/hospital/workbench", Hospital: "/hospital/workbench",
@ -87,8 +93,20 @@ const remember = ref(false);
const form = reactive({ username: "", password: "" }); const form = reactive({ username: "", password: "" });
const rules = { const rules = {
username: [{ required: true, message: "请输入账号", trigger: "blur" }], username: [
password: [{ required: true, message: "请输入密码", trigger: "blur" }], {
required: true,
message: () => t("login.pleaseEnterUserName"),
trigger: "blur",
},
],
password: [
{
required: true,
message: () => t("login.pleaseEnterPassWord"),
trigger: "blur",
},
],
}; };
const submit = async () => { const submit = async () => {
@ -106,7 +124,7 @@ const submit = async () => {
const home = HOME_BY_TYPE[userType]; const home = HOME_BY_TYPE[userType];
if (!home) { if (!home) {
ElMessage.error("未知用户类型,无法登录"); ElMessage.error(t("login.unknownUserType"));
return; return;
} }
@ -129,7 +147,7 @@ const submit = async () => {
localStorage.removeItem("csm_remember_account"); localStorage.removeItem("csm_remember_account");
} }
ElMessage.success("登录成功"); ElMessage.success(t("login.loginSuccess"));
router.replace(route.query.redirect || home); router.replace(route.query.redirect || home);
} catch (e) { } catch (e) {
// request ElMessage.error // request ElMessage.error
@ -153,50 +171,44 @@ onMounted(() => {
position: relative; position: relative;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background: linear-gradient(135deg, #e8edff 0%, #d6e0ff 100%); background-color: #b3defb;
background-image: url("@/assets/image/login/bg.gif");
background-position: bottom center;
background-size: 100% auto;
background-repeat: no-repeat;
overflow: hidden; overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
} }
.login-bg { .login-lang-switch {
position: absolute; position: absolute;
inset: 0; top: 64px;
background: url("@/assets/image/login/bg-login.png") center/cover no-repeat; right: 64px;
opacity: 0.55; z-index: 10;
} }
.login-wrapper { .login-wrapper {
position: relative; position: relative;
width: 100%;
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; background: rgba(255, 255, 255, 0.6);
border-radius: 24px 24px 24px 24px;
border: 1px solid #fff;
padding: 47px 50px 32px;
--el-color-primary: #2b7afa;
} }
.login-header { .login-header {
position: absolute; font-weight: bold;
top: 32px; font-size: 32px;
left: 64px; color: #2b7afa;
display: flex; margin-bottom: 48px;
align-items: center;
gap: 12px;
.logo img {
height: 44px;
}
.sys-name {
color: #303133;
font-size: 18px;
font-weight: 600;
}
} }
.login-card { .login-card {
width: 420px;
background: #fff;
border-radius: 8px;
padding: 36px 40px 24px;
box-shadow: 0 6px 32px rgba(60, 80, 160, 0.12);
} }
.login-title { .login-title {
@ -207,30 +219,57 @@ onMounted(() => {
margin-bottom: 20px; margin-bottom: 20px;
} }
.login-form { :deep(.login-form) {
:deep(.el-input__wrapper) { .el-form-item {
height: 44px; margin-bottom: 24px;
width: 368px;
}
.el-input__wrapper {
height: 56px;
background: #fafafa; background: #fafafa;
border-radius: 12px 12px 12px 12px;
border: 1px solid #e7e9ed;
&:hover {
border: 1px solid #97c2fd;
}
input {
font-weight: 400;
font-size: 16px;
caret-color: #2b7afa;
&::placeholder {
color: #bbbbbb;
}
} }
:deep(.el-form-item) {
margin-bottom: 18px;
} }
} }
.submit-btn { .submit-btn {
width: 100%; width: 368px;
height: 44px; height: 56px;
font-size: 16px; font-weight: bold;
font-weight: 600; font-size: 18px;
background: linear-gradient(135deg, #8b9ff5, #6c7cf3); color: #ffffff;
border-radius: 12px 12px 12px 12px;
background: linear-gradient(to right, #2b7afa, #56d2ff);
border: none; border: none;
letter-spacing: 4px; // letter-spacing: 0.8em;
&:hover {
background: linear-gradient(to right, #5695fb, #77d9ff);
}
&:active {
background: linear-gradient(to right, #2870e1, #4dbbe5);
}
} }
.login-footer { .login-footer {
margin-top: 18px; font-size: 14px;
color: #697589;
text-align: center; text-align: center;
color: #909399; margin-top: 50px;
font-size: 12px;
} }
</style> </style>

Loading…
Cancel
Save