Compare commits

...

3 Commits

Author SHA1 Message Date
Mr.j 9be59bee8a 优化完成设备管理和培训模块的加入到devlopment 2025-08-06 15:35:51 +08:00
Mr.j b44bcebf0f 解决冲突并合并 2025-08-06 14:59:01 +08:00
Mr.j 45d54c50d5 把设备管理和培训模块加入到devlopment分支 2025-08-06 14:53:27 +08:00
31 changed files with 7478 additions and 483 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
node_modules
dist
.gitignore
.env.development
.env.development

580
package-lock.json generated
View File

@ -60,21 +60,27 @@
"devDependencies": {
"@antfu/eslint-config": "^2.16.3",
"@arco-design/web-vue": "^2.57.0",
"@eslint/js": "^9.32.0",
"@types/crypto-js": "^4.2.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.2.5",
"@types/query-string": "^6.3.0",
"@typescript-eslint/eslint-plugin": "^8.39.0",
"@typescript-eslint/parser": "^8.39.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/tsconfig": "^0.1.3",
"boxen": "^7.1.1",
"eslint": "^9.0.0",
"eslint": "^9.32.0",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
"less": "^4.1.3",
"less-loader": "^11.0.0",
"picocolors": "^1.0.0",
"sass": "^1.62.1",
"sass-loader": "^13.2.2",
"typescript": "~5.0.4",
"typescript-eslint": "^8.39.0",
"unplugin-auto-import": "^0.16.4",
"unplugin-vue-components": "^0.25.1",
"vite": "^5.1.5",
@ -210,6 +216,18 @@
}
}
},
"node_modules/@antfu/eslint-config/node_modules/globals": {
"version": "15.15.0",
"resolved": "https://registry.npmmirror.com/globals/-/globals-15.15.0.tgz",
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
"dev": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@antfu/install-pkg": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.4.1.tgz",
@ -3324,17 +3342,16 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
"integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==",
"version": "8.39.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz",
"integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/type-utils": "8.38.0",
"@typescript-eslint/utils": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"@typescript-eslint/scope-manager": "8.39.0",
"@typescript-eslint/type-utils": "8.39.0",
"@typescript-eslint/utils": "8.39.0",
"@typescript-eslint/visitor-keys": "8.39.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@ -3348,9 +3365,9 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.38.0",
"@typescript-eslint/parser": "^8.39.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
@ -3364,16 +3381,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
"version": "8.39.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.39.0.tgz",
"integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"@typescript-eslint/scope-manager": "8.39.0",
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/typescript-estree": "8.39.0",
"@typescript-eslint/visitor-keys": "8.39.0",
"debug": "^4.3.4"
},
"engines": {
@ -3385,18 +3401,17 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
"integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
"version": "8.39.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.39.0.tgz",
"integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.38.0",
"@typescript-eslint/types": "^8.38.0",
"@typescript-eslint/tsconfig-utils": "^8.39.0",
"@typescript-eslint/types": "^8.39.0",
"debug": "^4.3.4"
},
"engines": {
@ -3407,18 +3422,17 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz",
"integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==",
"version": "8.39.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz",
"integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0"
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/visitor-keys": "8.39.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -3429,11 +3443,10 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
"integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
"version": "8.39.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz",
"integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@ -3442,19 +3455,18 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz",
"integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==",
"version": "8.39.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz",
"integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0",
"@typescript-eslint/utils": "8.38.0",
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/typescript-estree": "8.39.0",
"@typescript-eslint/utils": "8.39.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@ -3467,15 +3479,14 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
"integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==",
"version": "8.39.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.39.0.tgz",
"integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@ -3485,16 +3496,15 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
"integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
"version": "8.39.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz",
"integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.38.0",
"@typescript-eslint/tsconfig-utils": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"@typescript-eslint/project-service": "8.39.0",
"@typescript-eslint/tsconfig-utils": "8.39.0",
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/visitor-keys": "8.39.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -3510,20 +3520,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
"integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
"version": "8.39.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.39.0.tgz",
"integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0"
"@typescript-eslint/scope-manager": "8.39.0",
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/typescript-estree": "8.39.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -3534,17 +3543,16 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz",
"integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==",
"version": "8.39.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz",
"integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/types": "8.39.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@ -4823,6 +4831,28 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/array-includes": {
"version": "3.1.9",
"resolved": "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.9.tgz",
"integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"define-properties": "^1.2.1",
"es-abstract": "^1.24.0",
"es-object-atoms": "^1.1.1",
"get-intrinsic": "^1.3.0",
"is-string": "^1.1.1",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/array-unique": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
@ -4833,6 +4863,78 @@
"node": ">=0.10.0"
}
},
"node_modules/array.prototype.findlast": {
"version": "1.2.5",
"resolved": "https://registry.npmmirror.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
"integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
"es-abstract": "^1.23.2",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"es-shim-unscopables": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/array.prototype.flat": {
"version": "1.3.3",
"resolved": "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
"integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.8",
"define-properties": "^1.2.1",
"es-abstract": "^1.23.5",
"es-shim-unscopables": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/array.prototype.flatmap": {
"version": "1.3.3",
"resolved": "https://registry.npmmirror.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
"integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.8",
"define-properties": "^1.2.1",
"es-abstract": "^1.23.5",
"es-shim-unscopables": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/array.prototype.tosorted": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
"integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
"es-abstract": "^1.23.3",
"es-errors": "^1.3.0",
"es-shim-unscopables": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/arraybuffer.prototype.slice": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
@ -6437,6 +6539,18 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz",
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
"dev": true,
"dependencies": {
"esutils": "^2.0.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/dom-serializer": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
@ -6764,6 +6878,33 @@
"node": ">= 0.4"
}
},
"node_modules/es-iterator-helpers": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
"integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.3",
"define-properties": "^1.2.1",
"es-abstract": "^1.23.6",
"es-errors": "^1.3.0",
"es-set-tostringtag": "^2.0.3",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.6",
"globalthis": "^1.0.4",
"gopd": "^1.2.0",
"has-property-descriptors": "^1.0.2",
"has-proto": "^1.2.0",
"has-symbols": "^1.1.0",
"internal-slot": "^1.1.0",
"iterator.prototype": "^1.1.4",
"safe-array-concat": "^1.1.3"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz",
@ -6798,6 +6939,18 @@
"node": ">= 0.4"
}
},
"node_modules/es-shim-unscopables": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
"integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
"dev": true,
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-to-primitive": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
@ -7645,6 +7798,18 @@
"eslint": ">=8.23.0"
}
},
"node_modules/eslint-plugin-n/node_modules/globals": {
"version": "15.15.0",
"resolved": "https://registry.npmmirror.com/globals/-/globals-15.15.0.tgz",
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
"dev": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-plugin-no-only-tests": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.3.0.tgz",
@ -7692,6 +7857,86 @@
}
}
},
"node_modules/eslint-plugin-react": {
"version": "7.37.5",
"resolved": "https://registry.npmmirror.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
"integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
"dev": true,
"dependencies": {
"array-includes": "^3.1.8",
"array.prototype.findlast": "^1.2.5",
"array.prototype.flatmap": "^1.3.3",
"array.prototype.tosorted": "^1.1.4",
"doctrine": "^2.1.0",
"es-iterator-helpers": "^1.2.1",
"estraverse": "^5.3.0",
"hasown": "^2.0.2",
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
"minimatch": "^3.1.2",
"object.entries": "^1.1.9",
"object.fromentries": "^2.0.8",
"object.values": "^1.2.1",
"prop-types": "^15.8.1",
"resolve": "^2.0.0-next.5",
"semver": "^6.3.1",
"string.prototype.matchall": "^4.0.12",
"string.prototype.repeat": "^1.0.0"
},
"engines": {
"node": ">=4"
},
"peerDependencies": {
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
}
},
"node_modules/eslint-plugin-react/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/eslint-plugin-react/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/eslint-plugin-react/node_modules/resolve": {
"version": "2.0.0-next.5",
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.5.tgz",
"integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
"dev": true,
"dependencies": {
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/eslint-plugin-react/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/eslint-plugin-regexp": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.9.0.tgz",
@ -7786,6 +8031,18 @@
"eslint": ">=8.56.0"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/globals": {
"version": "15.15.0",
"resolved": "https://registry.npmmirror.com/globals/-/globals-15.15.0.tgz",
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
"dev": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-plugin-unused-imports": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz",
@ -8839,11 +9096,10 @@
"peer": true
},
"node_modules/globals": {
"version": "15.15.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
"version": "16.3.0",
"resolved": "https://registry.npmmirror.com/globals/-/globals-16.3.0.tgz",
"integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
@ -9995,6 +10251,23 @@
"node": ">=0.10.0"
}
},
"node_modules/iterator.prototype": {
"version": "1.1.5",
"resolved": "https://registry.npmmirror.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
"integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
"dev": true,
"dependencies": {
"define-data-property": "^1.1.4",
"es-object-atoms": "^1.0.0",
"get-intrinsic": "^1.2.6",
"get-proto": "^1.0.0",
"has-symbols": "^1.1.0",
"set-function-name": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/jest-worker": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
@ -10191,6 +10464,21 @@
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmmirror.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
"integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
"dev": true,
"dependencies": {
"array-includes": "^3.1.6",
"array.prototype.flat": "^1.3.1",
"object.assign": "^4.1.4",
"object.values": "^1.1.6"
},
"engines": {
"node": ">=4.0"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -10576,6 +10864,18 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@ -11412,6 +11712,39 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object.entries": {
"version": "1.1.9",
"resolved": "https://registry.npmmirror.com/object.entries/-/object.entries-1.1.9.tgz",
"integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"define-properties": "^1.2.1",
"es-object-atoms": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/object.fromentries": {
"version": "2.0.8",
"resolved": "https://registry.npmmirror.com/object.fromentries/-/object.fromentries-2.0.8.tgz",
"integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
"es-abstract": "^1.23.2",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object.pick": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
@ -11435,6 +11768,24 @@
"node": ">=0.10.0"
}
},
"node_modules/object.values": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/object.values/-/object.values-1.2.1.tgz",
"integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.3",
"define-properties": "^1.2.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@ -12053,6 +12404,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/prosemirror-changeset": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz",
@ -12365,6 +12727,12 @@
"safe-buffer": "^5.1.0"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
"node_modules/read-pkg": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@ -13897,6 +14265,43 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string.prototype.matchall": {
"version": "4.0.12",
"resolved": "https://registry.npmmirror.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
"integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.3",
"define-properties": "^1.2.1",
"es-abstract": "^1.23.6",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"get-intrinsic": "^1.2.6",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"internal-slot": "^1.1.0",
"regexp.prototype.flags": "^1.5.3",
"set-function-name": "^2.0.2",
"side-channel": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/string.prototype.repeat": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
"integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
"dev": true,
"dependencies": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5"
}
},
"node_modules/string.prototype.trim": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
@ -14888,6 +15293,29 @@
"node": ">=12.20"
}
},
"node_modules/typescript-eslint": {
"version": "8.39.0",
"resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.39.0.tgz",
"integrity": "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==",
"dev": true,
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.39.0",
"@typescript-eslint/parser": "8.39.0",
"@typescript-eslint/typescript-estree": "8.39.0",
"@typescript-eslint/utils": "8.39.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",

View File

@ -66,21 +66,27 @@
"devDependencies": {
"@antfu/eslint-config": "^2.16.3",
"@arco-design/web-vue": "^2.57.0",
"@eslint/js": "^9.32.0",
"@types/crypto-js": "^4.2.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.2.5",
"@types/query-string": "^6.3.0",
"@typescript-eslint/eslint-plugin": "^8.39.0",
"@typescript-eslint/parser": "^8.39.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/tsconfig": "^0.1.3",
"boxen": "^7.1.1",
"eslint": "^9.0.0",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
"less": "^4.1.3",
"less-loader": "^11.0.0",
"picocolors": "^1.0.0",
"sass": "^1.62.1",
"sass-loader": "^13.2.2",
"typescript": "~5.0.4",
"typescript-eslint": "^8.39.0",
"unplugin-auto-import": "^0.16.4",
"unplugin-vue-components": "^0.25.1",
"vite": "^5.1.5",

View File

@ -26,6 +26,15 @@ export function getFolderTreeApi() {
})
}
// 获取文件夹列表
export function getFolderListApi(params?: { page?: number; pageSize?: number; folderName?: string }) {
return request<{ list: KnowledgeFolder[]; total: number }>({
url: '/knowledge/folder-list',
method: 'get',
params,
})
}
// 获取文件列表(按文件夹)
export function getFilesApi(folderId: string) {
return request<KnowledgeFile[]>({
@ -35,17 +44,7 @@ export function getFilesApi(folderId: string) {
})
}
// 获取文件夹列表
export function getFolderListApi(params: { page: number; pageSize: number; folderName?: string }) {
return request<{
list: KnowledgeFolder[]
total: number
}>({
url: '/knowledge/folder-list',
method: 'get',
params,
})
}
// 创建文件夹
export function createFolderApi(data: { name: string; parentId?: string }) {

View File

@ -46,4 +46,7 @@ export function assignEquipment(equipmentId: string, userId: string) {
/** @desc 设备归还 */
export function returnEquipment(equipmentId: string) {
return http.put(`${BASE_URL}/${equipmentId}/return`)
}
}
// 导出设备采购 API
export * from './procurement'

View File

@ -0,0 +1,125 @@
import http from '@/utils/http'
import type { EquipmentListReq, EquipmentReq, EquipmentResp } from './type'
/**
* API
*/
export const equipmentProcurementApi = {
/**
*
*/
page: (params: EquipmentListReq) => {
console.log('🔍 API - equipmentProcurementApi.page 被调用')
console.log('🔍 API - 接收到的参数:', params)
console.log('🔍 API - 参数类型:', typeof params)
console.log('🔍 API - 参数的键值对:')
Object.entries(params).forEach(([key, value]) => {
console.log(` ${key}: ${value} (${typeof value})`)
})
// 确保参数格式正确
const requestParams = {
...params,
// 确保分页参数存在
page: params.page || 1,
pageSize: params.pageSize || 10,
}
console.log('🔍 API - 最终请求参数:', requestParams)
console.log('🔍 API - 准备发送GET请求到 /equipment/procurement/page')
console.log('🔍 API - 请求参数序列化前:', requestParams)
// 手动序列化参数进行调试使用URLSearchParams
const searchParams = new URLSearchParams()
Object.entries(requestParams).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
searchParams.append(key, String(value))
}
})
console.log('🔍 API - 手动序列化后的参数:', searchParams.toString())
// 参考设备模块的调用方式,直接将参数作为第二个参数传递
return http.get<ApiRes<PageRes<EquipmentResp>>>('/equipment/procurement/page', requestParams)
},
/**
* - 使API文档中的参数格式
*/
testPage: () => {
console.log('🧪 API - 测试参数传递')
// 使用API文档中的参数格式进行测试
const testParams = {
page: 1,
pageSize: 10,
equipmentName: '测试设备',
supplierName: '测试供应商',
quantity: 10,
unitPrice: 100.50,
totalPrice: 1005.00,
accountNumber: 'TEST001',
brand: '测试品牌',
locationStatus: 'spare',
physicalLocation: '测试位置',
purchaseOrder: 'PO001',
inventoryBasis: '测试依据',
dynamicRecord: '测试记录'
}
console.log('🧪 API - 测试参数:', testParams)
return http.get<ApiRes<PageRes<EquipmentResp>>>('/equipment/procurement/page', testParams)
},
/**
*
*/
add: (data: EquipmentReq) => {
return http.post<ApiRes<null>>('/equipment/procurement', data)
},
/**
*
*/
update: (equipmentId: string, data: EquipmentReq) => {
return http.put<ApiRes<null>>(`/equipment/procurement/${equipmentId}`, data)
},
/**
*
*/
delete: (equipmentId: string) => {
return http.del<ApiRes<null>>(`/equipment/procurement/${equipmentId}`)
},
/**
*
*/
detail: (equipmentId: string) => {
return http.get<ApiRes<EquipmentResp>>(`/equipment/procurement/detail/${equipmentId}`)
},
/**
*
*/
getStats: () => {
return http.get<ApiRes<unknown>>('/equipment/procurement/stats')
},
/**
*
*/
batchDelete: (equipmentIds: string[]) => {
return http.del<ApiRes<null>>('/equipment/procurement/batch', { data: equipmentIds })
},
/**
*
*/
export: (params: EquipmentListReq) => {
return http.get<Blob>('/equipment/procurement/export', {
params,
responseType: 'blob'
})
}
}

View File

@ -17,6 +17,9 @@ export * as HealthRecordAPI from './health-record'
export * as InsuranceFileAPI from './insurance-file'
export * as EmployeeAPI from './employee'
export * as RegulationAPI from './regulation'
export * as TrainingAPI from './training'
export * as EquipmentAPI from './equipment'
export * as BussinessAPI from './bussiness/bussiness'
export * from './area/type'
export * from './auth/type'

View File

@ -0,0 +1,44 @@
import http from '@/utils/http'
import type * as T from '@/types/training.d'
const BASE_URL = '/training'
/** @desc 分页查询培训计划列表 */
export function pageTrainingPlan(query: T.TrainingPlanPageQuery) {
return http.get<T.TrainingPlanResp[]>(`${BASE_URL}/plan/page`, query)
}
/** @desc 查询培训计划列表 */
export function listTrainingPlan(query?: T.TrainingPlanPageQuery) {
return http.get<T.TrainingPlanResp[]>(`${BASE_URL}/plan/list`, query)
}
/** @desc 查询培训计划详情 */
export function getTrainingPlanDetail(planId: string) {
return http.get<T.TrainingPlanResp>(`${BASE_URL}/plan/detail/${planId}`)
}
/** @desc 新增培训计划 */
export function createTrainingPlan(data: T.TrainingPlanReq) {
return http.post(`${BASE_URL}/plan`, data)
}
/** @desc 更新培训计划 */
export function updateTrainingPlan(planId: string, data: T.TrainingPlanReq) {
return http.put(`${BASE_URL}/plan/${planId}`, data)
}
/** @desc 删除培训计划 */
export function deleteTrainingPlan(planId: string) {
return http.del(`${BASE_URL}/plan/${planId}`)
}
/** @desc 发布培训计划 */
export function publishTrainingPlan(planId: string) {
return http.put(`${BASE_URL}/plan/${planId}/publish`)
}
/** @desc 取消培训计划 */
export function cancelTrainingPlan(planId: string) {
return http.put(`${BASE_URL}/plan/${planId}/cancel`)
}

View File

@ -80,7 +80,7 @@ import { onMounted, ref, nextTick } from 'vue'
import Message from './Message.vue'
import SettingDrawer from './SettingDrawer.vue'
import Search from './Search.vue'
import { getUnreadMessageCount } from '@/apis'
import { useUserStore } from '@/stores'
import { getToken } from '@/utils/auth'
import { useBreakpoint, useDevice } from '@/hooks'

View File

@ -11,8 +11,7 @@
>
<template #icon>
<MenuIcon
:svg-icon="onlyOneChild?.meta?.svgIcon || item?.meta?.svgIcon"
:icon="onlyOneChild?.meta?.icon || item?.meta?.icon"
:icon="onlyOneChild?.meta?.svgIcon || onlyOneChild?.meta?.icon || item?.meta?.svgIcon || item?.meta?.icon"
/>
</template>
<a-tooltip :content="onlyOneChild?.meta?.title" position="right">
@ -22,7 +21,7 @@
<a-sub-menu v-else v-bind="attrs" :key="item.path" :title="item?.meta?.title">
<template #icon>
<MenuIcon :icon="item?.meta?.icon" />
<MenuIcon :icon="item?.meta?.svgIcon || item?.meta?.icon" />
</template>
<MenuItem v-for="child in item.children" :key="child.path" :item="child"></MenuItem>
</a-sub-menu>

View File

@ -296,64 +296,98 @@ export const systemRoutes: RouteRecordRaw[] = [
path: '/asset-management',
name: 'AssetManagement',
component: Layout,
redirect: '/asset-management/device/inventory',
meta: { title: '资产管理', icon: 'property-safety', hidden: false, sort: 3 },
redirect: '/asset-management/device-management/device-center',
meta: { title: '资产管理', icon: 'property-safety', hidden: true, sort: 3 },
children: [
{
path: '/asset-management/intellectual-property1',
name: 'IntellectualProperty1',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '设备管理', icon: 'copyright', hidden: false },
path: '/asset-management/device-management',
name: 'DeviceManagement',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/asset-management/device-management/device-center',
meta: {
title: '设备管理',
icon: 'device',
hidden: false,
},
children: [
{
path: '/asset-management/intellectual-property1',
name: 'IntellectualProperty11',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '库存管理', hidden: false },
path: '/asset-management/device-management/device-center',
name: 'DeviceCenter',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '设备中心',
icon: 'appstore',
hidden: false,
},
},
{
path: '/asset-management/intellectual-property1',
name: 'IntellectualProperty12',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '设备采购', hidden: false },
path: '/asset-management/device-management/device-detail/:id',
name: 'DeviceDetail',
component: () => import('@/views/system-resource/device-management/detail.vue'),
meta: {
title: '设备详情',
icon: 'info-circle',
hidden: true,
},
},
{
path: '/asset-management/intellectual-property1',
name: 'IntellectualProperty13',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '在线管理', hidden: false },
path: '/asset-management/device-management/procurement',
name: 'DeviceProcurement',
component: () => import('@/views/system-resource/device-management/procurement/index.vue'),
meta: {
title: '设备采购',
icon: 'shopping-cart',
hidden: false,
},
},
{
path: '/asset-management/device-management/online',
name: 'DeviceOnline',
component: () => import('@/components/ParentView/index.vue'),
redirect: '/asset-management/device-management/online/drone',
meta: {
title: '在线管理',
icon: 'cloud',
hidden: false,
},
children: [
{
path: '/asset-management/intellectual-property11',
name: 'IntellectualProperty14',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '无人机', hidden: false },
path: '/asset-management/device-management/online/drone',
name: 'DeviceDrone',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '无人机',
icon: 'drone',
hidden: false,
},
},
{
path: '/asset-management/intellectual-property12',
name: 'IntellectualProperty15',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '机巢', hidden: false },
path: '/asset-management/device-management/online/nest',
name: 'DeviceNest',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '机巢',
icon: 'nest',
hidden: false,
},
},
{
path: '/asset-management/intellectual-property13',
name: 'IntellectualProperty16',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '其他智能终端', hidden: false },
},
{
path: '/asset-management/intellectual-property14',
name: 'IntellectualProperty17',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '车辆管理', hidden: false },
path: '/asset-management/device-management/online/smart-terminal',
name: 'DeviceSmartTerminal',
component: () => import('@/views/system-resource/device-management/index.vue'),
meta: {
title: '其他智能终端',
icon: 'terminal',
hidden: false,
},
},
],
},
],
},
{
path: '/asset-management/intellectual-property',
name: 'IntellectualProperty',
path: '/asset-management/other-assets',
name: 'OtherAssets',
component: () => import('@/views/system-resource/information-system/software-management/index.vue'),
meta: { title: '其他资产', icon: 'copyright', hidden: false },
},
@ -1159,6 +1193,25 @@ export const systemRoutes: RouteRecordRaw[] = [
},
],
},
{
path: '/training',
name: 'Training',
component: Layout,
redirect: '/training/plan',
meta: { title: '培训管理', icon: 'book', hidden: false, sort: 9 },
children: [
{
path: '/training/plan',
name: 'TrainingPlan',
component: () => import('@/views/training/plan/index.vue'),
meta: {
title: '培训计划',
icon: 'calendar',
hidden: false,
},
},
],
},
{
path: '/system-resource',
name: 'SystemResource',

View File

@ -150,6 +150,82 @@ const storeSetup = () => {
sort: 3,
},
],
},
{
id: 2000,
parentId: 0,
title: '资产管理',
type: 1,
path: '/asset-management',
name: 'AssetManagement',
component: 'Layout',
redirect: '/asset-management/device-management/device-center',
icon: 'desktop',
isExternal: false,
isCache: false,
isHidden: false,
sort: 2,
children: [
{
id: 2010,
parentId: 2000,
title: '设备管理',
type: 1,
path: '/asset-management/device-management',
name: 'DeviceManagement',
component: 'Layout',
redirect: '/asset-management/device-management/device-center',
icon: 'desktop',
isExternal: false,
isCache: false,
isHidden: false,
sort: 1,
children: [
{
id: 2011,
parentId: 2010,
title: '设备中心',
type: 2,
path: '/asset-management/device-management/device-center',
name: 'DeviceCenter',
component: 'system-resource/device-management/index',
icon: 'desktop',
isExternal: false,
isCache: false,
isHidden: false,
sort: 1,
},
{
id: 2012,
parentId: 2010,
title: '设备采购',
type: 2,
path: '/asset-management/device-management/procurement',
name: 'DeviceProcurement',
component: 'system-resource/device-management/procurement/index',
icon: 'shopping-cart',
isExternal: false,
isCache: false,
isHidden: false,
sort: 2,
},
{
id: 2013,
parentId: 2010,
title: '设备详情',
type: 2,
path: '/asset-management/device-management/device-detail/:id',
name: 'DeviceDetail',
component: 'system-resource/device-management/detail',
icon: 'file-text',
isExternal: false,
isCache: false,
isHidden: true,
sort: 3,
},
],
},
],
}]
// 使用已转换的数据生成路由
const asyncRoutes = formatAsyncRoutes(data as unknown as RouteItem[])

View File

@ -1,4 +1,4 @@
@import './var.scss';
@use './var.scss' as *;
body {
--margin: 14px; // 通用外边距

View File

@ -1,5 +1,5 @@
/* 全局样式 */
@import './var.scss';
@use './var.scss' as *;
.w-full {
width: 100%;

View File

@ -1,17 +1,17 @@
// 基础样式
@import './base.scss';
@use './base.scss';
// 全局类名样式
@import './global.scss';
@use './global.scss';
// 自定义原生滚动条样式
@import './scrollbar-reset.scss';
@use './scrollbar-reset.scss';
// 自定义 nprogress 插件进度条颜色
@import './nprogress.scss';
@use './nprogress.scss';
// 富文本的css主题颜色变量
@import './editor.scss';
@use './editor.scss';
// 动画类名
@import './animated.scss';
@use './animated.scss';

41
src/test-console.vue Normal file
View File

@ -0,0 +1,41 @@
<template>
<div>
<h2>Console.log 测试</h2>
<button @click="testConsole">测试 Console.log</button>
<p>{{ message }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const message = ref('')
const testConsole = () => {
console.log('测试 console.log 是否正常工作')
console.warn('测试 console.warn')
console.error('测试 console.error')
message.value = '请查看浏览器控制台,应该能看到上述日志信息'
}
</script>
<style scoped>
div {
padding: 20px;
text-align: center;
}
button {
padding: 10px 20px;
margin: 10px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #40a9ff;
}
</style>

View File

@ -70,6 +70,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@ -7,66 +7,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
Avatar: typeof import('./../components/Avatar/index.vue')['default']
Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
CellCopy: typeof import('./../components/CellCopy/index.vue')['default']
Chart: typeof import('./../components/Chart/index.vue')['default']
ColumnSetting: typeof import('./../components/GiTable/src/components/ColumnSetting.vue')['default']
CronForm: typeof import('./../components/GenCron/CronForm/index.vue')['default']
CronModal: typeof import('./../components/GenCron/CronModal/index.vue')['default']
DateRangePicker: typeof import('./../components/DateRangePicker/index.vue')['default']
DayForm: typeof import('./../components/GenCron/CronForm/component/day-form.vue')['default']
FilePreview: typeof import('./../components/FilePreview/index.vue')['default']
GiCellAvatar: typeof import('./../components/GiCell/GiCellAvatar.vue')['default']
GiCellGender: typeof import('./../components/GiCell/GiCellGender.vue')['default']
GiCellStatus: typeof import('./../components/GiCell/GiCellStatus.vue')['default']
GiCellTag: typeof import('./../components/GiCell/GiCellTag.vue')['default']
GiCellTags: typeof import('./../components/GiCell/GiCellTags.vue')['default']
GiCodeView: typeof import('./../components/GiCodeView/index.vue')['default']
GiDot: typeof import('./../components/GiDot/index.tsx')['default']
GiEditTable: typeof import('./../components/GiEditTable/GiEditTable.vue')['default']
GiFooter: typeof import('./../components/GiFooter/index.vue')['default']
GiForm: typeof import('./../components/GiForm/src/GiForm.vue')['default']
GiIconBox: typeof import('./../components/GiIconBox/index.vue')['default']
GiIconSelector: typeof import('./../components/GiIconSelector/index.vue')['default']
GiIframe: typeof import('./../components/GiIframe/index.vue')['default']
GiOption: typeof import('./../components/GiOption/index.vue')['default']
GiOptionItem: typeof import('./../components/GiOptionItem/index.vue')['default']
GiPageLayout: typeof import('./../components/GiPageLayout/index.vue')['default']
GiSpace: typeof import('./../components/GiSpace/index.vue')['default']
GiSplitButton: typeof import('./../components/GiSplitButton/index.vue')['default']
GiSplitPane: typeof import('./../components/GiSplitPane/index.vue')['default']
GiSplitPaneFlexibleBox: typeof import('./../components/GiSplitPane/components/GiSplitPaneFlexibleBox.vue')['default']
GiSvgIcon: typeof import('./../components/GiSvgIcon/index.vue')['default']
GiTable: typeof import('./../components/GiTable/src/GiTable.vue')['default']
GiTag: typeof import('./../components/GiTag/index.tsx')['default']
GiThemeBtn: typeof import('./../components/GiThemeBtn/index.vue')['default']
HourForm: typeof import('./../components/GenCron/CronForm/component/hour-form.vue')['default']
Icon403: typeof import('./../components/icons/Icon403.vue')['default']
Icon404: typeof import('./../components/icons/Icon404.vue')['default']
Icon500: typeof import('./../components/icons/Icon500.vue')['default']
IconBorders: typeof import('./../components/icons/IconBorders.vue')['default']
IconTableSize: typeof import('./../components/icons/IconTableSize.vue')['default']
IconTreeAdd: typeof import('./../components/icons/IconTreeAdd.vue')['default']
IconTreeReduce: typeof import('./../components/icons/IconTreeReduce.vue')['default']
ImageImport: typeof import('./../components/ImageImport/index.vue')['default']
ImageImportWizard: typeof import('./../components/ImageImportWizard/index.vue')['default']
IndustrialImageList: typeof import('./../components/IndustrialImageList/index.vue')['default']
JsonPretty: typeof import('./../components/JsonPretty/index.vue')['default']
MinuteForm: typeof import('./../components/GenCron/CronForm/component/minute-form.vue')['default']
MonthForm: typeof import('./../components/GenCron/CronForm/component/month-form.vue')['default']
ParentView: typeof import('./../components/ParentView/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SecondForm: typeof import('./../components/GenCron/CronForm/component/second-form.vue')['default']
SplitPanel: typeof import('./../components/SplitPanel/index.vue')['default']
TextCopy: typeof import('./../components/TextCopy/index.vue')['default']
TurbineGrid: typeof import('./../components/TurbineGrid/index.vue')['default']
UserSelect: typeof import('./../components/UserSelect/index.vue')['default']
Verify: typeof import('./../components/Verify/index.vue')['default']
VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default']
VerifySlide: typeof import('./../components/Verify/Verify/VerifySlide.vue')['default']
WeekForm: typeof import('./../components/GenCron/CronForm/component/week-form.vue')['default']
YearForm: typeof import('./../components/GenCron/CronForm/component/year-form.vue')['default']
}
}

71
src/types/training.d.ts vendored Normal file
View File

@ -0,0 +1,71 @@
export interface TrainingPlanPageQuery {
planName?: string
trainingType?: string
trainingLevel?: string
status?: string
trainer?: string
startTime?: string
endTime?: string
page?: number
pageSize?: number
}
export interface TrainingPlanReq {
planName: string
trainingType: string
trainingLevel: string
trainingContent?: string
trainer?: string
trainingLocation?: string
startTime: string
endTime: string
status?: string
maxParticipants?: number
requirements?: string
remark?: string
}
export interface TrainingPlanResp {
planId: string
planName: string
trainingType: string
trainingLevel: string
trainingContent?: string
trainer?: string
trainingLocation?: string
startTime: string
endTime: string
status: string
maxParticipants?: number
currentParticipants?: number
requirements?: string
remark?: string
createTime: string
createBy: string
materials?: TrainingMaterialResp[]
records?: TrainingRecordResp[]
}
export interface TrainingMaterialResp {
materialId: string
materialName: string
materialType: string
materialPath?: string
materialSize?: number
description?: string
sortOrder?: number
}
export interface TrainingRecordResp {
recordId: string
userId: string
userName: string
deptId?: string
deptName?: string
attendanceStatus: string
signInTime?: string
signOutTime?: string
score?: number
feedback?: string
certificateId?: string
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,496 @@
<template>
<div class="equipment-search-container">
<!-- 搜索按钮 -->
<div class="search-trigger">
<a-button type="primary" @click="showSearchModal = true">
<template #icon>
<IconSearch />
</template>
搜索设备
</a-button>
</div>
<!-- 搜索弹窗 -->
<a-modal
v-model:visible="showSearchModal"
title="设备搜索"
width="1000px"
:footer="false"
@cancel="handleCancel"
class="search-modal"
>
<div class="search-content">
<a-form layout="vertical" :model="searchForm" class="search-form">
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="设备名称">
<a-input
v-model="searchForm.equipmentName"
placeholder="请输入设备名称"
allow-clear
@input="debouncedSearch"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="设备类型">
<a-select
v-model="searchForm.equipmentType"
:options="equipmentTypeOptions"
placeholder="请选择设备类型"
allow-clear
@change="debouncedSearch"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="设备状态">
<a-select
v-model="searchForm.equipmentStatus"
:options="equipmentStatusOptions"
placeholder="请选择设备状态"
allow-clear
@change="debouncedSearch"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="位置状态">
<a-select
v-model="searchForm.locationStatus"
:options="locationStatusOptions"
placeholder="请选择位置状态"
allow-clear
@change="debouncedSearch"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="健康状态">
<a-select
v-model="searchForm.healthStatus"
:options="healthStatusOptions"
placeholder="请选择健康状态"
allow-clear
@change="debouncedSearch"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="使用状态">
<a-select
v-model="searchForm.useStatus"
:options="useStatusOptions"
placeholder="请选择使用状态"
allow-clear
@change="debouncedSearch"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="使用部门/人">
<a-input
v-model="searchForm.usingDepartment"
placeholder="请输入使用部门/人"
allow-clear
@input="debouncedSearch"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="负责人">
<a-input
v-model="searchForm.responsiblePerson"
placeholder="请输入负责人"
allow-clear
@input="debouncedSearch"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="维护人员">
<a-input
v-model="searchForm.maintenancePerson"
placeholder="请输入维护人员"
allow-clear
@input="debouncedSearch"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="序列号">
<a-input
v-model="searchForm.equipmentSn"
placeholder="请输入序列号"
allow-clear
@input="debouncedSearch"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="资产编号">
<a-input
v-model="searchForm.assetCode"
placeholder="请输入资产编号"
allow-clear
@input="debouncedSearch"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="品牌">
<a-input
v-model="searchForm.brand"
placeholder="请输入品牌"
allow-clear
@input="debouncedSearch"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="设备型号">
<a-input
v-model="searchForm.equipmentModel"
placeholder="请输入设备型号"
allow-clear
@input="debouncedSearch"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="物理位置">
<a-input
v-model="searchForm.physicalLocation"
placeholder="请输入物理位置"
allow-clear
@input="debouncedSearch"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="供应商">
<a-input
v-model="searchForm.supplierName"
placeholder="请输入供应商"
allow-clear
@input="debouncedSearch"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="发票">
<a-input
v-model="searchForm.invoice"
placeholder="请输入发票"
allow-clear
@input="debouncedSearch"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="条码">
<a-input
v-model="searchForm.barcode"
placeholder="请输入条码"
allow-clear
@input="debouncedSearch"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="库存条码">
<a-input
v-model="searchForm.inventoryBarcode"
placeholder="请输入库存条码"
allow-clear
@input="debouncedSearch"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="规格型号">
<a-input
v-model="searchForm.specification"
placeholder="请输入规格型号"
allow-clear
@input="debouncedSearch"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="资产备注">
<a-input
v-model="searchForm.assetRemark"
placeholder="请输入资产备注"
allow-clear
@input="debouncedSearch"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 搜索操作按钮 -->
<div class="search-actions">
<a-space>
<a-button type="primary" @click="handleSearch" :loading="loading">
<template #icon>
<IconSearch />
</template>
搜索
</a-button>
<a-button @click="handleReset">
<template #icon>
<IconRefresh />
</template>
重置
</a-button>
<a-button @click="handleCancel">
取消
</a-button>
</a-space>
</div>
</a-form>
</div>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import { IconRefresh, IconSearch } from '@arco-design/web-vue/es/icon'
import type { EquipmentPageQuery } from '@/types/equipment.d'
interface Props {
loading?: boolean
}
// eslint-disable-next-line unused-imports/no-unused-vars
const props = withDefaults(defineProps<Props>(), {
loading: false,
})
const emit = defineEmits<{
search: [params: EquipmentPageQuery]
reset: []
}>()
//
const showSearchModal = ref(false)
//
// eslint-disable-next-line ts/no-unsafe-function-type
const debounce = (func: Function, delay: number) => {
let timeoutId: NodeJS.Timeout
return (...args: any[]) => {
clearTimeout(timeoutId)
// eslint-disable-next-line prefer-spread
timeoutId = setTimeout(() => func.apply(null, args), delay)
}
}
const searchForm = reactive<EquipmentPageQuery>({
equipmentName: '',
equipmentType: '',
equipmentStatus: '',
locationStatus: '',
healthStatus: '',
usingDepartment: '',
invoice: '',
barcode: '',
importer: '',
equipmentSn: '',
assetCode: '',
brand: '',
responsiblePerson: '',
useStatus: '',
equipmentModel: '',
specification: '',
physicalLocation: '',
supplierName: '',
maintenancePerson: '',
inventoryBarcode: '',
assetRemark: '',
})
const equipmentTypeOptions = [
{ label: '计算机设备', value: 'computer' },
{ label: '网络设备', value: 'network' },
{ label: '存储设备', value: 'storage' },
{ label: '安全设备', value: 'security' },
{ label: '办公设备', value: 'office' },
{ label: '其他设备', value: 'other' },
]
const equipmentStatusOptions = [
{ label: '正常', value: 'normal' },
{ label: '维修中', value: 'repair' },
{ label: '报废', value: 'scrap' },
{ label: '闲置', value: 'idle' },
]
const locationStatusOptions = [
{ label: '在库', value: 'in_stock' },
{ label: '已分配', value: 'allocated' },
{ label: '外借中', value: 'borrowed' },
{ label: '维修中', value: 'repair' },
{ label: '已报废', value: 'scrapped' },
]
const healthStatusOptions = [
{ label: '良好', value: 'good' },
{ label: '一般', value: 'normal' },
{ label: '较差', value: 'poor' },
{ label: '故障', value: 'fault' },
]
const useStatusOptions = [
{ label: '空闲中', value: '0' },
{ label: '使用中', value: '1' },
]
//
const debouncedSearch = debounce(() => {
console.log('🔍 EquipmentSearch - 防抖搜索触发')
console.log('🔍 EquipmentSearch - 搜索表单数据:', searchForm)
emit('search', { ...searchForm })
}, 300)
const handleSearch = () => {
console.log('🔍 EquipmentSearch - 搜索按钮被点击')
console.log('🔍 EquipmentSearch - 搜索表单数据:', searchForm)
console.log('🔍 EquipmentSearch - 发送的搜索参数:', { ...searchForm })
emit('search', { ...searchForm })
showSearchModal.value = false
}
const handleReset = () => {
console.log('🔄 EquipmentSearch - 重置按钮被点击')
console.log('🔄 EquipmentSearch - 重置前的表单数据:', { ...searchForm })
Object.keys(searchForm).forEach((key) => {
searchForm[key as keyof EquipmentPageQuery] = '' as any
})
console.log('🔄 EquipmentSearch - 重置后的表单数据:', { ...searchForm })
emit('reset')
}
const handleCancel = () => {
showSearchModal.value = false
}
defineExpose({
reset: handleReset,
getSearchForm: () => ({ ...searchForm }),
showModal: () => { showSearchModal.value = true },
})
</script>
<style lang="scss" scoped>
.equipment-search-container {
.search-trigger {
margin-bottom: 16px;
}
}
.search-modal {
.arco-modal-header {
padding: 20px 24px 16px;
border-bottom: 1px solid var(--color-border);
.arco-modal-title {
font-size: 18px;
font-weight: 600;
color: var(--color-text-1);
}
}
.arco-modal-body {
padding: 24px;
}
}
.search-content {
.search-form {
.arco-form-item {
margin-bottom: 20px;
.arco-form-item-label {
font-weight: 500;
color: var(--color-text-1);
margin-bottom: 8px;
font-size: 14px;
}
}
.arco-input,
.arco-select {
border-radius: 6px;
border: 1px solid var(--color-border);
transition: all 0.2s ease;
&:hover {
border-color: var(--color-primary-light-3);
}
&:focus,
&.arco-input-focus,
&.arco-select-focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(var(--primary-6), 0.1);
}
}
.arco-input-inner {
padding: 8px 12px;
font-size: 14px;
}
.arco-select-view {
padding: 8px 12px;
font-size: 14px;
}
.arco-select-arrow {
color: var(--color-text-3);
}
.arco-input-inner::placeholder {
color: var(--color-text-3);
font-size: 14px;
}
.arco-input-clear-btn {
color: var(--color-text-3);
&:hover {
color: var(--color-text-2);
}
}
}
.search-actions {
display: flex;
justify-content: center;
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid var(--color-border);
}
}
</style>

View File

@ -54,7 +54,7 @@
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { IconUpload } from '@arco-design/web-vue/es/icon'
import { getEquipmentDetail } from '@/apis/equipment'
import { EquipmentAPI } from '@/apis'
import type { EquipmentResp } from '@/types/equipment.d'
defineOptions({ name: 'DeviceDetail' })
@ -245,7 +245,7 @@ const getHealthStatusText = (status: string) => {
//
const loadDeviceDetail = async () => {
try {
const res = await getEquipmentDetail(deviceId)
const res = await EquipmentAPI.getEquipmentDetail(deviceId)
const device = res.data as EquipmentResp
//

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,865 @@
<template>
<a-modal
:visible="visible"
:title="getModalTitle()"
width="1200px"
:confirm-loading="loading"
:ok-button-props="{ disabled: !isFormValid || isView }"
@cancel="handleCancel"
@ok="handleSubmit"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
auto-label-width
>
<!-- 标签页导航 -->
<div class="tab-navigation">
<div
v-for="tab in tabs"
:key="tab.key"
class="tab-item" :class="[{ active: activeTab === tab.key }]"
@click="activeTab = tab.key"
>
{{ tab.label }}
</div>
</div>
<!-- 标签页内容 -->
<div class="tab-content">
<!-- 基本信息 -->
<div v-show="activeTab === 'basic'">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="设备名称" field="equipmentName">
<a-input
v-model="formData.equipmentName"
placeholder="请输入设备名称"
:disabled="isView"
show-word-limit
:max-length="200"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="设备类型" field="equipmentType">
<a-select
v-model="formData.equipmentType"
:options="equipmentTypeOptions"
placeholder="请选择设备类型"
:disabled="isView"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="设备型号" field="equipmentModel">
<a-input
v-model="formData.equipmentModel"
placeholder="请输入设备型号"
:disabled="isView"
show-word-limit
:max-length="200"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="序列号" field="equipmentSn">
<a-input
v-model="formData.equipmentSn"
placeholder="请输入序列号"
:disabled="isView"
show-word-limit
:max-length="100"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="品牌" field="brand">
<a-input
v-model="formData.brand"
placeholder="请输入品牌"
:disabled="isView"
show-word-limit
:max-length="100"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="资产编号" field="assetCode">
<a-input
v-model="formData.assetCode"
placeholder="请输入资产编号"
:disabled="isView"
show-word-limit
:max-length="50"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="配置规格" field="specification">
<a-textarea
v-model="formData.specification"
placeholder="请输入配置规格参数"
:disabled="isView"
:rows="3"
show-word-limit
:max-length="500"
/>
</a-form-item>
</div>
<!-- 采购信息 -->
<div v-show="activeTab === 'procurement'">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="采购订单" field="purchaseOrder">
<a-input
v-model="formData.purchaseOrder"
placeholder="请输入采购订单号"
:disabled="isView"
show-word-limit
:max-length="100"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="供应商" field="supplierName">
<a-input
v-model="formData.supplierName"
placeholder="请输入供应商名称"
:disabled="isView"
show-word-limit
:max-length="200"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="数量" field="quantity">
<a-input-number
v-model="formData.quantity"
placeholder="请输入数量"
:min="1"
:disabled="isView"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="单价" field="unitPrice">
<a-input-number
v-model="formData.unitPrice"
placeholder="请输入单价"
:precision="2"
:min="0"
:disabled="isView"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="总价" field="totalPrice">
<a-input-number
v-model="formData.totalPrice"
placeholder="请输入总价"
:precision="2"
:min="0"
:disabled="isView"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="采购价格" field="purchasePrice">
<a-input-number
v-model="formData.purchasePrice"
placeholder="请输入采购价格"
:precision="2"
:min="0"
:disabled="isView"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="当前净值" field="currentNetValue">
<a-input-number
v-model="formData.currentNetValue"
placeholder="请输入当前净值"
:precision="2"
:min="0"
:disabled="isView"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="采购时间" field="purchaseTime">
<a-date-picker
v-model="formData.purchaseTime"
placeholder="请选择采购时间"
:disabled="isView"
show-time
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="入库时间" field="inStockTime">
<a-date-picker
v-model="formData.inStockTime"
placeholder="请选择入库时间"
:disabled="isView"
show-time
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="启用时间" field="activationTime">
<a-date-picker
v-model="formData.activationTime"
placeholder="请选择启用时间"
:disabled="isView"
show-time
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="预计报废时间" field="expectedScrapTime">
<a-date-picker
v-model="formData.expectedScrapTime"
placeholder="请选择预计报废时间"
:disabled="isView"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="折旧方法" field="depreciationMethod">
<a-select
v-model="formData.depreciationMethod"
placeholder="请选择折旧方法"
:disabled="isView"
>
<a-option value="straight_line">直线折旧</a-option>
<a-option value="declining_balance">余额递减</a-option>
<a-option value="sum_of_years">年数总和</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="折旧年限" field="depreciationYears">
<a-input-number
v-model="formData.depreciationYears"
placeholder="请输入折旧年限"
:min="1"
:max="50"
:disabled="isView"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="残值" field="salvageValue">
<a-input-number
v-model="formData.salvageValue"
placeholder="请输入残值"
:precision="2"
:min="0"
:disabled="isView"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="保修截止日期" field="warrantyExpireDate">
<a-date-picker
v-model="formData.warrantyExpireDate"
placeholder="请选择保修截止日期"
:disabled="isView"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 状态信息 -->
<div v-show="activeTab === 'status'">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="设备状态" field="equipmentStatus">
<a-select
v-model="formData.equipmentStatus"
:options="equipmentStatusOptions"
placeholder="请选择设备状态"
:disabled="isView"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="使用状态" field="useStatus">
<a-select
v-model="formData.useStatus"
:options="useStatusOptions"
placeholder="请选择使用状态"
:disabled="isView"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="位置状态" field="locationStatus">
<a-select
v-model="formData.locationStatus"
:options="locationStatusOptions"
placeholder="请选择位置状态"
:disabled="isView"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="健康状态" field="healthStatus">
<a-select
v-model="formData.healthStatus"
:options="healthStatusOptions"
placeholder="请选择健康状态"
:disabled="isView"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="负责人" field="responsiblePerson">
<a-input
v-model="formData.responsiblePerson"
placeholder="请输入负责人"
:disabled="isView"
show-word-limit
:max-length="100"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="维护人员" field="maintenancePerson">
<a-input
v-model="formData.maintenancePerson"
placeholder="请输入维护人员"
:disabled="isView"
show-word-limit
:max-length="100"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="物理位置" field="physicalLocation">
<a-input
v-model="formData.physicalLocation"
placeholder="请输入物理位置"
:disabled="isView"
show-word-limit
:max-length="200"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="库存条码" field="inventoryBarcode">
<a-input
v-model="formData.inventoryBarcode"
placeholder="请输入库存条码"
:disabled="isView"
show-word-limit
:max-length="100"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="上次维护日期" field="lastMaintenanceDate">
<a-date-picker
v-model="formData.lastMaintenanceDate"
placeholder="请选择上次维护日期"
:disabled="isView"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="下次维护日期" field="nextMaintenanceDate">
<a-date-picker
v-model="formData.nextMaintenanceDate"
placeholder="请选择下次维护日期"
:disabled="isView"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
</div>
<!-- 其他信息 -->
<div v-show="activeTab === 'other'">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="次户号" field="accountNumber">
<a-input
v-model="formData.accountNumber"
placeholder="请输入次户号"
:disabled="isView"
show-word-limit
:max-length="100"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="盘点依据" field="inventoryBasis">
<a-input
v-model="formData.inventoryBasis"
placeholder="请输入盘点依据"
:disabled="isView"
show-word-limit
:max-length="200"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="动态记录" field="dynamicRecord">
<a-textarea
v-model="formData.dynamicRecord"
placeholder="请输入动态记录"
:disabled="isView"
:rows="4"
show-word-limit
:max-length="1000"
/>
</a-form-item>
<a-form-item label="资产备注" field="assetRemark">
<a-textarea
v-model="formData.assetRemark"
placeholder="请输入资产备注"
:disabled="isView"
:rows="3"
show-word-limit
:max-length="500"
/>
</a-form-item>
</div>
</div>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { FormInstance } from '@arco-design/web-vue'
import { equipmentProcurementApi } from '@/apis/equipment/procurement'
import type { EquipmentResp, EquipmentReq } from '@/apis/equipment/type'
interface Props {
visible: boolean
procurementData?: EquipmentResp | null
mode: 'add' | 'edit' | 'view'
}
interface Emits {
(e: 'update:visible', value: boolean): void
(e: 'success'): void
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
procurementData: null,
mode: 'add',
})
const emit = defineEmits<Emits>()
const formRef = ref<FormInstance>()
const loading = ref(false)
const activeTab = ref('basic')
//
const tabs = [
{ key: 'basic', label: '基本信息' },
{ key: 'procurement', label: '采购信息' },
{ key: 'status', label: '状态信息' },
{ key: 'other', label: '其他信息' },
]
//
const formData = reactive<EquipmentReq>({
equipmentName: '',
equipmentModel: '',
equipmentType: '',
equipmentStatus: '',
useStatus: '',
equipmentSn: '',
assetCode: '',
brand: '',
specification: '',
locationStatus: '',
physicalLocation: '',
responsiblePerson: '',
healthStatus: '',
purchaseTime: '',
inStockTime: '',
activationTime: '',
expectedScrapTime: '',
actualScrapTime: '',
purchaseOrder: '',
supplierName: '',
purchasePrice: undefined,
currentNetValue: undefined,
depreciationMethod: '',
depreciationYears: undefined,
salvageValue: undefined,
warrantyExpireDate: '',
lastMaintenanceDate: '',
nextMaintenanceDate: '',
maintenancePerson: '',
inventoryBarcode: '',
assetRemark: '',
accountNumber: '',
quantity: 1,
unitPrice: undefined,
totalPrice: undefined,
inventoryBasis: '',
dynamicRecord: '',
})
//
const rules = {
equipmentName: [{ required: true, message: '请输入设备名称' }],
equipmentModel: [{ required: true, message: '请输入设备型号' }],
equipmentType: [{ required: true, message: '请选择设备类型' }],
equipmentSn: [{ required: true, message: '请输入设备序列号' }],
equipmentStatus: [{ required: true, message: '请选择设备状态' }],
useStatus: [{ required: true, message: '请选择使用状态' }],
purchaseOrder: [{ required: true, message: '请输入采购订单号' }],
supplierName: [{ required: true, message: '请输入供应商名称' }],
purchasePrice: [{ required: true, message: '请输入采购价格' }],
quantity: [{ required: true, message: '请输入数量' }],
unitPrice: [{ required: true, message: '请输入单价' }],
totalPrice: [{ required: true, message: '请输入总价' }],
}
//
const equipmentTypeOptions = [
{ label: '检测设备', value: 'detection' },
{ label: '安防设备', value: 'security' },
{ label: '办公设备', value: 'office' },
{ label: '车辆', value: 'car' },
{ label: '其他设备', value: 'other' },
]
const equipmentStatusOptions = [
{ label: '正常', value: 'normal' },
{ label: '维修中', value: 'repair' },
{ label: '已报废', value: 'scrap' },
{ label: '闲置', value: 'idle' },
{ label: '丢失', value: 'lost' },
]
const useStatusOptions = [
{ label: '空闲中', value: '0' },
{ label: '使用中', value: '1' },
]
const locationStatusOptions = [
{ label: '库存中', value: 'in_stock' },
{ label: '使用中', value: 'in_use' },
{ label: '维修中', value: 'repair' },
{ label: '已报废', value: 'scrapped' },
{ label: '外借中', value: 'on_loan' },
{ label: '丢失', value: 'lost' },
{ label: '闲置', value: 'idle' },
]
const healthStatusOptions = [
{ label: '优秀', value: 'excellent' },
{ label: '良好', value: 'good' },
{ label: '一般', value: 'normal' },
{ label: '较差', value: 'poor' },
{ label: '危险', value: 'critical' },
]
//
const isView = computed(() => props.mode === 'view')
const isFormValid = computed(() => {
return formData.equipmentName &&
formData.equipmentModel &&
formData.equipmentType &&
formData.equipmentSn
})
//
const getModalTitle = () => {
const titles = {
add: '新增采购记录',
edit: '编辑采购记录',
view: '查看采购记录',
}
return titles[props.mode]
}
//
watch(() => props.visible, (newVal) => {
if (newVal && props.procurementData) {
initFormData()
}
})
//
const initFormData = () => {
if (props.procurementData) {
Object.assign(formData, {
equipmentName: props.procurementData.equipmentName || '',
equipmentModel: props.procurementData.equipmentModel || '',
equipmentType: props.procurementData.equipmentType || '',
equipmentStatus: props.procurementData.equipmentStatus || '',
useStatus: props.procurementData.useStatus || '',
equipmentSn: props.procurementData.equipmentSn || '',
assetCode: props.procurementData.assetCode || '',
brand: props.procurementData.brand || '',
specification: props.procurementData.specification || '',
locationStatus: props.procurementData.locationStatus || '',
physicalLocation: props.procurementData.physicalLocation || '',
responsiblePerson: props.procurementData.responsiblePerson || '',
healthStatus: props.procurementData.healthStatus || '',
purchaseTime: props.procurementData.purchaseTime || '',
inStockTime: props.procurementData.inStockTime || '',
activationTime: props.procurementData.activationTime || '',
expectedScrapTime: props.procurementData.expectedScrapTime || '',
actualScrapTime: props.procurementData.actualScrapTime || '',
purchaseOrder: props.procurementData.purchaseOrder || '',
supplierName: props.procurementData.supplierName || '',
purchasePrice: props.procurementData.purchasePrice,
currentNetValue: props.procurementData.currentNetValue,
depreciationMethod: props.procurementData.depreciationMethod || '',
depreciationYears: props.procurementData.depreciationYears,
salvageValue: props.procurementData.salvageValue,
warrantyExpireDate: props.procurementData.warrantyExpireDate || '',
lastMaintenanceDate: props.procurementData.lastMaintenanceDate || '',
nextMaintenanceDate: props.procurementData.nextMaintenanceDate || '',
maintenancePerson: props.procurementData.maintenancePerson || '',
inventoryBarcode: props.procurementData.inventoryBarcode || '',
assetRemark: props.procurementData.assetRemark || '',
accountNumber: props.procurementData.accountNumber || '',
quantity: props.procurementData.quantity || 1,
unitPrice: props.procurementData.unitPrice,
totalPrice: props.procurementData.totalPrice,
inventoryBasis: props.procurementData.inventoryBasis || '',
dynamicRecord: props.procurementData.dynamicRecord || '',
})
} else {
resetForm()
}
}
//
const resetForm = () => {
Object.assign(formData, {
equipmentName: '',
equipmentModel: '',
equipmentType: '',
equipmentStatus: '',
useStatus: '',
equipmentSn: '',
assetCode: '',
brand: '',
specification: '',
locationStatus: '',
physicalLocation: '',
responsiblePerson: '',
healthStatus: '',
purchaseTime: '',
inStockTime: '',
activationTime: '',
expectedScrapTime: '',
actualScrapTime: '',
purchaseOrder: '',
supplierName: '',
purchasePrice: undefined,
currentNetValue: undefined,
depreciationMethod: '',
depreciationYears: undefined,
salvageValue: undefined,
warrantyExpireDate: '',
lastMaintenanceDate: '',
nextMaintenanceDate: '',
maintenancePerson: '',
inventoryBarcode: '',
assetRemark: '',
accountNumber: '',
quantity: 1,
unitPrice: undefined,
totalPrice: undefined,
inventoryBasis: '',
dynamicRecord: '',
})
formRef.value?.resetFields()
}
//
const handleSubmit = async () => {
try {
await formRef.value?.validate()
loading.value = true
if (props.mode === 'edit' && props.procurementData) {
await equipmentProcurementApi.update(props.procurementData.equipmentId, formData)
Message.success('更新成功')
} else {
await equipmentProcurementApi.add(formData)
Message.success('新增成功')
}
emit('success')
} catch (error: any) {
console.error('操作失败:', error)
Message.error(error?.message || '操作失败')
} finally {
loading.value = false
}
}
//
const handleCancel = () => {
emit('update:visible', false)
resetForm()
}
</script>
<style scoped lang="scss">
.tab-navigation {
display: flex;
border-bottom: 1px solid var(--color-border);
margin-bottom: 24px;
.tab-item {
padding: 12px 24px;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.3s ease;
font-weight: 500;
color: var(--color-text-2);
&:hover {
color: var(--color-primary);
}
&.active {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
}
}
}
.tab-content {
.arco-form-item {
margin-bottom: 24px;
.arco-form-item-label {
font-weight: 500;
color: var(--color-text-1);
margin-bottom: 8px;
}
}
.arco-input,
.arco-select,
.arco-input-number,
.arco-date-picker {
border-radius: 6px;
border: 1px solid var(--color-border);
transition: all 0.2s ease;
&:hover {
border-color: var(--color-primary-light-3);
}
&:focus,
&.arco-input-focus,
&.arco-select-focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(var(--primary-6), 0.1);
}
}
.arco-textarea {
border-radius: 6px;
border: 1px solid var(--color-border);
transition: all 0.2s ease;
&:hover {
border-color: var(--color-primary-light-3);
}
&:focus,
&.arco-textarea-focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(var(--primary-6), 0.1);
}
}
}
//
@media (max-width: 768px) {
.tab-navigation {
.tab-item {
padding: 8px 16px;
font-size: 14px;
}
}
.tab-content {
.arco-row {
.arco-col {
margin-bottom: 16px;
}
}
}
}
</style>

View File

@ -0,0 +1,491 @@
<template>
<a-button type="primary" @click="showSearchModal = true">
<template #icon>
<IconSearch />
</template>
搜索采购
</a-button>
<!-- 搜索弹窗 -->
<a-modal
v-model:visible="showSearchModal"
title="采购记录搜索"
width="1000px"
>
<a-form
ref="formRef"
:model="searchForm"
layout="vertical"
class="search-form"
>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="设备名称">
<a-input
v-model="searchForm.equipmentName"
placeholder="请输入设备名称"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="设备型号">
<a-input
v-model="searchForm.equipmentModel"
placeholder="请输入设备型号"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="设备类型">
<a-select
v-model="searchForm.equipmentType"
placeholder="请选择设备类型"
allow-clear
>
<a-option value="detection">检测设备</a-option>
<a-option value="security">安防设备</a-option>
<a-option value="office">办公设备</a-option>
<a-option value="car">车辆</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="品牌">
<a-input
v-model="searchForm.brand"
placeholder="请输入品牌"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="供应商">
<a-input
v-model="searchForm.supplierName"
placeholder="请输入供应商名称"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="采购订单">
<a-input
v-model="searchForm.purchaseOrder"
placeholder="请输入采购订单号"
allow-clear
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="设备状态">
<a-select
v-model="searchForm.equipmentStatus"
placeholder="请选择设备状态"
allow-clear
>
<a-option value="normal">正常</a-option>
<a-option value="repair">维修中</a-option>
<a-option value="scrap">已报废</a-option>
<a-option value="idle">闲置</a-option>
<a-option value="lost">丢失</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="位置状态">
<a-select
v-model="searchForm.locationStatus"
placeholder="请选择位置状态"
allow-clear
>
<a-option value="in_stock">库存中</a-option>
<a-option value="in_use">使用中</a-option>
<a-option value="repair">维修中</a-option>
<a-option value="scrapped">已报废</a-option>
<a-option value="on_loan">外借中</a-option>
<a-option value="lost">丢失</a-option>
<a-option value="idle">闲置</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="健康状态">
<a-select
v-model="searchForm.healthStatus"
placeholder="请选择健康状态"
allow-clear
>
<a-option value="excellent">优秀</a-option>
<a-option value="good">良好</a-option>
<a-option value="normal">一般</a-option>
<a-option value="poor">较差</a-option>
<a-option value="critical">危险</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="负责人">
<a-input
v-model="searchForm.responsiblePerson"
placeholder="请输入负责人"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="维护人员">
<a-input
v-model="searchForm.maintenancePerson"
placeholder="请输入维护人员"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="物理位置">
<a-input
v-model="searchForm.physicalLocation"
placeholder="请输入物理位置"
allow-clear
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="资产编号">
<a-input
v-model="searchForm.assetCode"
placeholder="请输入资产编号"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="设备序列号">
<a-input
v-model="searchForm.equipmentSn"
placeholder="请输入设备序列号"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="库存条码">
<a-input
v-model="searchForm.inventoryBarcode"
placeholder="请输入库存条码"
allow-clear
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="采购时间范围">
<a-range-picker
v-model="searchForm.purchaseTimeRange"
show-time
placeholder="['开始时间', '结束时间']"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="入库时间范围">
<a-range-picker
v-model="searchForm.inStockTimeRange"
show-time
placeholder="['开始时间', '结束时间']"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="启用时间范围">
<a-range-picker
v-model="searchForm.activationTimeRange"
show-time
placeholder="['开始时间', '结束时间']"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="价格范围">
<a-input-number
v-model="searchForm.minPrice"
placeholder="最低价格"
:min="0"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="至">
<a-input-number
v-model="searchForm.maxPrice"
placeholder="最高价格"
:min="0"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="配置规格">
<a-input
v-model="searchForm.specification"
placeholder="请输入配置规格关键词"
allow-clear
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="24">
<a-form-item label="备注">
<a-input
v-model="searchForm.assetRemark"
placeholder="请输入备注关键词"
allow-clear
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
<template #footer>
<div class="search-actions">
<a-space>
<a-button type="primary" @click="handleSearch" :loading="loading">
<template #icon>
<IconSearch />
</template>
搜索
</a-button>
<a-button @click="handleReset">
<template #icon>
<IconRefresh />
</template>
重置
</a-button>
<a-button @click="showSearchModal = false">
取消
</a-button>
</a-space>
</div>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { IconSearch, IconRefresh } from '@arco-design/web-vue/es/icon'
import type { EquipmentListReq } from '@/apis/equipment/type'
interface Props {
loading?: boolean
}
interface Emits {
(e: 'search', params: EquipmentListReq): void
(e: 'reset'): void
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
})
const emit = defineEmits<Emits>()
const showSearchModal = ref(false)
const formRef = ref()
//
const searchForm = reactive({
equipmentName: '',
equipmentModel: '',
equipmentType: '',
brand: '',
supplierName: '',
purchaseOrder: '',
equipmentStatus: '',
locationStatus: '',
healthStatus: '',
responsiblePerson: '',
maintenancePerson: '',
physicalLocation: '',
assetCode: '',
equipmentSn: '',
inventoryBarcode: '',
purchaseTimeRange: [],
inStockTimeRange: [],
activationTimeRange: [],
minPrice: undefined,
maxPrice: undefined,
specification: '',
assetRemark: '',
})
//
const handleSearch = () => {
const params: EquipmentListReq = {
equipmentName: searchForm.equipmentName || undefined,
equipmentModel: searchForm.equipmentModel || undefined,
equipmentType: searchForm.equipmentType || undefined,
brand: searchForm.brand || undefined,
supplierName: searchForm.supplierName || undefined,
purchaseOrder: searchForm.purchaseOrder || undefined,
equipmentStatus: searchForm.equipmentStatus || undefined,
locationStatus: searchForm.locationStatus || undefined,
healthStatus: searchForm.healthStatus || undefined,
responsiblePerson: searchForm.responsiblePerson || undefined,
maintenancePerson: searchForm.maintenancePerson || undefined,
physicalLocation: searchForm.physicalLocation || undefined,
assetCode: searchForm.assetCode || undefined,
equipmentSn: searchForm.equipmentSn || undefined,
inventoryBarcode: searchForm.inventoryBarcode || undefined,
specification: searchForm.specification || undefined,
assetRemark: searchForm.assetRemark || undefined,
}
//
if (searchForm.purchaseTimeRange && searchForm.purchaseTimeRange.length === 2) {
params.purchaseTimeStart = searchForm.purchaseTimeRange[0]
params.purchaseTimeEnd = searchForm.purchaseTimeRange[1]
}
if (searchForm.inStockTimeRange && searchForm.inStockTimeRange.length === 2) {
params.inStockTimeStart = searchForm.inStockTimeRange[0]
params.inStockTimeEnd = searchForm.inStockTimeRange[1]
}
if (searchForm.activationTimeRange && searchForm.activationTimeRange.length === 2) {
params.activationTimeStart = searchForm.activationTimeRange[0]
params.activationTimeEnd = searchForm.activationTimeRange[1]
}
//
if (searchForm.minPrice !== undefined) {
params.minPrice = searchForm.minPrice
}
if (searchForm.maxPrice !== undefined) {
params.maxPrice = searchForm.maxPrice
}
emit('search', params)
showSearchModal.value = false
}
//
const handleReset = () => {
Object.keys(searchForm).forEach(key => {
if (Array.isArray(searchForm[key])) {
searchForm[key] = []
} else {
searchForm[key] = ''
}
})
formRef.value?.resetFields()
emit('reset')
showSearchModal.value = false
}
</script>
<style scoped lang="scss">
.search-form {
.arco-form-item {
margin-bottom: 16px;
.arco-form-item-label {
font-weight: 500;
color: var(--color-text-1);
margin-bottom: 8px;
}
}
.arco-input,
.arco-select,
.arco-input-number {
border-radius: 6px;
border: 1px solid var(--color-border);
transition: all 0.2s ease;
&:hover {
border-color: var(--color-primary-light-3);
}
&:focus,
&.arco-input-focus,
&.arco-select-focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(var(--primary-6), 0.1);
}
}
.arco-range-picker {
border-radius: 6px;
border: 1px solid var(--color-border);
transition: all 0.2s ease;
&:hover {
border-color: var(--color-primary-light-3);
}
&:focus,
&.arco-range-picker-focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(var(--primary-6), 0.1);
}
}
}
.search-actions {
display: flex;
justify-content: center;
padding-top: 16px;
border-top: 1px solid var(--color-border);
.arco-btn {
border-radius: 6px;
font-weight: 500;
min-width: 80px;
}
}
//
@media (max-width: 768px) {
.search-form {
.arco-row {
.arco-col {
margin-bottom: 8px;
}
}
}
.search-actions {
.arco-space {
width: 100%;
justify-content: space-between;
}
}
}
</style>

View File

@ -0,0 +1,856 @@
<template>
<div class="equipment-procurement-container">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="page-title">
<IconDesktop style="font-size: 24px; margin-right: 12px; color: var(--color-primary);" />
<h1>设备采购管理</h1>
</div>
<div class="page-description">
管理企业设备采购流程包括采购申请订单管理供应商管理等
</div>
</div>
<div class="header-right">
<a-space>
<ProcurementSearch
:loading="loading"
@search="handleSearch"
@reset="handleReset"
/>
<a-button type="primary" @click="handleAdd" size="large">
<template #icon>
<IconPlus />
</template>
新增采购
</a-button>
</a-space>
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-container">
<a-row :gutter="16">
<a-col :span="6">
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<IconDesktop />
</div>
<div class="stat-info">
<div class="stat-number">{{ pagination.total }}</div>
<div class="stat-label">采购总数</div>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<IconClockCircle />
</div>
<div class="stat-info">
<div class="stat-number">{{ getPendingCount() }}</div>
<div class="stat-label">待处理</div>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
<IconCheckCircle />
</div>
<div class="stat-info">
<div class="stat-number">{{ getCompletedCount() }}</div>
<div class="stat-label">已完成</div>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
<IconApps />
</div>
<div class="stat-info">
<div class="stat-number">¥{{ getTotalAmount() }}</div>
<div class="stat-label">采购总额</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
</div>
<!-- 数据表格 -->
<a-card class="table-card" :bordered="false">
<template #title>
<div class="card-title">
<span>采购记录</span>
<div class="table-actions">
<a-space>
<a-button type="text" @click="refreshData">
<template #icon>
<IconRefresh />
</template>
刷新
</a-button>
<a-button type="text" @click="handleExport">
<template #icon>
<IconDownload />
</template>
导出
</a-button>
</a-space>
</div>
</div>
</template>
<a-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
:row-selection="rowSelection"
row-key="equipmentId"
@change="handleTableChange"
>
<!-- 设备状态 -->
<template #equipmentStatus="{ record }">
<a-tag :color="getEquipmentStatusColor(record.equipmentStatus)">
{{ getEquipmentStatusText(record.equipmentStatus) }}
</a-tag>
</template>
<!-- 位置状态 -->
<template #locationStatus="{ record }">
<a-tag :color="getLocationStatusColor(record.locationStatus)">
{{ getLocationStatusText(record.locationStatus) }}
</a-tag>
</template>
<!-- 健康状态 -->
<template #healthStatus="{ record }">
<a-tag :color="getHealthStatusColor(record.healthStatus)">
{{ getHealthStatusText(record.healthStatus) }}
</a-tag>
</template>
<!-- 采购价格 -->
<template #purchasePrice="{ record }">
<span v-if="record.purchasePrice" class="price-text">
¥{{ formatPrice(record.purchasePrice) }}
</span>
<span v-else class="no-data">-</span>
</template>
<!-- 当前净值 -->
<template #currentNetValue="{ record }">
<span v-if="record.currentNetValue" class="price-text">
¥{{ formatPrice(record.currentNetValue) }}
</span>
<span v-else class="no-data">-</span>
</template>
<!-- 创建时间 -->
<template #createTime="{ record }">
<span v-if="record.createTime" class="time-text">
{{ formatDateTime(record.createTime) }}
</span>
<span v-else class="no-data">-</span>
</template>
<!-- 操作 -->
<template #action="{ record }">
<a-space>
<a-button type="text" size="small" @click="handleView(record)">
查看
</a-button>
<a-button type="text" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-popconfirm
content="确定要删除这条采购记录吗?"
@ok="handleDelete(record)"
>
<a-button type="text" size="small" status="danger">
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</a-table>
</a-card>
<!-- 新增/编辑弹窗 -->
<ProcurementModal
v-model:visible="modalVisible"
:procurement-data="currentProcurement"
:mode="modalMode"
@success="handleModalSuccess"
/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, watch } from 'vue'
import { Modal } from '@arco-design/web-vue'
import {
IconCheckCircle,
IconClockCircle,
IconDownload,
IconPlus,
IconRefresh,
IconDesktop,
IconApps
} from '@arco-design/web-vue/es/icon'
import message from '@arco-design/web-vue/es/message'
import ProcurementModal from './components/ProcurementModal.vue'
import ProcurementSearch from './components/ProcurementSearch.vue'
import { equipmentProcurementApi } from '@/apis/equipment/procurement'
import type { EquipmentListReq, EquipmentResp } from '@/apis/equipment/type'
defineOptions({ name: 'EquipmentProcurement' })
//
const currentSearchParams = ref<EquipmentListReq>({
minPrice: undefined,
maxPrice: undefined
})
//
const tableData = ref<EquipmentResp[]>([])
const loading = ref(false)
//
const pagination = reactive<any>({
current: 1,
pageSize: 10,
total: 0,
showPageSize: true,
showJumper: true,
showTotal: (total: number) => `${total} 条记录`,
})
//
const modalVisible = ref(false)
const currentProcurement = ref<EquipmentResp | null>(null)
const modalMode = ref<'add' | 'edit' | 'view'>('add')
//
const selectedRowKeys = ref<string[]>([])
const rowSelection = reactive({
type: 'checkbox' as const,
showCheckedAll: true,
selectedRowKeys,
onChange: (keys: string[]) => {
selectedRowKeys.value = keys
},
})
//
const columns = [
{
title: '资产编号',
dataIndex: 'assetCode',
key: 'assetCode',
width: 120,
},
{
title: '设备名称',
dataIndex: 'equipmentName',
key: 'equipmentName',
width: 150,
},
{
title: '设备类型',
dataIndex: 'equipmentType',
key: 'equipmentType',
width: 120,
},
{
title: '设备型号',
dataIndex: 'equipmentModel',
key: 'equipmentModel',
width: 120,
},
{
title: '品牌',
dataIndex: 'brand',
key: 'brand',
width: 100,
},
{
title: '供应商',
dataIndex: 'supplierName',
key: 'supplierName',
width: 150,
},
{
title: '采购订单',
dataIndex: 'purchaseOrder',
key: 'purchaseOrder',
width: 120,
},
{
title: '采购价格',
dataIndex: 'purchasePrice',
key: 'purchasePrice',
slotName: 'purchasePrice',
width: 120,
},
{
title: '当前净值',
dataIndex: 'currentNetValue',
key: 'currentNetValue',
slotName: 'currentNetValue',
width: 120,
},
{
title: '设备状态',
dataIndex: 'equipmentStatus',
key: 'equipmentStatus',
slotName: 'equipmentStatus',
width: 120,
},
{
title: '位置状态',
dataIndex: 'locationStatus',
key: 'locationStatus',
slotName: 'locationStatus',
width: 120,
},
{
title: '健康状态',
dataIndex: 'healthStatus',
key: 'healthStatus',
slotName: 'healthStatus',
width: 100,
},
{
title: '负责人',
dataIndex: 'responsiblePerson',
key: 'responsiblePerson',
width: 100,
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
slotName: 'createTime',
width: 160,
},
{
title: '操作',
key: 'action',
slotName: 'action',
width: 200,
fixed: 'right',
},
]
//
const getEquipmentStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
normal: 'green',
repair: 'orange',
scrap: 'red',
idle: 'blue',
lost: 'gray',
}
return colorMap[status] || 'blue'
}
//
const getEquipmentStatusText = (status: string) => {
const textMap: Record<string, string> = {
normal: '正常',
repair: '维修中',
scrap: '已报废',
idle: '闲置',
lost: '丢失',
}
return textMap[status] || '未知'
}
//
const getLocationStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
in_stock: 'blue',
in_use: 'green',
repair: 'orange',
scrapped: 'red',
on_loan: 'purple',
lost: 'gray',
idle: 'cyan',
}
return colorMap[status] || 'blue'
}
//
const getLocationStatusText = (status: string) => {
const textMap: Record<string, string> = {
in_stock: '库存中',
in_use: '使用中',
repair: '维修中',
scrapped: '已报废',
on_loan: '外借中',
lost: '丢失',
idle: '闲置',
}
return textMap[status] || '未知'
}
//
const getHealthStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
excellent: 'green',
good: 'blue',
normal: 'orange',
poor: 'red',
critical: 'red',
}
return colorMap[status] || 'blue'
}
//
const getHealthStatusText = (status: string) => {
const textMap: Record<string, string> = {
excellent: '优秀',
good: '良好',
normal: '一般',
poor: '较差',
critical: '危险',
}
return textMap[status] || '未知'
}
//
const formatPrice = (price: number) => {
return price.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
}
//
const formatDateTime = (dateTime: string) => {
if (!dateTime) return '-'
const date = new Date(dateTime)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
}
//
const transformBackendData = (data: any[]): EquipmentResp[] => {
return data.map((item: any) => ({
equipmentId: item.equipmentId || item.id,
assetCode: item.assetCode,
equipmentName: item.equipmentName,
equipmentType: item.equipmentType,
equipmentTypeLabel: item.equipmentTypeLabel,
equipmentModel: item.equipmentModel,
equipmentSn: item.equipmentSn,
brand: item.brand,
specification: item.specification,
equipmentStatus: item.equipmentStatus,
equipmentStatusLabel: item.equipmentStatusLabel,
useStatus: item.useStatus,
locationStatus: item.locationStatus,
locationStatusLabel: item.locationStatusLabel,
physicalLocation: item.physicalLocation,
responsiblePerson: item.responsiblePerson,
healthStatus: item.healthStatus,
healthStatusLabel: item.healthStatusLabel,
purchaseTime: item.purchaseTime,
inStockTime: item.inStockTime,
activationTime: item.activationTime,
expectedScrapTime: item.expectedScrapTime,
actualScrapTime: item.actualScrapTime,
statusChangeTime: item.statusChangeTime,
purchaseOrder: item.purchaseOrder,
supplierName: item.supplierName,
purchasePrice: item.purchasePrice,
currentNetValue: item.currentNetValue,
depreciationMethod: item.depreciationMethod,
depreciationYears: item.depreciationYears,
salvageValue: item.salvageValue,
warrantyExpireDate: item.warrantyExpireDate,
lastMaintenanceDate: item.lastMaintenanceDate,
nextMaintenanceDate: item.nextMaintenanceDate,
maintenancePerson: item.maintenancePerson,
inventoryBarcode: item.inventoryBarcode,
assetRemark: item.assetRemark,
projectId: item.projectId,
projectName: item.projectName,
userId: item.userId,
name: item.name,
createTime: item.createTime,
updateTime: item.updateTime,
accountNumber: item.accountNumber,
quantity: item.quantity,
unitPrice: item.unitPrice,
totalPrice: item.totalPrice,
inventoryBasis: item.inventoryBasis,
dynamicRecord: item.dynamicRecord,
}))
}
//
const loadData = async (searchParams?: EquipmentListReq) => {
console.log('📊 loadData - 开始加载数据')
console.log('📊 loadData - 接收到的搜索参数:', searchParams)
loading.value = true
try {
const params: EquipmentListReq = {
pageSize: pagination.pageSize,
page: pagination.current,
minPrice: undefined,
maxPrice: undefined,
...(searchParams || {}),
}
console.log('📊 loadData - 构建的完整请求参数:', params)
const res = await equipmentProcurementApi.page(params)
console.log('API响应:', res)
if (res.success || res.status === 200 || res.code === 200) {
let dataList: any[] = []
if (Array.isArray(res.data)) {
dataList = res.data
} else if (res.data && Array.isArray((res.data as any).records)) {
dataList = (res.data as any).records
} else if (res.data && Array.isArray((res.data as any).list)) {
dataList = (res.data as any).list
} else if (res.data && Array.isArray((res.data as any).rows)) {
dataList = (res.data as any).rows
}
console.log('处理后的数据列表:', dataList)
if (dataList.length > 0) {
const transformedData = transformBackendData(dataList)
console.log('转换后的数据:', transformedData)
tableData.value = transformedData
} else {
tableData.value = []
}
pagination.total = (res.data as any)?.total || (res as any).total || dataList.length || 0
console.log('总数:', pagination.total)
} else {
message.error(res.msg || '加载数据失败')
}
} catch (error: any) {
console.error('加载数据失败:', error)
message.error(error?.message || '加载数据失败')
} finally {
loading.value = false
}
}
//
const handleSearch = (searchParams: EquipmentListReq) => {
console.log('🔍 主组件 - 接收到的搜索参数:', searchParams)
pagination.current = 1
currentSearchParams.value = { ...searchParams }
loadData(searchParams)
}
//
const handleReset = () => {
console.log('🔄 主组件 - 重置操作')
pagination.current = 1
currentSearchParams.value = {
minPrice: undefined,
maxPrice: undefined
}
loadData()
}
//
const handleTableChange = (pag: any) => {
pagination.current = pag.current || 1
pagination.pageSize = pag.pageSize || 10
loadData(currentSearchParams.value)
}
//
const handleAdd = () => {
modalMode.value = 'add'
currentProcurement.value = null
modalVisible.value = true
}
//
const handleView = (record: EquipmentResp) => {
modalMode.value = 'view'
currentProcurement.value = { ...record }
modalVisible.value = true
}
//
const handleEdit = (record: EquipmentResp) => {
modalMode.value = 'edit'
currentProcurement.value = { ...record }
modalVisible.value = true
}
//
const handleDelete = async (record: EquipmentResp) => {
try {
await equipmentProcurementApi.delete(record.equipmentId)
message.success('删除成功')
loadData(currentSearchParams.value)
} catch (error: any) {
console.error('删除失败:', error)
message.error(error?.message || '删除失败')
}
}
//
const handleModalSuccess = () => {
modalVisible.value = false
loadData(currentSearchParams.value)
}
//
const refreshData = () => {
loadData(currentSearchParams.value)
}
//
const handleExport = async () => {
try {
const response = await equipmentProcurementApi.export(currentSearchParams.value)
const blob = response.data || response
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `设备采购记录_${new Date().toISOString().split('T')[0]}.xlsx`
link.click()
window.URL.revokeObjectURL(url)
message.success('导出成功')
} catch (error: any) {
console.error('导出失败:', error)
message.error(error?.message || '导出失败')
}
}
//
const getPendingCount = () => {
return tableData.value.filter(item =>
item.equipmentStatus === 'pending' ||
item.locationStatus === 'pending'
).length
}
const getCompletedCount = () => {
return tableData.value.filter(item =>
item.equipmentStatus === 'completed' ||
item.locationStatus === 'completed'
).length
}
const getTotalAmount = () => {
const total = tableData.value.reduce((sum, item) => {
return sum + (item.purchasePrice || 0)
}, 0)
return formatPrice(total)
}
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.equipment-procurement-container {
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
.header-left {
.page-title {
display: flex;
align-items: center;
margin-bottom: 8px;
h1 {
margin: 0;
color: white;
font-size: 28px;
font-weight: 600;
background: linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
}
.page-description {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
}
}
.header-right {
.arco-btn {
border-radius: 8px;
font-weight: 500;
}
}
}
}
.stats-container {
margin-bottom: 24px;
.stat-card {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.stat-content {
display: flex;
align-items: center;
.stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
.arco-icon {
font-size: 24px;
color: white;
}
}
.stat-info {
flex: 1;
.stat-number {
font-size: 24px;
font-weight: 600;
color: var(--color-text-1);
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: var(--color-text-3);
}
}
}
}
}
.table-card {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
.card-title {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 18px;
font-weight: 600;
color: var(--color-text-1);
}
.table-actions {
.arco-btn {
border-radius: 6px;
}
}
}
.arco-table {
.arco-table-th {
background-color: var(--color-fill-2);
font-weight: 600;
}
.arco-table-tr:hover {
background-color: var(--color-fill-1);
}
}
}
.price-text {
color: #f56c6c;
font-weight: 500;
}
.time-text {
color: var(--color-text-2);
font-size: 12px;
}
.no-data {
color: var(--color-text-4);
font-style: italic;
}
}
//
@media (max-width: 768px) {
.equipment-procurement-container {
.page-header {
.header-content {
flex-direction: column;
align-items: flex-start;
.header-right {
margin-top: 16px;
width: 100%;
.arco-space {
width: 100%;
justify-content: space-between;
}
}
}
}
.stats-container {
.arco-col {
margin-bottom: 16px;
}
}
}
}
</style>

View File

@ -0,0 +1,136 @@
<template>
<div class="procurement-test">
<a-card title="设备采购模块测试" :bordered="false">
<template #extra>
<a-space>
<a-button type="primary" @click="testApi">
测试API
</a-button>
<a-button @click="testSearch">
测试搜索
</a-button>
<a-button @click="testAdd">
测试新增
</a-button>
</a-space>
</template>
<a-divider />
<div class="test-results">
<h3>测试结果</h3>
<a-textarea
v-model="testResults"
:rows="10"
placeholder="测试结果将显示在这里..."
readonly
/>
</div>
<a-divider />
<div class="test-params">
<h3>测试参数</h3>
<a-form layout="vertical">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="设备名称">
<a-input v-model="testParams.equipmentName" placeholder="测试设备" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="设备型号">
<a-input v-model="testParams.equipmentModel" placeholder="测试型号" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="供应商">
<a-input v-model="testParams.supplierName" placeholder="测试供应商" />
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { Message } from '@arco-design/web-vue'
import { equipmentProcurementApi } from '@/apis/equipment/procurement'
defineOptions({ name: 'ProcurementTest' })
const testResults = ref('')
const testParams = reactive({
equipmentName: '测试设备',
equipmentModel: '测试型号',
supplierName: '测试供应商',
})
//
const addTestResult = (message: string) => {
const timestamp = new Date().toLocaleString()
testResults.value += `[${timestamp}] ${message}\n`
}
// API
const testApi = async () => {
try {
addTestResult('开始测试API...')
const params = {
page: 1,
pageSize: 10,
equipmentName: testParams.equipmentName,
equipmentModel: testParams.equipmentModel,
supplierName: testParams.supplierName,
}
addTestResult(`请求参数: ${JSON.stringify(params, null, 2)}`)
const response = await equipmentProcurementApi.page(params)
addTestResult(`API响应: ${JSON.stringify(response, null, 2)}`)
Message.success('API测试成功')
} catch (error: any) {
addTestResult(`API测试失败: ${error.message}`)
Message.error('API测试失败')
}
}
//
const testSearch = () => {
addTestResult('测试搜索功能...')
addTestResult(`搜索参数: ${JSON.stringify(testParams, null, 2)}`)
Message.info('搜索测试完成')
}
//
const testAdd = () => {
addTestResult('测试新增功能...')
addTestResult(`新增参数: ${JSON.stringify(testParams, null, 2)}`)
Message.info('新增测试完成')
}
</script>
<style scoped lang="scss">
.procurement-test {
.test-results {
margin-bottom: 24px;
h3 {
margin-bottom: 16px;
color: var(--color-text-1);
}
}
.test-params {
h3 {
margin-bottom: 16px;
color: var(--color-text-1);
}
}
}
</style>

View File

@ -0,0 +1,81 @@
<template>
<div class="training-detail-container">
<a-card title="培训基本信息">
<!-- 培训基本信息展示区 -->
</a-card>
<a-card title="互动与留痕" style="margin-top: 16px">
<a-tabs>
<a-tab-pane key="sign" tab="签到">
<div class="sign-in-section">
<a-button type="primary" :disabled="signedIn" @click="handleSignIn">
{{ signedIn ? '已签到' : '点击签到' }}
</a-button>
<div v-if="signedIn" class="sign-in-success">签到成功</div>
<a-divider />
<div class="sign-in-records">
<h4>签到记录</h4>
<a-table :data="signInRecords" :columns="signInColumns" row-key="id" size="small" />
</div>
</div>
</a-tab-pane>
<a-tab-pane key="quiz" tab="答题">
<!-- 答题互动区 -->
</a-tab-pane>
<a-tab-pane key="discussion" tab="讨论">
<!-- 讨论区 -->
</a-tab-pane>
<a-tab-pane key="score" tab="打分/评价">
<!-- 打分/评价区 -->
</a-tab-pane>
<a-tab-pane key="feedback" tab="反馈/留痕">
<!-- 反馈/留痕区 -->
</a-tab-pane>
</a-tabs>
</a-card>
<a-card title="案例/公开课关联" style="margin-top: 16px">
<!-- 关联案例/公开课展示区 -->
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
//
const signedIn = ref(false)
//
const signInRecords = ref([
{ id: 1, name: '张三', time: '2024-06-01 09:00:00' },
{ id: 2, name: '李四', time: '2024-06-01 09:01:00' },
])
const signInColumns = [
{ title: '姓名', dataIndex: 'name', key: 'name' },
{ title: '签到时间', dataIndex: 'time', key: 'time' },
]
//
function handleSignIn() {
signedIn.value = true
// API
}
</script>
<style scoped>
.training-detail-container {
padding: 16px;
background: #f5f5f5;
min-height: 100vh;
}
.sign-in-section {
padding: 16px 0;
}
.sign-in-success {
color: #52c41a;
margin-top: 8px;
}
.sign-in-records {
margin-top: 24px;
}
</style>

View File

@ -0,0 +1,576 @@
<template>
<a-modal
:visible="visible"
:title="isEdit ? '编辑培训计划' : '新增培训计划'"
width="800px"
:confirm-loading="loading"
:ok-button-props="{ disabled: !isFormValid }"
@cancel="handleCancel"
@ok="handleSubmit"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
auto-label-width
>
<a-form-item label="计划名称" field="planName">
<a-input
v-model="formData.planName"
placeholder="请输入计划名称"
:disabled="isView"
show-word-limit
:max-length="100"
/>
</a-form-item>
<a-form-item label="培训类型" field="trainingType">
<a-select
v-model="formData.trainingType"
:options="trainingTypeOptions"
placeholder="请选择培训类型"
:disabled="isView"
/>
</a-form-item>
<a-form-item label="培训级别" field="trainingLevel">
<a-select
v-model="formData.trainingLevel"
:options="trainingLevelOptions"
placeholder="请选择培训级别"
:disabled="isView"
/>
</a-form-item>
<a-form-item label="培训内容" field="trainingContent">
<a-textarea
v-model="formData.trainingContent"
placeholder="请输入培训内容"
:rows="4"
:disabled="isView"
show-word-limit
:max-length="500"
/>
</a-form-item>
<a-form-item label="培训讲师" field="trainer">
<a-input
v-model="formData.trainer"
placeholder="请输入培训讲师"
:disabled="isView"
/>
</a-form-item>
<a-form-item label="培训地点" field="trainingLocation">
<a-input
v-model="formData.trainingLocation"
placeholder="请输入培训地点"
:disabled="isView"
/>
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="开始时间" field="startTime">
<a-date-picker
v-model="formData.startTime"
show-time
placeholder="请选择开始时间"
style="width: 100%"
:disabled="isView"
:disabled-date="(current) => current && current < dayjs().startOf('day')"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="结束时间" field="endTime">
<a-date-picker
v-model="formData.endTime"
show-time
placeholder="请选择结束时间"
style="width: 100%"
:disabled="isView"
:disabled-date="(current) => current && current < dayjs().startOf('day')"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="最大参训人数" field="maxParticipants">
<a-input-number
v-model="formData.maxParticipants"
placeholder="请输入最大参训人数"
:min="1"
:max="1000"
style="width: 100%"
:disabled="isView"
/>
</a-form-item>
<a-form-item label="培训要求" field="requirements">
<a-textarea
v-model="formData.requirements"
placeholder="请输入培训要求"
:rows="3"
:disabled="isView"
show-word-limit
:max-length="300"
/>
</a-form-item>
<a-form-item label="备注" field="remark">
<a-textarea
v-model="formData.remark"
placeholder="请输入备注"
:rows="3"
:disabled="isView"
show-word-limit
:max-length="200"
/>
</a-form-item>
<!-- 调试按钮 - 仅在开发环境显示 -->
<a-form-item v-if="isDev" label="调试">
<a-space>
<a-button type="dashed" size="small" @click="testFormData">
测试表单数据绑定
</a-button>
<a-button type="dashed" size="small" @click="fillTestData">
填充测试数据
</a-button>
<a-button size="small" type="dashed" @click="clearFormData">
清空表单数据
</a-button>
</a-space>
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { FormInstance } from '@arco-design/web-vue'
import dayjs from 'dayjs'
import { createTrainingPlan, updateTrainingPlan } from '@/apis/training'
import type { TrainingPlanReq, TrainingPlanResp } from '@/types/training.d'
defineOptions({ name: 'TrainingPlanModal' })
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
interface Props {
visible: boolean
planData?: TrainingPlanResp | null
}
interface Emits {
(e: 'update:visible', visible: boolean): void
(e: 'success'): void
}
//
const formRef = ref<FormInstance>()
const loading = ref(false)
//
const isDev = import.meta.env.DEV
//
const isEdit = computed(() => !!props.planData?.planId)
//
const isView = computed(() => !!props.planData?.planId && !isEdit.value)
//
interface FormDataType {
planName: string
trainingType: string
trainingLevel: string
trainingContent: string
trainer: string
trainingLocation: string
startTime: any // dayjsnull
endTime: any // dayjsnull
maxParticipants?: number
requirements: string
remark: string
status?: string
}
//
const formData = reactive<FormDataType>({
planName: '',
trainingType: '',
trainingLevel: '',
trainingContent: '',
trainer: '',
trainingLocation: '',
startTime: null,
endTime: null,
maxParticipants: undefined,
requirements: '',
remark: '',
status: 'DRAFT',
})
//
const rules = {
planName: [
{ required: true, message: '请输入计划名称', trigger: 'blur' },
{ min: 2, max: 100, message: '计划名称长度应在2-100个字符之间', trigger: 'blur' },
],
trainingType: [{ required: true, message: '请选择培训类型', trigger: 'change' }],
trainingLevel: [{ required: true, message: '请选择培训级别', trigger: 'change' }],
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
endTime: [
{ required: true, message: '请选择结束时间', trigger: 'change' },
{
validator: (value: any, callback: any) => {
if (value && formData.startTime && dayjs(value).isBefore(dayjs(formData.startTime))) {
callback('结束时间不能早于开始时间')
} else {
callback()
}
},
trigger: 'change',
},
],
maxParticipants: [
{
validator: (value: any, callback: any) => {
if (value !== undefined && value !== null) {
if (value < 1) {
callback('最大参训人数不能小于1')
} else if (value > 1000) {
callback('最大参训人数不能超过1000')
} else {
callback()
}
} else {
callback()
}
},
trigger: 'blur',
},
],
trainingContent: [
{ max: 500, message: '培训内容长度不能超过500个字符', trigger: 'blur' },
],
requirements: [
{ max: 300, message: '培训要求长度不能超过300个字符', trigger: 'blur' },
],
remark: [
{ max: 200, message: '备注长度不能超过200个字符', trigger: 'blur' },
],
}
//
const trainingTypeOptions = [
{ label: '安全教育', value: 'SAFETY' },
{ label: '技能培训', value: 'SKILL' },
{ label: '企业文化', value: 'CULTURE' },
]
//
const trainingLevelOptions = [
{ label: '现场级', value: 'SITE' },
{ label: '部门级', value: 'DEPARTMENT' },
{ label: '公司级', value: 'COMPANY' },
]
//
watch(
() => props.visible,
(visible) => {
if (visible) {
// eslint-disable-next-line ts/no-use-before-define
initFormData()
}
},
)
//
watch(
() => props.planData,
() => {
if (props.visible) {
// eslint-disable-next-line ts/no-use-before-define
initFormData()
}
},
)
//
watch(
formData,
(newData) => {
console.log('表单数据变化:', newData)
console.log('表单数据详情:', {
planName: newData.planName,
trainingType: newData.trainingType,
trainingLevel: newData.trainingLevel,
startTime: newData.startTime,
endTime: newData.endTime,
maxParticipants: newData.maxParticipants,
trainer: newData.trainer,
trainingLocation: newData.trainingLocation,
trainingContent: newData.trainingContent,
requirements: newData.requirements,
remark: newData.remark,
})
},
{ deep: true },
)
//
watch(
() => formData.startTime,
(newStartTime) => {
console.log('开始时间变化:', newStartTime)
if (newStartTime && formData.endTime && dayjs(formData.endTime).isBefore(dayjs(newStartTime))) {
//
formData.endTime = null
}
},
)
//
watch(
() => formData.endTime,
(newEndTime) => {
console.log('结束时间变化:', newEndTime)
},
)
//
const isFormValid = computed(() => {
return formData.planName.trim()
&& formData.trainingType
&& formData.trainingLevel
&& formData.startTime
&& formData.endTime
})
//
const initFormData = () => {
console.log('初始化表单数据props.planData:', props.planData)
if (props.planData) {
//
Object.assign(formData, {
planName: props.planData.planName || '',
trainingType: props.planData.trainingType || '',
trainingLevel: props.planData.trainingLevel || '',
trainingContent: props.planData.trainingContent || '',
trainer: props.planData.trainer || '',
trainingLocation: props.planData.trainingLocation || '',
startTime: props.planData.startTime ? dayjs(props.planData.startTime) : null,
endTime: props.planData.endTime ? dayjs(props.planData.endTime) : null,
maxParticipants: props.planData.maxParticipants,
requirements: props.planData.requirements || '',
remark: props.planData.remark || '',
status: props.planData.status || 'DRAFT',
})
} else {
//
// eslint-disable-next-line ts/no-use-before-define
resetFormData()
}
console.log('初始化后的表单数据:', formData)
}
//
const resetFormData = () => {
Object.assign(formData, {
planName: '',
trainingType: '',
trainingLevel: '',
trainingContent: '',
trainer: '',
trainingLocation: '',
startTime: null,
endTime: null,
maxParticipants: undefined,
requirements: '',
remark: '',
status: 'DRAFT',
})
}
//
const handleSubmit = async () => {
try {
console.log('开始提交表单,表单数据:', formData)
console.log('表单数据原始值:', {
planName: formData.planName,
trainingType: formData.trainingType,
trainingLevel: formData.trainingLevel,
startTime: formData.startTime,
endTime: formData.endTime,
maxParticipants: formData.maxParticipants,
trainer: formData.trainer,
trainingLocation: formData.trainingLocation,
trainingContent: formData.trainingContent,
requirements: formData.requirements,
remark: formData.remark,
})
//
await formRef.value?.validate()
loading.value = true
// -
if (!formData.planName?.trim()) {
throw new Error('计划名称不能为空')
}
if (!formData.trainingType) {
throw new Error('培训类型不能为空')
}
if (!formData.trainingLevel) {
throw new Error('培训级别不能为空')
}
if (!formData.startTime) {
throw new Error('开始时间不能为空')
}
if (!formData.endTime) {
throw new Error('结束时间不能为空')
}
//
const submitData: TrainingPlanReq = {
planName: formData.planName.trim(),
trainingType: formData.trainingType,
trainingLevel: formData.trainingLevel,
trainingContent: formData.trainingContent?.trim() || '',
trainer: formData.trainer?.trim() || '',
trainingLocation: formData.trainingLocation?.trim() || '',
startTime: formData.startTime ? dayjs(formData.startTime).format('YYYY-MM-DD HH:mm:ss') : '',
endTime: formData.endTime ? dayjs(formData.endTime).format('YYYY-MM-DD HH:mm:ss') : '',
status: formData.status || 'DRAFT',
maxParticipants: formData.maxParticipants || undefined,
requirements: formData.requirements?.trim() || '',
remark: formData.remark?.trim() || '',
}
console.log('提交的数据:', submitData)
console.log('提交的数据JSON:', JSON.stringify(submitData, null, 2))
if (isEdit.value && props.planData) {
console.log('执行更新操作')
await updateTrainingPlan(props.planData.planId, submitData)
Message.success('培训计划更新成功')
} else {
console.log('执行创建操作')
await createTrainingPlan(submitData)
Message.success('培训计划创建成功')
}
emit('success')
emit('update:visible', false)
} catch (error: any) {
console.error('提交失败:', error)
//
if (error?.response?.data?.message) {
Message.error(error.response.data.message)
} else if (error?.message) {
Message.error(error.message)
} else if (typeof error === 'string') {
Message.error(error)
} else {
Message.error('操作失败,请检查表单数据或稍后重试')
}
} finally {
loading.value = false
}
}
//
const handleCancel = () => {
//
formRef.value?.resetFields()
resetFormData()
emit('update:visible', false)
}
//
const testFormData = () => {
console.log('=== 表单数据绑定测试 ===')
console.log('当前表单数据:', formData)
console.log('当前表单数据详情:', {
planName: formData.planName,
trainingType: formData.trainingType,
trainingLevel: formData.trainingLevel,
startTime: formData.startTime,
endTime: formData.endTime,
maxParticipants: formData.maxParticipants,
trainer: formData.trainer,
trainingLocation: formData.trainingLocation,
trainingContent: formData.trainingContent,
requirements: formData.requirements,
remark: formData.remark,
})
//
formRef.value?.validate().then(() => {
console.log('✅ 表单验证通过')
}).catch((errors) => {
console.log('❌ 表单验证失败:', errors)
})
//
console.log('表单字段值测试:')
console.log('- planName:', formData.planName, typeof formData.planName)
console.log('- trainingType:', formData.trainingType, typeof formData.trainingType)
console.log('- trainingLevel:', formData.trainingLevel, typeof formData.trainingLevel)
console.log('- startTime:', formData.startTime, typeof formData.startTime)
console.log('- endTime:', formData.endTime, typeof formData.endTime)
}
//
const fillTestData = () => {
Object.assign(formData, {
planName: '测试计划名称',
trainingType: 'SAFETY',
trainingLevel: 'SITE',
trainingContent: '测试培训内容',
trainer: '测试讲师',
trainingLocation: '测试地点',
startTime: dayjs('2023-10-27 10:00'),
endTime: dayjs('2023-10-27 12:00'),
maxParticipants: 50,
requirements: '测试培训要求',
remark: '测试备注',
status: 'DRAFT',
})
console.log('已填充测试数据')
}
//
const clearFormData = () => {
resetFormData()
console.log('已清空表单数据')
}
//
const watchFormField = (fieldName: string) => {
watch(() => formData[fieldName as keyof typeof formData], (newValue, oldValue) => {
console.log(`字段 ${fieldName} 变化:`, { oldValue, newValue })
})
}
//
Object.keys(formData).forEach((fieldName) => {
watchFormField(fieldName)
})
</script>
<style lang="scss" scoped>
.ant-form-item {
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,577 @@
<template>
<div class="training-plan-container">
<!-- 搜索表单 -->
<a-card class="search-card" :bordered="false">
<a-form layout="inline" :model="searchForm" @submit="handleSearch">
<a-form-item label="计划名称">
<a-input
v-model:value="searchForm.planName"
placeholder="请输入计划名称"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item label="培训类型">
<a-select
v-model:value="searchForm.trainingType"
:options="trainingTypeOptions"
placeholder="请选择培训类型"
allow-clear
style="width: 150px"
/>
</a-form-item>
<a-form-item label="培训级别">
<a-select
v-model:value="searchForm.trainingLevel"
:options="trainingLevelOptions"
placeholder="请选择培训级别"
allow-clear
style="width: 150px"
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="searchForm.status"
:options="statusOptions"
placeholder="请选择状态"
allow-clear
style="width: 120px"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" :loading="loading">
<template #icon>
<IconSearch />
</template>
搜索
</a-button>
<a-button style="margin-left: 8px" @click="handleReset">
<template #icon>
<IconRefresh />
</template>
重置
</a-button>
</a-form-item>
</a-form>
</a-card>
<!-- 操作按钮 -->
<a-card class="table-card" :bordered="false">
<template #title>
<div class="card-title">
<span>培训计划列表</span>
<a-button type="primary" @click="handleAdd">
<template #icon>
<IconPlus />
</template>
新增计划
</a-button>
</div>
</template>
<!-- 数据表格 -->
<a-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
row-key="planId"
@change="handleTableChange"
>
<template #trainingType="{ record }">
<a-tag :color="getTrainingTypeColor(record.trainingType)">
{{ getTrainingTypeText(record.trainingType) }}
</a-tag>
</template>
<template #trainingLevel="{ record }">
<a-tag :color="getTrainingLevelColor(record.trainingLevel)">
{{ getTrainingLevelText(record.trainingLevel) }}
</a-tag>
</template>
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template #participants="{ record }">
<span>{{ record.currentParticipants || 0 }}/{{ record.maxParticipants || '-' }}</span>
</template>
<template #action="{ record }">
<a-space>
<a-button type="text" size="small" @click="handleView(record)">
查看
</a-button>
<a-button
v-if="record.status === 'DRAFT'"
type="text"
size="small"
@click="handleEdit(record)"
>
编辑
</a-button>
<a-button
v-if="record.status === 'DRAFT'"
type="text"
size="small"
@click="handlePublish(record)"
>
发布
</a-button>
<a-button
v-if="['DRAFT', 'PUBLISHED', 'IN_PROGRESS'].includes(record.status)"
type="text"
size="small"
danger
@click="handleCancel(record)"
>
取消
</a-button>
<a-button
v-if="record.status === 'DRAFT'"
type="text"
size="small"
danger
@click="handleDelete(record)"
>
删除
</a-button>
<a-button type="text" size="small" @click="handleDetail(record)">详情</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 新增/编辑弹窗 -->
<TrainingPlanModal
v-model:visible="modalVisible"
:plan-data="currentPlan"
@success="handleModalSuccess"
/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, watch } from 'vue'
import { Modal } from '@arco-design/web-vue'
import { IconPlus, IconRefresh, IconSearch } from '@arco-design/web-vue/es/icon'
import message from '@arco-design/web-vue/es/message'
import TrainingPlanModal from './components/TrainingPlanModal.vue'
import {
cancelTrainingPlan,
deleteTrainingPlan,
pageTrainingPlan,
publishTrainingPlan,
} from '@/apis/training'
import type { TrainingPlanPageQuery, TrainingPlanResp } from '@/types/training.d'
import router from '@/router'
defineOptions({ name: 'TrainingPlan' })
//
const searchForm = reactive<TrainingPlanPageQuery>({
planName: '',
trainingType: '',
trainingLevel: '',
status: '',
})
//
const tableData = ref<TrainingPlanResp[]>([])
const loading = ref(false)
//
const pagination = reactive<any>({
current: 1,
pageSize: 10,
total: 0,
showPageSize: true,
showJumper: true,
showTotal: (total: number) => `${total} 条记录`,
})
//
const modalVisible = ref(false)
const currentPlan = ref<TrainingPlanResp | null>(null)
//
const columns = [
{
title: '计划名称',
dataIndex: 'planName',
key: 'planName',
width: 200,
},
{
title: '培训类型',
dataIndex: 'trainingType',
key: 'trainingType',
slotName: 'trainingType',
width: 120,
},
{
title: '培训级别',
dataIndex: 'trainingLevel',
key: 'trainingLevel',
slotName: 'trainingLevel',
width: 120,
},
{
title: '培训讲师',
dataIndex: 'trainer',
key: 'trainer',
width: 120,
},
{
title: '培训地点',
dataIndex: 'trainingLocation',
key: 'trainingLocation',
width: 150,
},
{
title: '开始时间',
dataIndex: 'startTime',
key: 'startTime',
width: 150,
},
{
title: '结束时间',
dataIndex: 'endTime',
key: 'endTime',
width: 150,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
slotName: 'status',
width: 100,
},
{
title: '参训人数',
dataIndex: 'participants',
key: 'participants',
slotName: 'participants',
width: 100,
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 150,
},
{
title: '操作',
key: 'action',
slotName: 'action',
width: 200,
fixed: 'right',
},
]
//
const trainingTypeOptions = [
{ label: '安全教育', value: 'SAFETY' },
{ label: '技能培训', value: 'SKILL' },
{ label: '企业文化', value: 'CULTURE' },
]
const trainingLevelOptions = [
{ label: '现场级', value: 'SITE' },
{ label: '部门级', value: 'DEPARTMENT' },
{ label: '公司级', value: 'COMPANY' },
]
const statusOptions = [
{ label: '草稿', value: 'DRAFT' },
{ label: '已发布', value: 'PUBLISHED' },
{ label: '进行中', value: 'IN_PROGRESS' },
{ label: '已完成', value: 'COMPLETED' },
{ label: '已取消', value: 'CANCELLED' },
]
//
const getTrainingTypeText = (type: string) => {
const typeMap: Record<string, string> = {
SAFETY: '安全教育',
SKILL: '技能培训',
CULTURE: '企业文化',
安全教育: '安全教育',
技能培训: '技能培训',
企业文化: '企业文化',
}
return typeMap[type] || type
}
//
const getTrainingTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
SAFETY: 'red',
SKILL: 'blue',
CULTURE: 'green',
安全教育: 'red',
技能培训: 'blue',
企业文化: 'green',
}
return colorMap[type] || 'default'
}
//
const getTrainingLevelText = (level: string) => {
const levelMap: Record<string, string> = {
SITE: '现场级',
DEPARTMENT: '部门级',
COMPANY: '公司级',
现场级: '现场级',
部门级: '部门级',
公司级: '公司级',
}
return levelMap[level] || level
}
//
const getTrainingLevelColor = (level: string) => {
const colorMap: Record<string, string> = {
SITE: 'orange',
DEPARTMENT: 'purple',
COMPANY: 'cyan',
现场级: 'orange',
部门级: 'purple',
公司级: 'cyan',
}
return colorMap[level] || 'default'
}
//
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
DRAFT: '草稿',
PUBLISHED: '已发布',
IN_PROGRESS: '进行中',
COMPLETED: '已完成',
CANCELLED: '已取消',
}
return statusMap[status] || status
}
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
DRAFT: 'default',
PUBLISHED: 'blue',
IN_PROGRESS: 'processing',
COMPLETED: 'success',
CANCELLED: 'error',
}
return colorMap[status] || 'default'
}
//
const transformBackendData = (data: any[]) => {
console.log('转换前的数据:', data)
return data.map((item) => {
console.log('处理单个项目:', item)
return {
planId: item.planId || item.id,
planName: item.planName || item.name,
trainingType: item.trainingType || item.type,
trainingLevel: item.trainingLevel || item.level,
trainingContent: item.trainingContent || item.content,
trainer: item.trainer,
trainingLocation: item.trainingLocation || item.location,
startTime: item.startTime ? new Date(item.startTime).toLocaleString() : '',
endTime: item.endTime ? new Date(item.endTime).toLocaleString() : '',
status: item.status,
maxParticipants: item.maxParticipants || item.maxParticipantsNum,
currentParticipants: item.currentParticipants || item.currentParticipantsNum || 0,
requirements: item.requirements,
remark: item.remark,
createTime: item.createTime ? new Date(item.createTime).toLocaleString() : '',
createBy: item.createBy || item.createByUser,
}
})
}
//
const loadData = async () => {
loading.value = true
try {
const params = {
pageSize: pagination.pageSize,
page: pagination.current,
...searchForm, //
}
const res = await pageTrainingPlan(params)
console.log('API响应:', res)
//
if (res.success || res.status === 200 || res.code === 200) {
//
let dataList: any[] = []
//
if (Array.isArray(res.data)) {
dataList = res.data
} else if (res.data && Array.isArray((res as any).rows)) {
dataList = (res as any).rows
} else if (res.data && Array.isArray((res.data as any).records)) {
dataList = (res.data as any).records
} else if (res.data && Array.isArray((res.data as any).list)) {
dataList = (res.data as any).list
}
console.log('处理后的数据列表:', dataList)
if (dataList.length > 0) {
const transformedData = transformBackendData(dataList)
console.log('转换后的数据:', transformedData)
tableData.value = transformedData
} else {
tableData.value = []
}
//
pagination.total = (res as any).total || (res.data as any)?.total || dataList.length || 0
console.log('总数:', pagination.total)
} else {
message.error(res.msg || '加载数据失败')
}
} catch (error) {
console.error('加载数据失败:', error)
message.error('加载数据失败')
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
pagination.current = 1
loadData()
}
//
const handleReset = () => {
Object.assign(searchForm, {
planName: '',
trainingType: '',
trainingLevel: '',
status: '',
})
pagination.current = 1
loadData()
}
//
const handleTableChange = (pag: any) => {
pagination.current = pag.current || 1
pagination.pageSize = pag.pageSize || 10
loadData()
}
//
const handleAdd = () => {
currentPlan.value = null
modalVisible.value = true
}
//
const handleView = (record: TrainingPlanResp) => {
currentPlan.value = record
modalVisible.value = true
}
//
const handleEdit = (record: TrainingPlanResp) => {
currentPlan.value = record
modalVisible.value = true
}
//
const handlePublish = async (record: TrainingPlanResp) => {
try {
await publishTrainingPlan(record.planId)
message.success('发布成功')
loadData()
} catch (error) {
console.error('发布失败:', error)
message.error('发布失败')
}
}
//
const handleCancel = async (record: TrainingPlanResp) => {
Modal.confirm({
title: '确认取消',
content: `确定要取消培训计划"${record.planName}"吗?`,
onOk: async () => {
try {
await cancelTrainingPlan(record.planId)
message.success('取消成功')
loadData()
} catch (error) {
console.error('取消失败:', error)
message.error('取消失败')
}
},
})
}
//
const handleDelete = async (record: TrainingPlanResp) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除培训计划"${record.planName}"吗?`,
onOk: async () => {
try {
await deleteTrainingPlan(record.planId)
message.success('删除成功')
loadData()
} catch (error) {
console.error('删除失败:', error)
message.error('删除失败')
}
},
})
}
//
const handleDetail = (record: TrainingPlanResp) => {
// planId
router.push({ name: 'TrainingDetail', params: { id: record.planId } })
}
//
const handleModalSuccess = () => {
modalVisible.value = false
loadData()
}
//
watch(tableData, (_newData) => {
//
}, { deep: true })
//
onMounted(() => {
loadData()
})
</script>
<style lang="scss" scoped>
.training-plan-container {
padding: 16px;
background: #f5f5f5;
min-height: 100vh;
.search-card {
margin-bottom: 16px;
}
.table-card {
.card-title {
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
</style>

View File

@ -104,11 +104,11 @@ import { useWindowSize } from '@vueuse/core'
import {
type MessageQuery,
type MessageResp,
deleteMessage,
getUserMessage,
listMessage,
readAllMessage,
deleteMessage,
readMessage,
readAllMessage,
getUserMessage,
} from '@/apis'
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'