最新修改,初版
This commit is contained in:
314
package-lock.json
generated
314
package-lock.json
generated
@@ -12,7 +12,7 @@
|
||||
"axios": "^1.13.2",
|
||||
"element-plus": "^2.11.8",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^5.0.2"
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
@@ -83,6 +83,7 @@
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz",
|
||||
"integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.0",
|
||||
@@ -966,6 +967,7 @@
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -976,6 +978,7 @@
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
@@ -986,6 +989,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -1001,6 +1005,7 @@
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
@@ -1385,33 +1390,6 @@
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue-macros/common": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz",
|
||||
"integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-sfc": "^3.5.22",
|
||||
"ast-kit": "^2.1.2",
|
||||
"local-pkg": "^1.1.2",
|
||||
"magic-string-ast": "^1.0.2",
|
||||
"unplugin-utils": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/vue-macros"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^2.7.0 || ^3.2.25"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/babel-helper-vue-transform-on": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz",
|
||||
@@ -1516,13 +1494,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.5.tgz",
|
||||
"integrity": "sha512-DgVcW8H/Nral7LgZEecYFFYXnAvGuN9C3L3DtWekAncFBedBczpNW8iHKExfaM559Zm8wQWrwtYZ9lXthEHtDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-kit": "^8.0.5"
|
||||
}
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/devtools-core": {
|
||||
"version": "8.0.5",
|
||||
@@ -1565,6 +1540,7 @@
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.5.tgz",
|
||||
"integrity": "sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-shared": "^8.0.5",
|
||||
@@ -1580,6 +1556,7 @@
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.5.tgz",
|
||||
"integrity": "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rfdc": "^1.4.1"
|
||||
@@ -1723,18 +1700,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansis": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz",
|
||||
@@ -1745,38 +1710,6 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-kit": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
|
||||
"integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sxzz"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-walker-scope": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz",
|
||||
"integrity": "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.4",
|
||||
"ast-kit": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sxzz"
|
||||
}
|
||||
},
|
||||
"node_modules/async-validator": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||
@@ -1814,6 +1747,7 @@
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.8.0.tgz",
|
||||
"integrity": "sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
@@ -1903,21 +1837,6 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -1930,12 +1849,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/confbox": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
|
||||
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -1947,6 +1860,7 @@
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
|
||||
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-what": "^5.2.0"
|
||||
@@ -2211,16 +2125,11 @@
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/exsolve": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
@@ -2396,6 +2305,7 @@
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
@@ -2437,6 +2347,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
|
||||
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -2472,6 +2383,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
@@ -2484,6 +2396,7 @@
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
@@ -2499,23 +2412,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/local-pkg": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
|
||||
"integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mlly": "^1.7.4",
|
||||
"pkg-types": "^2.3.0",
|
||||
"quansync": "^0.2.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
@@ -2558,21 +2454,6 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string-ast": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz",
|
||||
"integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"magic-string": "^0.30.19"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sxzz"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -2613,37 +2494,9 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mlly": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
|
||||
"integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.15.0",
|
||||
"pathe": "^2.0.3",
|
||||
"pkg-types": "^1.3.1",
|
||||
"ufo": "^1.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mlly/node_modules/confbox": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
|
||||
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mlly/node_modules/pkg-types": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
|
||||
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.1.8",
|
||||
"mlly": "^1.7.4",
|
||||
"pathe": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mrmime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||
@@ -2661,12 +2514,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/muggle-string": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
|
||||
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -2728,12 +2575,14 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/perfect-debounce": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz",
|
||||
"integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
@@ -2746,6 +2595,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -2754,17 +2604,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.2.2",
|
||||
"exsolve": "^1.0.7",
|
||||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
@@ -2799,39 +2638,11 @@
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quansync": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||
"integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/sxzz"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/rfdc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
@@ -2889,12 +2700,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/scule": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
|
||||
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
@@ -2933,6 +2738,7 @@
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
|
||||
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -2942,6 +2748,7 @@
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz",
|
||||
"integrity": "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"copy-anything": "^4"
|
||||
@@ -2954,6 +2761,7 @@
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
@@ -2976,30 +2784,11 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/ufo": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
|
||||
"integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unplugin": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz",
|
||||
"integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"picomatch": "^4.0.3",
|
||||
"webpack-virtual-modules": "^0.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin-utils": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
|
||||
"integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pathe": "^2.0.3",
|
||||
@@ -3244,56 +3033,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.2.tgz",
|
||||
"integrity": "sha512-YFhwaE5c5JcJpNB1arpkl4/GnO32wiUWRB+OEj1T0DlDxEZoOfbltl2xEwktNU/9o1sGcGburIXSpbLpPFe/6w==",
|
||||
"version": "4.6.4",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
|
||||
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.28.6",
|
||||
"@vue-macros/common": "^3.1.1",
|
||||
"@vue/devtools-api": "^8.0.0",
|
||||
"ast-walker-scope": "^0.8.3",
|
||||
"chokidar": "^5.0.0",
|
||||
"json5": "^2.2.3",
|
||||
"local-pkg": "^1.1.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"mlly": "^1.8.0",
|
||||
"muggle-string": "^0.4.1",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.3",
|
||||
"scule": "^1.3.0",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"unplugin": "^3.0.0",
|
||||
"unplugin-utils": "^0.3.1",
|
||||
"yaml": "^2.8.2"
|
||||
"@vue/devtools-api": "^6.6.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pinia/colada": ">=0.21.2",
|
||||
"@vue/compiler-sfc": "^3.5.17",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@pinia/colada": {
|
||||
"optional": true
|
||||
},
|
||||
"@vue/compiler-sfc": {
|
||||
"optional": true
|
||||
},
|
||||
"pinia": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-virtual-modules": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
|
||||
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wsl-utils": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
||||
@@ -3321,7 +3074,10 @@
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"axios": "^1.13.2",
|
||||
"element-plus": "^2.11.8",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^5.0.2"
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
|
||||
@@ -42,5 +42,29 @@ export const ticketApi = {
|
||||
// 获取运营人员列表
|
||||
getOperators() {
|
||||
return request.get('/operators')
|
||||
},
|
||||
// 获取员工列表
|
||||
getStaffs(params){
|
||||
return request.get('/getStaffList', { params })
|
||||
}
|
||||
}
|
||||
|
||||
export const customerApi = {
|
||||
getCustomers(){
|
||||
return request.get('/customers')
|
||||
},
|
||||
|
||||
|
||||
// 添加客户,data 结构应为 { company_name: 'xxx' }
|
||||
addCustomerName(data){
|
||||
return request.post('/addcustomername',data)
|
||||
},
|
||||
|
||||
addEmployee(data) {
|
||||
return request.post('/addemployee', data) // 对应后端新增员工
|
||||
},
|
||||
deleteEmployee(id) {
|
||||
return request.delete(`/employee/${id}`) // 对应后端删除员工
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,24 +9,53 @@
|
||||
<el-radio-button :label="0">未分配</el-radio-button>
|
||||
<el-radio-button :label="1">进行中</el-radio-button>
|
||||
<el-radio-button :label="2">已完成</el-radio-button>
|
||||
<el-radio-button :label="3">已完结</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<div class="input-group">
|
||||
<el-form-item label="相关人员">
|
||||
<el-select v-model="localFilters.operator_name" placeholder="运营人员" clearable class="w-150">
|
||||
<el-option v-for="op in operators" :key="op" :label="op" :value="op" />
|
||||
<el-form-item label="运营人员">
|
||||
<el-select
|
||||
v-model="localFilters.operator_name"
|
||||
placeholder="全部运营人员"
|
||||
clearable
|
||||
filterable
|
||||
class="w-150"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option
|
||||
v-for="op in operators"
|
||||
:key="op.id"
|
||||
:label="`${op.name} (${op.company})`"
|
||||
:value="op.name"
|
||||
/>
|
||||
</el-select>
|
||||
<span class="divider">/</span>
|
||||
<el-select v-model="localFilters.principalpeople" placeholder="负责人" clearable class="w-150">
|
||||
<el-option v-for="op in operators" :key="op" :label="op" :value="op" />
|
||||
</el-form-item>
|
||||
|
||||
<span class="divider">/</span>
|
||||
|
||||
<el-form-item label="我方负责人">
|
||||
<el-select
|
||||
v-model="localFilters.staff_id"
|
||||
placeholder="全部负责人"
|
||||
clearable
|
||||
filterable
|
||||
class="w-150"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option
|
||||
v-for="staff in staffs"
|
||||
:key="staff.id"
|
||||
:label="staff.name"
|
||||
:value="staff.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="localFilters.search"
|
||||
placeholder="搜索网站名或备注..."
|
||||
placeholder="搜索网站、运营、备注..."
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
class="search-input"
|
||||
@@ -43,14 +72,14 @@
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch" class="search-btn">
|
||||
执行筛选
|
||||
</el-button>
|
||||
<el-button @click="$emit('reset')" plain>重置</el-button>
|
||||
<el-button @click="$emit('reset')" plain>重置条件</el-button>
|
||||
|
||||
<el-dropdown trigger="click" class="more-filter">
|
||||
<el-button icon="Sort">排序</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="localFilters.sort_order = 'desc'">时间降序</el-dropdown-item>
|
||||
<el-dropdown-item @click="localFilters.sort_order = 'asc'">时间升序</el-dropdown-item>
|
||||
<el-dropdown-item @click="setSort('desc')">创建时间 (新→旧)</el-dropdown-item>
|
||||
<el-dropdown-item @click="setSort('asc')">创建时间 (旧→新)</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -60,102 +89,93 @@
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, watch } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
<script setup>
|
||||
import { reactive, watch } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
operators: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['filter-change', 'reset'])
|
||||
|
||||
const localFilters = reactive({ ...props.filters })
|
||||
|
||||
watch(
|
||||
() => props.filters,
|
||||
(newFilters) => {
|
||||
Object.assign(localFilters, newFilters)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const handleSearch = () => {
|
||||
emit('filter-change', { ...localFilters })
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
// 传入的是所有客户员工的名字数组 [ "张三", "李四", ... ]
|
||||
operators: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 传入的是我方员工对象数组 [ {id: 1, name: "管理员"}, ... ]
|
||||
staffs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
</script>
|
||||
})
|
||||
|
||||
<style scoped>
|
||||
.filter-card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(to right, #ffffff, #f9fafc);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
const emit = defineEmits(['filter-change', 'reset'])
|
||||
|
||||
.filter-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(149, 157, 165, 0.2);
|
||||
}
|
||||
// 本地副本
|
||||
const localFilters = reactive({ ...props.filters })
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
// 深度监听父组件 filters,用于同步重置状态
|
||||
watch(
|
||||
() => props.filters,
|
||||
(newFilters) => {
|
||||
Object.assign(localFilters, newFilters)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
.filter-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
const handleSearch = () => {
|
||||
emit('filter-change', { ...localFilters })
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
const setSort = (order) => {
|
||||
localFilters.sort_order = order
|
||||
handleSearch()
|
||||
}
|
||||
</script>
|
||||
|
||||
.w-150 { width: 150px; }
|
||||
.search-input { width: 280px; }
|
||||
<style scoped>
|
||||
/* 保持你的原样样式,仅微调间距 */
|
||||
.filter-card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(to right, #ffffff, #f9fafc);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.filter-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(149, 157, 165, 0.2);
|
||||
}
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
.filter-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
.w-150 { width: 160px; }
|
||||
.search-input { width: 280px; }
|
||||
.divider { margin: 0 4px; color: #dcdfe6; }
|
||||
.filter-actions { display: flex; gap: 10px; padding-top: 4px; }
|
||||
.search-btn { padding: 12px 24px; font-weight: bold; letter-spacing: 1px; }
|
||||
|
||||
.divider {
|
||||
margin: 0 8px;
|
||||
color: #dcdfe6;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
padding: 12px 24px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
filter: brightness(1.1);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.search-container { flex-direction: column; }
|
||||
.search-input { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
@media (max-width: 1024px) {
|
||||
.search-input { width: 200px; }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.search-container { flex-direction: column; }
|
||||
.search-input { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
@@ -2,14 +2,13 @@
|
||||
<el-dialog v-model="dialogVisible" :title="isEditing ? '📝 编辑工单任务' : '✨ 创建新工单'" width="550px" class="modern-dialog"
|
||||
destroy-on-close @close="handleClose">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="compact-form">
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-title">业务基础信息</div>
|
||||
<el-form-item label="网站地址" prop="website">
|
||||
<el-input v-model="form.website" placeholder="" clearable>
|
||||
<el-input v-model="form.website" placeholder="请输入网站 URL" clearable>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Link />
|
||||
</el-icon>
|
||||
<el-icon><Link /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
@@ -18,28 +17,53 @@
|
||||
<div class="form-section people-grid">
|
||||
<div class="section-title column-full">权责分配</div>
|
||||
|
||||
<el-form-item label="运营人员 (外方)" prop="operator" class="client-item">
|
||||
<el-select v-model="form.operator" placeholder="请选择" style="width: 100%">
|
||||
<el-option v-for="op in operators" :key="op" :label="op" :value="op" />
|
||||
<el-form-item label="客户单位 (外方)" prop="customer_id" class="client-item">
|
||||
<el-select v-model="current_customer_id" placeholder="请选择单位" style="width: 100%" filterable @change="handleCustomerChange">
|
||||
<el-option
|
||||
v-for="item in customers"
|
||||
:key="item.customer_id"
|
||||
:label="item.company_name"
|
||||
:value="item.customer_id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="负责人 (我方)" prop="principalpeople" class="owner-item">
|
||||
<el-input v-model="form.principalpeople" placeholder="请输入姓名">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<UserFilled />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-form-item label="运营人员 (外方)" prop="operator" class="client-item">
|
||||
<el-select
|
||||
v-model="form.operator"
|
||||
placeholder="请选择人员"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
:disabled="!current_customer_id"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in filteredEmployees"
|
||||
:key="item.employee_id"
|
||||
:label="item.employee_name"
|
||||
:value="item.employee_name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="负责人 (我方)" prop="staff_id" class="owner-item column-full">
|
||||
<el-select v-model="form.staff_id" placeholder="请选择我方负责人" style="width: 100%" filterable>
|
||||
<el-option
|
||||
v-for="item in staffs"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<el-form-item label="当前进度">
|
||||
<el-radio-group v-model="form.is_complete" class="modern-radio-group">
|
||||
<el-radio-button :label="0">进行中</el-radio-button>
|
||||
<el-radio-button :label="1">已完成</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-form-item>
|
||||
|
||||
@@ -61,36 +85,51 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch, nextTick } from 'vue'
|
||||
import { Link, UserFilled } from '@element-plus/icons-vue'
|
||||
import { ref, reactive, watch, computed, nextTick } from 'vue'
|
||||
import { Link } from '@element-plus/icons-vue'
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'submit'])
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
formData: Object,
|
||||
operators: Array,
|
||||
customers: Array, // 外部客户单位列表
|
||||
customerEmployees: Array, // 外部客户员工列表
|
||||
staffs: Array, // 我方员工列表
|
||||
isEditing: Boolean
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
const current_customer_id = ref(null) // 级联用的中间变量
|
||||
|
||||
const form = reactive({
|
||||
website: '',
|
||||
operator: '',
|
||||
principalpeople: '',
|
||||
is_complete: 0,
|
||||
staff_id: null,
|
||||
is_complete: "未分配",
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
website: [{ required: true, message: '请输入网站地址', trigger: 'blur' }],
|
||||
operator: [{ required: true, message: '请选择运营人员', trigger: 'change' }],
|
||||
principalpeople: [{ required: true, message: '请输入负责人', trigger: 'blur' }]
|
||||
staff_id: [{ required: true, message: '请选择负责人', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 监听显隐
|
||||
// 核心逻辑:级联过滤
|
||||
const filteredEmployees = computed(() => {
|
||||
if (!current_customer_id.value) return []
|
||||
return props.customerEmployees.filter(emp => emp.customer_id === current_customer_id.value)
|
||||
})
|
||||
|
||||
// 单位切换时清空人员
|
||||
const handleCustomerChange = () => {
|
||||
form.operator = ''
|
||||
}
|
||||
|
||||
// 监听弹窗显隐与数据回显
|
||||
watch(() => props.modelValue, (val) => {
|
||||
dialogVisible.value = val
|
||||
if (val) {
|
||||
@@ -98,6 +137,11 @@ watch(() => props.modelValue, (val) => {
|
||||
resetForm()
|
||||
if (props.isEditing && props.formData) {
|
||||
Object.assign(form, props.formData)
|
||||
// 回显逻辑:根据 operator 姓名反查所属 customer_id
|
||||
const employee = props.customerEmployees.find(e => e.employee_name === props.formData.operator)
|
||||
if (employee) {
|
||||
current_customer_id.value = employee.customer_id
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -107,7 +151,14 @@ watch(() => dialogVisible.value, (val) => emit('update:modelValue', val))
|
||||
|
||||
const resetForm = () => {
|
||||
if (formRef.value) formRef.value.resetFields()
|
||||
Object.assign(form, { website: '', operator: '', principalpeople: '', is_complete: 0, remark: '' })
|
||||
current_customer_id.value = null
|
||||
Object.assign(form, {
|
||||
website: '',
|
||||
operator: '',
|
||||
staff_id: null,
|
||||
is_complete: "未分配",
|
||||
remark: ''
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => resetForm()
|
||||
@@ -119,7 +170,7 @@ const handleSubmit = async () => {
|
||||
submitting.value = true
|
||||
emit('submit', { ...form })
|
||||
} catch (e) {
|
||||
// 验证失败
|
||||
console.log('表单校验失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -127,79 +178,17 @@ const handleSubmit = async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 深度修改对话框圆角 */
|
||||
:deep(.modern-dialog) {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-title::before {
|
||||
content: "";
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
background: #409eff;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 两列布局 */
|
||||
.people-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.column-full {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
/* 人员项区分颜色提示 */
|
||||
:deep(.client-item .el-form-item__label) {
|
||||
color: #909399 !important;
|
||||
}
|
||||
|
||||
:deep(.owner-item .el-form-item__label) {
|
||||
color: #409eff !important;
|
||||
}
|
||||
|
||||
/* 现代单选框组 */
|
||||
.modern-radio-group {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
.dialog-footer {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 调整输入框间距 */
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
padding-bottom: 4px !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
:deep(.modern-dialog) { border-radius: 12px; overflow: hidden; }
|
||||
.form-section { margin-bottom: 24px; }
|
||||
.section-title { font-size: 14px; font-weight: 600; color: #1e293b; margin-bottom: 12px; display: flex; align-items: center; }
|
||||
.section-title::before { content: ""; width: 4px; height: 14px; background: #409eff; margin-right: 8px; border-radius: 2px; }
|
||||
.people-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||
.column-full { grid-column: span 2; }
|
||||
:deep(.client-item .el-form-item__label) { color: #909399 !important; }
|
||||
:deep(.owner-item .el-form-item__label) { color: #409eff !important; }
|
||||
.modern-radio-group { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); border-radius: 8px; overflow: hidden; }
|
||||
.dialog-footer { padding-top: 10px; }
|
||||
.submit-btn { padding-left: 24px; padding-right: 24px; font-weight: 600; }
|
||||
:deep(.el-form-item) { margin-bottom: 18px; }
|
||||
:deep(.el-form-item__label) { padding-bottom: 4px !important; font-size: 13px; }
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<el-card class="compact-card" shadow="never">
|
||||
<el-table :data="tickets" v-loading="loading" header-cell-class-name="table-header" style="width: 100%">
|
||||
|
||||
<el-table-column prop="website" label="网站地址" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" :href="row.website" target="_blank" class="site-link">
|
||||
@@ -9,22 +10,55 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="协同人员" width="240">
|
||||
<el-table-column label="数据量" width="100">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.datanum && row.datanum !== '0'" class="data-num-text">
|
||||
{{ row.datanum }}
|
||||
</span>
|
||||
<span v-else class="empty-text">未录入</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="任务进度" width="140">
|
||||
<template #default="{ row }">
|
||||
<div class="progress-container">
|
||||
<el-progress
|
||||
:percentage="Number(row.progress || 0)"
|
||||
:status="Number(row.progress) === 100 ? 'success' : ''"
|
||||
:stroke-width="6"
|
||||
:text-inside="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="协同人员" width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="staff-wrapper">
|
||||
<div class="staff-item client">
|
||||
<span class="staff-role">运营</span>
|
||||
<span class="staff-name">{{ row.operator }}</span>
|
||||
<span class="staff-name">{{ row.operator?.trim() || '未指派' }}</span>
|
||||
</div>
|
||||
<div class="staff-item internal">
|
||||
<span class="staff-role">负责</span>
|
||||
<span class="staff-name">{{ row.principalpeople }}</span>
|
||||
<span class="staff-name">
|
||||
{{ row.principalStaff?.name || '未指派' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="is_complete" label="状态" width="100" align="center">
|
||||
<el-table-column label="备注" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.remark" effect="dark" :content="row.remark" placement="top">
|
||||
<span class="remark-text">{{ row.remark }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="empty-text">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="is_complete" label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="status-badge" :style="getStatusStyle(row.is_complete)">
|
||||
{{ row.is_complete }}
|
||||
@@ -32,7 +66,24 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="时间节点" width="200">
|
||||
<el-table-column label="锁定状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="row.lock_time && row.lock_time !== '0'"
|
||||
type="success"
|
||||
size="small"
|
||||
effect="light"
|
||||
>已锁定</el-tag>
|
||||
<el-tag
|
||||
v-else
|
||||
type="info"
|
||||
size="small"
|
||||
effect="plain"
|
||||
>未锁定</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="时间节点" width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="time-wrapper">
|
||||
<span>创建:{{ formatTime(row.create_time) }}</span>
|
||||
@@ -41,13 +92,22 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="150" fixed="right" align="right">
|
||||
<el-table-column label="操作" width="120" fixed="right" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="$emit('edit', row)">编辑</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button link type="danger" @click="$emit('delete', row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<template #empty>
|
||||
<div v-if="!loading">
|
||||
<el-empty description="暂无相关工单数据" :image-size="120" />
|
||||
</div>
|
||||
<div v-else class="loading-placeholder">
|
||||
<p>正在努力加载中...</p>
|
||||
</div>
|
||||
</template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
@@ -62,8 +122,14 @@ defineEmits(['edit', 'delete'])
|
||||
|
||||
const formatTime = (time) => {
|
||||
if (!time) return '-';
|
||||
if (typeof time === 'string' && time.includes('-')) return time;
|
||||
|
||||
const date = new Date(time);
|
||||
return `${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`;
|
||||
const m = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const d = date.getDate().toString().padStart(2, '0');
|
||||
const h = date.getHours().toString().padStart(2, '0');
|
||||
const i = date.getMinutes().toString().padStart(2, '0');
|
||||
return `${m}-${d} ${h}:${i}`;
|
||||
}
|
||||
|
||||
const getStatusStyle = (status) => {
|
||||
@@ -78,127 +144,76 @@ const getStatusStyle = (status) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.progress-container {
|
||||
padding-right: 10px;
|
||||
}
|
||||
/* 调整进度条内部文字大小 */
|
||||
:deep(.el-progress__text) {
|
||||
font-size: 11px !important;
|
||||
min-width: 28px;
|
||||
}
|
||||
|
||||
.staff-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.staff-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.staff-role {
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.staff-name {
|
||||
padding-left: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 外方样式:低调工业风 */
|
||||
.staff-item.client {
|
||||
background: #f4f4f5;
|
||||
border-color: #e9e9eb;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.staff-item.client .staff-role {
|
||||
background: #909399;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 我方样式:专业商务蓝 */
|
||||
.staff-item.internal {
|
||||
background: #ecf5ff;
|
||||
border-color: #d9ecff;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.staff-item.internal .staff-role {
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
.compact-card {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 链接样式优化 */
|
||||
.site-link {
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 人员标签化区分 */
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.role-tag {
|
||||
.staff-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
width: fit-content;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.role-tag.client {
|
||||
background: #f0f2f5;
|
||||
color: #666;
|
||||
border-left: 3px solid #dcdfe6;
|
||||
.staff-role {
|
||||
width: 34px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
background: #909399;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.role-tag.owner {
|
||||
background: #e8f4ff;
|
||||
color: #1890ff;
|
||||
border-left: 3px solid #1890ff;
|
||||
.staff-name {
|
||||
padding-left: 6px;
|
||||
color: inherit;
|
||||
}
|
||||
.staff-item.client { background: #f4f4f5; color: #909399; border: 1px solid #e9e9eb; }
|
||||
.staff-item.internal { background: #ecf5ff; color: #409eff; border: 1px solid #d9ecff; }
|
||||
.staff-item.internal .staff-role { background: #409eff; }
|
||||
|
||||
/* 状态小红点风格 */
|
||||
.data-num-text {
|
||||
font-family: 'Monaco', monospace;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
}
|
||||
.remark-text {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
}
|
||||
.empty-text { color: #c0c4cc; font-size: 12px; }
|
||||
|
||||
.compact-card { border-radius: 8px; border: 1px solid #ebeef5; }
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0 10px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
font-size: 12px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
padding: 0 8px;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
font-size: 11px;
|
||||
border-radius: 11px;
|
||||
}
|
||||
|
||||
/* 时间列紧凑化 */
|
||||
.time-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.update-label {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* 表头加深,增加稳重感 */
|
||||
:deep(.table-header) {
|
||||
background-color: #fafafa !important;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
.update-label { color: #909399; }
|
||||
:deep(.table-header) { background-color: #fafafa !important; font-weight: 600; }
|
||||
</style>
|
||||
@@ -19,12 +19,16 @@
|
||||
<el-icon><icon-menu /></el-icon>
|
||||
<span>工单管理</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/stats">
|
||||
<el-icon>
|
||||
<document />
|
||||
</el-icon>
|
||||
<el-icon><document /></el-icon>
|
||||
<span>数据统计</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/customers">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>客户管理</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
@@ -207,7 +211,12 @@
|
||||
|
||||
<script setup>
|
||||
import { reactive, onMounted } from 'vue'
|
||||
import { Menu as IconMenu, Document } from '@element-plus/icons-vue' // 补充图标引入
|
||||
import {
|
||||
Menu as IconMenu,
|
||||
Document,
|
||||
User
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
|
||||
|
||||
14
src/main.js
14
src/main.js
@@ -1,17 +1,25 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router' // 1. 导入我们创建的 router
|
||||
import router from './router'
|
||||
|
||||
// 导入 Element Plus
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
// 导入中文语言包
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册所有图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(ElementPlus)
|
||||
app.use(router) // 2. 在这里使用 router
|
||||
// 在这里配置全局中文
|
||||
app.use(ElementPlus, {
|
||||
locale: zhCn,
|
||||
})
|
||||
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus' // 引入提示组件
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@@ -8,52 +9,97 @@ const router = createRouter({
|
||||
component: () => import('../layouts/MainLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '', // 默认访问 / 显示工单管理
|
||||
path: '',
|
||||
name: 'home',
|
||||
component: () => import('../views/TicketManagement.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
// 标记需要 admin 权限
|
||||
meta: { requiresAuth: true, role: 'admin' }
|
||||
},
|
||||
{
|
||||
path: 'stats', // 访问 /stats
|
||||
path: 'stats',
|
||||
name: 'stats',
|
||||
component: () => import('../views/StatsView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
meta: { requiresAuth: true, role: 'admin' }
|
||||
},
|
||||
{
|
||||
path: 'customers',
|
||||
name: 'customers',
|
||||
component: () => import('../views/CustomersView.vue'),
|
||||
meta: { requiresAuth: true, role: 'admin' }
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
name: 'user_workdesk',
|
||||
component: () => import('../views/UserView.vue'),
|
||||
// 标记只需要普通用户权限
|
||||
meta: { requiresAuth: true, role: 'user' }
|
||||
},
|
||||
{
|
||||
path: '/client',
|
||||
name: 'client_monitor',
|
||||
component: () => import('../views/ClientView.vue'),
|
||||
// 标记只需要普通用户权限
|
||||
meta: { requiresAuth: true, role: 'client' }
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('../views/LoginView.vue') // 懒加载登录组件,
|
||||
component: () => import('../views/LoginView.vue')
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
// 全局前置守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 1. 尝试从 localStorage 或 sessionStorage 获取 Token
|
||||
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
|
||||
|
||||
// 2. 判断目标路由是否需要登录权限
|
||||
// matches 可以检查嵌套路由中父级或子级是否有 requiresAuth
|
||||
// 获取用户信息以判断角色
|
||||
const userInfoStr = localStorage.getItem('user_info') || sessionStorage.getItem('user_info');
|
||||
const userInfo = userInfoStr ? JSON.parse(userInfoStr) : null;
|
||||
|
||||
// 简单的角色判定逻辑:如果用户名为 admin 则视为管理员,否则为普通用户
|
||||
// 你也可以根据后端返回的具体的 role 字段来判断
|
||||
const userRole = userInfo?.username === 'admin' ? 'admin' : 'user';
|
||||
|
||||
const isRequiresAuth = to.matched.some(record => record.meta.requiresAuth);
|
||||
|
||||
// 1. 未登录拦截
|
||||
if (isRequiresAuth && !token) {
|
||||
// 3. 如果需要登录但没有 Token,强制拦截并跳转到登录页
|
||||
// 顺便把当前想去的页面路径传给登录页,方便登录后跳回来(可选增强逻辑)
|
||||
next({
|
||||
name: 'login',
|
||||
query: { redirect: to.fullPath }
|
||||
});
|
||||
} else if (to.name === 'login' && token) {
|
||||
|
||||
// 4. 如果用户已经登录了,还想手动输入 /login 去登录页,直接把他弹回首页
|
||||
next({ name: 'home' });
|
||||
} else {
|
||||
// 5. 否则直接放行
|
||||
next();
|
||||
next({ name: 'login', query: { redirect: to.fullPath } });
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 已登录状态下的逻辑
|
||||
if (token) {
|
||||
// 如果想去登录页,根据角色重定向到对应首页
|
||||
if (to.name === 'login') {
|
||||
userRole === 'admin' ? next({ name: 'home' }) : next({ name: 'user_workdesk' });
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 核心修复:权限校验 ---
|
||||
// 检查目标路由是否定义了需要的角色
|
||||
const requiredRole = to.meta.role;
|
||||
|
||||
if (requiredRole && requiredRole !== userRole) {
|
||||
// 如果是员工想进管理员页面
|
||||
if (userRole === 'user' && requiredRole === 'admin') {
|
||||
ElMessage.error('权限不足,无法访问管理后台');
|
||||
next({ name: 'user_workdesk' }); // 踢回员工工作台
|
||||
return;
|
||||
}
|
||||
// 如果是管理员想进员工页面 (通常允许,如果不允许也可以踢回 /)
|
||||
if (userRole === 'admin' && requiredRole === 'user') {
|
||||
next({ name: 'home' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 正常放行
|
||||
next();
|
||||
});
|
||||
|
||||
export default router
|
||||
293
src/views/ClientView.vue
Normal file
293
src/views/ClientView.vue
Normal file
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<div class="client-view-container">
|
||||
<header class="client-header">
|
||||
<div class="brand">
|
||||
<div class="logo-box">
|
||||
<el-icon><DataLine /></el-icon>
|
||||
</div>
|
||||
<div class="brand-text">
|
||||
<span class="title">数据交付监控台</span>
|
||||
<span class="version">Real-time Monitor v2.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<el-space :size="20">
|
||||
<div class="user-badge">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>{{ currentUser.username }}</span>
|
||||
</div>
|
||||
<el-button type="danger" plain size="small" @click="handleLogout" :icon="SwitchButton">退出</el-button>
|
||||
</el-space>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="client-stats">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6" v-for="stat in summaryStats" :key="stat.label">
|
||||
<div class="stat-card-premium" :style="{ '--theme-color': stat.color }">
|
||||
<div class="stat-info">
|
||||
<span class="s-label">{{ stat.label }}</span>
|
||||
<span class="s-value">{{ stat.value }}</span>
|
||||
</div>
|
||||
<div class="stat-icon-wrapper">
|
||||
<el-icon><component :is="stat.icon" /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</section>
|
||||
|
||||
<main class="data-section">
|
||||
<div class="section-header">
|
||||
<div class="header-left">
|
||||
<span class="indicator"></span>
|
||||
<span class="section-title">实时交付看板</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button type="primary" link @click="loadData" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon> 同步最新数据
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="paginatedTickets" v-loading="loading" class="custom-client-table">
|
||||
<el-table-column prop="id" label="单号" width="90" align="center">
|
||||
<template #default="{row}">
|
||||
<span class="id-tag">#{{ row.id }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="website" label="网站地址" min-width="260">
|
||||
<template #default="{row}">
|
||||
<div class="project-cell">
|
||||
<el-icon class="project-icon"><Monitor /></el-icon>
|
||||
<span class="project-name" :title="row.website">{{ row.website }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="交付进度" width="260">
|
||||
<template #default="scope">
|
||||
<div class="progress-box">
|
||||
<div class="progress-labels">
|
||||
<span class="percent-val">{{ scope.row.progress || 0 }}%</span>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="Number(scope.row.progress || 0)"
|
||||
:status="Number(scope.row.progress) === 100 ? 'success' : ''"
|
||||
:stroke-width="10"
|
||||
:show-text="false"
|
||||
striped
|
||||
striped-flow
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="datanum" label="总数据量" width="160" align="center">
|
||||
<template #default="scope">
|
||||
<div class="data-val-box">
|
||||
<span class="data-num">{{ formatNumber(scope.row.datanum) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="交付状态" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<div :class="['status-dot-box', getStatusClass(scope.row.is_complete)]">
|
||||
<span class="dot"></span>
|
||||
<span class="text">{{ scope.row.is_complete }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="更新日期" width="140" align="center">
|
||||
<template #default="scope">
|
||||
<span class="date-text">{{ formatDateOnly(scope.row.lock_time || scope.row.update_time || scope.row.create_time) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<footer class="pagination-footer">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="limit"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { DataLine, SwitchButton, User, Refresh, Files, Timer, CircleCheck, Monitor } from '@element-plus/icons-vue'
|
||||
import { ticketApi } from '@/api/ticket'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const tickets = ref([])
|
||||
const page = ref(1)
|
||||
const limit = ref(15)
|
||||
|
||||
const currentUser = computed(() => {
|
||||
const info = localStorage.getItem('user_info') || sessionStorage.getItem('user_info')
|
||||
return info ? JSON.parse(info) : { username: '外部客户' }
|
||||
})
|
||||
|
||||
const filteredTickets = computed(() => {
|
||||
const allowed = ['未分配', '进行中', '已完成']
|
||||
return tickets.value.filter(t => allowed.includes(t.is_complete))
|
||||
})
|
||||
|
||||
const paginatedTickets = computed(() => {
|
||||
const start = (page.value - 1) * limit.value
|
||||
return filteredTickets.value.slice(start, start + limit.value)
|
||||
})
|
||||
|
||||
const total = computed(() => filteredTickets.value.length)
|
||||
|
||||
const summaryStats = computed(() => [
|
||||
{ label: '项目总数', value: total.value, color: '#409EFF', icon: Monitor },
|
||||
{ label: '等待排期', value: filteredTickets.value.filter(t => t.is_complete === '未分配').length, color: '#A8ABB2', icon: Files },
|
||||
{ label: '正在采集', value: filteredTickets.value.filter(t => t.is_complete === '进行中').length, color: '#E6A23C', icon: Timer },
|
||||
{ label: '已就绪', value: filteredTickets.value.filter(t => t.is_complete === '已完成').length, color: '#67C23A', icon: CircleCheck }
|
||||
])
|
||||
|
||||
const formatNumber = (val) => {
|
||||
if (!val || val === '0') return '0';
|
||||
const num = Number(val);
|
||||
return isNaN(num) ? val : num.toLocaleString();
|
||||
}
|
||||
|
||||
const getStatusClass = (status) => {
|
||||
const map = { '未分配': 'is-waiting', '进行中': 'is-running', '已完成': 'is-success' }
|
||||
return map[status] || ''
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await ticketApi.getTickets({ page: 1, limit: 1000 })
|
||||
if (res.code === 200 || res.code === true) {
|
||||
tickets.value = res.data.data
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('同步失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatDateOnly = (ts) => {
|
||||
if (!ts || ts === '0' || ts === '') return '-'
|
||||
const date = !isNaN(ts) ? new Date(String(ts).length === 10 ? ts * 1000 : parseInt(ts)) : new Date(ts)
|
||||
return isNaN(date.getTime()) ? '-' : `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const handlePageChange = (p) => {
|
||||
page.value = p
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.clear(); sessionStorage.clear(); router.push('/login')
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.client-view-container {
|
||||
padding: 24px 40px;
|
||||
background-color: #f6f8fb;
|
||||
min-height: 100vh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* 顶部导航 */
|
||||
.client-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 12px; }
|
||||
.logo-box {
|
||||
background: linear-gradient(135deg, #409EFF, #007aff);
|
||||
width: 38px; height: 38px;
|
||||
border-radius: 8px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: #fff; font-size: 20px;
|
||||
box-shadow: 0 4px 12px rgba(64,158,255,0.35);
|
||||
}
|
||||
.title { font-size: 19px; font-weight: 700; color: #1d1d1f; }
|
||||
.version { font-size: 10px; color: #a1a1a6; margin-left: 8px; letter-spacing: 0.5px; }
|
||||
|
||||
.user-badge {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
background: #fff; padding: 6px 14px;
|
||||
border-radius: 20px; font-size: 13px; color: #606266;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
/* 指标卡片 */
|
||||
.stat-card-premium {
|
||||
background: #fff;
|
||||
padding: 22px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.03);
|
||||
border-left: 4px solid var(--theme-color);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.stat-card-premium:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
|
||||
.s-label { font-size: 13px; color: #86868b; display: block; margin-bottom: 6px; }
|
||||
.s-value { font-size: 28px; font-weight: 800; color: #1d1d1f; }
|
||||
.stat-icon-wrapper { font-size: 36px; color: var(--theme-color); opacity: 0.1; }
|
||||
|
||||
/* 列表容器 */
|
||||
.data-section {
|
||||
background: #fff; padding: 24px; border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.02);
|
||||
}
|
||||
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.header-left { display: flex; align-items: center; gap: 10px; }
|
||||
.indicator { width: 3px; height: 16px; background: #409EFF; border-radius: 2px; }
|
||||
.section-title { font-size: 16px; font-weight: 600; color: #303133; }
|
||||
|
||||
/* 表格样式微调 */
|
||||
.id-tag { color: #909399; font-family: 'Consolas', monospace; font-size: 12px; }
|
||||
.project-cell { display: flex; align-items: center; gap: 8px; }
|
||||
.project-icon { color: #c0c4cc; font-size: 16px; }
|
||||
.project-name { font-weight: 500; color: #2c3e50; }
|
||||
|
||||
.progress-labels { display: flex; justify-content: flex-end; margin-bottom: 4px; }
|
||||
.percent-val { font-size: 11px; font-weight: 700; color: #409eff; }
|
||||
|
||||
.data-val-box { display: flex; flex-direction: column; align-items: center; }
|
||||
.data-num { font-size: 16px; font-weight: 700; color: #2c3e50; font-family: 'Helvetica Neue', sans-serif; }
|
||||
.unit { font-size: 9px; color: #a8abb2; transform: scale(0.9); }
|
||||
|
||||
/* 状态指示器 */
|
||||
.status-dot-box { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 500; }
|
||||
.dot { width: 6px; height: 6px; border-radius: 50%; }
|
||||
.is-waiting { color: #909399; } .is-waiting .dot { background: #909399; }
|
||||
.is-running { color: #E6A23C; } .is-running .dot { background: #E6A23C; box-shadow: 0 0 6px rgba(230,162,60,0.4); }
|
||||
.is-success { color: #67C23A; } .is-success .dot { background: #67C23A; }
|
||||
|
||||
.date-text { font-size: 12px; color: #909399; }
|
||||
.pagination-footer { margin-top: 24px; display: flex; justify-content: center; }
|
||||
|
||||
:deep(.el-table) { --el-table-header-bg-color: #f8f9fb; }
|
||||
:deep(.el-table__row:hover) { background-color: #faffff !important; }
|
||||
</style>
|
||||
253
src/views/CustomersView.vue
Normal file
253
src/views/CustomersView.vue
Normal file
@@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<div class="customers-container">
|
||||
<h2>客户及员工管理系统</h2>
|
||||
|
||||
<div class="action-bar">
|
||||
<div class="search-group">
|
||||
<el-input v-model="searchQuery" placeholder="搜索客户单位" style="width: 250px" clearable />
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<el-button type="primary" :icon="Plus" @click="handleAddCustomer">新增客户单位</el-button>
|
||||
<el-button type="success" :icon="User" @click="handleAddEmployee">新增员工</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="filteredData" border style="width: 100%; margin-top: 20px" v-loading="loading">
|
||||
<el-table-column prop="customer_id" label="客户ID" width="100" />
|
||||
<el-table-column prop="company_name" label="客户单位名称" />
|
||||
|
||||
<el-table-column label="员工管理" width="250" align="center">
|
||||
<template #default="scope">
|
||||
<span style="margin-right: 15px">
|
||||
共 <b style="color: #409EFF">{{ getEmployeeCount(scope.row.customer_id) }}</b> 人
|
||||
</span>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
link
|
||||
:icon="View"
|
||||
@click="showEmployeeDetails(scope.row)"
|
||||
>查看详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="danger" plain @click="handleDeleteCustomer(scope.row)">删除单位</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="customerDialogVisible" title="新增客户单位" width="400px">
|
||||
<el-form :model="customerForm" label-width="100px">
|
||||
<el-form-item label="单位名称" required>
|
||||
<el-input v-model="customerForm.company_name" placeholder="例如:方舟星河" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="customerDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitCustomer">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="employeeDialogVisible" title="新增客户员工" width="450px">
|
||||
<el-form :model="employeeForm" label-width="100px">
|
||||
<el-form-item label="员工姓名" required>
|
||||
<el-input v-model="employeeForm.employee_name" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属客户" required>
|
||||
<el-select v-model="employeeForm.customer_id" placeholder="请选择归属单位" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in customerList"
|
||||
:key="item.customer_id"
|
||||
:label="item.company_name"
|
||||
:value="item.customer_id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="employeeDialogVisible = false">取消</el-button>
|
||||
<el-button type="success" @click="submitEmployee">保存员工</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="employeeDetailVisible" :title="`【${activeCustomerName}】员工名单`" width="500px">
|
||||
<el-table :data="currentCustomerEmployees" border stripe>
|
||||
<el-table-column prop="employee_id" label="员工ID" width="100" />
|
||||
<el-table-column prop="employee_name" label="姓名" />
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
link
|
||||
@click="confirmDeleteEmployee(scope.row.employee_id)"
|
||||
>移除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Plus, User, View } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { customerApi } from '@/api/ticket' // 确保路径指向包含 addCustomerName 的 API 文件
|
||||
|
||||
// --- 状态数据 ---
|
||||
const loading = ref(false)
|
||||
const customerList = ref([])
|
||||
const employeeList = ref([])
|
||||
const searchQuery = ref('')
|
||||
|
||||
const employeeDetailVisible = ref(false)
|
||||
const activeCustomerName = ref('')
|
||||
const activeCustomerId = ref(null)
|
||||
|
||||
// --- 表单数据 ---
|
||||
const customerDialogVisible = ref(false)
|
||||
const customerForm = ref({ company_name: '' })
|
||||
|
||||
const employeeDialogVisible = ref(false)
|
||||
const employeeForm = ref({ employee_name: '', customer_id: null })
|
||||
|
||||
// --- 核心方法 ---
|
||||
|
||||
// 1. 获取全局数据
|
||||
const fetchCustomers = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await customerApi.getCustomers()
|
||||
if (res.code === 200) {
|
||||
customerList.value = res.customers || []
|
||||
employeeList.value = res.employees || []
|
||||
} else {
|
||||
ElMessage.error(res.msg || '数据加载失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('API Error:', error)
|
||||
ElMessage.error('无法连接到服务器')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 统计处理
|
||||
const getEmployeeCount = (id) => {
|
||||
return employeeList.value.filter(emp => emp.customer_id === id).length
|
||||
}
|
||||
|
||||
const currentCustomerEmployees = computed(() => {
|
||||
return employeeList.value.filter(emp => emp.customer_id === activeCustomerId.value)
|
||||
})
|
||||
|
||||
const filteredData = computed(() => {
|
||||
return customerList.value.filter(item =>
|
||||
item.company_name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
// 3. 客户管理逻辑
|
||||
const handleAddCustomer = () => {
|
||||
customerForm.value.company_name = ''
|
||||
customerDialogVisible.value = true
|
||||
}
|
||||
|
||||
const submitCustomer = async () => {
|
||||
if (!customerForm.value.company_name) return ElMessage.error('名称不能为空')
|
||||
try {
|
||||
const res = await customerApi.addCustomerName({
|
||||
company_name: customerForm.value.company_name
|
||||
})
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('客户单位创建成功')
|
||||
customerDialogVisible.value = false
|
||||
fetchCustomers()
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('创建失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteCustomer = (row) => {
|
||||
ElMessageBox.confirm(`确定删除单位【${row.company_name}】吗?这将导致关联员工数据异常!`, '警告', {
|
||||
type: 'error',
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消'
|
||||
}).then(async () => {
|
||||
// 这里应调用你后端的删除接口,例如:
|
||||
// await customerApi.deleteCustomer(row.customer_id)
|
||||
ElMessage.warning('请确保后端已实现删除逻辑')
|
||||
fetchCustomers()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 4. 员工管理逻辑
|
||||
const handleAddEmployee = () => {
|
||||
if (customerList.value.length === 0) return ElMessage.warning('请先创建客户单位')
|
||||
employeeForm.value = { employee_name: '', customer_id: null }
|
||||
employeeDialogVisible.value = true
|
||||
}
|
||||
|
||||
const submitEmployee = async () => {
|
||||
if (!employeeForm.value.employee_name || !employeeForm.value.customer_id) {
|
||||
return ElMessage.error('请填写员工姓名并选择所属单位')
|
||||
}
|
||||
try {
|
||||
// 注意:确保你的 api 文件中有 addEmployee 方法
|
||||
const res = await customerApi.addEmployee({
|
||||
emp_name: employeeForm.value.employee_name,
|
||||
customer_id: employeeForm.value.customer_id
|
||||
})
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('员工保存成功')
|
||||
employeeDialogVisible.value = false
|
||||
fetchCustomers()
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
const showEmployeeDetails = (row) => {
|
||||
activeCustomerId.value = row.customer_id
|
||||
activeCustomerName.value = row.company_name
|
||||
employeeDetailVisible.value = true
|
||||
}
|
||||
|
||||
// 需要修改,,这只是临时前端过滤,,并没有真正对接数据库
|
||||
const confirmDeleteEmployee = (empId) => {
|
||||
ElMessageBox.confirm('确定要从该单位移除此员工吗?', '提示', { type: 'warning' }).then(async () => {
|
||||
try {
|
||||
// 假设接口:await customerApi.deleteEmployee(empId)
|
||||
ElMessage.success('操作成功(请对接后端删除接口)')
|
||||
// 临时前端过滤
|
||||
employeeList.value = employeeList.value.filter(e => e.employee_id !== empId)
|
||||
} catch (e) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchCustomers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.customers-container { padding: 30px; background-color: #f5f7fa; min-height: 100vh; }
|
||||
h2 { margin-bottom: 25px; color: #303133; font-weight: 600; }
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
|
||||
}
|
||||
.button-group .el-button { margin-left: 10px; }
|
||||
:deep(.el-table__header) { color: #606266; font-weight: bold; }
|
||||
</style>
|
||||
@@ -87,12 +87,15 @@ const handleLogin = async () => {
|
||||
loading.value = true;
|
||||
|
||||
const res = await userApi.login(loginForm.username, loginForm.password);
|
||||
|
||||
// console.log(res)
|
||||
// 注意:这里由于你之前的封装,可能需要判断 res.data.code 还是 res.code
|
||||
// 如果用了拦截器,直接判断 res.code
|
||||
if (res.code === 200) {
|
||||
const token = res.data.token;
|
||||
const userInfo = JSON.stringify(res.data.user_info);
|
||||
const role = res.data.user_info.role;
|
||||
|
||||
// console.log(role)
|
||||
|
||||
// --- 核心逻辑开始 ---
|
||||
if (rememberMe.value) {
|
||||
@@ -108,8 +111,22 @@ const handleLogin = async () => {
|
||||
|
||||
ElMessage.success('身份验证通过,方舟系统加载中...');
|
||||
|
||||
// 3. 根据角色执行路由跳转
|
||||
setTimeout(() => {
|
||||
router.push('/');
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
router.push('/'); // 管理员跳转至主页
|
||||
break;
|
||||
case 'user':
|
||||
router.push('/user'); // 跳转至 UserView (请确保路由配置中有此路径)
|
||||
break;
|
||||
case 'client':
|
||||
router.push('/client'); // 跳转至 ClientView (请确保路由配置中有此路径)
|
||||
break;
|
||||
default:
|
||||
ElMessage.warning('未识别的角色权限,请联系管理员');
|
||||
break;
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
ElMessage.error(res.message || '登录失败');
|
||||
|
||||
@@ -1,21 +1,40 @@
|
||||
<template>
|
||||
<div class="ticket-management">
|
||||
<div class="stats-container">
|
||||
<div class="stat-item total">
|
||||
<div class="stat-label">总工单数</div>
|
||||
<div class="stat-value">{{ allStats.total }}</div>
|
||||
<div class="stat-card total">
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">总工单数</div>
|
||||
<div class="stat-value">{{ allStats.total }}</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon"><Memo /></el-icon>
|
||||
</div>
|
||||
<div class="stat-item pending">
|
||||
<div class="stat-label">未分配</div>
|
||||
<div class="stat-value">{{ allStats.pending }}</div>
|
||||
<div class="stat-card pending">
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">未分配</div>
|
||||
<div class="stat-value">{{ allStats.pending }}</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon"><Clock /></el-icon>
|
||||
</div>
|
||||
<div class="stat-item ongoing">
|
||||
<div class="stat-label">进行中</div>
|
||||
<div class="stat-value">{{ allStats.ongoing }}</div>
|
||||
<div class="stat-card ongoing">
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">进行中</div>
|
||||
<div class="stat-value">{{ allStats.ongoing }}</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon"><Loading /></el-icon>
|
||||
</div>
|
||||
<div class="stat-item success">
|
||||
<div class="stat-label">已完成/已完结</div>
|
||||
<div class="stat-value">{{ allStats.completed }}</div>
|
||||
<div class="stat-card success">
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">已完成</div>
|
||||
<div class="stat-value">{{ allStats.completed }}</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon"><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="stat-card finished">
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">已完结</div>
|
||||
<div class="stat-value">{{ allStats.is_completed }}</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon"><Checked /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,80 +43,149 @@
|
||||
<div class="left">
|
||||
<el-button type="primary" @click="handleAdd" :icon="Plus">新增任务</el-button>
|
||||
</div>
|
||||
<TicketFilter :filters="filters" :operators="operators" @filter-change="handleFilterChange"
|
||||
@reset="handleFilterReset" />
|
||||
<TicketFilter
|
||||
:operators="operatorsForFilter"
|
||||
:staffs="staffList"
|
||||
:filters="filters"
|
||||
@filter-change="handleFilterChange"
|
||||
@reset="handleFilterReset"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TicketList :tickets="tickets" :loading="loading" @edit="handleEdit" @delete="handleDelete" />
|
||||
<TicketList
|
||||
:tickets="tickets"
|
||||
:loading="loading"
|
||||
@edit="handleEdit"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
|
||||
<div class="pagination-container">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
|
||||
:total="pagination.total" :page-sizes="[15, 30, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[15, 30, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TicketForm v-model="showAddDialog" :form-data="currentTicket" :operators="operators" :is-editing="isEditing"
|
||||
@submit="handleFormSubmit" />
|
||||
<TicketForm
|
||||
v-model="showAddDialog"
|
||||
:form-data="currentTicket"
|
||||
:customers="customerList"
|
||||
:customer-employees="allCustomerEmployees"
|
||||
:staffs="staffList"
|
||||
:is-editing="isEditing"
|
||||
@submit="handleFormSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, computed} from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { ticketApi } from '@/api/ticket'
|
||||
import { Plus, Memo, Clock, Loading, CircleCheck, Checked } from '@element-plus/icons-vue'
|
||||
|
||||
// API 引入
|
||||
import { ticketApi ,customerApi} from '@/api/ticket'
|
||||
|
||||
// 组件引入
|
||||
import TicketFilter from '@/components/TicketFilter.vue'
|
||||
import TicketList from '@/components/TicketList.vue'
|
||||
import TicketForm from '@/components/TicketForm.vue'
|
||||
|
||||
// --- 响应式数据 ---
|
||||
// --- 响应式状态 ---
|
||||
const tickets = ref([])
|
||||
const operators = ref([])
|
||||
const staffList = ref([])
|
||||
const customerList = ref([])
|
||||
const allCustomerEmployees = ref([])
|
||||
const loading = ref(false)
|
||||
const showAddDialog = ref(false)
|
||||
const isEditing = ref(false)
|
||||
const currentTicket = ref({})
|
||||
|
||||
// 独立统计数据:专门给顶部的卡片使用
|
||||
const allStats = reactive({
|
||||
total: 0,
|
||||
pending: 0,
|
||||
ongoing: 0,
|
||||
completed: 0
|
||||
total: 0, pending: 0, ongoing: 0, completed: 0, is_completed: 0
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 15,
|
||||
total: 0
|
||||
page: 1, limit: 15, total: 0
|
||||
})
|
||||
|
||||
const filters = reactive({
|
||||
status: null,
|
||||
operator_name: null,
|
||||
principalpeople: null,
|
||||
staff_id: null,
|
||||
search: null,
|
||||
sort_order: 'desc'
|
||||
})
|
||||
|
||||
// --- 方法 ---
|
||||
// --- 核心数据加载方法 ---
|
||||
|
||||
|
||||
// 在 TicketManagement.vue 中利用 computed 提取
|
||||
const operatorsForFilter = computed(() => {
|
||||
// 返回包含 ID、姓名和公司名的对象数组
|
||||
return allCustomerEmployees.value.map(emp => {
|
||||
// 在 allCustomers 中找到该员工所属的公司名
|
||||
const company = customerList.value.find(c => c.customer_id === emp.customer_id);
|
||||
return {
|
||||
id: emp.employee_id,
|
||||
name: emp.employee_name,
|
||||
company: company ? company.company_name : '未知单位'
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 核心:获取全局统计数据
|
||||
* 逻辑:发送一个不带任何 filters 的请求,专门计算总数
|
||||
* 加载客户单位及对应员工数据
|
||||
* 根据你提供的接口返回结构进行了适配
|
||||
*/
|
||||
const loadCustomerData = async () => {
|
||||
try {
|
||||
const res = await customerApi.getCustomers()
|
||||
// 适配点:根据返回的 code: 200 逻辑进行判断
|
||||
if (res.code === 200) {
|
||||
// 适配点:直接从 res 中获取 customers 和 employees 数组
|
||||
customerList.value = res.customers || []
|
||||
allCustomerEmployees.value = res.employees || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取客户数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载我方员工列表
|
||||
*/
|
||||
const loadStaffData = async () => {
|
||||
try {
|
||||
const res = await ticketApi.getStaffs()
|
||||
if (res.code) {
|
||||
staffList.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取我方员工列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局统计数据
|
||||
*/
|
||||
const initGlobalStats = async () => {
|
||||
try {
|
||||
// 请求第一页,且不带任何筛选条件(filters)
|
||||
// 目的只是为了拿到全量数据来统计卡片
|
||||
const response = await ticketApi.getTickets({ page: 1, limit: 1000 })
|
||||
if (response.code) {
|
||||
const allData = response.data.data
|
||||
allStats.total = response.data.total
|
||||
allStats.pending = allData.filter(t => t.is_complete === '未分配').length
|
||||
allStats.ongoing = allData.filter(t => t.is_complete === '进行中').length
|
||||
allStats.completed = allData.filter(t => t.is_complete === '已完成' || t.is_complete === '已完结').length
|
||||
allStats.completed = allData.filter(t => t.is_complete === '已完成').length
|
||||
allStats.is_completed = allData.filter(t => t.is_complete === '已完结').length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('统计全局数据失败:', error)
|
||||
@@ -105,7 +193,7 @@ const initGlobalStats = async () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载列表数据(受分页和筛选影响)
|
||||
* 加载列表数据
|
||||
*/
|
||||
const loadTickets = async () => {
|
||||
loading.value = true
|
||||
@@ -116,10 +204,9 @@ const loadTickets = async () => {
|
||||
...filters
|
||||
}
|
||||
const response = await ticketApi.getTickets(params)
|
||||
// console.log('工单列表响应:', response)
|
||||
if (response.code) {
|
||||
tickets.value = response.data.data
|
||||
pagination.total = response.total
|
||||
tickets.value = response.data.data || []
|
||||
pagination.total = response.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载列表失败')
|
||||
@@ -128,195 +215,135 @@ const loadTickets = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选逻辑
|
||||
const handleFilterChange = (newFilters) => {
|
||||
Object.assign(filters, newFilters)
|
||||
pagination.page = 1
|
||||
loadTickets() // 只刷新列表,不刷新卡片
|
||||
}
|
||||
|
||||
const handleFilterReset = () => {
|
||||
Object.assign(filters, { status: null, operator_name: null, principalpeople: null, search: null, sort_order: 'desc' })
|
||||
pagination.page = 1
|
||||
loadTickets()
|
||||
}
|
||||
|
||||
// 新增/编辑/删除后,需要同时刷新列表和统计卡片
|
||||
const refreshAll = () => {
|
||||
loadTickets()
|
||||
initGlobalStats()
|
||||
}
|
||||
|
||||
const handleFormSubmit = async (formData) => {
|
||||
// ... 提交逻辑 ...
|
||||
// 成功后调用 refreshAll()
|
||||
// --- 事件处理 ---
|
||||
|
||||
const handleFilterChange = (newFilters) => {
|
||||
Object.assign(filters, newFilters)
|
||||
pagination.page = 1
|
||||
loadTickets()
|
||||
}
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
try {
|
||||
// 1. 弹出二次确认框,防止用户点错
|
||||
await ElMessageBox.confirm(
|
||||
'确定要删除该工单吗?此操作不可逆',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
|
||||
// 2. 开启 loading(如果有的话)
|
||||
// loading.value = true
|
||||
|
||||
// 3. 调用 API 发送删除请求
|
||||
const res = await ticketApi.deleteTicket(id)
|
||||
// console.log('删除响应:', res)
|
||||
|
||||
// 4. 根据你的后端返回结构判断是否成功
|
||||
// 假设后端返回 code 为 200 表示成功
|
||||
if (res.code) {
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
// 5. 成功后调用刷新列表的方法
|
||||
if (typeof refreshAll === 'function') {
|
||||
refreshAll()
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
// 处理取消操作或接口报错
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除出错:', error)
|
||||
ElMessage.error('系统错误,删除失败')
|
||||
}
|
||||
} finally {
|
||||
// loading.value = false
|
||||
}
|
||||
const handleFilterReset = () => {
|
||||
Object.assign(filters, {
|
||||
status: null,
|
||||
operator_name: null,
|
||||
staff_id: null,
|
||||
search: null,
|
||||
sort_order: 'desc'
|
||||
})
|
||||
pagination.page = 1
|
||||
loadTickets()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 新增任务:清空表单并打开弹窗
|
||||
*/
|
||||
const handleAdd = () => {
|
||||
isEditing.value = false
|
||||
// 初始化一个空的工单对象
|
||||
currentTicket.value = {
|
||||
website: '',
|
||||
operator: '',
|
||||
principalpeople: '',
|
||||
is_complete: '未分配'
|
||||
staff_id: null,
|
||||
is_complete: '未分配',
|
||||
remark: ''
|
||||
}
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 编辑任务:克隆数据并打开弹窗
|
||||
*/
|
||||
// 编辑工单
|
||||
const handleEdit = (row) => {
|
||||
isEditing.value = true
|
||||
// 使用解构赋值 {...row} 进行浅拷贝,避免修改弹窗时直接影响表格行
|
||||
currentTicket.value = { ...row }
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 加载运营人员列表(用于弹窗下拉框)
|
||||
*/
|
||||
const loadOperators = async () => {
|
||||
// 处理工单提交
|
||||
const handleFormSubmit = async (formData) => {
|
||||
try {
|
||||
const response = await ticketApi.getOperators()
|
||||
console.log('运营人员列表响应:', response.data)
|
||||
let response;
|
||||
if (isEditing.value) {
|
||||
response = await ticketApi.updateTicket(currentTicket.value.id, formData);
|
||||
} else {
|
||||
response = await ticketApi.addTicket(formData);
|
||||
}
|
||||
|
||||
// 这里的判断逻辑要根据你后端的实际返回结构调整
|
||||
// 如果后端直接返回数组:operators.value = response
|
||||
// 如果后端返回带 code 的结构:
|
||||
if (response.code) {
|
||||
operators.value = response.data || response
|
||||
ElMessage.success(isEditing.value ? '修改成功 ✨' : '创建成功 🚀');
|
||||
showAddDialog.value = false;
|
||||
refreshAll();
|
||||
} else {
|
||||
ElMessage.error(response.msg || '操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载运营人员失败:', error)
|
||||
ElMessage.error('提交失败,系统异常');
|
||||
}
|
||||
}
|
||||
|
||||
// 删除工单
|
||||
const handleDelete = async (id) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该工单吗?此操作不可逆', '提示', {
|
||||
type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消'
|
||||
})
|
||||
const res = await ticketApi.deleteTicket(id)
|
||||
// console.log('删除',res)
|
||||
if (res.code) {
|
||||
ElMessage.success('删除成功')
|
||||
refreshAll()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载所有运营人员
|
||||
const loadOperators = async () => {
|
||||
try {
|
||||
const response = await ticketApi.getOperators()
|
||||
if (response.code) {
|
||||
operators.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取运营人员筛选列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 分页逻辑
|
||||
const handleSizeChange = (val) => { pagination.limit = val; loadTickets(); }
|
||||
const handleCurrentChange = (val) => { pagination.page = val; loadTickets(); }
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadTickets() // 渲染表格
|
||||
initGlobalStats() // 锁定卡片统计
|
||||
loadTickets()
|
||||
initGlobalStats()
|
||||
loadOperators()
|
||||
loadStaffData()
|
||||
loadCustomerData() // 级联数据加载
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 保持原有样式不变 */
|
||||
.ticket-management {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
border-left: 5px solid #409eff;
|
||||
}
|
||||
|
||||
.stat-item.pending {
|
||||
border-left-color: #909399;
|
||||
}
|
||||
|
||||
.stat-item.ongoing {
|
||||
border-left-color: #e6a23c;
|
||||
}
|
||||
|
||||
.stat-item.success {
|
||||
border-left-color: #67c23a;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
/* 样式部分保持不变 */
|
||||
.ticket-management { padding: 20px; background-color: #f8fafc; min-height: 100vh; }
|
||||
.stats-container { display: grid; grid-template-columns: repeat(5, minmax(150px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
||||
.stat-card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); display: flex; justify-content: space-between; align-items: center; transition: all 0.3s ease; border-bottom: 3px solid transparent; }
|
||||
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); }
|
||||
.total { border-bottom-color: #409eff; }
|
||||
.total .stat-icon { color: #409eff; background: #ecf5ff; }
|
||||
.pending { border-bottom-color: #909399; }
|
||||
.pending .stat-icon { color: #909399; background: #f4f4f5; }
|
||||
.ongoing { border-bottom-color: #e6a23c; }
|
||||
.ongoing .stat-icon { color: #e6a23c; background: #fdf6ec; }
|
||||
.success { border-bottom-color: #67c23a; }
|
||||
.success .stat-icon { color: #67c23a; background: #f0f9eb; }
|
||||
.finished { border-bottom-color: #009688; }
|
||||
.finished .stat-icon { color: #009688; background: #e6f6f5; }
|
||||
.stat-label { font-size: 13px; color: #64748b; margin-bottom: 8px; font-weight: 500; }
|
||||
.stat-value { font-size: 24px; font-weight: 700; color: #1e293b; }
|
||||
.stat-icon { font-size: 24px; padding: 12px; border-radius: 10px; }
|
||||
.main-content { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); }
|
||||
.action-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.pagination-container { margin-top: 20px; display: flex; justify-content: flex-end; }
|
||||
@media (max-width: 1400px) { .stats-container { grid-template-columns: repeat(3, 1fr); } }
|
||||
@media (max-width: 768px) { .stats-container { grid-template-columns: 1fr 1fr; } }
|
||||
</style>
|
||||
409
src/views/UserView.vue
Normal file
409
src/views/UserView.vue
Normal file
@@ -0,0 +1,409 @@
|
||||
<template>
|
||||
<div class="user-view-container">
|
||||
<div class="user-header-card">
|
||||
<div class="header-main">
|
||||
<div class="welcome-container">
|
||||
<div class="welcome-section">
|
||||
<h2 class="greeting">{{ welcomeMessage }},{{ currentUser.username }}</h2>
|
||||
<p class="sub-text">欢迎回到你的垃圾堆,开启牛马的一天。</p>
|
||||
</div>
|
||||
|
||||
<div class="identity-badges">
|
||||
<el-space :size="8">
|
||||
<el-tag v-if="realStaffInfo" type="warning" effect="plain" round size="small">
|
||||
<el-icon><User /></el-icon> 牛马身份: {{ realStaffInfo.name }}
|
||||
</el-tag>
|
||||
<el-tag v-else type="danger" round size="small">未录入员工表,无权接单</el-tag>
|
||||
</el-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-action">
|
||||
<el-button type="danger" plain @click="handleLogout">
|
||||
<el-icon><SwitchButton /></el-icon> 退出登录
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-tabs-wrapper">
|
||||
<el-radio-group v-model="currentStatus" @change="handleStatusChange" class="custom-radio-group">
|
||||
<el-radio-button label="未分配">
|
||||
<el-icon><Notification /></el-icon> 待分配任务
|
||||
</el-radio-button>
|
||||
<el-radio-button label="进行中">
|
||||
<el-icon><Loading v-if="loading" /><Timer v-else /></el-icon> 我的进行中
|
||||
</el-radio-button>
|
||||
<el-radio-button label="已完成">
|
||||
<el-icon><CircleCheck /></el-icon> 我的已完成
|
||||
</el-radio-button>
|
||||
<el-radio-button label="已完结">
|
||||
<el-icon><Checked /></el-icon> 我的已完结
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<el-table :data="paginatedTickets" v-loading="loading" border style="width: 100%; margin-top: 20px" class="custom-table">
|
||||
<template #empty>
|
||||
<el-empty :image-size="120" :description="emptyText">
|
||||
<el-button type="primary" plain @click="fetchTickets">刷新列表</el-button>
|
||||
</el-empty>
|
||||
</template>
|
||||
|
||||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||
<el-table-column prop="website" label="网站名称" min-width="150" show-overflow-tooltip />
|
||||
|
||||
<el-table-column label="数据量" width="150" align="center">
|
||||
<template #default="scope">
|
||||
<div class="cell-content-card">
|
||||
<div class="data-display-area">
|
||||
<span v-if="scope.row.datanum && scope.row.datanum !== '0'" class="display-val">{{ scope.row.datanum }}</span>
|
||||
<span v-else class="no-data-placeholder">-</span>
|
||||
</div>
|
||||
<el-popover v-if="currentStatus !== '未分配'" placement="top" :width="200" trigger="click" @show="prepareEdit(scope.row, 'datanum')">
|
||||
<template #reference>
|
||||
<el-button type="primary" size="small" link class="edit-link-btn">
|
||||
{{ (scope.row.datanum && scope.row.datanum !== '0') ? '[ 修改数据 ]' : '[ 输入数据 ]' }}
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="popover-content">
|
||||
<div class="pop-header">录入站点数据量</div>
|
||||
<el-input v-model="editValue" placeholder="请输入数值" size="small" clearable />
|
||||
<div class="pop-footer">
|
||||
<el-button type="primary" size="small" @click="submitEdit(scope.row, 'datanum')">确认提交</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="任务进度" width="150" align="center">
|
||||
<template #default="scope">
|
||||
<div class="cell-content-card">
|
||||
<div class="data-display-area" style="width: 100%; padding: 0 10px;">
|
||||
<el-progress
|
||||
:percentage="Number(scope.row.progress || 0)"
|
||||
:status="Number(scope.row.progress) === 100 ? 'success' : ''"
|
||||
:stroke-width="12"
|
||||
striped
|
||||
striped-flow
|
||||
/>
|
||||
</div>
|
||||
<el-popover v-if="currentStatus === '进行中'" placement="top" :width="240" trigger="click" @show="prepareEdit(scope.row, 'progress')">
|
||||
<template #reference>
|
||||
<el-button type="primary" size="small" link class="edit-link-btn">
|
||||
[ 更新进度 ]
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="popover-content">
|
||||
<div class="pop-header">调整当前进度 (%)</div>
|
||||
<el-radio-group v-model="editValue" size="small" class="progress-radio-group">
|
||||
<el-radio-button :label="0">0</el-radio-button>
|
||||
<el-radio-button :label="30">30</el-radio-button>
|
||||
<el-radio-button :label="50">50</el-radio-button>
|
||||
<el-radio-button :label="70">70</el-radio-button>
|
||||
<el-radio-button :label="100">100</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div class="pop-footer">
|
||||
<el-button type="primary" size="small" @click="submitEdit(scope.row, 'progress')">确定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="operator" label="运营" width="120" align="center" />
|
||||
|
||||
<el-table-column label="备注" min-width="150" align="center">
|
||||
<template #default="scope">
|
||||
<div class="cell-content-card">
|
||||
<div class="data-display-area">
|
||||
<div v-if="scope.row.remark" class="display-remark" :title="scope.row.remark">{{ scope.row.remark }}</div>
|
||||
<span v-else class="no-data-placeholder">-</span>
|
||||
</div>
|
||||
<el-popover v-if="currentStatus !== '未分配'" placement="top" :width="250" trigger="click" @show="prepareEdit(scope.row, 'remark')">
|
||||
<template #reference>
|
||||
<el-button type="warning" size="small" link class="edit-link-btn">
|
||||
{{ scope.row.remark ? '[ 修改备注 ]' : '[ 输入备注 ]' }}
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="popover-content">
|
||||
<div class="pop-header">录入工单备注</div>
|
||||
<el-input v-model="editValue" type="textarea" :rows="3" placeholder="请输入备注" size="small" />
|
||||
<div class="pop-footer">
|
||||
<el-button type="primary" size="small" @click="submitEdit(scope.row, 'remark')">提交保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="['已完成', '已完结'].includes(currentStatus) ? '锁定日期' : '创建日期'" width="140" align="center">
|
||||
<template #default="scope">
|
||||
<span class="pure-date">
|
||||
{{ formatDateOnly(['已完成', '已完结'].includes(currentStatus) ? scope.row.lock_time : scope.row.create_time) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button v-if="scope.row.is_complete === '未分配'" type="primary" size="small" :disabled="!realStaffInfo" @click="handleAccept(scope.row)">立即接单</el-button>
|
||||
<el-button v-if="scope.row.is_complete === '进行中' && String(scope.row.staff_id) === String(realStaffInfo?.id)" type="success" size="small" @click="handleFinish(scope.row)">完成任务</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="filteredTickets.length"
|
||||
:page-sizes="[15, 30, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { User, SwitchButton, Timer, Notification, CircleCheck, Checked, Loading } from '@element-plus/icons-vue'
|
||||
import { ticketApi } from '@/api/ticket'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const allTickets = ref([])
|
||||
const currentStatus = ref('未分配')
|
||||
const realStaffInfo = ref(null)
|
||||
const editValue = ref('')
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(15)
|
||||
|
||||
const formatDateOnly = (timestamp) => {
|
||||
if (!timestamp || timestamp === '0' || timestamp === '') return '-';
|
||||
let date = !isNaN(timestamp) ? new Date(String(timestamp).length === 10 ? timestamp * 1000 : parseInt(timestamp)) : new Date(timestamp);
|
||||
if (isNaN(date.getTime())) return '-';
|
||||
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// js计算时间
|
||||
const welcomeMessage = computed(() => {
|
||||
const hour = new Date().getHours()
|
||||
if (hour < 6) return '凌晨好'; if (hour < 9) return '早上好'; if (hour < 12) return '上午好';
|
||||
if (hour < 14) return '中午好'; if (hour < 18) return '下午好'; return '晚上好'
|
||||
})
|
||||
|
||||
// 获取当前用户信息
|
||||
const currentUser = computed(() => {
|
||||
const info = localStorage.getItem('user_info') || sessionStorage.getItem('user_info')
|
||||
return info ? JSON.parse(info) : { id: null, username: '未知' }
|
||||
})
|
||||
|
||||
// 获取工单信息
|
||||
const fetchTickets = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await ticketApi.getTickets({ page: 1, limit: 1000 })
|
||||
// console.log(res)
|
||||
if (res.code === 200 || res.code === true) allTickets.value = res.data.data
|
||||
} catch (e) { ElMessage.error('获取列表失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
// 过滤工单
|
||||
const filteredTickets = computed(() => {
|
||||
return allTickets.value.filter(ticket => {
|
||||
if (ticket.is_complete !== currentStatus.value) return false
|
||||
if (currentStatus.value !== '未分配') return String(ticket.staff_id) === String(realStaffInfo.value?.id)
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
const paginatedTickets = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return filteredTickets.value.slice(start, end)
|
||||
})
|
||||
|
||||
const handleSizeChange = (val) => { pageSize.value = val; currentPage.value = 1; }
|
||||
const handleCurrentChange = (val) => { currentPage.value = val; }
|
||||
const handleStatusChange = () => { currentPage.value = 1; fetchTickets(); }
|
||||
|
||||
const prepareEdit = (row, field) => { editValue.value = row[field] || '' }
|
||||
|
||||
const submitEdit = async (row, field) => {
|
||||
try {
|
||||
const payload = {
|
||||
datanum: field === 'datanum' ? String(editValue.value) : String(row.datanum || ''),
|
||||
remark: field === 'remark' ? editValue.value : (row.remark || ''),
|
||||
progress: field === 'progress' ? Number(editValue.value) : Number(row.progress || 0)
|
||||
}
|
||||
const res = await ticketApi.updateTicket(row.id, payload)
|
||||
if (res.code === 200 || res.code === true) {
|
||||
row[field] = editValue.value;
|
||||
ElMessage.success('更新成功');
|
||||
document.body.click()
|
||||
}
|
||||
} catch (e) { ElMessage.error('操作失败') }
|
||||
}
|
||||
|
||||
// 点击接受工单
|
||||
const handleAccept = (row) => {
|
||||
ElMessageBox.confirm(`确认接手工单 #${row.id}?`).then(async () => {
|
||||
try {
|
||||
const res = await ticketApi.updateTicket(row.id, { staff_id: realStaffInfo.value.id, is_complete: '进行中', progress: 0 })
|
||||
if (res.code === 200 || res.code === true) { ElMessage.success('接单成功'); fetchTickets() }
|
||||
} catch (e) { ElMessage.error('操作失败') }
|
||||
})
|
||||
}
|
||||
|
||||
// 点击完成
|
||||
const handleFinish = (row) => {
|
||||
ElMessageBox.confirm('确认已完成任务?').then(async () => {
|
||||
try {
|
||||
const currentTimestamp = Math.floor(Date.now() / 1000);
|
||||
// 关键修改:在提交时增加 progress: 100
|
||||
const res = await ticketApi.updateTicket(row.id, {
|
||||
is_complete: '已完成',
|
||||
lock_time: String(currentTimestamp),
|
||||
progress: 100 // 默认改为100
|
||||
})
|
||||
|
||||
if (res.code === 200 || res.code === true) {
|
||||
ElMessage.success('任务已完成,进度已更新为100%');
|
||||
fetchTickets()
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
ElMessageBox.confirm('确定退出当前账号?', '提示', { type: 'warning' }).then(() => {
|
||||
localStorage.clear(); sessionStorage.clear(); router.push('/login')
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await ticketApi.getStaffs()
|
||||
if (res.code === 200 || res.code === true) {
|
||||
const match = (res.data || []).find(s => s.name === currentUser.value.username)
|
||||
if (match) realStaffInfo.value = match
|
||||
}
|
||||
} catch (e) {}
|
||||
fetchTickets()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 容器布局 */
|
||||
.header-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.welcome-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px; /* 这里统一控制欢迎语、副标题、标签之间的垂直间距 */
|
||||
}
|
||||
|
||||
/* 清除标题默认的垂直 margin,防止撑开间距 */
|
||||
.greeting {
|
||||
margin: 0;
|
||||
color: #1a1a1a;
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 副标题紧贴标题 */
|
||||
.sub-text {
|
||||
margin: 4px 0 0 0; /* 仅顶部保留微小间距 */
|
||||
color: #8c8c8c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 标签组位置微调 */
|
||||
.identity-badges {
|
||||
margin-top: 4px; /* 让标签组靠近上方的文字 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 针对 el-tag 内部图标和文字的微调 */
|
||||
:deep(.el-tag) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-style: dashed; /* 既然是牛马,边框可以搞成虚线更有自嘲感 */
|
||||
}
|
||||
|
||||
|
||||
.user-view-container { padding: 24px; background-color: #f0f2f5; min-height: 100vh; }
|
||||
.user-header-card { background: white; padding: 24px; border-radius: 12px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.04); }
|
||||
.greeting { margin: 0 0 8px 0; color: #1a1a1a; font-size: 24px; }
|
||||
.status-tabs-wrapper { background: white; padding: 8px; border-radius: 8px; display: inline-block; box-shadow: 0 2px 8px rgba(0,0,0,0.04); }
|
||||
.custom-radio-group :deep(.el-radio-button__inner) { border: none !important; border-radius: 6px !important; margin: 0 4px; padding: 10px 20px; display: flex; align-items: center; gap: 6px; }
|
||||
|
||||
.custom-table { border-radius: 12px; overflow: hidden; box-shadow: 0 4px 16px rgba(0,0,0,0.05); }
|
||||
.cell-content-card { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; min-height: 60px; }
|
||||
.data-display-area { min-height: 24px; display: flex; align-items: center; justify-content: center; width: 100%; }
|
||||
.display-val { font-weight: 700; color: #409eff; font-size: 15px; }
|
||||
.display-remark { font-size: 12px; color: #595959; max-width: 180px; line-height: 1.4; background: #f5f7fa; padding: 4px 10px; border-radius: 4px; text-align: center; }
|
||||
.no-data-placeholder { color: #dcdfe6; font-size: 12px; }
|
||||
.pure-date { color: #606266; font-weight: 500; font-family: monospace; font-size: 14px; }
|
||||
.edit-link-btn { font-size: 12px; height: auto; padding: 0; opacity: 0.7; }
|
||||
.edit-link-btn:hover { opacity: 1; text-decoration: underline; }
|
||||
|
||||
.pagination-container { margin-top: 24px; display: flex; justify-content: flex-end; padding: 10px 20px; background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.04); }
|
||||
|
||||
.progress-radio-group { display: flex; justify-content: center; margin: 10px 0; }
|
||||
.progress-radio-group :deep(.el-radio-button__inner) { padding: 8px 10px !important; }
|
||||
|
||||
:deep(.el-table__row) { height: 100px; }
|
||||
:deep(.el-table .cell) { overflow: visible !important; }
|
||||
.pop-header { font-size: 14px; font-weight: 600; margin-bottom: 12px; color: #434343; text-align: center; }
|
||||
.pop-footer { margin-top: 12px; text-align: right; }
|
||||
|
||||
|
||||
/* 1. 强行修正进度条内部容器的对齐方式 */
|
||||
:deep(.el-progress-bar__innerText) {
|
||||
display: flex !important;
|
||||
align-items: center !important; /* 垂直居中 */
|
||||
justify-content: center !important; /* 水平居中 */
|
||||
height: 100% !important;
|
||||
line-height: 1 !important;
|
||||
/* 移除默认的基线对齐 */
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
/* 2. 针对 100% 时出现的那个 svg 图标进行位移修正 */
|
||||
:deep(.el-progress-bar__innerText svg),
|
||||
:deep(.el-progress-bar__innerText .el-icon) {
|
||||
margin: 0 !important;
|
||||
/* 关键:利用相对定位向上平移 1px,抵消视觉上的下沉 */
|
||||
position: relative !important;
|
||||
top: -1px !important;
|
||||
/* 稍微调整图标大小,让它在 10px 的进度条里更协调 */
|
||||
width: 0.8em;
|
||||
height: 0.8em;
|
||||
}
|
||||
|
||||
/* 3. 如果还是觉得整体偏,可以给进度条容器加一个微调 */
|
||||
:deep(.el-progress) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: '/', // 添加这一行,确保部署在根目录或通过Nginx运行目录指向时路径正确
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
@@ -16,10 +17,10 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 8080, // 前端启动的端口
|
||||
port: 80, // 前端启动的端口
|
||||
proxy: {
|
||||
'/api': { // 所有以api开头的请求都会被代理
|
||||
target: 'http://localhost:80',
|
||||
target: 'http://112.5.15.136:25536',
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user