Compare commits
10 Commits
7f01db00e4
...
7b3194219b
| Author | SHA1 | Date |
|---|---|---|
|
|
7b3194219b | |
|
|
5c7728e3db | |
|
|
5500238be3 | |
|
|
913d6400e4 | |
|
|
7956379f71 | |
|
|
f111f4a8d5 | |
|
|
ea1eabafb0 | |
|
|
fa344f9c4e | |
|
|
29b51eca99 | |
|
|
890d2ae895 |
|
|
@ -25,4 +25,5 @@ vite.config.js
|
|||
vite.config.d.ts
|
||||
public/
|
||||
test-*.mjs
|
||||
test-server.cjs
|
||||
test-results/
|
||||
|
|
|
|||
|
|
@ -109,9 +109,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -161,9 +161,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
|
||||
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
|
||||
"version": "4.59.1",
|
||||
"resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.1.tgz",
|
||||
"integrity": "sha512-x6VG6U29+Ivlnajrg1IHdzXeAwSoEHBFVO+CtC9Brugx6de712CUJobRUxsIA0KYrQvCmzNrMPFTT1A4CCqNTg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -209,9 +209,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
||||
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz",
|
||||
"integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -822,6 +822,16 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.0.0.tgz",
|
||||
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0",
|
||||
"zrender": "6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
|
||||
|
|
@ -880,9 +890,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.20.2.tgz",
|
||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
|
|
@ -893,29 +903,29 @@
|
|||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
"@esbuild/aix-ppc64": "0.20.2",
|
||||
"@esbuild/android-arm": "0.20.2",
|
||||
"@esbuild/android-arm64": "0.20.2",
|
||||
"@esbuild/android-x64": "0.20.2",
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@esbuild/freebsd-arm64": "0.20.2",
|
||||
"@esbuild/freebsd-x64": "0.20.2",
|
||||
"@esbuild/linux-arm": "0.20.2",
|
||||
"@esbuild/linux-arm64": "0.20.2",
|
||||
"@esbuild/linux-ia32": "0.20.2",
|
||||
"@esbuild/linux-loong64": "0.20.2",
|
||||
"@esbuild/linux-mips64el": "0.20.2",
|
||||
"@esbuild/linux-ppc64": "0.20.2",
|
||||
"@esbuild/linux-riscv64": "0.20.2",
|
||||
"@esbuild/linux-s390x": "0.20.2",
|
||||
"@esbuild/linux-x64": "0.20.2",
|
||||
"@esbuild/netbsd-x64": "0.20.2",
|
||||
"@esbuild/openbsd-x64": "0.20.2",
|
||||
"@esbuild/sunos-x64": "0.20.2",
|
||||
"@esbuild/win32-arm64": "0.20.2",
|
||||
"@esbuild/win32-ia32": "0.20.2",
|
||||
"@esbuild/win32-x64": "0.20.2"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
|
|
@ -988,6 +998,21 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
|
||||
|
|
@ -1591,9 +1616,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.59.0.tgz",
|
||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||
"version": "4.59.1",
|
||||
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.59.1.tgz",
|
||||
"integrity": "sha512-iZKH8BeoCwTCBTZBZWQQMreekd4mdomwdjIQ40GC1oZm6o+8PnNMIxFOiCsGMWeS8iDJ7KZcl7KwmKk/0HOQpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -1607,31 +1632,31 @@
|
|||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.59.0",
|
||||
"@rollup/rollup-android-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.59.0",
|
||||
"@rollup/rollup-darwin-x64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.59.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.59.0",
|
||||
"@rollup/rollup-openbsd-x64": "4.59.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.59.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.59.0",
|
||||
"@rollup/rollup-android-arm-eabi": "4.59.1",
|
||||
"@rollup/rollup-android-arm64": "4.59.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.59.1",
|
||||
"@rollup/rollup-darwin-x64": "4.59.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.59.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.59.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.59.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.59.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.59.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.59.1",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.59.1",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.59.1",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.59.1",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.59.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.59.1",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.59.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.59.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.59.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.59.1",
|
||||
"@rollup/rollup-openbsd-x64": "4.59.1",
|
||||
"@rollup/rollup-openharmony-arm64": "4.59.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.59.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.59.1",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.59.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.59.1",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
|
|
@ -1784,6 +1809,12 @@
|
|||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-detect": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz",
|
||||
|
|
@ -1823,15 +1854,15 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.21",
|
||||
"resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz",
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"version": "5.2.14",
|
||||
"resolved": "https://registry.npmmirror.com/vite/-/vite-5.2.14.tgz",
|
||||
"integrity": "sha512-TFQLuwWLPms+NBNlh0D9LZQ+HXW471COABxw/9TEUBrjuHMo9BrYBPrN/SYAwIuVL+rLerycxiLT41t4f5MZpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
"esbuild": "^0.20.1",
|
||||
"postcss": "^8.4.38",
|
||||
"rollup": "^4.13.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
|
|
@ -1850,7 +1881,6 @@
|
|||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
|
|
@ -1868,9 +1898,6 @@
|
|||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
|
|
@ -1905,6 +1932,21 @@
|
|||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/vitest/-/vitest-1.6.1.tgz",
|
||||
|
|
@ -2126,6 +2168,15 @@
|
|||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-6.0.0.tgz",
|
||||
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@esbuild/darwin-x64",
|
||||
"version": "0.21.5",
|
||||
"version": "0.20.2",
|
||||
"description": "The macOS 64-bit binary for esbuild, a JavaScript bundler.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@rollup/rollup-darwin-x64",
|
||||
"version": "4.59.0",
|
||||
"version": "4.59.1",
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -20,7 +20,7 @@ const crypto__default = /*#__PURE__*/_interopDefaultCompat(crypto);
|
|||
const require$$0__default = /*#__PURE__*/_interopDefaultCompat(require$$0);
|
||||
const require$$1__default = /*#__PURE__*/_interopDefaultCompat(require$$1);
|
||||
|
||||
const version = "5.2.4";
|
||||
const version = "5.2.1";
|
||||
|
||||
function resolveCompiler(root) {
|
||||
const compiler = tryResolveCompiler(root) || tryResolveCompiler();
|
||||
|
|
@ -38,7 +38,7 @@ function tryResolveCompiler(root) {
|
|||
return tryRequire("vue/compiler-sfc", root);
|
||||
}
|
||||
}
|
||||
const _require = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
||||
const _require = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
||||
function tryRequire(id, from) {
|
||||
try {
|
||||
return from ? _require(_require.resolve(id, { paths: [from] })) : _require(id);
|
||||
|
|
@ -151,8 +151,7 @@ function getTempSrcDescriptor(filename, query) {
|
|||
start: { line: 0, column: 0 }
|
||||
}
|
||||
}
|
||||
],
|
||||
isTemp: true
|
||||
]
|
||||
};
|
||||
}
|
||||
function setSrcDescriptor(filename, entry, scoped) {
|
||||
|
|
@ -310,8 +309,6 @@ function resolveTemplateCompilerOptions(descriptor, options, ssr) {
|
|||
}
|
||||
return {
|
||||
...options.template,
|
||||
// @ts-expect-error TODO remove when 3.6 is out
|
||||
vapor: descriptor.vapor,
|
||||
id,
|
||||
ast: canReuseAST(options.compiler.version) ? descriptor.template?.ast : void 0,
|
||||
filename,
|
||||
|
|
@ -375,7 +372,8 @@ function resolveScript(descriptor, options, ssr, customElement) {
|
|||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const resolved = options.compiler.compileScript(descriptor, {
|
||||
let resolved = null;
|
||||
resolved = options.compiler.compileScript(descriptor, {
|
||||
...options.script,
|
||||
id: descriptor.id,
|
||||
isProd: options.isProduction,
|
||||
|
|
@ -441,7 +439,7 @@ function decodeInteger(reader, relative) {
|
|||
const shouldNegate = value & 1;
|
||||
value >>>= 1;
|
||||
if (shouldNegate) {
|
||||
value = -2147483648 | -value;
|
||||
value = -0x80000000 | -value;
|
||||
}
|
||||
return relative + value;
|
||||
}
|
||||
|
|
@ -1018,6 +1016,10 @@ function put(setarr, key) {
|
|||
}
|
||||
|
||||
const COLUMN = 0;
|
||||
const SOURCES_INDEX = 1;
|
||||
const SOURCE_LINE = 2;
|
||||
const SOURCE_COLUMN = 3;
|
||||
const NAMES_INDEX = 4;
|
||||
|
||||
const NO_NAME = -1;
|
||||
/**
|
||||
|
|
@ -1090,12 +1092,17 @@ function addSegmentInternal(skipable, map, genLine, genColumn, source, sourceLin
|
|||
const line = getLine(mappings, genLine);
|
||||
const index = getColumnIndex(line, genColumn);
|
||||
if (!source) {
|
||||
if (skipable && skipSourceless(line, index))
|
||||
return;
|
||||
return insert(line, index, [genColumn]);
|
||||
}
|
||||
const sourcesIndex = put(sources, source);
|
||||
const namesIndex = name ? put(names, name) : NO_NAME;
|
||||
if (sourcesIndex === sourcesContent.length)
|
||||
sourcesContent[sourcesIndex] = content !== null && content !== void 0 ? content : null;
|
||||
if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) {
|
||||
return;
|
||||
}
|
||||
return insert(line, index, name
|
||||
? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex]
|
||||
: [genColumn, sourcesIndex, sourceLine, sourceColumn]);
|
||||
|
|
@ -1135,6 +1142,32 @@ function putAll(setarr, array) {
|
|||
for (let i = 0; i < array.length; i++)
|
||||
put(setarr, array[i]);
|
||||
}
|
||||
function skipSourceless(line, index) {
|
||||
// The start of a line is already sourceless, so adding a sourceless segment to the beginning
|
||||
// doesn't generate any useful information.
|
||||
if (index === 0)
|
||||
return true;
|
||||
const prev = line[index - 1];
|
||||
// If the previous segment is also sourceless, then adding another sourceless segment doesn't
|
||||
// genrate any new information. Else, this segment will end the source/named segment and point to
|
||||
// a sourceless position, which is useful.
|
||||
return prev.length === 1;
|
||||
}
|
||||
function skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex) {
|
||||
// A source/named segment at the start of a line gives position at that genColumn
|
||||
if (index === 0)
|
||||
return false;
|
||||
const prev = line[index - 1];
|
||||
// If the previous segment is sourceless, then we're transitioning to a source.
|
||||
if (prev.length === 1)
|
||||
return false;
|
||||
// If the previous segment maps to the exact same source position, then this segment doesn't
|
||||
// provide any new position information.
|
||||
return (sourcesIndex === prev[SOURCES_INDEX] &&
|
||||
sourceLine === prev[SOURCE_LINE] &&
|
||||
sourceColumn === prev[SOURCE_COLUMN] &&
|
||||
namesIndex === (prev.length === 5 ? prev[NAMES_INDEX] : NO_NAME));
|
||||
}
|
||||
function addMappingInternal(skipable, map, mapping) {
|
||||
const { generated, source, original, name, content } = mapping;
|
||||
if (!source) {
|
||||
|
|
@ -1495,64 +1528,26 @@ function requireCommon () {
|
|||
createDebug.names = [];
|
||||
createDebug.skips = [];
|
||||
|
||||
const split = (typeof namespaces === 'string' ? namespaces : '')
|
||||
.trim()
|
||||
.replace(' ', ',')
|
||||
.split(',')
|
||||
.filter(Boolean);
|
||||
let i;
|
||||
const split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/);
|
||||
const len = split.length;
|
||||
|
||||
for (const ns of split) {
|
||||
if (ns[0] === '-') {
|
||||
createDebug.skips.push(ns.slice(1));
|
||||
for (i = 0; i < len; i++) {
|
||||
if (!split[i]) {
|
||||
// ignore empty strings
|
||||
continue;
|
||||
}
|
||||
|
||||
namespaces = split[i].replace(/\*/g, '.*?');
|
||||
|
||||
if (namespaces[0] === '-') {
|
||||
createDebug.skips.push(new RegExp('^' + namespaces.slice(1) + '$'));
|
||||
} else {
|
||||
createDebug.names.push(ns);
|
||||
createDebug.names.push(new RegExp('^' + namespaces + '$'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given string matches a namespace template, honoring
|
||||
* asterisks as wildcards.
|
||||
*
|
||||
* @param {String} search
|
||||
* @param {String} template
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function matchesTemplate(search, template) {
|
||||
let searchIndex = 0;
|
||||
let templateIndex = 0;
|
||||
let starIndex = -1;
|
||||
let matchIndex = 0;
|
||||
|
||||
while (searchIndex < search.length) {
|
||||
if (templateIndex < template.length && (template[templateIndex] === search[searchIndex] || template[templateIndex] === '*')) {
|
||||
// Match character or proceed with wildcard
|
||||
if (template[templateIndex] === '*') {
|
||||
starIndex = templateIndex;
|
||||
matchIndex = searchIndex;
|
||||
templateIndex++; // Skip the '*'
|
||||
} else {
|
||||
searchIndex++;
|
||||
templateIndex++;
|
||||
}
|
||||
} else if (starIndex !== -1) { // eslint-disable-line no-negated-condition
|
||||
// Backtrack to the last '*' and try to match more characters
|
||||
templateIndex = starIndex + 1;
|
||||
matchIndex++;
|
||||
searchIndex = matchIndex;
|
||||
} else {
|
||||
return false; // No match
|
||||
}
|
||||
}
|
||||
|
||||
// Handle trailing '*' in template
|
||||
while (templateIndex < template.length && template[templateIndex] === '*') {
|
||||
templateIndex++;
|
||||
}
|
||||
|
||||
return templateIndex === template.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable debug output.
|
||||
*
|
||||
|
|
@ -1561,8 +1556,8 @@ function requireCommon () {
|
|||
*/
|
||||
function disable() {
|
||||
const namespaces = [
|
||||
...createDebug.names,
|
||||
...createDebug.skips.map(namespace => '-' + namespace)
|
||||
...createDebug.names.map(toNamespace),
|
||||
...createDebug.skips.map(toNamespace).map(namespace => '-' + namespace)
|
||||
].join(',');
|
||||
createDebug.enable('');
|
||||
return namespaces;
|
||||
|
|
@ -1576,14 +1571,21 @@ function requireCommon () {
|
|||
* @api public
|
||||
*/
|
||||
function enabled(name) {
|
||||
for (const skip of createDebug.skips) {
|
||||
if (matchesTemplate(name, skip)) {
|
||||
if (name[name.length - 1] === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
let i;
|
||||
let len;
|
||||
|
||||
for (i = 0, len = createDebug.skips.length; i < len; i++) {
|
||||
if (createDebug.skips[i].test(name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const ns of createDebug.names) {
|
||||
if (matchesTemplate(name, ns)) {
|
||||
for (i = 0, len = createDebug.names.length; i < len; i++) {
|
||||
if (createDebug.names[i].test(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1591,6 +1593,19 @@ function requireCommon () {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert regexp to namespace
|
||||
*
|
||||
* @param {RegExp} regxep
|
||||
* @return {String} namespace
|
||||
* @api private
|
||||
*/
|
||||
function toNamespace(regexp) {
|
||||
return regexp.toString()
|
||||
.substring(2, regexp.toString().length - 2)
|
||||
.replace(/\.\*\?$/, '*');
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce `val`.
|
||||
*
|
||||
|
|
@ -1759,7 +1774,6 @@ function requireBrowser () {
|
|||
|
||||
// Is webkit? http://stackoverflow.com/a/16459606/376773
|
||||
// document is undefined in react-native: https://github.com/facebook/react-native/pull/1632
|
||||
// eslint-disable-next-line no-return-assign
|
||||
return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) ||
|
||||
// Is firebug? http://stackoverflow.com/a/398120/376773
|
||||
(typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) ||
|
||||
|
|
@ -2184,25 +2198,18 @@ function requireNode () {
|
|||
* treat as a browser.
|
||||
*/
|
||||
|
||||
var hasRequiredSrc;
|
||||
|
||||
function requireSrc () {
|
||||
if (hasRequiredSrc) return src.exports;
|
||||
hasRequiredSrc = 1;
|
||||
if (typeof process === 'undefined' || process.type === 'renderer' || process.browser === true || process.__nwjs) {
|
||||
src.exports = requireBrowser();
|
||||
} else {
|
||||
src.exports = requireNode();
|
||||
}
|
||||
return src.exports;
|
||||
if (typeof process === 'undefined' || process.type === 'renderer' || process.browser === true || process.__nwjs) {
|
||||
src.exports = requireBrowser();
|
||||
} else {
|
||||
src.exports = requireNode();
|
||||
}
|
||||
|
||||
var srcExports = requireSrc();
|
||||
var srcExports = src.exports;
|
||||
const _debug = /*@__PURE__*/getDefaultExportFromCjs(srcExports);
|
||||
|
||||
const debug = _debug("vite:hmr");
|
||||
const directRequestRE = /(?:\?|&)direct\b/;
|
||||
async function handleHotUpdate({ file, modules, read }, options, customElement, typeDepModules) {
|
||||
async function handleHotUpdate({ file, modules, read }, options, customElement) {
|
||||
const prevDescriptor = getDescriptor(file, options, false, true);
|
||||
if (!prevDescriptor) {
|
||||
return;
|
||||
|
|
@ -2303,15 +2310,17 @@ async function handleHotUpdate({ file, modules, read }, options, customElement,
|
|||
}
|
||||
debug(`[vue:update(${updateType.join("&")})] ${file}`);
|
||||
}
|
||||
return [...affectedModules, ...typeDepModules || []].filter(
|
||||
Boolean
|
||||
);
|
||||
return [...affectedModules].filter(Boolean);
|
||||
}
|
||||
function isEqualBlock(a, b) {
|
||||
if (!a && !b) return true;
|
||||
if (!a || !b) return false;
|
||||
if (a.src && b.src && a.src === b.src) return true;
|
||||
if (a.content !== b.content) return false;
|
||||
if (!a && !b)
|
||||
return true;
|
||||
if (!a || !b)
|
||||
return false;
|
||||
if (a.src && b.src && a.src === b.src)
|
||||
return true;
|
||||
if (a.content !== b.content)
|
||||
return false;
|
||||
const keysA = Object.keys(a.attrs);
|
||||
const keysB = Object.keys(b.attrs);
|
||||
if (keysA.length !== keysB.length) {
|
||||
|
|
@ -2550,20 +2559,11 @@ async function transformMain(code, filename, options, pluginContext, ssr, custom
|
|||
}
|
||||
let resolvedMap = void 0;
|
||||
if (options.sourceMap) {
|
||||
if (templateMap) {
|
||||
const from = scriptMap ?? {
|
||||
file: filename,
|
||||
sourceRoot: "",
|
||||
version: 3,
|
||||
sources: [],
|
||||
sourcesContent: [],
|
||||
names: [],
|
||||
mappings: ""
|
||||
};
|
||||
if (scriptMap && templateMap) {
|
||||
const gen = fromMap(
|
||||
// version property of result.map is declared as string
|
||||
// but actually it is `3`
|
||||
from
|
||||
scriptMap
|
||||
);
|
||||
const tracer = new TraceMap(
|
||||
// same above
|
||||
|
|
@ -2571,7 +2571,8 @@ async function transformMain(code, filename, options, pluginContext, ssr, custom
|
|||
);
|
||||
const offset = (scriptCode.match(/\r?\n/g)?.length ?? 0) + 1;
|
||||
eachMapping(tracer, (m) => {
|
||||
if (m.source == null) return;
|
||||
if (m.source == null)
|
||||
return;
|
||||
addMapping(gen, {
|
||||
source: m.source,
|
||||
original: { line: m.originalLine, column: m.originalColumn },
|
||||
|
|
@ -2584,7 +2585,7 @@ async function transformMain(code, filename, options, pluginContext, ssr, custom
|
|||
resolvedMap = toEncodedMap(gen);
|
||||
resolvedMap.sourcesContent = templateMap.sourcesContent;
|
||||
} else {
|
||||
resolvedMap = scriptMap;
|
||||
resolvedMap = scriptMap ?? templateMap;
|
||||
}
|
||||
}
|
||||
if (!attachedProps.length) {
|
||||
|
|
@ -2598,41 +2599,21 @@ async function transformMain(code, filename, options, pluginContext, ssr, custom
|
|||
let resolvedCode = output.join("\n");
|
||||
const lang = descriptor.scriptSetup?.lang || descriptor.script?.lang;
|
||||
if (lang && /tsx?$/.test(lang) && !descriptor.script?.src) {
|
||||
const { transformWithOxc } = await import('vite');
|
||||
if (transformWithOxc) {
|
||||
const { code: code2, map } = await transformWithOxc(
|
||||
resolvedCode,
|
||||
filename,
|
||||
{
|
||||
// #430 support decorators in .vue file
|
||||
// target can be overridden by oxc config target
|
||||
// @ts-ignore Rolldown-specific
|
||||
...options.devServer?.config.oxc,
|
||||
lang: "ts",
|
||||
sourcemap: options.sourceMap
|
||||
},
|
||||
resolvedMap
|
||||
);
|
||||
resolvedCode = code2;
|
||||
resolvedMap = resolvedMap ? map : resolvedMap;
|
||||
} else {
|
||||
const { code: code2, map } = await vite.transformWithEsbuild(
|
||||
resolvedCode,
|
||||
filename,
|
||||
{
|
||||
target: "esnext",
|
||||
charset: "utf8",
|
||||
// #430 support decorators in .vue file
|
||||
// target can be overridden by esbuild config target
|
||||
...options.devServer?.config.esbuild,
|
||||
loader: "ts",
|
||||
sourcemap: options.sourceMap
|
||||
},
|
||||
resolvedMap
|
||||
);
|
||||
resolvedCode = code2;
|
||||
resolvedMap = resolvedMap ? map : resolvedMap;
|
||||
}
|
||||
const { code: code2, map } = await vite.transformWithEsbuild(
|
||||
resolvedCode,
|
||||
filename,
|
||||
{
|
||||
target: "esnext",
|
||||
// #430 support decorators in .vue file
|
||||
// target can be overridden by esbuild config target
|
||||
...options.devServer?.config.esbuild,
|
||||
loader: "ts",
|
||||
sourcemap: options.sourceMap
|
||||
},
|
||||
resolvedMap
|
||||
);
|
||||
resolvedCode = code2;
|
||||
resolvedMap = resolvedMap ? map : resolvedMap;
|
||||
}
|
||||
return {
|
||||
code: resolvedCode,
|
||||
|
|
@ -2681,8 +2662,7 @@ async function genTemplateCode(descriptor, options, pluginContext, ssr, customEl
|
|||
}
|
||||
}
|
||||
async function genScriptCode(descriptor, options, pluginContext, ssr, customElement) {
|
||||
const vaporFlag = descriptor.vapor ? "__vapor: true" : "";
|
||||
let scriptCode = `const ${scriptIdentifier} = { ${vaporFlag} }`;
|
||||
let scriptCode = `const ${scriptIdentifier} = {}`;
|
||||
let map;
|
||||
const script = resolveScript(descriptor, options, ssr, customElement);
|
||||
if (script) {
|
||||
|
|
@ -2751,7 +2731,7 @@ async function genStyleCode(descriptor, pluginContext, customElement, attachedPr
|
|||
style.module
|
||||
);
|
||||
stylesCode += importCode;
|
||||
Object.assign(cssModulesMap ||= {}, nameMap);
|
||||
Object.assign(cssModulesMap || (cssModulesMap = {}), nameMap);
|
||||
} else {
|
||||
if (customElement) {
|
||||
stylesCode += `
|
||||
|
|
@ -2880,12 +2860,7 @@ async function transformStyle(code, descriptor, index, options, pluginContext, f
|
|||
) : { mappings: "" };
|
||||
return {
|
||||
code: result.code,
|
||||
map,
|
||||
meta: block.scoped && !descriptor.isTemp ? {
|
||||
vite: {
|
||||
cssScopeTo: [descriptor.filename, "default"]
|
||||
}
|
||||
} : void 0
|
||||
map
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -2909,7 +2884,6 @@ function vuePlugin(rawOptions = {}) {
|
|||
const customElement = options.value.features?.customElement || options.value.customElement;
|
||||
return typeof customElement === "boolean" ? () => customElement : vite.createFilter(customElement);
|
||||
});
|
||||
let transformCachedModule = false;
|
||||
return {
|
||||
name: "vite:vue",
|
||||
api: {
|
||||
|
|
@ -2930,42 +2904,26 @@ function vuePlugin(rawOptions = {}) {
|
|||
if (options.value.compiler.invalidateTypeCache) {
|
||||
options.value.compiler.invalidateTypeCache(ctx.file);
|
||||
}
|
||||
let typeDepModules;
|
||||
const matchesFilter = filter.value(ctx.file);
|
||||
if (typeDepToSFCMap.has(ctx.file)) {
|
||||
typeDepModules = handleTypeDepChange(
|
||||
typeDepToSFCMap.get(ctx.file),
|
||||
ctx
|
||||
);
|
||||
if (!matchesFilter) return typeDepModules;
|
||||
return handleTypeDepChange(typeDepToSFCMap.get(ctx.file), ctx);
|
||||
}
|
||||
if (matchesFilter) {
|
||||
if (filter.value(ctx.file)) {
|
||||
return handleHotUpdate(
|
||||
ctx,
|
||||
options.value,
|
||||
customElementFilter.value(ctx.file),
|
||||
typeDepModules
|
||||
customElementFilter.value(ctx.file)
|
||||
);
|
||||
}
|
||||
},
|
||||
config(config) {
|
||||
const parseDefine = (v) => {
|
||||
try {
|
||||
return typeof v === "string" ? JSON.parse(v) : v;
|
||||
} catch (err) {
|
||||
return v;
|
||||
}
|
||||
};
|
||||
return {
|
||||
resolve: {
|
||||
dedupe: config.build?.ssr ? [] : ["vue"]
|
||||
},
|
||||
define: {
|
||||
__VUE_OPTIONS_API__: options.value.features?.optionsAPI ?? parseDefine(config.define?.__VUE_OPTIONS_API__) ?? true,
|
||||
__VUE_PROD_DEVTOOLS__: (options.value.features?.prodDevtools || parseDefine(config.define?.__VUE_PROD_DEVTOOLS__)) ?? false,
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: (options.value.features?.prodHydrationMismatchDetails || parseDefine(
|
||||
config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__
|
||||
)) ?? false
|
||||
__VUE_OPTIONS_API__: !!((options.value.features?.optionsAPI ?? true) || config.define?.__VUE_OPTIONS_API__),
|
||||
__VUE_PROD_DEVTOOLS__: !!(options.value.features?.prodDevtools || config.define?.__VUE_PROD_DEVTOOLS__),
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: !!(options.value.features?.prodHydrationMismatchDetails || config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__)
|
||||
},
|
||||
ssr: {
|
||||
// @ts-ignore -- config.legacy.buildSsrCjsExternalHeuristics will be removed in Vite 5
|
||||
|
|
@ -2982,23 +2940,6 @@ function vuePlugin(rawOptions = {}) {
|
|||
isProduction: config.isProduction,
|
||||
devToolsEnabled: !!(options.value.features?.prodDevtools || config.define.__VUE_PROD_DEVTOOLS__ || !config.isProduction)
|
||||
};
|
||||
const _warn = config.logger.warn;
|
||||
config.logger.warn = (...args) => {
|
||||
const msg = args[0];
|
||||
if (msg.match(
|
||||
/\[lightningcss\] '(deep|slotted|global)' is not recognized as a valid pseudo-/
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
_warn(...args);
|
||||
};
|
||||
transformCachedModule = config.command === "build" && options.value.sourceMap && config.build.watch != null;
|
||||
},
|
||||
shouldTransformCachedModule({ id }) {
|
||||
if (transformCachedModule && parseVueRequest(id).query.vue) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
configureServer(server) {
|
||||
options.value.devServer = server;
|
||||
|
|
@ -3020,10 +2961,10 @@ function vuePlugin(rawOptions = {}) {
|
|||
}
|
||||
},
|
||||
load(id, opt) {
|
||||
const ssr = opt?.ssr === true;
|
||||
if (id === EXPORT_HELPER_ID) {
|
||||
return helperCode;
|
||||
}
|
||||
const ssr = opt?.ssr === true;
|
||||
const { filename, query } = parseVueRequest(id);
|
||||
if (query.vue) {
|
||||
if (query.src) {
|
||||
|
|
@ -3073,9 +3014,6 @@ function vuePlugin(rawOptions = {}) {
|
|||
);
|
||||
} else {
|
||||
const descriptor = query.src ? getSrcDescriptor(filename, query) || getTempSrcDescriptor(filename, query) : getDescriptor(filename, options.value);
|
||||
if (query.src) {
|
||||
this.addWatchFile(filename);
|
||||
}
|
||||
if (query.type === "template") {
|
||||
return transformTemplateAsModule(
|
||||
code,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Plugin, ViteDevServer } from 'vite';
|
||||
import { ViteDevServer, Plugin } from 'vite';
|
||||
import * as _compiler from 'vue/compiler-sfc';
|
||||
import { SFCScriptCompileOptions, SFCTemplateCompileOptions, SFCStyleCompileOptions } from 'vue/compiler-sfc';
|
||||
|
||||
|
|
@ -108,6 +108,4 @@ interface Api {
|
|||
}
|
||||
declare function vuePlugin(rawOptions?: Options): Plugin<Api>;
|
||||
|
||||
// @ts-ignore
|
||||
export = vuePlugin;
|
||||
export { type Api, type Options, type ResolvedOptions, type VueQuery, parseVueRequest };
|
||||
export { type Api, type Options, type ResolvedOptions, type VueQuery, vuePlugin as default, parseVueRequest };
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Plugin, ViteDevServer } from 'vite';
|
||||
import { ViteDevServer, Plugin } from 'vite';
|
||||
import * as _compiler from 'vue/compiler-sfc';
|
||||
import { SFCScriptCompileOptions, SFCTemplateCompileOptions, SFCStyleCompileOptions } from 'vue/compiler-sfc';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Plugin, ViteDevServer } from 'vite';
|
||||
import { ViteDevServer, Plugin } from 'vite';
|
||||
import * as _compiler from 'vue/compiler-sfc';
|
||||
import { SFCScriptCompileOptions, SFCTemplateCompileOptions, SFCStyleCompileOptions } from 'vue/compiler-sfc';
|
||||
|
||||
|
|
@ -108,6 +108,4 @@ interface Api {
|
|||
}
|
||||
declare function vuePlugin(rawOptions?: Options): Plugin<Api>;
|
||||
|
||||
// @ts-ignore
|
||||
export = vuePlugin;
|
||||
export { type Api, type Options, type ResolvedOptions, type VueQuery, parseVueRequest };
|
||||
export { type Api, type Options, type ResolvedOptions, type VueQuery, vuePlugin as default, parseVueRequest };
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import crypto from 'node:crypto';
|
|||
import require$$0 from 'tty';
|
||||
import require$$1 from 'util';
|
||||
|
||||
const version = "5.2.4";
|
||||
const version = "5.2.1";
|
||||
|
||||
function resolveCompiler(root) {
|
||||
const compiler = tryResolveCompiler(root) || tryResolveCompiler();
|
||||
|
|
@ -138,8 +138,7 @@ function getTempSrcDescriptor(filename, query) {
|
|||
start: { line: 0, column: 0 }
|
||||
}
|
||||
}
|
||||
],
|
||||
isTemp: true
|
||||
]
|
||||
};
|
||||
}
|
||||
function setSrcDescriptor(filename, entry, scoped) {
|
||||
|
|
@ -297,8 +296,6 @@ function resolveTemplateCompilerOptions(descriptor, options, ssr) {
|
|||
}
|
||||
return {
|
||||
...options.template,
|
||||
// @ts-expect-error TODO remove when 3.6 is out
|
||||
vapor: descriptor.vapor,
|
||||
id,
|
||||
ast: canReuseAST(options.compiler.version) ? descriptor.template?.ast : void 0,
|
||||
filename,
|
||||
|
|
@ -362,7 +359,8 @@ function resolveScript(descriptor, options, ssr, customElement) {
|
|||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const resolved = options.compiler.compileScript(descriptor, {
|
||||
let resolved = null;
|
||||
resolved = options.compiler.compileScript(descriptor, {
|
||||
...options.script,
|
||||
id: descriptor.id,
|
||||
isProd: options.isProduction,
|
||||
|
|
@ -428,7 +426,7 @@ function decodeInteger(reader, relative) {
|
|||
const shouldNegate = value & 1;
|
||||
value >>>= 1;
|
||||
if (shouldNegate) {
|
||||
value = -2147483648 | -value;
|
||||
value = -0x80000000 | -value;
|
||||
}
|
||||
return relative + value;
|
||||
}
|
||||
|
|
@ -1005,6 +1003,10 @@ function put(setarr, key) {
|
|||
}
|
||||
|
||||
const COLUMN = 0;
|
||||
const SOURCES_INDEX = 1;
|
||||
const SOURCE_LINE = 2;
|
||||
const SOURCE_COLUMN = 3;
|
||||
const NAMES_INDEX = 4;
|
||||
|
||||
const NO_NAME = -1;
|
||||
/**
|
||||
|
|
@ -1077,12 +1079,17 @@ function addSegmentInternal(skipable, map, genLine, genColumn, source, sourceLin
|
|||
const line = getLine(mappings, genLine);
|
||||
const index = getColumnIndex(line, genColumn);
|
||||
if (!source) {
|
||||
if (skipable && skipSourceless(line, index))
|
||||
return;
|
||||
return insert(line, index, [genColumn]);
|
||||
}
|
||||
const sourcesIndex = put(sources, source);
|
||||
const namesIndex = name ? put(names, name) : NO_NAME;
|
||||
if (sourcesIndex === sourcesContent.length)
|
||||
sourcesContent[sourcesIndex] = content !== null && content !== void 0 ? content : null;
|
||||
if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) {
|
||||
return;
|
||||
}
|
||||
return insert(line, index, name
|
||||
? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex]
|
||||
: [genColumn, sourcesIndex, sourceLine, sourceColumn]);
|
||||
|
|
@ -1122,6 +1129,32 @@ function putAll(setarr, array) {
|
|||
for (let i = 0; i < array.length; i++)
|
||||
put(setarr, array[i]);
|
||||
}
|
||||
function skipSourceless(line, index) {
|
||||
// The start of a line is already sourceless, so adding a sourceless segment to the beginning
|
||||
// doesn't generate any useful information.
|
||||
if (index === 0)
|
||||
return true;
|
||||
const prev = line[index - 1];
|
||||
// If the previous segment is also sourceless, then adding another sourceless segment doesn't
|
||||
// genrate any new information. Else, this segment will end the source/named segment and point to
|
||||
// a sourceless position, which is useful.
|
||||
return prev.length === 1;
|
||||
}
|
||||
function skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex) {
|
||||
// A source/named segment at the start of a line gives position at that genColumn
|
||||
if (index === 0)
|
||||
return false;
|
||||
const prev = line[index - 1];
|
||||
// If the previous segment is sourceless, then we're transitioning to a source.
|
||||
if (prev.length === 1)
|
||||
return false;
|
||||
// If the previous segment maps to the exact same source position, then this segment doesn't
|
||||
// provide any new position information.
|
||||
return (sourcesIndex === prev[SOURCES_INDEX] &&
|
||||
sourceLine === prev[SOURCE_LINE] &&
|
||||
sourceColumn === prev[SOURCE_COLUMN] &&
|
||||
namesIndex === (prev.length === 5 ? prev[NAMES_INDEX] : NO_NAME));
|
||||
}
|
||||
function addMappingInternal(skipable, map, mapping) {
|
||||
const { generated, source, original, name, content } = mapping;
|
||||
if (!source) {
|
||||
|
|
@ -1482,64 +1515,26 @@ function requireCommon () {
|
|||
createDebug.names = [];
|
||||
createDebug.skips = [];
|
||||
|
||||
const split = (typeof namespaces === 'string' ? namespaces : '')
|
||||
.trim()
|
||||
.replace(' ', ',')
|
||||
.split(',')
|
||||
.filter(Boolean);
|
||||
let i;
|
||||
const split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/);
|
||||
const len = split.length;
|
||||
|
||||
for (const ns of split) {
|
||||
if (ns[0] === '-') {
|
||||
createDebug.skips.push(ns.slice(1));
|
||||
for (i = 0; i < len; i++) {
|
||||
if (!split[i]) {
|
||||
// ignore empty strings
|
||||
continue;
|
||||
}
|
||||
|
||||
namespaces = split[i].replace(/\*/g, '.*?');
|
||||
|
||||
if (namespaces[0] === '-') {
|
||||
createDebug.skips.push(new RegExp('^' + namespaces.slice(1) + '$'));
|
||||
} else {
|
||||
createDebug.names.push(ns);
|
||||
createDebug.names.push(new RegExp('^' + namespaces + '$'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given string matches a namespace template, honoring
|
||||
* asterisks as wildcards.
|
||||
*
|
||||
* @param {String} search
|
||||
* @param {String} template
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function matchesTemplate(search, template) {
|
||||
let searchIndex = 0;
|
||||
let templateIndex = 0;
|
||||
let starIndex = -1;
|
||||
let matchIndex = 0;
|
||||
|
||||
while (searchIndex < search.length) {
|
||||
if (templateIndex < template.length && (template[templateIndex] === search[searchIndex] || template[templateIndex] === '*')) {
|
||||
// Match character or proceed with wildcard
|
||||
if (template[templateIndex] === '*') {
|
||||
starIndex = templateIndex;
|
||||
matchIndex = searchIndex;
|
||||
templateIndex++; // Skip the '*'
|
||||
} else {
|
||||
searchIndex++;
|
||||
templateIndex++;
|
||||
}
|
||||
} else if (starIndex !== -1) { // eslint-disable-line no-negated-condition
|
||||
// Backtrack to the last '*' and try to match more characters
|
||||
templateIndex = starIndex + 1;
|
||||
matchIndex++;
|
||||
searchIndex = matchIndex;
|
||||
} else {
|
||||
return false; // No match
|
||||
}
|
||||
}
|
||||
|
||||
// Handle trailing '*' in template
|
||||
while (templateIndex < template.length && template[templateIndex] === '*') {
|
||||
templateIndex++;
|
||||
}
|
||||
|
||||
return templateIndex === template.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable debug output.
|
||||
*
|
||||
|
|
@ -1548,8 +1543,8 @@ function requireCommon () {
|
|||
*/
|
||||
function disable() {
|
||||
const namespaces = [
|
||||
...createDebug.names,
|
||||
...createDebug.skips.map(namespace => '-' + namespace)
|
||||
...createDebug.names.map(toNamespace),
|
||||
...createDebug.skips.map(toNamespace).map(namespace => '-' + namespace)
|
||||
].join(',');
|
||||
createDebug.enable('');
|
||||
return namespaces;
|
||||
|
|
@ -1563,14 +1558,21 @@ function requireCommon () {
|
|||
* @api public
|
||||
*/
|
||||
function enabled(name) {
|
||||
for (const skip of createDebug.skips) {
|
||||
if (matchesTemplate(name, skip)) {
|
||||
if (name[name.length - 1] === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
let i;
|
||||
let len;
|
||||
|
||||
for (i = 0, len = createDebug.skips.length; i < len; i++) {
|
||||
if (createDebug.skips[i].test(name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const ns of createDebug.names) {
|
||||
if (matchesTemplate(name, ns)) {
|
||||
for (i = 0, len = createDebug.names.length; i < len; i++) {
|
||||
if (createDebug.names[i].test(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1578,6 +1580,19 @@ function requireCommon () {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert regexp to namespace
|
||||
*
|
||||
* @param {RegExp} regxep
|
||||
* @return {String} namespace
|
||||
* @api private
|
||||
*/
|
||||
function toNamespace(regexp) {
|
||||
return regexp.toString()
|
||||
.substring(2, regexp.toString().length - 2)
|
||||
.replace(/\.\*\?$/, '*');
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce `val`.
|
||||
*
|
||||
|
|
@ -1746,7 +1761,6 @@ function requireBrowser () {
|
|||
|
||||
// Is webkit? http://stackoverflow.com/a/16459606/376773
|
||||
// document is undefined in react-native: https://github.com/facebook/react-native/pull/1632
|
||||
// eslint-disable-next-line no-return-assign
|
||||
return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) ||
|
||||
// Is firebug? http://stackoverflow.com/a/398120/376773
|
||||
(typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) ||
|
||||
|
|
@ -2171,25 +2185,18 @@ function requireNode () {
|
|||
* treat as a browser.
|
||||
*/
|
||||
|
||||
var hasRequiredSrc;
|
||||
|
||||
function requireSrc () {
|
||||
if (hasRequiredSrc) return src.exports;
|
||||
hasRequiredSrc = 1;
|
||||
if (typeof process === 'undefined' || process.type === 'renderer' || process.browser === true || process.__nwjs) {
|
||||
src.exports = requireBrowser();
|
||||
} else {
|
||||
src.exports = requireNode();
|
||||
}
|
||||
return src.exports;
|
||||
if (typeof process === 'undefined' || process.type === 'renderer' || process.browser === true || process.__nwjs) {
|
||||
src.exports = requireBrowser();
|
||||
} else {
|
||||
src.exports = requireNode();
|
||||
}
|
||||
|
||||
var srcExports = requireSrc();
|
||||
var srcExports = src.exports;
|
||||
const _debug = /*@__PURE__*/getDefaultExportFromCjs(srcExports);
|
||||
|
||||
const debug = _debug("vite:hmr");
|
||||
const directRequestRE = /(?:\?|&)direct\b/;
|
||||
async function handleHotUpdate({ file, modules, read }, options, customElement, typeDepModules) {
|
||||
async function handleHotUpdate({ file, modules, read }, options, customElement) {
|
||||
const prevDescriptor = getDescriptor(file, options, false, true);
|
||||
if (!prevDescriptor) {
|
||||
return;
|
||||
|
|
@ -2290,15 +2297,17 @@ async function handleHotUpdate({ file, modules, read }, options, customElement,
|
|||
}
|
||||
debug(`[vue:update(${updateType.join("&")})] ${file}`);
|
||||
}
|
||||
return [...affectedModules, ...typeDepModules || []].filter(
|
||||
Boolean
|
||||
);
|
||||
return [...affectedModules].filter(Boolean);
|
||||
}
|
||||
function isEqualBlock(a, b) {
|
||||
if (!a && !b) return true;
|
||||
if (!a || !b) return false;
|
||||
if (a.src && b.src && a.src === b.src) return true;
|
||||
if (a.content !== b.content) return false;
|
||||
if (!a && !b)
|
||||
return true;
|
||||
if (!a || !b)
|
||||
return false;
|
||||
if (a.src && b.src && a.src === b.src)
|
||||
return true;
|
||||
if (a.content !== b.content)
|
||||
return false;
|
||||
const keysA = Object.keys(a.attrs);
|
||||
const keysB = Object.keys(b.attrs);
|
||||
if (keysA.length !== keysB.length) {
|
||||
|
|
@ -2537,20 +2546,11 @@ async function transformMain(code, filename, options, pluginContext, ssr, custom
|
|||
}
|
||||
let resolvedMap = void 0;
|
||||
if (options.sourceMap) {
|
||||
if (templateMap) {
|
||||
const from = scriptMap ?? {
|
||||
file: filename,
|
||||
sourceRoot: "",
|
||||
version: 3,
|
||||
sources: [],
|
||||
sourcesContent: [],
|
||||
names: [],
|
||||
mappings: ""
|
||||
};
|
||||
if (scriptMap && templateMap) {
|
||||
const gen = fromMap(
|
||||
// version property of result.map is declared as string
|
||||
// but actually it is `3`
|
||||
from
|
||||
scriptMap
|
||||
);
|
||||
const tracer = new TraceMap(
|
||||
// same above
|
||||
|
|
@ -2558,7 +2558,8 @@ async function transformMain(code, filename, options, pluginContext, ssr, custom
|
|||
);
|
||||
const offset = (scriptCode.match(/\r?\n/g)?.length ?? 0) + 1;
|
||||
eachMapping(tracer, (m) => {
|
||||
if (m.source == null) return;
|
||||
if (m.source == null)
|
||||
return;
|
||||
addMapping(gen, {
|
||||
source: m.source,
|
||||
original: { line: m.originalLine, column: m.originalColumn },
|
||||
|
|
@ -2571,7 +2572,7 @@ async function transformMain(code, filename, options, pluginContext, ssr, custom
|
|||
resolvedMap = toEncodedMap(gen);
|
||||
resolvedMap.sourcesContent = templateMap.sourcesContent;
|
||||
} else {
|
||||
resolvedMap = scriptMap;
|
||||
resolvedMap = scriptMap ?? templateMap;
|
||||
}
|
||||
}
|
||||
if (!attachedProps.length) {
|
||||
|
|
@ -2585,41 +2586,21 @@ async function transformMain(code, filename, options, pluginContext, ssr, custom
|
|||
let resolvedCode = output.join("\n");
|
||||
const lang = descriptor.scriptSetup?.lang || descriptor.script?.lang;
|
||||
if (lang && /tsx?$/.test(lang) && !descriptor.script?.src) {
|
||||
const { transformWithOxc } = await import('vite');
|
||||
if (transformWithOxc) {
|
||||
const { code: code2, map } = await transformWithOxc(
|
||||
resolvedCode,
|
||||
filename,
|
||||
{
|
||||
// #430 support decorators in .vue file
|
||||
// target can be overridden by oxc config target
|
||||
// @ts-ignore Rolldown-specific
|
||||
...options.devServer?.config.oxc,
|
||||
lang: "ts",
|
||||
sourcemap: options.sourceMap
|
||||
},
|
||||
resolvedMap
|
||||
);
|
||||
resolvedCode = code2;
|
||||
resolvedMap = resolvedMap ? map : resolvedMap;
|
||||
} else {
|
||||
const { code: code2, map } = await transformWithEsbuild(
|
||||
resolvedCode,
|
||||
filename,
|
||||
{
|
||||
target: "esnext",
|
||||
charset: "utf8",
|
||||
// #430 support decorators in .vue file
|
||||
// target can be overridden by esbuild config target
|
||||
...options.devServer?.config.esbuild,
|
||||
loader: "ts",
|
||||
sourcemap: options.sourceMap
|
||||
},
|
||||
resolvedMap
|
||||
);
|
||||
resolvedCode = code2;
|
||||
resolvedMap = resolvedMap ? map : resolvedMap;
|
||||
}
|
||||
const { code: code2, map } = await transformWithEsbuild(
|
||||
resolvedCode,
|
||||
filename,
|
||||
{
|
||||
target: "esnext",
|
||||
// #430 support decorators in .vue file
|
||||
// target can be overridden by esbuild config target
|
||||
...options.devServer?.config.esbuild,
|
||||
loader: "ts",
|
||||
sourcemap: options.sourceMap
|
||||
},
|
||||
resolvedMap
|
||||
);
|
||||
resolvedCode = code2;
|
||||
resolvedMap = resolvedMap ? map : resolvedMap;
|
||||
}
|
||||
return {
|
||||
code: resolvedCode,
|
||||
|
|
@ -2668,8 +2649,7 @@ async function genTemplateCode(descriptor, options, pluginContext, ssr, customEl
|
|||
}
|
||||
}
|
||||
async function genScriptCode(descriptor, options, pluginContext, ssr, customElement) {
|
||||
const vaporFlag = descriptor.vapor ? "__vapor: true" : "";
|
||||
let scriptCode = `const ${scriptIdentifier} = { ${vaporFlag} }`;
|
||||
let scriptCode = `const ${scriptIdentifier} = {}`;
|
||||
let map;
|
||||
const script = resolveScript(descriptor, options, ssr, customElement);
|
||||
if (script) {
|
||||
|
|
@ -2738,7 +2718,7 @@ async function genStyleCode(descriptor, pluginContext, customElement, attachedPr
|
|||
style.module
|
||||
);
|
||||
stylesCode += importCode;
|
||||
Object.assign(cssModulesMap ||= {}, nameMap);
|
||||
Object.assign(cssModulesMap || (cssModulesMap = {}), nameMap);
|
||||
} else {
|
||||
if (customElement) {
|
||||
stylesCode += `
|
||||
|
|
@ -2867,12 +2847,7 @@ async function transformStyle(code, descriptor, index, options, pluginContext, f
|
|||
) : { mappings: "" };
|
||||
return {
|
||||
code: result.code,
|
||||
map,
|
||||
meta: block.scoped && !descriptor.isTemp ? {
|
||||
vite: {
|
||||
cssScopeTo: [descriptor.filename, "default"]
|
||||
}
|
||||
} : void 0
|
||||
map
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -2896,7 +2871,6 @@ function vuePlugin(rawOptions = {}) {
|
|||
const customElement = options.value.features?.customElement || options.value.customElement;
|
||||
return typeof customElement === "boolean" ? () => customElement : createFilter(customElement);
|
||||
});
|
||||
let transformCachedModule = false;
|
||||
return {
|
||||
name: "vite:vue",
|
||||
api: {
|
||||
|
|
@ -2917,42 +2891,26 @@ function vuePlugin(rawOptions = {}) {
|
|||
if (options.value.compiler.invalidateTypeCache) {
|
||||
options.value.compiler.invalidateTypeCache(ctx.file);
|
||||
}
|
||||
let typeDepModules;
|
||||
const matchesFilter = filter.value(ctx.file);
|
||||
if (typeDepToSFCMap.has(ctx.file)) {
|
||||
typeDepModules = handleTypeDepChange(
|
||||
typeDepToSFCMap.get(ctx.file),
|
||||
ctx
|
||||
);
|
||||
if (!matchesFilter) return typeDepModules;
|
||||
return handleTypeDepChange(typeDepToSFCMap.get(ctx.file), ctx);
|
||||
}
|
||||
if (matchesFilter) {
|
||||
if (filter.value(ctx.file)) {
|
||||
return handleHotUpdate(
|
||||
ctx,
|
||||
options.value,
|
||||
customElementFilter.value(ctx.file),
|
||||
typeDepModules
|
||||
customElementFilter.value(ctx.file)
|
||||
);
|
||||
}
|
||||
},
|
||||
config(config) {
|
||||
const parseDefine = (v) => {
|
||||
try {
|
||||
return typeof v === "string" ? JSON.parse(v) : v;
|
||||
} catch (err) {
|
||||
return v;
|
||||
}
|
||||
};
|
||||
return {
|
||||
resolve: {
|
||||
dedupe: config.build?.ssr ? [] : ["vue"]
|
||||
},
|
||||
define: {
|
||||
__VUE_OPTIONS_API__: options.value.features?.optionsAPI ?? parseDefine(config.define?.__VUE_OPTIONS_API__) ?? true,
|
||||
__VUE_PROD_DEVTOOLS__: (options.value.features?.prodDevtools || parseDefine(config.define?.__VUE_PROD_DEVTOOLS__)) ?? false,
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: (options.value.features?.prodHydrationMismatchDetails || parseDefine(
|
||||
config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__
|
||||
)) ?? false
|
||||
__VUE_OPTIONS_API__: !!((options.value.features?.optionsAPI ?? true) || config.define?.__VUE_OPTIONS_API__),
|
||||
__VUE_PROD_DEVTOOLS__: !!(options.value.features?.prodDevtools || config.define?.__VUE_PROD_DEVTOOLS__),
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: !!(options.value.features?.prodHydrationMismatchDetails || config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__)
|
||||
},
|
||||
ssr: {
|
||||
// @ts-ignore -- config.legacy.buildSsrCjsExternalHeuristics will be removed in Vite 5
|
||||
|
|
@ -2969,23 +2927,6 @@ function vuePlugin(rawOptions = {}) {
|
|||
isProduction: config.isProduction,
|
||||
devToolsEnabled: !!(options.value.features?.prodDevtools || config.define.__VUE_PROD_DEVTOOLS__ || !config.isProduction)
|
||||
};
|
||||
const _warn = config.logger.warn;
|
||||
config.logger.warn = (...args) => {
|
||||
const msg = args[0];
|
||||
if (msg.match(
|
||||
/\[lightningcss\] '(deep|slotted|global)' is not recognized as a valid pseudo-/
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
_warn(...args);
|
||||
};
|
||||
transformCachedModule = config.command === "build" && options.value.sourceMap && config.build.watch != null;
|
||||
},
|
||||
shouldTransformCachedModule({ id }) {
|
||||
if (transformCachedModule && parseVueRequest(id).query.vue) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
configureServer(server) {
|
||||
options.value.devServer = server;
|
||||
|
|
@ -3007,10 +2948,10 @@ function vuePlugin(rawOptions = {}) {
|
|||
}
|
||||
},
|
||||
load(id, opt) {
|
||||
const ssr = opt?.ssr === true;
|
||||
if (id === EXPORT_HELPER_ID) {
|
||||
return helperCode;
|
||||
}
|
||||
const ssr = opt?.ssr === true;
|
||||
const { filename, query } = parseVueRequest(id);
|
||||
if (query.vue) {
|
||||
if (query.src) {
|
||||
|
|
@ -3060,9 +3001,6 @@ function vuePlugin(rawOptions = {}) {
|
|||
);
|
||||
} else {
|
||||
const descriptor = query.src ? getSrcDescriptor(filename, query) || getTempSrcDescriptor(filename, query) : getDescriptor(filename, options.value);
|
||||
if (query.src) {
|
||||
this.addWatchFile(filename);
|
||||
}
|
||||
if (query.type === "template") {
|
||||
return transformTemplateAsModule(
|
||||
code,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@vitejs/plugin-vue",
|
||||
"version": "5.2.4",
|
||||
"version": "5.2.1",
|
||||
"type": "commonjs",
|
||||
"license": "MIT",
|
||||
"author": "Evan You",
|
||||
|
|
@ -33,14 +33,14 @@
|
|||
"vue": "^3.2.25"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.8",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"debug": "^4.4.0",
|
||||
"rollup": "^4.40.2",
|
||||
"debug": "^4.3.7",
|
||||
"rollup": "^4.27.2",
|
||||
"slash": "^5.1.0",
|
||||
"source-map-js": "^1.2.1",
|
||||
"vite": "^6.3.5",
|
||||
"vue": "^3.5.13"
|
||||
"vite": "^6.0.0",
|
||||
"vue": "^3.5.12"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "unbuild --stub",
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -167,7 +167,8 @@ function extractFileFromTarGzip(buffer, subpath) {
|
|||
let size = parseInt(str(offset + 124, 12), 8);
|
||||
offset += 512;
|
||||
if (!isNaN(size)) {
|
||||
if (name === subpath) return buffer.subarray(offset, offset + size);
|
||||
if (name === subpath)
|
||||
return buffer.subarray(offset, offset + size);
|
||||
offset += size + 511 & ~511;
|
||||
}
|
||||
}
|
||||
|
|
@ -202,8 +203,10 @@ function removeRecursive(dir) {
|
|||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (stats.isDirectory()) removeRecursive(entryPath);
|
||||
else fs2.unlinkSync(entryPath);
|
||||
if (stats.isDirectory())
|
||||
removeRecursive(entryPath);
|
||||
else
|
||||
fs2.unlinkSync(entryPath);
|
||||
}
|
||||
fs2.rmdirSync(dir);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -340,7 +340,6 @@ export interface ResolveOptions {
|
|||
resolveDir?: string
|
||||
kind?: ImportKind
|
||||
pluginData?: any
|
||||
with?: Record<string, string>
|
||||
}
|
||||
|
||||
/** Documentation: https://esbuild.github.io/plugins/#resolve-results */
|
||||
|
|
@ -380,7 +379,6 @@ export interface OnResolveArgs {
|
|||
resolveDir: string
|
||||
kind: ImportKind
|
||||
pluginData: any
|
||||
with: Record<string, string>
|
||||
}
|
||||
|
||||
export type ImportKind =
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "esbuild",
|
||||
"version": "0.21.5",
|
||||
"version": "0.20.2",
|
||||
"description": "An extremely fast JavaScript and CSS bundler and minifier.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -18,29 +18,29 @@
|
|||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
"@esbuild/aix-ppc64": "0.20.2",
|
||||
"@esbuild/android-arm": "0.20.2",
|
||||
"@esbuild/android-arm64": "0.20.2",
|
||||
"@esbuild/android-x64": "0.20.2",
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@esbuild/freebsd-arm64": "0.20.2",
|
||||
"@esbuild/freebsd-x64": "0.20.2",
|
||||
"@esbuild/linux-arm": "0.20.2",
|
||||
"@esbuild/linux-arm64": "0.20.2",
|
||||
"@esbuild/linux-ia32": "0.20.2",
|
||||
"@esbuild/linux-loong64": "0.20.2",
|
||||
"@esbuild/linux-mips64el": "0.20.2",
|
||||
"@esbuild/linux-ppc64": "0.20.2",
|
||||
"@esbuild/linux-riscv64": "0.20.2",
|
||||
"@esbuild/linux-s390x": "0.20.2",
|
||||
"@esbuild/linux-x64": "0.20.2",
|
||||
"@esbuild/netbsd-x64": "0.20.2",
|
||||
"@esbuild/openbsd-x64": "0.20.2",
|
||||
"@esbuild/sunos-x64": "0.20.2",
|
||||
"@esbuild/win32-arm64": "0.20.2",
|
||||
"@esbuild/win32-ia32": "0.20.2",
|
||||
"@esbuild/win32-x64": "0.20.2"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
<a href="https://nodejs.org/en/about/previous-releases">
|
||||
<img src="https://img.shields.io/node/v/rollup.svg" alt="node compatibility">
|
||||
</a>
|
||||
<a href="https://packagephobia.now.sh/result?p=rollup">
|
||||
<img src="https://packagephobia.now.sh/badge?p=rollup" alt="install size" >
|
||||
<a href="https://packagephobia.com/result?p=rollup">
|
||||
<img src="https://packagephobia.com/badge?p=rollup" alt="install size" >
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/rollup/rollup">
|
||||
<img src="https://codecov.io/gh/rollup/rollup/graph/badge.svg" alt="code coverage" >
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ function _mergeNamespaces(n, m) {
|
|||
return Object.defineProperty(n, Symbol.toStringTag, { value: 'Module' });
|
||||
}
|
||||
|
||||
var version = "4.59.0";
|
||||
var version = "4.59.1";
|
||||
|
||||
// src/vlq.ts
|
||||
var comma = ",".charCodeAt(0);
|
||||
|
|
@ -19594,7 +19594,11 @@ function getDynamicallyDependentEntriesByDynamicEntry(dependentEntriesByModule,
|
|||
getDynamicImporters(dynamicEntry),
|
||||
dynamicEntry.implicitlyLoadedAfter
|
||||
])) {
|
||||
for (const entry of dependentEntriesByModule.get(importer)) {
|
||||
const importerEntries = dependentEntriesByModule.get(importer);
|
||||
if (!importerEntries) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of importerEntries) {
|
||||
dynamicallyDependentEntries.add(entry);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ function _mergeNamespaces(n, m) {
|
|||
|
||||
const promises__namespace = /*#__PURE__*/_interopNamespaceDefault(promises);
|
||||
|
||||
var version = "4.59.0";
|
||||
var version = "4.59.1";
|
||||
|
||||
function ensureArray$1(items) {
|
||||
if (Array.isArray(items)) {
|
||||
|
|
@ -21088,7 +21088,11 @@ function getDynamicallyDependentEntriesByDynamicEntry(dependentEntriesByModule,
|
|||
getDynamicImporters(dynamicEntry),
|
||||
dynamicEntry.implicitlyLoadedAfter
|
||||
])) {
|
||||
for (const entry of dependentEntriesByModule.get(importer)) {
|
||||
const importerEntries = dependentEntriesByModule.get(importer);
|
||||
if (!importerEntries) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of importerEntries) {
|
||||
dynamicallyDependentEntries.add(entry);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
@license
|
||||
Rollup.js v4.59.0
|
||||
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476
|
||||
Rollup.js v4.59.1
|
||||
Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
|
||||
|
||||
https://github.com/rollup/rollup
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "rollup",
|
||||
"version": "4.59.0",
|
||||
"version": "4.59.1",
|
||||
"description": "Next-generation ES module bundler",
|
||||
"main": "dist/rollup.js",
|
||||
"module": "dist/es/rollup.js",
|
||||
|
|
@ -114,31 +114,31 @@
|
|||
"homepage": "https://rollupjs.org/",
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2",
|
||||
"@rollup/rollup-darwin-arm64": "4.59.0",
|
||||
"@rollup/rollup-android-arm64": "4.59.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.59.0",
|
||||
"@rollup/rollup-android-arm-eabi": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
|
||||
"@rollup/rollup-darwin-x64": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.59.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.59.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.59.0",
|
||||
"@rollup/rollup-openbsd-x64": "4.59.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.59.0"
|
||||
"@rollup/rollup-darwin-arm64": "4.59.1",
|
||||
"@rollup/rollup-android-arm64": "4.59.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.59.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.59.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.59.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.59.1",
|
||||
"@rollup/rollup-android-arm-eabi": "4.59.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.59.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.59.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.59.1",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.59.1",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.59.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.59.1",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.59.1",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.59.1",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.59.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.59.1",
|
||||
"@rollup/rollup-darwin-x64": "4.59.1",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.59.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.59.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.59.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.59.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.59.1",
|
||||
"@rollup/rollup-openbsd-x64": "4.59.1",
|
||||
"@rollup/rollup-openharmony-arm64": "4.59.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
|
|
@ -147,12 +147,12 @@
|
|||
"core-js": "We only update manually as every update requires a snapshot update"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/commands": "^6.10.2",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/language": "^6.12.1",
|
||||
"@codemirror/commands": "^6.10.3",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/language": "^6.12.2",
|
||||
"@codemirror/search": "^6.6.0",
|
||||
"@codemirror/state": "^6.5.4",
|
||||
"@codemirror/view": "^6.39.14",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/view": "^6.40.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@inquirer/prompts": "^7.10.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5",
|
||||
|
|
@ -160,21 +160,21 @@
|
|||
"@napi-rs/cli": "3.4.1",
|
||||
"@rollup/plugin-alias": "^6.0.0",
|
||||
"@rollup/plugin-buble": "^1.0.3",
|
||||
"@rollup/plugin-commonjs": "^29.0.0",
|
||||
"@rollup/plugin-commonjs": "^29.0.2",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||
"@rollup/plugin-replace": "^6.0.3",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-typescript": "^12.3.0",
|
||||
"@rollup/pluginutils": "^5.3.0",
|
||||
"@shikijs/vitepress-twoslash": "^3.22.0",
|
||||
"@shikijs/vitepress-twoslash": "^4.0.2",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^20.19.33",
|
||||
"@types/node": "^20.19.37",
|
||||
"@types/picomatch": "^4.0.2",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/yargs-parser": "^21.0.3",
|
||||
"@vue/language-server": "^3.2.4",
|
||||
"acorn": "^8.15.0",
|
||||
"@vue/language-server": "^3.2.5",
|
||||
"acorn": "^8.16.0",
|
||||
"acorn-import-assertions": "^1.9.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"buble": "^0.20.0",
|
||||
|
|
@ -186,26 +186,26 @@
|
|||
"date-time": "^4.0.0",
|
||||
"es5-shim": "^4.6.7",
|
||||
"es6-shim": "^0.35.8",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint": "^10.0.3",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-unicorn": "^63.0.0",
|
||||
"eslint-plugin-vue": "^10.8.0",
|
||||
"fixturify": "^3.0.0",
|
||||
"flru": "^1.0.2",
|
||||
"fs-extra": "^11.3.3",
|
||||
"fs-extra": "^11.3.4",
|
||||
"github-api": "^3.4.0",
|
||||
"globals": "^17.3.0",
|
||||
"globals": "^17.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"is-reference": "^3.0.3",
|
||||
"lint-staged": "^16.2.7",
|
||||
"lint-staged": "^16.4.0",
|
||||
"locate-character": "^3.0.0",
|
||||
"magic-string": "^0.30.21",
|
||||
"memfs": "^4.56.10",
|
||||
"mocha": "11.3.0",
|
||||
"nodemon": "^3.1.11",
|
||||
"memfs": "^4.56.11",
|
||||
"mocha": "11.7.5",
|
||||
"nodemon": "^3.1.14",
|
||||
"npm-audit-resolver": "^3.0.0-RC.0",
|
||||
"nyc": "^17.1.0",
|
||||
"nyc": "^18.0.0",
|
||||
"patch-package": "^8.0.1",
|
||||
"picocolors": "^1.1.1",
|
||||
"picomatch": "^4.0.3",
|
||||
|
|
@ -215,7 +215,7 @@
|
|||
"pretty-bytes": "^7.1.0",
|
||||
"pretty-ms": "^9.3.0",
|
||||
"requirejs": "^2.3.8",
|
||||
"rollup": "^4.57.1",
|
||||
"rollup": "^4.59.0",
|
||||
"rollup-plugin-license": "^3.7.0",
|
||||
"semver": "^7.7.4",
|
||||
"shx": "^0.4.0",
|
||||
|
|
@ -223,24 +223,24 @@
|
|||
"source-map": "^0.7.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"systemjs": "^6.15.1",
|
||||
"terser": "^5.46.0",
|
||||
"terser": "^5.46.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.56.0",
|
||||
"typescript-eslint": "^8.57.1",
|
||||
"vite": "^7.3.1",
|
||||
"vitepress": "^1.6.4",
|
||||
"vue": "^3.5.28",
|
||||
"vue": "^3.5.30",
|
||||
"vue-eslint-parser": "^10.4.0",
|
||||
"vue-tsc": "^3.2.4",
|
||||
"vue-tsc": "^3.2.5",
|
||||
"wasm-pack": "^0.14.0",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"overrides": {
|
||||
"axios": "^1.13.5",
|
||||
"axios": "^1.13.6",
|
||||
"esbuild": ">0.24.2",
|
||||
"lodash-es": ">4.17.22",
|
||||
"path-scurry": {
|
||||
"lru-cache": "^11.2.6"
|
||||
"lru-cache": "^11.2.7"
|
||||
},
|
||||
"readable-stream": "npm:@built-in/readable-stream@1",
|
||||
"semver": "^7.7.4",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Vite is released under the MIT license:
|
|||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-present, VoidZero Inc. and Vite contributors
|
||||
Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
@ -25,7 +25,7 @@ SOFTWARE.
|
|||
|
||||
# Licenses of bundled dependencies
|
||||
The published Vite artifact additionally contains code with the following licenses:
|
||||
Apache-2.0, BSD-2-Clause, BlueOak-1.0.0, CC0-1.0, ISC, MIT
|
||||
Apache-2.0, BSD-2-Clause, CC0-1.0, ISC, MIT
|
||||
|
||||
# Bundled dependencies:
|
||||
## @ampproject/remapping
|
||||
|
|
@ -587,6 +587,64 @@ Repository: rollup/plugins
|
|||
|
||||
---------------------------------------
|
||||
|
||||
## acorn
|
||||
License: MIT
|
||||
By: Marijn Haverbeke, Ingvar Stepanyan, Adrian Heine
|
||||
Repository: https://github.com/acornjs/acorn.git
|
||||
|
||||
> MIT License
|
||||
>
|
||||
> Copyright (C) 2012-2022 by various contributors (see AUTHORS)
|
||||
>
|
||||
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
> of this software and associated documentation files (the "Software"), to deal
|
||||
> in the Software without restriction, including without limitation the rights
|
||||
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
> copies of the Software, and to permit persons to whom the Software is
|
||||
> furnished to do so, subject to the following conditions:
|
||||
>
|
||||
> The above copyright notice and this permission notice shall be included in
|
||||
> all copies or substantial portions of the Software.
|
||||
>
|
||||
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
> THE SOFTWARE.
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## acorn-walk
|
||||
License: MIT
|
||||
By: Marijn Haverbeke, Ingvar Stepanyan, Adrian Heine
|
||||
Repository: https://github.com/acornjs/acorn.git
|
||||
|
||||
> MIT License
|
||||
>
|
||||
> Copyright (C) 2012-2020 by various contributors (see AUTHORS)
|
||||
>
|
||||
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
> of this software and associated documentation files (the "Software"), to deal
|
||||
> in the Software without restriction, including without limitation the rights
|
||||
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
> copies of the Software, and to permit persons to whom the Software is
|
||||
> furnished to do so, subject to the following conditions:
|
||||
>
|
||||
> The above copyright notice and this permission notice shall be included in
|
||||
> all copies or substantial portions of the Software.
|
||||
>
|
||||
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
> THE SOFTWARE.
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## ansi-regex
|
||||
License: MIT
|
||||
By: Sindre Sorhus
|
||||
|
|
@ -1470,6 +1528,57 @@ Repository: git@github.com:follow-redirects/follow-redirects.git
|
|||
|
||||
---------------------------------------
|
||||
|
||||
## fs.realpath
|
||||
License: ISC
|
||||
By: Isaac Z. Schlueter
|
||||
Repository: git+https://github.com/isaacs/fs.realpath.git
|
||||
|
||||
> The ISC License
|
||||
>
|
||||
> Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
>
|
||||
> Permission to use, copy, modify, and/or distribute this software for any
|
||||
> purpose with or without fee is hereby granted, provided that the above
|
||||
> copyright notice and this permission notice appear in all copies.
|
||||
>
|
||||
> THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
> WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
> MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
> ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
> WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
> IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
>
|
||||
> ----
|
||||
>
|
||||
> This library bundles a version of the `fs.realpath` and `fs.realpathSync`
|
||||
> methods from Node.js v0.10 under the terms of the Node.js MIT license.
|
||||
>
|
||||
> Node's license follows, also included at the header of `old.js` which contains
|
||||
> the licensed code:
|
||||
>
|
||||
> Copyright Joyent, Inc. and other Node contributors.
|
||||
>
|
||||
> Permission is hereby granted, free of charge, to any person obtaining a
|
||||
> copy of this software and associated documentation files (the "Software"),
|
||||
> to deal in the Software without restriction, including without limitation
|
||||
> the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
> and/or sell copies of the Software, and to permit persons to whom the
|
||||
> Software is furnished to do so, subject to the following conditions:
|
||||
>
|
||||
> The above copyright notice and this permission notice shall be included in
|
||||
> all copies or substantial portions of the Software.
|
||||
>
|
||||
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
> FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
> DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## generic-names
|
||||
License: MIT
|
||||
By: Alexey Litvinov
|
||||
|
|
@ -1506,7 +1615,7 @@ Repository: git://github.com/isaacs/node-glob.git
|
|||
|
||||
> The ISC License
|
||||
>
|
||||
> Copyright (c) 2009-2023 Isaac Z. Schlueter and Contributors
|
||||
> Copyright (c) 2009-2022 Isaac Z. Schlueter and Contributors
|
||||
>
|
||||
> Permission to use, copy, modify, and/or distribute this software for any
|
||||
> purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
@ -1589,6 +1698,51 @@ Repository: git+https://github.com/css-modules/icss-utils.git
|
|||
|
||||
---------------------------------------
|
||||
|
||||
## inflight
|
||||
License: ISC
|
||||
By: Isaac Z. Schlueter
|
||||
Repository: https://github.com/npm/inflight.git
|
||||
|
||||
> The ISC License
|
||||
>
|
||||
> Copyright (c) Isaac Z. Schlueter
|
||||
>
|
||||
> Permission to use, copy, modify, and/or distribute this software for any
|
||||
> purpose with or without fee is hereby granted, provided that the above
|
||||
> copyright notice and this permission notice appear in all copies.
|
||||
>
|
||||
> THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
> WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
> MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
> ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
> WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
> IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## inherits
|
||||
License: ISC
|
||||
Repository: git://github.com/isaacs/inherits
|
||||
|
||||
> The ISC License
|
||||
>
|
||||
> Copyright (c) Isaac Z. Schlueter
|
||||
>
|
||||
> Permission to use, copy, modify, and/or distribute this software for any
|
||||
> purpose with or without fee is hereby granted, provided that the above
|
||||
> copyright notice and this permission notice appear in all copies.
|
||||
>
|
||||
> THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
> REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
> FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
> INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
> LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
> OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
> PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## is-binary-path
|
||||
License: MIT
|
||||
By: Sindre Sorhus
|
||||
|
|
@ -1956,29 +2110,6 @@ Repository: lodash/lodash
|
|||
|
||||
---------------------------------------
|
||||
|
||||
## lru-cache
|
||||
License: ISC
|
||||
By: Isaac Z. Schlueter
|
||||
Repository: git://github.com/isaacs/node-lru-cache.git
|
||||
|
||||
> The ISC License
|
||||
>
|
||||
> Copyright (c) 2010-2023 Isaac Z. Schlueter and Contributors
|
||||
>
|
||||
> Permission to use, copy, modify, and/or distribute this software for any
|
||||
> purpose with or without fee is hereby granted, provided that the above
|
||||
> copyright notice and this permission notice appear in all copies.
|
||||
>
|
||||
> THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
> WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
> MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
> ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
> WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
> IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## magic-string
|
||||
License: MIT
|
||||
By: Rich Harris
|
||||
|
|
@ -2074,29 +2205,6 @@ Repository: git://github.com/isaacs/minimatch.git
|
|||
|
||||
---------------------------------------
|
||||
|
||||
## minipass
|
||||
License: ISC
|
||||
By: Isaac Z. Schlueter
|
||||
Repository: https://github.com/isaacs/minipass
|
||||
|
||||
> The ISC License
|
||||
>
|
||||
> Copyright (c) 2017-2023 npm, Inc., Isaac Z. Schlueter, and Contributors
|
||||
>
|
||||
> Permission to use, copy, modify, and/or distribute this software for any
|
||||
> purpose with or without fee is hereby granted, provided that the above
|
||||
> copyright notice and this permission notice appear in all copies.
|
||||
>
|
||||
> THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
> WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
> MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
> ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
> WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
> IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## mlly
|
||||
License: MIT
|
||||
Repository: unjs/mlly
|
||||
|
|
@ -2271,6 +2379,29 @@ Repository: jshttp/on-finished
|
|||
|
||||
---------------------------------------
|
||||
|
||||
## once
|
||||
License: ISC
|
||||
By: Isaac Z. Schlueter
|
||||
Repository: git://github.com/isaacs/once
|
||||
|
||||
> The ISC License
|
||||
>
|
||||
> Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
>
|
||||
> Permission to use, copy, modify, and/or distribute this software for any
|
||||
> purpose with or without fee is hereby granted, provided that the above
|
||||
> copyright notice and this permission notice appear in all copies.
|
||||
>
|
||||
> THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
> WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
> MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
> ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
> WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
> IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## open
|
||||
License: MIT
|
||||
By: Sindre Sorhus
|
||||
|
|
@ -2363,69 +2494,6 @@ Repository: sindresorhus/path-key
|
|||
|
||||
---------------------------------------
|
||||
|
||||
## path-scurry
|
||||
License: BlueOak-1.0.0
|
||||
By: Isaac Z. Schlueter
|
||||
Repository: git+https://github.com/isaacs/path-scurry
|
||||
|
||||
> # Blue Oak Model License
|
||||
>
|
||||
> Version 1.0.0
|
||||
>
|
||||
> ## Purpose
|
||||
>
|
||||
> This license gives everyone as much permission to work with
|
||||
> this software as possible, while protecting contributors
|
||||
> from liability.
|
||||
>
|
||||
> ## Acceptance
|
||||
>
|
||||
> In order to receive this license, you must agree to its
|
||||
> rules. The rules of this license are both obligations
|
||||
> under that agreement and conditions to your license.
|
||||
> You must not do anything with this software that triggers
|
||||
> a rule that you cannot or will not follow.
|
||||
>
|
||||
> ## Copyright
|
||||
>
|
||||
> Each contributor licenses you to do everything with this
|
||||
> software that would otherwise infringe that contributor's
|
||||
> copyright in it.
|
||||
>
|
||||
> ## Notices
|
||||
>
|
||||
> You must ensure that everyone who gets a copy of
|
||||
> any part of this software from you, with or without
|
||||
> changes, also gets the text of this license or a link to
|
||||
> <https://blueoakcouncil.org/license/1.0.0>.
|
||||
>
|
||||
> ## Excuse
|
||||
>
|
||||
> If anyone notifies you in writing that you have not
|
||||
> complied with [Notices](#notices), you can keep your
|
||||
> license by taking all practical steps to comply within 30
|
||||
> days after the notice. If you do not do so, your license
|
||||
> ends immediately.
|
||||
>
|
||||
> ## Patent
|
||||
>
|
||||
> Each contributor licenses you to do everything with this
|
||||
> software that would otherwise infringe any patent claims
|
||||
> they can license or become able to license.
|
||||
>
|
||||
> ## Reliability
|
||||
>
|
||||
> No contributor can revoke this license.
|
||||
>
|
||||
> ## No Liability
|
||||
>
|
||||
> ***As far as the law allows, this software comes as is,
|
||||
> without any warranty or condition, and no contributor
|
||||
> will be liable to anyone for any damages related to this
|
||||
> software or this license, under any kind of legal claim.***
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## periscopic
|
||||
License: MIT
|
||||
Repository: Rich-Harris/periscopic
|
||||
|
|
@ -3375,6 +3443,29 @@ Repository: git://github.com/isaacs/node-which.git
|
|||
|
||||
---------------------------------------
|
||||
|
||||
## wrappy
|
||||
License: ISC
|
||||
By: Isaac Z. Schlueter
|
||||
Repository: https://github.com/npm/wrappy
|
||||
|
||||
> The ISC License
|
||||
>
|
||||
> Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
>
|
||||
> Permission to use, copy, modify, and/or distribute this software for any
|
||||
> purpose with or without fee is hereby granted, provided that the above
|
||||
> copyright notice and this permission notice appear in all copies.
|
||||
>
|
||||
> THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
> WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
> MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
> ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
> WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
> ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
> IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## ws
|
||||
License: MIT
|
||||
By: Einar Otto Stangvik
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@
|
|||
|
||||
Vite (French word for "fast", pronounced `/vit/`) is a new breed of frontend build tool that significantly improves the frontend development experience. It consists of two major parts:
|
||||
|
||||
- A dev server that serves your source files over [native ES modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), with [rich built-in features](https://vite.dev/guide/features.html) and astonishingly fast [Hot Module Replacement (HMR)](https://vite.dev/guide/features.html#hot-module-replacement).
|
||||
- A dev server that serves your source files over [native ES modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), with [rich built-in features](https://vitejs.dev/guide/features.html) and astonishingly fast [Hot Module Replacement (HMR)](https://vitejs.dev/guide/features.html#hot-module-replacement).
|
||||
|
||||
- A [build command](https://vite.dev/guide/build.html) that bundles your code with [Rollup](https://rollupjs.org), pre-configured to output highly optimized static assets for production.
|
||||
- A [build command](https://vitejs.dev/guide/build.html) that bundles your code with [Rollup](https://rollupjs.org), pre-configured to output highly optimized static assets for production.
|
||||
|
||||
In addition, Vite is highly extensible via its [Plugin API](https://vite.dev/guide/api-plugin.html) and [JavaScript API](https://vite.dev/guide/api-javascript.html) with full typing support.
|
||||
In addition, Vite is highly extensible via its [Plugin API](https://vitejs.dev/guide/api-plugin.html) and [JavaScript API](https://vitejs.dev/guide/api-javascript.html) with full typing support.
|
||||
|
||||
[Read the Docs to Learn More](https://vite.dev).
|
||||
[Read the Docs to Learn More](https://vitejs.dev).
|
||||
|
|
|
|||
|
|
@ -54,10 +54,6 @@ declare module '*.apng' {
|
|||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.bmp' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
declare module '*.png' {
|
||||
const src: string
|
||||
export default src
|
||||
|
|
@ -246,11 +242,3 @@ declare module '*?inline' {
|
|||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare interface VitePreloadErrorEvent extends Event {
|
||||
payload: Error
|
||||
}
|
||||
|
||||
declare interface WindowEventMap {
|
||||
'vite:preloadError': VitePreloadErrorEvent
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,24 +1,30 @@
|
|||
const context = (() => {
|
||||
if (typeof globalThis !== "undefined") {
|
||||
return globalThis;
|
||||
} else if (typeof self !== "undefined") {
|
||||
return self;
|
||||
} else if (typeof window !== "undefined") {
|
||||
return window;
|
||||
} else {
|
||||
return Function("return this")();
|
||||
}
|
||||
if (typeof globalThis !== 'undefined') {
|
||||
return globalThis;
|
||||
}
|
||||
else if (typeof self !== 'undefined') {
|
||||
return self;
|
||||
}
|
||||
else if (typeof window !== 'undefined') {
|
||||
return window;
|
||||
}
|
||||
else {
|
||||
return Function('return this')();
|
||||
}
|
||||
})();
|
||||
// assign defines
|
||||
const defines = __DEFINES__;
|
||||
Object.keys(defines).forEach((key) => {
|
||||
const segments = key.split(".");
|
||||
let target = context;
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const segment = segments[i];
|
||||
if (i === segments.length - 1) {
|
||||
target[segment] = defines[key];
|
||||
} else {
|
||||
target = target[segment] || (target[segment] = {});
|
||||
const segments = key.split('.');
|
||||
let target = context;
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const segment = segments[i];
|
||||
if (i === segments.length - 1) {
|
||||
target[segment] = defines[key];
|
||||
}
|
||||
else {
|
||||
target = target[segment] || (target[segment] = {});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
//# sourceMappingURL=env.mjs.map
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,993 +0,0 @@
|
|||
import { C as getDefaultExportFromCjs } from './dep-BK3b2jBa.js';
|
||||
import require$$0 from 'path';
|
||||
import require$$0__default from 'fs';
|
||||
import { l as lib } from './dep-IQS-Za7F.js';
|
||||
|
||||
import { fileURLToPath as __cjs_fileURLToPath } from 'node:url';
|
||||
import { dirname as __cjs_dirname } from 'node:path';
|
||||
import { createRequire as __cjs_createRequire } from 'node:module';
|
||||
|
||||
const __filename = __cjs_fileURLToPath(import.meta.url);
|
||||
const __dirname = __cjs_dirname(__filename);
|
||||
const require = __cjs_createRequire(import.meta.url);
|
||||
const __require = require;
|
||||
function _mergeNamespaces(n, m) {
|
||||
for (var i = 0; i < m.length; i++) {
|
||||
var e = m[i];
|
||||
if (typeof e !== 'string' && !Array.isArray(e)) { for (var k in e) {
|
||||
if (k !== 'default' && !(k in n)) {
|
||||
n[k] = e[k];
|
||||
}
|
||||
} }
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
var formatImportPrelude$2 = function formatImportPrelude(layer, media, supports) {
|
||||
const parts = [];
|
||||
|
||||
if (typeof layer !== "undefined") {
|
||||
let layerParams = "layer";
|
||||
if (layer) {
|
||||
layerParams = `layer(${layer})`;
|
||||
}
|
||||
|
||||
parts.push(layerParams);
|
||||
}
|
||||
|
||||
if (typeof supports !== "undefined") {
|
||||
parts.push(`supports(${supports})`);
|
||||
}
|
||||
|
||||
if (typeof media !== "undefined") {
|
||||
parts.push(media);
|
||||
}
|
||||
|
||||
return parts.join(" ")
|
||||
};
|
||||
|
||||
const formatImportPrelude$1 = formatImportPrelude$2;
|
||||
|
||||
// Base64 encode an import with conditions
|
||||
// The order of conditions is important and is interleaved with cascade layer declarations
|
||||
// Each group of conditions and cascade layers needs to be interpreted in order
|
||||
// To achieve this we create a list of base64 encoded imports, where each import contains a stylesheet with another import.
|
||||
// Each import can define a single group of conditions and a single cascade layer.
|
||||
var base64EncodedImport = function base64EncodedConditionalImport(prelude, conditions) {
|
||||
conditions.reverse();
|
||||
const first = conditions.pop();
|
||||
let params = `${prelude} ${formatImportPrelude$1(
|
||||
first.layer,
|
||||
first.media,
|
||||
first.supports,
|
||||
)}`;
|
||||
|
||||
for (const condition of conditions) {
|
||||
params = `'data:text/css;base64,${Buffer.from(`@import ${params}`).toString(
|
||||
"base64",
|
||||
)}' ${formatImportPrelude$1(
|
||||
condition.layer,
|
||||
condition.media,
|
||||
condition.supports,
|
||||
)}`;
|
||||
}
|
||||
|
||||
return params
|
||||
};
|
||||
|
||||
const base64EncodedConditionalImport = base64EncodedImport;
|
||||
|
||||
var applyConditions$1 = function applyConditions(bundle, atRule) {
|
||||
bundle.forEach(stmt => {
|
||||
if (
|
||||
stmt.type === "charset" ||
|
||||
stmt.type === "warning" ||
|
||||
!stmt.conditions?.length
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (stmt.type === "import") {
|
||||
stmt.node.params = base64EncodedConditionalImport(
|
||||
stmt.fullUri,
|
||||
stmt.conditions,
|
||||
);
|
||||
return
|
||||
}
|
||||
|
||||
const { nodes } = stmt;
|
||||
const { parent } = nodes[0];
|
||||
|
||||
const atRules = [];
|
||||
|
||||
// Convert conditions to at-rules
|
||||
for (const condition of stmt.conditions) {
|
||||
if (typeof condition.media !== "undefined") {
|
||||
const mediaNode = atRule({
|
||||
name: "media",
|
||||
params: condition.media,
|
||||
source: parent.source,
|
||||
});
|
||||
|
||||
atRules.push(mediaNode);
|
||||
}
|
||||
|
||||
if (typeof condition.supports !== "undefined") {
|
||||
const supportsNode = atRule({
|
||||
name: "supports",
|
||||
params: `(${condition.supports})`,
|
||||
source: parent.source,
|
||||
});
|
||||
|
||||
atRules.push(supportsNode);
|
||||
}
|
||||
|
||||
if (typeof condition.layer !== "undefined") {
|
||||
const layerNode = atRule({
|
||||
name: "layer",
|
||||
params: condition.layer,
|
||||
source: parent.source,
|
||||
});
|
||||
|
||||
atRules.push(layerNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Add nodes to AST
|
||||
const outerAtRule = atRules.shift();
|
||||
const innerAtRule = atRules.reduce((previous, next) => {
|
||||
previous.append(next);
|
||||
return next
|
||||
}, outerAtRule);
|
||||
|
||||
parent.insertBefore(nodes[0], outerAtRule);
|
||||
|
||||
// remove nodes
|
||||
nodes.forEach(node => {
|
||||
node.parent = undefined;
|
||||
});
|
||||
|
||||
// better output
|
||||
nodes[0].raws.before = nodes[0].raws.before || "\n";
|
||||
|
||||
// wrap new rules with media query and/or layer at rule
|
||||
innerAtRule.append(nodes);
|
||||
|
||||
stmt.type = "nodes";
|
||||
stmt.nodes = [outerAtRule];
|
||||
delete stmt.node;
|
||||
});
|
||||
};
|
||||
|
||||
var applyRaws$1 = function applyRaws(bundle) {
|
||||
bundle.forEach((stmt, index) => {
|
||||
if (index === 0) return
|
||||
|
||||
if (stmt.parent) {
|
||||
const { before } = stmt.parent.node.raws;
|
||||
if (stmt.type === "nodes") stmt.nodes[0].raws.before = before;
|
||||
else stmt.node.raws.before = before;
|
||||
} else if (stmt.type === "nodes") {
|
||||
stmt.nodes[0].raws.before = stmt.nodes[0].raws.before || "\n";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var applyStyles$1 = function applyStyles(bundle, styles) {
|
||||
styles.nodes = [];
|
||||
|
||||
// Strip additional statements.
|
||||
bundle.forEach(stmt => {
|
||||
if (["charset", "import"].includes(stmt.type)) {
|
||||
stmt.node.parent = undefined;
|
||||
styles.append(stmt.node);
|
||||
} else if (stmt.type === "nodes") {
|
||||
stmt.nodes.forEach(node => {
|
||||
node.parent = undefined;
|
||||
styles.append(node);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var readCache$1 = {exports: {}};
|
||||
|
||||
var pify$2 = {exports: {}};
|
||||
|
||||
var processFn = function (fn, P, opts) {
|
||||
return function () {
|
||||
var that = this;
|
||||
var args = new Array(arguments.length);
|
||||
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
args[i] = arguments[i];
|
||||
}
|
||||
|
||||
return new P(function (resolve, reject) {
|
||||
args.push(function (err, result) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (opts.multiArgs) {
|
||||
var results = new Array(arguments.length - 1);
|
||||
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
results[i - 1] = arguments[i];
|
||||
}
|
||||
|
||||
resolve(results);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
|
||||
fn.apply(that, args);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
var pify$1 = pify$2.exports = function (obj, P, opts) {
|
||||
if (typeof P !== 'function') {
|
||||
opts = P;
|
||||
P = Promise;
|
||||
}
|
||||
|
||||
opts = opts || {};
|
||||
opts.exclude = opts.exclude || [/.+Sync$/];
|
||||
|
||||
var filter = function (key) {
|
||||
var match = function (pattern) {
|
||||
return typeof pattern === 'string' ? key === pattern : pattern.test(key);
|
||||
};
|
||||
|
||||
return opts.include ? opts.include.some(match) : !opts.exclude.some(match);
|
||||
};
|
||||
|
||||
var ret = typeof obj === 'function' ? function () {
|
||||
if (opts.excludeMain) {
|
||||
return obj.apply(this, arguments);
|
||||
}
|
||||
|
||||
return processFn(obj, P, opts).apply(this, arguments);
|
||||
} : {};
|
||||
|
||||
return Object.keys(obj).reduce(function (ret, key) {
|
||||
var x = obj[key];
|
||||
|
||||
ret[key] = typeof x === 'function' && filter(key) ? processFn(x, P, opts) : x;
|
||||
|
||||
return ret;
|
||||
}, ret);
|
||||
};
|
||||
|
||||
pify$1.all = pify$1;
|
||||
|
||||
var pifyExports = pify$2.exports;
|
||||
|
||||
var fs = require$$0__default;
|
||||
var path$3 = require$$0;
|
||||
var pify = pifyExports;
|
||||
|
||||
var stat = pify(fs.stat);
|
||||
var readFile = pify(fs.readFile);
|
||||
var resolve = path$3.resolve;
|
||||
|
||||
var cache = Object.create(null);
|
||||
|
||||
function convert(content, encoding) {
|
||||
if (Buffer.isEncoding(encoding)) {
|
||||
return content.toString(encoding);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
readCache$1.exports = function (path, encoding) {
|
||||
path = resolve(path);
|
||||
|
||||
return stat(path).then(function (stats) {
|
||||
var item = cache[path];
|
||||
|
||||
if (item && item.mtime.getTime() === stats.mtime.getTime()) {
|
||||
return convert(item.content, encoding);
|
||||
}
|
||||
|
||||
return readFile(path).then(function (data) {
|
||||
cache[path] = {
|
||||
mtime: stats.mtime,
|
||||
content: data
|
||||
};
|
||||
|
||||
return convert(data, encoding);
|
||||
});
|
||||
}).catch(function (err) {
|
||||
cache[path] = null;
|
||||
return Promise.reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
readCache$1.exports.sync = function (path, encoding) {
|
||||
path = resolve(path);
|
||||
|
||||
try {
|
||||
var stats = fs.statSync(path);
|
||||
var item = cache[path];
|
||||
|
||||
if (item && item.mtime.getTime() === stats.mtime.getTime()) {
|
||||
return convert(item.content, encoding);
|
||||
}
|
||||
|
||||
var data = fs.readFileSync(path);
|
||||
|
||||
cache[path] = {
|
||||
mtime: stats.mtime,
|
||||
content: data
|
||||
};
|
||||
|
||||
return convert(data, encoding);
|
||||
} catch (err) {
|
||||
cache[path] = null;
|
||||
throw err;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
readCache$1.exports.get = function (path, encoding) {
|
||||
path = resolve(path);
|
||||
if (cache[path]) {
|
||||
return convert(cache[path].content, encoding);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
readCache$1.exports.clear = function () {
|
||||
cache = Object.create(null);
|
||||
};
|
||||
|
||||
var readCacheExports = readCache$1.exports;
|
||||
|
||||
const anyDataURLRegexp = /^data:text\/css(?:;(base64|plain))?,/i;
|
||||
const base64DataURLRegexp = /^data:text\/css;base64,/i;
|
||||
const plainDataURLRegexp = /^data:text\/css;plain,/i;
|
||||
|
||||
function isValid(url) {
|
||||
return anyDataURLRegexp.test(url)
|
||||
}
|
||||
|
||||
function contents(url) {
|
||||
if (base64DataURLRegexp.test(url)) {
|
||||
// "data:text/css;base64,".length === 21
|
||||
return Buffer.from(url.slice(21), "base64").toString()
|
||||
}
|
||||
|
||||
if (plainDataURLRegexp.test(url)) {
|
||||
// "data:text/css;plain,".length === 20
|
||||
return decodeURIComponent(url.slice(20))
|
||||
}
|
||||
|
||||
// "data:text/css,".length === 14
|
||||
return decodeURIComponent(url.slice(14))
|
||||
}
|
||||
|
||||
var dataUrl = {
|
||||
isValid,
|
||||
contents,
|
||||
};
|
||||
|
||||
const readCache = readCacheExports;
|
||||
const dataURL$1 = dataUrl;
|
||||
|
||||
var loadContent$1 = function loadContent(filename) {
|
||||
if (dataURL$1.isValid(filename)) {
|
||||
return dataURL$1.contents(filename)
|
||||
}
|
||||
|
||||
return readCache(filename, "utf-8")
|
||||
};
|
||||
|
||||
// external tooling
|
||||
const valueParser = lib;
|
||||
|
||||
// extended tooling
|
||||
const { stringify } = valueParser;
|
||||
|
||||
var parseStatements$1 = function parseStatements(result, styles, conditions, from) {
|
||||
const statements = [];
|
||||
let nodes = [];
|
||||
|
||||
styles.each(node => {
|
||||
let stmt;
|
||||
if (node.type === "atrule") {
|
||||
if (node.name === "import")
|
||||
stmt = parseImport(result, node, conditions, from);
|
||||
else if (node.name === "charset")
|
||||
stmt = parseCharset(result, node, conditions, from);
|
||||
}
|
||||
|
||||
if (stmt) {
|
||||
if (nodes.length) {
|
||||
statements.push({
|
||||
type: "nodes",
|
||||
nodes,
|
||||
conditions: [...conditions],
|
||||
from,
|
||||
});
|
||||
nodes = [];
|
||||
}
|
||||
statements.push(stmt);
|
||||
} else nodes.push(node);
|
||||
});
|
||||
|
||||
if (nodes.length) {
|
||||
statements.push({
|
||||
type: "nodes",
|
||||
nodes,
|
||||
conditions: [...conditions],
|
||||
from,
|
||||
});
|
||||
}
|
||||
|
||||
return statements
|
||||
};
|
||||
|
||||
function parseCharset(result, atRule, conditions, from) {
|
||||
if (atRule.prev()) {
|
||||
return result.warn("@charset must precede all other statements", {
|
||||
node: atRule,
|
||||
})
|
||||
}
|
||||
return {
|
||||
type: "charset",
|
||||
node: atRule,
|
||||
conditions: [...conditions],
|
||||
from,
|
||||
}
|
||||
}
|
||||
|
||||
function parseImport(result, atRule, conditions, from) {
|
||||
let prev = atRule.prev();
|
||||
|
||||
// `@import` statements may follow other `@import` statements.
|
||||
if (prev) {
|
||||
do {
|
||||
if (
|
||||
prev.type === "comment" ||
|
||||
(prev.type === "atrule" && prev.name === "import")
|
||||
) {
|
||||
prev = prev.prev();
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
} while (prev)
|
||||
}
|
||||
|
||||
// All `@import` statements may be preceded by `@charset` or `@layer` statements.
|
||||
// But the `@import` statements must be consecutive.
|
||||
if (prev) {
|
||||
do {
|
||||
if (
|
||||
prev.type === "comment" ||
|
||||
(prev.type === "atrule" &&
|
||||
(prev.name === "charset" || (prev.name === "layer" && !prev.nodes)))
|
||||
) {
|
||||
prev = prev.prev();
|
||||
continue
|
||||
}
|
||||
|
||||
return result.warn(
|
||||
"@import must precede all other statements (besides @charset or empty @layer)",
|
||||
{ node: atRule },
|
||||
)
|
||||
} while (prev)
|
||||
}
|
||||
|
||||
if (atRule.nodes) {
|
||||
return result.warn(
|
||||
"It looks like you didn't end your @import statement correctly. " +
|
||||
"Child nodes are attached to it.",
|
||||
{ node: atRule },
|
||||
)
|
||||
}
|
||||
|
||||
const params = valueParser(atRule.params).nodes;
|
||||
const stmt = {
|
||||
type: "import",
|
||||
uri: "",
|
||||
fullUri: "",
|
||||
node: atRule,
|
||||
conditions: [...conditions],
|
||||
from,
|
||||
};
|
||||
|
||||
let layer;
|
||||
let media;
|
||||
let supports;
|
||||
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
const node = params[i];
|
||||
|
||||
if (node.type === "space" || node.type === "comment") continue
|
||||
|
||||
if (node.type === "string") {
|
||||
if (stmt.uri) {
|
||||
return result.warn(`Multiple url's in '${atRule.toString()}'`, {
|
||||
node: atRule,
|
||||
})
|
||||
}
|
||||
|
||||
if (!node.value) {
|
||||
return result.warn(`Unable to find uri in '${atRule.toString()}'`, {
|
||||
node: atRule,
|
||||
})
|
||||
}
|
||||
|
||||
stmt.uri = node.value;
|
||||
stmt.fullUri = stringify(node);
|
||||
continue
|
||||
}
|
||||
|
||||
if (node.type === "function" && /^url$/i.test(node.value)) {
|
||||
if (stmt.uri) {
|
||||
return result.warn(`Multiple url's in '${atRule.toString()}'`, {
|
||||
node: atRule,
|
||||
})
|
||||
}
|
||||
|
||||
if (!node.nodes?.[0]?.value) {
|
||||
return result.warn(`Unable to find uri in '${atRule.toString()}'`, {
|
||||
node: atRule,
|
||||
})
|
||||
}
|
||||
|
||||
stmt.uri = node.nodes[0].value;
|
||||
stmt.fullUri = stringify(node);
|
||||
continue
|
||||
}
|
||||
|
||||
if (!stmt.uri) {
|
||||
return result.warn(`Unable to find uri in '${atRule.toString()}'`, {
|
||||
node: atRule,
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
(node.type === "word" || node.type === "function") &&
|
||||
/^layer$/i.test(node.value)
|
||||
) {
|
||||
if (typeof layer !== "undefined") {
|
||||
return result.warn(`Multiple layers in '${atRule.toString()}'`, {
|
||||
node: atRule,
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof supports !== "undefined") {
|
||||
return result.warn(
|
||||
`layers must be defined before support conditions in '${atRule.toString()}'`,
|
||||
{
|
||||
node: atRule,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (node.nodes) {
|
||||
layer = stringify(node.nodes);
|
||||
} else {
|
||||
layer = "";
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (node.type === "function" && /^supports$/i.test(node.value)) {
|
||||
if (typeof supports !== "undefined") {
|
||||
return result.warn(
|
||||
`Multiple support conditions in '${atRule.toString()}'`,
|
||||
{
|
||||
node: atRule,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
supports = stringify(node.nodes);
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
media = stringify(params.slice(i));
|
||||
break
|
||||
}
|
||||
|
||||
if (!stmt.uri) {
|
||||
return result.warn(`Unable to find uri in '${atRule.toString()}'`, {
|
||||
node: atRule,
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
typeof media !== "undefined" ||
|
||||
typeof layer !== "undefined" ||
|
||||
typeof supports !== "undefined"
|
||||
) {
|
||||
stmt.conditions.push({
|
||||
layer,
|
||||
media,
|
||||
supports,
|
||||
});
|
||||
}
|
||||
|
||||
return stmt
|
||||
}
|
||||
|
||||
// builtin tooling
|
||||
const path$2 = require$$0;
|
||||
|
||||
// placeholder tooling
|
||||
let sugarss;
|
||||
|
||||
var processContent$1 = function processContent(
|
||||
result,
|
||||
content,
|
||||
filename,
|
||||
options,
|
||||
postcss,
|
||||
) {
|
||||
const { plugins } = options;
|
||||
const ext = path$2.extname(filename);
|
||||
|
||||
const parserList = [];
|
||||
|
||||
// SugarSS support:
|
||||
if (ext === ".sss") {
|
||||
if (!sugarss) {
|
||||
/* c8 ignore next 3 */
|
||||
try {
|
||||
sugarss = __require('sugarss');
|
||||
} catch {} // Ignore
|
||||
}
|
||||
if (sugarss)
|
||||
return runPostcss(postcss, content, filename, plugins, [sugarss])
|
||||
}
|
||||
|
||||
// Syntax support:
|
||||
if (result.opts.syntax?.parse) {
|
||||
parserList.push(result.opts.syntax.parse);
|
||||
}
|
||||
|
||||
// Parser support:
|
||||
if (result.opts.parser) parserList.push(result.opts.parser);
|
||||
// Try the default as a last resort:
|
||||
parserList.push(null);
|
||||
|
||||
return runPostcss(postcss, content, filename, plugins, parserList)
|
||||
};
|
||||
|
||||
function runPostcss(postcss, content, filename, plugins, parsers, index) {
|
||||
if (!index) index = 0;
|
||||
return postcss(plugins)
|
||||
.process(content, {
|
||||
from: filename,
|
||||
parser: parsers[index],
|
||||
})
|
||||
.catch(err => {
|
||||
// If there's an error, try the next parser
|
||||
index++;
|
||||
// If there are no parsers left, throw it
|
||||
if (index === parsers.length) throw err
|
||||
return runPostcss(postcss, content, filename, plugins, parsers, index)
|
||||
})
|
||||
}
|
||||
|
||||
const path$1 = require$$0;
|
||||
|
||||
const dataURL = dataUrl;
|
||||
const parseStatements = parseStatements$1;
|
||||
const processContent = processContent$1;
|
||||
const resolveId$1 = (id) => id;
|
||||
const formatImportPrelude = formatImportPrelude$2;
|
||||
|
||||
async function parseStyles$1(
|
||||
result,
|
||||
styles,
|
||||
options,
|
||||
state,
|
||||
conditions,
|
||||
from,
|
||||
postcss,
|
||||
) {
|
||||
const statements = parseStatements(result, styles, conditions, from);
|
||||
|
||||
for (const stmt of statements) {
|
||||
if (stmt.type !== "import" || !isProcessableURL(stmt.uri)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (options.filter && !options.filter(stmt.uri)) {
|
||||
// rejected by filter
|
||||
continue
|
||||
}
|
||||
|
||||
await resolveImportId(result, stmt, options, state, postcss);
|
||||
}
|
||||
|
||||
let charset;
|
||||
const imports = [];
|
||||
const bundle = [];
|
||||
|
||||
function handleCharset(stmt) {
|
||||
if (!charset) charset = stmt;
|
||||
// charsets aren't case-sensitive, so convert to lower case to compare
|
||||
else if (
|
||||
stmt.node.params.toLowerCase() !== charset.node.params.toLowerCase()
|
||||
) {
|
||||
throw stmt.node.error(
|
||||
`Incompatible @charset statements:
|
||||
${stmt.node.params} specified in ${stmt.node.source.input.file}
|
||||
${charset.node.params} specified in ${charset.node.source.input.file}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// squash statements and their children
|
||||
statements.forEach(stmt => {
|
||||
if (stmt.type === "charset") handleCharset(stmt);
|
||||
else if (stmt.type === "import") {
|
||||
if (stmt.children) {
|
||||
stmt.children.forEach((child, index) => {
|
||||
if (child.type === "import") imports.push(child);
|
||||
else if (child.type === "charset") handleCharset(child);
|
||||
else bundle.push(child);
|
||||
// For better output
|
||||
if (index === 0) child.parent = stmt;
|
||||
});
|
||||
} else imports.push(stmt);
|
||||
} else if (stmt.type === "nodes") {
|
||||
bundle.push(stmt);
|
||||
}
|
||||
});
|
||||
|
||||
return charset ? [charset, ...imports.concat(bundle)] : imports.concat(bundle)
|
||||
}
|
||||
|
||||
async function resolveImportId(result, stmt, options, state, postcss) {
|
||||
if (dataURL.isValid(stmt.uri)) {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
stmt.children = await loadImportContent(
|
||||
result,
|
||||
stmt,
|
||||
stmt.uri,
|
||||
options,
|
||||
state,
|
||||
postcss,
|
||||
);
|
||||
|
||||
return
|
||||
} else if (dataURL.isValid(stmt.from.slice(-1))) {
|
||||
// Data urls can't be used as a base url to resolve imports.
|
||||
throw stmt.node.error(
|
||||
`Unable to import '${stmt.uri}' from a stylesheet that is embedded in a data url`,
|
||||
)
|
||||
}
|
||||
|
||||
const atRule = stmt.node;
|
||||
let sourceFile;
|
||||
if (atRule.source?.input?.file) {
|
||||
sourceFile = atRule.source.input.file;
|
||||
}
|
||||
const base = sourceFile
|
||||
? path$1.dirname(atRule.source.input.file)
|
||||
: options.root;
|
||||
|
||||
const paths = [await options.resolve(stmt.uri, base, options, atRule)].flat();
|
||||
|
||||
// Ensure that each path is absolute:
|
||||
const resolved = await Promise.all(
|
||||
paths.map(file => {
|
||||
return !path$1.isAbsolute(file)
|
||||
? resolveId$1(file)
|
||||
: file
|
||||
}),
|
||||
);
|
||||
|
||||
// Add dependency messages:
|
||||
resolved.forEach(file => {
|
||||
result.messages.push({
|
||||
type: "dependency",
|
||||
plugin: "postcss-import",
|
||||
file,
|
||||
parent: sourceFile,
|
||||
});
|
||||
});
|
||||
|
||||
const importedContent = await Promise.all(
|
||||
resolved.map(file => {
|
||||
return loadImportContent(result, stmt, file, options, state, postcss)
|
||||
}),
|
||||
);
|
||||
|
||||
// Merge loaded statements
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
stmt.children = importedContent.flat().filter(x => !!x);
|
||||
}
|
||||
|
||||
async function loadImportContent(
|
||||
result,
|
||||
stmt,
|
||||
filename,
|
||||
options,
|
||||
state,
|
||||
postcss,
|
||||
) {
|
||||
const atRule = stmt.node;
|
||||
const { conditions, from } = stmt;
|
||||
const stmtDuplicateCheckKey = conditions
|
||||
.map(condition =>
|
||||
formatImportPrelude(condition.layer, condition.media, condition.supports),
|
||||
)
|
||||
.join(":");
|
||||
|
||||
if (options.skipDuplicates) {
|
||||
// skip files already imported at the same scope
|
||||
if (state.importedFiles[filename]?.[stmtDuplicateCheckKey]) {
|
||||
return
|
||||
}
|
||||
|
||||
// save imported files to skip them next time
|
||||
if (!state.importedFiles[filename]) {
|
||||
state.importedFiles[filename] = {};
|
||||
}
|
||||
state.importedFiles[filename][stmtDuplicateCheckKey] = true;
|
||||
}
|
||||
|
||||
if (from.includes(filename)) {
|
||||
return
|
||||
}
|
||||
|
||||
const content = await options.load(filename, options);
|
||||
|
||||
if (content.trim() === "" && options.warnOnEmpty) {
|
||||
result.warn(`${filename} is empty`, { node: atRule });
|
||||
return
|
||||
}
|
||||
|
||||
// skip previous imported files not containing @import rules
|
||||
if (
|
||||
options.skipDuplicates &&
|
||||
state.hashFiles[content]?.[stmtDuplicateCheckKey]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const importedResult = await processContent(
|
||||
result,
|
||||
content,
|
||||
filename,
|
||||
options,
|
||||
postcss,
|
||||
);
|
||||
|
||||
const styles = importedResult.root;
|
||||
result.messages = result.messages.concat(importedResult.messages);
|
||||
|
||||
if (options.skipDuplicates) {
|
||||
const hasImport = styles.some(child => {
|
||||
return child.type === "atrule" && child.name === "import"
|
||||
});
|
||||
if (!hasImport) {
|
||||
// save hash files to skip them next time
|
||||
if (!state.hashFiles[content]) {
|
||||
state.hashFiles[content] = {};
|
||||
}
|
||||
|
||||
state.hashFiles[content][stmtDuplicateCheckKey] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// recursion: import @import from imported file
|
||||
return parseStyles$1(
|
||||
result,
|
||||
styles,
|
||||
options,
|
||||
state,
|
||||
conditions,
|
||||
[...from, filename],
|
||||
postcss,
|
||||
)
|
||||
}
|
||||
|
||||
function isProcessableURL(uri) {
|
||||
// skip protocol base uri (protocol://url) or protocol-relative
|
||||
if (/^(?:[a-z]+:)?\/\//i.test(uri)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// check for fragment or query
|
||||
try {
|
||||
// needs a base to parse properly
|
||||
const url = new URL(uri, "https://example.com");
|
||||
if (url.search) {
|
||||
return false
|
||||
}
|
||||
} catch {} // Ignore
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var parseStyles_1 = parseStyles$1;
|
||||
|
||||
// builtin tooling
|
||||
const path = require$$0;
|
||||
|
||||
// internal tooling
|
||||
const applyConditions = applyConditions$1;
|
||||
const applyRaws = applyRaws$1;
|
||||
const applyStyles = applyStyles$1;
|
||||
const loadContent = loadContent$1;
|
||||
const parseStyles = parseStyles_1;
|
||||
const resolveId = (id) => id;
|
||||
|
||||
function AtImport(options) {
|
||||
options = {
|
||||
root: process.cwd(),
|
||||
path: [],
|
||||
skipDuplicates: true,
|
||||
resolve: resolveId,
|
||||
load: loadContent,
|
||||
plugins: [],
|
||||
addModulesDirectories: [],
|
||||
warnOnEmpty: true,
|
||||
...options,
|
||||
};
|
||||
|
||||
options.root = path.resolve(options.root);
|
||||
|
||||
// convert string to an array of a single element
|
||||
if (typeof options.path === "string") options.path = [options.path];
|
||||
|
||||
if (!Array.isArray(options.path)) options.path = [];
|
||||
|
||||
options.path = options.path.map(p => path.resolve(options.root, p));
|
||||
|
||||
return {
|
||||
postcssPlugin: "postcss-import",
|
||||
async Once(styles, { result, atRule, postcss }) {
|
||||
const state = {
|
||||
importedFiles: {},
|
||||
hashFiles: {},
|
||||
};
|
||||
|
||||
if (styles.source?.input?.file) {
|
||||
state.importedFiles[styles.source.input.file] = {};
|
||||
}
|
||||
|
||||
if (options.plugins && !Array.isArray(options.plugins)) {
|
||||
throw new Error("plugins option must be an array")
|
||||
}
|
||||
|
||||
const bundle = await parseStyles(
|
||||
result,
|
||||
styles,
|
||||
options,
|
||||
state,
|
||||
[],
|
||||
[],
|
||||
postcss,
|
||||
);
|
||||
|
||||
applyRaws(bundle);
|
||||
applyConditions(bundle, atRule);
|
||||
applyStyles(bundle, styles);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
AtImport.postcss = true;
|
||||
|
||||
var postcssImport = AtImport;
|
||||
|
||||
var index = /*@__PURE__*/getDefaultExportFromCjs(postcssImport);
|
||||
|
||||
var index$1 = /*#__PURE__*/_mergeNamespaces({
|
||||
__proto__: null,
|
||||
default: index
|
||||
}, [postcssImport]);
|
||||
|
||||
export { index$1 as i };
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
|
@ -1,21 +1,18 @@
|
|||
import path from 'node:path';
|
||||
import fs__default from 'node:fs';
|
||||
import fs from 'node:fs';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import { EventEmitter } from 'events';
|
||||
import { B as colors, v as createLogger, r as resolveConfig } from './chunks/dep-BK3b2jBa.js';
|
||||
import { A as colors, v as createLogger, r as resolveConfig } from './chunks/dep-p3C6MpSJ.js';
|
||||
import { VERSION } from './constants.js';
|
||||
import 'node:fs/promises';
|
||||
import 'node:url';
|
||||
import 'node:util';
|
||||
import 'node:module';
|
||||
import 'node:crypto';
|
||||
import 'tty';
|
||||
import 'path';
|
||||
import 'esbuild';
|
||||
import 'fs';
|
||||
import 'node:events';
|
||||
import 'node:stream';
|
||||
import 'node:string_decoder';
|
||||
import 'assert';
|
||||
import 'node:child_process';
|
||||
import 'node:http';
|
||||
import 'node:https';
|
||||
|
|
@ -27,6 +24,7 @@ import 'stream';
|
|||
import 'os';
|
||||
import 'child_process';
|
||||
import 'node:os';
|
||||
import 'node:crypto';
|
||||
import 'node:dns';
|
||||
import 'crypto';
|
||||
import 'module';
|
||||
|
|
@ -34,6 +32,7 @@ import 'node:assert';
|
|||
import 'node:v8';
|
||||
import 'node:worker_threads';
|
||||
import 'node:buffer';
|
||||
import 'node:events';
|
||||
import 'rollup/parseAst';
|
||||
import 'querystring';
|
||||
import 'node:readline';
|
||||
|
|
@ -41,8 +40,6 @@ import 'zlib';
|
|||
import 'buffer';
|
||||
import 'https';
|
||||
import 'tls';
|
||||
import 'node:net';
|
||||
import 'assert';
|
||||
import 'node:zlib';
|
||||
|
||||
function toArr(any) {
|
||||
|
|
@ -658,258 +655,269 @@ class CAC extends EventEmitter {
|
|||
|
||||
const cac = (name = "") => new CAC(name);
|
||||
|
||||
const cli = cac("vite");
|
||||
const cli = cac('vite');
|
||||
let profileSession = global.__vite_profile_session;
|
||||
let profileCount = 0;
|
||||
const stopProfiler = (log) => {
|
||||
if (!profileSession) return;
|
||||
return new Promise((res, rej) => {
|
||||
profileSession.post("Profiler.stop", (err, { profile }) => {
|
||||
if (!err) {
|
||||
const outPath = path.resolve(
|
||||
`./vite-profile-${profileCount++}.cpuprofile`
|
||||
);
|
||||
fs__default.writeFileSync(outPath, JSON.stringify(profile));
|
||||
log(
|
||||
colors.yellow(
|
||||
`CPU profile written to ${colors.white(colors.dim(outPath))}`
|
||||
)
|
||||
);
|
||||
profileSession = void 0;
|
||||
res();
|
||||
} else {
|
||||
rej(err);
|
||||
}
|
||||
if (!profileSession)
|
||||
return;
|
||||
return new Promise((res, rej) => {
|
||||
profileSession.post('Profiler.stop', (err, { profile }) => {
|
||||
// Write profile to disk, upload, etc.
|
||||
if (!err) {
|
||||
const outPath = path.resolve(`./vite-profile-${profileCount++}.cpuprofile`);
|
||||
fs.writeFileSync(outPath, JSON.stringify(profile));
|
||||
log(colors.yellow(`CPU profile written to ${colors.white(colors.dim(outPath))}`));
|
||||
profileSession = undefined;
|
||||
res();
|
||||
}
|
||||
else {
|
||||
rej(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
const filterDuplicateOptions = (options) => {
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
if (Array.isArray(value)) {
|
||||
options[key] = value[value.length - 1];
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
if (Array.isArray(value)) {
|
||||
options[key] = value[value.length - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* removing global flags before passing as command specific sub-configs
|
||||
*/
|
||||
function cleanOptions(options) {
|
||||
const ret = { ...options };
|
||||
delete ret["--"];
|
||||
delete ret.c;
|
||||
delete ret.config;
|
||||
delete ret.base;
|
||||
delete ret.l;
|
||||
delete ret.logLevel;
|
||||
delete ret.clearScreen;
|
||||
delete ret.d;
|
||||
delete ret.debug;
|
||||
delete ret.f;
|
||||
delete ret.filter;
|
||||
delete ret.m;
|
||||
delete ret.mode;
|
||||
if ("sourcemap" in ret) {
|
||||
const sourcemap = ret.sourcemap;
|
||||
ret.sourcemap = sourcemap === "true" ? true : sourcemap === "false" ? false : ret.sourcemap;
|
||||
}
|
||||
return ret;
|
||||
const ret = { ...options };
|
||||
delete ret['--'];
|
||||
delete ret.c;
|
||||
delete ret.config;
|
||||
delete ret.base;
|
||||
delete ret.l;
|
||||
delete ret.logLevel;
|
||||
delete ret.clearScreen;
|
||||
delete ret.d;
|
||||
delete ret.debug;
|
||||
delete ret.f;
|
||||
delete ret.filter;
|
||||
delete ret.m;
|
||||
delete ret.mode;
|
||||
// convert the sourcemap option to a boolean if necessary
|
||||
if ('sourcemap' in ret) {
|
||||
const sourcemap = ret.sourcemap;
|
||||
ret.sourcemap =
|
||||
sourcemap === 'true'
|
||||
? true
|
||||
: sourcemap === 'false'
|
||||
? false
|
||||
: ret.sourcemap;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* host may be a number (like 0), should convert to string
|
||||
*/
|
||||
const convertHost = (v) => {
|
||||
if (typeof v === "number") {
|
||||
return String(v);
|
||||
}
|
||||
return v;
|
||||
if (typeof v === 'number') {
|
||||
return String(v);
|
||||
}
|
||||
return v;
|
||||
};
|
||||
/**
|
||||
* base may be a number (like 0), should convert to empty string
|
||||
*/
|
||||
const convertBase = (v) => {
|
||||
if (v === 0) {
|
||||
return "";
|
||||
}
|
||||
return v;
|
||||
if (v === 0) {
|
||||
return '';
|
||||
}
|
||||
return v;
|
||||
};
|
||||
cli.option("-c, --config <file>", `[string] use specified config file`).option("--base <path>", `[string] public base path (default: /)`, {
|
||||
type: [convertBase]
|
||||
}).option("-l, --logLevel <level>", `[string] info | warn | error | silent`).option("--clearScreen", `[boolean] allow/disable clear screen when logging`).option("-d, --debug [feat]", `[string | boolean] show debug logs`).option("-f, --filter <filter>", `[string] filter debug logs`).option("-m, --mode <mode>", `[string] set env mode`);
|
||||
cli.command("[root]", "start dev server").alias("serve").alias("dev").option("--host [host]", `[string] specify hostname`, { type: [convertHost] }).option("--port <port>", `[number] specify port`).option("--open [path]", `[boolean | string] open browser on startup`).option("--cors", `[boolean] enable CORS`).option("--strictPort", `[boolean] exit if specified port is already in use`).option(
|
||||
"--force",
|
||||
`[boolean] force the optimizer to ignore the cache and re-bundle`
|
||||
).action(async (root, options) => {
|
||||
filterDuplicateOptions(options);
|
||||
const { createServer } = await import('./chunks/dep-BK3b2jBa.js').then(function (n) { return n.F; });
|
||||
try {
|
||||
const server = await createServer({
|
||||
root,
|
||||
base: options.base,
|
||||
mode: options.mode,
|
||||
configFile: options.config,
|
||||
logLevel: options.logLevel,
|
||||
clearScreen: options.clearScreen,
|
||||
optimizeDeps: { force: options.force },
|
||||
server: cleanOptions(options)
|
||||
});
|
||||
if (!server.httpServer) {
|
||||
throw new Error("HTTP server not available");
|
||||
}
|
||||
await server.listen();
|
||||
const info = server.config.logger.info;
|
||||
const viteStartTime = global.__vite_start_time ?? false;
|
||||
const startupDurationString = viteStartTime ? colors.dim(
|
||||
`ready in ${colors.reset(
|
||||
colors.bold(Math.ceil(performance.now() - viteStartTime))
|
||||
)} ms`
|
||||
) : "";
|
||||
const hasExistingLogs = process.stdout.bytesWritten > 0 || process.stderr.bytesWritten > 0;
|
||||
info(
|
||||
`
|
||||
${colors.green(
|
||||
`${colors.bold("VITE")} v${VERSION}`
|
||||
)} ${startupDurationString}
|
||||
`,
|
||||
{
|
||||
clear: !hasExistingLogs
|
||||
}
|
||||
);
|
||||
server.printUrls();
|
||||
const customShortcuts = [];
|
||||
if (profileSession) {
|
||||
customShortcuts.push({
|
||||
key: "p",
|
||||
description: "start/stop the profiler",
|
||||
async action(server2) {
|
||||
if (profileSession) {
|
||||
await stopProfiler(server2.config.logger.info);
|
||||
} else {
|
||||
const inspector = await import('node:inspector').then(
|
||||
(r) => r.default
|
||||
);
|
||||
await new Promise((res) => {
|
||||
profileSession = new inspector.Session();
|
||||
profileSession.connect();
|
||||
profileSession.post("Profiler.enable", () => {
|
||||
profileSession.post("Profiler.start", () => {
|
||||
server2.config.logger.info("Profiler started");
|
||||
res();
|
||||
});
|
||||
});
|
||||
cli
|
||||
.option('-c, --config <file>', `[string] use specified config file`)
|
||||
.option('--base <path>', `[string] public base path (default: /)`, {
|
||||
type: [convertBase],
|
||||
})
|
||||
.option('-l, --logLevel <level>', `[string] info | warn | error | silent`)
|
||||
.option('--clearScreen', `[boolean] allow/disable clear screen when logging`)
|
||||
.option('-d, --debug [feat]', `[string | boolean] show debug logs`)
|
||||
.option('-f, --filter <filter>', `[string] filter debug logs`)
|
||||
.option('-m, --mode <mode>', `[string] set env mode`);
|
||||
// dev
|
||||
cli
|
||||
.command('[root]', 'start dev server') // default command
|
||||
.alias('serve') // the command is called 'serve' in Vite's API
|
||||
.alias('dev') // alias to align with the script name
|
||||
.option('--host [host]', `[string] specify hostname`, { type: [convertHost] })
|
||||
.option('--port <port>', `[number] specify port`)
|
||||
.option('--open [path]', `[boolean | string] open browser on startup`)
|
||||
.option('--cors', `[boolean] enable CORS`)
|
||||
.option('--strictPort', `[boolean] exit if specified port is already in use`)
|
||||
.option('--force', `[boolean] force the optimizer to ignore the cache and re-bundle`)
|
||||
.action(async (root, options) => {
|
||||
filterDuplicateOptions(options);
|
||||
// output structure is preserved even after bundling so require()
|
||||
// is ok here
|
||||
const { createServer } = await import('./chunks/dep-p3C6MpSJ.js').then(function (n) { return n.E; });
|
||||
try {
|
||||
const server = await createServer({
|
||||
root,
|
||||
base: options.base,
|
||||
mode: options.mode,
|
||||
configFile: options.config,
|
||||
logLevel: options.logLevel,
|
||||
clearScreen: options.clearScreen,
|
||||
optimizeDeps: { force: options.force },
|
||||
server: cleanOptions(options),
|
||||
});
|
||||
if (!server.httpServer) {
|
||||
throw new Error('HTTP server not available');
|
||||
}
|
||||
await server.listen();
|
||||
const info = server.config.logger.info;
|
||||
const viteStartTime = global.__vite_start_time ?? false;
|
||||
const startupDurationString = viteStartTime
|
||||
? colors.dim(`ready in ${colors.reset(colors.bold(Math.ceil(performance.now() - viteStartTime)))} ms`)
|
||||
: '';
|
||||
const hasExistingLogs = process.stdout.bytesWritten > 0 || process.stderr.bytesWritten > 0;
|
||||
info(`\n ${colors.green(`${colors.bold('VITE')} v${VERSION}`)} ${startupDurationString}\n`, {
|
||||
clear: !hasExistingLogs,
|
||||
});
|
||||
server.printUrls();
|
||||
const customShortcuts = [];
|
||||
if (profileSession) {
|
||||
customShortcuts.push({
|
||||
key: 'p',
|
||||
description: 'start/stop the profiler',
|
||||
async action(server) {
|
||||
if (profileSession) {
|
||||
await stopProfiler(server.config.logger.info);
|
||||
}
|
||||
else {
|
||||
const inspector = await import('node:inspector').then((r) => r.default);
|
||||
await new Promise((res) => {
|
||||
profileSession = new inspector.Session();
|
||||
profileSession.connect();
|
||||
profileSession.post('Profiler.enable', () => {
|
||||
profileSession.post('Profiler.start', () => {
|
||||
server.config.logger.info('Profiler started');
|
||||
res();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
server.bindCLIShortcuts({ print: true, customShortcuts });
|
||||
}
|
||||
catch (e) {
|
||||
const logger = createLogger(options.logLevel);
|
||||
logger.error(colors.red(`error when starting dev server:\n${e.stack}`), {
|
||||
error: e,
|
||||
});
|
||||
stopProfiler(logger.info);
|
||||
process.exit(1);
|
||||
}
|
||||
server.bindCLIShortcuts({ print: true, customShortcuts });
|
||||
} catch (e) {
|
||||
const logger = createLogger(options.logLevel);
|
||||
logger.error(colors.red(`error when starting dev server:
|
||||
${e.stack}`), {
|
||||
error: e
|
||||
});
|
||||
stopProfiler(logger.info);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
cli.command("build [root]", "build for production").option("--target <target>", `[string] transpile target (default: 'modules')`).option("--outDir <dir>", `[string] output directory (default: dist)`).option(
|
||||
"--assetsDir <dir>",
|
||||
`[string] directory under outDir to place assets in (default: assets)`
|
||||
).option(
|
||||
"--assetsInlineLimit <number>",
|
||||
`[number] static asset base64 inline threshold in bytes (default: 4096)`
|
||||
).option(
|
||||
"--ssr [entry]",
|
||||
`[string] build specified entry for server-side rendering`
|
||||
).option(
|
||||
"--sourcemap [output]",
|
||||
`[boolean | "inline" | "hidden"] output source maps for build (default: false)`
|
||||
).option(
|
||||
"--minify [minifier]",
|
||||
`[boolean | "terser" | "esbuild"] enable/disable minification, or specify minifier to use (default: esbuild)`
|
||||
).option("--manifest [name]", `[boolean | string] emit build manifest json`).option("--ssrManifest [name]", `[boolean | string] emit ssr manifest json`).option(
|
||||
"--emptyOutDir",
|
||||
`[boolean] force empty outDir when it's outside of root`
|
||||
).option("-w, --watch", `[boolean] rebuilds when modules have changed on disk`).action(async (root, options) => {
|
||||
filterDuplicateOptions(options);
|
||||
const { build } = await import('./chunks/dep-BK3b2jBa.js').then(function (n) { return n.G; });
|
||||
const buildOptions = cleanOptions(options);
|
||||
try {
|
||||
await build({
|
||||
root,
|
||||
base: options.base,
|
||||
mode: options.mode,
|
||||
configFile: options.config,
|
||||
logLevel: options.logLevel,
|
||||
clearScreen: options.clearScreen,
|
||||
build: buildOptions
|
||||
});
|
||||
} catch (e) {
|
||||
createLogger(options.logLevel).error(
|
||||
colors.red(`error during build:
|
||||
${e.stack}`),
|
||||
{ error: e }
|
||||
);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
stopProfiler((message) => createLogger(options.logLevel).info(message));
|
||||
}
|
||||
// build
|
||||
cli
|
||||
.command('build [root]', 'build for production')
|
||||
.option('--target <target>', `[string] transpile target (default: 'modules')`)
|
||||
.option('--outDir <dir>', `[string] output directory (default: dist)`)
|
||||
.option('--assetsDir <dir>', `[string] directory under outDir to place assets in (default: assets)`)
|
||||
.option('--assetsInlineLimit <number>', `[number] static asset base64 inline threshold in bytes (default: 4096)`)
|
||||
.option('--ssr [entry]', `[string] build specified entry for server-side rendering`)
|
||||
.option('--sourcemap [output]', `[boolean | "inline" | "hidden"] output source maps for build (default: false)`)
|
||||
.option('--minify [minifier]', `[boolean | "terser" | "esbuild"] enable/disable minification, ` +
|
||||
`or specify minifier to use (default: esbuild)`)
|
||||
.option('--manifest [name]', `[boolean | string] emit build manifest json`)
|
||||
.option('--ssrManifest [name]', `[boolean | string] emit ssr manifest json`)
|
||||
.option('--emptyOutDir', `[boolean] force empty outDir when it's outside of root`)
|
||||
.option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`)
|
||||
.action(async (root, options) => {
|
||||
filterDuplicateOptions(options);
|
||||
const { build } = await import('./chunks/dep-p3C6MpSJ.js').then(function (n) { return n.F; });
|
||||
const buildOptions = cleanOptions(options);
|
||||
try {
|
||||
await build({
|
||||
root,
|
||||
base: options.base,
|
||||
mode: options.mode,
|
||||
configFile: options.config,
|
||||
logLevel: options.logLevel,
|
||||
clearScreen: options.clearScreen,
|
||||
build: buildOptions,
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
createLogger(options.logLevel).error(colors.red(`error during build:\n${e.stack}`), { error: e });
|
||||
process.exit(1);
|
||||
}
|
||||
finally {
|
||||
stopProfiler((message) => createLogger(options.logLevel).info(message));
|
||||
}
|
||||
});
|
||||
cli.command("optimize [root]", "pre-bundle dependencies").option(
|
||||
"--force",
|
||||
`[boolean] force the optimizer to ignore the cache and re-bundle`
|
||||
).action(
|
||||
async (root, options) => {
|
||||
// optimize
|
||||
cli
|
||||
.command('optimize [root]', 'pre-bundle dependencies')
|
||||
.option('--force', `[boolean] force the optimizer to ignore the cache and re-bundle`)
|
||||
.action(async (root, options) => {
|
||||
filterDuplicateOptions(options);
|
||||
const { optimizeDeps } = await import('./chunks/dep-BK3b2jBa.js').then(function (n) { return n.E; });
|
||||
const { optimizeDeps } = await import('./chunks/dep-p3C6MpSJ.js').then(function (n) { return n.D; });
|
||||
try {
|
||||
const config = await resolveConfig(
|
||||
{
|
||||
root,
|
||||
base: options.base,
|
||||
configFile: options.config,
|
||||
logLevel: options.logLevel,
|
||||
mode: options.mode
|
||||
},
|
||||
"serve"
|
||||
);
|
||||
await optimizeDeps(config, options.force, true);
|
||||
} catch (e) {
|
||||
createLogger(options.logLevel).error(
|
||||
colors.red(`error when optimizing deps:
|
||||
${e.stack}`),
|
||||
{ error: e }
|
||||
);
|
||||
process.exit(1);
|
||||
const config = await resolveConfig({
|
||||
root,
|
||||
base: options.base,
|
||||
configFile: options.config,
|
||||
logLevel: options.logLevel,
|
||||
mode: options.mode,
|
||||
}, 'serve');
|
||||
await optimizeDeps(config, options.force, true);
|
||||
}
|
||||
}
|
||||
);
|
||||
cli.command("preview [root]", "locally preview production build").option("--host [host]", `[string] specify hostname`, { type: [convertHost] }).option("--port <port>", `[number] specify port`).option("--strictPort", `[boolean] exit if specified port is already in use`).option("--open [path]", `[boolean | string] open browser on startup`).option("--outDir <dir>", `[string] output directory (default: dist)`).action(
|
||||
async (root, options) => {
|
||||
catch (e) {
|
||||
createLogger(options.logLevel).error(colors.red(`error when optimizing deps:\n${e.stack}`), { error: e });
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
// preview
|
||||
cli
|
||||
.command('preview [root]', 'locally preview production build')
|
||||
.option('--host [host]', `[string] specify hostname`, { type: [convertHost] })
|
||||
.option('--port <port>', `[number] specify port`)
|
||||
.option('--strictPort', `[boolean] exit if specified port is already in use`)
|
||||
.option('--open [path]', `[boolean | string] open browser on startup`)
|
||||
.option('--outDir <dir>', `[string] output directory (default: dist)`)
|
||||
.action(async (root, options) => {
|
||||
filterDuplicateOptions(options);
|
||||
const { preview } = await import('./chunks/dep-BK3b2jBa.js').then(function (n) { return n.H; });
|
||||
const { preview } = await import('./chunks/dep-p3C6MpSJ.js').then(function (n) { return n.G; });
|
||||
try {
|
||||
const server = await preview({
|
||||
root,
|
||||
base: options.base,
|
||||
configFile: options.config,
|
||||
logLevel: options.logLevel,
|
||||
mode: options.mode,
|
||||
build: {
|
||||
outDir: options.outDir
|
||||
},
|
||||
preview: {
|
||||
port: options.port,
|
||||
strictPort: options.strictPort,
|
||||
host: options.host,
|
||||
open: options.open
|
||||
}
|
||||
});
|
||||
server.printUrls();
|
||||
server.bindCLIShortcuts({ print: true });
|
||||
} catch (e) {
|
||||
createLogger(options.logLevel).error(
|
||||
colors.red(`error when starting preview server:
|
||||
${e.stack}`),
|
||||
{ error: e }
|
||||
);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
stopProfiler((message) => createLogger(options.logLevel).info(message));
|
||||
const server = await preview({
|
||||
root,
|
||||
base: options.base,
|
||||
configFile: options.config,
|
||||
logLevel: options.logLevel,
|
||||
mode: options.mode,
|
||||
build: {
|
||||
outDir: options.outDir,
|
||||
},
|
||||
preview: {
|
||||
port: options.port,
|
||||
strictPort: options.strictPort,
|
||||
host: options.host,
|
||||
open: options.open,
|
||||
},
|
||||
});
|
||||
server.printUrls();
|
||||
server.bindCLIShortcuts({ print: true });
|
||||
}
|
||||
}
|
||||
);
|
||||
catch (e) {
|
||||
createLogger(options.logLevel).error(colors.red(`error when starting preview server:\n${e.stack}`), { error: e });
|
||||
process.exit(1);
|
||||
}
|
||||
finally {
|
||||
stopProfiler((message) => createLogger(options.logLevel).info(message));
|
||||
}
|
||||
});
|
||||
cli.help();
|
||||
cli.version(VERSION);
|
||||
cli.parse();
|
||||
|
|
|
|||
|
|
@ -2,112 +2,114 @@ import path, { resolve } from 'node:path';
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const { version } = JSON.parse(
|
||||
readFileSync(new URL("../../package.json", import.meta.url)).toString()
|
||||
);
|
||||
const { version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url)).toString());
|
||||
const VERSION = version;
|
||||
const DEFAULT_MAIN_FIELDS = [
|
||||
"browser",
|
||||
"module",
|
||||
"jsnext:main",
|
||||
// moment still uses this...
|
||||
"jsnext"
|
||||
'browser',
|
||||
'module',
|
||||
'jsnext:main',
|
||||
'jsnext',
|
||||
];
|
||||
// Baseline support browserslist
|
||||
// "defaults and supports es6-module and supports es6-module-dynamic-import"
|
||||
// Higher browser versions may be needed for extra features.
|
||||
const ESBUILD_MODULES_TARGET = [
|
||||
"es2020",
|
||||
// support import.meta.url
|
||||
"edge88",
|
||||
"firefox78",
|
||||
"chrome87",
|
||||
"safari14"
|
||||
'es2020',
|
||||
'edge88',
|
||||
'firefox78',
|
||||
'chrome87',
|
||||
'safari14',
|
||||
];
|
||||
const DEFAULT_EXTENSIONS = [
|
||||
".mjs",
|
||||
".js",
|
||||
".mts",
|
||||
".ts",
|
||||
".jsx",
|
||||
".tsx",
|
||||
".json"
|
||||
'.mjs',
|
||||
'.js',
|
||||
'.mts',
|
||||
'.ts',
|
||||
'.jsx',
|
||||
'.tsx',
|
||||
'.json',
|
||||
];
|
||||
const DEFAULT_CONFIG_FILES = [
|
||||
"vite.config.js",
|
||||
"vite.config.mjs",
|
||||
"vite.config.ts",
|
||||
"vite.config.cjs",
|
||||
"vite.config.mts",
|
||||
"vite.config.cts"
|
||||
'vite.config.js',
|
||||
'vite.config.mjs',
|
||||
'vite.config.ts',
|
||||
'vite.config.cjs',
|
||||
'vite.config.mts',
|
||||
'vite.config.cts',
|
||||
];
|
||||
const JS_TYPES_RE = /\.(?:j|t)sx?$|\.mjs$/;
|
||||
const CSS_LANGS_RE = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/;
|
||||
const OPTIMIZABLE_ENTRY_RE = /\.[cm]?[jt]s$/;
|
||||
const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/;
|
||||
/**
|
||||
* Prefix for resolved fs paths, since windows paths may not be valid as URLs.
|
||||
*/
|
||||
const FS_PREFIX = `/@fs/`;
|
||||
const CLIENT_PUBLIC_PATH = `/@vite/client`;
|
||||
const ENV_PUBLIC_PATH = `/@vite/env`;
|
||||
const VITE_PACKAGE_DIR = resolve(
|
||||
// import.meta.url is `dist/node/constants.js` after bundle
|
||||
fileURLToPath(import.meta.url),
|
||||
"../../.."
|
||||
);
|
||||
const CLIENT_ENTRY = resolve(VITE_PACKAGE_DIR, "dist/client/client.mjs");
|
||||
const ENV_ENTRY = resolve(VITE_PACKAGE_DIR, "dist/client/env.mjs");
|
||||
// import.meta.url is `dist/node/constants.js` after bundle
|
||||
fileURLToPath(import.meta.url), '../../..');
|
||||
const CLIENT_ENTRY = resolve(VITE_PACKAGE_DIR, 'dist/client/client.mjs');
|
||||
const ENV_ENTRY = resolve(VITE_PACKAGE_DIR, 'dist/client/env.mjs');
|
||||
const CLIENT_DIR = path.dirname(CLIENT_ENTRY);
|
||||
// ** READ THIS ** before editing `KNOWN_ASSET_TYPES`.
|
||||
// If you add an asset to `KNOWN_ASSET_TYPES`, make sure to also add it
|
||||
// to the TypeScript declaration file `packages/vite/client.d.ts` and
|
||||
// add a mime type to the `registerCustomMime` in
|
||||
// `packages/vite/src/node/plugin/assets.ts` if mime type cannot be
|
||||
// looked up by mrmime.
|
||||
const KNOWN_ASSET_TYPES = [
|
||||
// images
|
||||
"apng",
|
||||
"bmp",
|
||||
"png",
|
||||
"jpe?g",
|
||||
"jfif",
|
||||
"pjpeg",
|
||||
"pjp",
|
||||
"gif",
|
||||
"svg",
|
||||
"ico",
|
||||
"webp",
|
||||
"avif",
|
||||
// media
|
||||
"mp4",
|
||||
"webm",
|
||||
"ogg",
|
||||
"mp3",
|
||||
"wav",
|
||||
"flac",
|
||||
"aac",
|
||||
"opus",
|
||||
"mov",
|
||||
"m4a",
|
||||
"vtt",
|
||||
// fonts
|
||||
"woff2?",
|
||||
"eot",
|
||||
"ttf",
|
||||
"otf",
|
||||
// other
|
||||
"webmanifest",
|
||||
"pdf",
|
||||
"txt"
|
||||
// images
|
||||
'apng',
|
||||
'png',
|
||||
'jpe?g',
|
||||
'jfif',
|
||||
'pjpeg',
|
||||
'pjp',
|
||||
'gif',
|
||||
'svg',
|
||||
'ico',
|
||||
'webp',
|
||||
'avif',
|
||||
// media
|
||||
'mp4',
|
||||
'webm',
|
||||
'ogg',
|
||||
'mp3',
|
||||
'wav',
|
||||
'flac',
|
||||
'aac',
|
||||
'opus',
|
||||
'mov',
|
||||
'm4a',
|
||||
'vtt',
|
||||
// fonts
|
||||
'woff2?',
|
||||
'eot',
|
||||
'ttf',
|
||||
'otf',
|
||||
// other
|
||||
'webmanifest',
|
||||
'pdf',
|
||||
'txt',
|
||||
];
|
||||
const DEFAULT_ASSETS_RE = new RegExp(
|
||||
`\\.(` + KNOWN_ASSET_TYPES.join("|") + `)(\\?.*)?$`
|
||||
);
|
||||
const DEFAULT_ASSETS_RE = new RegExp(`\\.(` + KNOWN_ASSET_TYPES.join('|') + `)(\\?.*)?$`);
|
||||
const DEP_VERSION_RE = /[?&](v=[\w.-]+)\b/;
|
||||
const loopbackHosts = /* @__PURE__ */ new Set([
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
"::1",
|
||||
"0000:0000:0000:0000:0000:0000:0000:0001"
|
||||
const loopbackHosts = new Set([
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
'::1',
|
||||
'0000:0000:0000:0000:0000:0000:0000:0001',
|
||||
]);
|
||||
const wildcardHosts = /* @__PURE__ */ new Set([
|
||||
"0.0.0.0",
|
||||
"::",
|
||||
"0000:0000:0000:0000:0000:0000:0000:0000"
|
||||
const wildcardHosts = new Set([
|
||||
'0.0.0.0',
|
||||
'::',
|
||||
'0000:0000:0000:0000:0000:0000:0000:0000',
|
||||
]);
|
||||
const DEFAULT_DEV_PORT = 5173;
|
||||
const DEFAULT_PREVIEW_PORT = 4173;
|
||||
const DEFAULT_ASSETS_INLINE_LIMIT = 4096;
|
||||
const defaultAllowedOrigins = /^https?:\/\/(?:(?:[^:]+\.)?localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/;
|
||||
const METADATA_FILENAME = "_metadata.json";
|
||||
const METADATA_FILENAME = '_metadata.json';
|
||||
|
||||
export { CLIENT_DIR, CLIENT_ENTRY, CLIENT_PUBLIC_PATH, CSS_LANGS_RE, DEFAULT_ASSETS_INLINE_LIMIT, DEFAULT_ASSETS_RE, DEFAULT_CONFIG_FILES, DEFAULT_DEV_PORT, DEFAULT_EXTENSIONS, DEFAULT_MAIN_FIELDS, DEFAULT_PREVIEW_PORT, DEP_VERSION_RE, ENV_ENTRY, ENV_PUBLIC_PATH, ESBUILD_MODULES_TARGET, FS_PREFIX, JS_TYPES_RE, KNOWN_ASSET_TYPES, METADATA_FILENAME, OPTIMIZABLE_ENTRY_RE, SPECIAL_QUERY_RE, VERSION, VITE_PACKAGE_DIR, defaultAllowedOrigins, loopbackHosts, wildcardHosts };
|
||||
export { CLIENT_DIR, CLIENT_ENTRY, CLIENT_PUBLIC_PATH, CSS_LANGS_RE, DEFAULT_ASSETS_INLINE_LIMIT, DEFAULT_ASSETS_RE, DEFAULT_CONFIG_FILES, DEFAULT_DEV_PORT, DEFAULT_EXTENSIONS, DEFAULT_MAIN_FIELDS, DEFAULT_PREVIEW_PORT, DEP_VERSION_RE, ENV_ENTRY, ENV_PUBLIC_PATH, ESBUILD_MODULES_TARGET, FS_PREFIX, JS_TYPES_RE, KNOWN_ASSET_TYPES, METADATA_FILENAME, OPTIMIZABLE_ENTRY_RE, SPECIAL_QUERY_RE, VERSION, VITE_PACKAGE_DIR, loopbackHosts, wildcardHosts };
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/// <reference types="node" />
|
||||
import { PluginHooks, RollupError, SourceMap, ModuleInfo, PartialResolvedId, MinimalPluginContext, InputOptions, CustomPluginOptions, LoadResult, SourceDescription, RollupOptions, WatcherOptions, InputOption, ModuleFormat, RollupOutput, RollupWatcher, SourceMapInput, ExistingRawSourceMap, OutputBundle, OutputChunk, ObjectHook, PluginContext, ResolveIdResult, TransformPluginContext, GetManualChunk } from 'rollup';
|
||||
import { PluginHooks, RollupError, SourceMap, ModuleInfo, PartialResolvedId, InputOptions, CustomPluginOptions, SourceDescription, LoadResult, RollupOptions, WatcherOptions, InputOption, ModuleFormat, RollupOutput, RollupWatcher, SourceMapInput, ExistingRawSourceMap, OutputBundle, OutputChunk, ObjectHook, PluginContext, ResolveIdResult, TransformPluginContext, GetManualChunk } from 'rollup';
|
||||
import * as rollup from 'rollup';
|
||||
export { rollup as Rollup };
|
||||
export { parseAst, parseAstAsync } from 'rollup/parseAst';
|
||||
|
|
@ -414,7 +414,6 @@ declare namespace HttpProxy {
|
|||
* @param req - Client request.
|
||||
* @param res - Client response.
|
||||
* @param options - Additional options.
|
||||
* @param callback - Error callback.
|
||||
*/
|
||||
web(
|
||||
req: http.IncomingMessage,
|
||||
|
|
@ -429,7 +428,6 @@ declare namespace HttpProxy {
|
|||
* @param socket - Client socket.
|
||||
* @param head - Client head.
|
||||
* @param options - Additional options.
|
||||
* @param callback - Error callback.
|
||||
*/
|
||||
ws(
|
||||
req: http.IncomingMessage,
|
||||
|
|
@ -620,12 +618,6 @@ interface ProxyOptions extends HttpProxy.ServerOptions {
|
|||
* webpack-dev-server style bypass function
|
||||
*/
|
||||
bypass?: (req: http.IncomingMessage, res: http.ServerResponse, options: ProxyOptions) => void | null | undefined | false | string;
|
||||
/**
|
||||
* rewrite the Origin header of a WebSocket request to match the the target
|
||||
*
|
||||
* **Exercise caution as rewriting the Origin can leave the proxying open to [CSRF attacks](https://owasp.org/www-community/attacks/csrf).**
|
||||
*/
|
||||
rewriteWsOrigin?: boolean | undefined;
|
||||
}
|
||||
|
||||
type LogType = 'error' | 'warn' | 'info';
|
||||
|
|
@ -669,18 +661,6 @@ interface CommonServerOptions {
|
|||
* Set to 0.0.0.0 to listen on all addresses, including LAN and public addresses.
|
||||
*/
|
||||
host?: string | boolean;
|
||||
/**
|
||||
* The hostnames that Vite is allowed to respond to.
|
||||
* `localhost` and subdomains under `.localhost` and all IP addresses are allowed by default.
|
||||
* When using HTTPS, this check is skipped.
|
||||
*
|
||||
* If a string starts with `.`, it will allow that hostname without the `.` and all subdomains under the hostname.
|
||||
* For example, `.example.com` will allow `example.com`, `foo.example.com`, and `foo.bar.example.com`.
|
||||
*
|
||||
* If set to `true`, the server is allowed to respond to requests for any hosts.
|
||||
* This is not recommended as it will be vulnerable to DNS rebinding attacks.
|
||||
*/
|
||||
allowedHosts?: string[] | true;
|
||||
/**
|
||||
* Enable TLS + HTTP/2.
|
||||
* Note: this downgrades to TLS only when the proxy option is also used.
|
||||
|
|
@ -716,14 +696,8 @@ interface CommonServerOptions {
|
|||
/**
|
||||
* Configure CORS for the dev server.
|
||||
* Uses https://github.com/expressjs/cors.
|
||||
*
|
||||
* When enabling this option, **we recommend setting a specific value
|
||||
* rather than `true`** to avoid exposing the source code to untrusted origins.
|
||||
*
|
||||
* Set to `true` to allow all methods from any origin, or configure separately
|
||||
* using an object.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
cors?: CorsOptions | boolean;
|
||||
/**
|
||||
|
|
@ -735,13 +709,7 @@ interface CommonServerOptions {
|
|||
* https://github.com/expressjs/cors#configuration-options
|
||||
*/
|
||||
interface CorsOptions {
|
||||
/**
|
||||
* Configures the Access-Control-Allow-Origin CORS header.
|
||||
*
|
||||
* **We recommend setting a specific value rather than
|
||||
* `true`** to avoid exposing the source code to untrusted origins.
|
||||
*/
|
||||
origin?: CorsOrigin | ((origin: string | undefined, cb: (err: Error, origins: CorsOrigin) => void) => void);
|
||||
origin?: CorsOrigin | ((origin: string, cb: (err: Error, origins: CorsOrigin) => void) => void);
|
||||
methods?: string | string[];
|
||||
allowedHeaders?: string | string[];
|
||||
exposedHeaders?: string | string[];
|
||||
|
|
@ -905,50 +873,29 @@ declare class ModuleGraph {
|
|||
* https://github.com/preactjs/wmr/blob/main/packages/wmr/src/lib/rollup-plugin-container.js
|
||||
*/
|
||||
|
||||
declare class PluginContainer {
|
||||
config: ResolvedConfig;
|
||||
moduleGraph?: ModuleGraph | undefined;
|
||||
watcher?: FSWatcher | undefined;
|
||||
plugins: readonly Plugin<any>[];
|
||||
private _pluginContextMap;
|
||||
private _pluginContextMapSsr;
|
||||
private _resolvedRollupOptions?;
|
||||
private _processesing;
|
||||
private _seenResolves;
|
||||
private _closed;
|
||||
private _moduleNodeToLoadAddedImports;
|
||||
getSortedPluginHooks: PluginHookUtils['getSortedPluginHooks'];
|
||||
getSortedPlugins: PluginHookUtils['getSortedPlugins'];
|
||||
watchFiles: Set<string>;
|
||||
minimalContext: MinimalPluginContext;
|
||||
private _updateModuleLoadAddedImports;
|
||||
private _getAddedImports;
|
||||
interface PluginContainer {
|
||||
options: InputOptions;
|
||||
getModuleInfo(id: string): ModuleInfo | null;
|
||||
private handleHookPromise;
|
||||
get options(): InputOptions;
|
||||
resolveRollupOptions(): Promise<InputOptions>;
|
||||
private _getPluginContext;
|
||||
private hookParallel;
|
||||
buildStart(_options?: InputOptions): Promise<void>;
|
||||
resolveId(rawId: string, importer?: string | undefined, options?: {
|
||||
buildStart(options: InputOptions): Promise<void>;
|
||||
resolveId(id: string, importer?: string, options?: {
|
||||
attributes?: Record<string, string>;
|
||||
custom?: CustomPluginOptions;
|
||||
skip?: Set<Plugin>;
|
||||
ssr?: boolean;
|
||||
isEntry?: boolean;
|
||||
}): Promise<PartialResolvedId | null>;
|
||||
load(id: string, options?: {
|
||||
ssr?: boolean;
|
||||
}): Promise<LoadResult | null>;
|
||||
transform(code: string, id: string, options?: {
|
||||
ssr?: boolean;
|
||||
inMap?: SourceDescription['map'];
|
||||
ssr?: boolean;
|
||||
}): Promise<{
|
||||
code: string;
|
||||
map: SourceMap | {
|
||||
mappings: '';
|
||||
} | null;
|
||||
}>;
|
||||
load(id: string, options?: {
|
||||
ssr?: boolean;
|
||||
}): Promise<LoadResult | null>;
|
||||
watchChange(id: string, change: {
|
||||
event: 'create' | 'update' | 'delete';
|
||||
}): Promise<void>;
|
||||
|
|
@ -960,7 +907,6 @@ declare class PluginContainer {
|
|||
|
||||
declare const WebSocketAlias: typeof WebSocket
|
||||
interface WebSocketAlias extends WebSocket {}
|
||||
|
||||
// WebSocket socket.
|
||||
declare class WebSocket extends EventEmitter {
|
||||
/** The connection is not yet open. */
|
||||
|
|
@ -1229,6 +1175,7 @@ declare class WebSocket extends EventEmitter {
|
|||
listener: (...args: any[]) => void,
|
||||
): this
|
||||
}
|
||||
// tslint:disable-line no-empty-interface
|
||||
|
||||
declare namespace WebSocket {
|
||||
/**
|
||||
|
|
@ -1478,9 +1425,9 @@ declare namespace WebSocket {
|
|||
}
|
||||
|
||||
const WebSocketServer: typeof Server
|
||||
interface WebSocketServer extends Server {}
|
||||
interface WebSocketServer extends Server {} // tslint:disable-line no-empty-interface
|
||||
const WebSocket: typeof WebSocketAlias
|
||||
interface WebSocket extends WebSocketAlias {}
|
||||
interface WebSocket extends WebSocketAlias {} // tslint:disable-line no-empty-interface
|
||||
|
||||
// WebSocket stream
|
||||
function createWebSocketStream(
|
||||
|
|
@ -1497,7 +1444,7 @@ interface HmrOptions {
|
|||
path?: string;
|
||||
timeout?: number;
|
||||
overlay?: boolean;
|
||||
server?: HttpServer;
|
||||
server?: Server;
|
||||
}
|
||||
interface HmrContext {
|
||||
file: string;
|
||||
|
|
@ -1613,11 +1560,6 @@ interface ServerOptions extends CommonServerOptions {
|
|||
* Configure HMR-specific options (port, host, path & protocol)
|
||||
*/
|
||||
hmr?: HmrOptions | boolean;
|
||||
/**
|
||||
* Do not start the websocket connection.
|
||||
* @experimental
|
||||
*/
|
||||
ws?: false;
|
||||
/**
|
||||
* Warm-up files to transform and cache the results in advance. This improves the
|
||||
* initial page load during server starts and prevents transform waterfalls.
|
||||
|
|
@ -1647,7 +1589,7 @@ interface ServerOptions extends CommonServerOptions {
|
|||
*
|
||||
* This is needed to proxy WebSocket connections to the parent server.
|
||||
*/
|
||||
server: HttpServer;
|
||||
server: http.Server;
|
||||
};
|
||||
/**
|
||||
* Options for files served via '/\@fs/'.
|
||||
|
|
@ -2122,9 +2064,6 @@ interface RollupDynamicImportVarsOptions {
|
|||
declare namespace Terser {
|
||||
export type ECMA = 5 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020
|
||||
|
||||
export type ConsoleProperty = keyof typeof console
|
||||
type DropConsoleOption = boolean | ConsoleProperty[]
|
||||
|
||||
export interface ParseOptions {
|
||||
bare_returns?: boolean
|
||||
/** @deprecated legacy option. Currently, all supported EcmaScript is valid to parse. */
|
||||
|
|
@ -2145,7 +2084,7 @@ declare namespace Terser {
|
|||
dead_code?: boolean
|
||||
defaults?: boolean
|
||||
directives?: boolean
|
||||
drop_console?: DropConsoleOption
|
||||
drop_console?: boolean
|
||||
drop_debugger?: boolean
|
||||
ecma?: ECMA
|
||||
evaluate?: boolean
|
||||
|
|
@ -2168,7 +2107,6 @@ declare namespace Terser {
|
|||
passes?: number
|
||||
properties?: boolean
|
||||
pure_funcs?: string[]
|
||||
pure_new?: boolean
|
||||
pure_getters?: boolean | 'strict'
|
||||
reduce_funcs?: boolean
|
||||
reduce_vars?: boolean
|
||||
|
|
@ -2218,7 +2156,7 @@ declare namespace Terser {
|
|||
* Obtains the nth most favored (usually shortest) identifier to rename a variable to.
|
||||
* The mangler will increment n and retry until the return value is not in use in scope, and is not a reserved word.
|
||||
* This function is expected to be stable; Evaluating get(n) === get(n) should always return true.
|
||||
* @param n The ordinal of the identifier.
|
||||
* @param n - The ordinal of the identifier.
|
||||
*/
|
||||
get(n: number): string
|
||||
}
|
||||
|
|
@ -2230,8 +2168,8 @@ declare namespace Terser {
|
|||
/**
|
||||
* Modifies the internal weighting of the input characters by the specified delta.
|
||||
* Will be invoked on the entire printed AST, and then deduct mangleable identifiers.
|
||||
* @param chars The characters to modify the weighting of.
|
||||
* @param delta The numeric weight to add to the characters.
|
||||
* @param chars - The characters to modify the weighting of.
|
||||
* @param delta - The numeric weight to add to the characters.
|
||||
*/
|
||||
consider(chars: string, delta: number): number
|
||||
/**
|
||||
|
|
@ -2314,7 +2252,7 @@ declare namespace Terser {
|
|||
module?: boolean
|
||||
nameCache?: object
|
||||
format?: FormatOptions
|
||||
/** @deprecated */
|
||||
/** @deprecated deprecated */
|
||||
output?: FormatOptions
|
||||
parse?: ParseOptions
|
||||
safari10?: boolean
|
||||
|
|
@ -2334,7 +2272,6 @@ declare namespace Terser {
|
|||
includeSources?: boolean
|
||||
filename?: string
|
||||
root?: string
|
||||
asObject?: boolean
|
||||
url?: string | 'inline'
|
||||
}
|
||||
}
|
||||
|
|
@ -2556,7 +2493,7 @@ interface LibraryOptions {
|
|||
*/
|
||||
fileName?: string | ((format: ModuleFormat, entryName: string) => string);
|
||||
}
|
||||
type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife' | 'system';
|
||||
type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife';
|
||||
interface ModulePreloadOptions {
|
||||
/**
|
||||
* Whether to inject a module preload polyfill.
|
||||
|
|
@ -2687,7 +2624,7 @@ interface DepOptimizationConfig {
|
|||
* When enabled, it will hold the first optimized deps results until all static
|
||||
* imports are crawled on cold start. This avoids the need for full-page reloads
|
||||
* when new dependencies are discovered and they trigger the generation of new
|
||||
* common chunks. If all dependencies are found by the scanner plus the explicitly
|
||||
* common chunks. If all dependencies are found by the scanner plus the explicitely
|
||||
* defined ones in `include`, it is better to disable this option to let the
|
||||
* browser process more requests in parallel.
|
||||
* @default true
|
||||
|
|
@ -2923,7 +2860,6 @@ type LightningCSSOptions = {
|
|||
pseudoClasses?: PseudoClasses
|
||||
unusedSymbols?: string[]
|
||||
cssModules?: CSSModulesConfig
|
||||
errorRecovery?: boolean
|
||||
}
|
||||
|
||||
interface CSSOptions {
|
||||
|
|
@ -3426,18 +3362,6 @@ interface LegacyOptions {
|
|||
* https://github.com/vitejs/vite/discussions/14697.
|
||||
*/
|
||||
proxySsrExternalModules?: boolean;
|
||||
/**
|
||||
* In Vite 6.0.8 / 5.4.11 and below, WebSocket server was able to connect from any web pages. However,
|
||||
* that could be exploited by a malicious web page.
|
||||
*
|
||||
* In Vite 6.0.9+ / 5.4.12+, the WebSocket server now requires a token to connect from a web page.
|
||||
* But this may break some plugins and frameworks that connects to the WebSocket server
|
||||
* on their own. Enabling this option will make Vite skip the token check.
|
||||
*
|
||||
* **We do not recommend enabling this option unless you are sure that you are fine with
|
||||
* that security weakness.**
|
||||
*/
|
||||
skipWebSocketTokenCheck?: boolean;
|
||||
}
|
||||
interface ResolvedWorkerOptions {
|
||||
format: 'es' | 'iife';
|
||||
|
|
@ -3479,17 +3403,6 @@ type ResolvedConfig = Readonly<Omit<UserConfig, 'plugins' | 'css' | 'assetsInclu
|
|||
worker: ResolvedWorkerOptions;
|
||||
appType: AppType;
|
||||
experimental: ExperimentalOptions;
|
||||
/**
|
||||
* The token to connect to the WebSocket server from browsers.
|
||||
*
|
||||
* We recommend using `import.meta.hot` rather than connecting
|
||||
* to the WebSocket server directly.
|
||||
* If you have a usecase that requires connecting to the WebSocket
|
||||
* server, please create an issue so that we can discuss.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
webSocketToken: string;
|
||||
} & PluginHookUtils>;
|
||||
interface PluginHookUtils {
|
||||
getSortedPlugins: <K extends keyof Plugin>(hookName: K) => PluginWithRequiredHook<K>[];
|
||||
|
|
@ -3567,10 +3480,8 @@ declare function searchForWorkspaceRoot(current: string, root?: string): string;
|
|||
|
||||
/**
|
||||
* Check if the url is allowed to be served, via the `server.fs` config.
|
||||
* @deprecated Use the `isFileLoadingAllowed` function instead.
|
||||
*/
|
||||
declare function isFileServingAllowed(url: string, server: ViteDevServer): boolean;
|
||||
declare function isFileLoadingAllowed(server: ViteDevServer, filePath: string): boolean;
|
||||
|
||||
declare function loadEnv(mode: string, envDir: string, prefixes?: string | string[]): Record<string, string>;
|
||||
declare function resolveEnvPrefix({ envPrefix, }: UserConfig): string[];
|
||||
|
|
@ -3624,4 +3535,4 @@ declare class ServerHMRConnector implements HMRRuntimeConnection {
|
|||
onUpdate(handler: (payload: HMRPayload) => void): void;
|
||||
}
|
||||
|
||||
export { type Alias, type AliasOptions, type AnymatchFn, type AnymatchPattern, type AppType, type AwaitWriteFinishOptions, type BindCLIShortcutsOptions, type BuildOptions, type CLIShortcut, type CSSModulesOptions, type CSSOptions, type CommonServerOptions, type ConfigEnv, Connect, type CorsOptions, type CorsOrigin, type DepOptimizationConfig, type DepOptimizationMetadata, type DepOptimizationOptions, type ESBuildOptions, type ESBuildTransformResult, type ExperimentalOptions, type ExportsData, FSWatcher, type FetchModuleOptions, type FileSystemServeOptions, type FilterPattern, type HMRBroadcaster, type HMRBroadcasterClient, type HMRChannel, type HTMLOptions, type HmrContext, type HmrOptions, type HookHandler, type HtmlTagDescriptor, HttpProxy, type HttpServer, type IndexHtmlTransform, type IndexHtmlTransformContext, type IndexHtmlTransformHook, type IndexHtmlTransformResult, type InlineConfig, type InternalResolveOptions, type JsonOptions, type LegacyOptions, type LibraryFormats, type LibraryOptions, type LightningCSSOptions, type LogErrorOptions, type LogLevel, type LogOptions, type LogType, type Logger, type LoggerOptions, type MainThreadRuntimeOptions, type Manifest, type ManifestChunk, type MapToFunction, type AnymatchMatcher as Matcher, ModuleGraph, ModuleNode, type ModulePreloadOptions, type OptimizedDepInfo, type Plugin, PluginContainer, type PluginHookUtils, type PluginOption, type PreprocessCSSResult, type PreviewOptions, type PreviewServer, type PreviewServerHook, type ProxyOptions, type RenderBuiltAssetUrl, type ResolveFn, type ResolveModulePreloadDependenciesFn, type ResolveOptions, type ResolvedBuildOptions, type ResolvedCSSOptions, type ResolvedConfig, type ResolvedModulePreloadOptions, type ResolvedPreviewOptions, type ResolvedSSROptions, type ResolvedServerOptions, type ResolvedServerUrls, type ResolvedUrl, type ResolvedWorkerOptions, type ResolverFunction, type ResolverObject, type RollupCommonJSOptions, type RollupDynamicImportVarsOptions, type SSROptions, type SSRTarget, type SendOptions, type ServerHMRChannel, ServerHMRConnector, type ServerHook, type ServerOptions, SplitVendorChunkCache, type SsrDepOptimizationOptions, Terser, type TerserOptions, type TransformOptions, type TransformResult, type UserConfig, type UserConfigExport, type UserConfigFn, type UserConfigFnObject, type UserConfigFnPromise, type ViteDevServer, type WatchOptions, WebSocket, WebSocketAlias, type WebSocketClient, type WebSocketCustomListener, WebSocketServer, build, buildErrorMessage, createFilter, createLogger, createServer, createViteRuntime, defineConfig, fetchModule, formatPostcssSourceMap, isCSSRequest, isFileLoadingAllowed, isFileServingAllowed, loadConfigFromFile, loadEnv, mergeAlias, mergeConfig, normalizePath, optimizeDeps, preprocessCSS, preview, resolveConfig, resolveEnvPrefix, rollupVersion, searchForWorkspaceRoot, send, sortUserPlugins, splitVendorChunk, splitVendorChunkPlugin, transformWithEsbuild, VERSION as version };
|
||||
export { type Alias, type AliasOptions, type AnymatchFn, type AnymatchPattern, type AppType, type AwaitWriteFinishOptions, type BindCLIShortcutsOptions, type BuildOptions, type CLIShortcut, type CSSModulesOptions, type CSSOptions, type CommonServerOptions, type ConfigEnv, Connect, type CorsOptions, type CorsOrigin, type DepOptimizationConfig, type DepOptimizationMetadata, type DepOptimizationOptions, type ESBuildOptions, type ESBuildTransformResult, type ExperimentalOptions, type ExportsData, FSWatcher, type FetchModuleOptions, type FileSystemServeOptions, type FilterPattern, type HMRBroadcaster, type HMRBroadcasterClient, type HMRChannel, type HTMLOptions, type HmrContext, type HmrOptions, type HookHandler, type HtmlTagDescriptor, HttpProxy, type IndexHtmlTransform, type IndexHtmlTransformContext, type IndexHtmlTransformHook, type IndexHtmlTransformResult, type InlineConfig, type InternalResolveOptions, type JsonOptions, type LegacyOptions, type LibraryFormats, type LibraryOptions, type LightningCSSOptions, type LogErrorOptions, type LogLevel, type LogOptions, type LogType, type Logger, type LoggerOptions, type MainThreadRuntimeOptions, type Manifest, type ManifestChunk, type MapToFunction, type AnymatchMatcher as Matcher, ModuleGraph, ModuleNode, type ModulePreloadOptions, type OptimizedDepInfo, type Plugin, type PluginContainer, type PluginHookUtils, type PluginOption, type PreprocessCSSResult, type PreviewOptions, type PreviewServer, type PreviewServerHook, type ProxyOptions, type RenderBuiltAssetUrl, type ResolveFn, type ResolveModulePreloadDependenciesFn, type ResolveOptions, type ResolvedBuildOptions, type ResolvedCSSOptions, type ResolvedConfig, type ResolvedModulePreloadOptions, type ResolvedPreviewOptions, type ResolvedSSROptions, type ResolvedServerOptions, type ResolvedServerUrls, type ResolvedUrl, type ResolvedWorkerOptions, type ResolverFunction, type ResolverObject, type RollupCommonJSOptions, type RollupDynamicImportVarsOptions, type SSROptions, type SSRTarget, type SendOptions, type ServerHMRChannel, ServerHMRConnector, type ServerHook, type ServerOptions, SplitVendorChunkCache, type SsrDepOptimizationOptions, Terser, type TerserOptions, type TransformOptions, type TransformResult, type UserConfig, type UserConfigExport, type UserConfigFn, type UserConfigFnObject, type UserConfigFnPromise, type ViteDevServer, type WatchOptions, WebSocket, WebSocketAlias, type WebSocketClient, type WebSocketCustomListener, WebSocketServer, build, buildErrorMessage, createFilter, createLogger, createServer, createViteRuntime, defineConfig, fetchModule, formatPostcssSourceMap, isCSSRequest, isFileServingAllowed, loadConfigFromFile, loadEnv, mergeAlias, mergeConfig, normalizePath, optimizeDeps, preprocessCSS, preview, resolveConfig, resolveEnvPrefix, rollupVersion, searchForWorkspaceRoot, send, sortUserPlugins, splitVendorChunk, splitVendorChunkPlugin, transformWithEsbuild, VERSION as version };
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export { parseAst, parseAstAsync } from 'rollup/parseAst';
|
||||
import { i as isInNodeModules, a as arraify } from './chunks/dep-BK3b2jBa.js';
|
||||
export { b as build, g as buildErrorMessage, k as createFilter, v as createLogger, c as createServer, d as defineConfig, h as fetchModule, f as formatPostcssSourceMap, y as isFileLoadingAllowed, x as isFileServingAllowed, l as loadConfigFromFile, z as loadEnv, j as mergeAlias, m as mergeConfig, n as normalizePath, o as optimizeDeps, e as preprocessCSS, p as preview, r as resolveConfig, A as resolveEnvPrefix, q as rollupVersion, w as searchForWorkspaceRoot, u as send, s as sortUserPlugins, t as transformWithEsbuild } from './chunks/dep-BK3b2jBa.js';
|
||||
import { i as isInNodeModules, a as arraify } from './chunks/dep-p3C6MpSJ.js';
|
||||
export { b as build, g as buildErrorMessage, k as createFilter, v as createLogger, c as createServer, d as defineConfig, h as fetchModule, f as formatPostcssSourceMap, x as isFileServingAllowed, l as loadConfigFromFile, y as loadEnv, j as mergeAlias, m as mergeConfig, n as normalizePath, o as optimizeDeps, e as preprocessCSS, p as preview, r as resolveConfig, z as resolveEnvPrefix, q as rollupVersion, w as searchForWorkspaceRoot, u as send, s as sortUserPlugins, t as transformWithEsbuild } from './chunks/dep-p3C6MpSJ.js';
|
||||
export { VERSION as version } from './constants.js';
|
||||
export { version as esbuildVersion } from 'esbuild';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
|
|
@ -11,25 +11,23 @@ import 'node:url';
|
|||
import 'node:util';
|
||||
import 'node:perf_hooks';
|
||||
import 'node:module';
|
||||
import 'node:crypto';
|
||||
import 'tty';
|
||||
import 'path';
|
||||
import 'fs';
|
||||
import 'node:events';
|
||||
import 'node:stream';
|
||||
import 'node:string_decoder';
|
||||
import 'events';
|
||||
import 'assert';
|
||||
import 'node:child_process';
|
||||
import 'node:http';
|
||||
import 'node:https';
|
||||
import 'util';
|
||||
import 'net';
|
||||
import 'events';
|
||||
import 'url';
|
||||
import 'http';
|
||||
import 'stream';
|
||||
import 'os';
|
||||
import 'child_process';
|
||||
import 'node:os';
|
||||
import 'node:crypto';
|
||||
import 'node:dns';
|
||||
import 'crypto';
|
||||
import 'module';
|
||||
|
|
@ -37,225 +35,238 @@ import 'node:assert';
|
|||
import 'node:v8';
|
||||
import 'node:worker_threads';
|
||||
import 'node:buffer';
|
||||
import 'node:events';
|
||||
import 'querystring';
|
||||
import 'node:readline';
|
||||
import 'zlib';
|
||||
import 'buffer';
|
||||
import 'https';
|
||||
import 'tls';
|
||||
import 'node:net';
|
||||
import 'assert';
|
||||
import 'node:zlib';
|
||||
|
||||
const CSS_LANGS_RE = (
|
||||
// eslint-disable-next-line regexp/no-unused-capturing-group
|
||||
/\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/
|
||||
);
|
||||
// This file will be built for both ESM and CJS. Avoid relying on other modules as possible.
|
||||
// copy from constants.ts
|
||||
const CSS_LANGS_RE =
|
||||
// eslint-disable-next-line regexp/no-unused-capturing-group
|
||||
/\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/;
|
||||
const isCSSRequest = (request) => CSS_LANGS_RE.test(request);
|
||||
// Use splitVendorChunkPlugin() to get the same manualChunks strategy as Vite 2.7
|
||||
// We don't recommend using this strategy as a general solution moving forward
|
||||
// splitVendorChunk is a simple index/vendor strategy that was used in Vite
|
||||
// until v2.8. It is exposed to let people continue to use it in case it was
|
||||
// working well for their setups.
|
||||
// The cache needs to be reset on buildStart for watch mode to work correctly
|
||||
// Don't use this manualChunks strategy for ssr, lib mode, and 'umd' or 'iife'
|
||||
/**
|
||||
* @deprecated use build.rollupOptions.output.manualChunks or framework specific configuration
|
||||
*/
|
||||
class SplitVendorChunkCache {
|
||||
cache;
|
||||
constructor() {
|
||||
this.cache = /* @__PURE__ */ new Map();
|
||||
}
|
||||
reset() {
|
||||
this.cache = /* @__PURE__ */ new Map();
|
||||
}
|
||||
}
|
||||
function splitVendorChunk(options = {}) {
|
||||
const cache = options.cache ?? new SplitVendorChunkCache();
|
||||
return (id, { getModuleInfo }) => {
|
||||
if (isInNodeModules(id) && !isCSSRequest(id) && staticImportedByEntry(id, getModuleInfo, cache.cache)) {
|
||||
return "vendor";
|
||||
cache;
|
||||
constructor() {
|
||||
this.cache = new Map();
|
||||
}
|
||||
};
|
||||
reset() {
|
||||
this.cache = new Map();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @deprecated use build.rollupOptions.output.manualChunks or framework specific configuration
|
||||
*/
|
||||
function splitVendorChunk(options = {}) {
|
||||
const cache = options.cache ?? new SplitVendorChunkCache();
|
||||
return (id, { getModuleInfo }) => {
|
||||
if (isInNodeModules(id) &&
|
||||
!isCSSRequest(id) &&
|
||||
staticImportedByEntry(id, getModuleInfo, cache.cache)) {
|
||||
return 'vendor';
|
||||
}
|
||||
};
|
||||
}
|
||||
function staticImportedByEntry(id, getModuleInfo, cache, importStack = []) {
|
||||
if (cache.has(id)) {
|
||||
return cache.get(id);
|
||||
}
|
||||
if (importStack.includes(id)) {
|
||||
cache.set(id, false);
|
||||
return false;
|
||||
}
|
||||
const mod = getModuleInfo(id);
|
||||
if (!mod) {
|
||||
cache.set(id, false);
|
||||
return false;
|
||||
}
|
||||
if (mod.isEntry) {
|
||||
cache.set(id, true);
|
||||
return true;
|
||||
}
|
||||
const someImporterIs = mod.importers.some(
|
||||
(importer) => staticImportedByEntry(
|
||||
importer,
|
||||
getModuleInfo,
|
||||
cache,
|
||||
importStack.concat(id)
|
||||
)
|
||||
);
|
||||
cache.set(id, someImporterIs);
|
||||
return someImporterIs;
|
||||
if (cache.has(id)) {
|
||||
return cache.get(id);
|
||||
}
|
||||
if (importStack.includes(id)) {
|
||||
// circular deps!
|
||||
cache.set(id, false);
|
||||
return false;
|
||||
}
|
||||
const mod = getModuleInfo(id);
|
||||
if (!mod) {
|
||||
cache.set(id, false);
|
||||
return false;
|
||||
}
|
||||
if (mod.isEntry) {
|
||||
cache.set(id, true);
|
||||
return true;
|
||||
}
|
||||
const someImporterIs = mod.importers.some((importer) => staticImportedByEntry(importer, getModuleInfo, cache, importStack.concat(id)));
|
||||
cache.set(id, someImporterIs);
|
||||
return someImporterIs;
|
||||
}
|
||||
/**
|
||||
* @deprecated use build.rollupOptions.output.manualChunks or framework specific configuration
|
||||
*/
|
||||
function splitVendorChunkPlugin() {
|
||||
const caches = [];
|
||||
function createSplitVendorChunk(output, config) {
|
||||
const cache = new SplitVendorChunkCache();
|
||||
caches.push(cache);
|
||||
const build = config.build ?? {};
|
||||
const format = output?.format;
|
||||
if (!build.ssr && !build.lib && format !== "umd" && format !== "iife") {
|
||||
return splitVendorChunk({ cache });
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: "vite:split-vendor-chunk",
|
||||
config(config) {
|
||||
let outputs = config?.build?.rollupOptions?.output;
|
||||
if (outputs) {
|
||||
outputs = arraify(outputs);
|
||||
for (const output of outputs) {
|
||||
const viteManualChunks = createSplitVendorChunk(output, config);
|
||||
if (viteManualChunks) {
|
||||
if (output.manualChunks) {
|
||||
if (typeof output.manualChunks === "function") {
|
||||
const userManualChunks = output.manualChunks;
|
||||
output.manualChunks = (id, api) => {
|
||||
return userManualChunks(id, api) ?? viteManualChunks(id, api);
|
||||
};
|
||||
} else {
|
||||
console.warn(
|
||||
"(!) the `splitVendorChunk` plugin doesn't have any effect when using the object form of `build.rollupOptions.output.manualChunks`. Consider using the function form instead."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
output.manualChunks = viteManualChunks;
|
||||
}
|
||||
}
|
||||
const caches = [];
|
||||
function createSplitVendorChunk(output, config) {
|
||||
const cache = new SplitVendorChunkCache();
|
||||
caches.push(cache);
|
||||
const build = config.build ?? {};
|
||||
const format = output?.format;
|
||||
if (!build.ssr && !build.lib && format !== 'umd' && format !== 'iife') {
|
||||
return splitVendorChunk({ cache });
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: createSplitVendorChunk({}, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
buildStart() {
|
||||
caches.forEach((cache) => cache.reset());
|
||||
}
|
||||
};
|
||||
return {
|
||||
name: 'vite:split-vendor-chunk',
|
||||
config(config) {
|
||||
let outputs = config?.build?.rollupOptions?.output;
|
||||
if (outputs) {
|
||||
outputs = arraify(outputs);
|
||||
for (const output of outputs) {
|
||||
const viteManualChunks = createSplitVendorChunk(output, config);
|
||||
if (viteManualChunks) {
|
||||
if (output.manualChunks) {
|
||||
if (typeof output.manualChunks === 'function') {
|
||||
const userManualChunks = output.manualChunks;
|
||||
output.manualChunks = (id, api) => {
|
||||
return userManualChunks(id, api) ?? viteManualChunks(id, api);
|
||||
};
|
||||
}
|
||||
else {
|
||||
// else, leave the object form of manualChunks untouched, as
|
||||
// we can't safely replicate rollup handling.
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("(!) the `splitVendorChunk` plugin doesn't have any effect when using the object form of `build.rollupOptions.output.manualChunks`. Consider using the function form instead.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
output.manualChunks = viteManualChunks;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: createSplitVendorChunk({}, config),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
buildStart() {
|
||||
caches.forEach((cache) => cache.reset());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
class ServerHMRBroadcasterClient {
|
||||
constructor(hmrChannel) {
|
||||
this.hmrChannel = hmrChannel;
|
||||
}
|
||||
send(...args) {
|
||||
let payload;
|
||||
if (typeof args[0] === "string") {
|
||||
payload = {
|
||||
type: "custom",
|
||||
event: args[0],
|
||||
data: args[1]
|
||||
};
|
||||
} else {
|
||||
payload = args[0];
|
||||
hmrChannel;
|
||||
constructor(hmrChannel) {
|
||||
this.hmrChannel = hmrChannel;
|
||||
}
|
||||
if (payload.type !== "custom") {
|
||||
throw new Error(
|
||||
"Cannot send non-custom events from the client to the server."
|
||||
);
|
||||
send(...args) {
|
||||
let payload;
|
||||
if (typeof args[0] === 'string') {
|
||||
payload = {
|
||||
type: 'custom',
|
||||
event: args[0],
|
||||
data: args[1],
|
||||
};
|
||||
}
|
||||
else {
|
||||
payload = args[0];
|
||||
}
|
||||
if (payload.type !== 'custom') {
|
||||
throw new Error('Cannot send non-custom events from the client to the server.');
|
||||
}
|
||||
this.hmrChannel.send(payload);
|
||||
}
|
||||
this.hmrChannel.send(payload);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The connector class to establish HMR communication between the server and the Vite runtime.
|
||||
* @experimental
|
||||
*/
|
||||
class ServerHMRConnector {
|
||||
handlers = [];
|
||||
hmrChannel;
|
||||
hmrClient;
|
||||
connected = false;
|
||||
constructor(server) {
|
||||
const hmrChannel = server.hot?.channels.find(
|
||||
(c) => c.name === "ssr"
|
||||
);
|
||||
if (!hmrChannel) {
|
||||
throw new Error(
|
||||
"Your version of Vite doesn't support HMR during SSR. Please, use Vite 5.1 or higher."
|
||||
);
|
||||
handlers = [];
|
||||
hmrChannel;
|
||||
hmrClient;
|
||||
connected = false;
|
||||
constructor(server) {
|
||||
const hmrChannel = server.hot?.channels.find((c) => c.name === 'ssr');
|
||||
if (!hmrChannel) {
|
||||
throw new Error("Your version of Vite doesn't support HMR during SSR. Please, use Vite 5.1 or higher.");
|
||||
}
|
||||
this.hmrClient = new ServerHMRBroadcasterClient(hmrChannel);
|
||||
hmrChannel.api.outsideEmitter.on('send', (payload) => {
|
||||
this.handlers.forEach((listener) => listener(payload));
|
||||
});
|
||||
this.hmrChannel = hmrChannel;
|
||||
}
|
||||
isReady() {
|
||||
return this.connected;
|
||||
}
|
||||
send(message) {
|
||||
const payload = JSON.parse(message);
|
||||
this.hmrChannel.api.innerEmitter.emit(payload.event, payload.data, this.hmrClient);
|
||||
}
|
||||
onUpdate(handler) {
|
||||
this.handlers.push(handler);
|
||||
handler({ type: 'connected' });
|
||||
this.connected = true;
|
||||
}
|
||||
this.hmrClient = new ServerHMRBroadcasterClient(hmrChannel);
|
||||
hmrChannel.api.outsideEmitter.on("send", (payload) => {
|
||||
this.handlers.forEach((listener) => listener(payload));
|
||||
});
|
||||
this.hmrChannel = hmrChannel;
|
||||
}
|
||||
isReady() {
|
||||
return this.connected;
|
||||
}
|
||||
send(message) {
|
||||
const payload = JSON.parse(message);
|
||||
this.hmrChannel.api.innerEmitter.emit(
|
||||
payload.event,
|
||||
payload.data,
|
||||
this.hmrClient
|
||||
);
|
||||
}
|
||||
onUpdate(handler) {
|
||||
this.handlers.push(handler);
|
||||
handler({ type: "connected" });
|
||||
this.connected = true;
|
||||
}
|
||||
}
|
||||
|
||||
function createHMROptions(server, options) {
|
||||
if (server.config.server.hmr === false || options.hmr === false) {
|
||||
return false;
|
||||
}
|
||||
const connection = new ServerHMRConnector(server);
|
||||
return {
|
||||
connection,
|
||||
logger: options.hmr?.logger
|
||||
};
|
||||
if (server.config.server.hmr === false || options.hmr === false) {
|
||||
return false;
|
||||
}
|
||||
const connection = new ServerHMRConnector(server);
|
||||
return {
|
||||
connection,
|
||||
logger: options.hmr?.logger,
|
||||
};
|
||||
}
|
||||
const prepareStackTrace = {
|
||||
retrieveFile(id) {
|
||||
if (existsSync(id)) {
|
||||
return readFileSync(id, "utf-8");
|
||||
}
|
||||
}
|
||||
retrieveFile(id) {
|
||||
if (existsSync(id)) {
|
||||
return readFileSync(id, 'utf-8');
|
||||
}
|
||||
},
|
||||
};
|
||||
function resolveSourceMapOptions(options) {
|
||||
if (options.sourcemapInterceptor != null) {
|
||||
if (options.sourcemapInterceptor === "prepareStackTrace") {
|
||||
return prepareStackTrace;
|
||||
if (options.sourcemapInterceptor != null) {
|
||||
if (options.sourcemapInterceptor === 'prepareStackTrace') {
|
||||
return prepareStackTrace;
|
||||
}
|
||||
if (typeof options.sourcemapInterceptor === 'object') {
|
||||
return { ...prepareStackTrace, ...options.sourcemapInterceptor };
|
||||
}
|
||||
return options.sourcemapInterceptor;
|
||||
}
|
||||
if (typeof options.sourcemapInterceptor === "object") {
|
||||
return { ...prepareStackTrace, ...options.sourcemapInterceptor };
|
||||
if (typeof process !== 'undefined' && 'setSourceMapsEnabled' in process) {
|
||||
return 'node';
|
||||
}
|
||||
return options.sourcemapInterceptor;
|
||||
}
|
||||
if (typeof process !== "undefined" && "setSourceMapsEnabled" in process) {
|
||||
return "node";
|
||||
}
|
||||
return prepareStackTrace;
|
||||
return prepareStackTrace;
|
||||
}
|
||||
/**
|
||||
* Create an instance of the Vite SSR runtime that support HMR.
|
||||
* @experimental
|
||||
*/
|
||||
async function createViteRuntime(server, options = {}) {
|
||||
const hmr = createHMROptions(server, options);
|
||||
return new ViteRuntime(
|
||||
{
|
||||
...options,
|
||||
root: server.config.root,
|
||||
fetchModule: server.ssrFetchModule,
|
||||
hmr,
|
||||
sourcemapInterceptor: resolveSourceMapOptions(options)
|
||||
},
|
||||
options.runner || new ESModulesRunner()
|
||||
);
|
||||
const hmr = createHMROptions(server, options);
|
||||
return new ViteRuntime({
|
||||
...options,
|
||||
root: server.config.root,
|
||||
fetchModule: server.ssrFetchModule,
|
||||
hmr,
|
||||
sourcemapInterceptor: resolveSourceMapOptions(options),
|
||||
}, options.runner || new ESModulesRunner());
|
||||
}
|
||||
|
||||
export { ServerHMRConnector, createViteRuntime, isCSSRequest, splitVendorChunk, splitVendorChunkPlugin };
|
||||
|
|
|
|||
|
|
@ -51,22 +51,24 @@ function normalizeString(path, allowAboveRoot) {
|
|||
char = "/";
|
||||
}
|
||||
if (char === "/") {
|
||||
if (!(lastSlash === index - 1 || dots === 1)) if (dots === 2) {
|
||||
if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") {
|
||||
if (res.length > 2) {
|
||||
const lastSlashIndex = res.lastIndexOf("/");
|
||||
lastSlashIndex === -1 ? (res = "", lastSegmentLength = 0) : (res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/")), lastSlash = index, dots = 0;
|
||||
continue;
|
||||
} else if (res.length > 0) {
|
||||
res = "", lastSegmentLength = 0, lastSlash = index, dots = 0;
|
||||
continue;
|
||||
if (!(lastSlash === index - 1 || dots === 1))
|
||||
if (dots === 2) {
|
||||
if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") {
|
||||
if (res.length > 2) {
|
||||
const lastSlashIndex = res.lastIndexOf("/");
|
||||
lastSlashIndex === -1 ? (res = "", lastSegmentLength = 0) : (res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/")), lastSlash = index, dots = 0;
|
||||
continue;
|
||||
} else if (res.length > 0) {
|
||||
res = "", lastSegmentLength = 0, lastSlash = index, dots = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
allowAboveRoot && (res += res.length > 0 ? "/.." : "..", lastSegmentLength = 2);
|
||||
} else
|
||||
res.length > 0 ? res += `/${path.slice(lastSlash + 1, index)}` : res = path.slice(lastSlash + 1, index), lastSegmentLength = index - lastSlash - 1;
|
||||
allowAboveRoot && (res += res.length > 0 ? "/.." : "..", lastSegmentLength = 2);
|
||||
} else
|
||||
res.length > 0 ? res += `/${path.slice(lastSlash + 1, index)}` : res = path.slice(lastSlash + 1, index), lastSegmentLength = index - lastSlash - 1;
|
||||
lastSlash = index, dots = 0;
|
||||
} else char === "." && dots !== -1 ? ++dots : dots = -1;
|
||||
} else
|
||||
char === "." && dots !== -1 ? ++dots : dots = -1;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
|
@ -94,46 +96,38 @@ for (let i = 0; i < chars.length; i++) {
|
|||
const c = chars.charCodeAt(i);
|
||||
intToChar[i] = c, charToInt[c] = i;
|
||||
}
|
||||
function decodeInteger(reader, relative) {
|
||||
function decode(mappings) {
|
||||
const state = new Int32Array(5), decoded = [];
|
||||
let index = 0;
|
||||
do {
|
||||
const semi = indexOf(mappings, index), line = [];
|
||||
let sorted = !0, lastCol = 0;
|
||||
state[0] = 0;
|
||||
for (let i = index; i < semi; i++) {
|
||||
let seg;
|
||||
i = decodeInteger(mappings, i, state, 0);
|
||||
const col = state[0];
|
||||
col < lastCol && (sorted = !1), lastCol = col, hasMoreVlq(mappings, i, semi) ? (i = decodeInteger(mappings, i, state, 1), i = decodeInteger(mappings, i, state, 2), i = decodeInteger(mappings, i, state, 3), hasMoreVlq(mappings, i, semi) ? (i = decodeInteger(mappings, i, state, 4), seg = [col, state[1], state[2], state[3], state[4]]) : seg = [col, state[1], state[2], state[3]]) : seg = [col], line.push(seg);
|
||||
}
|
||||
sorted || sort(line), decoded.push(line), index = semi + 1;
|
||||
} while (index <= mappings.length);
|
||||
return decoded;
|
||||
}
|
||||
function indexOf(mappings, index) {
|
||||
const idx = mappings.indexOf(";", index);
|
||||
return idx === -1 ? mappings.length : idx;
|
||||
}
|
||||
function decodeInteger(mappings, pos, state, j) {
|
||||
let value = 0, shift = 0, integer = 0;
|
||||
do {
|
||||
const c = reader.next();
|
||||
const c = mappings.charCodeAt(pos++);
|
||||
integer = charToInt[c], value |= (integer & 31) << shift, shift += 5;
|
||||
} while (integer & 32);
|
||||
const shouldNegate = value & 1;
|
||||
return value >>>= 1, shouldNegate && (value = -2147483648 | -value), relative + value;
|
||||
return value >>>= 1, shouldNegate && (value = -2147483648 | -value), state[j] += value, pos;
|
||||
}
|
||||
function hasMoreVlq(reader, max) {
|
||||
return reader.pos >= max ? !1 : reader.peek() !== comma;
|
||||
}
|
||||
class StringReader {
|
||||
constructor(buffer) {
|
||||
this.pos = 0, this.buffer = buffer;
|
||||
}
|
||||
next() {
|
||||
return this.buffer.charCodeAt(this.pos++);
|
||||
}
|
||||
peek() {
|
||||
return this.buffer.charCodeAt(this.pos);
|
||||
}
|
||||
indexOf(char) {
|
||||
const { buffer, pos } = this, idx = buffer.indexOf(char, pos);
|
||||
return idx === -1 ? buffer.length : idx;
|
||||
}
|
||||
}
|
||||
function decode(mappings) {
|
||||
const { length } = mappings, reader = new StringReader(mappings), decoded = [];
|
||||
let genColumn = 0, sourcesIndex = 0, sourceLine = 0, sourceColumn = 0, namesIndex = 0;
|
||||
do {
|
||||
const semi = reader.indexOf(";"), line = [];
|
||||
let sorted = !0, lastCol = 0;
|
||||
for (genColumn = 0; reader.pos < semi; ) {
|
||||
let seg;
|
||||
genColumn = decodeInteger(reader, genColumn), genColumn < lastCol && (sorted = !1), lastCol = genColumn, hasMoreVlq(reader, semi) ? (sourcesIndex = decodeInteger(reader, sourcesIndex), sourceLine = decodeInteger(reader, sourceLine), sourceColumn = decodeInteger(reader, sourceColumn), hasMoreVlq(reader, semi) ? (namesIndex = decodeInteger(reader, namesIndex), seg = [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex]) : seg = [genColumn, sourcesIndex, sourceLine, sourceColumn]) : seg = [genColumn], line.push(seg), reader.pos++;
|
||||
}
|
||||
sorted || sort(line), decoded.push(line), reader.pos = semi + 1;
|
||||
} while (reader.pos <= length);
|
||||
return decoded;
|
||||
function hasMoreVlq(mappings, i, length) {
|
||||
return i >= length ? !1 : mappings.charCodeAt(i) !== comma;
|
||||
}
|
||||
function sort(line) {
|
||||
line.sort(sortComparator);
|
||||
|
|
@ -206,13 +200,7 @@ function traceSegmentInternal(segments, memo, line, column, bias) {
|
|||
return found ? index = (bias === LEAST_UPPER_BOUND ? upperBound : lowerBound)(segments, column, index) : bias === LEAST_UPPER_BOUND && index++, index === -1 || index === segments.length ? -1 : index;
|
||||
}
|
||||
class DecodedMap {
|
||||
constructor(map, from) {
|
||||
this.map = map;
|
||||
const { mappings, names, sources } = map;
|
||||
this.version = map.version, this.names = names || [], this._encoded = mappings || "", this._decodedMemo = memoizedState(), this.url = from, this.resolvedSources = (sources || []).map(
|
||||
(s) => posixResolve(s || "", from)
|
||||
);
|
||||
}
|
||||
map;
|
||||
_encoded;
|
||||
_decoded;
|
||||
_decodedMemo;
|
||||
|
|
@ -220,6 +208,11 @@ class DecodedMap {
|
|||
version;
|
||||
names = [];
|
||||
resolvedSources;
|
||||
constructor(map, from) {
|
||||
this.map = map;
|
||||
const { mappings, names, sources } = map;
|
||||
this.version = map.version, this.names = names || [], this._encoded = mappings || "", this._decodedMemo = memoizedState(), this.url = from, this.resolvedSources = (sources || []).map((s) => posixResolve(s || "", from));
|
||||
}
|
||||
}
|
||||
function memoizedState() {
|
||||
return {
|
||||
|
|
@ -232,9 +225,7 @@ function getOriginalPosition(map, needle) {
|
|||
const result = originalPositionFor(map, needle);
|
||||
return result.column == null ? null : result;
|
||||
}
|
||||
const VITE_RUNTIME_SOURCEMAPPING_REGEXP = new RegExp(
|
||||
`//# ${SOURCEMAPPING_URL}=data:application/json;base64,(.+)`
|
||||
);
|
||||
const VITE_RUNTIME_SOURCEMAPPING_REGEXP = new RegExp(`//# ${SOURCEMAPPING_URL}=data:application/json;base64,(.+)`);
|
||||
class ModuleCacheMap extends Map {
|
||||
root;
|
||||
constructor(root, entries) {
|
||||
|
|
@ -276,16 +267,17 @@ class ModuleCacheMap extends Map {
|
|||
const module = this.get(id);
|
||||
module.evaluated = !1, module.meta = void 0, module.map = void 0, module.promise = void 0, module.exports = void 0, module.imports?.clear();
|
||||
}
|
||||
isImported({
|
||||
importedId,
|
||||
importedBy
|
||||
}, seen = /* @__PURE__ */ new Set()) {
|
||||
if (importedId = this.normalize(importedId), importedBy = this.normalize(importedBy), importedBy === importedId) return !0;
|
||||
if (seen.has(importedId)) return !1;
|
||||
isImported({ importedId, importedBy }, seen = /* @__PURE__ */ new Set()) {
|
||||
if (importedId = this.normalize(importedId), importedBy = this.normalize(importedBy), importedBy === importedId)
|
||||
return !0;
|
||||
if (seen.has(importedId))
|
||||
return !1;
|
||||
seen.add(importedId);
|
||||
const importers = this.getByModuleId(importedId)?.importers;
|
||||
if (!importers) return !1;
|
||||
if (importers.has(importedBy)) return !0;
|
||||
if (!importers)
|
||||
return !1;
|
||||
if (importers.has(importedBy))
|
||||
return !0;
|
||||
for (const importer of importers)
|
||||
if (this.isImported({
|
||||
importedBy,
|
||||
|
|
@ -300,7 +292,8 @@ class ModuleCacheMap extends Map {
|
|||
invalidateDepTree(ids, invalidated = /* @__PURE__ */ new Set()) {
|
||||
for (const _id of ids) {
|
||||
const id = this.normalize(_id);
|
||||
if (invalidated.has(id)) continue;
|
||||
if (invalidated.has(id))
|
||||
continue;
|
||||
invalidated.add(id);
|
||||
const mod = super.get(id);
|
||||
mod?.importers && this.invalidateDepTree(mod.importers, invalidated), super.delete(id);
|
||||
|
|
@ -313,7 +306,8 @@ class ModuleCacheMap extends Map {
|
|||
invalidateSubDepTree(ids, invalidated = /* @__PURE__ */ new Set()) {
|
||||
for (const _id of ids) {
|
||||
const id = this.normalize(_id);
|
||||
if (invalidated.has(id)) continue;
|
||||
if (invalidated.has(id))
|
||||
continue;
|
||||
invalidated.add(id);
|
||||
const subIds = Array.from(super.entries()).filter(([, mod]) => mod.importers?.has(id)).map(([key]) => key);
|
||||
subIds.length && this.invalidateSubDepTree(subIds, invalidated), super.delete(id);
|
||||
|
|
@ -322,21 +316,28 @@ class ModuleCacheMap extends Map {
|
|||
}
|
||||
getSourceMap(moduleId) {
|
||||
const mod = this.get(moduleId);
|
||||
if (mod.map) return mod.map;
|
||||
if (!mod.meta || !("code" in mod.meta)) return null;
|
||||
const mapString = VITE_RUNTIME_SOURCEMAPPING_REGEXP.exec(mod.meta.code)?.[1];
|
||||
if (!mapString) return null;
|
||||
if (mod.map)
|
||||
return mod.map;
|
||||
if (!mod.meta || !("code" in mod.meta))
|
||||
return null;
|
||||
const mapString = mod.meta.code.match(VITE_RUNTIME_SOURCEMAPPING_REGEXP)?.[1];
|
||||
if (!mapString)
|
||||
return null;
|
||||
const baseFile = mod.meta.file || moduleId.split("?")[0];
|
||||
return mod.map = new DecodedMap(JSON.parse(decodeBase64(mapString)), baseFile), mod.map;
|
||||
}
|
||||
}
|
||||
const prefixedBuiltins = /* @__PURE__ */ new Set(["node:test"]);
|
||||
function normalizeModuleId(file, root) {
|
||||
if (prefixedBuiltins.has(file)) return file;
|
||||
if (prefixedBuiltins.has(file))
|
||||
return file;
|
||||
let unixFile = slash(file).replace(/^\/@fs\//, isWindows ? "" : "/").replace(/^node:/, "").replace(/^\/+/, "/");
|
||||
return unixFile.startsWith(root) && (unixFile = unixFile.slice(root.length - 1)), unixFile.replace(/^file:\//, "/");
|
||||
}
|
||||
class HMRContext {
|
||||
hmrClient;
|
||||
ownerPath;
|
||||
newListeners;
|
||||
constructor(hmrClient, ownerPath) {
|
||||
this.hmrClient = hmrClient, this.ownerPath = ownerPath, hmrClient.dataMap.has(ownerPath) || hmrClient.dataMap.set(ownerPath, {});
|
||||
const mod = hmrClient.hotModulesMap.get(ownerPath);
|
||||
|
|
@ -345,14 +346,10 @@ class HMRContext {
|
|||
if (staleListeners)
|
||||
for (const [event, staleFns] of staleListeners) {
|
||||
const listeners = hmrClient.customListenersMap.get(event);
|
||||
listeners && hmrClient.customListenersMap.set(
|
||||
event,
|
||||
listeners.filter((l) => !staleFns.includes(l))
|
||||
);
|
||||
listeners && hmrClient.customListenersMap.set(event, listeners.filter((l) => !staleFns.includes(l)));
|
||||
}
|
||||
this.newListeners = /* @__PURE__ */ new Map(), hmrClient.ctxToListenersMap.set(ownerPath, this.newListeners);
|
||||
}
|
||||
newListeners;
|
||||
get data() {
|
||||
return this.hmrClient.dataMap.get(this.ownerPath);
|
||||
}
|
||||
|
|
@ -385,9 +382,7 @@ class HMRContext {
|
|||
this.hmrClient.notifyListeners("vite:invalidate", {
|
||||
path: this.ownerPath,
|
||||
message
|
||||
}), this.send("vite:invalidate", { path: this.ownerPath, message }), this.hmrClient.logger.debug(
|
||||
`[vite] invalidate ${this.ownerPath}${message ? `: ${message}` : ""}`
|
||||
);
|
||||
}), this.send("vite:invalidate", { path: this.ownerPath, message }), this.hmrClient.logger.debug(`[vite] invalidate ${this.ownerPath}${message ? `: ${message}` : ""}`);
|
||||
}
|
||||
on(event, cb) {
|
||||
const addToMap = (map) => {
|
||||
|
|
@ -411,9 +406,7 @@ class HMRContext {
|
|||
removeFromMap(this.hmrClient.customListenersMap), removeFromMap(this.newListeners);
|
||||
}
|
||||
send(event, data) {
|
||||
this.hmrClient.messenger.send(
|
||||
JSON.stringify({ type: "custom", event, data })
|
||||
);
|
||||
this.hmrClient.messenger.send(JSON.stringify({ type: "custom", event, data }));
|
||||
}
|
||||
acceptDeps(deps, callback = () => {
|
||||
}) {
|
||||
|
|
@ -428,6 +421,7 @@ class HMRContext {
|
|||
}
|
||||
}
|
||||
class HMRMessenger {
|
||||
connection;
|
||||
constructor(connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
|
@ -440,9 +434,8 @@ class HMRMessenger {
|
|||
}
|
||||
}
|
||||
class HMRClient {
|
||||
constructor(logger, connection, importUpdatedModule) {
|
||||
this.logger = logger, this.importUpdatedModule = importUpdatedModule, this.messenger = new HMRMessenger(connection);
|
||||
}
|
||||
logger;
|
||||
importUpdatedModule;
|
||||
hotModulesMap = /* @__PURE__ */ new Map();
|
||||
disposeMap = /* @__PURE__ */ new Map();
|
||||
pruneMap = /* @__PURE__ */ new Map();
|
||||
|
|
@ -450,6 +443,9 @@ class HMRClient {
|
|||
customListenersMap = /* @__PURE__ */ new Map();
|
||||
ctxToListenersMap = /* @__PURE__ */ new Map();
|
||||
messenger;
|
||||
constructor(logger, connection, importUpdatedModule) {
|
||||
this.logger = logger, this.importUpdatedModule = importUpdatedModule, this.messenger = new HMRMessenger(connection);
|
||||
}
|
||||
async notifyListeners(event, data) {
|
||||
const cbs = this.customListenersMap.get(event);
|
||||
cbs && await Promise.allSettled(cbs.map((cb) => cb(data)));
|
||||
|
|
@ -461,20 +457,17 @@ class HMRClient {
|
|||
// but they may have left behind side effects that need to be cleaned up
|
||||
// (.e.g style injections)
|
||||
async prunePaths(paths) {
|
||||
await Promise.all(
|
||||
paths.map((path) => {
|
||||
const disposer = this.disposeMap.get(path);
|
||||
if (disposer) return disposer(this.dataMap.get(path));
|
||||
})
|
||||
), paths.forEach((path) => {
|
||||
await Promise.all(paths.map((path) => {
|
||||
const disposer = this.disposeMap.get(path);
|
||||
if (disposer)
|
||||
return disposer(this.dataMap.get(path));
|
||||
})), paths.forEach((path) => {
|
||||
const fn = this.pruneMap.get(path);
|
||||
fn && fn(this.dataMap.get(path));
|
||||
});
|
||||
}
|
||||
warnFailedUpdate(err, path) {
|
||||
err.message.includes("fetch") || this.logger.error(err), this.logger.error(
|
||||
`[hmr] Failed to reload ${path}. This could be due to syntax errors or importing non-existent modules. (see errors above)`
|
||||
);
|
||||
err.message.includes("fetch") || this.logger.error(err), this.logger.error(`[hmr] Failed to reload ${path}. This could be due to syntax errors or importing non-existent modules. (see errors above)`);
|
||||
}
|
||||
updateQueue = [];
|
||||
pendingUpdateQueue = !1;
|
||||
|
|
@ -495,9 +488,7 @@ class HMRClient {
|
|||
if (!mod)
|
||||
return;
|
||||
let fetchedModule;
|
||||
const isSelfUpdate = path === acceptedPath, qualifiedCallbacks = mod.callbacks.filter(
|
||||
({ deps }) => deps.includes(acceptedPath)
|
||||
);
|
||||
const isSelfUpdate = path === acceptedPath, qualifiedCallbacks = mod.callbacks.filter(({ deps }) => deps.includes(acceptedPath));
|
||||
if (isSelfUpdate || qualifiedCallbacks.length > 0) {
|
||||
const disposer = this.disposeMap.get(acceptedPath);
|
||||
disposer && await disposer(this.dataMap.get(acceptedPath));
|
||||
|
|
@ -509,22 +500,18 @@ class HMRClient {
|
|||
}
|
||||
return () => {
|
||||
for (const { deps, fn } of qualifiedCallbacks)
|
||||
fn(
|
||||
deps.map((dep) => dep === acceptedPath ? fetchedModule : void 0)
|
||||
);
|
||||
fn(deps.map((dep) => dep === acceptedPath ? fetchedModule : void 0));
|
||||
const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`;
|
||||
this.logger.debug(`[vite] hot updated: ${loggedPath}`);
|
||||
};
|
||||
}
|
||||
}
|
||||
function analyzeImportedModDifference(mod, rawId, moduleType, metadata) {
|
||||
if (!metadata?.isDynamicImport && metadata?.importedNames?.length) {
|
||||
if (!metadata?.isDynamicImport && moduleType !== "module" && metadata?.importedNames?.length) {
|
||||
const missingBindings = metadata.importedNames.filter((s) => !(s in mod));
|
||||
if (missingBindings.length) {
|
||||
const lastBinding = missingBindings[missingBindings.length - 1];
|
||||
throw moduleType === "module" ? new SyntaxError(
|
||||
`[vite] The requested module '${rawId}' does not provide an export named '${lastBinding}'`
|
||||
) : new SyntaxError(`[vite] Named export '${lastBinding}' not found. The requested module '${rawId}' is a CommonJS module, which may not support all module.exports as named exports.
|
||||
throw new SyntaxError(`[vite] Named export '${lastBinding}' not found. The requested module '${rawId}' is a CommonJS module, which may not support all module.exports as named exports.
|
||||
CommonJS modules can always be imported via the default export, for example using:
|
||||
|
||||
import pkg from '${rawId}';
|
||||
|
|
@ -533,6 +520,15 @@ const {${missingBindings.join(", ")}} = pkg;
|
|||
}
|
||||
}
|
||||
}
|
||||
function proxyGuardOnlyEsm(mod, rawId, metadata) {
|
||||
return metadata?.importedNames?.length ? new Proxy(mod, {
|
||||
get(mod2, prop) {
|
||||
if (prop !== "then" && !(prop in mod2))
|
||||
throw new SyntaxError(`[vite] The requested module '${rawId}' does not provide an export named '${prop.toString()}'`);
|
||||
return mod2[prop];
|
||||
}
|
||||
}) : mod;
|
||||
}
|
||||
const ssrModuleExportsKey = "__vite_ssr_exports__", ssrImportKey = "__vite_ssr_import__", ssrDynamicImportKey = "__vite_ssr_dynamic_import__", ssrExportAllKey = "__vite_ssr_exportAll__", ssrImportMetaKey = "__vite_ssr_import_meta__", noop = () => {
|
||||
}, silentConsole = {
|
||||
debug: noop,
|
||||
|
|
@ -550,28 +546,23 @@ async function handleHMRPayload(runtime, payload) {
|
|||
hmrClient.logger.debug("[vite] connected."), hmrClient.messenger.flush();
|
||||
break;
|
||||
case "update":
|
||||
await hmrClient.notifyListeners("vite:beforeUpdate", payload), await Promise.all(
|
||||
payload.updates.map(async (update) => {
|
||||
if (update.type === "js-update")
|
||||
return update.acceptedPath = unwrapId(update.acceptedPath), update.path = unwrapId(update.path), hmrClient.queueUpdate(update);
|
||||
hmrClient.logger.error(
|
||||
"[vite] css hmr is not supported in runtime mode."
|
||||
);
|
||||
})
|
||||
), await hmrClient.notifyListeners("vite:afterUpdate", payload);
|
||||
await hmrClient.notifyListeners("vite:beforeUpdate", payload), await Promise.all(payload.updates.map(async (update) => {
|
||||
if (update.type === "js-update")
|
||||
return update.acceptedPath = unwrapId(update.acceptedPath), update.path = unwrapId(update.path), hmrClient.queueUpdate(update);
|
||||
hmrClient.logger.error("[vite] css hmr is not supported in runtime mode.");
|
||||
})), await hmrClient.notifyListeners("vite:afterUpdate", payload);
|
||||
break;
|
||||
case "custom": {
|
||||
await hmrClient.notifyListeners(payload.event, payload.data);
|
||||
break;
|
||||
}
|
||||
case "full-reload": {
|
||||
const { triggeredBy } = payload, clearEntrypoints = triggeredBy ? [...runtime.entrypoints].filter(
|
||||
(entrypoint) => runtime.moduleCache.isImported({
|
||||
importedId: triggeredBy,
|
||||
importedBy: entrypoint
|
||||
})
|
||||
) : [...runtime.entrypoints];
|
||||
if (!clearEntrypoints.length) break;
|
||||
const { triggeredBy } = payload, clearEntrypoints = triggeredBy ? [...runtime.entrypoints].filter((entrypoint) => runtime.moduleCache.isImported({
|
||||
importedId: triggeredBy,
|
||||
importedBy: entrypoint
|
||||
})) : [...runtime.entrypoints];
|
||||
if (!clearEntrypoints.length)
|
||||
break;
|
||||
hmrClient.logger.debug("[vite] program reload"), await hmrClient.notifyListeners("vite:beforeFullReload", payload), runtime.moduleCache.clear();
|
||||
for (const id of clearEntrypoints)
|
||||
await runtime.executeUrl(id);
|
||||
|
|
@ -583,11 +574,9 @@ async function handleHMRPayload(runtime, payload) {
|
|||
case "error": {
|
||||
await hmrClient.notifyListeners("vite:error", payload);
|
||||
const err = payload.err;
|
||||
hmrClient.logger.error(
|
||||
`[vite] Internal Server Error
|
||||
hmrClient.logger.error(`[vite] Internal Server Error
|
||||
${err.message}
|
||||
${err.stack}`
|
||||
);
|
||||
${err.stack}`);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
|
@ -618,12 +607,11 @@ class Queue {
|
|||
const sourceMapCache = {}, fileContentsCache = {}, moduleGraphs = /* @__PURE__ */ new Set(), retrieveFileHandlers = /* @__PURE__ */ new Set(), retrieveSourceMapHandlers = /* @__PURE__ */ new Set(), createExecHandlers = (handlers) => (...args) => {
|
||||
for (const handler of handlers) {
|
||||
const result = handler(...args);
|
||||
if (result) return result;
|
||||
if (result)
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}, retrieveFileFromHandlers = createExecHandlers(retrieveFileHandlers), retrieveSourceMapFromHandlers = createExecHandlers(
|
||||
retrieveSourceMapHandlers
|
||||
);
|
||||
}, retrieveFileFromHandlers = createExecHandlers(retrieveFileHandlers), retrieveSourceMapFromHandlers = createExecHandlers(retrieveSourceMapHandlers);
|
||||
let overridden = !1;
|
||||
const originalPrepare = Error.prepareStackTrace;
|
||||
function resetInterceptor(runtime, options) {
|
||||
|
|
@ -633,7 +621,8 @@ function interceptStackTrace(runtime, options = {}) {
|
|||
return overridden || (Error.prepareStackTrace = prepareStackTrace, overridden = !0), moduleGraphs.add(runtime.moduleCache), options.retrieveFile && retrieveFileHandlers.add(options.retrieveFile), options.retrieveSourceMap && retrieveSourceMapHandlers.add(options.retrieveSourceMap), () => resetInterceptor(runtime, options);
|
||||
}
|
||||
function supportRelativeURL(file, url) {
|
||||
if (!file) return url;
|
||||
if (!file)
|
||||
return url;
|
||||
const dir = posixDirname(slash(file)), match = /^\w+:\/\/[^/]*/.exec(dir);
|
||||
let protocol = match ? match[0] : "";
|
||||
const startPath = dir.slice(protocol.length);
|
||||
|
|
@ -652,24 +641,29 @@ function getRuntimeSourceMap(position) {
|
|||
return null;
|
||||
}
|
||||
function retrieveFile(path) {
|
||||
if (path in fileContentsCache) return fileContentsCache[path];
|
||||
if (path in fileContentsCache)
|
||||
return fileContentsCache[path];
|
||||
const content = retrieveFileFromHandlers(path);
|
||||
return typeof content == "string" ? (fileContentsCache[path] = content, content) : null;
|
||||
}
|
||||
function retrieveSourceMapURL(source) {
|
||||
const fileData = retrieveFile(source);
|
||||
if (!fileData) return null;
|
||||
if (!fileData)
|
||||
return null;
|
||||
const re = /\/\/[@#]\s*sourceMappingURL=([^\s'"]+)\s*$|\/\*[@#]\s*sourceMappingURL=[^\s*'"]+\s*\*\/\s*$/gm;
|
||||
let lastMatch, match;
|
||||
for (; match = re.exec(fileData); ) lastMatch = match;
|
||||
for (; match = re.exec(fileData); )
|
||||
lastMatch = match;
|
||||
return lastMatch ? lastMatch[1] : null;
|
||||
}
|
||||
const reSourceMap = /^data:application\/json[^,]+base64,/;
|
||||
function retrieveSourceMap(source) {
|
||||
const urlAndMap = retrieveSourceMapFromHandlers(source);
|
||||
if (urlAndMap) return urlAndMap;
|
||||
if (urlAndMap)
|
||||
return urlAndMap;
|
||||
let sourceMappingURL = retrieveSourceMapURL(source);
|
||||
if (!sourceMappingURL) return null;
|
||||
if (!sourceMappingURL)
|
||||
return null;
|
||||
let sourceMapData;
|
||||
if (reSourceMap.test(sourceMappingURL)) {
|
||||
const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(",") + 1);
|
||||
|
|
@ -682,7 +676,8 @@ function retrieveSourceMap(source) {
|
|||
} : null;
|
||||
}
|
||||
function mapSourcePosition(position) {
|
||||
if (!position.source) return position;
|
||||
if (!position.source)
|
||||
return position;
|
||||
let sourceMap = getRuntimeSourceMap(position);
|
||||
if (sourceMap || (sourceMap = sourceMapCache[position.source]), !sourceMap) {
|
||||
const urlAndMap = retrieveSourceMap(position.source);
|
||||
|
|
@ -690,10 +685,7 @@ function mapSourcePosition(position) {
|
|||
const url = urlAndMap.url;
|
||||
sourceMap = sourceMapCache[position.source] = {
|
||||
url,
|
||||
map: new DecodedMap(
|
||||
typeof urlAndMap.map == "string" ? JSON.parse(urlAndMap.map) : urlAndMap.map,
|
||||
url
|
||||
)
|
||||
map: new DecodedMap(typeof urlAndMap.map == "string" ? JSON.parse(urlAndMap.map) : urlAndMap.map, url)
|
||||
};
|
||||
const contents = sourceMap.map?.map.sourcesContent;
|
||||
sourceMap.map && contents && sourceMap.map.resolvedSources.forEach((source, i) => {
|
||||
|
|
@ -712,10 +704,7 @@ function mapSourcePosition(position) {
|
|||
if (sourceMap && sourceMap.map && sourceMap.url) {
|
||||
const originalPosition = getOriginalPosition(sourceMap.map, position);
|
||||
if (originalPosition && originalPosition.source != null)
|
||||
return originalPosition.source = supportRelativeURL(
|
||||
sourceMap.url,
|
||||
originalPosition.source
|
||||
), sourceMap.vite && (originalPosition._vite = !0), originalPosition;
|
||||
return originalPosition.source = supportRelativeURL(sourceMap.url, originalPosition.source), sourceMap.vite && (originalPosition._vite = !0), originalPosition;
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
|
@ -749,14 +738,13 @@ function CallSiteToString() {
|
|||
const functionName = this.getFunctionName();
|
||||
let addSuffix = !0;
|
||||
const isConstructor = this.isConstructor();
|
||||
if (this.isToplevel() || isConstructor)
|
||||
isConstructor ? line += `new ${functionName || "<anonymous>"}` : functionName ? line += functionName : (line += fileLocation, addSuffix = !1);
|
||||
else {
|
||||
if (!(this.isToplevel() || isConstructor)) {
|
||||
let typeName = this.getTypeName();
|
||||
typeName === "[object Object]" && (typeName = "null");
|
||||
const methodName = this.getMethodName();
|
||||
functionName ? (typeName && functionName.indexOf(typeName) !== 0 && (line += `${typeName}.`), line += functionName, methodName && functionName.indexOf(`.${methodName}`) !== functionName.length - methodName.length - 1 && (line += ` [as ${methodName}]`)) : line += `${typeName}.${methodName || "<anonymous>"}`;
|
||||
}
|
||||
} else
|
||||
isConstructor ? line += `new ${functionName || "<anonymous>"}` : functionName ? line += functionName : (line += fileLocation, addSuffix = !1);
|
||||
return addSuffix && (line += ` (${fileLocation})`), line;
|
||||
}
|
||||
function cloneCallSite(frame) {
|
||||
|
|
@ -813,29 +801,18 @@ function prepareStackTrace(error, stack) {
|
|||
function enableSourceMapSupport(runtime) {
|
||||
if (runtime.options.sourcemapInterceptor === "node") {
|
||||
if (typeof process > "u")
|
||||
throw new TypeError(
|
||||
`Cannot use "sourcemapInterceptor: 'node'" because global "process" variable is not available.`
|
||||
);
|
||||
throw new TypeError(`Cannot use "sourcemapInterceptor: 'node'" because global "process" variable is not available.`);
|
||||
if (typeof process.setSourceMapsEnabled != "function")
|
||||
throw new TypeError(
|
||||
`Cannot use "sourcemapInterceptor: 'node'" because "process.setSourceMapsEnabled" function is not available. Please use Node >= 16.6.0.`
|
||||
);
|
||||
throw new TypeError(`Cannot use "sourcemapInterceptor: 'node'" because "process.setSourceMapsEnabled" function is not available. Please use Node >= 16.6.0.`);
|
||||
const isEnabledAlready = process.sourceMapsEnabled ?? !1;
|
||||
return process.setSourceMapsEnabled(!0), () => !isEnabledAlready && process.setSourceMapsEnabled(!1);
|
||||
}
|
||||
return interceptStackTrace(
|
||||
runtime,
|
||||
typeof runtime.options.sourcemapInterceptor == "object" ? runtime.options.sourcemapInterceptor : void 0
|
||||
);
|
||||
return interceptStackTrace(runtime, typeof runtime.options.sourcemapInterceptor == "object" ? runtime.options.sourcemapInterceptor : void 0);
|
||||
}
|
||||
class ViteRuntime {
|
||||
constructor(options, runner, debug) {
|
||||
this.options = options, this.runner = runner, this.debug = debug, this.moduleCache = options.moduleCache ?? new ModuleCacheMap(options.root), typeof options.hmr == "object" && (this.hmrClient = new HMRClient(
|
||||
options.hmr.logger === !1 ? silentConsole : options.hmr.logger || console,
|
||||
options.hmr.connection,
|
||||
({ acceptedPath, ssrInvalidates }) => (this.moduleCache.invalidate(acceptedPath), ssrInvalidates && this.invalidateFiles(ssrInvalidates), this.executeUrl(acceptedPath))
|
||||
), options.hmr.connection.onUpdate(createHMRHandler(this))), options.sourcemapInterceptor !== !1 && (this._resetSourceMapSupport = enableSourceMapSupport(this));
|
||||
}
|
||||
options;
|
||||
runner;
|
||||
debug;
|
||||
/**
|
||||
* Holds the cache of modules
|
||||
* Keys of the map are ids
|
||||
|
|
@ -847,13 +824,14 @@ class ViteRuntime {
|
|||
fileToIdMap = /* @__PURE__ */ new Map();
|
||||
envProxy = new Proxy({}, {
|
||||
get(_, p) {
|
||||
throw new Error(
|
||||
`[vite-runtime] Dynamic access of "import.meta.env" is not supported. Please, use "import.meta.env.${String(p)}" instead.`
|
||||
);
|
||||
throw new Error(`[vite-runtime] Dynamic access of "import.meta.env" is not supported. Please, use "import.meta.env.${String(p)}" instead.`);
|
||||
}
|
||||
});
|
||||
_destroyed = !1;
|
||||
_resetSourceMapSupport;
|
||||
constructor(options, runner, debug) {
|
||||
this.options = options, this.runner = runner, this.debug = debug, this.moduleCache = options.moduleCache ?? new ModuleCacheMap(options.root), typeof options.hmr == "object" && (this.hmrClient = new HMRClient(options.hmr.logger === !1 ? silentConsole : options.hmr.logger || console, options.hmr.connection, ({ acceptedPath, ssrInvalidates }) => (this.moduleCache.invalidate(acceptedPath), ssrInvalidates && this.invalidateFiles(ssrInvalidates), this.executeUrl(acceptedPath))), options.hmr.connection.onUpdate(createHMRHandler(this))), options.sourcemapInterceptor !== !1 && (this._resetSourceMapSupport = enableSourceMapSupport(this));
|
||||
}
|
||||
/**
|
||||
* URL to execute. Accepts file path, server path or id relative to the root.
|
||||
*/
|
||||
|
|
@ -913,7 +891,7 @@ class ViteRuntime {
|
|||
if (!("externalize" in fetchResult))
|
||||
return exports;
|
||||
const { id, type } = fetchResult;
|
||||
return type !== "module" && type !== "commonjs" || analyzeImportedModDifference(exports, id, type, metadata), exports;
|
||||
return type !== "module" && type !== "commonjs" ? exports : (analyzeImportedModDifference(exports, id, type, metadata), proxyGuardOnlyEsm(exports, id, metadata));
|
||||
}
|
||||
async cachedRequest(id, fetchedModule, callstack = [], metadata) {
|
||||
const moduleId = fetchedModule.id;
|
||||
|
|
@ -926,10 +904,8 @@ class ViteRuntime {
|
|||
const getStack = () => `stack:
|
||||
${[...callstack, moduleId].reverse().map((p) => ` - ${p}`).join(`
|
||||
`)}`;
|
||||
this.debug(
|
||||
`[vite-runtime] module ${moduleId} takes over 2s to load.
|
||||
${getStack()}`
|
||||
);
|
||||
this.debug(`[vite-runtime] module ${moduleId} takes over 2s to load.
|
||||
${getStack()}`);
|
||||
}, 2e3));
|
||||
try {
|
||||
if (mod.promise)
|
||||
|
|
@ -972,9 +948,7 @@ ${getStack()}`
|
|||
const { code, file } = fetchResult;
|
||||
if (code == null) {
|
||||
const importer = callstack[callstack.length - 2];
|
||||
throw new Error(
|
||||
`[vite-runtime] Failed to load "${id}"${importer ? ` imported from ${importer}` : ""}`
|
||||
);
|
||||
throw new Error(`[vite-runtime] Failed to load "${id}"${importer ? ` imported from ${importer}` : ""}`);
|
||||
}
|
||||
const modulePath = cleanUrl(file || moduleId), href = posixPathToFileHref(modulePath), filename = modulePath, dirname2 = posixDirname(modulePath), meta = {
|
||||
filename: isWindows ? toWindowsPath(filename) : filename,
|
||||
|
|
@ -982,9 +956,7 @@ ${getStack()}`
|
|||
url: href,
|
||||
env: this.envProxy,
|
||||
resolve(id2, parent) {
|
||||
throw new Error(
|
||||
'[vite-runtime] "import.meta.resolve" is not supported.'
|
||||
);
|
||||
throw new Error('[vite-runtime] "import.meta.resolve" is not supported.');
|
||||
},
|
||||
// should be replaced during transformation
|
||||
glob() {
|
||||
|
|
@ -1042,13 +1014,7 @@ class ESModulesRunner {
|
|||
ssrExportAllKey,
|
||||
// source map should already be inlined by Vite
|
||||
'"use strict";' + code
|
||||
)(
|
||||
context[ssrModuleExportsKey],
|
||||
context[ssrImportMetaKey],
|
||||
context[ssrImportKey],
|
||||
context[ssrDynamicImportKey],
|
||||
context[ssrExportAllKey]
|
||||
), Object.seal(context[ssrModuleExportsKey]);
|
||||
)(context[ssrModuleExportsKey], context[ssrImportMetaKey], context[ssrImportKey], context[ssrDynamicImportKey], context[ssrExportAllKey]), Object.seal(context[ssrModuleExportsKey]);
|
||||
}
|
||||
runExternalModule(filepath) {
|
||||
return import(filepath);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
|
||||
warnCjsUsage()
|
||||
|
||||
// type utils
|
||||
module.exports.defineConfig = (config) => config
|
||||
|
||||
// proxy cjs utils (sync functions)
|
||||
// eslint-disable-next-line n/no-missing-require -- will be generated by build
|
||||
Object.assign(module.exports, require('./dist/node-cjs/publicUtils.cjs'))
|
||||
|
||||
// async functions, can be redirect from ESM build
|
||||
|
|
@ -25,36 +28,11 @@ asyncFunctions.forEach((name) => {
|
|||
|
||||
function warnCjsUsage() {
|
||||
if (process.env.VITE_CJS_IGNORE_WARNING) return
|
||||
const logLevelIndex = process.argv.findIndex((arg) =>
|
||||
/^(?:-l|--logLevel)/.test(arg),
|
||||
)
|
||||
if (logLevelIndex > 0) {
|
||||
const logLevelValue = process.argv[logLevelIndex + 1]
|
||||
if (logLevelValue === 'silent' || logLevelValue === 'error') {
|
||||
return
|
||||
}
|
||||
if (/silent|error/.test(process.argv[logLevelIndex])) {
|
||||
return
|
||||
}
|
||||
}
|
||||
const yellow = (str) => `\u001b[33m${str}\u001b[39m`
|
||||
console.warn(
|
||||
const log = process.env.VITE_CJS_TRACE ? console.trace : console.warn
|
||||
log(
|
||||
yellow(
|
||||
`The CJS build of Vite's Node API is deprecated. See https://vite.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.`,
|
||||
`The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.`,
|
||||
),
|
||||
)
|
||||
if (process.env.VITE_CJS_TRACE) {
|
||||
const e = {}
|
||||
const stackTraceLimit = Error.stackTraceLimit
|
||||
Error.stackTraceLimit = 100
|
||||
Error.captureStackTrace(e)
|
||||
Error.stackTraceLimit = stackTraceLimit
|
||||
console.log(
|
||||
e.stack
|
||||
.split('\n')
|
||||
.slice(1)
|
||||
.filter((line) => !line.includes('(node:'))
|
||||
.join('\n'),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* @deprecated The CJS build of Vite's Node API is deprecated. See https://vite.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.
|
||||
* @deprecated The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.
|
||||
*/
|
||||
declare const module: any
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "vite",
|
||||
"version": "5.4.21",
|
||||
"version": "5.2.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"author": "Evan You",
|
||||
|
|
@ -68,30 +68,33 @@
|
|||
"bugs": {
|
||||
"url": "https://github.com/vitejs/vite/issues"
|
||||
},
|
||||
"homepage": "https://vite.dev",
|
||||
"homepage": "https://vitejs.dev",
|
||||
"funding": "https://github.com/vitejs/vite?sponsor=1",
|
||||
"//": "READ CONTRIBUTING.md to understand what to put under deps vs. devDeps!",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
"esbuild": "^0.20.1",
|
||||
"postcss": "^8.4.38",
|
||||
"rollup": "^4.13.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"@babel/parser": "^7.25.6",
|
||||
"@babel/parser": "^7.24.6",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"@polka/compression": "^1.0.0-next.25",
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
"@rollup/plugin-commonjs": "^26.0.1",
|
||||
"@rollup/plugin-commonjs": "^25.0.8",
|
||||
"@rollup/plugin-dynamic-import-vars": "^2.1.2",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@rollup/pluginutils": "^5.1.0",
|
||||
"@types/escape-html": "^1.0.4",
|
||||
"@types/pnpapi": "^0.0.5",
|
||||
"acorn": "^8.11.3",
|
||||
"acorn-walk": "^8.3.2",
|
||||
"artichokie": "^0.2.1",
|
||||
"cac": "^6.7.14",
|
||||
"chokidar": "^3.6.0",
|
||||
|
|
@ -99,21 +102,21 @@
|
|||
"convert-source-map": "^2.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"debug": "^4.3.6",
|
||||
"debug": "^4.3.4",
|
||||
"dep-types": "link:./src/types",
|
||||
"dotenv": "^16.4.5",
|
||||
"dotenv-expand": "^11.0.6",
|
||||
"es-module-lexer": "^1.5.4",
|
||||
"es-module-lexer": "^1.5.3",
|
||||
"escape-html": "^1.0.3",
|
||||
"estree-walker": "^3.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
"http-proxy": "^1.18.1",
|
||||
"launch-editor-middleware": "^2.9.1",
|
||||
"lightningcss": "^1.26.0",
|
||||
"magic-string": "^0.30.11",
|
||||
"micromatch": "^4.0.8",
|
||||
"mlly": "^1.7.1",
|
||||
"launch-editor-middleware": "^2.6.1",
|
||||
"lightningcss": "^1.25.1",
|
||||
"magic-string": "^0.30.10",
|
||||
"micromatch": "^4.0.7",
|
||||
"mlly": "^1.7.0",
|
||||
"mrmime": "^2.0.0",
|
||||
"open": "^8.4.2",
|
||||
"parse5": "^7.1.2",
|
||||
|
|
@ -127,25 +130,23 @@
|
|||
"resolve.exports": "^2.0.2",
|
||||
"rollup-plugin-dts": "^6.1.1",
|
||||
"rollup-plugin-esbuild": "^6.1.1",
|
||||
"rollup-plugin-license": "^3.5.2",
|
||||
"sass": "^1.77.8",
|
||||
"sass-embedded": "^1.77.8",
|
||||
"rollup-plugin-license": "^3.4.0",
|
||||
"sass": "^1.77.2",
|
||||
"sirv": "^2.0.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"strip-literal": "^2.1.0",
|
||||
"tsconfck": "^3.1.4",
|
||||
"tslib": "^2.7.0",
|
||||
"tsconfck": "^3.0.3",
|
||||
"tslib": "^2.6.2",
|
||||
"types": "link:./types",
|
||||
"ufo": "^1.5.4",
|
||||
"ws": "^8.18.0"
|
||||
"ufo": "^1.5.3",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
|
|
@ -157,9 +158,6 @@
|
|||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
|
|
@ -177,12 +175,12 @@
|
|||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsx scripts/dev.ts",
|
||||
"dev": "rimraf dist && pnpm run build-bundle -w",
|
||||
"build": "rimraf dist && run-s build-bundle build-types",
|
||||
"build-bundle": "rollup --config rollup.config.ts --configPlugin esbuild",
|
||||
"build-bundle": "rollup --config rollup.config.ts --configPlugin typescript",
|
||||
"build-types": "run-s build-types-temp build-types-roll build-types-check",
|
||||
"build-types-temp": "tsc --emitDeclarationOnly --outDir temp -p src/node",
|
||||
"build-types-roll": "rollup --config rollup.dts.config.ts --configPlugin esbuild && rimraf temp",
|
||||
"build-types-roll": "rollup --config rollup.dts.config.ts --configPlugin typescript && rimraf temp",
|
||||
"build-types-check": "tsc --project tsconfig.check.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint --cache --ext .ts src/**",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"dependencies": {
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.7.0",
|
||||
"echarts": "^6.0.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.3.0"
|
||||
|
|
@ -1552,6 +1553,16 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.0.0.tgz",
|
||||
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0",
|
||||
"zrender": "6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
|
||||
|
|
@ -2529,6 +2540,12 @@
|
|||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-detect": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz",
|
||||
|
|
@ -2882,6 +2899,15 @@
|
|||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-6.0.0.tgz",
|
||||
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:simple": "node dev-server.mjs",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:e2e": "playwright test"
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
"dependencies": {
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.7.0",
|
||||
"echarts": "^6.0.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.3.0"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
export interface AuditLog {
|
||||
id: string
|
||||
username: string
|
||||
operation: string
|
||||
module: string
|
||||
action: string
|
||||
targetType?: string
|
||||
targetId?: string
|
||||
content?: string
|
||||
ipAddress: string
|
||||
requestMethod: string
|
||||
requestUrl: string
|
||||
status: 'SUCCESS' | 'FAIL'
|
||||
executionTimeMs?: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface AuditLogQuery {
|
||||
page?: number
|
||||
size?: number
|
||||
module?: string
|
||||
action?: string
|
||||
username?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
export function getAuditLogs(params: AuditLogQuery) {
|
||||
return request({
|
||||
url: '/api/audit-logs',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getAuditModules() {
|
||||
return request({
|
||||
url: '/api/audit-logs/modules',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getAuditActions() {
|
||||
return request({
|
||||
url: '/api/audit-logs/actions',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getAuditStats() {
|
||||
return request({
|
||||
url: '/api/audit-logs/stats',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
|
@ -2,17 +2,17 @@ import request from '@/utils/request'
|
|||
import type { LoginRequest, LoginResponse } from '@/types'
|
||||
|
||||
export const login = (data: LoginRequest) => {
|
||||
return request.post<LoginResponse>('/auth/login', data)
|
||||
return request.post<LoginResponse>('/api/auth/login', data)
|
||||
}
|
||||
|
||||
export const logout = () => {
|
||||
return request.post('/auth/logout')
|
||||
return request.post('/api/auth/logout')
|
||||
}
|
||||
|
||||
export const getCurrentUser = () => {
|
||||
return request.get('/auth/me')
|
||||
return request.get('/api/auth/me')
|
||||
}
|
||||
|
||||
export const refreshToken = () => {
|
||||
return request.post('/auth/refresh')
|
||||
return request.post('/api/auth/refresh')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
export interface EnergyMeter {
|
||||
id?: string
|
||||
meterCode: string
|
||||
meterName: string
|
||||
energyType: string
|
||||
installationLocation?: string
|
||||
ratedCapacity?: number
|
||||
unitPrice?: number
|
||||
status?: string
|
||||
}
|
||||
|
||||
export interface EnergyConsumption {
|
||||
id?: string
|
||||
meterId: string
|
||||
consumptionDate: string
|
||||
previousReading: number
|
||||
currentReading: number
|
||||
consumption: number
|
||||
amount?: number
|
||||
recordMethod?: string
|
||||
}
|
||||
|
||||
export function getEnergyMeters(projectId: string, energyType?: string) {
|
||||
return request({
|
||||
url: '/api/v1/ops/energy-meters',
|
||||
method: 'get',
|
||||
params: { projectId, energyType }
|
||||
})
|
||||
}
|
||||
|
||||
export function getEnergyMeter(id: string) {
|
||||
return request({
|
||||
url: `/api/v1/ops/energy-meters/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function createEnergyMeter(data: EnergyMeter) {
|
||||
return request({
|
||||
url: '/api/v1/ops/energy-meters',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateEnergyMeter(id: string, data: EnergyMeter) {
|
||||
return request({
|
||||
url: `/api/v1/ops/energy-meters/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteEnergyMeter(id: string) {
|
||||
return request({
|
||||
url: `/api/v1/ops/energy-meters/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function recordEnergyConsumption(data: { meterId: string; currentReading: number; recordedBy?: string }) {
|
||||
return request({
|
||||
url: '/api/v1/ops/energy-consumption',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getEnergyConsumption(meterId: string, startDate?: string, endDate?: string) {
|
||||
return request({
|
||||
url: `/api/v1/ops/energy-consumption/${meterId}`,
|
||||
method: 'get',
|
||||
params: { startDate, endDate }
|
||||
})
|
||||
}
|
||||
|
||||
export function getConsumptionByType(projectId: string, month: string) {
|
||||
return request({
|
||||
url: '/api/v1/ops/energy-statistics/by-type',
|
||||
method: 'get',
|
||||
params: { projectId, month }
|
||||
})
|
||||
}
|
||||
|
||||
export function getUnitConsumption(projectId: string, month: string) {
|
||||
return request({
|
||||
url: '/api/v1/ops/energy-statistics/unit-consumption',
|
||||
method: 'get',
|
||||
params: { projectId, month }
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// ==================== 设备健康度类型 ====================
|
||||
|
||||
export interface EquipmentHealth {
|
||||
equipmentId: string
|
||||
equipmentName: string
|
||||
healthScore: number
|
||||
healthLevel: string
|
||||
lastCheckTime: string
|
||||
nextCheckTime?: string
|
||||
riskLevel: string
|
||||
mainRiskFactors: string[]
|
||||
maintenanceSuggestions: string[]
|
||||
}
|
||||
|
||||
export interface HealthHistory {
|
||||
id: string
|
||||
equipmentId: string
|
||||
healthScore: number
|
||||
healthLevel: string
|
||||
recordTime: string
|
||||
factors: Record<string, number>
|
||||
}
|
||||
|
||||
export interface EquipmentFailure {
|
||||
id: string
|
||||
equipmentId: string
|
||||
equipmentName: string
|
||||
failureTime: string
|
||||
failureType: string
|
||||
failureLevel: string
|
||||
description: string
|
||||
repairTime?: string
|
||||
repairMethod?: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface MTBFData {
|
||||
equipmentId: string
|
||||
equipmentName: string
|
||||
mtbfDays: number
|
||||
totalFailures: number
|
||||
totalOperatingDays: number
|
||||
lastFailureTime?: string
|
||||
}
|
||||
|
||||
export interface MTTRData {
|
||||
equipmentId: string
|
||||
equipmentName: string
|
||||
mttrHours: number
|
||||
totalRepairTime: number
|
||||
totalRepairs: number
|
||||
lastRepairTime?: string
|
||||
}
|
||||
|
||||
export interface HealthTrendData {
|
||||
date: string
|
||||
score: number
|
||||
level: string
|
||||
}
|
||||
|
||||
// ==================== 设备健康 API ====================
|
||||
|
||||
// 获取设备健康度
|
||||
export function getEquipmentHealth(equipmentId: string) {
|
||||
return request.get<EquipmentHealth>(`/api/v1/ops/equipment-health/${equipmentId}`)
|
||||
}
|
||||
|
||||
// 获取健康度历史
|
||||
export function getHealthHistory(equipmentId: string, days: number = 30) {
|
||||
return request.get<HealthHistory[]>(`/api/v1/ops/equipment-health/${equipmentId}/history`, {
|
||||
params: { days }
|
||||
})
|
||||
}
|
||||
|
||||
// 计算设备健康度
|
||||
export function calculateHealth(equipmentId: string) {
|
||||
return request.post<EquipmentHealth>(`/api/v1/ops/equipment-health/calculate`, { equipmentId })
|
||||
}
|
||||
|
||||
// 获取故障历史列表
|
||||
export function getFailureHistory(equipmentId: string) {
|
||||
return request.get<EquipmentFailure[]>(`/api/v1/ops/equipment-failure-history/${equipmentId}`)
|
||||
}
|
||||
|
||||
// 记录故障
|
||||
export function recordFailure(data: {
|
||||
equipmentId: string
|
||||
failureTime: string
|
||||
failureType: string
|
||||
failureLevel: string
|
||||
description: string
|
||||
}) {
|
||||
return request.post('/api/v1/ops/equipment-failure-history', data)
|
||||
}
|
||||
|
||||
// 获取 MTBF
|
||||
export function getEquipmentMTBF(equipmentId: string) {
|
||||
return request.get<MTBFData>(`/api/v1/ops/equipment-mtbf/${equipmentId}`)
|
||||
}
|
||||
|
||||
// 获取 MTTR
|
||||
export function getEquipmentMTTR(equipmentId: string) {
|
||||
return request.get<MTTRData>(`/api/v1/ops/equipment-mttr/${equipmentId}`)
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// ==================== 设备相关类型 ====================
|
||||
|
||||
export interface EquipmentForm {
|
||||
id?: string
|
||||
code: string
|
||||
name: string
|
||||
isEquipment?: boolean
|
||||
designLifeYears?: number
|
||||
ratedPower?: number
|
||||
ratedVoltage?: string
|
||||
ratedCurrent?: number
|
||||
maintenanceVendor?: string
|
||||
maintenanceVendorPhone?: string
|
||||
specialEquipmentType?: string
|
||||
inspectionCycle?: number
|
||||
nextInspectionDate?: string
|
||||
}
|
||||
|
||||
export interface Equipment {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
isEquipment: boolean
|
||||
designLifeYears?: number
|
||||
ratedPower?: number
|
||||
ratedVoltage?: string
|
||||
ratedCurrent?: number
|
||||
maintenanceVendor?: string
|
||||
maintenanceVendorPhone?: string
|
||||
specialEquipmentType?: string
|
||||
inspectionCycle?: number
|
||||
nextInspectionDate?: string
|
||||
spaceNodeId?: string
|
||||
spaceNodeName?: string
|
||||
projectId?: string
|
||||
projectName?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface PageResponse<T> {
|
||||
content: T[]
|
||||
totalElements: number
|
||||
totalPages: number
|
||||
size: number
|
||||
number: number
|
||||
}
|
||||
|
||||
// ==================== 设备 API ====================
|
||||
|
||||
// 获取设备列表
|
||||
export function getEquipmentList(projectId: string) {
|
||||
return request.get<PageResponse<Equipment>>('/api/v1/mdm/space-nodes/equipment', {
|
||||
params: { projectId }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取设备详情
|
||||
export function getEquipmentDetail(id: string) {
|
||||
return request.get<Equipment>(`/api/v1/mdm/space-nodes/${id}/equipment`)
|
||||
}
|
||||
|
||||
// 获取特种设备列表
|
||||
export function getSpecialEquipment(projectId: string) {
|
||||
return request.get<Equipment[]>('/api/v1/mdm/space-nodes/special-equipment', {
|
||||
params: { projectId }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取即将年检设备
|
||||
export function getExpiringInspection(projectId: string, daysAhead?: number) {
|
||||
return request.get<Equipment[]>('/api/v1/mdm/space-nodes/expiring-inspection', {
|
||||
params: { projectId, daysAhead }
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// ==================== 点检模板类型 ====================
|
||||
|
||||
export interface InspectionItem {
|
||||
id: string
|
||||
itemName: string
|
||||
checkMethod: string
|
||||
checkStandard: string
|
||||
isRequired: boolean
|
||||
remarks?: string
|
||||
}
|
||||
|
||||
export interface InspectionTemplate {
|
||||
id: string
|
||||
name: string
|
||||
equipmentType: string
|
||||
projectId: string
|
||||
projectName: string
|
||||
inspectionItems: InspectionItem[]
|
||||
enabled: boolean
|
||||
createdBy?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface TemplateFormData {
|
||||
id?: string
|
||||
name: string
|
||||
equipmentType: string
|
||||
projectId: string
|
||||
inspectionItems: InspectionItem[]
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
// ==================== 点检模板 API ====================
|
||||
|
||||
// 获取模板列表
|
||||
export function getInspectionTemplates(projectId: string) {
|
||||
return request.get<InspectionTemplate[]>('/api/v1/ops/inspection-templates', {
|
||||
params: { projectId }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取模板详情
|
||||
export function getInspectionTemplateDetail(id: string) {
|
||||
return request.get<InspectionTemplate>(`/api/v1/ops/inspection-templates/${id}`)
|
||||
}
|
||||
|
||||
// 创建模板
|
||||
export function createInspectionTemplate(data: TemplateFormData) {
|
||||
return request.post<InspectionTemplate>('/api/v1/ops/inspection-templates', data)
|
||||
}
|
||||
|
||||
// 更新模板
|
||||
export function updateInspectionTemplate(id: string, data: TemplateFormData) {
|
||||
return request.put<InspectionTemplate>(`/api/v1/ops/inspection-templates/${id}`, data)
|
||||
}
|
||||
|
||||
// 复制模板
|
||||
export function copyInspectionTemplate(id: string, targetProjectId?: string) {
|
||||
return request.post<InspectionTemplate>(`/api/v1/ops/inspection-templates/${id}/copy`, {
|
||||
targetProjectId
|
||||
})
|
||||
}
|
||||
|
||||
// 按设备类型获取模板
|
||||
export function getTemplatesByEquipmentType(equipmentType: string) {
|
||||
return request.get<InspectionTemplate[]>(`/api/v1/ops/inspection-templates/by-type/${equipmentType}`)
|
||||
}
|
||||
|
||||
// 删除模板
|
||||
export function deleteInspectionTemplate(id: string) {
|
||||
return request.delete(`/api/v1/ops/inspection-templates/${id}`)
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// ==================== 维保计划相关类型 ====================
|
||||
|
||||
export interface MaintenancePlan {
|
||||
id: string
|
||||
name: string
|
||||
projectId: string
|
||||
projectName?: string
|
||||
triggerType: 'MANUAL' | 'SCHEDULED' | 'AUTOMATIC'
|
||||
equipmentId?: string
|
||||
equipmentName?: string
|
||||
spaceNodeId?: string
|
||||
spaceNodeName?: string
|
||||
description?: string
|
||||
enabled: boolean
|
||||
cronExpression?: string
|
||||
nextTriggerTime?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface MaintenancePlanForm {
|
||||
id?: string
|
||||
name: string
|
||||
projectId: string
|
||||
triggerType: 'MANUAL' | 'SCHEDULED' | 'AUTOMATIC'
|
||||
equipmentId?: string
|
||||
spaceNodeId?: string
|
||||
description?: string
|
||||
enabled?: boolean
|
||||
cronExpression?: string
|
||||
}
|
||||
|
||||
// ==================== 维保任务相关类型 ====================
|
||||
|
||||
export type TaskStatus = 'PENDING' | 'ACCEPTED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED'
|
||||
|
||||
export interface MaintenanceTask {
|
||||
id: string
|
||||
planId?: string
|
||||
planName?: string
|
||||
projectId: string
|
||||
projectName?: string
|
||||
equipmentId?: string
|
||||
equipmentName?: string
|
||||
spaceNodeId?: string
|
||||
spaceNodeName?: string
|
||||
title: string
|
||||
description?: string
|
||||
status: TaskStatus
|
||||
assigneeId?: string
|
||||
assigneeName?: string
|
||||
scheduledDate?: string
|
||||
startTime?: string
|
||||
completedTime?: string
|
||||
cancellationReason?: string
|
||||
completionNotes?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface TaskQueryParams {
|
||||
projectId?: string
|
||||
status?: TaskStatus
|
||||
assigneeId?: string
|
||||
page?: number
|
||||
size?: number
|
||||
}
|
||||
|
||||
// ==================== 维保计划 API ====================
|
||||
|
||||
// 获取维保计划列表
|
||||
export function getMaintenancePlans(projectId: string, triggerType?: string) {
|
||||
return request.get<MaintenancePlan[]>({
|
||||
url: '/api/v1/ops/maintenance-plans',
|
||||
params: { projectId, triggerType }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取维保计划详情
|
||||
export function getMaintenancePlan(id: string) {
|
||||
return request.get<MaintenancePlan>({
|
||||
url: `/api/v1/ops/maintenance-plans/${id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 创建维保计划
|
||||
export function createMaintenancePlan(data: MaintenancePlanForm) {
|
||||
return request.post({
|
||||
url: '/api/v1/ops/maintenance-plans',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新维保计划
|
||||
export function updateMaintenancePlan(id: string, data: MaintenancePlanForm) {
|
||||
return request.put({
|
||||
url: `/api/v1/ops/maintenance-plans/${id}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除/停用维保计划
|
||||
export function deleteMaintenancePlan(id: string) {
|
||||
return request.delete({
|
||||
url: `/api/v1/ops/maintenance-plans/${id}`
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 维保任务 API ====================
|
||||
|
||||
// 获取维保任务列表
|
||||
export function getMaintenanceTasks(params: TaskQueryParams) {
|
||||
return request.get({
|
||||
url: '/api/v1/ops/maintenance-tasks',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取维保任务详情
|
||||
export function getMaintenanceTask(id: string) {
|
||||
return request.get<MaintenanceTask>({
|
||||
url: `/api/v1/ops/maintenance-tasks/${id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 接受任务
|
||||
export function acceptMaintenanceTask(id: string, userId: string) {
|
||||
return request.post({
|
||||
url: `/api/v1/ops/maintenance-tasks/${id}/accept`,
|
||||
params: { userId }
|
||||
})
|
||||
}
|
||||
|
||||
// 开始执行任务
|
||||
export function startMaintenanceTask(id: string) {
|
||||
return request.post({
|
||||
url: `/api/v1/ops/maintenance-tasks/${id}/start`
|
||||
})
|
||||
}
|
||||
|
||||
// 完成维保
|
||||
export function completeMaintenanceTask(id: string, data: { completionNotes?: string }) {
|
||||
return request.post({
|
||||
url: `/api/v1/ops/maintenance-tasks/${id}/complete`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 取消任务
|
||||
export function cancelMaintenanceTask(id: string) {
|
||||
return request.post({
|
||||
url: `/api/v1/ops/maintenance-tasks/${id}/cancel`
|
||||
})
|
||||
}
|
||||
|
|
@ -1,26 +1,121 @@
|
|||
import request from '@/utils/request'
|
||||
import type { Project } from '@/types'
|
||||
import type {
|
||||
ProjectQuery,
|
||||
PageResponse,
|
||||
ProjectStatistics,
|
||||
ProjectMember,
|
||||
ProjectConfig,
|
||||
ProjectSelectorItem,
|
||||
StatusChangeRequest,
|
||||
AddMemberRequest
|
||||
} from '@/types/project'
|
||||
|
||||
// ==================== 基础 CRUD ====================
|
||||
|
||||
// PM-001 分页查询项目列表
|
||||
export const queryProjects = (params: ProjectQuery) => {
|
||||
return request.get<PageResponse<Project>>('/api/mdm/projects', { params })
|
||||
}
|
||||
|
||||
// 获取所有项目(兼容旧接口)
|
||||
export const getProjects = () => {
|
||||
return request.get<Project[]>('/projects')
|
||||
return request.get<Project[]>('/api/mdm/projects/all')
|
||||
}
|
||||
|
||||
// 获取项目详情
|
||||
export const getProject = (id: string) => {
|
||||
return request.get<Project>(`/projects/${id}`)
|
||||
return request.get<Project>(`/api/mdm/projects/${id}`)
|
||||
}
|
||||
|
||||
// 根据编码获取项目
|
||||
export const getProjectByCode = (code: string) => {
|
||||
return request.get<Project>(`/projects/code/${code}`)
|
||||
return request.get<Project>(`/api/mdm/projects/code/${code}`)
|
||||
}
|
||||
|
||||
// 创建项目
|
||||
export const createProject = (data: Partial<Project>) => {
|
||||
return request.post<Project>('/projects', data)
|
||||
return request.post<Project>('/api/mdm/projects', data)
|
||||
}
|
||||
|
||||
// 更新项目
|
||||
export const updateProject = (id: string, data: Partial<Project>) => {
|
||||
return request.put<Project>(`/projects/${id}`, data)
|
||||
return request.put<Project>(`/api/mdm/projects/${id}`, data)
|
||||
}
|
||||
|
||||
// 删除项目
|
||||
export const deleteProject = (id: string) => {
|
||||
return request.delete(`/projects/${id}`)
|
||||
return request.delete(`/api/mdm/projects/${id}`)
|
||||
}
|
||||
|
||||
// ==================== 统计数据 ====================
|
||||
|
||||
// PM-002 获取项目统计数据
|
||||
export const getProjectStatistics = (id: string) => {
|
||||
return request.get<ProjectStatistics>(`/api/mdm/projects/${id}/statistics`)
|
||||
}
|
||||
|
||||
// ==================== 成员管理 ====================
|
||||
|
||||
// PM-003 获取项目成员列表
|
||||
export const getProjectMembers = (projectId: string, params?: { page?: number; size?: number }) => {
|
||||
return request.get<PageResponse<ProjectMember>>(`/api/mdm/projects/${projectId}/members`, { params })
|
||||
}
|
||||
|
||||
// 添加项目成员
|
||||
export const addProjectMembers = (projectId: string, data: AddMemberRequest) => {
|
||||
return request.post(`/api/mdm/projects/${projectId}/members`, data)
|
||||
}
|
||||
|
||||
// 移除项目成员
|
||||
export const removeProjectMember = (projectId: string, memberId: string) => {
|
||||
return request.delete(`/api/mdm/projects/${projectId}/members/${memberId}`)
|
||||
}
|
||||
|
||||
// 更新成员角色
|
||||
export const updateMemberRole = (projectId: string, memberId: string, roleInProject: string) => {
|
||||
return request.put(`/api/mdm/projects/${projectId}/members/${memberId}/role`, { roleInProject })
|
||||
}
|
||||
|
||||
// ==================== 编码生成 ====================
|
||||
|
||||
// PM-005 生成项目编码
|
||||
export const generateProjectCode = () => {
|
||||
return request.get<{ code: string }>('/api/mdm/projects/generate-code')
|
||||
}
|
||||
|
||||
// ==================== 状态管理 ====================
|
||||
|
||||
// PM-006 变更项目状态
|
||||
export const changeProjectStatus = (id: string, data: StatusChangeRequest) => {
|
||||
return request.put(`/api/mdm/projects/${id}/status`, data)
|
||||
}
|
||||
|
||||
// 启用项目
|
||||
export const enableProject = (id: string) => {
|
||||
return changeProjectStatus(id, { status: 'ACTIVE' })
|
||||
}
|
||||
|
||||
// 禁用项目
|
||||
export const disableProject = (id: string, reason?: string) => {
|
||||
return changeProjectStatus(id, { status: 'DISABLED', reason })
|
||||
}
|
||||
|
||||
// ==================== 配置管理 ====================
|
||||
|
||||
// PM-008 获取项目配置
|
||||
export const getProjectConfig = (id: string) => {
|
||||
return request.get<ProjectConfig>(`/api/mdm/projects/${id}/config`)
|
||||
}
|
||||
|
||||
// 更新项目配置
|
||||
export const updateProjectConfig = (id: string, data: Partial<ProjectConfig>) => {
|
||||
return request.put<ProjectConfig>(`/api/mdm/projects/${id}/config`, data)
|
||||
}
|
||||
|
||||
// ==================== 选择器 ====================
|
||||
|
||||
// PM-010 获取项目选择器列表
|
||||
export const getProjectSelectorList = (params?: { keyword?: string }) => {
|
||||
return request.get<ProjectSelectorItem[]>('/api/mdm/projects/selector', { params })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import request from '@/utils/request'
|
||||
import type { Role } from '@/types'
|
||||
import type { Role, Permission } from '@/types'
|
||||
|
||||
export const getRoles = () => {
|
||||
return request.get<Role[]>('/api/roles')
|
||||
|
|
@ -9,6 +9,10 @@ export const getRole = (id: string) => {
|
|||
return request.get<Role>(`/api/roles/${id}`)
|
||||
}
|
||||
|
||||
export const getRolePermissions = (id: string) => {
|
||||
return request.get<Permission[]>(`/api/roles/${id}/permissions`)
|
||||
}
|
||||
|
||||
export const getRolesByProject = (projectId: string) => {
|
||||
return request.get<Role[]>(`/api/roles/project/${projectId}`)
|
||||
}
|
||||
|
|
@ -36,3 +40,7 @@ export const getUserRoles = (userId: string) => {
|
|||
export const removeRoleFromUser = (userId: string, roleId: string) => {
|
||||
return request.delete(`/api/users/${userId}/roles/${roleId}`)
|
||||
}
|
||||
|
||||
export const getRoleUsers = (roleId: string) => {
|
||||
return request.get(`/api/roles/${roleId}/users`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import request from '@/utils/request'
|
||||
import type { SpaceNode, SpaceNodeTree, SpaceNodeCreateForm, SpaceNodeUpdateForm } from '@/types/space'
|
||||
|
||||
export const getSpaceNodes = (projectId: string) => {
|
||||
return request.get<SpaceNode[]>(`/api/v1/mdm/space-nodes/project/${projectId}`)
|
||||
}
|
||||
|
||||
export const getSpaceTree = (projectId: string) => {
|
||||
return request.get<SpaceNodeTree[]>(`/api/v1/mdm/space-nodes/project/${projectId}/tree`)
|
||||
}
|
||||
|
||||
export const getSpaceRoots = (projectId: string) => {
|
||||
return request.get<SpaceNode[]>(`/api/v1/mdm/space-nodes/project/${projectId}/roots`)
|
||||
}
|
||||
|
||||
export const getSpaceChildren = (parentId: string) => {
|
||||
return request.get<SpaceNode[]>(`/api/v1/mdm/space-nodes/parent/${parentId}/children`)
|
||||
}
|
||||
|
||||
export const getSpaceNode = (id: string) => {
|
||||
return request.get<SpaceNode>(`/api/v1/mdm/space-nodes/${id}`)
|
||||
}
|
||||
|
||||
export const getSpaceNodesByType = (projectId: string, nodeType: string) => {
|
||||
return request.get<SpaceNode[]>(`/api/v1/mdm/space-nodes/project/${projectId}/type/${nodeType}`)
|
||||
}
|
||||
|
||||
export const createSpaceNode = (data: SpaceNodeCreateForm) => {
|
||||
return request.post<SpaceNode>('/api/v1/mdm/space-nodes', data)
|
||||
}
|
||||
|
||||
export const updateSpaceNode = (id: string, data: SpaceNodeUpdateForm) => {
|
||||
return request.put<SpaceNode>(`/api/v1/mdm/space-nodes/${id}`, data)
|
||||
}
|
||||
|
||||
export const deleteSpaceNode = (id: string) => {
|
||||
return request.delete(`/api/v1/mdm/space-nodes/${id}`)
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// ==================== 备件相关类型 ====================
|
||||
|
||||
// 备件分类
|
||||
export interface SparePartCategory {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
// 备件
|
||||
export interface SparePart {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
categoryId?: string
|
||||
categoryName?: string
|
||||
projectId: string
|
||||
projectName?: string
|
||||
unit?: string
|
||||
currentStock?: number
|
||||
safeStock?: number
|
||||
lowStockWarning?: boolean
|
||||
description?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
// 备件表单
|
||||
export interface SparePartForm {
|
||||
id?: string
|
||||
name: string
|
||||
code: string
|
||||
categoryId?: string
|
||||
projectId: string
|
||||
unit?: string
|
||||
currentStock?: number
|
||||
safeStock?: number
|
||||
description?: string
|
||||
}
|
||||
|
||||
// 库存记录
|
||||
export interface StockRecord {
|
||||
id: string
|
||||
sparePartId: string
|
||||
sparePartName?: string
|
||||
operationType: 'IN' | 'OUT'
|
||||
quantity: number
|
||||
beforeStock?: number
|
||||
afterStock?: number
|
||||
relatedOrderId?: string
|
||||
relatedOrderNo?: string
|
||||
operatorId?: string
|
||||
operatorName?: string
|
||||
remark?: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
// 入库请求
|
||||
export interface InStockRequest {
|
||||
sparePartId: string
|
||||
quantity: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
// 出库请求
|
||||
export interface OutStockRequest {
|
||||
sparePartId: string
|
||||
quantity: number
|
||||
relatedOrderId?: string
|
||||
relatedOrderNo?: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
// 分页响应
|
||||
export interface PageResponse<T> {
|
||||
content: T[]
|
||||
totalElements: number
|
||||
totalPages: number
|
||||
size: number
|
||||
number: number
|
||||
}
|
||||
|
||||
// ==================== 备件分类 API ====================
|
||||
|
||||
// 获取分类列表
|
||||
export function getSparePartCategories() {
|
||||
return request.get<SparePartCategory[]>('/api/v1/ops/spare-parts/categories')
|
||||
}
|
||||
|
||||
// 创建分类
|
||||
export function createSparePartCategory(data: { name: string; description?: string }) {
|
||||
return request.post('/api/v1/ops/spare-parts/categories', data)
|
||||
}
|
||||
|
||||
// ==================== 备件 API ====================
|
||||
|
||||
// 获取备件列表
|
||||
export function getSparePartList(projectId: string, categoryId?: string) {
|
||||
return request.get<PageResponse<SparePart>>('/api/v1/ops/spare-parts', {
|
||||
params: { projectId, categoryId }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取备件详情
|
||||
export function getSparePartDetail(id: string) {
|
||||
return request.get<SparePart>(`/api/v1/ops/spare-parts/${id}`)
|
||||
}
|
||||
|
||||
// 创建备件
|
||||
export function createSparePart(data: SparePartForm) {
|
||||
return request.post('/api/v1/ops/spare-parts', data)
|
||||
}
|
||||
|
||||
// 更新备件
|
||||
export function updateSparePart(id: string, data: SparePartForm) {
|
||||
return request.put(`/api/v1/ops/spare-parts/${id}`, data)
|
||||
}
|
||||
|
||||
// 删除备件
|
||||
export function deleteSparePart(id: string) {
|
||||
return request.delete(`/api/v1/ops/spare-parts/${id}`)
|
||||
}
|
||||
|
||||
// 获取低库存备件
|
||||
export function getLowStockSpareParts(projectId: string) {
|
||||
return request.get<SparePart[]>('/api/v1/ops/spare-parts/low-stock', {
|
||||
params: { projectId }
|
||||
})
|
||||
}
|
||||
|
||||
// 入库
|
||||
export function inStock(data: InStockRequest) {
|
||||
return request.post('/api/v1/ops/spare-parts/in-stock', data)
|
||||
}
|
||||
|
||||
// 出库
|
||||
export function outStock(data: OutStockRequest) {
|
||||
return request.post('/api/v1/ops/spare-parts/out-stock', data)
|
||||
}
|
||||
|
||||
// 获取备件记录
|
||||
export function getSparePartRecords(id: string) {
|
||||
return request.get<StockRecord[]>(`/api/v1/ops/spare-parts/${id}/records`)
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
export function getConfig() {
|
||||
return request({
|
||||
url: '/api/config',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function updateConfig(data: Record<string, string>) {
|
||||
return request({
|
||||
url: '/api/config',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
|
@ -2,12 +2,18 @@
|
|||
import { useRouter } from 'vue-router'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
title?: string
|
||||
showBack?: boolean
|
||||
actions?: { icon: string; text: string; onClick?: () => void }[]
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: ''
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
(e: 'back'): void
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
|
|
@ -24,7 +30,10 @@ const handleBack = () => {
|
|||
<path d="M12.5 15L7.5 10L12.5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<h2 class="page-title">{{ title }}</h2>
|
||||
<h2 v-if="title" class="page-title">{{ title }}</h2>
|
||||
<template v-else>
|
||||
<slot name="title"></slot>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="actions && actions.length > 0" class="page-header-actions">
|
||||
<button
|
||||
|
|
@ -36,6 +45,7 @@ const handleBack = () => {
|
|||
{{ action.text }}
|
||||
</button>
|
||||
</div>
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
|
||||
interface Props {
|
||||
current?: number
|
||||
|
|
@ -34,16 +35,18 @@ const handleChange = (page: number, size: number) => {
|
|||
|
||||
<template>
|
||||
<div class="table-pagination">
|
||||
<a-pagination
|
||||
v-model:current="current"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
:show-size-changer="true"
|
||||
:show-quick-jumper="true"
|
||||
:page-size-options="pageSizes.map(String)"
|
||||
:show-total="(total: number) => `共 ${total} 条`"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<a-config-provider :locale="zhCN">
|
||||
<a-pagination
|
||||
v-model:current="current"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
:show-size-changer="true"
|
||||
:show-quick-jumper="true"
|
||||
:page-size-options="pageSizes.map(String)"
|
||||
:show-total="(total: number) => `共 ${total} 条`"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</a-config-provider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,309 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { Tree, Button, Space, Card, Descriptions, DescriptionsItem, Table, Tag, message, Drawer, Form, Input, Select, Popconfirm, Empty } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||
import type { DataNode } from 'ant-design-vue/es/tree'
|
||||
import {
|
||||
getSpaceTree,
|
||||
getSpaceNode,
|
||||
createSpaceNode,
|
||||
updateSpaceNode,
|
||||
deleteSpaceNode
|
||||
} from '@/api/space'
|
||||
import type { SpaceNode, SpaceNodeCreateForm, SpaceNodeType, SpaceNodeCategory } from '@/types/space'
|
||||
import { SpaceNodeTypeMap, SpaceNodeCategoryMap } from '@/types/space'
|
||||
|
||||
interface Props {
|
||||
projectId?: string
|
||||
mode?: 'view' | 'edit'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
mode: 'view'
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const treeLoading = ref(false)
|
||||
const selectedNode = ref<SpaceNode | null>(null)
|
||||
const treeData = ref<SpaceNodeTree[]>([])
|
||||
|
||||
interface SpaceNodeTree extends SpaceNode {
|
||||
children?: SpaceNodeTree[]
|
||||
}
|
||||
|
||||
const drawerVisible = ref(false)
|
||||
const drawerTitle = ref('')
|
||||
const submitting = ref(false)
|
||||
const formState = ref<SpaceNodeCreateForm>({
|
||||
projectId: props.projectId,
|
||||
name: '',
|
||||
nodeCategory: 'BUILDING',
|
||||
nodeType: 'BUILDING',
|
||||
parentId: undefined,
|
||||
sortOrder: 0,
|
||||
status: 'ACTIVE'
|
||||
})
|
||||
|
||||
const expandedKeys = ref<string[]>([])
|
||||
const selectedKeys = ref<string[]>([])
|
||||
|
||||
const categoryOptions = Object.entries(SpaceNodeCategoryMap).map(([value, { label }]) => ({ value, label }))
|
||||
const typeOptions = computed(() => {
|
||||
const category = formState.value.nodeCategory
|
||||
if (category === 'BUILDING') {
|
||||
return Object.entries(SpaceNodeTypeMap).filter(([key]) => ['BUILDING', 'UNIT', 'FLOOR', 'ROOM'].includes(key)).map(([value, { label }]) => ({ value, label }))
|
||||
} else if (category === 'FACILITY') {
|
||||
return Object.entries(SpaceNodeTypeMap).filter(([key]) => ['EQUIPMENT_ROOM', 'ELECTRIC_ROOM', 'WATER_ROOM', 'PARKING_LOT', 'STORAGE'].includes(key)).map(([value, { label }]) => ({ value, label }))
|
||||
} else if (category === 'OUTDOOR') {
|
||||
return Object.entries(SpaceNodeTypeMap).filter(([key]) => ['GREEN_AREA', 'ROAD', 'PARKING_SPACE'].includes(key)).map(([value, { label }]) => ({ value, label }))
|
||||
}
|
||||
return Object.entries(SpaceNodeTypeMap).map(([value, { label }]) => ({ value, label }))
|
||||
})
|
||||
|
||||
const fetchTree = async () => {
|
||||
if (!props.projectId) return
|
||||
treeLoading.value = true
|
||||
try {
|
||||
const res = await getSpaceTree(props.projectId)
|
||||
treeData.value = res.data.data || []
|
||||
if (treeData.value.length > 0 && expandedKeys.value.length === 0) {
|
||||
expandedKeys.value = [treeData.value[0].id]
|
||||
selectedKeys.value = [treeData.value[0].id]
|
||||
selectedNode.value = treeData.value[0]
|
||||
}
|
||||
} catch {
|
||||
message.error('获取空间树失败')
|
||||
} finally {
|
||||
treeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const transformToTreeData = (nodes: SpaceNodeTree[]): DataNode[] => {
|
||||
return nodes.map(node => ({
|
||||
key: node.id,
|
||||
title: node.name,
|
||||
isLeaf: !node.children || node.children.length === 0,
|
||||
children: node.children ? transformToTreeData(node.children) : undefined
|
||||
}))
|
||||
}
|
||||
|
||||
const handleTreeSelect = async (keys: string[]) => {
|
||||
if (keys.length === 0) return
|
||||
const nodeId = keys[0]
|
||||
selectedKeys.value = [nodeId]
|
||||
try {
|
||||
const res = await getSpaceNode(nodeId)
|
||||
selectedNode.value = res.data.data
|
||||
} catch {
|
||||
message.error('获取节点详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleTreeExpand = (keys: string[]) => {
|
||||
expandedKeys.value = keys
|
||||
}
|
||||
|
||||
const handleAdd = (parentId?: string) => {
|
||||
drawerTitle.value = parentId ? '新增子节点' : '新增根节点'
|
||||
formState.value = {
|
||||
projectId: props.projectId,
|
||||
name: '',
|
||||
nodeCategory: 'BUILDING',
|
||||
nodeType: 'BUILDING',
|
||||
parentId: parentId,
|
||||
sortOrder: 0,
|
||||
status: 'ACTIVE'
|
||||
}
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
if (!selectedNode.value) return
|
||||
drawerTitle.value = '编辑节点'
|
||||
formState.value = {
|
||||
projectId: props.projectId,
|
||||
name: selectedNode.value.name,
|
||||
fullName: selectedNode.value.fullName,
|
||||
shortName: selectedNode.value.shortName,
|
||||
nodeCategory: selectedNode.value.nodeCategory,
|
||||
nodeType: selectedNode.value.nodeType,
|
||||
parentId: selectedNode.value.parentId,
|
||||
sortOrder: selectedNode.value.sortOrder || 0,
|
||||
status: selectedNode.value.status || 'ACTIVE'
|
||||
}
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!selectedNode.value) return
|
||||
try {
|
||||
await deleteSpaceNode(selectedNode.value.id)
|
||||
message.success('删除成功')
|
||||
selectedNode.value = null
|
||||
fetchTree()
|
||||
} catch {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
submitting.value = true
|
||||
if (selectedNode.value && drawerTitle.value === '编辑节点') {
|
||||
await updateSpaceNode(selectedNode.value.id, formState.value)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await createSpaceNode(formState.value)
|
||||
message.success('创建成功')
|
||||
}
|
||||
drawerVisible.value = false
|
||||
fetchTree()
|
||||
} catch {
|
||||
message.error('操作失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.projectId, () => {
|
||||
if (props.projectId) {
|
||||
fetchTree()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const columns: ColumnsType = [
|
||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '类型', dataIndex: 'nodeType', key: 'nodeType' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-manage-container">
|
||||
<Card v-if="projectId" size="small" :bodyStyle="{ padding: '12px' }">
|
||||
<div class="space-header">
|
||||
<Space>
|
||||
<Button type="primary" size="small" @click="handleAdd()">
|
||||
<PlusOutlined /> 新增根节点
|
||||
</Button>
|
||||
<Button size="small" :disabled="!selectedNode" @click="handleAdd(selectedNode?.id)">
|
||||
<PlusOutlined /> 新增子节点
|
||||
</Button>
|
||||
<Button size="small" :disabled="!selectedNode" @click="handleEdit">
|
||||
<EditOutlined /> 编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
v-if="selectedNode"
|
||||
title="确认删除?"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<Button size="small" danger :disabled="!selectedNode">
|
||||
<DeleteOutlined /> 删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</div>
|
||||
<div class="space-content">
|
||||
<div class="space-tree">
|
||||
<Tree
|
||||
v-if="treeData.length > 0"
|
||||
:treeData="transformToTreeData(treeData)"
|
||||
:expandedKeys="expandedKeys"
|
||||
:selectedKeys="selectedKeys"
|
||||
:loading="treeLoading"
|
||||
:show-icon="true"
|
||||
@select="handleTreeSelect"
|
||||
@expand="handleTreeExpand"
|
||||
/>
|
||||
<Empty v-else description="暂无空间数据" />
|
||||
</div>
|
||||
<div class="space-detail">
|
||||
<template v-if="selectedNode">
|
||||
<Descriptions :column="1" size="small" bordered>
|
||||
<DescriptionsItem label="名称">{{ selectedNode.name }}</DescriptionsItem>
|
||||
<DescriptionsItem label="类型">{{ SpaceNodeTypeMap[selectedNode.nodeType as SpaceNodeType]?.label || selectedNode.nodeType }}</DescriptionsItem>
|
||||
<DescriptionsItem label="状态">
|
||||
<Tag :color="selectedNode.status === 'ACTIVE' ? 'green' : 'red'">
|
||||
{{ selectedNode.status === 'ACTIVE' ? '正常' : '禁用' }}
|
||||
</Tag>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="全称">{{ selectedNode.fullName || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="简称">{{ selectedNode.shortName || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="建筑面积">{{ selectedNode.buildingArea || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="地址">{{ selectedNode.address || '-' }}</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</template>
|
||||
<Empty v-else description="请选择空间节点" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div v-else style="text-align: center; padding: 40px; color: #999">
|
||||
请先保存项目后再管理空间
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
v-model:open="drawerVisible"
|
||||
:title="drawerTitle"
|
||||
width="400px"
|
||||
@close="drawerVisible = false"
|
||||
>
|
||||
<Form :model="formState" layout="vertical">
|
||||
<Form.Item label="节点大类" name="nodeCategory">
|
||||
<Select v-model:value="formState.nodeCategory" :options="categoryOptions" />
|
||||
</Form.Item>
|
||||
<Form.Item label="节点类型" name="nodeType">
|
||||
<Select v-model:value="formState.nodeType" :options="typeOptions" />
|
||||
</Form.Item>
|
||||
<Form.Item label="名称" name="name">
|
||||
<Input v-model:value="formState.name" placeholder="请输入名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="全称" name="fullName">
|
||||
<Input v-model:value="formState.fullName" placeholder="请输入全称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简称" name="shortName">
|
||||
<Input v-model:value="formState.shortName" placeholder="请输入简称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Select v-model:value="formState.status" :options="[{ value: 'ACTIVE', label: '正常' }, { value: 'DISABLED', label: '禁用' }]" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="drawerVisible = false">取消</Button>
|
||||
<Button type="primary" :loading="submitting" @click="handleSubmit">确定</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.space-manage-container {
|
||||
min-height: 400px;
|
||||
}
|
||||
.space-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.space-content {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
min-height: 300px;
|
||||
}
|
||||
.space-tree {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
.space-detail {
|
||||
flex: 1;
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,18 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, h } from 'vue'
|
||||
import { CheckCircleOutlined, CloseCircleFilled, ExclamationCircleFilled, MinusCircleFilled } from '@ant-design/icons-vue'
|
||||
|
||||
interface Props {
|
||||
status: string
|
||||
map?: Record<string, { color: string; label: string }>
|
||||
map?: Record<string, { color: string; label: string; icon?: string }>
|
||||
defaultColor?: string
|
||||
defaultLabel?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
map: () => ({
|
||||
ACTIVE: { color: 'success', label: '正常' },
|
||||
LOCKED: { color: 'warning', label: '锁定' },
|
||||
DISABLED: { color: 'error', label: '禁用' }
|
||||
ACTIVE: { color: 'success', label: '正常', icon: 'check' },
|
||||
ENABLED: { color: 'success', label: '启用', icon: 'check' },
|
||||
LOCKED: { color: 'warning', label: '锁定', icon: 'warning' },
|
||||
DISABLED: { color: 'error', label: '禁用', icon: 'close' }
|
||||
}),
|
||||
defaultColor: 'default',
|
||||
defaultLabel: ''
|
||||
|
|
@ -33,25 +35,43 @@ const bgMap: Record<string, string> = {
|
|||
}
|
||||
|
||||
const currentStatus = computed(() => {
|
||||
return props.map[props.status] || { color: props.defaultColor, label: props.defaultLabel || props.status }
|
||||
return props.map[props.status] || { color: props.defaultColor, label: props.defaultLabel || props.status, icon: 'minus' }
|
||||
})
|
||||
|
||||
const tagColor = computed(() => colorMap[currentStatus.value.color] || colorMap.default)
|
||||
const tagBg = computed(() => bgMap[currentStatus.value.color] || bgMap.default)
|
||||
|
||||
const renderIcon = () => {
|
||||
const icon = currentStatus.value.icon || 'minus'
|
||||
const style = { fontSize: '12px', marginRight: '4px' }
|
||||
switch (icon) {
|
||||
case 'check':
|
||||
return h(CheckCircleOutlined, { style })
|
||||
case 'close':
|
||||
return h(CloseCircleFilled, { style })
|
||||
case 'warning':
|
||||
return h(ExclamationCircleFilled, { style })
|
||||
default:
|
||||
return h(MinusCircleFilled, { style })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="status-tag" :style="{ color: tagColor, backgroundColor: tagBg }">
|
||||
<component :is="renderIcon" />
|
||||
{{ currentStatus.label }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,92 +1,201 @@
|
|||
<script setup lang="ts">
|
||||
import { EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||
import { Popconfirm } from 'ant-design-vue'
|
||||
import { EllipsisOutlined } from '@ant-design/icons-vue'
|
||||
import { Dropdown, Menu, Popconfirm } from 'ant-design-vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface ActionItem {
|
||||
key: string
|
||||
label: string
|
||||
icon?: any
|
||||
danger?: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
actions?: ActionItem[]
|
||||
showView?: boolean
|
||||
showEdit?: boolean
|
||||
showDelete?: boolean
|
||||
viewText?: string
|
||||
editText?: string
|
||||
deleteText?: string
|
||||
deleteTitle?: string
|
||||
deleteDescription?: string
|
||||
maxVisible?: number
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
actions: () => [],
|
||||
showView: false,
|
||||
showEdit: true,
|
||||
showDelete: true,
|
||||
viewText: '查看',
|
||||
editText: '编辑',
|
||||
deleteText: '删除',
|
||||
deleteTitle: '确认删除',
|
||||
deleteDescription: '删除后不可恢复,是否继续?',
|
||||
maxVisible: 3
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'edit'): void
|
||||
(e: 'delete'): void
|
||||
(e: 'view'): void
|
||||
(e: 'action', key: string): void
|
||||
}>()
|
||||
|
||||
const handleAction = (key: string) => {
|
||||
emit('action', key)
|
||||
// 所有按钮(固定 + actions + 删除)
|
||||
const allActions = computed(() => {
|
||||
const result: ActionItem[] = []
|
||||
// 查看按钮优先
|
||||
if (props.showView) {
|
||||
result.push({ key: 'view', label: props.viewText })
|
||||
}
|
||||
// 编辑按钮
|
||||
if (props.showEdit) {
|
||||
result.push({ key: 'edit', label: props.editText })
|
||||
}
|
||||
// actions 数组
|
||||
result.push(...props.actions)
|
||||
// 删除按钮
|
||||
if (props.showDelete) {
|
||||
result.push({ key: 'delete', label: props.deleteText, danger: true })
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
// 可见按钮数量(3个以内全显示,超过3个只显示2个)
|
||||
const visibleCount = computed(() => {
|
||||
return allActions.value.length <= 3 ? allActions.value.length : 2
|
||||
})
|
||||
|
||||
// 可见按钮
|
||||
const visibleActions = computed(() => {
|
||||
return allActions.value.slice(0, visibleCount.value)
|
||||
})
|
||||
|
||||
// 更多按钮
|
||||
const moreActions = computed(() => {
|
||||
return allActions.value.slice(visibleCount.value)
|
||||
})
|
||||
|
||||
const hasMoreActions = computed(() => moreActions.value.length > 0)
|
||||
|
||||
const handleActionClick = (key: string) => {
|
||||
if (key === 'edit') {
|
||||
emit('edit')
|
||||
} else if (key === 'view') {
|
||||
emit('view')
|
||||
} else if (key === 'delete') {
|
||||
emit('delete')
|
||||
} else {
|
||||
emit('action', key)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMenuClick = (e: { key: string | number }) => {
|
||||
const key = String(e.key)
|
||||
if (key === 'edit') {
|
||||
emit('edit')
|
||||
} else if (key === 'view') {
|
||||
emit('view')
|
||||
} else if (key === 'delete') {
|
||||
emit('delete')
|
||||
} else {
|
||||
emit('action', key)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFixedClick = (key: string) => {
|
||||
if (key === 'view') {
|
||||
emit('view')
|
||||
} else if (key === 'edit') {
|
||||
emit('edit')
|
||||
} else {
|
||||
emit('action', key)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteConfirm = () => {
|
||||
emit('delete')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="table-actions">
|
||||
<!-- 自定义操作 -->
|
||||
<template v-for="action in actions" :key="action.key">
|
||||
<a-button
|
||||
v-if="action.danger"
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
@click="handleAction(action.key)"
|
||||
<span class="table-actions">
|
||||
<!-- 可见按钮 -->
|
||||
<template v-for="action in visibleActions" :key="action.key">
|
||||
<!-- 删除按钮需要 Popconfirm -->
|
||||
<Popconfirm
|
||||
v-if="action.key === 'delete'"
|
||||
:title="deleteTitle"
|
||||
:description="deleteDescription"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDeleteConfirm"
|
||||
>
|
||||
<component v-if="action.icon" :is="action.icon" />
|
||||
{{ action.label }}
|
||||
</a-button>
|
||||
<a-button type="link" danger size="small" class="table-action-btn">
|
||||
{{ action.label }}
|
||||
</a-button>
|
||||
</Popconfirm>
|
||||
<!-- 普通按钮 -->
|
||||
<a-button
|
||||
v-else
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleAction(action.key)"
|
||||
class="table-action-btn"
|
||||
@click="handleFixedClick(action.key)"
|
||||
>
|
||||
<component v-if="action.icon" :is="action.icon" />
|
||||
{{ action.label }}
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 编辑按钮 -->
|
||||
<a-button
|
||||
v-if="showEdit"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="emit('edit')"
|
||||
>
|
||||
<EditOutlined /> {{ editText }}
|
||||
</a-button>
|
||||
|
||||
<!-- 删除按钮 -->
|
||||
<Popconfirm
|
||||
v-if="showDelete"
|
||||
:title="deleteTitle"
|
||||
:description="deleteDescription"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="emit('delete')"
|
||||
>
|
||||
<a-button type="link" danger size="small">
|
||||
<DeleteOutlined /> {{ deleteText }}
|
||||
<!-- 更多操作下拉菜单 -->
|
||||
<Dropdown v-if="hasMoreActions" placement="bottomRight" :overlay-style="{ minWidth: '80px' }">
|
||||
<a-button type="link" size="small" class="table-action-btn more-btn">
|
||||
<EllipsisOutlined />
|
||||
</a-button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
<template #overlay>
|
||||
<Menu @click="handleMenuClick" class="more-menu">
|
||||
<Menu.Item
|
||||
v-for="action in moreActions"
|
||||
:key="action.key"
|
||||
class="more-menu-item"
|
||||
>
|
||||
<span :class="{ 'text-danger': action.danger }">{{ action.label }}</span>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-actions {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.table-action-btn {
|
||||
padding: 0 4px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.table-action-btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
:deep(.more-menu) {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
:deep(.more-menu-item) {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -63,3 +63,6 @@ export { default as PasswordItem } from './PasswordItem/index.vue'
|
|||
// 业务组件 - 详情
|
||||
export { default as DescriptionList } from './DescriptionList/index.vue'
|
||||
export { default as ProfileCard } from './ProfileCard/index.vue'
|
||||
|
||||
// 业务组件 - 空间
|
||||
export { default as SpaceTree } from './SpaceTree/index.vue'
|
||||
|
|
|
|||
|
|
@ -45,11 +45,107 @@ const router = createRouter({
|
|||
component: () => import('@/views/system/Audit.vue'),
|
||||
meta: { title: '审计日志' }
|
||||
},
|
||||
{
|
||||
path: 'system/settings',
|
||||
name: 'Settings',
|
||||
component: () => import('@/views/system/Settings.vue'),
|
||||
meta: { title: '系统设置' }
|
||||
},
|
||||
{
|
||||
path: 'project/list',
|
||||
name: 'ProjectList',
|
||||
component: () => import('@/views/project/List.vue'),
|
||||
meta: { title: '项目管理' }
|
||||
},
|
||||
{
|
||||
path: 'project/detail/:id',
|
||||
name: 'ProjectDetail',
|
||||
component: () => import('@/views/project/Detail.vue'),
|
||||
meta: { title: '项目详情' }
|
||||
},
|
||||
{
|
||||
path: 'project/:id/space',
|
||||
name: 'ProjectSpace',
|
||||
component: () => import('@/views/space/Space.vue'),
|
||||
meta: { title: '空间管理' }
|
||||
},
|
||||
{
|
||||
path: 'equipment/list',
|
||||
name: 'EquipmentList',
|
||||
component: () => import('@/views/equipment/EquipmentList.vue'),
|
||||
meta: { title: '设备管理' }
|
||||
},
|
||||
{
|
||||
path: 'equipment/detail/:id',
|
||||
name: 'EquipmentDetail',
|
||||
component: () => import('@/views/equipment/EquipmentDetail.vue'),
|
||||
meta: { title: '设备详情' }
|
||||
},
|
||||
{
|
||||
path: 'equipment/health',
|
||||
name: 'EquipmentHealth',
|
||||
component: () => import('@/views/equipment/EquipmentHealth.vue'),
|
||||
meta: { title: '设备健康预测' }
|
||||
},
|
||||
{
|
||||
path: 'inspection/templates',
|
||||
name: 'InspectionTemplates',
|
||||
component: () => import('@/views/inspection/TemplateList.vue'),
|
||||
meta: { title: '点检模板' }
|
||||
},
|
||||
{
|
||||
path: 'maintenance/plans',
|
||||
name: 'MaintenancePlans',
|
||||
component: () => import('@/views/maintenance/PlanList.vue'),
|
||||
meta: { title: '维保计划' }
|
||||
},
|
||||
{
|
||||
path: 'maintenance/tasks',
|
||||
name: 'MaintenanceTasks',
|
||||
component: () => import('@/views/maintenance/TaskList.vue'),
|
||||
meta: { title: '维保任务' }
|
||||
},
|
||||
{
|
||||
path: 'energy/meters',
|
||||
name: 'EnergyMeters',
|
||||
component: () => import('@/views/energy/MeterList.vue'),
|
||||
meta: { title: '计量点管理' }
|
||||
},
|
||||
{
|
||||
path: 'energy/consumption',
|
||||
name: 'EnergyConsumption',
|
||||
component: () => import('@/views/energy/ConsumptionRecord.vue'),
|
||||
meta: { title: '能耗录入' }
|
||||
},
|
||||
{
|
||||
path: 'energy/statistics',
|
||||
name: 'EnergyStatistics',
|
||||
component: () => import('@/views/energy/EnergyStatistics.vue'),
|
||||
meta: { title: '能耗统计' }
|
||||
},
|
||||
{
|
||||
path: 'sparepart/list',
|
||||
name: 'SparePartList',
|
||||
component: () => import('@/views/sparepart/SparePartList.vue'),
|
||||
meta: { title: '备件管理' }
|
||||
},
|
||||
{
|
||||
path: 'sparepart/detail/:id',
|
||||
name: 'SparePartDetail',
|
||||
component: () => import('@/views/sparepart/SparePartDetail.vue'),
|
||||
meta: { title: '备件详情' }
|
||||
},
|
||||
{
|
||||
path: 'sparepart/stock/in',
|
||||
name: 'SparePartInStock',
|
||||
component: () => import('@/views/sparepart/StockOperation.vue'),
|
||||
meta: { title: '备件入库' }
|
||||
},
|
||||
{
|
||||
path: 'sparepart/stock/out',
|
||||
name: 'SparePartOutStock',
|
||||
component: () => import('@/views/sparepart/StockOperation.vue'),
|
||||
meta: { title: '备件出库' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,27 +148,58 @@ interface Props {
|
|||
</template>
|
||||
```
|
||||
|
||||
#### TableActions 行操作
|
||||
#### TableActions 行操作(统一组件)
|
||||
|
||||
**使用方式:**
|
||||
```vue
|
||||
<template>
|
||||
<div class="table-actions">
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<EditOutlined /> 编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确认删除?"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDelete(record)"
|
||||
>
|
||||
<a-button type="link" danger size="small">
|
||||
<DeleteOutlined /> 删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 基础用法:编辑 + 删除 -->
|
||||
<TableActions @edit="handleEdit(record)" @delete="handleDelete(record.id)" />
|
||||
|
||||
<!-- 带查看按钮 -->
|
||||
<TableActions show-view @view="handleView(record)" @edit="handleEdit(record)" @delete="handleDelete(record.id)" />
|
||||
|
||||
<!-- 自定义操作 -->
|
||||
<TableActions :actions="[{ key: 'export', label: '导出', danger: false }]" @action="handleAction" />
|
||||
```
|
||||
|
||||
**属性说明:**
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| showView | boolean | false | 是否显示查看按钮 |
|
||||
| showEdit | boolean | true | 是否显示编辑按钮 |
|
||||
| showDelete | boolean | true | 是否显示删除按钮 |
|
||||
| viewText | string | '查看' | 查看按钮文本 |
|
||||
| editText | string | '编辑' | 编辑按钮文本 |
|
||||
| deleteText | string | '删除' | 删除按钮文本 |
|
||||
| deleteTitle | string | '确认删除' | 删除确认标题 |
|
||||
| deleteDescription | string | '删除后不可恢复,是否继续?' | 删除确认描述 |
|
||||
| actions | ActionItem[] | [] | 自定义操作按钮列表 |
|
||||
|
||||
**ActionItem 类型:**
|
||||
```typescript
|
||||
interface ActionItem {
|
||||
key: string // 按钮标识
|
||||
label: string // 按钮文本
|
||||
danger?: boolean // 是否危险操作(红色)
|
||||
}
|
||||
```
|
||||
|
||||
**事件说明:**
|
||||
|
||||
| 事件 | 参数 | 说明 |
|
||||
|------|------|------|
|
||||
| view | - | 点击查看按钮 |
|
||||
| edit | - | 点击编辑按钮 |
|
||||
| delete | - | 点击删除按钮(确认后触发) |
|
||||
| action | key: string | 点击自定义操作按钮 |
|
||||
|
||||
**样式规范:**
|
||||
- 使用 `Space` 组件包裹,间距为 0
|
||||
- 按钮使用 `type="link"` + `size="small"`
|
||||
- 按钮内边距 `padding: 0 4px`
|
||||
- 删除按钮使用 `danger` 属性
|
||||
|
||||
---
|
||||
|
||||
### 4. Detail 详情组件
|
||||
|
|
@ -804,10 +835,12 @@ const handleChange = (value: string) => {
|
|||
#### TableActions 表格行操作
|
||||
| 状态 | 功能 | 说明 |
|
||||
|------|------|------|
|
||||
| ✅ 已支持 | 查看按钮 | showView |
|
||||
| ✅ 已支持 | 编辑按钮 | showEdit |
|
||||
| ✅ 已支持 | 删除按钮 | showDelete |
|
||||
| ✅ 已支持 | 自定义操作 | actions prop |
|
||||
| ✅ 已支持 | 删除确认 | Popconfirm |
|
||||
| ✅ 已支持 | 按钮文本配置 | viewText/editText/deleteText |
|
||||
| 🔲 待开发 | 更多操作 | more-actions dropdown |
|
||||
| 🔲 待开发 | 成功反馈 | success-message |
|
||||
| 🔲 待开发 | 二次确认配置 | confirm-title/description |
|
||||
|
|
|
|||
|
|
@ -55,23 +55,17 @@ export interface Project {
|
|||
name: string
|
||||
description?: string
|
||||
address?: string
|
||||
projectType?: 'RESIDENTIAL' | 'OFFICE' | 'INDUSTRIAL_PARK'
|
||||
province?: string
|
||||
city?: string
|
||||
district?: string
|
||||
status: 'ACTIVE' | 'DISABLED'
|
||||
status: 'ACTIVE' | 'DISABLED' | 'PENDING' | 'ARCHIVED'
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface SpaceNode {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
projectCode: string
|
||||
nodeType: string
|
||||
parentCode?: string
|
||||
building?: string
|
||||
unit?: string
|
||||
floor?: string
|
||||
roomNumber?: string
|
||||
area?: number
|
||||
status: 'ACTIVE' | 'DISABLED'
|
||||
}
|
||||
// 导出项目相关类型
|
||||
export * from './project'
|
||||
|
||||
// 导出空间相关类型
|
||||
export * from './space'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
// 项目状态枚举
|
||||
export type ProjectStatus = 'ACTIVE' | 'DISABLED' | 'PENDING' | 'ARCHIVED'
|
||||
|
||||
// 项目状态映射
|
||||
export const ProjectStatusMap: Record<ProjectStatus, { label: string; color: string }> = {
|
||||
ACTIVE: { label: '正常', color: 'success' },
|
||||
DISABLED: { label: '禁用', color: 'error' },
|
||||
PENDING: { label: '待审核', color: 'warning' },
|
||||
ARCHIVED: { label: '已归档', color: 'default' }
|
||||
}
|
||||
|
||||
// 项目类型枚举
|
||||
export type ProjectType = 'RESIDENTIAL' | 'OFFICE' | 'INDUSTRIAL_PARK'
|
||||
|
||||
// 项目类型映射
|
||||
export const ProjectTypeMap: Record<ProjectType, { label: string; color: string }> = {
|
||||
RESIDENTIAL: { label: '住宅', color: 'green' },
|
||||
OFFICE: { label: '办公', color: 'blue' },
|
||||
INDUSTRIAL_PARK: { label: '产业园区', color: 'purple' }
|
||||
}
|
||||
|
||||
// 项目查询参数
|
||||
export interface ProjectQuery {
|
||||
keyword?: string
|
||||
status?: ProjectStatus
|
||||
page?: number
|
||||
size?: number
|
||||
sort?: string
|
||||
}
|
||||
|
||||
// 分页响应
|
||||
export interface PageResponse<T> {
|
||||
content: T[]
|
||||
totalElements: number
|
||||
totalPages: number
|
||||
size: number
|
||||
number: number
|
||||
first: boolean
|
||||
last: boolean
|
||||
empty: boolean
|
||||
}
|
||||
|
||||
// 项目统计信息
|
||||
export interface ProjectStatistics {
|
||||
memberCount: number
|
||||
buildingCount: number
|
||||
roomCount: number
|
||||
ownerCount: number
|
||||
tenantCount: number
|
||||
activeTaskCount: number
|
||||
completedTaskCount: number
|
||||
}
|
||||
|
||||
// 项目成员
|
||||
export interface ProjectMember {
|
||||
id: string
|
||||
projectId: string
|
||||
userId: string
|
||||
userName: string
|
||||
realName?: string
|
||||
phone?: string
|
||||
roleInProject: string
|
||||
joinedAt: string
|
||||
status: 'ACTIVE' | 'INACTIVE'
|
||||
}
|
||||
|
||||
// 项目成员角色
|
||||
export const ProjectMemberRoleMap: Record<string, { label: string; color: string }> = {
|
||||
PROJECT_MANAGER: { label: '项目经理', color: 'blue' },
|
||||
PROJECT_ADMIN: { label: '项目管理员', color: 'green' },
|
||||
OPERATION_STAFF: { label: '运营人员', color: 'orange' },
|
||||
FINANCE_STAFF: { label: '财务人员', color: 'purple' },
|
||||
VIEWER: { label: '查看者', color: 'default' }
|
||||
}
|
||||
|
||||
// 项目配置
|
||||
export interface ProjectConfig {
|
||||
id: string
|
||||
projectId: string
|
||||
enableReservation: boolean
|
||||
enableVisitor: boolean
|
||||
enableComplaint: boolean
|
||||
enablePayment: boolean
|
||||
enableAnnouncement: boolean
|
||||
enableSurvey: boolean
|
||||
enableVote: boolean
|
||||
enableMaintenance: boolean
|
||||
enableAsset: boolean
|
||||
customConfig?: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// 项目选择器项
|
||||
export interface ProjectSelectorItem {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
status: ProjectStatus
|
||||
address?: string
|
||||
}
|
||||
|
||||
// 项目表单数据
|
||||
export interface ProjectFormData {
|
||||
id?: string
|
||||
name?: string
|
||||
description?: string
|
||||
address?: string
|
||||
projectType?: ProjectType
|
||||
province?: string
|
||||
city?: string
|
||||
district?: string
|
||||
status?: ProjectStatus
|
||||
}
|
||||
|
||||
// 状态变更请求
|
||||
export interface StatusChangeRequest {
|
||||
status: ProjectStatus
|
||||
reason?: string
|
||||
}
|
||||
|
||||
// 添加成员请求
|
||||
export interface AddMemberRequest {
|
||||
userIds: string[]
|
||||
roleInProject: string
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
export type SpaceNodeCategory = 'BUILDING' | 'PARKING' | 'FACILITY' | 'AREA'
|
||||
|
||||
export type SpaceNodeType =
|
||||
| 'BUILDING'
|
||||
| 'UNIT'
|
||||
| 'FLOOR'
|
||||
| 'ROOM'
|
||||
| 'SHOP'
|
||||
| 'GARAGE'
|
||||
| 'PARKING_AREA'
|
||||
| 'PARKING_SPACE'
|
||||
| 'EQUIPMENT_ROOM'
|
||||
| 'PROPERTY_OFFICE'
|
||||
| 'SECURITY_ROOM'
|
||||
| 'PUBLIC_AREA'
|
||||
| 'GREEN_AREA'
|
||||
| 'ROAD'
|
||||
|
||||
export const SpaceNodeCategoryMap: Record<SpaceNodeCategory, { label: string }> = {
|
||||
BUILDING: { label: '建筑空间' },
|
||||
PARKING: { label: '停车空间' },
|
||||
FACILITY: { label: '设施空间' },
|
||||
AREA: { label: '区域空间' }
|
||||
}
|
||||
|
||||
export const SpaceNodeTypeMap: Record<SpaceNodeType, { label: string; category: SpaceNodeCategory }> = {
|
||||
BUILDING: { label: '楼栋', category: 'BUILDING' },
|
||||
UNIT: { label: '单元', category: 'BUILDING' },
|
||||
FLOOR: { label: '楼层', category: 'BUILDING' },
|
||||
ROOM: { label: '房间', category: 'BUILDING' },
|
||||
SHOP: { label: '商铺', category: 'BUILDING' },
|
||||
GARAGE: { label: '车库', category: 'PARKING' },
|
||||
PARKING_AREA: { label: '停车区域', category: 'PARKING' },
|
||||
PARKING_SPACE: { label: '车位', category: 'PARKING' },
|
||||
EQUIPMENT_ROOM: { label: '设备房', category: 'FACILITY' },
|
||||
PROPERTY_OFFICE: { label: '物业用房', category: 'FACILITY' },
|
||||
SECURITY_ROOM: { label: '门岗', category: 'FACILITY' },
|
||||
PUBLIC_AREA: { label: '公共区域', category: 'AREA' },
|
||||
GREEN_AREA: { label: '绿化区域', category: 'AREA' },
|
||||
ROAD: { label: '道路', category: 'AREA' }
|
||||
}
|
||||
|
||||
export interface SpaceNode {
|
||||
id: string
|
||||
projectId: string
|
||||
code: string
|
||||
name: string
|
||||
fullName?: string
|
||||
shortName?: string
|
||||
nodeCategory: SpaceNodeCategory
|
||||
nodeType: SpaceNodeType
|
||||
usageType?: string
|
||||
parentId?: string
|
||||
parentCode?: string
|
||||
treePath?: string
|
||||
treePathName?: string
|
||||
level?: number
|
||||
sortOrder?: number
|
||||
status?: string
|
||||
deliveryStatus?: string
|
||||
decorationStatus?: string
|
||||
buildingArea?: number
|
||||
usableArea?: number
|
||||
sharedArea?: number
|
||||
landArea?: number
|
||||
longitude?: number
|
||||
latitude?: number
|
||||
altitude?: number
|
||||
floorNumber?: number
|
||||
province?: string
|
||||
city?: string
|
||||
district?: string
|
||||
street?: string
|
||||
address?: string
|
||||
attributes?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
createdBy?: string
|
||||
updatedBy?: string
|
||||
isDeleted?: boolean
|
||||
}
|
||||
|
||||
export interface SpaceNodeTree extends SpaceNode {
|
||||
children: SpaceNodeTree[]
|
||||
}
|
||||
|
||||
export interface SpaceNodeCreateForm {
|
||||
projectId: string
|
||||
name: string
|
||||
fullName?: string
|
||||
shortName?: string
|
||||
nodeCategory: SpaceNodeCategory
|
||||
nodeType: SpaceNodeType
|
||||
usageType?: string
|
||||
parentId?: string
|
||||
sortOrder?: number
|
||||
status?: string
|
||||
deliveryStatus?: string
|
||||
decorationStatus?: string
|
||||
buildingArea?: number
|
||||
usableArea?: number
|
||||
sharedArea?: number
|
||||
landArea?: number
|
||||
longitude?: number
|
||||
latitude?: number
|
||||
altitude?: number
|
||||
floorNumber?: number
|
||||
province?: string
|
||||
city?: string
|
||||
district?: string
|
||||
street?: string
|
||||
address?: string
|
||||
attributes?: string
|
||||
}
|
||||
|
||||
export interface SpaceNodeUpdateForm {
|
||||
name?: string
|
||||
fullName?: string
|
||||
shortName?: string
|
||||
nodeCategory?: SpaceNodeCategory
|
||||
nodeType?: SpaceNodeType
|
||||
usageType?: string
|
||||
sortOrder?: number
|
||||
status?: string
|
||||
deliveryStatus?: string
|
||||
decorationStatus?: string
|
||||
buildingArea?: number
|
||||
usableArea?: number
|
||||
sharedArea?: number
|
||||
landArea?: number
|
||||
longitude?: number
|
||||
latitude?: number
|
||||
altitude?: number
|
||||
floorNumber?: number
|
||||
province?: string
|
||||
city?: string
|
||||
district?: string
|
||||
street?: string
|
||||
address?: string
|
||||
attributes?: string
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'ax
|
|||
import type { ApiResponse } from '@/types'
|
||||
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api',
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080',
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -15,17 +15,55 @@ import {
|
|||
UserOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { Col, Row } from 'ant-design-vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const stats = [
|
||||
{ label: '用户总数', value: '1,286', change: '+12.5%', up: true, icon: UserOutlined },
|
||||
{ label: '角色总数', value: '8', change: '-', up: true, icon: TeamOutlined },
|
||||
{ label: '项目总数', value: '24', change: '+8.3%', up: true, icon: ProjectOutlined },
|
||||
{ label: '空间节点', value: '156', change: '-2.1%', up: false, icon: ApartmentOutlined }
|
||||
{ label: '用户总数', value: 1286, change: '+12.5%', up: true, icon: UserOutlined },
|
||||
{ label: '角色总数', value: 8, change: '-', up: true, icon: TeamOutlined },
|
||||
{ label: '项目总数', value: 24, change: '+8.3%', up: true, icon: ProjectOutlined },
|
||||
{ label: '空间节点', value: 156, change: '-2.1%', up: false, icon: ApartmentOutlined }
|
||||
]
|
||||
|
||||
const displayValues = ref(stats.map(() => 0))
|
||||
const animationComplete = ref(stats.map(() => false))
|
||||
|
||||
const easeOutQuart = (t: number): number => {
|
||||
return 1 - Math.pow(1 - t, 4)
|
||||
}
|
||||
|
||||
const animateValue = (index: number, endValue: number, duration: number = 1500) => {
|
||||
const startTime = performance.now()
|
||||
const startValue = 0
|
||||
|
||||
const tick = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const easedProgress = easeOutQuart(progress)
|
||||
const currentValue = Math.round(startValue + (endValue - startValue) * easedProgress)
|
||||
|
||||
displayValues.value[index] = currentValue
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(tick)
|
||||
} else {
|
||||
animationComplete.value[index] = true
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
stats.forEach((stat, index) => {
|
||||
setTimeout(() => {
|
||||
animateValue(index, stat.value, 1500)
|
||||
}, index * 150)
|
||||
})
|
||||
})
|
||||
|
||||
const todos = [
|
||||
{ title: '待处理工单', count: 12 },
|
||||
{ title: '待审核报修', count: 5 },
|
||||
|
|
@ -48,6 +86,38 @@ const notices = [
|
|||
]
|
||||
|
||||
const chartData = [65, 78, 52, 91, 68, 85, 73]
|
||||
const displayHeights = ref(chartData.map(() => 0))
|
||||
const chartAnimationComplete = ref(false)
|
||||
|
||||
const animateChart = () => {
|
||||
const duration = 1200
|
||||
const startTime = performance.now()
|
||||
|
||||
const tick = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const easedProgress = easeOutQuart(progress)
|
||||
|
||||
displayHeights.value = chartData.map(v => v * easedProgress)
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(tick)
|
||||
} else {
|
||||
chartAnimationComplete.value = true
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
stats.forEach((stat, index) => {
|
||||
setTimeout(() => {
|
||||
animateValue(index, stat.value, 1500)
|
||||
}, index * 150)
|
||||
})
|
||||
setTimeout(animateChart, 600)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -60,13 +130,15 @@ const chartData = [65, 78, 52, 91, 68, 85, 73]
|
|||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-row">
|
||||
<div v-for="s in stats" :key="s.label" class="stat-card">
|
||||
<div v-for="(s, index) in stats" :key="s.label" class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<component :is="s.icon" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">{{ s.label }}</div>
|
||||
<div class="stat-value">{{ s.value }}</div>
|
||||
<div class="stat-value" :class="{ 'counting': !animationComplete[index] }">
|
||||
{{ displayValues[index].toLocaleString() }}
|
||||
</div>
|
||||
<div v-if="s.change !== '-'" class="stat-change" :class="s.up ? 'up' : 'down'">
|
||||
<component :is="s.up ? ArrowUpOutlined : ArrowDownOutlined" />
|
||||
{{ s.change }}
|
||||
|
|
@ -84,7 +156,7 @@ const chartData = [65, 78, 52, 91, 68, 85, 73]
|
|||
</h3>
|
||||
<div class="chart">
|
||||
<div v-for="(v, i) in chartData" :key="i" class="bar-item">
|
||||
<div class="bar" :style="{ height: v + '%' }"></div>
|
||||
<div class="bar" :style="{ height: displayHeights[i] + '%' }"></div>
|
||||
<span class="bar-label">周{{ ['一', '二', '三', '四', '五', '六', '日'][i] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -186,4 +258,12 @@ const chartData = [65, 78, 52, 91, 68, 85, 73]
|
|||
justify-content: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-value.counting {
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { RouterView, useRouter } from 'vue-router'
|
||||
import { h, computed } from 'vue'
|
||||
import { RouterView, useRouter, useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { Layout, Menu, Button } from 'ant-design-vue'
|
||||
import type { MenuProps } from 'ant-design-vue'
|
||||
|
|
@ -10,21 +10,83 @@ import {
|
|||
TeamOutlined,
|
||||
AppstoreOutlined,
|
||||
BuildOutlined,
|
||||
HeatMapOutlined,
|
||||
LogoutOutlined,
|
||||
AuditOutlined
|
||||
AuditOutlined,
|
||||
SettingOutlined,
|
||||
ToolOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const { Header, Sider, Content } = Layout
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const selectedKeys = computed(() => [route.path])
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{ key: '/dashboard', icon: () => h(DashboardOutlined), label: '仪表盘' },
|
||||
{ key: '/system/users', icon: () => h(UserOutlined), label: '用户管理' },
|
||||
{ key: '/system/roles', icon: () => h(TeamOutlined), label: '角色管理' },
|
||||
{ key: '/system/permissions', icon: () => h(AppstoreOutlined), label: '权限管理' },
|
||||
{ key: '/system/audit', icon: () => h(AuditOutlined), label: '审计日志' },
|
||||
{ key: '/project/list', icon: () => h(BuildOutlined), label: '项目管理' }
|
||||
{
|
||||
key: 'workbench',
|
||||
label: '工作台',
|
||||
type: 'group',
|
||||
children: [
|
||||
{ key: '/dashboard', icon: () => h(DashboardOutlined), label: '仪表盘' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'basic',
|
||||
label: '基础管理',
|
||||
type: 'group',
|
||||
children: [
|
||||
{ key: '/project/list', icon: () => h(BuildOutlined), label: '项目管理' },
|
||||
{ key: '/equipment/list', icon: () => h(ToolOutlined), label: '设备管理' },
|
||||
{ key: '/equipment/health', icon: () => h(ToolOutlined), label: '设备健康预测' },
|
||||
{ key: '/inspection/templates', icon: () => h(ToolOutlined), label: '点检模板' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'operation',
|
||||
label: '运营管理',
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
key: '/sparepart/list',
|
||||
icon: () => h(ToolOutlined),
|
||||
label: '备件管理'
|
||||
},
|
||||
{
|
||||
key: '/maintenance/plans',
|
||||
icon: () => h(ToolOutlined),
|
||||
label: '维保计划'
|
||||
},
|
||||
{
|
||||
key: '/maintenance/tasks',
|
||||
icon: () => h(ToolOutlined),
|
||||
label: '维保任务'
|
||||
},
|
||||
{
|
||||
key: 'energy',
|
||||
icon: () => h(HeatMapOutlined),
|
||||
label: '能耗管理',
|
||||
children: [
|
||||
{ key: '/energy/meters', label: '计量点管理' },
|
||||
{ key: '/energy/consumption', label: '能耗录入' },
|
||||
{ key: '/energy/statistics', label: '能耗统计' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'system',
|
||||
label: '系统管理',
|
||||
type: 'group',
|
||||
children: [
|
||||
{ key: '/system/users', icon: () => h(UserOutlined), label: '用户管理' },
|
||||
{ key: '/system/roles', icon: () => h(TeamOutlined), label: '角色管理' },
|
||||
{ key: '/system/audit', icon: () => h(AuditOutlined), label: '审计日志' },
|
||||
{ key: '/system/settings', icon: () => h(SettingOutlined), label: '系统设置' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const handleMenuClick = (e: any) => {
|
||||
|
|
@ -45,6 +107,7 @@ const handleLogout = async () => {
|
|||
theme="dark"
|
||||
mode="inline"
|
||||
:items="menuItems"
|
||||
:selectedKeys="selectedKeys"
|
||||
@click="handleMenuClick"
|
||||
/>
|
||||
</Sider>
|
||||
|
|
@ -72,4 +135,9 @@ const handleLogout = async () => {
|
|||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 分组标题颜色比菜单项淡 */
|
||||
:deep(.ant-menu-item-group-title) {
|
||||
color: rgba(255, 255, 255, 0.45) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,417 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Button, Select, Space, message, Card, Statistic, Row, Col, Table, DatePicker, InputNumber, Form } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import { ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import {
|
||||
getEnergyMeters,
|
||||
getEnergyMeter,
|
||||
recordEnergyConsumption,
|
||||
getEnergyConsumption,
|
||||
type EnergyMeter,
|
||||
type EnergyConsumption
|
||||
} from '@/api/energy'
|
||||
import { getProjectSelectorList } from '@/api/project'
|
||||
|
||||
// 能源类型映射
|
||||
const energyTypeMap: Record<string, string> = {
|
||||
ELECTRICITY: '电力',
|
||||
WATER: '水',
|
||||
GAS: '燃气',
|
||||
CENTRAL_HEATING: '集中供热',
|
||||
CENTRAL_COOLING: '集中供冷'
|
||||
}
|
||||
|
||||
// 项目选择选项
|
||||
const projectOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 计量点选项
|
||||
const meterOptions = ref<{ value: string; label: string; energyType: string }[]>([])
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
projectId: '',
|
||||
meterId: ''
|
||||
})
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const recordLoading = ref(false)
|
||||
const consumptionList = ref<EnergyConsumption[]>([])
|
||||
const selectedMeter = ref<EnergyMeter | null>(null)
|
||||
const lastRecord = ref<EnergyConsumption | null>(null)
|
||||
|
||||
// 录入表单
|
||||
const formState = reactive({
|
||||
meterId: '',
|
||||
currentReading: undefined as number | undefined,
|
||||
recordedBy: ''
|
||||
})
|
||||
|
||||
// 计算消耗量和费用
|
||||
const calculatedConsumption = computed(() => {
|
||||
if (!selectedMeter.value || formState.currentReading === undefined || lastRecord.value === null) {
|
||||
return { consumption: 0, amount: 0 }
|
||||
}
|
||||
const consumption = formState.currentReading - lastRecord.value.currentReading
|
||||
const amount = consumption * (selectedMeter.value.unitPrice || 0)
|
||||
return { consumption: Math.max(0, consumption), amount }
|
||||
})
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType = [
|
||||
{ title: '记录日期', dataIndex: 'consumptionDate', key: 'consumptionDate', width: 120 },
|
||||
{ title: '上次读数', dataIndex: 'previousReading', key: 'previousReading', width: 120 },
|
||||
{ title: '当前读数', dataIndex: 'currentReading', key: 'currentReading', width: 120 },
|
||||
{ title: '消耗量', dataIndex: 'consumption', key: 'consumption', width: 100 },
|
||||
{ title: '费用(元)', dataIndex: 'amount', key: 'amount', width: 100 },
|
||||
{ title: '记录方式', dataIndex: 'recordMethod', key: 'recordMethod', width: 100 }
|
||||
]
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectSelectorList()
|
||||
projectOptions.value = (res.data.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}))
|
||||
} catch {
|
||||
message.error('获取项目列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取计量点列表
|
||||
const fetchMeters = async () => {
|
||||
if (!queryParams.projectId) {
|
||||
meterOptions.value = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getEnergyMeters(queryParams.projectId)
|
||||
const data = res.data.data
|
||||
const meters: EnergyMeter[] = data.content || data || []
|
||||
meterOptions.value = meters.map(m => ({
|
||||
value: m.id!,
|
||||
label: `${m.meterName} (${m.meterCode})`,
|
||||
energyType: m.energyType
|
||||
}))
|
||||
queryParams.meterId = ''
|
||||
selectedMeter.value = null
|
||||
lastRecord.value = null
|
||||
formState.currentReading = undefined
|
||||
} catch {
|
||||
message.error('获取计量点列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取计量点详情和上条记录
|
||||
const fetchMeterDetail = async () => {
|
||||
if (!queryParams.meterId) {
|
||||
selectedMeter.value = null
|
||||
lastRecord.value = null
|
||||
return
|
||||
}
|
||||
try {
|
||||
const meterRes = await getEnergyMeter(queryParams.meterId)
|
||||
selectedMeter.value = meterRes.data.data
|
||||
|
||||
// 获取历史记录找最近一条
|
||||
const historyRes = await getEnergyConsumption(queryParams.meterId)
|
||||
const history = historyRes.data.data || []
|
||||
if (history.length > 0) {
|
||||
lastRecord.value = history[0]
|
||||
} else {
|
||||
lastRecord.value = null
|
||||
}
|
||||
} catch {
|
||||
message.error('获取计量点详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取历史记录
|
||||
const fetchHistory = async () => {
|
||||
if (!queryParams.meterId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getEnergyConsumption(queryParams.meterId)
|
||||
consumptionList.value = res.data.data || []
|
||||
} catch {
|
||||
message.error('获取能耗记录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 选择项目
|
||||
const handleProjectChange = () => {
|
||||
queryParams.meterId = ''
|
||||
selectedMeter.value = null
|
||||
lastRecord.value = null
|
||||
formState.currentReading = undefined
|
||||
consumptionList.value = []
|
||||
fetchMeters()
|
||||
}
|
||||
|
||||
// 选择计量点
|
||||
const handleMeterChange = () => {
|
||||
fetchMeterDetail()
|
||||
fetchHistory()
|
||||
}
|
||||
|
||||
// 提交记录
|
||||
const handleSubmit = async () => {
|
||||
if (!formState.meterId) {
|
||||
message.warning('请选择计量点')
|
||||
return
|
||||
}
|
||||
if (formState.currentReading === undefined) {
|
||||
message.warning('请输入当前读数')
|
||||
return
|
||||
}
|
||||
if (lastRecord.value && formState.currentReading < lastRecord.value.currentReading) {
|
||||
message.warning('当前读数不能小于上次读数')
|
||||
return
|
||||
}
|
||||
recordLoading.value = true
|
||||
try {
|
||||
await recordEnergyConsumption({
|
||||
meterId: formState.meterId,
|
||||
currentReading: formState.currentReading!,
|
||||
recordedBy: formState.recordedBy
|
||||
})
|
||||
message.success('录入成功')
|
||||
// 刷新数据
|
||||
fetchMeterDetail()
|
||||
fetchHistory()
|
||||
formState.currentReading = undefined
|
||||
} catch {
|
||||
message.error('录入失败')
|
||||
} finally {
|
||||
recordLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.projectId = ''
|
||||
queryParams.meterId = ''
|
||||
selectedMeter.value = null
|
||||
lastRecord.value = null
|
||||
formState.currentReading = undefined
|
||||
formState.recordedBy = ''
|
||||
consumptionList.value = []
|
||||
meterOptions.value = []
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProjects()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">能耗录入</h2>
|
||||
</div>
|
||||
|
||||
<Row :gutter="24">
|
||||
<!-- 左侧:录入表单 -->
|
||||
<Col :span="10">
|
||||
<Card title="录入能耗数据" class="record-card">
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-section">
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="选择项目">
|
||||
<Select
|
||||
v-model:value="queryParams.projectId"
|
||||
placeholder="请选择项目"
|
||||
style="width: 100%"
|
||||
:options="projectOptions"
|
||||
@change="handleProjectChange"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="选择计量点">
|
||||
<Select
|
||||
v-model:value="queryParams.meterId"
|
||||
placeholder="请选择计量点"
|
||||
style="width: 100%"
|
||||
:options="meterOptions"
|
||||
@change="handleMeterChange"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<!-- 计量点信息 -->
|
||||
<div v-if="selectedMeter" class="meter-info">
|
||||
<Row :gutter="16">
|
||||
<Col :span="12">
|
||||
<Statistic title="能源类型" :value="energyTypeMap[selectedMeter.energyType] || selectedMeter.energyType" />
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Statistic title="单价" :value="selectedMeter.unitPrice || 0" suffix="元" :precision="2" />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row :gutter="16" style="margin-top: 16px">
|
||||
<Col :span="12">
|
||||
<Statistic title="额定容量" :value="selectedMeter.ratedCapacity || 0" suffix="kW" />
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Statistic title="安装位置" :value="selectedMeter.installationLocation || '-'" />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<!-- 上次记录 -->
|
||||
<div v-if="lastRecord" class="last-record">
|
||||
<h4>上次记录</h4>
|
||||
<Row :gutter="16">
|
||||
<Col :span="12">
|
||||
<Statistic title="记录日期" :value="lastRecord.consumptionDate" />
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Statistic title="上次读数" :value="lastRecord.currentReading" />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<!-- 录入表单 -->
|
||||
<div class="record-form">
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="当前读数">
|
||||
<InputNumber
|
||||
v-model:value="formState.currentReading"
|
||||
placeholder="请输入当前读数"
|
||||
style="width: 100%"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="记录人">
|
||||
<Select
|
||||
v-model:value="formState.recordedBy"
|
||||
placeholder="请输入记录人"
|
||||
style="width: 100%"
|
||||
allow-clear
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<!-- 计算结果预览 -->
|
||||
<div v-if="formState.currentReading !== undefined" class="calculation-preview">
|
||||
<Row :gutter="16">
|
||||
<Col :span="12">
|
||||
<Statistic title="消耗量" :value="calculatedConsumption.consumption" :precision="2" />
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Statistic title="费用" :value="calculatedConsumption.amount" suffix="元" :precision="2" />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<Space style="width: 100%; justify-content: flex-end; margin-top: 16px">
|
||||
<Button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</Button>
|
||||
<Button type="primary" :loading="recordLoading" @click="handleSubmit">
|
||||
提交记录
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<!-- 右侧:历史记录 -->
|
||||
<Col :span="14">
|
||||
<Card title="历史记录" class="history-card">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="consumptionList"
|
||||
:loading="loading"
|
||||
:row-key="(record: EnergyConsumption) => record.id || record.consumptionDate"
|
||||
:pagination="{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
}"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'consumption'">
|
||||
{{ record.consumption }} kWh
|
||||
</template>
|
||||
<template v-else-if="column.key === 'amount'">
|
||||
{{ record.amount ? `¥${record.amount.toFixed(2)}` : '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'recordMethod'">
|
||||
{{ record.recordMethod === 'MANUAL' ? '手动录入' : '自动抄表' }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty v-if="!queryParams.meterId" description="请先选择计量点" />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.record-card,
|
||||
.history-card {
|
||||
height: calc(100vh - 200px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.meter-info {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.last-record {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.last-record h4 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.record-form {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.calculation-preview {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: #e6f7ff;
|
||||
border: 1px solid #91d5ff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { Button, Select, Space, message, Row, Col, Card, Statistic, Table, DatePicker } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import { getConsumptionByType, getUnitConsumption } from '@/api/energy'
|
||||
import { getProjectSelectorList } from '@/api/project'
|
||||
|
||||
// 能源类型映射
|
||||
const energyTypeMap: Record<string, { color: string; text: string }> = {
|
||||
ELECTRICITY: { color: '#faad14', text: '电力' },
|
||||
WATER: { color: '#1890ff', text: '水' },
|
||||
GAS: { color: '#fa541c', text: '燃气' },
|
||||
CENTRAL_HEATING: { color: '#f5222d', text: '集中供热' },
|
||||
CENTRAL_COOLING: { color: '#13c2c2', text: '集中供冷' }
|
||||
}
|
||||
|
||||
// 项目选择选项
|
||||
const projectOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
projectId: '',
|
||||
month: ''
|
||||
})
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const byTypeData = ref<{ energyType: string; consumption: number; amount: number }[]>([])
|
||||
const unitData = ref<{ indicatorName: string; value: number; unit: string }[]>([])
|
||||
|
||||
// 总计
|
||||
const totalConsumption = ref(0)
|
||||
const totalAmount = ref(0)
|
||||
|
||||
// 表格列定义(分项统计)
|
||||
const byTypeColumns: ColumnsType = [
|
||||
{ title: '能源类型', dataIndex: 'energyType', key: 'energyType', width: 150 },
|
||||
{ title: '消耗量(kWh)', dataIndex: 'consumption', key: 'consumption', width: 150 },
|
||||
{ title: '费用(元)', dataIndex: 'amount', key: 'amount', width: 150 },
|
||||
{
|
||||
title: '占比',
|
||||
key: 'percentage',
|
||||
width: 150,
|
||||
customRender: ({ record }: { record: { consumption: number } }) => {
|
||||
const pct = totalConsumption.value > 0
|
||||
? ((record.consumption / totalConsumption.value) * 100).toFixed(1)
|
||||
: '0.0'
|
||||
return `${pct}%`
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 单方能耗列定义
|
||||
const unitColumns: ColumnsType = [
|
||||
{ title: '指标名称', dataIndex: 'indicatorName', key: 'indicatorName', width: 200 },
|
||||
{ title: '指标值', dataIndex: 'value', key: 'value', width: 150 },
|
||||
{ title: '单位', dataIndex: 'unit', key: 'unit', width: 100 }
|
||||
]
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectSelectorList()
|
||||
projectOptions.value = (res.data.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}))
|
||||
} catch {
|
||||
message.error('获取项目列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取月度格式化
|
||||
const getCurrentMonth = () => {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
return `${year}-${month}`
|
||||
}
|
||||
|
||||
// 获取分项统计数据
|
||||
const fetchByTypeData = async () => {
|
||||
if (!queryParams.projectId || !queryParams.month) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getConsumptionByType(queryParams.projectId, queryParams.month)
|
||||
byTypeData.value = res.data.data || []
|
||||
// 计算总计
|
||||
totalConsumption.value = byTypeData.value.reduce((sum, item) => sum + item.consumption, 0)
|
||||
totalAmount.value = byTypeData.value.reduce((sum, item) => sum + item.amount, 0)
|
||||
} catch {
|
||||
message.error('获取分项统计数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取单方能耗数据
|
||||
const fetchUnitData = async () => {
|
||||
if (!queryParams.projectId || !queryParams.month) return
|
||||
try {
|
||||
const res = await getUnitConsumption(queryParams.projectId, queryParams.month)
|
||||
unitData.value = res.data.data || []
|
||||
} catch {
|
||||
message.error('获取单方能耗数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
if (!queryParams.projectId) {
|
||||
message.warning('请先选择项目')
|
||||
return
|
||||
}
|
||||
if (!queryParams.month) {
|
||||
message.warning('请选择月份')
|
||||
return
|
||||
}
|
||||
fetchByTypeData()
|
||||
fetchUnitData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.projectId = ''
|
||||
queryParams.month = ''
|
||||
byTypeData.value = []
|
||||
unitData.value = []
|
||||
totalConsumption.value = 0
|
||||
totalAmount.value = 0
|
||||
}
|
||||
|
||||
// 获取能源类型标签
|
||||
const getEnergyTypeTag = (type: string) => {
|
||||
return energyTypeMap[type] || { color: 'default', text: type }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProjects()
|
||||
queryParams.month = getCurrentMonth()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">能耗统计</h2>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<Space>
|
||||
<Select
|
||||
v-model:value="queryParams.projectId"
|
||||
placeholder="请选择项目"
|
||||
style="width: 240px"
|
||||
allow-clear
|
||||
:options="projectOptions"
|
||||
/>
|
||||
<DatePicker
|
||||
v-model:value="queryParams.month"
|
||||
picker="month"
|
||||
placeholder="选择月份"
|
||||
style="width: 140px"
|
||||
:format="(value: any) => value ? value.format('YYYY-MM') : ''"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
<SearchOutlined /> 查询
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<Row :gutter="16" style="margin-bottom: 24px">
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic
|
||||
title="月度总消耗"
|
||||
:value="totalConsumption"
|
||||
suffix="kWh"
|
||||
:precision="2"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic
|
||||
title="月度总费用"
|
||||
:value="totalAmount"
|
||||
prefix="¥"
|
||||
:precision="2"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic
|
||||
title="计量点数量"
|
||||
:value="byTypeData.length"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic
|
||||
title="统计月份"
|
||||
:value="queryParams.month || '-'"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 分项统计饼图区域 -->
|
||||
<Row :gutter="24" style="margin-bottom: 24px">
|
||||
<Col :span="24">
|
||||
<Card title="分项能耗构成">
|
||||
<!-- 简单饼图实现 -->
|
||||
<div class="pie-container">
|
||||
<div class="pie-chart">
|
||||
<div
|
||||
v-for="(item, index) in byTypeData"
|
||||
:key="item.energyType"
|
||||
class="pie-segment"
|
||||
:style="{
|
||||
'--percentage': `${(item.consumption / totalConsumption) * 100 || 0}%`,
|
||||
'--color': getEnergyTypeTag(item.energyType).color,
|
||||
'--index': index
|
||||
}"
|
||||
>
|
||||
<div class="pie-label">
|
||||
<span class="pie-color" :style="{ background: getEnergyTypeTag(item.energyType).color }"></span>
|
||||
<span class="pie-text">{{ getEnergyTypeTag(item.energyType).text }}</span>
|
||||
<span class="pie-value">{{ ((item.consumption / totalConsumption) * 100 || 0).toFixed(1) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row :gutter="24">
|
||||
<!-- 分项能耗表格 -->
|
||||
<Col :span="12">
|
||||
<Card title="分项能耗明细">
|
||||
<a-table
|
||||
:columns="byTypeColumns"
|
||||
:data-source="byTypeData"
|
||||
:loading="loading"
|
||||
:row-key="(record: any) => record.energyType"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'energyType'">
|
||||
<span :style="{ color: getEnergyTypeTag(record.energyType).color }">
|
||||
{{ getEnergyTypeTag(record.energyType).text }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty v-if="!queryParams.projectId || byTypeData.length === 0" description="请先选择项目和月份进行查询" />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<!-- 单方能耗指标 -->
|
||||
<Col :span="12">
|
||||
<Card title="单方能耗指标">
|
||||
<a-table
|
||||
:columns="unitColumns"
|
||||
:data-source="unitData"
|
||||
:loading="loading"
|
||||
:row-key="(record: any) => record.indicatorName"
|
||||
:pagination="false"
|
||||
/>
|
||||
<a-empty v-if="!queryParams.projectId || unitData.length === 0" description="请先选择项目和月份进行查询" />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 简单饼图样式 */
|
||||
.pie-container {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.pie-chart {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pie-segment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
border-left: 4px solid var(--color);
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.pie-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pie-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.pie-text {
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.pie-value {
|
||||
color: #8c8c8c;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,380 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Button, Select, Space, message, Tag, Modal, Form, Input, InputNumber, Popconfirm } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import {
|
||||
getEnergyMeters,
|
||||
createEnergyMeter,
|
||||
updateEnergyMeter,
|
||||
deleteEnergyMeter,
|
||||
type EnergyMeter
|
||||
} from '@/api/energy'
|
||||
import { getProjectSelectorList } from '@/api/project'
|
||||
import { TableActions, Pagination } from '@/components'
|
||||
|
||||
// 能源类型映射
|
||||
const energyTypeMap: Record<string, { color: string; text: string }> = {
|
||||
ELECTRICITY: { color: 'gold', text: '电力' },
|
||||
WATER: { color: 'blue', text: '水' },
|
||||
GAS: { color: 'orange', text: '燃气' },
|
||||
CENTRAL_HEATING: { color: 'red', text: '集中供热' },
|
||||
CENTRAL_COOLING: { color: 'cyan', text: '集中供冷' }
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType = [
|
||||
{ title: '计量点编码', dataIndex: 'meterCode', key: 'meterCode', width: 150 },
|
||||
{ title: '计量点名称', dataIndex: 'meterName', key: 'meterName', width: 180 },
|
||||
{ title: '能源类型', dataIndex: 'energyType', key: 'energyType', width: 100 },
|
||||
{ title: '安装位置', dataIndex: 'installationLocation', key: 'installationLocation', width: 150 },
|
||||
{ title: '额定容量', dataIndex: 'ratedCapacity', key: 'ratedCapacity', width: 100 },
|
||||
{ title: '单价(元)', dataIndex: 'unitPrice', key: 'unitPrice', width: 100 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 80 },
|
||||
{ title: '操作', key: 'action', width: 120, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// 项目选择选项
|
||||
const projectOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 能源类型选项
|
||||
const energyTypeOptions = [
|
||||
{ value: 'ELECTRICITY', label: '电力' },
|
||||
{ value: 'WATER', label: '水' },
|
||||
{ value: 'GAS', label: '燃气' },
|
||||
{ value: 'CENTRAL_HEATING', label: '集中供热' },
|
||||
{ value: 'CENTRAL_COOLING', label: '集中供冷' }
|
||||
]
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
projectId: '',
|
||||
energyType: undefined as string | undefined
|
||||
})
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const tableData = ref<EnergyMeter[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 新建/编辑 Modal
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('新建计量点')
|
||||
const modalLoading = ref(false)
|
||||
const editingRecord = ref<EnergyMeter | null>(null)
|
||||
const formState = reactive<EnergyMeter>({
|
||||
meterCode: '',
|
||||
meterName: '',
|
||||
energyType: 'ELECTRICITY',
|
||||
installationLocation: '',
|
||||
ratedCapacity: undefined,
|
||||
unitPrice: undefined,
|
||||
status: 'ACTIVE'
|
||||
})
|
||||
|
||||
// 表单验证
|
||||
const rules = {
|
||||
meterCode: [{ required: true, message: '请输入计量点编码' }],
|
||||
meterName: [{ required: true, message: '请输入计量点名称' }],
|
||||
energyType: [{ required: true, message: '请选择能源类型' }]
|
||||
}
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectSelectorList()
|
||||
projectOptions.value = (res.data.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}))
|
||||
} catch {
|
||||
message.error('获取项目列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取计量点列表
|
||||
const fetchMeterList = async () => {
|
||||
if (!queryParams.projectId) {
|
||||
message.warning('请先选择项目')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getEnergyMeters(queryParams.projectId, queryParams.energyType)
|
||||
const data = res.data.data
|
||||
tableData.value = data.content || data || []
|
||||
pagination.total = data.totalElements || (Array.isArray(data) ? data.length : 0)
|
||||
} catch {
|
||||
message.error('获取计量点列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchMeterList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.projectId = ''
|
||||
queryParams.energyType = undefined
|
||||
pagination.current = 1
|
||||
tableData.value = []
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
pagination.current = page
|
||||
pagination.pageSize = pageSize
|
||||
fetchMeterList()
|
||||
}
|
||||
|
||||
// 打开新建 Modal
|
||||
const handleAdd = () => {
|
||||
editingRecord.value = null
|
||||
modalTitle.value = '新建计量点'
|
||||
Object.assign(formState, {
|
||||
id: undefined,
|
||||
meterCode: '',
|
||||
meterName: '',
|
||||
energyType: 'ELECTRICITY',
|
||||
installationLocation: '',
|
||||
ratedCapacity: undefined,
|
||||
unitPrice: undefined,
|
||||
status: 'ACTIVE'
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 打开编辑 Modal
|
||||
const handleEdit = (record: EnergyMeter) => {
|
||||
editingRecord.value = record
|
||||
modalTitle.value = '编辑计量点'
|
||||
Object.assign(formState, {
|
||||
id: record.id,
|
||||
meterCode: record.meterCode,
|
||||
meterName: record.meterName,
|
||||
energyType: record.energyType,
|
||||
installationLocation: record.installationLocation || '',
|
||||
ratedCapacity: record.ratedCapacity,
|
||||
unitPrice: record.unitPrice,
|
||||
status: record.status || 'ACTIVE'
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 删除计量点
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteEnergyMeter(id)
|
||||
message.success('删除成功')
|
||||
fetchMeterList()
|
||||
} catch {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
modalLoading.value = true
|
||||
try {
|
||||
if (editingRecord.value) {
|
||||
await updateEnergyMeter(editingRecord.value.id!, formState)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await createEnergyMeter({ ...formState, status: 'ACTIVE' } as EnergyMeter)
|
||||
message.success('创建成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
fetchMeterList()
|
||||
} catch {
|
||||
message.error(editingRecord.value ? '更新失败' : '创建失败')
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取能源类型标签
|
||||
const getEnergyTypeTag = (type: string) => {
|
||||
const config = energyTypeMap[type] || { color: 'default', text: type }
|
||||
return config
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProjects()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">计量点管理</h2>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<Space>
|
||||
<Select
|
||||
v-model:value="queryParams.projectId"
|
||||
placeholder="请选择项目"
|
||||
style="width: 240px"
|
||||
allow-clear
|
||||
:options="projectOptions"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="queryParams.energyType"
|
||||
placeholder="能源类型"
|
||||
style="width: 140px"
|
||||
allow-clear
|
||||
:options="energyTypeOptions"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
<SearchOutlined /> 查询
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 表格区 -->
|
||||
<div class="table-card">
|
||||
<div class="table-toolbar">
|
||||
<Button type="primary" @click="handleAdd">
|
||||
<PlusOutlined /> 新建计量点
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:row-key="(record: EnergyMeter) => record.id || record.meterCode"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
}"
|
||||
@change="(pag: any) => handlePageChange(pag.current, pag.pageSize)"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'energyType'">
|
||||
<Tag :color="getEnergyTypeTag(record.energyType).color">
|
||||
{{ getEnergyTypeTag(record.energyType).text }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'ratedCapacity'">
|
||||
{{ record.ratedCapacity ? `${record.ratedCapacity} kW` : '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'unitPrice'">
|
||||
{{ record.unitPrice ? `¥${record.unitPrice}` : '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<Tag :color="record.status === 'ACTIVE' ? 'green' : 'red'">
|
||||
{{ record.status === 'ACTIVE' ? '启用' : '停用' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEdit(record)">编辑</Button>
|
||||
<Popconfirm title="确定删除该计量点?" @confirm="handleDelete(record.id!)">
|
||||
<Button type="link" size="small" danger>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 未选择项目提示 -->
|
||||
<a-empty v-if="!queryParams.projectId && tableData.length === 0" description="请先选择项目" />
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑 Modal -->
|
||||
<Modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
@ok="handleSubmit"
|
||||
@cancel="modalVisible = false"
|
||||
width="560px"
|
||||
>
|
||||
<Form
|
||||
:model="formState"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
class="meter-form"
|
||||
>
|
||||
<Form.Item label="计量点编码" name="meterCode">
|
||||
<Input v-model:value="formState.meterCode" placeholder="请输入计量点编码" />
|
||||
</Form.Item>
|
||||
<Form.Item label="计量点名称" name="meterName">
|
||||
<Input v-model:value="formState.meterName" placeholder="请输入计量点名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="能源类型" name="energyType">
|
||||
<Select v-model:value="formState.energyType" :options="energyTypeOptions" />
|
||||
</Form.Item>
|
||||
<Form.Item label="安装位置" name="installationLocation">
|
||||
<Input v-model:value="formState.installationLocation" placeholder="请输入安装位置" />
|
||||
</Form.Item>
|
||||
<Form.Item label="额定容量(kW)" name="ratedCapacity">
|
||||
<InputNumber v-model:value="formState.ratedCapacity" placeholder="请输入额定容量" style="width: 100%" />
|
||||
</Form.Item>
|
||||
<Form.Item label="单价(元)" name="unitPrice">
|
||||
<InputNumber v-model:value="formState.unitPrice" placeholder="请输入单价" style="width: 100%" :precision="2" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.meter-form {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Descriptions, DescriptionsItem, Tabs, TabPane, Tag, message, Spin } from 'ant-design-vue'
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue'
|
||||
import { getEquipmentDetail, type Equipment } from '@/api/equipment'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const equipment = ref<Equipment | null>(null)
|
||||
|
||||
// 获取设备详情
|
||||
const fetchEquipmentDetail = async () => {
|
||||
const id = route.params.id as string
|
||||
if (!id) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getEquipmentDetail(id)
|
||||
equipment.value = res.data.data
|
||||
} catch {
|
||||
message.error('获取设备详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回列表
|
||||
const handleBack = () => {
|
||||
router.push('/equipment/list')
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date: string | Date | undefined) => {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 判断年检状态
|
||||
const getInspectionStatus = (dateStr: string | undefined) => {
|
||||
if (!dateStr) return { color: 'default', text: '未设置' }
|
||||
const date = new Date(dateStr)
|
||||
const now = new Date()
|
||||
const diff = date.getTime() - now.getTime()
|
||||
const days = diff / (1000 * 60 * 60 * 24)
|
||||
|
||||
if (days < 0) {
|
||||
return { color: 'red', text: '已过期' }
|
||||
}
|
||||
if (days <= 30) {
|
||||
return { color: 'orange', text: '即将年检' }
|
||||
}
|
||||
return { color: 'green', text: '正常' }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchEquipmentDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<Spin :spinning="loading">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<div class="page-header-left">
|
||||
<Button type="text" @click="handleBack">
|
||||
<ArrowLeftOutlined /> 返回
|
||||
</Button>
|
||||
<h2 class="page-title">{{ equipment?.name || '设备详情' }}</h2>
|
||||
</div>
|
||||
<div class="page-header-right">
|
||||
<Tag v-if="equipment?.isEquipment" color="blue">设备</Tag>
|
||||
<Tag v-if="equipment?.specialEquipmentType" color="orange">{{ equipment.specialEquipmentType }}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="equipment">
|
||||
<!-- 基本信息 -->
|
||||
<div class="section-card">
|
||||
<h3 class="section-title">基本信息</h3>
|
||||
<Descriptions :column="3" bordered size="small">
|
||||
<DescriptionsItem label="设备编码">{{ equipment.code || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="设备名称">{{ equipment.name || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="所属项目">{{ equipment.projectName || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="安装位置">{{ equipment.spaceNodeName || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="设计寿命">{{ equipment.designLifeYears ? `${equipment.designLifeYears} 年` : '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="创建时间">{{ formatDate(equipment.createdAt) }}</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 标签页详情 -->
|
||||
<div class="section-card">
|
||||
<Tabs>
|
||||
<!-- 技术参数 -->
|
||||
<TabPane key="tech" tab="技术参数">
|
||||
<Descriptions :column="2" bordered size="small">
|
||||
<DescriptionsItem label="额定功率">
|
||||
{{ equipment.ratedPower ? `${equipment.ratedPower} kW` : '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="额定电压">{{ equipment.ratedVoltage || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="额定电流">
|
||||
{{ equipment.ratedCurrent ? `${equipment.ratedCurrent} A` : '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="设计寿命">
|
||||
{{ equipment.designLifeYears ? `${equipment.designLifeYears} 年` : '-' }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</TabPane>
|
||||
|
||||
<!-- 维保信息 -->
|
||||
<TabPane key="maintenance" tab="维保信息">
|
||||
<Descriptions :column="2" bordered size="small">
|
||||
<DescriptionsItem label="维保商">{{ equipment.maintenanceVendor || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="维保商电话">{{ equipment.maintenanceVendorPhone || '-' }}</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</TabPane>
|
||||
|
||||
<!-- 特种设备 -->
|
||||
<TabPane v-if="equipment.specialEquipmentType" key="special" tab="特种设备">
|
||||
<Descriptions :column="2" bordered size="small">
|
||||
<DescriptionsItem label="特种设备类型">
|
||||
<Tag color="orange">{{ equipment.specialEquipmentType }}</Tag>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="年检周期">
|
||||
{{ equipment.inspectionCycle ? `${equipment.inspectionCycle} 月` : '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="下次年检日期">
|
||||
{{ formatDate(equipment.nextInspectionDate) }}
|
||||
<Tag
|
||||
:color="getInspectionStatus(equipment.nextInspectionDate).color"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
{{ getInspectionStatus(equipment.nextInspectionDate).text }}
|
||||
</Tag>
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 无数据 -->
|
||||
<a-empty v-if="!loading && !equipment" description="未找到设备信息" />
|
||||
</Spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.page-header-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,441 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Select, Button, Space, message, Card, Tag, Row, Col, Statistic, Spin } from 'ant-design-vue'
|
||||
import { SearchOutlined, ReloadOutlined, SyncOutlined } from '@ant-design/icons-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import * as echarts from 'echarts'
|
||||
import {
|
||||
getEquipmentHealth,
|
||||
getHealthHistory,
|
||||
getFailureHistory,
|
||||
getEquipmentMTBF,
|
||||
getEquipmentMTTR,
|
||||
calculateHealth,
|
||||
type EquipmentHealth,
|
||||
type HealthHistory,
|
||||
type EquipmentFailure,
|
||||
type MTBFData,
|
||||
type MTTRData
|
||||
} from '@/api/equipment-health'
|
||||
import { getEquipmentList, type Equipment } from '@/api/equipment'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const calculating = ref(false)
|
||||
const selectedEquipmentId = ref<string>('')
|
||||
const projectId = ref<string>('')
|
||||
|
||||
// 数据
|
||||
const equipmentOptions = ref<{ value: string; label: string }[]>([])
|
||||
const healthData = ref<EquipmentHealth | null>(null)
|
||||
const healthHistory = ref<HealthHistory[]>([])
|
||||
const failureHistory = ref<EquipmentFailure[]>([])
|
||||
const mtbfData = ref<MTBFData | null>(null)
|
||||
const mttrData = ref<MTTRData | null>(null)
|
||||
|
||||
// 图表实例
|
||||
let healthChart: echarts.ECharts | null = null
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
const chartDom = document.getElementById('health-chart')
|
||||
if (chartDom) {
|
||||
healthChart = echarts.init(chartDom)
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染健康度趋势图
|
||||
const renderHealthChart = () => {
|
||||
if (!healthChart) return
|
||||
|
||||
const dates = healthHistory.value.map(h => h.recordTime.substring(0, 10))
|
||||
const scores = healthHistory.value.map(h => h.healthScore)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
boundaryGap: false
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
axisLabel: {
|
||||
formatter: '{value}%'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '健康度',
|
||||
type: 'line',
|
||||
data: scores,
|
||||
smooth: true,
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(82, 196, 26, 0.4)' },
|
||||
{ offset: 1, color: 'rgba(82, 196, 26, 0.05)' }
|
||||
])
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#52c41a'
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#52c41a'
|
||||
}
|
||||
}
|
||||
],
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
}
|
||||
}
|
||||
|
||||
healthChart.setOption(option)
|
||||
}
|
||||
|
||||
// 获取设备列表
|
||||
const fetchEquipmentList = async (pId: string) => {
|
||||
if (!pId) return
|
||||
try {
|
||||
const res = await getEquipmentList(pId)
|
||||
const data = res.data.data
|
||||
equipmentOptions.value = (data.content || []).map((item: Equipment) => ({
|
||||
value: item.id,
|
||||
label: `${item.name} (${item.code})`
|
||||
}))
|
||||
} catch {
|
||||
message.error('获取设备列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取健康数据
|
||||
const fetchHealthData = async () => {
|
||||
if (!selectedEquipmentId.value) {
|
||||
message.warning('请先选择设备')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const [healthRes, historyRes, failureRes, mtbfRes, mttrRes] = await Promise.all([
|
||||
getEquipmentHealth(selectedEquipmentId.value),
|
||||
getHealthHistory(selectedEquipmentId.value, 30),
|
||||
getFailureHistory(selectedEquipmentId.value),
|
||||
getEquipmentMTBF(selectedEquipmentId.value),
|
||||
getEquipmentMTTR(selectedEquipmentId.value)
|
||||
])
|
||||
|
||||
healthData.value = healthRes.data.data
|
||||
healthHistory.value = historyRes.data.data || []
|
||||
failureHistory.value = failureRes.data.data || []
|
||||
mtbfData.value = mtbfRes.data.data
|
||||
mttrData.value = mttrRes.data.data
|
||||
|
||||
// 渲染图表
|
||||
setTimeout(() => {
|
||||
initChart()
|
||||
renderHealthChart()
|
||||
}, 100)
|
||||
} catch {
|
||||
message.error('获取健康数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 计算健康度
|
||||
const handleCalculate = async () => {
|
||||
if (!selectedEquipmentId.value) {
|
||||
message.warning('请先选择设备')
|
||||
return
|
||||
}
|
||||
calculating.value = true
|
||||
try {
|
||||
await calculateHealth(selectedEquipmentId.value)
|
||||
message.success('健康度计算完成')
|
||||
fetchHealthData()
|
||||
} catch {
|
||||
message.error('计算健康度失败')
|
||||
} finally {
|
||||
calculating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 健康等级颜色
|
||||
const getHealthLevelColor = (level: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
'优秀': 'green',
|
||||
'良好': 'cyan',
|
||||
'一般': 'orange',
|
||||
'较差': 'red',
|
||||
'危险': 'red'
|
||||
}
|
||||
return colorMap[level] || 'default'
|
||||
}
|
||||
|
||||
// 风险等级颜色
|
||||
const getRiskLevelColor = (level: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
'低': 'green',
|
||||
'中': 'orange',
|
||||
'高': 'red'
|
||||
}
|
||||
return colorMap[level] || 'default'
|
||||
}
|
||||
|
||||
// 故障等级颜色
|
||||
const getFailureLevelColor = (level: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
'轻微': 'green',
|
||||
'中等': 'orange',
|
||||
'严重': 'red'
|
||||
}
|
||||
return colorMap[level] || 'default'
|
||||
}
|
||||
|
||||
// 故障历史列定义
|
||||
const failureColumns: ColumnsType = [
|
||||
{ title: '故障时间', dataIndex: 'failureTime', key: 'failureTime', width: 160 },
|
||||
{ title: '故障类型', dataIndex: 'failureType', key: 'failureType', width: 120 },
|
||||
{ title: '故障等级', dataIndex: 'failureLevel', key: 'failureLevel', width: 100 },
|
||||
{ title: '故障描述', dataIndex: 'description', key: 'description' },
|
||||
{ title: '维修时间', dataIndex: 'repairTime', key: 'repairTime', width: 160 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 }
|
||||
]
|
||||
|
||||
// 项目变更
|
||||
const handleProjectChange = (value: string) => {
|
||||
projectId.value = value
|
||||
selectedEquipmentId.value = ''
|
||||
equipmentOptions.value = []
|
||||
healthData.value = null
|
||||
healthHistory.value = []
|
||||
failureHistory.value = []
|
||||
mtbfData.value = null
|
||||
mttrData.value = null
|
||||
if (value) {
|
||||
fetchEquipmentList(value)
|
||||
}
|
||||
}
|
||||
|
||||
// 设备变更
|
||||
const handleEquipmentChange = (value: string) => {
|
||||
selectedEquipmentId.value = value
|
||||
}
|
||||
|
||||
// 查看设备详情
|
||||
const handleViewEquipment = () => {
|
||||
if (selectedEquipmentId.value) {
|
||||
router.push(`/equipment/detail/${selectedEquipmentId.value}`)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', () => {
|
||||
healthChart?.resize()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">设备健康预测</h2>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<Space>
|
||||
<Select
|
||||
v-model:value="projectId"
|
||||
placeholder="请选择项目"
|
||||
style="width: 240px"
|
||||
@change="handleProjectChange"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="selectedEquipmentId"
|
||||
placeholder="请选择设备"
|
||||
style="width: 280px"
|
||||
:disabled="!projectId"
|
||||
:options="equipmentOptions"
|
||||
@change="handleEquipmentChange"
|
||||
/>
|
||||
<Button type="primary" @click="fetchHealthData" :loading="loading">
|
||||
<SearchOutlined /> 查询
|
||||
</Button>
|
||||
<Button @click="handleCalculate" :loading="calculating">
|
||||
<SyncOutlined /> 计算健康度
|
||||
</Button>
|
||||
<Button @click="handleViewEquipment" :disabled="!selectedEquipmentId">
|
||||
查看设备详情
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Spin :spinning="loading">
|
||||
<!-- 健康度概览 -->
|
||||
<Row :gutter="16" style="margin-bottom: 16px">
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic
|
||||
title="健康度评分"
|
||||
:value="healthData?.healthScore ?? '-'"
|
||||
suffix="分"
|
||||
:value-style="{ color: healthData && healthData.healthScore >= 80 ? '#52c41a' : healthData && healthData.healthScore >= 60 ? '#faad14' : '#ff4d4f' }"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic title="健康等级" :value="healthData?.healthLevel ?? '-'">
|
||||
<template #suffix>
|
||||
<Tag :color="getHealthLevelColor(healthData?.healthLevel || '')" style="margin-left: 8px">
|
||||
{{ healthData?.healthLevel || '-' }}
|
||||
</Tag>
|
||||
</template>
|
||||
</Statistic>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic title="风险等级" :value="healthData?.riskLevel ?? '-'">
|
||||
<template #suffix>
|
||||
<Tag :color="getRiskLevelColor(healthData?.riskLevel || '')" style="margin-left: 8px">
|
||||
{{ healthData?.riskLevel || '-' }}
|
||||
</Tag>
|
||||
</template>
|
||||
</Statistic>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic
|
||||
title="最后检测"
|
||||
:value="healthData?.lastCheckTime ? healthData.lastCheckTime.substring(0, 10) : '-'"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- MTBF / MTTR 指标 -->
|
||||
<Row :gutter="16" style="margin-bottom: 16px">
|
||||
<Col :span="12">
|
||||
<Card title="MTBF (平均故障间隔时间)">
|
||||
<Row :gutter="16">
|
||||
<Col :span="8">
|
||||
<Statistic title="MTBF" :value="mtbfData?.mtbfDays ?? '-'" suffix="天" />
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Statistic title="总故障次数" :value="mtbfData?.totalFailures ?? '-'" />
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Statistic title="总运行时长" :value="mtbfData?.totalOperatingDays ?? '-'" suffix="天" />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Card title="MTTR (平均修复时间)">
|
||||
<Row :gutter="16">
|
||||
<Col :span="8">
|
||||
<Statistic title="MTTR" :value="mttrData?.mttrHours ?? '-'" suffix="小时" />
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Statistic title="总维修次数" :value="mttrData?.totalRepairs ?? '-'" />
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Statistic title="总维修时长" :value="mttrData?.totalRepairTime ?? '-'" suffix="小时" />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 健康度趋势图 -->
|
||||
<Card title="健康度趋势 (近30天)" style="margin-bottom: 16px">
|
||||
<div id="health-chart" style="width: 100%; height: 300px"></div>
|
||||
<a-empty v-if="healthHistory.length === 0 && !loading" description="暂无趋势数据" />
|
||||
</Card>
|
||||
|
||||
<!-- 风险因素和维护建议 -->
|
||||
<Row :gutter="16" style="margin-bottom: 16px" v-if="healthData">
|
||||
<Col :span="12">
|
||||
<Card title="主要风险因素">
|
||||
<div v-if="healthData.mainRiskFactors && healthData.mainRiskFactors.length > 0">
|
||||
<Tag v-for="(factor, index) in healthData.mainRiskFactors" :key="index" color="orange" style="margin-bottom: 8px">
|
||||
{{ factor }}
|
||||
</Tag>
|
||||
</div>
|
||||
<a-empty v-else description="暂无风险因素" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Card title="维护建议">
|
||||
<div v-if="healthData.maintenanceSuggestions && healthData.maintenanceSuggestions.length > 0">
|
||||
<div v-for="(suggestion, index) in healthData.maintenanceSuggestions" :key="index" style="margin-bottom: 8px">
|
||||
<span style="color: #1890ff">{{ index + 1 }}.</span> {{ suggestion }}
|
||||
</div>
|
||||
</div>
|
||||
<a-empty v-else description="暂无维护建议" />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 故障历史 -->
|
||||
<Card title="故障历史">
|
||||
<a-table
|
||||
:columns="failureColumns"
|
||||
:data-source="failureHistory"
|
||||
:row-key="(record: EquipmentFailure) => record.id"
|
||||
:pagination="{ pageSize: 5 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'failureLevel'">
|
||||
<Tag :color="getFailureLevelColor(record.failureLevel)">
|
||||
{{ record.failureLevel }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<Tag :color="record.status === '已修复' ? 'green' : 'orange'">
|
||||
{{ record.status }}
|
||||
</Tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty v-if="failureHistory.length === 0" description="暂无故障记录" />
|
||||
</Card>
|
||||
</Spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,421 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Button, Select, Space, message, Tag, Tabs, Badge } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
ExclamationCircleOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { getEquipmentList, getSpecialEquipment, getExpiringInspection, type Equipment } from '@/api/equipment'
|
||||
import { getProjectSelectorList } from '@/api/project'
|
||||
import { TableActions, Pagination } from '@/components'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date: string | Date | undefined) => {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 判断是否即将过期(30天内)
|
||||
const isExpiringSoon = (dateStr: string | undefined) => {
|
||||
if (!dateStr) return false
|
||||
const date = new Date(dateStr)
|
||||
const now = new Date()
|
||||
const diff = date.getTime() - now.getTime()
|
||||
const days = diff / (1000 * 60 * 60 * 24)
|
||||
return days > 0 && days <= 30
|
||||
}
|
||||
|
||||
// 判断是否已过期
|
||||
const isExpired = (dateStr: string | undefined) => {
|
||||
if (!dateStr) return false
|
||||
const date = new Date(dateStr)
|
||||
const now = new Date()
|
||||
return date < now
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType = [
|
||||
{ title: '设备编码', dataIndex: 'code', key: 'code', width: 140 },
|
||||
{ title: '设备名称', dataIndex: 'name', key: 'name', width: 180 },
|
||||
{ title: '所属项目', dataIndex: 'projectName', key: 'projectName', width: 150 },
|
||||
{ title: '安装位置', dataIndex: 'spaceNodeName', key: 'spaceNodeName', width: 150 },
|
||||
{ title: '额定功率', dataIndex: 'ratedPower', key: 'ratedPower', width: 100 },
|
||||
{ title: '额定电压', dataIndex: 'ratedVoltage', key: 'ratedVoltage', width: 100 },
|
||||
{ title: '特种设备', dataIndex: 'specialEquipmentType', key: 'specialEquipmentType', width: 120 },
|
||||
{ title: '下次年检', dataIndex: 'nextInspectionDate', key: 'nextInspectionDate', width: 120 },
|
||||
{ title: '操作', key: 'action', width: 100, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// 特种设备列定义
|
||||
const specialColumns: ColumnsType = [
|
||||
{ title: '设备编码', dataIndex: 'code', key: 'code', width: 140 },
|
||||
{ title: '设备名称', dataIndex: 'name', key: 'name', width: 180 },
|
||||
{ title: '特种设备类型', dataIndex: 'specialEquipmentType', key: 'specialEquipmentType', width: 120 },
|
||||
{ title: '年检周期', dataIndex: 'inspectionCycle', key: 'inspectionCycle', width: 100 },
|
||||
{ title: '下次年检', dataIndex: 'nextInspectionDate', key: 'nextInspectionDate', width: 120 },
|
||||
{ title: '维保商', dataIndex: 'maintenanceVendor', key: 'maintenanceVendor', width: 150 },
|
||||
{ title: '联系电话', dataIndex: 'maintenanceVendorPhone', key: 'maintenanceVendorPhone', width: 120 },
|
||||
{ title: '操作', key: 'action', width: 100, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// 项目选择选项
|
||||
const projectOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
projectId: '',
|
||||
tabKey: 'all'
|
||||
})
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const tableData = ref<Equipment[]>([])
|
||||
const specialData = ref<Equipment[]>([])
|
||||
const expiringData = ref<Equipment[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 当前展示的数据
|
||||
const displayData = computed(() => {
|
||||
if (queryParams.tabKey === 'special') return specialData.value
|
||||
if (queryParams.tabKey === 'expiring') return expiringData.value
|
||||
return tableData.value
|
||||
})
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectSelectorList()
|
||||
projectOptions.value = (res.data.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}))
|
||||
} catch {
|
||||
message.error('获取项目列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备列表
|
||||
const fetchEquipmentList = async () => {
|
||||
if (!queryParams.projectId) {
|
||||
message.warning('请先选择项目')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getEquipmentList(queryParams.projectId)
|
||||
const data = res.data.data
|
||||
tableData.value = data.content || []
|
||||
pagination.total = data.totalElements || 0
|
||||
} catch {
|
||||
message.error('获取设备列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取特种设备列表
|
||||
const fetchSpecialEquipment = async () => {
|
||||
if (!queryParams.projectId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getSpecialEquipment(queryParams.projectId)
|
||||
specialData.value = res.data.data || []
|
||||
} catch {
|
||||
message.error('获取特种设备列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取即将年检设备
|
||||
const fetchExpiringEquipment = async () => {
|
||||
if (!queryParams.projectId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getExpiringInspection(queryParams.projectId, 90)
|
||||
expiringData.value = res.data.data || []
|
||||
} catch {
|
||||
message.error('获取即将年检设备失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
if (queryParams.tabKey === 'all') {
|
||||
fetchEquipmentList()
|
||||
} else if (queryParams.tabKey === 'special') {
|
||||
fetchSpecialEquipment()
|
||||
} else if (queryParams.tabKey === 'expiring') {
|
||||
fetchExpiringEquipment()
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.projectId = ''
|
||||
pagination.current = 1
|
||||
tableData.value = []
|
||||
specialData.value = []
|
||||
expiringData.value = []
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
pagination.current = page
|
||||
pagination.pageSize = pageSize
|
||||
if (queryParams.tabKey === 'all') {
|
||||
fetchEquipmentList()
|
||||
}
|
||||
}
|
||||
|
||||
// Tab 切换
|
||||
const handleTabChange = (key: string) => {
|
||||
queryParams.tabKey = key
|
||||
pagination.current = 1
|
||||
if (key === 'all') {
|
||||
if (tableData.value.length === 0 && queryParams.projectId) {
|
||||
fetchEquipmentList()
|
||||
}
|
||||
} else if (key === 'special') {
|
||||
if (specialData.value.length === 0 && queryParams.projectId) {
|
||||
fetchSpecialEquipment()
|
||||
}
|
||||
} else if (key === 'expiring') {
|
||||
if (expiringData.value.length === 0 && queryParams.projectId) {
|
||||
fetchExpiringEquipment()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查看设备详情
|
||||
const handleView = (record: Equipment) => {
|
||||
router.push(`/equipment/detail/${record.id}`)
|
||||
}
|
||||
|
||||
// 年检状态标签
|
||||
const getInspectionStatus = (record: Equipment) => {
|
||||
if (!record.nextInspectionDate) return null
|
||||
if (isExpired(record.nextInspectionDate)) {
|
||||
return { color: 'red', text: '已过期' }
|
||||
}
|
||||
if (isExpiringSoon(record.nextInspectionDate)) {
|
||||
return { color: 'orange', text: '即将年检' }
|
||||
}
|
||||
return { color: 'green', text: '正常' }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProjects()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">设备管理</h2>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<Space>
|
||||
<Select
|
||||
v-model:value="queryParams.projectId"
|
||||
placeholder="请选择项目"
|
||||
style="width: 240px"
|
||||
allow-clear
|
||||
:options="projectOptions"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
<SearchOutlined /> 查询
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<div class="table-card">
|
||||
<Tabs v-model:activeKey="queryParams.tabKey" @change="handleTabChange">
|
||||
<Tabs.TabPane key="all">
|
||||
<template #tab>全部设备</template>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="special">
|
||||
<template #tab>特种设备</template>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="expiring">
|
||||
<template #tab>
|
||||
<Badge :count="expiringData.length" :offset="[10, 0]" :overflow-count="99">
|
||||
即将年检
|
||||
</Badge>
|
||||
</template>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
|
||||
<!-- 全部设备表格 -->
|
||||
<a-table
|
||||
v-if="queryParams.tabKey === 'all'"
|
||||
:columns="columns"
|
||||
:data-source="displayData"
|
||||
:loading="loading"
|
||||
:row-key="(record: Equipment) => record.id"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
}"
|
||||
@change="(pag: any) => handlePageChange(pag.current, pag.pageSize)"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'ratedPower'">
|
||||
{{ record.ratedPower ? `${record.ratedPower} kW` : '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'ratedVoltage'">
|
||||
{{ record.ratedVoltage || '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'specialEquipmentType'">
|
||||
<Tag v-if="record.specialEquipmentType" color="orange">
|
||||
{{ record.specialEquipmentType }}
|
||||
</Tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'nextInspectionDate'">
|
||||
<Badge
|
||||
v-if="getInspectionStatus(record)"
|
||||
:status="getInspectionStatus(record)?.color === 'red' ? 'error' : getInspectionStatus(record)?.color === 'orange' ? 'warning' : 'success'"
|
||||
:text="getInspectionStatus(record)?.text"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<TableActions
|
||||
:show-edit="false"
|
||||
:show-delete="false"
|
||||
:actions="[
|
||||
{ key: 'view', label: '查看' }
|
||||
]"
|
||||
@view="handleView(record as Equipment)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 特种设备表格 -->
|
||||
<a-table
|
||||
v-else-if="queryParams.tabKey === 'special'"
|
||||
:columns="specialColumns"
|
||||
:data-source="displayData"
|
||||
:loading="loading"
|
||||
:row-key="(record: Equipment) => record.id"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'inspectionCycle'">
|
||||
{{ record.inspectionCycle ? `${record.inspectionCycle} 月` : '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'nextInspectionDate'">
|
||||
<Badge
|
||||
v-if="getInspectionStatus(record)"
|
||||
:status="getInspectionStatus(record)?.color === 'red' ? 'error' : getInspectionStatus(record)?.color === 'orange' ? 'warning' : 'success'"
|
||||
:text="getInspectionStatus(record)?.text"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<TableActions
|
||||
:show-edit="false"
|
||||
:show-delete="false"
|
||||
:actions="[
|
||||
{ key: 'view', label: '查看' }
|
||||
]"
|
||||
@view="handleView(record as Equipment)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 即将年检表格 -->
|
||||
<a-table
|
||||
v-else-if="queryParams.tabKey === 'expiring'"
|
||||
:columns="specialColumns"
|
||||
:data-source="displayData"
|
||||
:loading="loading"
|
||||
:row-key="(record: Equipment) => record.id"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'inspectionCycle'">
|
||||
{{ record.inspectionCycle ? `${record.inspectionCycle} 月` : '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'nextInspectionDate'">
|
||||
<span style="color: #faad14">
|
||||
<ExclamationCircleOutlined /> {{ formatDate(record.nextInspectionDate) }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<TableActions
|
||||
:show-edit="false"
|
||||
:show-delete="false"
|
||||
:actions="[
|
||||
{ key: 'view', label: '查看' }
|
||||
]"
|
||||
@view="handleView(record as Equipment)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 未选择项目提示 -->
|
||||
<a-empty v-if="!queryParams.projectId && displayData.length === 0" description="请先选择项目" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,536 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import {
|
||||
Button,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Switch,
|
||||
message,
|
||||
Popconfirm
|
||||
} from 'ant-design-vue'
|
||||
import { PlusOutlined, EditOutlined, CopyOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import {
|
||||
getInspectionTemplates,
|
||||
getInspectionTemplateDetail,
|
||||
createInspectionTemplate,
|
||||
updateInspectionTemplate,
|
||||
copyInspectionTemplate,
|
||||
deleteInspectionTemplate,
|
||||
type InspectionTemplate,
|
||||
type InspectionItem,
|
||||
type TemplateFormData
|
||||
} from '@/api/inspection-template'
|
||||
import { getProjectSelectorList } from '@/api/project'
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const modalLoading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const currentTemplateId = ref<string>('')
|
||||
|
||||
// 项目选项
|
||||
const projectOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 设备类型选项
|
||||
const equipmentTypeOptions = [
|
||||
{ value: '电梯', label: '电梯' },
|
||||
{ value: '消防设备', label: '消防设备' },
|
||||
{ value: '空调', label: '空调' },
|
||||
{ value: '给排水', label: '给排水' },
|
||||
{ value: '配电', label: '配电' },
|
||||
{ value: '安防', label: '安防' },
|
||||
{ value: '其他', label: '其他' }
|
||||
]
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
projectId: ''
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<InspectionTemplate[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<TemplateFormData>({
|
||||
name: '',
|
||||
equipmentType: '',
|
||||
projectId: '',
|
||||
inspectionItems: [],
|
||||
enabled: true
|
||||
})
|
||||
|
||||
// 点检项目表单
|
||||
const itemFormData = reactive<Partial<InspectionItem>>({
|
||||
itemName: '',
|
||||
checkMethod: '',
|
||||
checkStandard: '',
|
||||
isRequired: true,
|
||||
remarks: ''
|
||||
})
|
||||
|
||||
// 表单验证
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入模板名称' }],
|
||||
equipmentType: [{ required: true, message: '请选择设备类型' }],
|
||||
projectId: [{ required: true, message: '请选择项目' }]
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType = [
|
||||
{ title: '模板名称', dataIndex: 'name', key: 'name', width: 180 },
|
||||
{ title: '设备类型', dataIndex: 'equipmentType', key: 'equipmentType', width: 120 },
|
||||
{ title: '所属项目', dataIndex: 'projectName', key: 'projectName', width: 150 },
|
||||
{ title: '点检项目数', key: 'itemCount', width: 100 },
|
||||
{ title: '启用状态', dataIndex: 'enabled', key: 'enabled', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 160 },
|
||||
{ title: '操作', key: 'action', width: 180, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectSelectorList()
|
||||
projectOptions.value = (res.data.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}))
|
||||
} catch {
|
||||
message.error('获取项目列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取模板列表
|
||||
const fetchTemplateList = async () => {
|
||||
if (!queryParams.projectId) {
|
||||
message.warning('请先选择项目')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getInspectionTemplates(queryParams.projectId)
|
||||
tableData.value = res.data.data || []
|
||||
pagination.total = tableData.value.length
|
||||
} catch {
|
||||
message.error('获取模板列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchTemplateList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.projectId = ''
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
|
||||
// 新建模板
|
||||
const handleCreate = () => {
|
||||
isEdit.value = false
|
||||
currentTemplateId.value = ''
|
||||
resetForm()
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑模板
|
||||
const handleEdit = async (record: InspectionTemplate) => {
|
||||
isEdit.value = true
|
||||
currentTemplateId.value = record.id
|
||||
modalLoading.value = true
|
||||
try {
|
||||
const res = await getInspectionTemplateDetail(record.id)
|
||||
const detail = res.data.data
|
||||
formData.name = detail.name
|
||||
formData.equipmentType = detail.equipmentType
|
||||
formData.projectId = detail.projectId
|
||||
formData.inspectionItems = detail.inspectionItems || []
|
||||
formData.enabled = detail.enabled
|
||||
modalVisible.value = true
|
||||
} catch {
|
||||
message.error('获取模板详情失败')
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 复制模板
|
||||
const handleCopy = async (record: InspectionTemplate) => {
|
||||
try {
|
||||
await copyInspectionTemplate(record.id)
|
||||
message.success('模板复制成功')
|
||||
fetchTemplateList()
|
||||
} catch {
|
||||
message.error('模板复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除模板
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteInspectionTemplate(id)
|
||||
message.success('模板删除成功')
|
||||
fetchTemplateList()
|
||||
} catch {
|
||||
message.error('模板删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 保存模板
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await updateInspectionTemplate(currentTemplateId.value, formData)
|
||||
message.success('模板更新成功')
|
||||
} else {
|
||||
await createInspectionTemplate(formData)
|
||||
message.success('模板创建成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
fetchTemplateList()
|
||||
} catch {
|
||||
message.error('保存模板失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.name = ''
|
||||
formData.equipmentType = ''
|
||||
formData.projectId = queryParams.projectId
|
||||
formData.inspectionItems = []
|
||||
formData.enabled = true
|
||||
itemFormData.itemName = ''
|
||||
itemFormData.checkMethod = ''
|
||||
itemFormData.checkStandard = ''
|
||||
itemFormData.isRequired = true
|
||||
itemFormData.remarks = ''
|
||||
}
|
||||
|
||||
// 添加点检项目
|
||||
const handleAddItem = () => {
|
||||
if (!itemFormData.itemName) {
|
||||
message.warning('请输入点检项目名称')
|
||||
return
|
||||
}
|
||||
if (!itemFormData.checkMethod) {
|
||||
message.warning('请输入检查方法')
|
||||
return
|
||||
}
|
||||
if (!itemFormData.checkStandard) {
|
||||
message.warning('请输入检查标准')
|
||||
return
|
||||
}
|
||||
|
||||
const newItem: InspectionItem = {
|
||||
id: Date.now().toString(),
|
||||
itemName: itemFormData.itemName!,
|
||||
checkMethod: itemFormData.checkMethod!,
|
||||
checkStandard: itemFormData.checkStandard!,
|
||||
isRequired: itemFormData.isRequired!,
|
||||
remarks: itemFormData.remarks || ''
|
||||
}
|
||||
|
||||
formData.inspectionItems.push(newItem)
|
||||
resetItemForm()
|
||||
}
|
||||
|
||||
// 删除点检项目
|
||||
const handleRemoveItem = (index: number) => {
|
||||
formData.inspectionItems.splice(index, 1)
|
||||
}
|
||||
|
||||
// 重置点检项目表单
|
||||
const resetItemForm = () => {
|
||||
itemFormData.itemName = ''
|
||||
itemFormData.checkMethod = ''
|
||||
itemFormData.checkStandard = ''
|
||||
itemFormData.isRequired = true
|
||||
itemFormData.remarks = ''
|
||||
}
|
||||
|
||||
// 项目变更
|
||||
const handleProjectChange = (value: string) => {
|
||||
queryParams.projectId = value
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
if (isEdit.value) {
|
||||
formData.projectId = value
|
||||
}
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
pagination.current = page
|
||||
pagination.pageSize = pageSize
|
||||
}
|
||||
|
||||
// 模板状态
|
||||
const getEnabledStatus = (enabled: boolean) => {
|
||||
return enabled ? { color: 'green', text: '启用' } : { color: 'default', text: '停用' }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProjects()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">点检模板管理</h2>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<Space>
|
||||
<Select
|
||||
v-model:value="queryParams.projectId"
|
||||
placeholder="请选择项目"
|
||||
style="width: 240px"
|
||||
:options="projectOptions"
|
||||
@change="handleProjectChange"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
查询
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 操作区 -->
|
||||
<div class="table-toolbar">
|
||||
<Button type="primary" @click="handleCreate" :disabled="!queryParams.projectId">
|
||||
<PlusOutlined /> 新建模板
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<div class="table-card">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:row-key="(record: InspectionTemplate) => record.id"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
}"
|
||||
@change="(pag: any) => handlePageChange(pag.current, pag.pageSize)"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'itemCount'">
|
||||
{{ record.inspectionItems?.length || 0 }} 项
|
||||
</template>
|
||||
<template v-else-if="column.key === 'enabled'">
|
||||
<Tag :color="getEnabledStatus(record.enabled).color">
|
||||
{{ getEnabledStatus(record.enabled).text }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'createdAt'">
|
||||
{{ record.createdAt ? record.createdAt.substring(0, 19) : '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEdit(record)">
|
||||
<EditOutlined /> 编辑
|
||||
</Button>
|
||||
<Button type="link" size="small" @click="handleCopy(record)">
|
||||
<CopyOutlined /> 复制
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除该模板吗?"
|
||||
@confirm="handleDelete(record.id)"
|
||||
>
|
||||
<Button type="link" size="small" danger>
|
||||
<DeleteOutlined /> 删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<a-empty v-if="tableData.length === 0 && !loading" description="请先选择项目" />
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑模态框 -->
|
||||
<Modal
|
||||
v-model:open="modalVisible"
|
||||
:title="isEdit ? '编辑模板' : '新建模板'"
|
||||
width="900px"
|
||||
:footer="null"
|
||||
@cancel="modalVisible = false"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<a-form
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
@finish="handleSave"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="模板名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入模板名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="设备类型" name="equipmentType">
|
||||
<a-select v-model:value="formData.equipmentType" placeholder="请选择设备类型" :options="equipmentTypeOptions" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="所属项目" name="projectId">
|
||||
<a-select v-model:value="formData.projectId" placeholder="请选择项目" :options="projectOptions" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="启用状态">
|
||||
<Switch v-model:checked="formData.enabled" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 点检项目 -->
|
||||
<a-divider>点检项目</a-divider>
|
||||
|
||||
<div class="inspection-items">
|
||||
<a-table
|
||||
:columns="[
|
||||
{ title: '项目名称', dataIndex: 'itemName', key: 'itemName' },
|
||||
{ title: '检查方法', dataIndex: 'checkMethod', key: 'checkMethod' },
|
||||
{ title: '检查标准', dataIndex: 'checkStandard', key: 'checkStandard' },
|
||||
{ title: '必检', dataIndex: 'isRequired', key: 'isRequired', width: 80 },
|
||||
{ title: '操作', key: 'action', width: 80 }
|
||||
]"
|
||||
:data-source="formData.inspectionItems"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'isRequired'">
|
||||
{{ record.isRequired ? '是' : '否' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Button type="link" size="small" danger @click="handleRemoveItem(index)">
|
||||
删除
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 添加点检项目表单 -->
|
||||
<div class="add-item-form">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="6">
|
||||
<a-input v-model:value="itemFormData.itemName" placeholder="项目名称" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input v-model:value="itemFormData.checkMethod" placeholder="检查方法" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input v-model:value="itemFormData.checkStandard" placeholder="检查标准" />
|
||||
</a-col>
|
||||
<a-col :span="3">
|
||||
<a-switch v-model:checked="itemFormData.isRequired" checked-children="必" un-checked-children="选" />
|
||||
</a-col>
|
||||
<a-col :span="3">
|
||||
<Button type="primary" size="small" @click="handleAddItem">添加</Button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表单操作 -->
|
||||
<div class="form-actions">
|
||||
<Space>
|
||||
<Button @click="modalVisible = false">取消</Button>
|
||||
<Button type="primary" html-type="submit">保存</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</a-form>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.inspection-items {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.add-item-form {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,397 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { Button, Select, Space, message, Tag, Modal, Form, Input, InputNumber, Popconfirm } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import {
|
||||
getMaintenancePlans,
|
||||
createMaintenancePlan,
|
||||
updateMaintenancePlan,
|
||||
deleteMaintenancePlan,
|
||||
type MaintenancePlan,
|
||||
type MaintenancePlanForm
|
||||
} from '@/api/maintenance'
|
||||
import { getProjectSelectorList } from '@/api/project'
|
||||
import { TableActions, Pagination } from '@/components'
|
||||
|
||||
// 触发类型映射
|
||||
const triggerTypeMap: Record<string, { text: string; color: string }> = {
|
||||
MANUAL: { text: '手动', color: 'default' },
|
||||
SCHEDULED: { text: '定时', color: 'blue' },
|
||||
AUTOMATIC: { text: '自动', color: 'green' }
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType = [
|
||||
{ title: '计划名称', dataIndex: 'name', key: 'name', width: 180 },
|
||||
{ title: '所属项目', dataIndex: 'projectName', key: 'projectName', width: 150 },
|
||||
{ title: '触发类型', dataIndex: 'triggerType', key: 'triggerType', width: 100 },
|
||||
{ title: '关联设备', dataIndex: 'equipmentName', key: 'equipmentName', width: 150 },
|
||||
{ title: 'Cron表达式', dataIndex: 'cronExpression', key: 'cronExpression', width: 120 },
|
||||
{ title: '下次触发时间', dataIndex: 'nextTriggerTime', key: 'nextTriggerTime', width: 160 },
|
||||
{ title: '状态', dataIndex: 'enabled', key: 'enabled', width: 80 },
|
||||
{ title: '操作', key: 'action', width: 120, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// 项目选择选项
|
||||
const projectOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 触发类型选项
|
||||
const triggerTypeOptions = [
|
||||
{ value: 'MANUAL', label: '手动' },
|
||||
{ value: 'SCHEDULED', label: '定时' },
|
||||
{ value: 'AUTOMATIC', label: '自动' }
|
||||
]
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
projectId: '',
|
||||
triggerType: undefined as string | undefined
|
||||
})
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const tableData = ref<MaintenancePlan[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 模态框状态
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('新建维保计划')
|
||||
const formLoading = ref(false)
|
||||
const editingPlan = ref<MaintenancePlan | null>(null)
|
||||
|
||||
// 表单数据
|
||||
const formState = reactive<MaintenancePlanForm>({
|
||||
name: '',
|
||||
projectId: '',
|
||||
triggerType: 'MANUAL',
|
||||
equipmentId: undefined,
|
||||
spaceNodeId: undefined,
|
||||
description: '',
|
||||
enabled: true,
|
||||
cronExpression: ''
|
||||
})
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectSelectorList()
|
||||
projectOptions.value = (res.data.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}))
|
||||
} catch {
|
||||
message.error('获取项目列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取维保计划列表
|
||||
const fetchPlanList = async () => {
|
||||
if (!queryParams.projectId) {
|
||||
message.warning('请先选择项目')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getMaintenancePlans(queryParams.projectId, queryParams.triggerType)
|
||||
const data = res.data.data || []
|
||||
tableData.value = data
|
||||
pagination.total = data.length
|
||||
} catch {
|
||||
message.error('获取维保计划列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchPlanList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.projectId = ''
|
||||
queryParams.triggerType = undefined
|
||||
pagination.current = 1
|
||||
tableData.value = []
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
pagination.current = page
|
||||
pagination.pageSize = pageSize
|
||||
fetchPlanList()
|
||||
}
|
||||
|
||||
// 打开新建模态框
|
||||
const handleAdd = () => {
|
||||
editingPlan.value = null
|
||||
modalTitle.value = '新建维保计划'
|
||||
formState.name = ''
|
||||
formState.projectId = queryParams.projectId
|
||||
formState.triggerType = 'MANUAL'
|
||||
formState.equipmentId = undefined
|
||||
formState.spaceNodeId = undefined
|
||||
formState.description = ''
|
||||
formState.enabled = true
|
||||
formState.cronExpression = ''
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 打开编辑模态框
|
||||
const handleEdit = (record: MaintenancePlan) => {
|
||||
editingPlan.value = record
|
||||
modalTitle.value = '编辑维保计划'
|
||||
formState.id = record.id
|
||||
formState.name = record.name
|
||||
formState.projectId = record.projectId
|
||||
formState.triggerType = record.triggerType
|
||||
formState.equipmentId = record.equipmentId
|
||||
formState.spaceNodeId = record.spaceNodeId
|
||||
formState.description = record.description || ''
|
||||
formState.enabled = record.enabled
|
||||
formState.cronExpression = record.cronExpression || ''
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (editingPlan.value) {
|
||||
await updateMaintenancePlan(editingPlan.value.id, formState)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await createMaintenancePlan(formState)
|
||||
message.success('创建成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
fetchPlanList()
|
||||
} catch {
|
||||
message.error(editingPlan.value ? '更新失败' : '创建失败')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除计划
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteMaintenancePlan(id)
|
||||
message.success('停用成功')
|
||||
fetchPlanList()
|
||||
} catch {
|
||||
message.error('停用失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date: string | Date | undefined) => {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const hour = String(d.getHours()).padStart(2, '0')
|
||||
const minute = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProjects()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">维保计划</h2>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<Space>
|
||||
<Select
|
||||
v-model:value="queryParams.projectId"
|
||||
placeholder="请选择项目"
|
||||
style="width: 240px"
|
||||
allow-clear
|
||||
:options="projectOptions"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="queryParams.triggerType"
|
||||
placeholder="触发类型"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
:options="triggerTypeOptions"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
<SearchOutlined /> 查询
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</Button>
|
||||
<Button type="primary" @click="handleAdd">
|
||||
<PlusOutlined /> 新建
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 表格区 -->
|
||||
<div class="table-card">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:row-key="(record: MaintenancePlan) => record.id"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
}"
|
||||
@change="(pag: any) => handlePageChange(pag.current, pag.pageSize)"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'triggerType'">
|
||||
<Tag :color="triggerTypeMap[record.triggerType]?.color">
|
||||
{{ triggerTypeMap[record.triggerType]?.text }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'nextTriggerTime'">
|
||||
{{ formatDate(record.nextTriggerTime) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'enabled'">
|
||||
<Tag :color="record.enabled ? 'green' : 'red'">
|
||||
{{ record.enabled ? '启用' : '停用' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEdit(record)">
|
||||
<EditOutlined /> 编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要停用该计划吗?"
|
||||
@confirm="handleDelete(record.id)"
|
||||
>
|
||||
<Button type="link" size="small" danger>
|
||||
<DeleteOutlined /> 停用
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 未选择项目提示 -->
|
||||
<a-empty v-if="!queryParams.projectId && tableData.length === 0" description="请先选择项目" />
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑模态框 -->
|
||||
<Modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:footer="null"
|
||||
width="600px"
|
||||
@cancel="modalVisible = false"
|
||||
>
|
||||
<Form
|
||||
:model="formState"
|
||||
layout="vertical"
|
||||
@finish="handleSubmit"
|
||||
>
|
||||
<Form.Item label="计划名称" name="name" :rules="[{ required: true, message: '请输入计划名称' }]">
|
||||
<Input v-model:value="formState.name" placeholder="请输入计划名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="所属项目" name="projectId" :rules="[{ required: true, message: '请选择项目' }]">
|
||||
<Select
|
||||
v-model:value="formState.projectId"
|
||||
placeholder="请选择项目"
|
||||
:options="projectOptions"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="触发类型" name="triggerType" :rules="[{ required: true, message: '请选择触发类型' }]">
|
||||
<Select
|
||||
v-model:value="formState.triggerType"
|
||||
placeholder="请选择触发类型"
|
||||
:options="triggerTypeOptions"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Cron表达式" name="cronExpression">
|
||||
<Input v-model:value="formState.cronExpression" placeholder="如: 0 0 2 * * ?" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="描述" name="description">
|
||||
<Input.TextArea v-model:value="formState.description" placeholder="请输入描述" :rows="3" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="启用状态" name="enabled">
|
||||
<Select
|
||||
v-model:value="formState.enabled"
|
||||
:options="[
|
||||
{ value: true, label: '启用' },
|
||||
{ value: false, label: '停用' }
|
||||
]"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style="margin-bottom: 0; text-align: right">
|
||||
<Space>
|
||||
<Button @click="modalVisible = false">取消</Button>
|
||||
<Button type="primary" html-type="submit" :loading="formLoading">
|
||||
确定
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,401 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Button, Select, Space, message, Badge, Modal, Input, Form, Popconfirm } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
CheckCircleOutlined,
|
||||
PlayCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
UserOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import {
|
||||
getMaintenanceTasks,
|
||||
acceptMaintenanceTask,
|
||||
startMaintenanceTask,
|
||||
completeMaintenanceTask,
|
||||
cancelMaintenanceTask,
|
||||
type MaintenanceTask,
|
||||
type TaskStatus
|
||||
} from '@/api/maintenance'
|
||||
import { getProjectSelectorList } from '@/api/project'
|
||||
import { getUserList } from '@/api/user'
|
||||
|
||||
// 任务状态映射
|
||||
const statusMap: Record<TaskStatus, { text: string; color: string; status: 'default' | 'processing' | 'success' | 'error' | 'warning' | 'default' }> = {
|
||||
PENDING: { text: '待接受', color: 'default', status: 'default' },
|
||||
ACCEPTED: { text: '已接受', color: 'blue', status: 'processing' },
|
||||
IN_PROGRESS: { text: '进行中', color: 'processing', status: 'processing' },
|
||||
COMPLETED: { text: '已完成', color: 'success', status: 'success' },
|
||||
CANCELLED: { text: '已取消', color: 'error', status: 'error' }
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType = [
|
||||
{ title: '任务标题', dataIndex: 'title', key: 'title', width: 180 },
|
||||
{ title: '所属项目', dataIndex: 'projectName', key: 'projectName', width: 150 },
|
||||
{ title: '关联设备', dataIndex: 'equipmentName', key: 'equipmentName', width: 150 },
|
||||
{ title: '负责人', dataIndex: 'assigneeName', key: 'assigneeName', width: 100 },
|
||||
{ title: '计划日期', dataIndex: 'scheduledDate', key: 'scheduledDate', width: 120 },
|
||||
{ title: '开始时间', dataIndex: 'startTime', key: 'startTime', width: 160 },
|
||||
{ title: '完成时间', dataIndex: 'completedTime', key: 'completedTime', width: 160 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||
{ title: '操作', key: 'action', width: 200, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// 项目选择选项
|
||||
const projectOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ value: 'PENDING', label: '待接受' },
|
||||
{ value: 'ACCEPTED', label: '已接受' },
|
||||
{ value: 'IN_PROGRESS', label: '进行中' },
|
||||
{ value: 'COMPLETED', label: '已完成' },
|
||||
{ value: 'CANCELLED', label: '取消' }
|
||||
]
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
projectId: '',
|
||||
status: undefined as TaskStatus | undefined,
|
||||
assigneeId: undefined as string | undefined
|
||||
})
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const tableData = ref<MaintenanceTask[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 取消模态框
|
||||
const cancelModalVisible = ref(false)
|
||||
const cancelReason = ref('')
|
||||
const cancellingTaskId = ref<string | null>(null)
|
||||
|
||||
// 完成模态框
|
||||
const completeModalVisible = ref(false)
|
||||
const completionNotes = ref('')
|
||||
const completingTaskId = ref<string | null>(null)
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectSelectorList()
|
||||
projectOptions.value = (res.data.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}))
|
||||
} catch {
|
||||
message.error('获取项目列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取维保任务列表
|
||||
const fetchTaskList = async () => {
|
||||
if (!queryParams.projectId) {
|
||||
message.warning('请先选择项目')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getMaintenanceTasks({
|
||||
projectId: queryParams.projectId,
|
||||
status: queryParams.status,
|
||||
assigneeId: queryParams.assigneeId
|
||||
})
|
||||
const data = res.data.data || []
|
||||
tableData.value = data
|
||||
pagination.total = data.length
|
||||
} catch {
|
||||
message.error('获取维保任务列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchTaskList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.projectId = ''
|
||||
queryParams.status = undefined
|
||||
queryParams.assigneeId = undefined
|
||||
pagination.current = 1
|
||||
tableData.value = []
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
pagination.current = page
|
||||
pagination.pageSize = pageSize
|
||||
fetchTaskList()
|
||||
}
|
||||
|
||||
// 接受任务
|
||||
const handleAccept = async (id: string) => {
|
||||
try {
|
||||
await acceptMaintenanceTask(id, 'current-user-id')
|
||||
message.success('任务已接受')
|
||||
fetchTaskList()
|
||||
} catch {
|
||||
message.error('接受任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 开始任务
|
||||
const handleStart = async (id: string) => {
|
||||
try {
|
||||
await startMaintenanceTask(id)
|
||||
message.success('任务已开始')
|
||||
fetchTaskList()
|
||||
} catch {
|
||||
message.error('开始任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 打开完成模态框
|
||||
const handleOpenComplete = (id: string) => {
|
||||
completingTaskId.value = id
|
||||
completionNotes.value = ''
|
||||
completeModalVisible.value = true
|
||||
}
|
||||
|
||||
// 完成任务
|
||||
const handleComplete = async () => {
|
||||
if (!completingTaskId.value) return
|
||||
try {
|
||||
await completeMaintenanceTask(completingTaskId.value, { completionNotes: completionNotes.value })
|
||||
message.success('任务已完成')
|
||||
completeModalVisible.value = false
|
||||
completingTaskId.value = null
|
||||
completionNotes.value = ''
|
||||
fetchTaskList()
|
||||
} catch {
|
||||
message.error('完成任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 打开取消模态框
|
||||
const handleOpenCancel = (id: string) => {
|
||||
cancellingTaskId.value = id
|
||||
cancelReason.value = ''
|
||||
cancelModalVisible.value = true
|
||||
}
|
||||
|
||||
// 取消任务
|
||||
const handleCancel = async () => {
|
||||
if (!cancellingTaskId.value) return
|
||||
try {
|
||||
await cancelMaintenanceTask(cancellingTaskId.value)
|
||||
message.success('任务已取消')
|
||||
cancelModalVisible.value = false
|
||||
cancellingTaskId.value = null
|
||||
cancelReason.value = ''
|
||||
fetchTaskList()
|
||||
} catch {
|
||||
message.error('取消任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (date: string | Date | undefined) => {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const hour = String(d.getHours()).padStart(2, '0')
|
||||
const minute = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProjects()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">维保任务</h2>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<Space>
|
||||
<Select
|
||||
v-model:value="queryParams.projectId"
|
||||
placeholder="请选择项目"
|
||||
style="width: 240px"
|
||||
allow-clear
|
||||
:options="projectOptions"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="queryParams.status"
|
||||
placeholder="任务状态"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
:options="statusOptions"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
<SearchOutlined /> 查询
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 表格区 -->
|
||||
<div class="table-card">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:row-key="(record: MaintenanceTask) => record.id"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
}"
|
||||
@change="(pag: any) => handlePageChange(pag.current, pag.pageSize)"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'assigneeName'">
|
||||
<Space>
|
||||
<UserOutlined />
|
||||
{{ record.assigneeName || '-' }}
|
||||
</Space>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'scheduledDate'">
|
||||
{{ record.scheduledDate || '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'startTime'">
|
||||
{{ formatDateTime(record.startTime) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'completedTime'">
|
||||
{{ formatDateTime(record.completedTime) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<Badge
|
||||
:status="statusMap[record.status]?.status"
|
||||
:text="statusMap[record.status]?.text"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<!-- 待接受状态显示接受按钮 -->
|
||||
<Button
|
||||
v-if="record.status === 'PENDING'"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleAccept(record.id)"
|
||||
>
|
||||
<CheckCircleOutlined /> 接受
|
||||
</Button>
|
||||
<!-- 已接受状态显示开始按钮 -->
|
||||
<Button
|
||||
v-if="record.status === 'ACCEPTED'"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleStart(record.id)"
|
||||
>
|
||||
<PlayCircleOutlined /> 开始
|
||||
</Button>
|
||||
<!-- 进行中状态显示完成按钮 -->
|
||||
<Button
|
||||
v-if="record.status === 'IN_PROGRESS'"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleOpenComplete(record.id)"
|
||||
>
|
||||
<CheckCircleOutlined /> 完成
|
||||
</Button>
|
||||
<!-- 待接受、已接受、进行中状态显示取消按钮 -->
|
||||
<Button
|
||||
v-if="['PENDING', 'ACCEPTED', 'IN_PROGRESS'].includes(record.status)"
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
@click="handleOpenCancel(record.id)"
|
||||
>
|
||||
<CloseCircleOutlined /> 取消
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 未选择项目提示 -->
|
||||
<a-empty v-if="!queryParams.projectId && tableData.length === 0" description="请先选择项目" />
|
||||
</div>
|
||||
|
||||
<!-- 取消任务模态框 -->
|
||||
<Modal
|
||||
v-model:open="cancelModalVisible"
|
||||
title="取消任务"
|
||||
@ok="handleCancel"
|
||||
>
|
||||
<p>确定要取消该维保任务吗?</p>
|
||||
</Modal>
|
||||
|
||||
<!-- 完成任务模态框 -->
|
||||
<Modal
|
||||
v-model:open="completeModalVisible"
|
||||
title="完成任务"
|
||||
@ok="handleComplete"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="完成备注">
|
||||
<Input.TextArea
|
||||
v-model:value="completionNotes"
|
||||
placeholder="请输入完成备注"
|
||||
:rows="4"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,598 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { Key } from 'ant-design-vue/es/_util/type'
|
||||
import {
|
||||
Card,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Descriptions,
|
||||
DescriptionsItem,
|
||||
Tag,
|
||||
Button,
|
||||
Table,
|
||||
Statistic,
|
||||
Row,
|
||||
Col,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Select,
|
||||
Switch,
|
||||
Popconfirm,
|
||||
message,
|
||||
Spin,
|
||||
Empty,
|
||||
Modal,
|
||||
Drawer,
|
||||
Space
|
||||
} from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
EditOutlined,
|
||||
UserAddOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import {
|
||||
getProject,
|
||||
getProjectStatistics,
|
||||
getProjectMembers,
|
||||
addProjectMembers,
|
||||
removeProjectMember,
|
||||
getProjectConfig,
|
||||
updateProjectConfig,
|
||||
updateProject
|
||||
} from '@/api/project'
|
||||
import type { Project } from '@/types'
|
||||
import type { ProjectStatistics, ProjectMember, ProjectConfig, PageResponse, ProjectStatus, ProjectType } from '@/types/project'
|
||||
import { ProjectStatusMap, ProjectMemberRoleMap, ProjectTypeMap } from '@/types/project'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 项目ID
|
||||
const projectId = computed(() => route.params.id as string)
|
||||
const activeTab = computed(() => route.query.tab as string || 'info')
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const statisticsLoading = ref(false)
|
||||
const membersLoading = ref(false)
|
||||
const configLoading = ref(false)
|
||||
|
||||
// 数据
|
||||
const project = ref<Project | null>(null)
|
||||
const statistics = ref<ProjectStatistics | null>(null)
|
||||
const members = ref<ProjectMember[]>([])
|
||||
const config = ref<ProjectConfig | null>(null)
|
||||
|
||||
// 成员分页
|
||||
const memberPagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 添加成员弹窗
|
||||
const addMemberVisible = ref(false)
|
||||
const addMemberForm = reactive({
|
||||
userIds: [] as string[],
|
||||
roleInProject: 'VIEWER'
|
||||
})
|
||||
const addMemberLoading = ref(false)
|
||||
|
||||
// 配置保存
|
||||
const configSaving = ref(false)
|
||||
|
||||
// 编辑抽屉
|
||||
const editDrawerVisible = ref(false)
|
||||
const editDrawerTitle = ref('编辑项目')
|
||||
const editFormRef = ref()
|
||||
const editSubmitting = ref(false)
|
||||
|
||||
const editFormState = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
address: '',
|
||||
projectType: 'RESIDENTIAL' as ProjectType,
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
status: 'ACTIVE'
|
||||
})
|
||||
|
||||
// 获取项目详情
|
||||
const fetchProject = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProject(projectId.value)
|
||||
project.value = res.data
|
||||
} catch {
|
||||
message.error('获取项目详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
const fetchStatistics = async () => {
|
||||
statisticsLoading.value = true
|
||||
try {
|
||||
const res = await getProjectStatistics(projectId.value)
|
||||
statistics.value = res.data
|
||||
} catch {
|
||||
// 忽略错误
|
||||
} finally {
|
||||
statisticsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取成员列表
|
||||
const fetchMembers = async () => {
|
||||
membersLoading.value = true
|
||||
try {
|
||||
const res = await getProjectMembers(projectId.value, {
|
||||
page: memberPagination.current - 1,
|
||||
size: memberPagination.pageSize
|
||||
})
|
||||
const data = res.data as PageResponse<ProjectMember>
|
||||
members.value = data.content
|
||||
memberPagination.total = data.totalElements
|
||||
} catch {
|
||||
message.error('获取成员列表失败')
|
||||
} finally {
|
||||
membersLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
const fetchConfig = async () => {
|
||||
configLoading.value = true
|
||||
try {
|
||||
const res = await getProjectConfig(projectId.value)
|
||||
config.value = res.data
|
||||
} catch {
|
||||
message.error('获取配置失败')
|
||||
} finally {
|
||||
configLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Tab切换
|
||||
const handleTabChange = (key: Key) => {
|
||||
router.replace({ query: { tab: String(key) } })
|
||||
if (key === 'member' && members.value.length === 0) {
|
||||
fetchMembers()
|
||||
} else if (key === 'config' && !config.value) {
|
||||
fetchConfig()
|
||||
}
|
||||
}
|
||||
|
||||
// 返回列表
|
||||
const handleBack = () => {
|
||||
router.push('/project/list')
|
||||
}
|
||||
|
||||
// 编辑项目
|
||||
const handleEdit = () => {
|
||||
if (!project.value) return
|
||||
editFormState.value = {
|
||||
name: project.value.name || '',
|
||||
description: project.value.description || '',
|
||||
address: project.value.address || '',
|
||||
projectType: project.value.projectType || 'RESIDENTIAL',
|
||||
province: project.value.province || '',
|
||||
city: project.value.city || '',
|
||||
district: project.value.district || '',
|
||||
status: project.value.status || 'ACTIVE'
|
||||
}
|
||||
editDrawerVisible.value = true
|
||||
}
|
||||
|
||||
// 提交编辑
|
||||
const submitEdit = async () => {
|
||||
try {
|
||||
await editFormRef.value.validate()
|
||||
editSubmitting.value = true
|
||||
await updateProject(projectId.value, editFormState.value)
|
||||
message.success('更新成功')
|
||||
editDrawerVisible.value = false
|
||||
fetchProject()
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) return
|
||||
message.error('更新失败')
|
||||
} finally {
|
||||
editSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 添加成员
|
||||
const handleAddMember = () => {
|
||||
addMemberForm.userIds = []
|
||||
addMemberForm.roleInProject = 'VIEWER'
|
||||
addMemberVisible.value = true
|
||||
}
|
||||
|
||||
// 提交添加成员
|
||||
const submitAddMember = async () => {
|
||||
if (addMemberForm.userIds.length === 0) {
|
||||
message.warning('请选择要添加的成员')
|
||||
return
|
||||
}
|
||||
addMemberLoading.value = true
|
||||
try {
|
||||
await addProjectMembers(projectId.value, {
|
||||
userIds: addMemberForm.userIds,
|
||||
roleInProject: addMemberForm.roleInProject
|
||||
})
|
||||
message.success('添加成功')
|
||||
addMemberVisible.value = false
|
||||
fetchMembers()
|
||||
} catch {
|
||||
message.error('添加失败')
|
||||
} finally {
|
||||
addMemberLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 移除成员
|
||||
const handleRemoveMember = async (memberId: string) => {
|
||||
try {
|
||||
await removeProjectMember(projectId.value, memberId)
|
||||
message.success('移除成功')
|
||||
fetchMembers()
|
||||
} catch {
|
||||
message.error('移除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 成员分页变化
|
||||
const handleMemberTableChange = (pag: any) => {
|
||||
memberPagination.current = pag.current
|
||||
memberPagination.pageSize = pag.pageSize
|
||||
fetchMembers()
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
const handleSaveConfig = async () => {
|
||||
if (!config.value) return
|
||||
configSaving.value = true
|
||||
try {
|
||||
await updateProjectConfig(projectId.value, config.value)
|
||||
message.success('保存成功')
|
||||
} catch {
|
||||
message.error('保存失败')
|
||||
} finally {
|
||||
configSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态标签
|
||||
const getStatusTag = (status: ProjectStatus) => {
|
||||
const config = ProjectStatusMap[status] || { label: status, color: 'default' }
|
||||
return { color: config.color, label: config.label }
|
||||
}
|
||||
|
||||
// 成员角色选项
|
||||
const roleOptions = Object.entries(ProjectMemberRoleMap).map(([value, { label }]) => ({
|
||||
value,
|
||||
label
|
||||
}))
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = Object.entries(ProjectStatusMap).map(([value, { label }]) => ({
|
||||
value,
|
||||
label
|
||||
}))
|
||||
|
||||
// 类型选项
|
||||
const typeOptions = Object.entries(ProjectTypeMap).map(([value, { label }]) => ({
|
||||
value,
|
||||
label
|
||||
}))
|
||||
|
||||
// 成员表格列
|
||||
const memberColumns: ColumnsType = [
|
||||
{ title: '用户名', dataIndex: 'userName', key: 'userName', width: 120 },
|
||||
{ title: '姓名', dataIndex: 'realName', key: 'realName', width: 120 },
|
||||
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 140 },
|
||||
{ title: '角色', dataIndex: 'roleInProject', key: 'roleInProject', width: 120 },
|
||||
{ title: '加入时间', dataIndex: 'joinedAt', key: 'joinedAt', width: 180 },
|
||||
{ title: '操作', key: 'action', width: 100, fixed: 'right' }
|
||||
]
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchProject()
|
||||
fetchStatistics()
|
||||
if (activeTab.value === 'member') {
|
||||
fetchMembers()
|
||||
} else if (activeTab.value === 'config') {
|
||||
fetchConfig()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<Button type="text" @click="handleBack">
|
||||
<ArrowLeftOutlined /> 返回
|
||||
</Button>
|
||||
<h2 class="page-title">项目详情</h2>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<Button type="primary" @click="handleEdit">
|
||||
<EditOutlined /> 编辑
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Spin :spinning="loading">
|
||||
<template v-if="project">
|
||||
<!-- 统计卡片 -->
|
||||
<Row :gutter="16" class="statistics-row">
|
||||
<Col :span="4">
|
||||
<Card>
|
||||
<Statistic title="成员数" :value="statistics?.memberCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Card>
|
||||
<Statistic title="楼栋数" :value="statistics?.buildingCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Card>
|
||||
<Statistic title="房间数" :value="statistics?.roomCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Card>
|
||||
<Statistic title="业主数" :value="statistics?.ownerCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Card>
|
||||
<Statistic title="租户数" :value="statistics?.tenantCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Card>
|
||||
<Statistic title="进行中任务" :value="statistics?.activeTaskCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- Tab 页签 -->
|
||||
<Card class="content-card">
|
||||
<Tabs :active-key="activeTab" @change="handleTabChange">
|
||||
<!-- 基本信息 -->
|
||||
<TabPane key="info" tab="基本信息">
|
||||
<Descriptions :column="2" bordered>
|
||||
<DescriptionsItem label="项目编码">{{ project.code }}</DescriptionsItem>
|
||||
<DescriptionsItem label="项目名称">{{ project.name }}</DescriptionsItem>
|
||||
<DescriptionsItem label="状态">
|
||||
<Tag :color="getStatusTag(project.status).color">
|
||||
{{ getStatusTag(project.status).label }}
|
||||
</Tag>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="省份">{{ project.province || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="城市">{{ project.city || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="区县">{{ project.district || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="详细地址" :span="2">{{ project.address || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="描述" :span="2">{{ project.description || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="创建时间">{{ project.createdAt || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="更新时间">{{ project.updatedAt || '-' }}</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</TabPane>
|
||||
|
||||
<!-- 成员管理 -->
|
||||
<TabPane key="member" tab="成员管理">
|
||||
<div class="tab-header">
|
||||
<Button type="primary" @click="handleAddMember">
|
||||
<UserAddOutlined /> 添加成员
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
:columns="memberColumns"
|
||||
:data-source="members"
|
||||
:loading="membersLoading"
|
||||
:row-key="(record: ProjectMember) => record.id"
|
||||
:pagination="memberPagination"
|
||||
@change="handleMemberTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'roleInProject'">
|
||||
<Tag :color="ProjectMemberRoleMap[record.roleInProject]?.color || 'default'">
|
||||
{{ ProjectMemberRoleMap[record.roleInProject]?.label || record.roleInProject }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Popconfirm
|
||||
title="确认移除该成员?"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="handleRemoveMember(record.id)"
|
||||
>
|
||||
<Button type="link" danger size="small">
|
||||
<DeleteOutlined /> 移除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</TabPane>
|
||||
|
||||
<!-- 项目配置 -->
|
||||
<TabPane key="config" tab="项目配置">
|
||||
<Spin :spinning="configLoading">
|
||||
<template v-if="config">
|
||||
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 14 }">
|
||||
<Card title="业务功能开关" size="small" class="config-card">
|
||||
<FormItem label="预约功能">
|
||||
<Switch v-model:checked="config.enableReservation" />
|
||||
</FormItem>
|
||||
<FormItem label="访客管理">
|
||||
<Switch v-model:checked="config.enableVisitor" />
|
||||
</FormItem>
|
||||
<FormItem label="投诉建议">
|
||||
<Switch v-model:checked="config.enableComplaint" />
|
||||
</FormItem>
|
||||
<FormItem label="在线缴费">
|
||||
<Switch v-model:checked="config.enablePayment" />
|
||||
</FormItem>
|
||||
<FormItem label="公告通知">
|
||||
<Switch v-model:checked="config.enableAnnouncement" />
|
||||
</FormItem>
|
||||
<FormItem label="问卷调查">
|
||||
<Switch v-model:checked="config.enableSurvey" />
|
||||
</FormItem>
|
||||
<FormItem label="投票表决">
|
||||
<Switch v-model:checked="config.enableVote" />
|
||||
</FormItem>
|
||||
<FormItem label="设备维保">
|
||||
<Switch v-model:checked="config.enableMaintenance" />
|
||||
</FormItem>
|
||||
<FormItem label="资产管理">
|
||||
<Switch v-model:checked="config.enableAsset" />
|
||||
</FormItem>
|
||||
</Card>
|
||||
<div class="config-footer">
|
||||
<Button type="primary" :loading="configSaving" @click="handleSaveConfig">
|
||||
保存配置
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
<Empty v-else description="暂无配置数据" />
|
||||
</Spin>
|
||||
</TabPane>
|
||||
|
||||
<!-- 操作日志 -->
|
||||
<TabPane key="log" tab="操作日志">
|
||||
<Empty description="暂无操作日志" />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</template>
|
||||
<Empty v-else description="项目不存在" />
|
||||
</Spin>
|
||||
|
||||
<!-- 添加成员弹窗 -->
|
||||
<Modal
|
||||
v-model:open="addMemberVisible"
|
||||
title="添加成员"
|
||||
:confirm-loading="addMemberLoading"
|
||||
@ok="submitAddMember"
|
||||
>
|
||||
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<FormItem label="用户ID" required>
|
||||
<Input
|
||||
v-model:value="addMemberForm.userIds[0]"
|
||||
placeholder="请输入用户ID(暂支持单个添加)"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="角色" required>
|
||||
<Select v-model:value="addMemberForm.roleInProject" :options="roleOptions" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Drawer
|
||||
v-model:open="editDrawerVisible"
|
||||
:title="editDrawerTitle"
|
||||
width="500px"
|
||||
@close="editDrawerVisible = false"
|
||||
>
|
||||
<Form
|
||||
ref="editFormRef"
|
||||
:model="editFormState"
|
||||
layout="vertical"
|
||||
:rules="{
|
||||
name: [{ required: true, message: '请输入项目名称' }]
|
||||
}"
|
||||
>
|
||||
<Form.Item label="项目名称" name="name">
|
||||
<Input v-model:value="editFormState.name" placeholder="请输入项目名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="项目类型" name="projectType">
|
||||
<Select v-model:value="editFormState.projectType" :options="typeOptions" />
|
||||
</Form.Item>
|
||||
<Form.Item label="描述" name="description">
|
||||
<Input.TextArea v-model:value="editFormState.description" placeholder="请输入描述" :rows="2" />
|
||||
</Form.Item>
|
||||
<Form.Item label="省份" name="province">
|
||||
<Input v-model:value="editFormState.province" placeholder="请输入省份" />
|
||||
</Form.Item>
|
||||
<Form.Item label="城市" name="city">
|
||||
<Input v-model:value="editFormState.city" placeholder="请输入城市" />
|
||||
</Form.Item>
|
||||
<Form.Item label="区县" name="district">
|
||||
<Input v-model:value="editFormState.district" placeholder="请输入区县" />
|
||||
</Form.Item>
|
||||
<Form.Item label="详细地址" name="address">
|
||||
<Input v-model:value="editFormState.address" placeholder="请输入详细地址" />
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Select v-model:value="editFormState.status" :options="statusOptions" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="editDrawerVisible = false">取消</Button>
|
||||
<Button type="primary" :loading="editSubmitting" @click="submitEdit">确定</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.statistics-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.config-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.config-footer {
|
||||
text-align: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,59 +1,226 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Table, Button, Drawer, Form, Input, Select, Space, Popconfirm, message } from 'ant-design-vue'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import { getProjects, createProject, updateProject, deleteProject } from '@/api/project'
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Button, Drawer, Form, Input, Select, Space, message, Tag, Descriptions, DescriptionsItem, Card, Statistic, Row, Col, Tabs, TabPane, Table, Empty, Switch } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
UserAddOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import {
|
||||
queryProjects,
|
||||
createProject,
|
||||
updateProject,
|
||||
deleteProject,
|
||||
enableProject,
|
||||
disableProject,
|
||||
getProjectStatistics,
|
||||
getProjectConfig,
|
||||
updateProjectConfig
|
||||
} from '@/api/project'
|
||||
import { TableActions, Pagination, StatusTag, SpaceTree } from '@/components'
|
||||
import type { Project } from '@/types'
|
||||
import type { ProjectQuery, ProjectStatus, ProjectFormData, PageResponse, ProjectType } from '@/types/project'
|
||||
import { ProjectStatusMap, ProjectTypeMap } from '@/types/project'
|
||||
|
||||
interface ProjectFormData {
|
||||
id?: string
|
||||
code?: string
|
||||
name?: string
|
||||
description?: string
|
||||
address?: string
|
||||
province?: string
|
||||
city?: string
|
||||
district?: string
|
||||
status?: string
|
||||
const router = useRouter()
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date: string | Date) => {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: '项目编码', dataIndex: 'code', key: 'code', width: 120 },
|
||||
// 表格列定义
|
||||
const columns: ColumnsType = [
|
||||
{ title: '项目名称', dataIndex: 'name', key: 'name', width: 200 },
|
||||
{ title: '类型', dataIndex: 'projectType', key: 'projectType', width: 100 },
|
||||
{ title: '地址', dataIndex: 'address', key: 'address', ellipsis: true },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 80 },
|
||||
{ title: '操作', key: 'action', width: 120, fixed: 'right' }
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 120 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
fixed: 'right' as const,
|
||||
customRender: () => undefined
|
||||
}
|
||||
]
|
||||
|
||||
const projects = ref<Project[]>([])
|
||||
// 状态选项
|
||||
const statusOptions = Object.entries(ProjectStatusMap).map(([value, { label }]) => ({
|
||||
value,
|
||||
label
|
||||
}))
|
||||
|
||||
// 类型选项
|
||||
const typeOptions = Object.entries(ProjectTypeMap).map(([value, { label }]) => ({
|
||||
value,
|
||||
label
|
||||
}))
|
||||
|
||||
// 成员表格列
|
||||
const memberColumns: ColumnsType = [
|
||||
{ title: '姓名', dataIndex: 'realName', key: 'realName' },
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||
{ title: '角色', dataIndex: 'roleInProject', key: 'roleInProject' },
|
||||
{ title: '加入时间', dataIndex: 'createdAt', key: 'createdAt' }
|
||||
]
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive<ProjectQuery>({
|
||||
keyword: '',
|
||||
status: undefined,
|
||||
page: 0,
|
||||
size: 10
|
||||
})
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const tableData = ref<Project[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 分页后的数据
|
||||
const paginatedData = computed(() => {
|
||||
const start = (pagination.current - 1) * pagination.pageSize
|
||||
const end = start + pagination.pageSize
|
||||
return tableData.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 抽屉状态
|
||||
const drawerVisible = ref(false)
|
||||
const drawerTitle = ref('')
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
|
||||
const formState = ref<ProjectFormData>({
|
||||
id: '',
|
||||
code: '',
|
||||
// 查看抽屉状态
|
||||
const viewDrawerVisible = ref(false)
|
||||
const viewProject = ref<Project | null>(null)
|
||||
const viewLoading = ref(false)
|
||||
const viewStatistics = ref<any>(null)
|
||||
const viewMembers = ref<any[]>([])
|
||||
const viewActiveTab = ref('info')
|
||||
|
||||
// 编辑抽屉状态
|
||||
const editDrawerVisible = ref(false)
|
||||
const editProject = ref<Project | null>(null)
|
||||
const editLoading = ref(false)
|
||||
const editActiveTab = ref('info')
|
||||
const editFormState = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
address: '',
|
||||
projectType: 'RESIDENTIAL' as ProjectType,
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
status: 'ACTIVE'
|
||||
})
|
||||
const editSubmitting = ref(false)
|
||||
|
||||
// 项目配置状态
|
||||
const projectConfig = ref<any>({
|
||||
enableReservation: false,
|
||||
enableVisitor: false,
|
||||
enableComplaint: true,
|
||||
enablePayment: false,
|
||||
enableAnnouncement: true,
|
||||
enableSurvey: false,
|
||||
enableVote: false,
|
||||
enableMaintenance: true,
|
||||
enableAsset: false,
|
||||
customConfig: {}
|
||||
})
|
||||
const configLoading = ref(false)
|
||||
const configSaving = ref(false)
|
||||
|
||||
// 新增表单状态
|
||||
const createFormState = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
address: '',
|
||||
projectType: 'RESIDENTIAL' as ProjectType,
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
status: 'ACTIVE'
|
||||
})
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'ACTIVE', label: '正常', color: 'success' },
|
||||
{ value: 'DISABLED', label: '禁用', color: 'error' }
|
||||
]
|
||||
// 点击项目名称查看详情
|
||||
const handleNameClick = async (record: Project) => {
|
||||
viewProject.value = record
|
||||
viewActiveTab.value = 'info'
|
||||
viewDrawerVisible.value = true
|
||||
await fetchViewData(record.id)
|
||||
}
|
||||
|
||||
const fetchViewData = async (id: string) => {
|
||||
viewLoading.value = true
|
||||
try {
|
||||
const [statsRes] = await Promise.all([
|
||||
getProjectStatistics(id).catch(() => ({ data: { data: null } }))
|
||||
])
|
||||
viewStatistics.value = statsRes.data?.data || null
|
||||
} finally {
|
||||
viewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑项目
|
||||
const handleEdit = async (record: Project) => {
|
||||
editProject.value = record
|
||||
editActiveTab.value = 'info'
|
||||
editDrawerVisible.value = true
|
||||
editFormState.value = {
|
||||
name: record.name,
|
||||
description: record.description || '',
|
||||
address: record.address || '',
|
||||
projectType: record.projectType || 'RESIDENTIAL',
|
||||
province: record.province || '',
|
||||
city: record.city || '',
|
||||
district: record.district || '',
|
||||
status: record.status || 'ACTIVE'
|
||||
}
|
||||
loadProjectConfig(record.id)
|
||||
}
|
||||
|
||||
const loadProjectConfig = async (projectId: string) => {
|
||||
configLoading.value = true
|
||||
try {
|
||||
const res = await getProjectConfig(projectId)
|
||||
if (res.data?.data) {
|
||||
projectConfig.value = res.data.data
|
||||
}
|
||||
} catch {
|
||||
// 使用默认值
|
||||
} finally {
|
||||
configLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProjects()
|
||||
projects.value = res.data
|
||||
const params: ProjectQuery = {
|
||||
...queryParams,
|
||||
page: pagination.current - 1,
|
||||
size: pagination.pageSize
|
||||
}
|
||||
const res = await queryProjects(params)
|
||||
const data = res.data.data as PageResponse<Project>
|
||||
tableData.value = data.content
|
||||
pagination.total = data.totalElements
|
||||
} catch {
|
||||
message.error('获取项目列表失败')
|
||||
} finally {
|
||||
|
|
@ -61,14 +228,34 @@ const fetchProjects = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
drawerTitle.value = '新增项目'
|
||||
formState.value = {
|
||||
id: '',
|
||||
code: '',
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchProjects()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.keyword = ''
|
||||
queryParams.status = undefined
|
||||
pagination.current = 1
|
||||
fetchProjects()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
pagination.current = page
|
||||
pagination.pageSize = pageSize
|
||||
fetchProjects()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = async () => {
|
||||
createFormState.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
address: '',
|
||||
projectType: 'RESIDENTIAL',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
|
|
@ -77,22 +264,58 @@ const handleAdd = () => {
|
|||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record: Project) => {
|
||||
drawerTitle.value = '编辑项目'
|
||||
formState.value = {
|
||||
id: record.id,
|
||||
code: record.code,
|
||||
// 快速编辑项目
|
||||
const handleQuickEdit = async (record: Project) => {
|
||||
editProject.value = record
|
||||
editActiveTab.value = 'info'
|
||||
editFormState.value = {
|
||||
name: record.name,
|
||||
description: record.description || '',
|
||||
address: record.address || '',
|
||||
projectType: record.projectType || 'RESIDENTIAL',
|
||||
province: record.province || '',
|
||||
city: record.city || '',
|
||||
district: record.district || '',
|
||||
status: record.status
|
||||
status: record.status || 'ACTIVE'
|
||||
}
|
||||
drawerVisible.value = true
|
||||
editDrawerVisible.value = true
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleViewProject = async (record: Project) => {
|
||||
viewProject.value = record
|
||||
viewActiveTab.value = 'info'
|
||||
viewDrawerVisible.value = true
|
||||
await fetchViewData(record.id)
|
||||
}
|
||||
|
||||
// 成员管理
|
||||
const handleMemberManage = (record: Project) => {
|
||||
router.push(`/project/detail/${record.id}?tab=member`)
|
||||
}
|
||||
|
||||
// 空间管理
|
||||
const handleSpaceManage = (record: Project) => {
|
||||
router.push(`/project/${record.id}/space?name=${encodeURIComponent(record.name)}`)
|
||||
}
|
||||
|
||||
// 切换状态
|
||||
const handleToggleStatus = async (record: Project) => {
|
||||
try {
|
||||
if (record.status === 'ACTIVE') {
|
||||
await disableProject(record.id)
|
||||
message.success('已禁用项目')
|
||||
} else {
|
||||
await enableProject(record.id)
|
||||
message.success('已启用项目')
|
||||
}
|
||||
fetchProjects()
|
||||
} catch {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteProject(id)
|
||||
|
|
@ -103,47 +326,57 @@ const handleDelete = async (id: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 提交表单
|
||||
const handleCreateSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
submitting.value = true
|
||||
|
||||
if (formState.value.id) {
|
||||
await updateProject(formState.value.id, formState.value)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await createProject(formState.value)
|
||||
message.success('创建成功')
|
||||
}
|
||||
await createProject(createFormState.value)
|
||||
message.success('创建成功')
|
||||
drawerVisible.value = false
|
||||
fetchProjects()
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) return
|
||||
message.error('操作失败')
|
||||
message.error('创建失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
drawerVisible.value = false
|
||||
const handleEditSubmit = async () => {
|
||||
try {
|
||||
editSubmitting.value = true
|
||||
await updateProject(editProject.value!.id, editFormState.value)
|
||||
message.success('更新成功')
|
||||
editDrawerVisible.value = false
|
||||
fetchProjects()
|
||||
} catch (error: any) {
|
||||
message.error('更新失败')
|
||||
} finally {
|
||||
editSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
ACTIVE: 'success',
|
||||
DISABLED: 'error'
|
||||
}
|
||||
return map[status] || 'default'
|
||||
// 状态标签配置
|
||||
const statusTagMap = {
|
||||
ACTIVE: { color: 'success', label: '正常', icon: 'check' },
|
||||
INACTIVE: { color: 'error', label: '禁用', icon: 'close' },
|
||||
DRAFT: { color: 'warning', label: '草稿', icon: 'warning' },
|
||||
ARCHIVED: { color: 'default', label: '归档', icon: 'minus' }
|
||||
}
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
ACTIVE: '正常',
|
||||
DISABLED: '禁用'
|
||||
// 是否可以切换状态
|
||||
const canToggleStatus = (status: ProjectStatus) => {
|
||||
return status === 'ACTIVE' || status === 'INACTIVE'
|
||||
}
|
||||
|
||||
// 获取切换状态按钮配置
|
||||
const getToggleAction = (record: Project) => {
|
||||
if (!canToggleStatus(record.status)) return null
|
||||
const isActive = record.status === 'ACTIVE'
|
||||
return {
|
||||
key: 'toggle',
|
||||
label: isActive ? '禁用' : '启用',
|
||||
danger: isActive
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
onMounted(fetchProjects)
|
||||
|
|
@ -161,91 +394,391 @@ onMounted(fetchProjects)
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<div class="table-card">
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="projects"
|
||||
:loading="loading"
|
||||
:row-key="(record: Project) => record.id"
|
||||
:pagination="{ pageSize: 10, showSizeChanger: true, showTotal: (total: number) => `共 ${total} 条` }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusLabel(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEdit(record)">
|
||||
<EditOutlined /> 编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确认删除"
|
||||
description="删除后不可恢复,是否继续?"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDelete(record.id)"
|
||||
>
|
||||
<Button type="link" danger size="small">
|
||||
<DeleteOutlined /> 删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<Space>
|
||||
<Input
|
||||
v-model:value="queryParams.keyword"
|
||||
placeholder="搜索项目名称/编码"
|
||||
style="width: 240px"
|
||||
allow-clear
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="queryParams.status"
|
||||
placeholder="请选择状态"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
<SearchOutlined /> 查询
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 抽屉 -->
|
||||
<!-- 表格 -->
|
||||
<div class="table-card">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="paginatedData"
|
||||
:loading="loading"
|
||||
:row-key="(record: Project) => record.id"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<a @click="handleNameClick(record as Project)" class="project-name-link">
|
||||
{{ record.name }}
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'projectType'">
|
||||
<a-tag :color="ProjectTypeMap[record.projectType as ProjectType]?.color">
|
||||
{{ ProjectTypeMap[record.projectType as ProjectType]?.label || '-' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'createdAt'">
|
||||
{{ formatDate(record.createdAt) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<StatusTag :status="record.status" :map="statusTagMap" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<TableActions
|
||||
:show-edit="false"
|
||||
:show-delete="false"
|
||||
:actions="[
|
||||
{ key: 'view', label: '查看' },
|
||||
{ key: 'edit', label: '编辑' },
|
||||
{ key: 'space', label: '空间' },
|
||||
{ key: 'member', label: '成员' },
|
||||
getToggleAction(record as Project),
|
||||
{ key: 'delete', label: '删除', danger: true }
|
||||
].filter(Boolean)"
|
||||
@action="(key) => {
|
||||
if (key === 'space') handleSpaceManage(record as Project)
|
||||
else if (key === 'member') handleMemberManage(record as Project)
|
||||
else if (key === 'toggle') handleToggleStatus(record as Project)
|
||||
else if (key === 'delete') handleDelete((record as Project).id)
|
||||
}"
|
||||
@view="handleView(record as Project)"
|
||||
@edit="handleEdit(record as Project)"
|
||||
@delete="handleDelete((record as Project).id)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<Pagination
|
||||
v-model:current="pagination.current"
|
||||
v-model:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新增抽屉 -->
|
||||
<Drawer
|
||||
v-model:open="drawerVisible"
|
||||
:title="drawerTitle"
|
||||
title="新增项目"
|
||||
width="560px"
|
||||
:footer-style="{ textAlign: 'right' }"
|
||||
@close="handleClose"
|
||||
@close="drawerVisible = false"
|
||||
>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:model="createFormState"
|
||||
layout="vertical"
|
||||
:rules="{
|
||||
code: [{ required: true, message: '请输入项目编码' }],
|
||||
name: [{ required: true, message: '请输入项目名称' }]
|
||||
}"
|
||||
>
|
||||
<Form.Item label="项目编码" name="code">
|
||||
<Input v-model:value="formState.code" :disabled="!!formState.id" placeholder="请输入项目编码" />
|
||||
</Form.Item>
|
||||
<Form.Item label="项目名称" name="name">
|
||||
<Input v-model:value="formState.name" placeholder="请输入项目名称" />
|
||||
<Input v-model:value="createFormState.name" placeholder="请输入项目名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="项目类型" name="projectType">
|
||||
<Select v-model:value="createFormState.projectType" placeholder="请选择项目类型" :options="typeOptions" />
|
||||
</Form.Item>
|
||||
<Form.Item label="描述" name="description">
|
||||
<Input.TextArea v-model:value="formState.description" placeholder="请输入描述" :rows="2" />
|
||||
</Form.Item>
|
||||
<Form.Item label="地址" name="address">
|
||||
<Input v-model:value="formState.address" placeholder="请输入详细地址" />
|
||||
<Input.TextArea v-model:value="createFormState.description" placeholder="请输入描述" :rows="2" />
|
||||
</Form.Item>
|
||||
<Form.Item label="省份" name="province">
|
||||
<Input v-model:value="formState.province" placeholder="请输入省份" />
|
||||
<Input v-model:value="createFormState.province" placeholder="请输入省份" />
|
||||
</Form.Item>
|
||||
<Form.Item label="城市" name="city">
|
||||
<Input v-model:value="formState.city" placeholder="请输入城市" />
|
||||
<Input v-model:value="createFormState.city" placeholder="请输入城市" />
|
||||
</Form.Item>
|
||||
<Form.Item label="区县" name="district">
|
||||
<Input v-model:value="formState.district" placeholder="请输入区县" />
|
||||
<Input v-model:value="createFormState.district" placeholder="请输入区县" />
|
||||
</Form.Item>
|
||||
<Form.Item label="详细地址" name="address">
|
||||
<Input v-model:value="createFormState.address" placeholder="请输入详细地址" />
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Select v-model:value="formState.status" :options="statusOptions" />
|
||||
<Select v-model:value="createFormState.status" :options="statusOptions" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="handleClose">取消</Button>
|
||||
<Button type="primary" :loading="submitting" @click="handleSubmit">确定</Button>
|
||||
<Button @click="drawerVisible = false">取消</Button>
|
||||
<Button type="primary" :loading="submitting" @click="handleCreateSubmit">确定</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Drawer>
|
||||
|
||||
<!-- 编辑抽屉 -->
|
||||
<Drawer
|
||||
v-model:open="editDrawerVisible"
|
||||
title="编辑项目"
|
||||
width="1200px"
|
||||
:destroyOnClose="true"
|
||||
>
|
||||
<template v-if="editProject">
|
||||
<Tabs v-model:activeKey="editActiveTab">
|
||||
<TabPane key="info" tab="基本信息">
|
||||
<Form
|
||||
:model="editFormState"
|
||||
layout="vertical"
|
||||
:rules="{
|
||||
name: [{ required: true, message: '请输入项目名称' }]
|
||||
}"
|
||||
>
|
||||
<Row :gutter="16">
|
||||
<Col :span="12">
|
||||
<Form.Item label="项目名称" name="name">
|
||||
<Input v-model:value="editFormState.name" placeholder="请输入项目名称" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Form.Item label="项目类型" name="projectType">
|
||||
<Select v-model:value="editFormState.projectType" :options="typeOptions" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Form.Item label="省份" name="province">
|
||||
<Input v-model:value="editFormState.province" placeholder="请输入省份" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Form.Item label="城市" name="city">
|
||||
<Input v-model:value="editFormState.city" placeholder="请输入城市" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Form.Item label="区县" name="district">
|
||||
<Input v-model:value="editFormState.district" placeholder="请输入区县" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Form.Item label="状态" name="status">
|
||||
<Select v-model:value="editFormState.status" :options="statusOptions" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="24">
|
||||
<Form.Item label="详细地址" name="address">
|
||||
<Input.TextArea v-model:value="editFormState.address" placeholder="请输入详细地址" :rows="2" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="24">
|
||||
<Form.Item label="描述" name="description">
|
||||
<Input.TextArea v-model:value="editFormState.description" placeholder="请输入描述" :rows="3" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</TabPane>
|
||||
<TabPane key="members" tab="成员管理">
|
||||
<div style="text-align: right; margin-bottom: 8px">
|
||||
<Button type="primary" size="small">
|
||||
<UserAddOutlined /> 添加成员
|
||||
</Button>
|
||||
</div>
|
||||
<Table :columns="memberColumns" :dataSource="[]" :pagination="false" size="small" />
|
||||
</TabPane>
|
||||
<TabPane key="space" tab="空间管理">
|
||||
<div style="margin-bottom: 8px">
|
||||
<SpaceTree :projectId="editProject?.id" mode="edit" />
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane key="config" tab="项目配置">
|
||||
<Row :gutter="16">
|
||||
<Col :span="8">
|
||||
<Form.Item label="预约功能" name="enableReservation">
|
||||
<Switch v-model:checked="projectConfig.enableReservation" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="访客管理" name="enableVisitor">
|
||||
<Switch v-model:checked="projectConfig.enableVisitor" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="投诉建议" name="enableComplaint">
|
||||
<Switch v-model:checked="projectConfig.enableComplaint" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="缴费支付" name="enablePayment">
|
||||
<Switch v-model:checked="projectConfig.enablePayment" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="公告通知" name="enableAnnouncement">
|
||||
<Switch v-model:checked="projectConfig.enableAnnouncement" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="问卷调查" name="enableSurvey">
|
||||
<Switch v-model:checked="projectConfig.enableSurvey" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="投票表决" name="enableVote">
|
||||
<Switch v-model:checked="projectConfig.enableVote" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="设备维保" name="enableMaintenance">
|
||||
<Switch v-model:checked="projectConfig.enableMaintenance" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="资产管理" name="enableAsset">
|
||||
<Switch v-model:checked="projectConfig.enableAsset" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="editDrawerVisible = false">取消</Button>
|
||||
<Button type="primary" :loading="editSubmitting" @click="handleEditSubmit">确定</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Drawer>
|
||||
|
||||
<Drawer
|
||||
v-model:open="viewDrawerVisible"
|
||||
title="项目详情"
|
||||
width="900px"
|
||||
:destroyOnClose="true"
|
||||
>
|
||||
<template v-if="viewProject">
|
||||
<Row :gutter="16" class="statistics-row">
|
||||
<Col :span="4">
|
||||
<Card size="small">
|
||||
<Statistic title="成员数" :value="viewStatistics?.memberCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Card size="small">
|
||||
<Statistic title="楼栋数" :value="viewStatistics?.buildingCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Card size="small">
|
||||
<Statistic title="房间数" :value="viewStatistics?.roomCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Card size="small">
|
||||
<Statistic title="业主数" :value="viewStatistics?.ownerCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Card size="small">
|
||||
<Statistic title="租户数" :value="viewStatistics?.tenantCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Card size="small">
|
||||
<Statistic title="进行中任务" :value="viewStatistics?.activeTaskCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Tabs v-model:activeKey="viewActiveTab" style="margin-top: 16px">
|
||||
<TabPane key="info" tab="基本信息">
|
||||
<Descriptions :column="2" bordered size="small">
|
||||
<DescriptionsItem label="项目名称">{{ viewProject.name }}</DescriptionsItem>
|
||||
<DescriptionsItem label="项目类型">
|
||||
<a-tag :color="ProjectTypeMap[viewProject.projectType as ProjectType]?.color">
|
||||
{{ ProjectTypeMap[viewProject.projectType as ProjectType]?.label || '-' }}
|
||||
</a-tag>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="状态">
|
||||
<a-tag :color="ProjectStatusMap[viewProject.status as ProjectStatus]?.color">
|
||||
{{ ProjectStatusMap[viewProject.status as ProjectStatus]?.label || '-' }}
|
||||
</a-tag>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="描述" :span="2">{{ viewProject.description || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="省份">{{ viewProject.province || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="城市">{{ viewProject.city || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="区县">{{ viewProject.district || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="详细地址" :span="2">{{ viewProject.address || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="创建时间">{{ viewProject.createdAt || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="更新时间">{{ viewProject.updatedAt || '-' }}</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</TabPane>
|
||||
<TabPane key="members" tab="成员管理">
|
||||
<div style="text-align: right; margin-bottom: 8px">
|
||||
<Button type="primary" size="small">
|
||||
<UserAddOutlined /> 添加成员
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
:columns="memberColumns"
|
||||
:dataSource="viewMembers"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane key="config" tab="功能配置">
|
||||
<Empty description="暂无配置数据" />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</template>
|
||||
</Drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.project-name-link {
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.project-name-link:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { Select, Spin, Tag } from 'ant-design-vue'
|
||||
import type { SelectValue } from 'ant-design-vue/es/select'
|
||||
import { getProjectSelectorList } from '@/api/project'
|
||||
import type { ProjectSelectorItem } from '@/types/project'
|
||||
import { ProjectStatusMap } from '@/types/project'
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
value?: string | string[]
|
||||
multiple?: boolean
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
filterStatus?: string[]
|
||||
}>()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: string | string[] | undefined): void
|
||||
(e: 'change', value: string | string[] | undefined, item: ProjectSelectorItem | ProjectSelectorItem[] | undefined): void
|
||||
}>()
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const options = ref<ProjectSelectorItem[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const selectedValue = computed({
|
||||
get: () => props.value,
|
||||
set: (val) => emit('update:value', val)
|
||||
})
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async (keyword?: string) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProjectSelectorList({ keyword })
|
||||
let data = res.data || []
|
||||
|
||||
// 过滤状态
|
||||
if (props.filterStatus && props.filterStatus.length > 0) {
|
||||
data = data.filter(item => props.filterStatus!.includes(item.status))
|
||||
}
|
||||
|
||||
options.value = data
|
||||
} catch {
|
||||
options.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = (value: string) => {
|
||||
searchKeyword.value = value
|
||||
fetchProjects(value)
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleChange = (value: SelectValue) => {
|
||||
let selectedItems: ProjectSelectorItem | ProjectSelectorItem[] | undefined
|
||||
|
||||
if (props.multiple && Array.isArray(value)) {
|
||||
const values = value as string[]
|
||||
selectedItems = options.value.filter(item => values.includes(item.id))
|
||||
emit('change', values, selectedItems)
|
||||
} else if (typeof value === 'string') {
|
||||
selectedItems = options.value.find(item => item.id === value)
|
||||
emit('change', value, selectedItems)
|
||||
} else {
|
||||
emit('change', undefined, undefined)
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉展开时加载
|
||||
const handleDropdownVisibleChange = (open: boolean) => {
|
||||
if (open && options.value.length === 0) {
|
||||
fetchProjects()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态标签颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
return ProjectStatusMap[status as keyof typeof ProjectStatusMap]?.color || 'default'
|
||||
}
|
||||
|
||||
// 初始化加载
|
||||
watch(() => props.value, (val) => {
|
||||
if (val && options.value.length === 0) {
|
||||
fetchProjects()
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select
|
||||
v-model:value="selectedValue"
|
||||
:mode="multiple ? 'multiple' : undefined"
|
||||
:placeholder="placeholder || '请选择项目'"
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
:filter-option="false"
|
||||
show-search
|
||||
allow-clear
|
||||
@search="handleSearch"
|
||||
@change="handleChange"
|
||||
@dropdown-visible-change="handleDropdownVisibleChange"
|
||||
>
|
||||
<template #notFoundContent>
|
||||
<Spin v-if="loading" size="small" />
|
||||
<span v-else>暂无数据</span>
|
||||
</template>
|
||||
<Select.Option
|
||||
v-for="item in options"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name"
|
||||
>
|
||||
<div class="project-option">
|
||||
<span class="project-name">{{ item.name }}</span>
|
||||
<span class="project-code">{{ item.code }}</span>
|
||||
<Tag :color="getStatusColor(item.status)" size="small">
|
||||
{{ ProjectStatusMap[item.status as keyof typeof ProjectStatusMap]?.label || item.status }}
|
||||
</Tag>
|
||||
</div>
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.project-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.project-code {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Button, Tree, Card, Table, Form, Input, Select, Modal, message, Drawer, Space, InputNumber } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, HomeOutlined, ApartmentOutlined } from '@ant-design/icons-vue'
|
||||
import {
|
||||
getSpaceTree,
|
||||
getSpaceNode,
|
||||
getSpaceChildren,
|
||||
createSpaceNode,
|
||||
updateSpaceNode,
|
||||
deleteSpaceNode
|
||||
} from '@/api/space'
|
||||
import { StatusTag, Pagination, TableActions } from '@/components'
|
||||
import type { SpaceNode, SpaceNodeTree, SpaceNodeCreateForm, SpaceNodeUpdateForm, SpaceNodeCategory, SpaceNodeType } from '@/types/space'
|
||||
import { SpaceNodeTypeMap, SpaceNodeCategoryMap } from '@/types/space'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const projectId = computed(() => route.params.id as string)
|
||||
const projectName = computed(() => route.query.name as string || '项目空间')
|
||||
|
||||
const loading = ref(false)
|
||||
const treeLoading = ref(false)
|
||||
const selectedNode = ref<SpaceNode | null>(null)
|
||||
const treeData = ref<SpaceNodeTree[]>([])
|
||||
|
||||
const drawerVisible = ref(false)
|
||||
const drawerTitle = ref('')
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
|
||||
const formState = ref<SpaceNodeCreateForm>({
|
||||
projectId: projectId.value,
|
||||
name: '',
|
||||
nodeCategory: 'BUILDING',
|
||||
nodeType: 'BUILDING',
|
||||
parentId: undefined,
|
||||
sortOrder: 0,
|
||||
status: 'ACTIVE'
|
||||
})
|
||||
|
||||
const expandedKeys = ref<string[]>([])
|
||||
const selectedKeys = ref<string[]>([])
|
||||
|
||||
const fetchTree = async () => {
|
||||
treeLoading.value = true
|
||||
try {
|
||||
const res = await getSpaceTree(projectId.value)
|
||||
treeData.value = res.data.data || []
|
||||
if (treeData.value.length > 0 && expandedKeys.value.length === 0) {
|
||||
expandedKeys.value = [treeData.value[0].id]
|
||||
selectedKeys.value = [treeData.value[0].id]
|
||||
selectedNode.value = treeData.value[0]
|
||||
}
|
||||
} catch {
|
||||
message.error('获取空间树失败')
|
||||
} finally {
|
||||
treeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTreeSelect = async (keys: string[], info: any) => {
|
||||
if (keys.length === 0) return
|
||||
const nodeId = keys[0]
|
||||
selectedKeys.value = [nodeId]
|
||||
try {
|
||||
const res = await getSpaceNode(nodeId)
|
||||
selectedNode.value = res.data.data
|
||||
} catch {
|
||||
message.error('获取节点详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleTreeExpand = (keys: string[]) => {
|
||||
expandedKeys.value = keys
|
||||
}
|
||||
|
||||
const handleAdd = (parentId?: string) => {
|
||||
drawerTitle.value = parentId ? '新增子节点' : '新增根节点'
|
||||
formState.value = {
|
||||
projectId: projectId.value,
|
||||
name: '',
|
||||
nodeCategory: 'BUILDING',
|
||||
nodeType: 'BUILDING',
|
||||
parentId: parentId,
|
||||
sortOrder: 0,
|
||||
status: 'ACTIVE'
|
||||
}
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record: SpaceNode) => {
|
||||
drawerTitle.value = '编辑节点'
|
||||
formState.value = {
|
||||
projectId: projectId.value,
|
||||
name: record.name,
|
||||
fullName: record.fullName,
|
||||
shortName: record.shortName,
|
||||
nodeCategory: record.nodeCategory,
|
||||
nodeType: record.nodeType,
|
||||
usageType: record.usageType,
|
||||
parentId: record.parentId,
|
||||
sortOrder: record.sortOrder || 0,
|
||||
status: record.status || 'ACTIVE',
|
||||
buildingArea: record.buildingArea,
|
||||
usableArea: record.usableArea,
|
||||
floorNumber: record.floorNumber,
|
||||
address: record.address
|
||||
}
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
submitting.value = true
|
||||
|
||||
await createSpaceNode(formState.value as any)
|
||||
message.success('创建成功')
|
||||
drawerVisible.value = false
|
||||
fetchTree()
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) return
|
||||
message.error('操作失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteSpaceNode(id)
|
||||
message.success('删除成功')
|
||||
fetchTree()
|
||||
selectedNode.value = null
|
||||
} catch {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
drawerVisible.value = false
|
||||
}
|
||||
|
||||
const categoryOptions = computed(() =>
|
||||
Object.entries(SpaceNodeCategoryMap).map(([value, { label }]) => ({ value, label }))
|
||||
)
|
||||
|
||||
const typeOptions = computed(() => {
|
||||
const category = formState.value.nodeCategory
|
||||
return Object.entries(SpaceNodeTypeMap)
|
||||
.filter(([_, config]) => config.category === category)
|
||||
.map(([value, config]) => ({ value, label: config.label }))
|
||||
})
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'ACTIVE', label: '正常' },
|
||||
{ value: 'INACTIVE', label: '禁用' }
|
||||
]
|
||||
|
||||
const statusTagMap = {
|
||||
ACTIVE: { color: 'success', label: '正常' },
|
||||
INACTIVE: { color: 'error', label: '禁用' }
|
||||
}
|
||||
|
||||
const columns: ColumnsType = [
|
||||
{ title: '名称', dataIndex: 'name', key: 'name', width: 150 },
|
||||
{ title: '类型', dataIndex: 'nodeType', key: 'nodeType', width: 80 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 80 },
|
||||
{ title: '面积', dataIndex: 'buildingArea', key: 'buildingArea', width: 100 },
|
||||
{ title: '地址', dataIndex: 'address', key: 'address', ellipsis: true }
|
||||
]
|
||||
|
||||
const getNodeTypeLabel = (type: SpaceNodeType) => {
|
||||
return SpaceNodeTypeMap[type]?.label || type
|
||||
}
|
||||
|
||||
onMounted(fetchTree)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">{{ projectName }} - 空间管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<Button type="primary" @click="handleAdd()">
|
||||
<PlusOutlined /> 新增节点
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-layout">
|
||||
<Card class="tree-card" :loading="treeLoading">
|
||||
<template #title>
|
||||
<span>空间结构</span>
|
||||
</template>
|
||||
<template #extra>
|
||||
<Button type="link" size="small" @click="handleAdd()">
|
||||
<PlusOutlined />
|
||||
</Button>
|
||||
</template>
|
||||
<div class="tree-container">
|
||||
<Tree
|
||||
v-if="treeData.length > 0"
|
||||
:tree-data="treeData"
|
||||
:expanded-keys="expandedKeys"
|
||||
:selected-keys="selectedKeys"
|
||||
:show-icon="true"
|
||||
@select="handleTreeSelect"
|
||||
@expand="handleTreeExpand"
|
||||
>
|
||||
<template #icon="{ node }">
|
||||
<HomeOutlined v-if="(node as any).nodeType === 'ROOM'" />
|
||||
<ApartmentOutlined v-else />
|
||||
</template>
|
||||
</Tree>
|
||||
<a-empty v-else description="暂无空间数据">
|
||||
<Button type="primary" @click="handleAdd()">添加第一个节点</Button>
|
||||
</a-empty>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="detail-card">
|
||||
<template #title>
|
||||
<span>节点详情</span>
|
||||
</template>
|
||||
<template v-if="selectedNode">
|
||||
<div class="detail-info">
|
||||
<a-descriptions :column="2" size="small" bordered>
|
||||
<a-descriptions-item label="名称">{{ selectedNode.name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="类型">{{ getNodeTypeLabel(selectedNode.nodeType) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<StatusTag :status="selectedNode.status" :map="statusTagMap" />
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="建筑面积">{{ selectedNode.buildingArea }} ㎡</a-descriptions-item>
|
||||
<a-descriptions-item label="使用面积">{{ selectedNode.usableArea }} ㎡</a-descriptions-item>
|
||||
<a-descriptions-item label="楼层" :span="2">{{ selectedNode.floorNumber }}</a-descriptions-item>
|
||||
<a-descriptions-item label="地址" :span="2">{{ selectedNode.address || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="完整路径" :span="2">{{ selectedNode.treePathName || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<div class="detail-actions">
|
||||
<Button type="primary" @click="handleEdit(selectedNode)">
|
||||
<EditOutlined /> 编辑
|
||||
</Button>
|
||||
<Button danger @click="handleDelete(selectedNode.id)">
|
||||
<DeleteOutlined /> 删除
|
||||
</Button>
|
||||
<Button @click="handleAdd(selectedNode.id, selectedNode.code)">
|
||||
<PlusOutlined /> 添加子节点
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-empty v-else description="请从左侧选择节点查看详情" />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
v-model:open="drawerVisible"
|
||||
:title="drawerTitle"
|
||||
width="500px"
|
||||
@close="handleClose"
|
||||
>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
layout="vertical"
|
||||
:rules="{
|
||||
name: [{ required: true, message: '请输入名称' }],
|
||||
nodeCategory: [{ required: true, message: '请选择节点大类' }],
|
||||
nodeType: [{ required: true, message: '请选择节点类型' }]
|
||||
}"
|
||||
>
|
||||
<Form.Item label="节点大类" name="nodeCategory">
|
||||
<Select
|
||||
v-model:value="formState.nodeCategory"
|
||||
:options="categoryOptions"
|
||||
@change="formState.nodeType = undefined"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="节点类型" name="nodeType">
|
||||
<Select
|
||||
v-model:value="formState.nodeType"
|
||||
:options="typeOptions"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="名称" name="name">
|
||||
<Input v-model:value="formState.name" placeholder="请输入名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="全称" name="fullName">
|
||||
<Input v-model:value="formState.fullName" placeholder="请输入全称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简称" name="shortName">
|
||||
<Input v-model:value="formState.shortName" placeholder="请输入简称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Select v-model:value="formState.status" :options="statusOptions" />
|
||||
</Form.Item>
|
||||
<Form.Item label="建筑面积" name="buildingArea">
|
||||
<InputNumber v-model:value="formState.buildingArea" placeholder="请输入建筑面积" style="width: 100%" />
|
||||
</Form.Item>
|
||||
<Form.Item label="使用面积" name="usableArea">
|
||||
<InputNumber v-model:value="formState.usableArea" placeholder="请输入使用面积" style="width: 100%" />
|
||||
</Form.Item>
|
||||
<Form.Item label="楼层" name="floorNumber">
|
||||
<InputNumber v-model:value="formState.floorNumber" placeholder="请输入楼层" style="width: 100%" />
|
||||
</Form.Item>
|
||||
<Form.Item label="地址" name="address">
|
||||
<Input v-model:value="formState.address" placeholder="请输入地址" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="handleClose">取消</Button>
|
||||
<Button type="primary" :loading="submitting" @click="handleSubmit">确定</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.space-layout {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.tree-card {
|
||||
width: 320px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
max-height: calc(100vh - 300px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Descriptions, Button, Space, Card, Statistic, Row, Col, message } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
InboxOutlined,
|
||||
ExportOutlined,
|
||||
ReloadOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import {
|
||||
getSparePartDetail,
|
||||
getSparePartRecords,
|
||||
type SparePart,
|
||||
type StockRecord
|
||||
} from '@/api/sparepart'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const recordLoading = ref(false)
|
||||
const sparePart = ref<SparePart | null>(null)
|
||||
const records = ref<StockRecord[]>([])
|
||||
|
||||
const id = route.params.id as string
|
||||
|
||||
// 表格列定义
|
||||
const recordColumns: ColumnsType = [
|
||||
{ title: '操作类型', dataIndex: 'operationType', key: 'operationType', width: 100 },
|
||||
{ title: '数量', dataIndex: 'quantity', key: 'quantity', width: 80 },
|
||||
{ title: '操作前库存', dataIndex: 'beforeStock', key: 'beforeStock', width: 100 },
|
||||
{ title: '操作后库存', dataIndex: 'afterStock', key: 'afterStock', width: 100 },
|
||||
{ title: '关联工单', dataIndex: 'relatedOrderNo', key: 'relatedOrderNo', width: 140 },
|
||||
{ title: '操作人', dataIndex: 'operatorName', key: 'operatorName', width: 100 },
|
||||
{ title: '备注', dataIndex: 'remark', key: 'remark' },
|
||||
{ title: '操作时间', dataIndex: 'createdAt', key: 'createdAt', width: 180 }
|
||||
]
|
||||
|
||||
// 获取备件详情
|
||||
const fetchDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getSparePartDetail(id)
|
||||
sparePart.value = res.data.data
|
||||
} catch {
|
||||
message.error('获取备件详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取记录
|
||||
const fetchRecords = async () => {
|
||||
recordLoading.value = true
|
||||
try {
|
||||
const res = await getSparePartRecords(id)
|
||||
records.value = res.data.data || []
|
||||
} catch {
|
||||
message.error('获取库存记录失败')
|
||||
} finally {
|
||||
recordLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回列表
|
||||
const handleBack = () => {
|
||||
router.push('/sparepart/list')
|
||||
}
|
||||
|
||||
// 入库
|
||||
const handleInStock = () => {
|
||||
router.push(`/sparepart/stock/in?sparePartId=${id}`)
|
||||
}
|
||||
|
||||
// 出库
|
||||
const handleOutStock = () => {
|
||||
router.push(`/sparepart/stock/out?sparePartId=${id}`)
|
||||
}
|
||||
|
||||
// 操作类型显示
|
||||
const getOperationType = (type: string) => {
|
||||
return type === 'IN' ? '入库' : '出库'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDetail()
|
||||
fetchRecords()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<Space>
|
||||
<Button @click="handleBack">
|
||||
<ArrowLeftOutlined /> 返回
|
||||
</Button>
|
||||
<h2 class="page-title">备件详情</h2>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 备件基本信息 -->
|
||||
<Card title="基本信息" style="margin-bottom: 16px">
|
||||
<Descriptions :column="3" bordered>
|
||||
<Descriptions.Item label="备件编码">{{ sparePart?.code || '-' }}</Descriptions.Item>
|
||||
<Descriptions.Item label="备件名称">{{ sparePart?.name || '-' }}</Descriptions.Item>
|
||||
<Descriptions.Item label="分类">{{ sparePart?.categoryName || '-' }}</Descriptions.Item>
|
||||
<Descriptions.Item label="单位">{{ sparePart?.unit || '-' }}</Descriptions.Item>
|
||||
<Descriptions.Item label="所属项目">{{ sparePart?.projectName || '-' }}</Descriptions.Item>
|
||||
<Descriptions.Item label="描述">{{ sparePart?.description || '-' }}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<!-- 库存信息 -->
|
||||
<Card title="库存信息" style="margin-bottom: 16px">
|
||||
<Row :gutter="24">
|
||||
<Col :span="6">
|
||||
<Statistic
|
||||
title="当前库存"
|
||||
:value="sparePart?.currentStock || 0"
|
||||
:suffix="sparePart?.unit || '个'"
|
||||
/>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Statistic
|
||||
title="安全库存"
|
||||
:value="sparePart?.safeStock || 0"
|
||||
:suffix="sparePart?.unit || '个'"
|
||||
/>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Statistic
|
||||
title="库存状态"
|
||||
:value="sparePart?.lowStockWarning ? '低库存' : '正常'"
|
||||
:value-style="sparePart?.lowStockWarning ? { color: '#cf1322' } : { color: '#3f8600' }"
|
||||
/>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Space direction="vertical">
|
||||
<Button type="primary" @click="handleInStock">
|
||||
<InboxOutlined /> 入库
|
||||
</Button>
|
||||
<Button @click="handleOutStock">
|
||||
<ExportOutlined /> 出库
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<!-- 库存记录 -->
|
||||
<Card title="库存记录">
|
||||
<template #extra>
|
||||
<Button @click="fetchRecords" :loading="recordLoading">
|
||||
<ReloadOutlined /> 刷新
|
||||
</Button>
|
||||
</template>
|
||||
<a-table
|
||||
:columns="recordColumns"
|
||||
:data-source="records"
|
||||
:loading="recordLoading"
|
||||
:row-key="(record: StockRecord) => record.id"
|
||||
:pagination="{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
}"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'operationType'">
|
||||
<a-tag :color="record.operationType === 'IN' ? 'green' : 'blue'">
|
||||
{{ getOperationType(record.operationType) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'quantity'">
|
||||
<span :style="{ color: record.operationType === 'IN' ? '#3f8600' : '#cf1322' }">
|
||||
{{ record.operationType === 'IN' ? '+' : '-' }}{{ record.quantity }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'createdAt'">
|
||||
{{ record.createdAt ? new Date(record.createdAt).toLocaleString() : '-' }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,445 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Button, Select, Space, message, Tag, Modal, Form, Input, InputNumber, Popconfirm } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
ExclamationCircleOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import {
|
||||
getSparePartList,
|
||||
getSparePartCategories,
|
||||
createSparePart,
|
||||
updateSparePart,
|
||||
deleteSparePart,
|
||||
getLowStockSpareParts,
|
||||
type SparePart,
|
||||
type SparePartCategory,
|
||||
type SparePartForm
|
||||
} from '@/api/sparepart'
|
||||
import { getProjectSelectorList } from '@/api/project'
|
||||
import { TableActions } from '@/components'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType = [
|
||||
{ title: '备件编码', dataIndex: 'code', key: 'code', width: 140 },
|
||||
{ title: '备件名称', dataIndex: 'name', key: 'name', width: 180 },
|
||||
{ title: '分类', dataIndex: 'categoryName', key: 'categoryName', width: 120 },
|
||||
{ title: '单位', dataIndex: 'unit', key: 'unit', width: 80 },
|
||||
{ title: '当前库存', dataIndex: 'currentStock', key: 'currentStock', width: 100 },
|
||||
{ title: '安全库存', dataIndex: 'safeStock', key: 'safeStock', width: 100 },
|
||||
{
|
||||
title: '库存状态',
|
||||
dataIndex: 'lowStockWarning',
|
||||
key: 'lowStockWarning',
|
||||
width: 100
|
||||
},
|
||||
{ title: '操作', key: 'action', width: 180, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// 项目选择选项
|
||||
const projectOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 分类选项
|
||||
const categoryOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
projectId: '',
|
||||
categoryId: undefined as string | undefined
|
||||
})
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const tableData = ref<SparePart[]>([])
|
||||
const lowStockData = ref<SparePart[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 模态框状态
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('新建备件')
|
||||
const modalLoading = ref(false)
|
||||
const editingSparePart = ref<SparePart | null>(null)
|
||||
|
||||
// 表单
|
||||
const formRef = ref()
|
||||
const formState = reactive<SparePartForm>({
|
||||
name: '',
|
||||
code: '',
|
||||
categoryId: undefined,
|
||||
projectId: '',
|
||||
unit: '',
|
||||
currentStock: 0,
|
||||
safeStock: 0,
|
||||
description: ''
|
||||
})
|
||||
|
||||
// 表单验证
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入备件名称' }],
|
||||
code: [{ required: true, message: '请输入备件编码' }],
|
||||
projectId: [{ required: true, message: '请选择项目' }]
|
||||
}
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectSelectorList()
|
||||
projectOptions.value = (res.data.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}))
|
||||
} catch {
|
||||
message.error('获取项目列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分类列表
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const res = await getSparePartCategories()
|
||||
categoryOptions.value = (res.data.data || []).map((item: SparePartCategory) => ({
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}))
|
||||
} catch {
|
||||
message.error('获取分类列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取备件列表
|
||||
const fetchSparePartList = async () => {
|
||||
if (!queryParams.projectId) {
|
||||
message.warning('请先选择项目')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getSparePartList(queryParams.projectId, queryParams.categoryId)
|
||||
const data = res.data.data
|
||||
tableData.value = data.content || []
|
||||
pagination.total = data.totalElements || 0
|
||||
} catch {
|
||||
message.error('获取备件列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取低库存备件
|
||||
const fetchLowStockSpareParts = async () => {
|
||||
if (!queryParams.projectId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getLowStockSpareParts(queryParams.projectId)
|
||||
lowStockData.value = res.data.data || []
|
||||
} catch {
|
||||
message.error('获取低库存备件失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchSparePartList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.projectId = ''
|
||||
queryParams.categoryId = undefined
|
||||
pagination.current = 1
|
||||
tableData.value = []
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
pagination.current = page
|
||||
pagination.pageSize = pageSize
|
||||
fetchSparePartList()
|
||||
}
|
||||
|
||||
// 打开新建模态框
|
||||
const handleAdd = () => {
|
||||
editingSparePart.value = null
|
||||
modalTitle.value = '新建备件'
|
||||
Object.assign(formState, {
|
||||
id: undefined,
|
||||
name: '',
|
||||
code: '',
|
||||
categoryId: undefined,
|
||||
projectId: queryParams.projectId,
|
||||
unit: '',
|
||||
currentStock: 0,
|
||||
safeStock: 0,
|
||||
description: ''
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 打开编辑模态框
|
||||
const handleEdit = (record: SparePart) => {
|
||||
editingSparePart.value = record
|
||||
modalTitle.value = '编辑备件'
|
||||
Object.assign(formState, {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
code: record.code,
|
||||
categoryId: record.categoryId,
|
||||
projectId: record.projectId,
|
||||
unit: record.unit || '',
|
||||
currentStock: record.currentStock || 0,
|
||||
safeStock: record.safeStock || 0,
|
||||
description: record.description || ''
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleView = (record: SparePart) => {
|
||||
router.push(`/sparepart/detail/${record.id}`)
|
||||
}
|
||||
|
||||
// 删除备件
|
||||
const handleDelete = (record: SparePart) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除备件 "${record.name}" 吗?`,
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteSparePart(record.id)
|
||||
message.success('删除成功')
|
||||
fetchSparePartList()
|
||||
} catch {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
modalLoading.value = true
|
||||
if (editingSparePart.value) {
|
||||
await updateSparePart(editingSparePart.value.id, formState)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await createSparePart(formState)
|
||||
message.success('创建成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
fetchSparePartList()
|
||||
} catch (error) {
|
||||
console.error('表单验证失败', error)
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 入库
|
||||
const handleInStock = (record: SparePart) => {
|
||||
router.push(`/sparepart/stock/in?sparePartId=${record.id}`)
|
||||
}
|
||||
|
||||
// 出库
|
||||
const handleOutStock = (record: SparePart) => {
|
||||
router.push(`/sparepart/stock/out?sparePartId=${record.id}`)
|
||||
}
|
||||
|
||||
// 库存状态显示
|
||||
const getStockStatus = (record: SparePart) => {
|
||||
if (record.lowStockWarning) {
|
||||
return { color: 'error', text: '低库存' }
|
||||
}
|
||||
return { color: 'success', text: '正常' }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProjects()
|
||||
fetchCategories()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">备件管理</h2>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<Space>
|
||||
<Select
|
||||
v-model:value="queryParams.projectId"
|
||||
placeholder="请选择项目"
|
||||
style="width: 240px"
|
||||
allow-clear
|
||||
:options="projectOptions"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="queryParams.categoryId"
|
||||
placeholder="请选择分类"
|
||||
style="width: 160px"
|
||||
allow-clear
|
||||
:options="categoryOptions"
|
||||
@change="handleSearch"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
<SearchOutlined /> 查询
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<div class="table-card">
|
||||
<div style="margin-bottom: 16px">
|
||||
<Space>
|
||||
<Button type="primary" @click="handleAdd" :disabled="!queryParams.projectId">
|
||||
<PlusOutlined /> 新建备件
|
||||
</Button>
|
||||
<Button @click="fetchLowStockSpareParts" :disabled="!queryParams.projectId">
|
||||
<ExclamationCircleOutlined /> 低库存备件
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:row-key="(record: SparePart) => record.id"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
}"
|
||||
@change="(pag: any) => handlePageChange(pag.current, pag.pageSize)"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'lowStockWarning'">
|
||||
<Tag v-if="record.lowStockWarning" color="red">
|
||||
{{ record.lowStockWarning ? '低库存' : '正常' }}
|
||||
</Tag>
|
||||
<Tag v-else color="green">正常</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleView(record)">查看</Button>
|
||||
<Button type="link" size="small" @click="handleEdit(record)">编辑</Button>
|
||||
<Button type="link" size="small" @click="handleInStock(record)">入库</Button>
|
||||
<Button type="link" size="small" @click="handleOutStock(record)">出库</Button>
|
||||
<Button type="link" size="small" danger @click="handleDelete(record)">删除</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 未选择项目提示 -->
|
||||
<a-empty v-if="!queryParams.projectId && tableData.length === 0" description="请先选择项目" />
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑模态框 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
@ok="handleSubmit"
|
||||
width="600px"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="备件编码" name="code">
|
||||
<a-input v-model:value="formState.code" placeholder="请输入备件编码" />
|
||||
</a-form-item>
|
||||
<a-form-item label="备件名称" name="name">
|
||||
<a-input v-model:value="formState.name" placeholder="请输入备件名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="分类" name="categoryId">
|
||||
<a-select
|
||||
v-model:value="formState.categoryId"
|
||||
placeholder="请选择分类"
|
||||
:options="categoryOptions"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="单位" name="unit">
|
||||
<a-input v-model:value="formState.unit" placeholder="如:个、件、套" />
|
||||
</a-form-item>
|
||||
<a-form-item label="当前库存" name="currentStock">
|
||||
<a-input-number
|
||||
v-model:value="formState.currentStock"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="安全库存" name="safeStock">
|
||||
<a-input-number
|
||||
v-model:value="formState.safeStock"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="description">
|
||||
<a-textarea v-model:value="formState.description" placeholder="请输入备注" :rows="3" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Card, Form, InputNumber, Input, Button, Space, message, Result } from 'ant-design-vue'
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
InboxOutlined,
|
||||
ExportOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import {
|
||||
getSparePartDetail,
|
||||
inStock,
|
||||
outStock,
|
||||
type SparePart,
|
||||
type InStockRequest,
|
||||
type OutStockRequest
|
||||
} from '@/api/sparepart'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const sparePart = ref<SparePart | null>(null)
|
||||
|
||||
const formRef = ref()
|
||||
const formState = reactive({
|
||||
quantity: 0,
|
||||
relatedOrderNo: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
quantity: [{ required: true, message: '请输入数量', type: 'number' }]
|
||||
}
|
||||
|
||||
const isInStock = computed(() => route.path.includes('/stock/in'))
|
||||
const operationType = computed(() => isInStock.value ? '入库' : '出库')
|
||||
const operationIcon = computed(() => isInStock.value ? InboxOutlined : ExportOutlined)
|
||||
|
||||
const sparePartId = route.query.sparePartId as string
|
||||
|
||||
// 获取备件信息
|
||||
const fetchSparePart = async () => {
|
||||
if (!sparePartId) {
|
||||
message.error('缺少备件ID')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getSparePartDetail(sparePartId)
|
||||
sparePart.value = res.data.data
|
||||
} catch {
|
||||
message.error('获取备件信息失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
submitLoading.value = true
|
||||
|
||||
if (isInStock.value) {
|
||||
const data: InStockRequest = {
|
||||
sparePartId,
|
||||
quantity: formState.quantity,
|
||||
remark: formState.remark
|
||||
}
|
||||
await inStock(data)
|
||||
message.success('入库成功')
|
||||
} else {
|
||||
const data: OutStockRequest = {
|
||||
sparePartId,
|
||||
quantity: formState.quantity,
|
||||
relatedOrderNo: formState.relatedOrderNo,
|
||||
remark: formState.remark
|
||||
}
|
||||
await outStock(data)
|
||||
message.success('出库成功')
|
||||
}
|
||||
|
||||
router.back()
|
||||
} catch (error) {
|
||||
console.error('操作失败', error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSparePart()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<Space>
|
||||
<Button @click="handleBack">
|
||||
<ArrowLeftOutlined /> 返回
|
||||
</Button>
|
||||
<h2 class="page-title">{{ operationType }}</h2>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Card v-if="sparePart">
|
||||
<!-- 备件信息 -->
|
||||
<div class="sparepart-info">
|
||||
<span class="info-label">备件信息:</span>
|
||||
<span class="info-value">
|
||||
{{ sparePart.name }} ({{ sparePart.code }})
|
||||
</span>
|
||||
<span class="info-label" style="margin-left: 24px">当前库存:</span>
|
||||
<span class="info-value">
|
||||
{{ sparePart.currentStock || 0 }} {{ sparePart.unit || '个' }}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- 操作表单 -->
|
||||
<Card :title="operationType" style="margin-top: 16px">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<Form.Item :label="operationType + '数量'" name="quantity">
|
||||
<InputNumber
|
||||
v-model:value="formState.quantity"
|
||||
:min="1"
|
||||
style="width: 200px"
|
||||
:placeholder="'请输入' + operationType + '数量'"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item v-if="!isInStock" label="关联工单号" name="relatedOrderNo">
|
||||
<Input
|
||||
v-model:value="formState.relatedOrderNo"
|
||||
placeholder="请输入关联工单号(可选)"
|
||||
style="width: 300px"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="备注" name="remark">
|
||||
<Input.TextArea
|
||||
v-model:value="formState.remark"
|
||||
placeholder="请输入备注(可选)"
|
||||
:rows="3"
|
||||
style="width: 400px"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item :wrapper-col="{ offset: 6, span: 16 }">
|
||||
<Space>
|
||||
<Button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<component :is="operationIcon" /> {{ operationType }}
|
||||
</Button>
|
||||
<Button @click="handleBack">取消</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<Card v-if="loading" style="margin-top: 16px">
|
||||
<Result title="加载中..." />
|
||||
</Card>
|
||||
|
||||
<!-- 缺少参数 -->
|
||||
<Card v-if="!sparePartId && !loading" style="margin-top: 16px">
|
||||
<Result
|
||||
status="warning"
|
||||
title="缺少参数"
|
||||
sub-title="未指定备件ID,无法进行库存操作"
|
||||
>
|
||||
<template #extra>
|
||||
<Button type="primary" @click="handleBack">返回</Button>
|
||||
</template>
|
||||
</Result>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.sparepart-info {
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #262626;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,200 +1,334 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Table, Button, Space, Input, Select, DatePicker, Tag, message } from 'ant-design-vue'
|
||||
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import { Table, Button, Space, Input, Select, DatePicker, Tag, message, ConfigProvider } from 'ant-design-vue'
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
import { SearchOutlined, ReloadOutlined, ExportOutlined } from '@ant-design/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { getAuditLogs, getAuditModules, getAuditActions, getAuditStats } from '@/api/audit'
|
||||
import type { AuditLog } from '@/api/audit'
|
||||
import {
|
||||
FilterBar,
|
||||
TableCard,
|
||||
TableToolbar,
|
||||
Pagination
|
||||
} from '@/components'
|
||||
|
||||
// 审计日志类型
|
||||
interface AuditLog {
|
||||
id: string
|
||||
time: string
|
||||
operator: string
|
||||
type: 'PERMISSION' | 'ROLE' | 'PROJECT'
|
||||
content: string
|
||||
target: string
|
||||
ip: string
|
||||
}
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{ title: '时间', dataIndex: 'time', key: 'time', width: 180 },
|
||||
{ title: '操作用户', dataIndex: 'operator', key: 'operator', width: 120 },
|
||||
{ title: '操作类型', dataIndex: 'type', key: 'type', width: 100 },
|
||||
{ title: '操作内容', dataIndex: 'content', key: 'content', ellipsis: true },
|
||||
{ title: '目标对象', dataIndex: 'target', key: 'target', width: 150 },
|
||||
{ title: 'IP地址', dataIndex: 'ip', key: 'ip', width: 140 }
|
||||
{ title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 170 },
|
||||
{ title: '操作用户', dataIndex: 'username', key: 'username', width: 100 },
|
||||
{ title: '功能模块', dataIndex: 'module', key: 'module', width: 100 },
|
||||
{ title: '操作类型', dataIndex: 'action', key: 'action', width: 90 },
|
||||
{ title: '操作描述', dataIndex: 'operation', key: 'operation', width: 200 },
|
||||
{ title: 'IP地址', dataIndex: 'ipAddress', key: 'ipAddress', width: 130 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 80 },
|
||||
{ title: '耗时', dataIndex: 'executionTimeMs', key: 'executionTimeMs', width: 80 }
|
||||
]
|
||||
|
||||
// 数据
|
||||
const logs = ref<AuditLog[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 筛选
|
||||
const filters = ref({
|
||||
type: undefined as string | undefined,
|
||||
dateRange: [] as [dayjs.Dayjs, dayjs.Dayjs] | null,
|
||||
operator: ''
|
||||
const pagination = ref({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 操作类型选项
|
||||
const typeOptions = [
|
||||
{ value: 'PERMISSION', label: '权限变更' },
|
||||
{ value: 'ROLE', label: '角色分配' },
|
||||
{ value: 'PROJECT', label: '项目参与' }
|
||||
]
|
||||
const stats = ref({
|
||||
total: 0,
|
||||
retentionDays: 30
|
||||
})
|
||||
|
||||
// 模拟数据
|
||||
const mockLogs: AuditLog[] = [
|
||||
{
|
||||
id: '1',
|
||||
time: '2026-03-21 10:30:25',
|
||||
operator: 'admin',
|
||||
type: 'PERMISSION',
|
||||
content: '修改用户「张三」的项目权限,添加「数据导出」权限',
|
||||
target: '用户:张三',
|
||||
ip: '192.168.1.100'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
time: '2026-03-21 09:15:42',
|
||||
operator: 'admin',
|
||||
type: 'ROLE',
|
||||
content: '为用户「李四」分配「项目经理」角色',
|
||||
target: '用户:李四',
|
||||
ip: '192.168.1.100'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
time: '2026-03-20 16:45:33',
|
||||
operator: 'manager',
|
||||
type: 'PROJECT',
|
||||
content: '将「王五」从「智慧社区项目」中移除',
|
||||
target: '智慧社区项目',
|
||||
ip: '192.168.1.105'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
time: '2026-03-20 14:20:18',
|
||||
operator: 'admin',
|
||||
type: 'PERMISSION',
|
||||
content: '撤销用户「赵六」的「系统管理」权限',
|
||||
target: '用户:赵六',
|
||||
ip: '192.168.1.100'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
time: '2026-03-19 11:05:56',
|
||||
operator: 'manager',
|
||||
type: 'ROLE',
|
||||
content: '更新角色「审计员」的权限配置',
|
||||
target: '角色:审计员',
|
||||
ip: '192.168.1.105'
|
||||
}
|
||||
]
|
||||
const moduleOptions = ref<{ value: string; label: string }[]>([])
|
||||
const actionOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 获取操作类型标签
|
||||
const getTypeColor = (type: string) => {
|
||||
const map: Record<string, string> = {
|
||||
PERMISSION: 'blue',
|
||||
ROLE: 'green',
|
||||
PROJECT: 'orange'
|
||||
const filters = ref({
|
||||
module: undefined as string | undefined,
|
||||
action: undefined as string | undefined,
|
||||
username: '',
|
||||
dateRange: null as [Dayjs, Dayjs] | null
|
||||
})
|
||||
|
||||
const loadModules = async () => {
|
||||
try {
|
||||
const res = await getAuditModules()
|
||||
moduleOptions.value = res.data.data || []
|
||||
} catch {
|
||||
moduleOptions.value = [
|
||||
{ value: 'USER', label: '用户管理' },
|
||||
{ value: 'ROLE', label: '角色管理' },
|
||||
{ value: 'PROJECT', label: '项目管理' },
|
||||
{ value: 'AUTH', label: '登录认证' }
|
||||
]
|
||||
}
|
||||
return map[type] || 'default'
|
||||
}
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
const map: Record<string, string> = {
|
||||
PERMISSION: '权限变更',
|
||||
ROLE: '角色分配',
|
||||
PROJECT: '项目参与'
|
||||
const loadActions = async () => {
|
||||
try {
|
||||
const res = await getAuditActions()
|
||||
actionOptions.value = res.data.data || []
|
||||
} catch {
|
||||
actionOptions.value = [
|
||||
{ value: 'CREATE', label: '创建' },
|
||||
{ value: 'UPDATE', label: '修改' },
|
||||
{ value: 'DELETE', label: '删除' },
|
||||
{ value: 'QUERY', label: '查询' },
|
||||
{ value: 'LOGIN', label: '登录' },
|
||||
{ value: 'LOGOUT', label: '登出' }
|
||||
]
|
||||
}
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = () => {
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const res = await getAuditStats()
|
||||
stats.value = res.data.data || { total: 0, retentionDays: 30 }
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
// 模拟API延迟
|
||||
setTimeout(() => {
|
||||
logs.value = mockLogs.filter((log) => {
|
||||
// 按类型筛选
|
||||
if (filters.value.type && log.type !== filters.value.type) return false
|
||||
// 按用户筛选
|
||||
if (filters.value.operator && !log.operator.includes(filters.value.operator)) return false
|
||||
// 按日期范围筛选
|
||||
if (filters.value.dateRange && filters.value.dateRange.length === 2) {
|
||||
const logDate = dayjs(log.time).startOf('day')
|
||||
const [start, end] = filters.value.dateRange
|
||||
if (logDate.isBefore(start, 'day') || logDate.isAfter(end, 'day')) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.value.current - 1,
|
||||
size: pagination.value.pageSize
|
||||
}
|
||||
|
||||
if (filters.value.module) {
|
||||
params.module = filters.value.module
|
||||
}
|
||||
if (filters.value.action) {
|
||||
params.action = filters.value.action
|
||||
}
|
||||
if (filters.value.username) {
|
||||
params.username = filters.value.username
|
||||
}
|
||||
if (filters.value.dateRange && filters.value.dateRange.length === 2) {
|
||||
params.startDate = filters.value.dateRange[0].format('YYYY-MM-DDTHH:mm:ss')
|
||||
params.endDate = filters.value.dateRange[1].format('YYYY-MM-DDTHH:mm:ss')
|
||||
}
|
||||
|
||||
const res = await getAuditLogs(params)
|
||||
const data = res.data.data
|
||||
logs.value = data.content || []
|
||||
pagination.value.total = data.totalElements || 0
|
||||
} catch (error) {
|
||||
message.error('获取审计日志失败')
|
||||
logs.value = []
|
||||
pagination.value.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
filters.value = {
|
||||
type: undefined,
|
||||
dateRange: null,
|
||||
operator: ''
|
||||
}
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.value.current = pag.current
|
||||
pagination.value.pageSize = pag.pageSize
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
const handleSearch = () => {
|
||||
pagination.value.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
filters.value = {
|
||||
module: undefined,
|
||||
action: undefined,
|
||||
username: '',
|
||||
dateRange: null
|
||||
}
|
||||
pagination.value.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const getModuleLabel = (module: string) => {
|
||||
const map: Record<string, string> = {
|
||||
USER: '用户管理',
|
||||
ROLE: '角色管理',
|
||||
PERMISSION: '权限管理',
|
||||
PROJECT: '项目管理',
|
||||
AUTH: '登录认证'
|
||||
}
|
||||
return map[module] || module
|
||||
}
|
||||
|
||||
const getActionLabel = (action: string) => {
|
||||
const map: Record<string, string> = {
|
||||
CREATE: '创建',
|
||||
UPDATE: '修改',
|
||||
DELETE: '删除',
|
||||
QUERY: '查询',
|
||||
VIEW: '查看',
|
||||
LOGIN: '登录',
|
||||
LOGOUT: '登出',
|
||||
EXPORT: '导出',
|
||||
IMPORT: '导入',
|
||||
ASSIGN: '分配',
|
||||
REVOKE: '撤销'
|
||||
}
|
||||
return map[action] || action
|
||||
}
|
||||
|
||||
const getActionColor = (action: string) => {
|
||||
const map: Record<string, string> = {
|
||||
CREATE: 'green',
|
||||
UPDATE: 'blue',
|
||||
DELETE: 'red',
|
||||
QUERY: 'default',
|
||||
VIEW: 'cyan',
|
||||
LOGIN: 'cyan',
|
||||
LOGOUT: 'default',
|
||||
EXPORT: 'purple',
|
||||
IMPORT: 'orange',
|
||||
ASSIGN: 'blue',
|
||||
REVOKE: 'orange'
|
||||
}
|
||||
return map[action] || 'default'
|
||||
}
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
return status === 'SUCCESS' ? '成功' : '失败'
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
return status === 'SUCCESS' ? 'success' : 'error'
|
||||
}
|
||||
|
||||
const formatDuration = (ms?: number) => {
|
||||
if (ms === undefined || ms === null) return '-'
|
||||
if (ms < 1000) return `${ms}ms`
|
||||
return `${(ms / 1000).toFixed(2)}s`
|
||||
}
|
||||
|
||||
const disabledDate = (current: Dayjs) => {
|
||||
const thirtyDaysAgo = dayjs().subtract(30, 'day').startOf('day')
|
||||
return current && (current < thirtyDaysAgo || current > dayjs().endOf('day'))
|
||||
}
|
||||
|
||||
const handleExport = () => {
|
||||
message.info('导出功能开发中')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadModules()
|
||||
loadActions()
|
||||
loadStats()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigProvider :locale="zhCN">
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">操作审计日志</h2>
|
||||
<div class="page-header-actions">
|
||||
<span class="subtitle">保留最近 {{ stats.retentionDays }} 天的操作记录,共 {{ stats.total }} 条</span>
|
||||
<Button type="primary" @click="handleExport">
|
||||
<ExportOutlined /> 导出Excel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<FilterBar>
|
||||
<Space wrap>
|
||||
<Select
|
||||
v-model:value="filters.type"
|
||||
placeholder="操作类型"
|
||||
:options="typeOptions"
|
||||
v-model:value="filters.module"
|
||||
placeholder="功能模块"
|
||||
:options="moduleOptions"
|
||||
allow-clear
|
||||
style="width: 140px"
|
||||
/>
|
||||
<DatePicker.RangePicker v-model:value="filters.dateRange" style="width: 260px" />
|
||||
<Input
|
||||
v-model:value="filters.operator"
|
||||
placeholder="操作用户"
|
||||
<Select
|
||||
v-model:value="filters.action"
|
||||
placeholder="操作类型"
|
||||
:options="actionOptions"
|
||||
allow-clear
|
||||
style="width: 140px"
|
||||
/>
|
||||
<Button type="primary" @click="loadData">
|
||||
<DatePicker.RangePicker
|
||||
v-model:value="filters.dateRange"
|
||||
style="width: 320px"
|
||||
:disabled-date="disabledDate"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
:placeholder="['开始时间', '结束时间']"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="filters.username"
|
||||
placeholder="操作用户"
|
||||
style="width: 140px"
|
||||
allow-clear
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
<SearchOutlined /> 查询
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
<!-- 表格 -->
|
||||
<div class="table-card">
|
||||
<Table
|
||||
<TableCard>
|
||||
<TableToolbar @refresh="loadData" />
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="logs"
|
||||
:loading="loading"
|
||||
:row-key="(record: AuditLog) => record.id"
|
||||
:pagination="{ pageSize: 10, showSizeChanger: true, showTotal: (total: number) => `共 ${total} 条` }"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'type'">
|
||||
<Tag :color="getTypeColor(record.type)">
|
||||
{{ getTypeLabel(record.type) }}
|
||||
<template v-if="column.key === 'module'">
|
||||
{{ getModuleLabel(record.module) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Tag :color="getActionColor(record.action)">
|
||||
{{ getActionLabel(record.action) }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<Tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusLabel(record.status) }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'executionTimeMs'">
|
||||
{{ formatDuration(record.executionTimeMs) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'createdAt'">
|
||||
{{ dayjs(record.createdAt).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
</a-table>
|
||||
|
||||
<Pagination
|
||||
v-model:current="pagination.current"
|
||||
v-model:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@change="handleTableChange"
|
||||
/>
|
||||
</TableCard>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue