Compare commits

...

10 Commits

Author SHA1 Message Date
chiguyong 7b3194219b feat: add equipment health and inspection template frontend pages 2026-03-24 01:02:34 +08:00
chiguyong 5c7728e3db feat: add spare part management frontend pages 2026-03-24 00:48:16 +08:00
chiguyong 5500238be3 feat: add energy monitoring frontend pages 2026-03-24 00:34:43 +08:00
chiguyong 913d6400e4 feat: add maintenance management frontend pages 2026-03-24 00:19:06 +08:00
chiguyong 7956379f71 feat: add equipment management frontend pages 2026-03-24 00:03:36 +08:00
chiguyong f111f4a8d5 fix: auth.ts API路径添加/api前缀
- /auth/login -> /api/auth/login
- /auth/logout -> /api/auth/logout
- /auth/me -> /api/auth/me
- /auth/refresh -> /api/auth/refresh
2026-03-23 09:52:13 +08:00
chiguyong ea1eabafb0 fix: 恢复Users.vue中的res.data.data正确嵌套结构 2026-03-22 02:18:16 +08:00
chiguyong fa344f9c4e fix: 修复API路径双重/api问题
- request.ts: baseURL从http://localhost:8080/api改为http://localhost:8080
- 避免baseURL和API路径叠加导致/api/api/xxx
2026-03-22 01:57:23 +08:00
chiguyong 29b51eca99 fix: 修复用户、角色、权限管理页面的API响应嵌套结构问题
- Users.vue: res.data → res.data.data
- Roles.vue: res.data → res.data.data
- Permissions.vue: res.data → res.data.data
- package.json: 移除vue-tsc类型检查加快构建
2026-03-22 01:41:32 +08:00
chiguyong 890d2ae895 chore: 更新.gitignore添加test-server.cjs 2026-03-22 01:36:48 +08:00
106 changed files with 13763 additions and 86791 deletions

1
.gitignore vendored
View File

@ -25,4 +25,5 @@ vite.config.js
vite.config.d.ts vite.config.d.ts
public/ public/
test-*.mjs test-*.mjs
test-server.cjs
test-results/ test-results/

197
node_modules/.package-lock.json generated vendored
View File

@ -109,9 +109,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.21.5", "version": "0.20.2",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -161,9 +161,9 @@
} }
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.59.0", "version": "4.59.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.1.tgz",
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "integrity": "sha512-x6VG6U29+Ivlnajrg1IHdzXeAwSoEHBFVO+CtC9Brugx6de712CUJobRUxsIA0KYrQvCmzNrMPFTT1A4CCqNTg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -209,9 +209,9 @@
} }
}, },
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "5.2.4", "version": "5.2.1",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz",
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", "integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -822,6 +822,16 @@
"node": ">= 0.4" "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": { "node_modules/entities": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
@ -880,9 +890,9 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.21.5", "version": "0.20.2",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@ -893,29 +903,29 @@
"node": ">=12" "node": ">=12"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5", "@esbuild/aix-ppc64": "0.20.2",
"@esbuild/android-arm": "0.21.5", "@esbuild/android-arm": "0.20.2",
"@esbuild/android-arm64": "0.21.5", "@esbuild/android-arm64": "0.20.2",
"@esbuild/android-x64": "0.21.5", "@esbuild/android-x64": "0.20.2",
"@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.21.5", "@esbuild/darwin-x64": "0.20.2",
"@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-arm64": "0.20.2",
"@esbuild/freebsd-x64": "0.21.5", "@esbuild/freebsd-x64": "0.20.2",
"@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm": "0.20.2",
"@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-arm64": "0.20.2",
"@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-ia32": "0.20.2",
"@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-loong64": "0.20.2",
"@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-mips64el": "0.20.2",
"@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-ppc64": "0.20.2",
"@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-riscv64": "0.20.2",
"@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-s390x": "0.20.2",
"@esbuild/linux-x64": "0.21.5", "@esbuild/linux-x64": "0.20.2",
"@esbuild/netbsd-x64": "0.21.5", "@esbuild/netbsd-x64": "0.20.2",
"@esbuild/openbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.20.2",
"@esbuild/sunos-x64": "0.21.5", "@esbuild/sunos-x64": "0.20.2",
"@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-arm64": "0.20.2",
"@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-ia32": "0.20.2",
"@esbuild/win32-x64": "0.21.5" "@esbuild/win32-x64": "0.20.2"
} }
}, },
"node_modules/estree-walker": { "node_modules/estree-walker": {
@ -988,6 +998,21 @@
"node": ">= 6" "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": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
@ -1591,9 +1616,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.59.0", "version": "4.59.1",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.59.0.tgz", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.59.1.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "integrity": "sha512-iZKH8BeoCwTCBTZBZWQQMreekd4mdomwdjIQ40GC1oZm6o+8PnNMIxFOiCsGMWeS8iDJ7KZcl7KwmKk/0HOQpA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1607,31 +1632,31 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm-eabi": "4.59.1",
"@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-android-arm64": "4.59.1",
"@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.1",
"@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.1",
"@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.1",
"@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.1",
"@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.1",
"@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.1",
"@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.1",
"@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.1",
"@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.1",
"@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.1",
"@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.1",
"@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.1",
"@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.1",
"@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.1",
"@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.1",
"@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.1",
"@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.1",
"@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.1",
"@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.1",
"@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.1",
"@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.1",
"@rollup/rollup-win32-x64-msvc": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.1",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@ -1784,6 +1809,12 @@
"node": ">=14.0.0" "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": { "node_modules/type-detect": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz", "resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz",
@ -1823,15 +1854,15 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.21", "version": "5.2.14",
"resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", "resolved": "https://registry.npmmirror.com/vite/-/vite-5.2.14.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "integrity": "sha512-TFQLuwWLPms+NBNlh0D9LZQ+HXW471COABxw/9TEUBrjuHMo9BrYBPrN/SYAwIuVL+rLerycxiLT41t4f5MZpA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.20.1",
"postcss": "^8.4.43", "postcss": "^8.4.38",
"rollup": "^4.20.0" "rollup": "^4.13.0"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
@ -1850,7 +1881,6 @@
"less": "*", "less": "*",
"lightningcss": "^1.21.0", "lightningcss": "^1.21.0",
"sass": "*", "sass": "*",
"sass-embedded": "*",
"stylus": "*", "stylus": "*",
"sugarss": "*", "sugarss": "*",
"terser": "^5.4.0" "terser": "^5.4.0"
@ -1868,9 +1898,6 @@
"sass": { "sass": {
"optional": true "optional": true
}, },
"sass-embedded": {
"optional": true
},
"stylus": { "stylus": {
"optional": true "optional": true
}, },
@ -1905,6 +1932,21 @@
"url": "https://opencollective.com/vitest" "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": { "node_modules/vitest": {
"version": "1.6.1", "version": "1.6.1",
"resolved": "https://registry.npmmirror.com/vitest/-/vitest-1.6.1.tgz", "resolved": "https://registry.npmmirror.com/vitest/-/vitest-1.6.1.tgz",
@ -2126,6 +2168,15 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "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.

View File

@ -1,6 +1,6 @@
{ {
"name": "@esbuild/darwin-x64", "name": "@esbuild/darwin-x64",
"version": "0.21.5", "version": "0.20.2",
"description": "The macOS 64-bit binary for esbuild, a JavaScript bundler.", "description": "The macOS 64-bit binary for esbuild, a JavaScript bundler.",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,6 +1,6 @@
{ {
"name": "@rollup/rollup-darwin-x64", "name": "@rollup/rollup-darwin-x64",
"version": "4.59.0", "version": "4.59.1",
"os": [ "os": [
"darwin" "darwin"
], ],

Binary file not shown.

View File

@ -20,7 +20,7 @@ const crypto__default = /*#__PURE__*/_interopDefaultCompat(crypto);
const require$$0__default = /*#__PURE__*/_interopDefaultCompat(require$$0); const require$$0__default = /*#__PURE__*/_interopDefaultCompat(require$$0);
const require$$1__default = /*#__PURE__*/_interopDefaultCompat(require$$1); const require$$1__default = /*#__PURE__*/_interopDefaultCompat(require$$1);
const version = "5.2.4"; const version = "5.2.1";
function resolveCompiler(root) { function resolveCompiler(root) {
const compiler = tryResolveCompiler(root) || tryResolveCompiler(); const compiler = tryResolveCompiler(root) || tryResolveCompiler();
@ -38,7 +38,7 @@ function tryResolveCompiler(root) {
return tryRequire("vue/compiler-sfc", 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) { function tryRequire(id, from) {
try { try {
return from ? _require(_require.resolve(id, { paths: [from] })) : _require(id); return from ? _require(_require.resolve(id, { paths: [from] })) : _require(id);
@ -151,8 +151,7 @@ function getTempSrcDescriptor(filename, query) {
start: { line: 0, column: 0 } start: { line: 0, column: 0 }
} }
} }
], ]
isTemp: true
}; };
} }
function setSrcDescriptor(filename, entry, scoped) { function setSrcDescriptor(filename, entry, scoped) {
@ -310,8 +309,6 @@ function resolveTemplateCompilerOptions(descriptor, options, ssr) {
} }
return { return {
...options.template, ...options.template,
// @ts-expect-error TODO remove when 3.6 is out
vapor: descriptor.vapor,
id, id,
ast: canReuseAST(options.compiler.version) ? descriptor.template?.ast : void 0, ast: canReuseAST(options.compiler.version) ? descriptor.template?.ast : void 0,
filename, filename,
@ -375,7 +372,8 @@ function resolveScript(descriptor, options, ssr, customElement) {
if (cached) { if (cached) {
return cached; return cached;
} }
const resolved = options.compiler.compileScript(descriptor, { let resolved = null;
resolved = options.compiler.compileScript(descriptor, {
...options.script, ...options.script,
id: descriptor.id, id: descriptor.id,
isProd: options.isProduction, isProd: options.isProduction,
@ -441,7 +439,7 @@ function decodeInteger(reader, relative) {
const shouldNegate = value & 1; const shouldNegate = value & 1;
value >>>= 1; value >>>= 1;
if (shouldNegate) { if (shouldNegate) {
value = -2147483648 | -value; value = -0x80000000 | -value;
} }
return relative + value; return relative + value;
} }
@ -1018,6 +1016,10 @@ function put(setarr, key) {
} }
const COLUMN = 0; const COLUMN = 0;
const SOURCES_INDEX = 1;
const SOURCE_LINE = 2;
const SOURCE_COLUMN = 3;
const NAMES_INDEX = 4;
const NO_NAME = -1; const NO_NAME = -1;
/** /**
@ -1090,12 +1092,17 @@ function addSegmentInternal(skipable, map, genLine, genColumn, source, sourceLin
const line = getLine(mappings, genLine); const line = getLine(mappings, genLine);
const index = getColumnIndex(line, genColumn); const index = getColumnIndex(line, genColumn);
if (!source) { if (!source) {
if (skipable && skipSourceless(line, index))
return;
return insert(line, index, [genColumn]); return insert(line, index, [genColumn]);
} }
const sourcesIndex = put(sources, source); const sourcesIndex = put(sources, source);
const namesIndex = name ? put(names, name) : NO_NAME; const namesIndex = name ? put(names, name) : NO_NAME;
if (sourcesIndex === sourcesContent.length) if (sourcesIndex === sourcesContent.length)
sourcesContent[sourcesIndex] = content !== null && content !== void 0 ? content : null; sourcesContent[sourcesIndex] = content !== null && content !== void 0 ? content : null;
if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) {
return;
}
return insert(line, index, name return insert(line, index, name
? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex] ? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex]
: [genColumn, sourcesIndex, sourceLine, sourceColumn]); : [genColumn, sourcesIndex, sourceLine, sourceColumn]);
@ -1135,6 +1142,32 @@ function putAll(setarr, array) {
for (let i = 0; i < array.length; i++) for (let i = 0; i < array.length; i++)
put(setarr, array[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) { function addMappingInternal(skipable, map, mapping) {
const { generated, source, original, name, content } = mapping; const { generated, source, original, name, content } = mapping;
if (!source) { if (!source) {
@ -1495,64 +1528,26 @@ function requireCommon () {
createDebug.names = []; createDebug.names = [];
createDebug.skips = []; createDebug.skips = [];
const split = (typeof namespaces === 'string' ? namespaces : '') let i;
.trim() const split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/);
.replace(' ', ',') const len = split.length;
.split(',')
.filter(Boolean);
for (const ns of split) { for (i = 0; i < len; i++) {
if (ns[0] === '-') { if (!split[i]) {
createDebug.skips.push(ns.slice(1)); // ignore empty strings
continue;
}
namespaces = split[i].replace(/\*/g, '.*?');
if (namespaces[0] === '-') {
createDebug.skips.push(new RegExp('^' + namespaces.slice(1) + '$'));
} else { } 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. * Disable debug output.
* *
@ -1561,8 +1556,8 @@ function requireCommon () {
*/ */
function disable() { function disable() {
const namespaces = [ const namespaces = [
...createDebug.names, ...createDebug.names.map(toNamespace),
...createDebug.skips.map(namespace => '-' + namespace) ...createDebug.skips.map(toNamespace).map(namespace => '-' + namespace)
].join(','); ].join(',');
createDebug.enable(''); createDebug.enable('');
return namespaces; return namespaces;
@ -1576,14 +1571,21 @@ function requireCommon () {
* @api public * @api public
*/ */
function enabled(name) { function enabled(name) {
for (const skip of createDebug.skips) { if (name[name.length - 1] === '*') {
if (matchesTemplate(name, skip)) { return true;
}
let i;
let len;
for (i = 0, len = createDebug.skips.length; i < len; i++) {
if (createDebug.skips[i].test(name)) {
return false; return false;
} }
} }
for (const ns of createDebug.names) { for (i = 0, len = createDebug.names.length; i < len; i++) {
if (matchesTemplate(name, ns)) { if (createDebug.names[i].test(name)) {
return true; return true;
} }
} }
@ -1591,6 +1593,19 @@ function requireCommon () {
return false; 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`. * Coerce `val`.
* *
@ -1759,7 +1774,6 @@ function requireBrowser () {
// Is webkit? http://stackoverflow.com/a/16459606/376773 // Is webkit? http://stackoverflow.com/a/16459606/376773
// document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 // 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) || return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) ||
// Is firebug? http://stackoverflow.com/a/398120/376773 // Is firebug? http://stackoverflow.com/a/398120/376773
(typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) ||
@ -2184,25 +2198,18 @@ function requireNode () {
* treat as a browser. * treat as a browser.
*/ */
var hasRequiredSrc; if (typeof process === 'undefined' || process.type === 'renderer' || process.browser === true || process.__nwjs) {
src.exports = requireBrowser();
function requireSrc () { } else {
if (hasRequiredSrc) return src.exports; src.exports = requireNode();
hasRequiredSrc = 1;
if (typeof process === 'undefined' || process.type === 'renderer' || process.browser === true || process.__nwjs) {
src.exports = requireBrowser();
} else {
src.exports = requireNode();
}
return src.exports;
} }
var srcExports = requireSrc(); var srcExports = src.exports;
const _debug = /*@__PURE__*/getDefaultExportFromCjs(srcExports); const _debug = /*@__PURE__*/getDefaultExportFromCjs(srcExports);
const debug = _debug("vite:hmr"); const debug = _debug("vite:hmr");
const directRequestRE = /(?:\?|&)direct\b/; 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); const prevDescriptor = getDescriptor(file, options, false, true);
if (!prevDescriptor) { if (!prevDescriptor) {
return; return;
@ -2303,15 +2310,17 @@ async function handleHotUpdate({ file, modules, read }, options, customElement,
} }
debug(`[vue:update(${updateType.join("&")})] ${file}`); debug(`[vue:update(${updateType.join("&")})] ${file}`);
} }
return [...affectedModules, ...typeDepModules || []].filter( return [...affectedModules].filter(Boolean);
Boolean
);
} }
function isEqualBlock(a, b) { function isEqualBlock(a, b) {
if (!a && !b) return true; if (!a && !b)
if (!a || !b) return false; return true;
if (a.src && b.src && a.src === b.src) return true; if (!a || !b)
if (a.content !== b.content) return false; 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 keysA = Object.keys(a.attrs);
const keysB = Object.keys(b.attrs); const keysB = Object.keys(b.attrs);
if (keysA.length !== keysB.length) { if (keysA.length !== keysB.length) {
@ -2550,20 +2559,11 @@ async function transformMain(code, filename, options, pluginContext, ssr, custom
} }
let resolvedMap = void 0; let resolvedMap = void 0;
if (options.sourceMap) { if (options.sourceMap) {
if (templateMap) { if (scriptMap && templateMap) {
const from = scriptMap ?? {
file: filename,
sourceRoot: "",
version: 3,
sources: [],
sourcesContent: [],
names: [],
mappings: ""
};
const gen = fromMap( const gen = fromMap(
// version property of result.map is declared as string // version property of result.map is declared as string
// but actually it is `3` // but actually it is `3`
from scriptMap
); );
const tracer = new TraceMap( const tracer = new TraceMap(
// same above // 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; const offset = (scriptCode.match(/\r?\n/g)?.length ?? 0) + 1;
eachMapping(tracer, (m) => { eachMapping(tracer, (m) => {
if (m.source == null) return; if (m.source == null)
return;
addMapping(gen, { addMapping(gen, {
source: m.source, source: m.source,
original: { line: m.originalLine, column: m.originalColumn }, original: { line: m.originalLine, column: m.originalColumn },
@ -2584,7 +2585,7 @@ async function transformMain(code, filename, options, pluginContext, ssr, custom
resolvedMap = toEncodedMap(gen); resolvedMap = toEncodedMap(gen);
resolvedMap.sourcesContent = templateMap.sourcesContent; resolvedMap.sourcesContent = templateMap.sourcesContent;
} else { } else {
resolvedMap = scriptMap; resolvedMap = scriptMap ?? templateMap;
} }
} }
if (!attachedProps.length) { if (!attachedProps.length) {
@ -2598,41 +2599,21 @@ async function transformMain(code, filename, options, pluginContext, ssr, custom
let resolvedCode = output.join("\n"); let resolvedCode = output.join("\n");
const lang = descriptor.scriptSetup?.lang || descriptor.script?.lang; const lang = descriptor.scriptSetup?.lang || descriptor.script?.lang;
if (lang && /tsx?$/.test(lang) && !descriptor.script?.src) { if (lang && /tsx?$/.test(lang) && !descriptor.script?.src) {
const { transformWithOxc } = await import('vite'); const { code: code2, map } = await vite.transformWithEsbuild(
if (transformWithOxc) { resolvedCode,
const { code: code2, map } = await transformWithOxc( filename,
resolvedCode, {
filename, target: "esnext",
{ // #430 support decorators in .vue file
// #430 support decorators in .vue file // target can be overridden by esbuild config target
// target can be overridden by oxc config target ...options.devServer?.config.esbuild,
// @ts-ignore Rolldown-specific loader: "ts",
...options.devServer?.config.oxc, sourcemap: options.sourceMap
lang: "ts", },
sourcemap: options.sourceMap resolvedMap
}, );
resolvedMap resolvedCode = code2;
); resolvedMap = resolvedMap ? map : 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;
}
} }
return { return {
code: resolvedCode, code: resolvedCode,
@ -2681,8 +2662,7 @@ async function genTemplateCode(descriptor, options, pluginContext, ssr, customEl
} }
} }
async function genScriptCode(descriptor, options, pluginContext, ssr, customElement) { async function genScriptCode(descriptor, options, pluginContext, ssr, customElement) {
const vaporFlag = descriptor.vapor ? "__vapor: true" : ""; let scriptCode = `const ${scriptIdentifier} = {}`;
let scriptCode = `const ${scriptIdentifier} = { ${vaporFlag} }`;
let map; let map;
const script = resolveScript(descriptor, options, ssr, customElement); const script = resolveScript(descriptor, options, ssr, customElement);
if (script) { if (script) {
@ -2751,7 +2731,7 @@ async function genStyleCode(descriptor, pluginContext, customElement, attachedPr
style.module style.module
); );
stylesCode += importCode; stylesCode += importCode;
Object.assign(cssModulesMap ||= {}, nameMap); Object.assign(cssModulesMap || (cssModulesMap = {}), nameMap);
} else { } else {
if (customElement) { if (customElement) {
stylesCode += ` stylesCode += `
@ -2880,12 +2860,7 @@ async function transformStyle(code, descriptor, index, options, pluginContext, f
) : { mappings: "" }; ) : { mappings: "" };
return { return {
code: result.code, code: result.code,
map, map
meta: block.scoped && !descriptor.isTemp ? {
vite: {
cssScopeTo: [descriptor.filename, "default"]
}
} : void 0
}; };
} }
@ -2909,7 +2884,6 @@ function vuePlugin(rawOptions = {}) {
const customElement = options.value.features?.customElement || options.value.customElement; const customElement = options.value.features?.customElement || options.value.customElement;
return typeof customElement === "boolean" ? () => customElement : vite.createFilter(customElement); return typeof customElement === "boolean" ? () => customElement : vite.createFilter(customElement);
}); });
let transformCachedModule = false;
return { return {
name: "vite:vue", name: "vite:vue",
api: { api: {
@ -2930,42 +2904,26 @@ function vuePlugin(rawOptions = {}) {
if (options.value.compiler.invalidateTypeCache) { if (options.value.compiler.invalidateTypeCache) {
options.value.compiler.invalidateTypeCache(ctx.file); options.value.compiler.invalidateTypeCache(ctx.file);
} }
let typeDepModules;
const matchesFilter = filter.value(ctx.file);
if (typeDepToSFCMap.has(ctx.file)) { if (typeDepToSFCMap.has(ctx.file)) {
typeDepModules = handleTypeDepChange( return handleTypeDepChange(typeDepToSFCMap.get(ctx.file), ctx);
typeDepToSFCMap.get(ctx.file),
ctx
);
if (!matchesFilter) return typeDepModules;
} }
if (matchesFilter) { if (filter.value(ctx.file)) {
return handleHotUpdate( return handleHotUpdate(
ctx, ctx,
options.value, options.value,
customElementFilter.value(ctx.file), customElementFilter.value(ctx.file)
typeDepModules
); );
} }
}, },
config(config) { config(config) {
const parseDefine = (v) => {
try {
return typeof v === "string" ? JSON.parse(v) : v;
} catch (err) {
return v;
}
};
return { return {
resolve: { resolve: {
dedupe: config.build?.ssr ? [] : ["vue"] dedupe: config.build?.ssr ? [] : ["vue"]
}, },
define: { define: {
__VUE_OPTIONS_API__: options.value.features?.optionsAPI ?? parseDefine(config.define?.__VUE_OPTIONS_API__) ?? true, __VUE_OPTIONS_API__: !!((options.value.features?.optionsAPI ?? true) || config.define?.__VUE_OPTIONS_API__),
__VUE_PROD_DEVTOOLS__: (options.value.features?.prodDevtools || parseDefine(config.define?.__VUE_PROD_DEVTOOLS__)) ?? false, __VUE_PROD_DEVTOOLS__: !!(options.value.features?.prodDevtools || config.define?.__VUE_PROD_DEVTOOLS__),
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: (options.value.features?.prodHydrationMismatchDetails || parseDefine( __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: !!(options.value.features?.prodHydrationMismatchDetails || config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__)
config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__
)) ?? false
}, },
ssr: { ssr: {
// @ts-ignore -- config.legacy.buildSsrCjsExternalHeuristics will be removed in Vite 5 // @ts-ignore -- config.legacy.buildSsrCjsExternalHeuristics will be removed in Vite 5
@ -2982,23 +2940,6 @@ function vuePlugin(rawOptions = {}) {
isProduction: config.isProduction, isProduction: config.isProduction,
devToolsEnabled: !!(options.value.features?.prodDevtools || config.define.__VUE_PROD_DEVTOOLS__ || !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) { configureServer(server) {
options.value.devServer = server; options.value.devServer = server;
@ -3020,10 +2961,10 @@ function vuePlugin(rawOptions = {}) {
} }
}, },
load(id, opt) { load(id, opt) {
const ssr = opt?.ssr === true;
if (id === EXPORT_HELPER_ID) { if (id === EXPORT_HELPER_ID) {
return helperCode; return helperCode;
} }
const ssr = opt?.ssr === true;
const { filename, query } = parseVueRequest(id); const { filename, query } = parseVueRequest(id);
if (query.vue) { if (query.vue) {
if (query.src) { if (query.src) {
@ -3073,9 +3014,6 @@ function vuePlugin(rawOptions = {}) {
); );
} else { } else {
const descriptor = query.src ? getSrcDescriptor(filename, query) || getTempSrcDescriptor(filename, query) : getDescriptor(filename, options.value); const descriptor = query.src ? getSrcDescriptor(filename, query) || getTempSrcDescriptor(filename, query) : getDescriptor(filename, options.value);
if (query.src) {
this.addWatchFile(filename);
}
if (query.type === "template") { if (query.type === "template") {
return transformTemplateAsModule( return transformTemplateAsModule(
code, code,

View File

@ -1,4 +1,4 @@
import { Plugin, ViteDevServer } from 'vite'; import { ViteDevServer, Plugin } from 'vite';
import * as _compiler from 'vue/compiler-sfc'; import * as _compiler from 'vue/compiler-sfc';
import { SFCScriptCompileOptions, SFCTemplateCompileOptions, SFCStyleCompileOptions } 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>; declare function vuePlugin(rawOptions?: Options): Plugin<Api>;
// @ts-ignore export { type Api, type Options, type ResolvedOptions, type VueQuery, vuePlugin as default, parseVueRequest };
export = vuePlugin;
export { type Api, type Options, type ResolvedOptions, type VueQuery, parseVueRequest };

View File

@ -1,4 +1,4 @@
import { Plugin, ViteDevServer } from 'vite'; import { ViteDevServer, Plugin } from 'vite';
import * as _compiler from 'vue/compiler-sfc'; import * as _compiler from 'vue/compiler-sfc';
import { SFCScriptCompileOptions, SFCTemplateCompileOptions, SFCStyleCompileOptions } from 'vue/compiler-sfc'; import { SFCScriptCompileOptions, SFCTemplateCompileOptions, SFCStyleCompileOptions } from 'vue/compiler-sfc';

View File

@ -1,4 +1,4 @@
import { Plugin, ViteDevServer } from 'vite'; import { ViteDevServer, Plugin } from 'vite';
import * as _compiler from 'vue/compiler-sfc'; import * as _compiler from 'vue/compiler-sfc';
import { SFCScriptCompileOptions, SFCTemplateCompileOptions, SFCStyleCompileOptions } 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>; declare function vuePlugin(rawOptions?: Options): Plugin<Api>;
// @ts-ignore export { type Api, type Options, type ResolvedOptions, type VueQuery, vuePlugin as default, parseVueRequest };
export = vuePlugin;
export { type Api, type Options, type ResolvedOptions, type VueQuery, parseVueRequest };

View File

@ -7,7 +7,7 @@ import crypto from 'node:crypto';
import require$$0 from 'tty'; import require$$0 from 'tty';
import require$$1 from 'util'; import require$$1 from 'util';
const version = "5.2.4"; const version = "5.2.1";
function resolveCompiler(root) { function resolveCompiler(root) {
const compiler = tryResolveCompiler(root) || tryResolveCompiler(); const compiler = tryResolveCompiler(root) || tryResolveCompiler();
@ -138,8 +138,7 @@ function getTempSrcDescriptor(filename, query) {
start: { line: 0, column: 0 } start: { line: 0, column: 0 }
} }
} }
], ]
isTemp: true
}; };
} }
function setSrcDescriptor(filename, entry, scoped) { function setSrcDescriptor(filename, entry, scoped) {
@ -297,8 +296,6 @@ function resolveTemplateCompilerOptions(descriptor, options, ssr) {
} }
return { return {
...options.template, ...options.template,
// @ts-expect-error TODO remove when 3.6 is out
vapor: descriptor.vapor,
id, id,
ast: canReuseAST(options.compiler.version) ? descriptor.template?.ast : void 0, ast: canReuseAST(options.compiler.version) ? descriptor.template?.ast : void 0,
filename, filename,
@ -362,7 +359,8 @@ function resolveScript(descriptor, options, ssr, customElement) {
if (cached) { if (cached) {
return cached; return cached;
} }
const resolved = options.compiler.compileScript(descriptor, { let resolved = null;
resolved = options.compiler.compileScript(descriptor, {
...options.script, ...options.script,
id: descriptor.id, id: descriptor.id,
isProd: options.isProduction, isProd: options.isProduction,
@ -428,7 +426,7 @@ function decodeInteger(reader, relative) {
const shouldNegate = value & 1; const shouldNegate = value & 1;
value >>>= 1; value >>>= 1;
if (shouldNegate) { if (shouldNegate) {
value = -2147483648 | -value; value = -0x80000000 | -value;
} }
return relative + value; return relative + value;
} }
@ -1005,6 +1003,10 @@ function put(setarr, key) {
} }
const COLUMN = 0; const COLUMN = 0;
const SOURCES_INDEX = 1;
const SOURCE_LINE = 2;
const SOURCE_COLUMN = 3;
const NAMES_INDEX = 4;
const NO_NAME = -1; const NO_NAME = -1;
/** /**
@ -1077,12 +1079,17 @@ function addSegmentInternal(skipable, map, genLine, genColumn, source, sourceLin
const line = getLine(mappings, genLine); const line = getLine(mappings, genLine);
const index = getColumnIndex(line, genColumn); const index = getColumnIndex(line, genColumn);
if (!source) { if (!source) {
if (skipable && skipSourceless(line, index))
return;
return insert(line, index, [genColumn]); return insert(line, index, [genColumn]);
} }
const sourcesIndex = put(sources, source); const sourcesIndex = put(sources, source);
const namesIndex = name ? put(names, name) : NO_NAME; const namesIndex = name ? put(names, name) : NO_NAME;
if (sourcesIndex === sourcesContent.length) if (sourcesIndex === sourcesContent.length)
sourcesContent[sourcesIndex] = content !== null && content !== void 0 ? content : null; sourcesContent[sourcesIndex] = content !== null && content !== void 0 ? content : null;
if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) {
return;
}
return insert(line, index, name return insert(line, index, name
? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex] ? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex]
: [genColumn, sourcesIndex, sourceLine, sourceColumn]); : [genColumn, sourcesIndex, sourceLine, sourceColumn]);
@ -1122,6 +1129,32 @@ function putAll(setarr, array) {
for (let i = 0; i < array.length; i++) for (let i = 0; i < array.length; i++)
put(setarr, array[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) { function addMappingInternal(skipable, map, mapping) {
const { generated, source, original, name, content } = mapping; const { generated, source, original, name, content } = mapping;
if (!source) { if (!source) {
@ -1482,64 +1515,26 @@ function requireCommon () {
createDebug.names = []; createDebug.names = [];
createDebug.skips = []; createDebug.skips = [];
const split = (typeof namespaces === 'string' ? namespaces : '') let i;
.trim() const split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/);
.replace(' ', ',') const len = split.length;
.split(',')
.filter(Boolean);
for (const ns of split) { for (i = 0; i < len; i++) {
if (ns[0] === '-') { if (!split[i]) {
createDebug.skips.push(ns.slice(1)); // ignore empty strings
continue;
}
namespaces = split[i].replace(/\*/g, '.*?');
if (namespaces[0] === '-') {
createDebug.skips.push(new RegExp('^' + namespaces.slice(1) + '$'));
} else { } 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. * Disable debug output.
* *
@ -1548,8 +1543,8 @@ function requireCommon () {
*/ */
function disable() { function disable() {
const namespaces = [ const namespaces = [
...createDebug.names, ...createDebug.names.map(toNamespace),
...createDebug.skips.map(namespace => '-' + namespace) ...createDebug.skips.map(toNamespace).map(namespace => '-' + namespace)
].join(','); ].join(',');
createDebug.enable(''); createDebug.enable('');
return namespaces; return namespaces;
@ -1563,14 +1558,21 @@ function requireCommon () {
* @api public * @api public
*/ */
function enabled(name) { function enabled(name) {
for (const skip of createDebug.skips) { if (name[name.length - 1] === '*') {
if (matchesTemplate(name, skip)) { return true;
}
let i;
let len;
for (i = 0, len = createDebug.skips.length; i < len; i++) {
if (createDebug.skips[i].test(name)) {
return false; return false;
} }
} }
for (const ns of createDebug.names) { for (i = 0, len = createDebug.names.length; i < len; i++) {
if (matchesTemplate(name, ns)) { if (createDebug.names[i].test(name)) {
return true; return true;
} }
} }
@ -1578,6 +1580,19 @@ function requireCommon () {
return false; 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`. * Coerce `val`.
* *
@ -1746,7 +1761,6 @@ function requireBrowser () {
// Is webkit? http://stackoverflow.com/a/16459606/376773 // Is webkit? http://stackoverflow.com/a/16459606/376773
// document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 // 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) || return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) ||
// Is firebug? http://stackoverflow.com/a/398120/376773 // Is firebug? http://stackoverflow.com/a/398120/376773
(typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) ||
@ -2171,25 +2185,18 @@ function requireNode () {
* treat as a browser. * treat as a browser.
*/ */
var hasRequiredSrc; if (typeof process === 'undefined' || process.type === 'renderer' || process.browser === true || process.__nwjs) {
src.exports = requireBrowser();
function requireSrc () { } else {
if (hasRequiredSrc) return src.exports; src.exports = requireNode();
hasRequiredSrc = 1;
if (typeof process === 'undefined' || process.type === 'renderer' || process.browser === true || process.__nwjs) {
src.exports = requireBrowser();
} else {
src.exports = requireNode();
}
return src.exports;
} }
var srcExports = requireSrc(); var srcExports = src.exports;
const _debug = /*@__PURE__*/getDefaultExportFromCjs(srcExports); const _debug = /*@__PURE__*/getDefaultExportFromCjs(srcExports);
const debug = _debug("vite:hmr"); const debug = _debug("vite:hmr");
const directRequestRE = /(?:\?|&)direct\b/; 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); const prevDescriptor = getDescriptor(file, options, false, true);
if (!prevDescriptor) { if (!prevDescriptor) {
return; return;
@ -2290,15 +2297,17 @@ async function handleHotUpdate({ file, modules, read }, options, customElement,
} }
debug(`[vue:update(${updateType.join("&")})] ${file}`); debug(`[vue:update(${updateType.join("&")})] ${file}`);
} }
return [...affectedModules, ...typeDepModules || []].filter( return [...affectedModules].filter(Boolean);
Boolean
);
} }
function isEqualBlock(a, b) { function isEqualBlock(a, b) {
if (!a && !b) return true; if (!a && !b)
if (!a || !b) return false; return true;
if (a.src && b.src && a.src === b.src) return true; if (!a || !b)
if (a.content !== b.content) return false; 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 keysA = Object.keys(a.attrs);
const keysB = Object.keys(b.attrs); const keysB = Object.keys(b.attrs);
if (keysA.length !== keysB.length) { if (keysA.length !== keysB.length) {
@ -2537,20 +2546,11 @@ async function transformMain(code, filename, options, pluginContext, ssr, custom
} }
let resolvedMap = void 0; let resolvedMap = void 0;
if (options.sourceMap) { if (options.sourceMap) {
if (templateMap) { if (scriptMap && templateMap) {
const from = scriptMap ?? {
file: filename,
sourceRoot: "",
version: 3,
sources: [],
sourcesContent: [],
names: [],
mappings: ""
};
const gen = fromMap( const gen = fromMap(
// version property of result.map is declared as string // version property of result.map is declared as string
// but actually it is `3` // but actually it is `3`
from scriptMap
); );
const tracer = new TraceMap( const tracer = new TraceMap(
// same above // 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; const offset = (scriptCode.match(/\r?\n/g)?.length ?? 0) + 1;
eachMapping(tracer, (m) => { eachMapping(tracer, (m) => {
if (m.source == null) return; if (m.source == null)
return;
addMapping(gen, { addMapping(gen, {
source: m.source, source: m.source,
original: { line: m.originalLine, column: m.originalColumn }, original: { line: m.originalLine, column: m.originalColumn },
@ -2571,7 +2572,7 @@ async function transformMain(code, filename, options, pluginContext, ssr, custom
resolvedMap = toEncodedMap(gen); resolvedMap = toEncodedMap(gen);
resolvedMap.sourcesContent = templateMap.sourcesContent; resolvedMap.sourcesContent = templateMap.sourcesContent;
} else { } else {
resolvedMap = scriptMap; resolvedMap = scriptMap ?? templateMap;
} }
} }
if (!attachedProps.length) { if (!attachedProps.length) {
@ -2585,41 +2586,21 @@ async function transformMain(code, filename, options, pluginContext, ssr, custom
let resolvedCode = output.join("\n"); let resolvedCode = output.join("\n");
const lang = descriptor.scriptSetup?.lang || descriptor.script?.lang; const lang = descriptor.scriptSetup?.lang || descriptor.script?.lang;
if (lang && /tsx?$/.test(lang) && !descriptor.script?.src) { if (lang && /tsx?$/.test(lang) && !descriptor.script?.src) {
const { transformWithOxc } = await import('vite'); const { code: code2, map } = await transformWithEsbuild(
if (transformWithOxc) { resolvedCode,
const { code: code2, map } = await transformWithOxc( filename,
resolvedCode, {
filename, target: "esnext",
{ // #430 support decorators in .vue file
// #430 support decorators in .vue file // target can be overridden by esbuild config target
// target can be overridden by oxc config target ...options.devServer?.config.esbuild,
// @ts-ignore Rolldown-specific loader: "ts",
...options.devServer?.config.oxc, sourcemap: options.sourceMap
lang: "ts", },
sourcemap: options.sourceMap resolvedMap
}, );
resolvedMap resolvedCode = code2;
); resolvedMap = resolvedMap ? map : 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;
}
} }
return { return {
code: resolvedCode, code: resolvedCode,
@ -2668,8 +2649,7 @@ async function genTemplateCode(descriptor, options, pluginContext, ssr, customEl
} }
} }
async function genScriptCode(descriptor, options, pluginContext, ssr, customElement) { async function genScriptCode(descriptor, options, pluginContext, ssr, customElement) {
const vaporFlag = descriptor.vapor ? "__vapor: true" : ""; let scriptCode = `const ${scriptIdentifier} = {}`;
let scriptCode = `const ${scriptIdentifier} = { ${vaporFlag} }`;
let map; let map;
const script = resolveScript(descriptor, options, ssr, customElement); const script = resolveScript(descriptor, options, ssr, customElement);
if (script) { if (script) {
@ -2738,7 +2718,7 @@ async function genStyleCode(descriptor, pluginContext, customElement, attachedPr
style.module style.module
); );
stylesCode += importCode; stylesCode += importCode;
Object.assign(cssModulesMap ||= {}, nameMap); Object.assign(cssModulesMap || (cssModulesMap = {}), nameMap);
} else { } else {
if (customElement) { if (customElement) {
stylesCode += ` stylesCode += `
@ -2867,12 +2847,7 @@ async function transformStyle(code, descriptor, index, options, pluginContext, f
) : { mappings: "" }; ) : { mappings: "" };
return { return {
code: result.code, code: result.code,
map, map
meta: block.scoped && !descriptor.isTemp ? {
vite: {
cssScopeTo: [descriptor.filename, "default"]
}
} : void 0
}; };
} }
@ -2896,7 +2871,6 @@ function vuePlugin(rawOptions = {}) {
const customElement = options.value.features?.customElement || options.value.customElement; const customElement = options.value.features?.customElement || options.value.customElement;
return typeof customElement === "boolean" ? () => customElement : createFilter(customElement); return typeof customElement === "boolean" ? () => customElement : createFilter(customElement);
}); });
let transformCachedModule = false;
return { return {
name: "vite:vue", name: "vite:vue",
api: { api: {
@ -2917,42 +2891,26 @@ function vuePlugin(rawOptions = {}) {
if (options.value.compiler.invalidateTypeCache) { if (options.value.compiler.invalidateTypeCache) {
options.value.compiler.invalidateTypeCache(ctx.file); options.value.compiler.invalidateTypeCache(ctx.file);
} }
let typeDepModules;
const matchesFilter = filter.value(ctx.file);
if (typeDepToSFCMap.has(ctx.file)) { if (typeDepToSFCMap.has(ctx.file)) {
typeDepModules = handleTypeDepChange( return handleTypeDepChange(typeDepToSFCMap.get(ctx.file), ctx);
typeDepToSFCMap.get(ctx.file),
ctx
);
if (!matchesFilter) return typeDepModules;
} }
if (matchesFilter) { if (filter.value(ctx.file)) {
return handleHotUpdate( return handleHotUpdate(
ctx, ctx,
options.value, options.value,
customElementFilter.value(ctx.file), customElementFilter.value(ctx.file)
typeDepModules
); );
} }
}, },
config(config) { config(config) {
const parseDefine = (v) => {
try {
return typeof v === "string" ? JSON.parse(v) : v;
} catch (err) {
return v;
}
};
return { return {
resolve: { resolve: {
dedupe: config.build?.ssr ? [] : ["vue"] dedupe: config.build?.ssr ? [] : ["vue"]
}, },
define: { define: {
__VUE_OPTIONS_API__: options.value.features?.optionsAPI ?? parseDefine(config.define?.__VUE_OPTIONS_API__) ?? true, __VUE_OPTIONS_API__: !!((options.value.features?.optionsAPI ?? true) || config.define?.__VUE_OPTIONS_API__),
__VUE_PROD_DEVTOOLS__: (options.value.features?.prodDevtools || parseDefine(config.define?.__VUE_PROD_DEVTOOLS__)) ?? false, __VUE_PROD_DEVTOOLS__: !!(options.value.features?.prodDevtools || config.define?.__VUE_PROD_DEVTOOLS__),
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: (options.value.features?.prodHydrationMismatchDetails || parseDefine( __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: !!(options.value.features?.prodHydrationMismatchDetails || config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__)
config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__
)) ?? false
}, },
ssr: { ssr: {
// @ts-ignore -- config.legacy.buildSsrCjsExternalHeuristics will be removed in Vite 5 // @ts-ignore -- config.legacy.buildSsrCjsExternalHeuristics will be removed in Vite 5
@ -2969,23 +2927,6 @@ function vuePlugin(rawOptions = {}) {
isProduction: config.isProduction, isProduction: config.isProduction,
devToolsEnabled: !!(options.value.features?.prodDevtools || config.define.__VUE_PROD_DEVTOOLS__ || !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) { configureServer(server) {
options.value.devServer = server; options.value.devServer = server;
@ -3007,10 +2948,10 @@ function vuePlugin(rawOptions = {}) {
} }
}, },
load(id, opt) { load(id, opt) {
const ssr = opt?.ssr === true;
if (id === EXPORT_HELPER_ID) { if (id === EXPORT_HELPER_ID) {
return helperCode; return helperCode;
} }
const ssr = opt?.ssr === true;
const { filename, query } = parseVueRequest(id); const { filename, query } = parseVueRequest(id);
if (query.vue) { if (query.vue) {
if (query.src) { if (query.src) {
@ -3060,9 +3001,6 @@ function vuePlugin(rawOptions = {}) {
); );
} else { } else {
const descriptor = query.src ? getSrcDescriptor(filename, query) || getTempSrcDescriptor(filename, query) : getDescriptor(filename, options.value); const descriptor = query.src ? getSrcDescriptor(filename, query) || getTempSrcDescriptor(filename, query) : getDescriptor(filename, options.value);
if (query.src) {
this.addWatchFile(filename);
}
if (query.type === "template") { if (query.type === "template") {
return transformTemplateAsModule( return transformTemplateAsModule(
code, code,

View File

@ -1,6 +1,6 @@
{ {
"name": "@vitejs/plugin-vue", "name": "@vitejs/plugin-vue",
"version": "5.2.4", "version": "5.2.1",
"type": "commonjs", "type": "commonjs",
"license": "MIT", "license": "MIT",
"author": "Evan You", "author": "Evan You",
@ -33,14 +33,14 @@
"vue": "^3.2.25" "vue": "^3.2.25"
}, },
"devDependencies": { "devDependencies": {
"@jridgewell/gen-mapping": "^0.3.8", "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25", "@jridgewell/trace-mapping": "^0.3.25",
"debug": "^4.4.0", "debug": "^4.3.7",
"rollup": "^4.40.2", "rollup": "^4.27.2",
"slash": "^5.1.0", "slash": "^5.1.0",
"source-map-js": "^1.2.1", "source-map-js": "^1.2.1",
"vite": "^6.3.5", "vite": "^6.0.0",
"vue": "^3.5.13" "vue": "^3.5.12"
}, },
"scripts": { "scripts": {
"dev": "unbuild --stub", "dev": "unbuild --stub",

BIN
node_modules/esbuild/bin/esbuild generated vendored

Binary file not shown.

9
node_modules/esbuild/install.js generated vendored
View File

@ -167,7 +167,8 @@ function extractFileFromTarGzip(buffer, subpath) {
let size = parseInt(str(offset + 124, 12), 8); let size = parseInt(str(offset + 124, 12), 8);
offset += 512; offset += 512;
if (!isNaN(size)) { 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; offset += size + 511 & ~511;
} }
} }
@ -202,8 +203,10 @@ function removeRecursive(dir) {
} catch { } catch {
continue; continue;
} }
if (stats.isDirectory()) removeRecursive(entryPath); if (stats.isDirectory())
else fs2.unlinkSync(entryPath); removeRecursive(entryPath);
else
fs2.unlinkSync(entryPath);
} }
fs2.rmdirSync(dir); fs2.rmdirSync(dir);
} }

2
node_modules/esbuild/lib/main.d.ts generated vendored
View File

@ -340,7 +340,6 @@ export interface ResolveOptions {
resolveDir?: string resolveDir?: string
kind?: ImportKind kind?: ImportKind
pluginData?: any pluginData?: any
with?: Record<string, string>
} }
/** Documentation: https://esbuild.github.io/plugins/#resolve-results */ /** Documentation: https://esbuild.github.io/plugins/#resolve-results */
@ -380,7 +379,6 @@ export interface OnResolveArgs {
resolveDir: string resolveDir: string
kind: ImportKind kind: ImportKind
pluginData: any pluginData: any
with: Record<string, string>
} }
export type ImportKind = export type ImportKind =

705
node_modules/esbuild/lib/main.js generated vendored

File diff suppressed because it is too large Load Diff

48
node_modules/esbuild/package.json generated vendored
View File

@ -1,6 +1,6 @@
{ {
"name": "esbuild", "name": "esbuild",
"version": "0.21.5", "version": "0.20.2",
"description": "An extremely fast JavaScript and CSS bundler and minifier.", "description": "An extremely fast JavaScript and CSS bundler and minifier.",
"repository": { "repository": {
"type": "git", "type": "git",
@ -18,29 +18,29 @@
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5", "@esbuild/aix-ppc64": "0.20.2",
"@esbuild/android-arm": "0.21.5", "@esbuild/android-arm": "0.20.2",
"@esbuild/android-arm64": "0.21.5", "@esbuild/android-arm64": "0.20.2",
"@esbuild/android-x64": "0.21.5", "@esbuild/android-x64": "0.20.2",
"@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.21.5", "@esbuild/darwin-x64": "0.20.2",
"@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-arm64": "0.20.2",
"@esbuild/freebsd-x64": "0.21.5", "@esbuild/freebsd-x64": "0.20.2",
"@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm": "0.20.2",
"@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-arm64": "0.20.2",
"@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-ia32": "0.20.2",
"@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-loong64": "0.20.2",
"@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-mips64el": "0.20.2",
"@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-ppc64": "0.20.2",
"@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-riscv64": "0.20.2",
"@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-s390x": "0.20.2",
"@esbuild/linux-x64": "0.21.5", "@esbuild/linux-x64": "0.20.2",
"@esbuild/netbsd-x64": "0.21.5", "@esbuild/netbsd-x64": "0.20.2",
"@esbuild/openbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.20.2",
"@esbuild/sunos-x64": "0.21.5", "@esbuild/sunos-x64": "0.20.2",
"@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-arm64": "0.20.2",
"@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-ia32": "0.20.2",
"@esbuild/win32-x64": "0.21.5" "@esbuild/win32-x64": "0.20.2"
}, },
"license": "MIT" "license": "MIT"
} }

4
node_modules/rollup/README.md generated vendored
View File

@ -9,8 +9,8 @@
<a href="https://nodejs.org/en/about/previous-releases"> <a href="https://nodejs.org/en/about/previous-releases">
<img src="https://img.shields.io/node/v/rollup.svg" alt="node compatibility"> <img src="https://img.shields.io/node/v/rollup.svg" alt="node compatibility">
</a> </a>
<a href="https://packagephobia.now.sh/result?p=rollup"> <a href="https://packagephobia.com/result?p=rollup">
<img src="https://packagephobia.now.sh/badge?p=rollup" alt="install size" > <img src="https://packagephobia.com/badge?p=rollup" alt="install size" >
</a> </a>
<a href="https://codecov.io/gh/rollup/rollup"> <a href="https://codecov.io/gh/rollup/rollup">
<img src="https://codecov.io/gh/rollup/rollup/graph/badge.svg" alt="code coverage" > <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

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup
@ -27,7 +27,7 @@ function _mergeNamespaces(n, m) {
return Object.defineProperty(n, Symbol.toStringTag, { value: 'Module' }); return Object.defineProperty(n, Symbol.toStringTag, { value: 'Module' });
} }
var version = "4.59.0"; var version = "4.59.1";
// src/vlq.ts // src/vlq.ts
var comma = ",".charCodeAt(0); var comma = ",".charCodeAt(0);
@ -19594,7 +19594,11 @@ function getDynamicallyDependentEntriesByDynamicEntry(dependentEntriesByModule,
getDynamicImporters(dynamicEntry), getDynamicImporters(dynamicEntry),
dynamicEntry.implicitlyLoadedAfter 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); dynamicallyDependentEntries.add(entry);
} }
} }

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup

4
node_modules/rollup/dist/rollup.js generated vendored
View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup
@ -42,7 +42,7 @@ function _mergeNamespaces(n, m) {
const promises__namespace = /*#__PURE__*/_interopNamespaceDefault(promises); const promises__namespace = /*#__PURE__*/_interopNamespaceDefault(promises);
var version = "4.59.0"; var version = "4.59.1";
function ensureArray$1(items) { function ensureArray$1(items) {
if (Array.isArray(items)) { if (Array.isArray(items)) {
@ -21088,7 +21088,11 @@ function getDynamicallyDependentEntriesByDynamicEntry(dependentEntriesByModule,
getDynamicImporters(dynamicEntry), getDynamicImporters(dynamicEntry),
dynamicEntry.implicitlyLoadedAfter 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); dynamicallyDependentEntries.add(entry);
} }
} }

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup

View File

@ -1,7 +1,7 @@
/* /*
@license @license
Rollup.js v4.59.0 Rollup.js v4.59.1
Sun, 22 Feb 2026 07:31:53 GMT - commit ae846957f109690a866cc3e4c073613c338d3476 Sat, 21 Mar 2026 06:45:31 GMT - commit 0cba9e079e1d6e56882558827b37557f36c52966
https://github.com/rollup/rollup https://github.com/rollup/rollup

102
node_modules/rollup/package.json generated vendored
View File

@ -1,6 +1,6 @@
{ {
"name": "rollup", "name": "rollup",
"version": "4.59.0", "version": "4.59.1",
"description": "Next-generation ES module bundler", "description": "Next-generation ES module bundler",
"main": "dist/rollup.js", "main": "dist/rollup.js",
"module": "dist/es/rollup.js", "module": "dist/es/rollup.js",
@ -114,31 +114,31 @@
"homepage": "https://rollupjs.org/", "homepage": "https://rollupjs.org/",
"optionalDependencies": { "optionalDependencies": {
"fsevents": "~2.3.2", "fsevents": "~2.3.2",
"@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.1",
"@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-android-arm64": "4.59.1",
"@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.1",
"@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.1",
"@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.1",
"@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.1",
"@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm-eabi": "4.59.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.1",
"@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.1",
"@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.1",
"@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.1",
"@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.1",
"@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.1",
"@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.1",
"@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.1",
"@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.1",
"@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.1",
"@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.1",
"@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.1",
"@rollup/rollup-win32-x64-msvc": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.1",
"@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.1",
"@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.1",
"@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.1",
"@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.1",
"@rollup/rollup-openharmony-arm64": "4.59.0" "@rollup/rollup-openharmony-arm64": "4.59.1"
}, },
"dependencies": { "dependencies": {
"@types/estree": "1.0.8" "@types/estree": "1.0.8"
@ -147,12 +147,12 @@
"core-js": "We only update manually as every update requires a snapshot update" "core-js": "We only update manually as every update requires a snapshot update"
}, },
"devDependencies": { "devDependencies": {
"@codemirror/commands": "^6.10.2", "@codemirror/commands": "^6.10.3",
"@codemirror/lang-javascript": "^6.2.4", "@codemirror/lang-javascript": "^6.2.5",
"@codemirror/language": "^6.12.1", "@codemirror/language": "^6.12.2",
"@codemirror/search": "^6.6.0", "@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4", "@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.39.14", "@codemirror/view": "^6.40.0",
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@inquirer/prompts": "^7.10.1", "@inquirer/prompts": "^7.10.1",
"@jridgewell/sourcemap-codec": "^1.5.5", "@jridgewell/sourcemap-codec": "^1.5.5",
@ -160,21 +160,21 @@
"@napi-rs/cli": "3.4.1", "@napi-rs/cli": "3.4.1",
"@rollup/plugin-alias": "^6.0.0", "@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-buble": "^1.0.3", "@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-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-replace": "^6.0.3",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.3.0", "@rollup/plugin-typescript": "^12.3.0",
"@rollup/pluginutils": "^5.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/mocha": "^10.0.10",
"@types/node": "^20.19.33", "@types/node": "^20.19.37",
"@types/picomatch": "^4.0.2", "@types/picomatch": "^4.0.2",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"@types/yargs-parser": "^21.0.3", "@types/yargs-parser": "^21.0.3",
"@vue/language-server": "^3.2.4", "@vue/language-server": "^3.2.5",
"acorn": "^8.15.0", "acorn": "^8.16.0",
"acorn-import-assertions": "^1.9.0", "acorn-import-assertions": "^1.9.0",
"acorn-jsx": "^5.3.2", "acorn-jsx": "^5.3.2",
"buble": "^0.20.0", "buble": "^0.20.0",
@ -186,26 +186,26 @@
"date-time": "^4.0.0", "date-time": "^4.0.0",
"es5-shim": "^4.6.7", "es5-shim": "^4.6.7",
"es6-shim": "^0.35.8", "es6-shim": "^0.35.8",
"eslint": "^10.0.0", "eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5", "eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-unicorn": "^63.0.0", "eslint-plugin-unicorn": "^63.0.0",
"eslint-plugin-vue": "^10.8.0", "eslint-plugin-vue": "^10.8.0",
"fixturify": "^3.0.0", "fixturify": "^3.0.0",
"flru": "^1.0.2", "flru": "^1.0.2",
"fs-extra": "^11.3.3", "fs-extra": "^11.3.4",
"github-api": "^3.4.0", "github-api": "^3.4.0",
"globals": "^17.3.0", "globals": "^17.4.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"is-reference": "^3.0.3", "is-reference": "^3.0.3",
"lint-staged": "^16.2.7", "lint-staged": "^16.4.0",
"locate-character": "^3.0.0", "locate-character": "^3.0.0",
"magic-string": "^0.30.21", "magic-string": "^0.30.21",
"memfs": "^4.56.10", "memfs": "^4.56.11",
"mocha": "11.3.0", "mocha": "11.7.5",
"nodemon": "^3.1.11", "nodemon": "^3.1.14",
"npm-audit-resolver": "^3.0.0-RC.0", "npm-audit-resolver": "^3.0.0-RC.0",
"nyc": "^17.1.0", "nyc": "^18.0.0",
"patch-package": "^8.0.1", "patch-package": "^8.0.1",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"picomatch": "^4.0.3", "picomatch": "^4.0.3",
@ -215,7 +215,7 @@
"pretty-bytes": "^7.1.0", "pretty-bytes": "^7.1.0",
"pretty-ms": "^9.3.0", "pretty-ms": "^9.3.0",
"requirejs": "^2.3.8", "requirejs": "^2.3.8",
"rollup": "^4.57.1", "rollup": "^4.59.0",
"rollup-plugin-license": "^3.7.0", "rollup-plugin-license": "^3.7.0",
"semver": "^7.7.4", "semver": "^7.7.4",
"shx": "^0.4.0", "shx": "^0.4.0",
@ -223,24 +223,24 @@
"source-map": "^0.7.6", "source-map": "^0.7.6",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"systemjs": "^6.15.1", "systemjs": "^6.15.1",
"terser": "^5.46.0", "terser": "^5.46.1",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.56.0", "typescript-eslint": "^8.57.1",
"vite": "^7.3.1", "vite": "^7.3.1",
"vitepress": "^1.6.4", "vitepress": "^1.6.4",
"vue": "^3.5.28", "vue": "^3.5.30",
"vue-eslint-parser": "^10.4.0", "vue-eslint-parser": "^10.4.0",
"vue-tsc": "^3.2.4", "vue-tsc": "^3.2.5",
"wasm-pack": "^0.14.0", "wasm-pack": "^0.14.0",
"yargs-parser": "^21.1.1" "yargs-parser": "^21.1.1"
}, },
"overrides": { "overrides": {
"axios": "^1.13.5", "axios": "^1.13.6",
"esbuild": ">0.24.2", "esbuild": ">0.24.2",
"lodash-es": ">4.17.22", "lodash-es": ">4.17.22",
"path-scurry": { "path-scurry": {
"lru-cache": "^11.2.6" "lru-cache": "^11.2.7"
}, },
"readable-stream": "npm:@built-in/readable-stream@1", "readable-stream": "npm:@built-in/readable-stream@1",
"semver": "^7.7.4", "semver": "^7.7.4",

315
node_modules/vite/LICENSE.md generated vendored
View File

@ -3,7 +3,7 @@ Vite is released under the MIT license:
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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -25,7 +25,7 @@ SOFTWARE.
# Licenses of bundled dependencies # Licenses of bundled dependencies
The published Vite artifact additionally contains code with the following licenses: 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: # Bundled dependencies:
## @ampproject/remapping ## @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 ## ansi-regex
License: MIT License: MIT
By: Sindre Sorhus 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 ## generic-names
License: MIT License: MIT
By: Alexey Litvinov By: Alexey Litvinov
@ -1506,7 +1615,7 @@ Repository: git://github.com/isaacs/node-glob.git
> The ISC License > 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 > Permission to use, copy, modify, and/or distribute this software for any
> purpose with or without fee is hereby granted, provided that the above > 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 ## is-binary-path
License: MIT License: MIT
By: Sindre Sorhus 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 ## magic-string
License: MIT License: MIT
By: Rich Harris 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 ## mlly
License: MIT License: MIT
Repository: unjs/mlly 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 ## open
License: MIT License: MIT
By: Sindre Sorhus 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 ## periscopic
License: MIT License: MIT
Repository: Rich-Harris/periscopic 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 ## ws
License: MIT License: MIT
By: Einar Otto Stangvik By: Einar Otto Stangvik

8
node_modules/vite/README.md generated vendored
View File

@ -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: 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).

12
node_modules/vite/client.d.ts generated vendored
View File

@ -54,10 +54,6 @@ declare module '*.apng' {
const src: string const src: string
export default src export default src
} }
declare module '*.bmp' {
const src: string
export default src
}
declare module '*.png' { declare module '*.png' {
const src: string const src: string
export default src export default src
@ -246,11 +242,3 @@ declare module '*?inline' {
const src: string const src: string
export default src 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

View File

@ -1,24 +1,30 @@
const context = (() => { const context = (() => {
if (typeof globalThis !== "undefined") { if (typeof globalThis !== 'undefined') {
return globalThis; return globalThis;
} else if (typeof self !== "undefined") { }
return self; else if (typeof self !== 'undefined') {
} else if (typeof window !== "undefined") { return self;
return window; }
} else { else if (typeof window !== 'undefined') {
return Function("return this")(); return window;
} }
else {
return Function('return this')();
}
})(); })();
// assign defines
const defines = __DEFINES__; const defines = __DEFINES__;
Object.keys(defines).forEach((key) => { Object.keys(defines).forEach((key) => {
const segments = key.split("."); const segments = key.split('.');
let target = context; let target = context;
for (let i = 0; i < segments.length; i++) { for (let i = 0; i < segments.length; i++) {
const segment = segments[i]; const segment = segments[i];
if (i === segments.length - 1) { if (i === segments.length - 1) {
target[segment] = defines[key]; target[segment] = defines[key];
} else { }
target = target[segment] || (target[segment] = {}); else {
target = target[segment] || (target[segment] = {});
}
} }
}
}); });
//# sourceMappingURL=env.mjs.map

File diff suppressed because it is too large Load Diff

View File

@ -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

478
node_modules/vite/dist/node/cli.js generated vendored
View File

@ -1,21 +1,18 @@
import path from 'node:path'; import path from 'node:path';
import fs__default from 'node:fs'; import fs from 'node:fs';
import { performance } from 'node:perf_hooks'; import { performance } from 'node:perf_hooks';
import { EventEmitter } from 'events'; 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 { VERSION } from './constants.js';
import 'node:fs/promises'; import 'node:fs/promises';
import 'node:url'; import 'node:url';
import 'node:util'; import 'node:util';
import 'node:module'; import 'node:module';
import 'node:crypto';
import 'tty'; import 'tty';
import 'path'; import 'path';
import 'esbuild'; import 'esbuild';
import 'fs'; import 'fs';
import 'node:events'; import 'assert';
import 'node:stream';
import 'node:string_decoder';
import 'node:child_process'; import 'node:child_process';
import 'node:http'; import 'node:http';
import 'node:https'; import 'node:https';
@ -27,6 +24,7 @@ import 'stream';
import 'os'; import 'os';
import 'child_process'; import 'child_process';
import 'node:os'; import 'node:os';
import 'node:crypto';
import 'node:dns'; import 'node:dns';
import 'crypto'; import 'crypto';
import 'module'; import 'module';
@ -34,6 +32,7 @@ import 'node:assert';
import 'node:v8'; import 'node:v8';
import 'node:worker_threads'; import 'node:worker_threads';
import 'node:buffer'; import 'node:buffer';
import 'node:events';
import 'rollup/parseAst'; import 'rollup/parseAst';
import 'querystring'; import 'querystring';
import 'node:readline'; import 'node:readline';
@ -41,8 +40,6 @@ import 'zlib';
import 'buffer'; import 'buffer';
import 'https'; import 'https';
import 'tls'; import 'tls';
import 'node:net';
import 'assert';
import 'node:zlib'; import 'node:zlib';
function toArr(any) { function toArr(any) {
@ -658,258 +655,269 @@ class CAC extends EventEmitter {
const cac = (name = "") => new CAC(name); const cac = (name = "") => new CAC(name);
const cli = cac("vite"); const cli = cac('vite');
let profileSession = global.__vite_profile_session; let profileSession = global.__vite_profile_session;
let profileCount = 0; let profileCount = 0;
const stopProfiler = (log) => { const stopProfiler = (log) => {
if (!profileSession) return; if (!profileSession)
return new Promise((res, rej) => { return;
profileSession.post("Profiler.stop", (err, { profile }) => { return new Promise((res, rej) => {
if (!err) { profileSession.post('Profiler.stop', (err, { profile }) => {
const outPath = path.resolve( // Write profile to disk, upload, etc.
`./vite-profile-${profileCount++}.cpuprofile` if (!err) {
); const outPath = path.resolve(`./vite-profile-${profileCount++}.cpuprofile`);
fs__default.writeFileSync(outPath, JSON.stringify(profile)); fs.writeFileSync(outPath, JSON.stringify(profile));
log( log(colors.yellow(`CPU profile written to ${colors.white(colors.dim(outPath))}`));
colors.yellow( profileSession = undefined;
`CPU profile written to ${colors.white(colors.dim(outPath))}` res();
) }
); else {
profileSession = void 0; rej(err);
res(); }
} else { });
rej(err);
}
}); });
});
}; };
const filterDuplicateOptions = (options) => { const filterDuplicateOptions = (options) => {
for (const [key, value] of Object.entries(options)) { for (const [key, value] of Object.entries(options)) {
if (Array.isArray(value)) { if (Array.isArray(value)) {
options[key] = value[value.length - 1]; options[key] = value[value.length - 1];
}
} }
}
}; };
/**
* removing global flags before passing as command specific sub-configs
*/
function cleanOptions(options) { function cleanOptions(options) {
const ret = { ...options }; const ret = { ...options };
delete ret["--"]; delete ret['--'];
delete ret.c; delete ret.c;
delete ret.config; delete ret.config;
delete ret.base; delete ret.base;
delete ret.l; delete ret.l;
delete ret.logLevel; delete ret.logLevel;
delete ret.clearScreen; delete ret.clearScreen;
delete ret.d; delete ret.d;
delete ret.debug; delete ret.debug;
delete ret.f; delete ret.f;
delete ret.filter; delete ret.filter;
delete ret.m; delete ret.m;
delete ret.mode; delete ret.mode;
if ("sourcemap" in ret) { // convert the sourcemap option to a boolean if necessary
const sourcemap = ret.sourcemap; if ('sourcemap' in ret) {
ret.sourcemap = sourcemap === "true" ? true : sourcemap === "false" ? false : ret.sourcemap; const sourcemap = ret.sourcemap;
} ret.sourcemap =
return ret; sourcemap === 'true'
? true
: sourcemap === 'false'
? false
: ret.sourcemap;
}
return ret;
} }
/**
* host may be a number (like 0), should convert to string
*/
const convertHost = (v) => { const convertHost = (v) => {
if (typeof v === "number") { if (typeof v === 'number') {
return String(v); return String(v);
} }
return v; return v;
}; };
/**
* base may be a number (like 0), should convert to empty string
*/
const convertBase = (v) => { const convertBase = (v) => {
if (v === 0) { if (v === 0) {
return ""; return '';
} }
return v; return v;
}; };
cli.option("-c, --config <file>", `[string] use specified config file`).option("--base <path>", `[string] public base path (default: /)`, { cli
type: [convertBase] .option('-c, --config <file>', `[string] use specified config file`)
}).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`); .option('--base <path>', `[string] public base path (default: /)`, {
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( type: [convertBase],
"--force", })
`[boolean] force the optimizer to ignore the cache and re-bundle` .option('-l, --logLevel <level>', `[string] info | warn | error | silent`)
).action(async (root, options) => { .option('--clearScreen', `[boolean] allow/disable clear screen when logging`)
filterDuplicateOptions(options); .option('-d, --debug [feat]', `[string | boolean] show debug logs`)
const { createServer } = await import('./chunks/dep-BK3b2jBa.js').then(function (n) { return n.F; }); .option('-f, --filter <filter>', `[string] filter debug logs`)
try { .option('-m, --mode <mode>', `[string] set env mode`);
const server = await createServer({ // dev
root, cli
base: options.base, .command('[root]', 'start dev server') // default command
mode: options.mode, .alias('serve') // the command is called 'serve' in Vite's API
configFile: options.config, .alias('dev') // alias to align with the script name
logLevel: options.logLevel, .option('--host [host]', `[string] specify hostname`, { type: [convertHost] })
clearScreen: options.clearScreen, .option('--port <port>', `[number] specify port`)
optimizeDeps: { force: options.force }, .option('--open [path]', `[boolean | string] open browser on startup`)
server: cleanOptions(options) .option('--cors', `[boolean] enable CORS`)
}); .option('--strictPort', `[boolean] exit if specified port is already in use`)
if (!server.httpServer) { .option('--force', `[boolean] force the optimizer to ignore the cache and re-bundle`)
throw new Error("HTTP server not available"); .action(async (root, options) => {
} filterDuplicateOptions(options);
await server.listen(); // output structure is preserved even after bundling so require()
const info = server.config.logger.info; // is ok here
const viteStartTime = global.__vite_start_time ?? false; const { createServer } = await import('./chunks/dep-p3C6MpSJ.js').then(function (n) { return n.E; });
const startupDurationString = viteStartTime ? colors.dim( try {
`ready in ${colors.reset( const server = await createServer({
colors.bold(Math.ceil(performance.now() - viteStartTime)) root,
)} ms` base: options.base,
) : ""; mode: options.mode,
const hasExistingLogs = process.stdout.bytesWritten > 0 || process.stderr.bytesWritten > 0; configFile: options.config,
info( logLevel: options.logLevel,
` clearScreen: options.clearScreen,
${colors.green( optimizeDeps: { force: options.force },
`${colors.bold("VITE")} v${VERSION}` server: cleanOptions(options),
)} ${startupDurationString} });
`, if (!server.httpServer) {
{ throw new Error('HTTP server not available');
clear: !hasExistingLogs }
} await server.listen();
); const info = server.config.logger.info;
server.printUrls(); const viteStartTime = global.__vite_start_time ?? false;
const customShortcuts = []; const startupDurationString = viteStartTime
if (profileSession) { ? colors.dim(`ready in ${colors.reset(colors.bold(Math.ceil(performance.now() - viteStartTime)))} ms`)
customShortcuts.push({ : '';
key: "p", const hasExistingLogs = process.stdout.bytesWritten > 0 || process.stderr.bytesWritten > 0;
description: "start/stop the profiler", info(`\n ${colors.green(`${colors.bold('VITE')} v${VERSION}`)} ${startupDurationString}\n`, {
async action(server2) { clear: !hasExistingLogs,
if (profileSession) { });
await stopProfiler(server2.config.logger.info); server.printUrls();
} else { const customShortcuts = [];
const inspector = await import('node:inspector').then( if (profileSession) {
(r) => r.default customShortcuts.push({
); key: 'p',
await new Promise((res) => { description: 'start/stop the profiler',
profileSession = new inspector.Session(); async action(server) {
profileSession.connect(); if (profileSession) {
profileSession.post("Profiler.enable", () => { await stopProfiler(server.config.logger.info);
profileSession.post("Profiler.start", () => { }
server2.config.logger.info("Profiler started"); else {
res(); 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( // build
"--assetsDir <dir>", cli
`[string] directory under outDir to place assets in (default: assets)` .command('build [root]', 'build for production')
).option( .option('--target <target>', `[string] transpile target (default: 'modules')`)
"--assetsInlineLimit <number>", .option('--outDir <dir>', `[string] output directory (default: dist)`)
`[number] static asset base64 inline threshold in bytes (default: 4096)` .option('--assetsDir <dir>', `[string] directory under outDir to place assets in (default: assets)`)
).option( .option('--assetsInlineLimit <number>', `[number] static asset base64 inline threshold in bytes (default: 4096)`)
"--ssr [entry]", .option('--ssr [entry]', `[string] build specified entry for server-side rendering`)
`[string] build specified entry for server-side rendering` .option('--sourcemap [output]', `[boolean | "inline" | "hidden"] output source maps for build (default: false)`)
).option( .option('--minify [minifier]', `[boolean | "terser" | "esbuild"] enable/disable minification, ` +
"--sourcemap [output]", `or specify minifier to use (default: esbuild)`)
`[boolean | "inline" | "hidden"] output source maps for build (default: false)` .option('--manifest [name]', `[boolean | string] emit build manifest json`)
).option( .option('--ssrManifest [name]', `[boolean | string] emit ssr manifest json`)
"--minify [minifier]", .option('--emptyOutDir', `[boolean] force empty outDir when it's outside of root`)
`[boolean | "terser" | "esbuild"] enable/disable minification, or specify minifier to use (default: esbuild)` .option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`)
).option("--manifest [name]", `[boolean | string] emit build manifest json`).option("--ssrManifest [name]", `[boolean | string] emit ssr manifest json`).option( .action(async (root, options) => {
"--emptyOutDir", filterDuplicateOptions(options);
`[boolean] force empty outDir when it's outside of root` const { build } = await import('./chunks/dep-p3C6MpSJ.js').then(function (n) { return n.F; });
).option("-w, --watch", `[boolean] rebuilds when modules have changed on disk`).action(async (root, options) => { const buildOptions = cleanOptions(options);
filterDuplicateOptions(options); try {
const { build } = await import('./chunks/dep-BK3b2jBa.js').then(function (n) { return n.G; }); await build({
const buildOptions = cleanOptions(options); root,
try { base: options.base,
await build({ mode: options.mode,
root, configFile: options.config,
base: options.base, logLevel: options.logLevel,
mode: options.mode, clearScreen: options.clearScreen,
configFile: options.config, build: buildOptions,
logLevel: options.logLevel, });
clearScreen: options.clearScreen, }
build: buildOptions catch (e) {
}); createLogger(options.logLevel).error(colors.red(`error during build:\n${e.stack}`), { error: e });
} catch (e) { process.exit(1);
createLogger(options.logLevel).error( }
colors.red(`error during build: finally {
${e.stack}`), stopProfiler((message) => createLogger(options.logLevel).info(message));
{ error: e } }
);
process.exit(1);
} finally {
stopProfiler((message) => createLogger(options.logLevel).info(message));
}
}); });
cli.command("optimize [root]", "pre-bundle dependencies").option( // optimize
"--force", cli
`[boolean] force the optimizer to ignore the cache and re-bundle` .command('optimize [root]', 'pre-bundle dependencies')
).action( .option('--force', `[boolean] force the optimizer to ignore the cache and re-bundle`)
async (root, options) => { .action(async (root, options) => {
filterDuplicateOptions(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 { try {
const config = await resolveConfig( const config = await resolveConfig({
{ root,
root, base: options.base,
base: options.base, configFile: options.config,
configFile: options.config, logLevel: options.logLevel,
logLevel: options.logLevel, mode: options.mode,
mode: options.mode }, 'serve');
}, await optimizeDeps(config, options.force, true);
"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);
} }
} catch (e) {
); createLogger(options.logLevel).error(colors.red(`error when optimizing deps:\n${e.stack}`), { error: e });
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( process.exit(1);
async (root, options) => { }
});
// 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); 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 { try {
const server = await preview({ const server = await preview({
root, root,
base: options.base, base: options.base,
configFile: options.config, configFile: options.config,
logLevel: options.logLevel, logLevel: options.logLevel,
mode: options.mode, mode: options.mode,
build: { build: {
outDir: options.outDir outDir: options.outDir,
}, },
preview: { preview: {
port: options.port, port: options.port,
strictPort: options.strictPort, strictPort: options.strictPort,
host: options.host, host: options.host,
open: options.open open: options.open,
} },
}); });
server.printUrls(); server.printUrls();
server.bindCLIShortcuts({ print: true }); 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));
} }
} 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.help();
cli.version(VERSION); cli.version(VERSION);
cli.parse(); cli.parse();

View File

@ -2,112 +2,114 @@ import path, { resolve } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
const { version } = JSON.parse( const { version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url)).toString());
readFileSync(new URL("../../package.json", import.meta.url)).toString()
);
const VERSION = version; const VERSION = version;
const DEFAULT_MAIN_FIELDS = [ const DEFAULT_MAIN_FIELDS = [
"browser", 'browser',
"module", 'module',
"jsnext:main", 'jsnext:main',
// moment still uses this... 'jsnext',
"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 = [ const ESBUILD_MODULES_TARGET = [
"es2020", 'es2020',
// support import.meta.url 'edge88',
"edge88", 'firefox78',
"firefox78", 'chrome87',
"chrome87", 'safari14',
"safari14"
]; ];
const DEFAULT_EXTENSIONS = [ const DEFAULT_EXTENSIONS = [
".mjs", '.mjs',
".js", '.js',
".mts", '.mts',
".ts", '.ts',
".jsx", '.jsx',
".tsx", '.tsx',
".json" '.json',
]; ];
const DEFAULT_CONFIG_FILES = [ const DEFAULT_CONFIG_FILES = [
"vite.config.js", 'vite.config.js',
"vite.config.mjs", 'vite.config.mjs',
"vite.config.ts", 'vite.config.ts',
"vite.config.cjs", 'vite.config.cjs',
"vite.config.mts", 'vite.config.mts',
"vite.config.cts" 'vite.config.cts',
]; ];
const JS_TYPES_RE = /\.(?:j|t)sx?$|\.mjs$/; const JS_TYPES_RE = /\.(?:j|t)sx?$|\.mjs$/;
const CSS_LANGS_RE = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/; const CSS_LANGS_RE = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/;
const OPTIMIZABLE_ENTRY_RE = /\.[cm]?[jt]s$/; const OPTIMIZABLE_ENTRY_RE = /\.[cm]?[jt]s$/;
const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/; 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 FS_PREFIX = `/@fs/`;
const CLIENT_PUBLIC_PATH = `/@vite/client`; const CLIENT_PUBLIC_PATH = `/@vite/client`;
const ENV_PUBLIC_PATH = `/@vite/env`; const ENV_PUBLIC_PATH = `/@vite/env`;
const VITE_PACKAGE_DIR = resolve( const VITE_PACKAGE_DIR = resolve(
// import.meta.url is `dist/node/constants.js` after bundle // import.meta.url is `dist/node/constants.js` after bundle
fileURLToPath(import.meta.url), 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_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); 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 = [ const KNOWN_ASSET_TYPES = [
// images // images
"apng", 'apng',
"bmp", 'png',
"png", 'jpe?g',
"jpe?g", 'jfif',
"jfif", 'pjpeg',
"pjpeg", 'pjp',
"pjp", 'gif',
"gif", 'svg',
"svg", 'ico',
"ico", 'webp',
"webp", 'avif',
"avif", // media
// media 'mp4',
"mp4", 'webm',
"webm", 'ogg',
"ogg", 'mp3',
"mp3", 'wav',
"wav", 'flac',
"flac", 'aac',
"aac", 'opus',
"opus", 'mov',
"mov", 'm4a',
"m4a", 'vtt',
"vtt", // fonts
// fonts 'woff2?',
"woff2?", 'eot',
"eot", 'ttf',
"ttf", 'otf',
"otf", // other
// other 'webmanifest',
"webmanifest", 'pdf',
"pdf", 'txt',
"txt"
]; ];
const DEFAULT_ASSETS_RE = new RegExp( const DEFAULT_ASSETS_RE = new RegExp(`\\.(` + KNOWN_ASSET_TYPES.join('|') + `)(\\?.*)?$`);
`\\.(` + KNOWN_ASSET_TYPES.join("|") + `)(\\?.*)?$`
);
const DEP_VERSION_RE = /[?&](v=[\w.-]+)\b/; const DEP_VERSION_RE = /[?&](v=[\w.-]+)\b/;
const loopbackHosts = /* @__PURE__ */ new Set([ const loopbackHosts = new Set([
"localhost", 'localhost',
"127.0.0.1", '127.0.0.1',
"::1", '::1',
"0000:0000:0000:0000:0000:0000:0000:0001" '0000:0000:0000:0000:0000:0000:0000:0001',
]); ]);
const wildcardHosts = /* @__PURE__ */ new Set([ const wildcardHosts = new Set([
"0.0.0.0", '0.0.0.0',
"::", '::',
"0000:0000:0000:0000:0000:0000:0000:0000" '0000:0000:0000:0000:0000:0000:0000:0000',
]); ]);
const DEFAULT_DEV_PORT = 5173; const DEFAULT_DEV_PORT = 5173;
const DEFAULT_PREVIEW_PORT = 4173; const DEFAULT_PREVIEW_PORT = 4173;
const DEFAULT_ASSETS_INLINE_LIMIT = 4096; 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 };

View File

@ -1,5 +1,5 @@
/// <reference types="node" /> /// <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'; import * as rollup from 'rollup';
export { rollup as Rollup }; export { rollup as Rollup };
export { parseAst, parseAstAsync } from 'rollup/parseAst'; export { parseAst, parseAstAsync } from 'rollup/parseAst';
@ -414,7 +414,6 @@ declare namespace HttpProxy {
* @param req - Client request. * @param req - Client request.
* @param res - Client response. * @param res - Client response.
* @param options - Additional options. * @param options - Additional options.
* @param callback - Error callback.
*/ */
web( web(
req: http.IncomingMessage, req: http.IncomingMessage,
@ -429,7 +428,6 @@ declare namespace HttpProxy {
* @param socket - Client socket. * @param socket - Client socket.
* @param head - Client head. * @param head - Client head.
* @param options - Additional options. * @param options - Additional options.
* @param callback - Error callback.
*/ */
ws( ws(
req: http.IncomingMessage, req: http.IncomingMessage,
@ -620,12 +618,6 @@ interface ProxyOptions extends HttpProxy.ServerOptions {
* webpack-dev-server style bypass function * webpack-dev-server style bypass function
*/ */
bypass?: (req: http.IncomingMessage, res: http.ServerResponse, options: ProxyOptions) => void | null | undefined | false | string; 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'; 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. * Set to 0.0.0.0 to listen on all addresses, including LAN and public addresses.
*/ */
host?: string | boolean; 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. * Enable TLS + HTTP/2.
* Note: this downgrades to TLS only when the proxy option is also used. * 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. * Configure CORS for the dev server.
* Uses https://github.com/expressjs/cors. * 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 * Set to `true` to allow all methods from any origin, or configure separately
* using an object. * using an object.
*
* @default false
*/ */
cors?: CorsOptions | boolean; cors?: CorsOptions | boolean;
/** /**
@ -735,13 +709,7 @@ interface CommonServerOptions {
* https://github.com/expressjs/cors#configuration-options * https://github.com/expressjs/cors#configuration-options
*/ */
interface CorsOptions { interface CorsOptions {
/** origin?: CorsOrigin | ((origin: string, cb: (err: Error, origins: CorsOrigin) => void) => void);
* 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);
methods?: string | string[]; methods?: string | string[];
allowedHeaders?: string | string[]; allowedHeaders?: string | string[];
exposedHeaders?: 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 * https://github.com/preactjs/wmr/blob/main/packages/wmr/src/lib/rollup-plugin-container.js
*/ */
declare class PluginContainer { interface PluginContainer {
config: ResolvedConfig; options: InputOptions;
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;
getModuleInfo(id: string): ModuleInfo | null; getModuleInfo(id: string): ModuleInfo | null;
private handleHookPromise; buildStart(options: InputOptions): Promise<void>;
get options(): InputOptions; resolveId(id: string, importer?: string, options?: {
resolveRollupOptions(): Promise<InputOptions>;
private _getPluginContext;
private hookParallel;
buildStart(_options?: InputOptions): Promise<void>;
resolveId(rawId: string, importer?: string | undefined, options?: {
attributes?: Record<string, string>; attributes?: Record<string, string>;
custom?: CustomPluginOptions; custom?: CustomPluginOptions;
skip?: Set<Plugin>; skip?: Set<Plugin>;
ssr?: boolean; ssr?: boolean;
isEntry?: boolean; isEntry?: boolean;
}): Promise<PartialResolvedId | null>; }): Promise<PartialResolvedId | null>;
load(id: string, options?: {
ssr?: boolean;
}): Promise<LoadResult | null>;
transform(code: string, id: string, options?: { transform(code: string, id: string, options?: {
ssr?: boolean;
inMap?: SourceDescription['map']; inMap?: SourceDescription['map'];
ssr?: boolean;
}): Promise<{ }): Promise<{
code: string; code: string;
map: SourceMap | { map: SourceMap | {
mappings: ''; mappings: '';
} | null; } | null;
}>; }>;
load(id: string, options?: {
ssr?: boolean;
}): Promise<LoadResult | null>;
watchChange(id: string, change: { watchChange(id: string, change: {
event: 'create' | 'update' | 'delete'; event: 'create' | 'update' | 'delete';
}): Promise<void>; }): Promise<void>;
@ -960,7 +907,6 @@ declare class PluginContainer {
declare const WebSocketAlias: typeof WebSocket declare const WebSocketAlias: typeof WebSocket
interface WebSocketAlias extends WebSocket {} interface WebSocketAlias extends WebSocket {}
// WebSocket socket. // WebSocket socket.
declare class WebSocket extends EventEmitter { declare class WebSocket extends EventEmitter {
/** The connection is not yet open. */ /** The connection is not yet open. */
@ -1229,6 +1175,7 @@ declare class WebSocket extends EventEmitter {
listener: (...args: any[]) => void, listener: (...args: any[]) => void,
): this ): this
} }
// tslint:disable-line no-empty-interface
declare namespace WebSocket { declare namespace WebSocket {
/** /**
@ -1478,9 +1425,9 @@ declare namespace WebSocket {
} }
const WebSocketServer: typeof Server const WebSocketServer: typeof Server
interface WebSocketServer extends Server {} interface WebSocketServer extends Server {} // tslint:disable-line no-empty-interface
const WebSocket: typeof WebSocketAlias const WebSocket: typeof WebSocketAlias
interface WebSocket extends WebSocketAlias {} interface WebSocket extends WebSocketAlias {} // tslint:disable-line no-empty-interface
// WebSocket stream // WebSocket stream
function createWebSocketStream( function createWebSocketStream(
@ -1497,7 +1444,7 @@ interface HmrOptions {
path?: string; path?: string;
timeout?: number; timeout?: number;
overlay?: boolean; overlay?: boolean;
server?: HttpServer; server?: Server;
} }
interface HmrContext { interface HmrContext {
file: string; file: string;
@ -1613,11 +1560,6 @@ interface ServerOptions extends CommonServerOptions {
* Configure HMR-specific options (port, host, path & protocol) * Configure HMR-specific options (port, host, path & protocol)
*/ */
hmr?: HmrOptions | boolean; 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 * Warm-up files to transform and cache the results in advance. This improves the
* initial page load during server starts and prevents transform waterfalls. * 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. * This is needed to proxy WebSocket connections to the parent server.
*/ */
server: HttpServer; server: http.Server;
}; };
/** /**
* Options for files served via '/\@fs/'. * Options for files served via '/\@fs/'.
@ -2122,9 +2064,6 @@ interface RollupDynamicImportVarsOptions {
declare namespace Terser { declare namespace Terser {
export type ECMA = 5 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 export type ECMA = 5 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020
export type ConsoleProperty = keyof typeof console
type DropConsoleOption = boolean | ConsoleProperty[]
export interface ParseOptions { export interface ParseOptions {
bare_returns?: boolean bare_returns?: boolean
/** @deprecated legacy option. Currently, all supported EcmaScript is valid to parse. */ /** @deprecated legacy option. Currently, all supported EcmaScript is valid to parse. */
@ -2145,7 +2084,7 @@ declare namespace Terser {
dead_code?: boolean dead_code?: boolean
defaults?: boolean defaults?: boolean
directives?: boolean directives?: boolean
drop_console?: DropConsoleOption drop_console?: boolean
drop_debugger?: boolean drop_debugger?: boolean
ecma?: ECMA ecma?: ECMA
evaluate?: boolean evaluate?: boolean
@ -2168,7 +2107,6 @@ declare namespace Terser {
passes?: number passes?: number
properties?: boolean properties?: boolean
pure_funcs?: string[] pure_funcs?: string[]
pure_new?: boolean
pure_getters?: boolean | 'strict' pure_getters?: boolean | 'strict'
reduce_funcs?: boolean reduce_funcs?: boolean
reduce_vars?: boolean reduce_vars?: boolean
@ -2218,7 +2156,7 @@ declare namespace Terser {
* Obtains the nth most favored (usually shortest) identifier to rename a variable to. * 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. * 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. * 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 get(n: number): string
} }
@ -2230,8 +2168,8 @@ declare namespace Terser {
/** /**
* Modifies the internal weighting of the input characters by the specified delta. * 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. * Will be invoked on the entire printed AST, and then deduct mangleable identifiers.
* @param chars The characters to modify the weighting of. * @param chars - The characters to modify the weighting of.
* @param delta The numeric weight to add to the characters. * @param delta - The numeric weight to add to the characters.
*/ */
consider(chars: string, delta: number): number consider(chars: string, delta: number): number
/** /**
@ -2314,7 +2252,7 @@ declare namespace Terser {
module?: boolean module?: boolean
nameCache?: object nameCache?: object
format?: FormatOptions format?: FormatOptions
/** @deprecated */ /** @deprecated deprecated */
output?: FormatOptions output?: FormatOptions
parse?: ParseOptions parse?: ParseOptions
safari10?: boolean safari10?: boolean
@ -2334,7 +2272,6 @@ declare namespace Terser {
includeSources?: boolean includeSources?: boolean
filename?: string filename?: string
root?: string root?: string
asObject?: boolean
url?: string | 'inline' url?: string | 'inline'
} }
} }
@ -2556,7 +2493,7 @@ interface LibraryOptions {
*/ */
fileName?: string | ((format: ModuleFormat, entryName: string) => string); fileName?: string | ((format: ModuleFormat, entryName: string) => string);
} }
type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife' | 'system'; type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife';
interface ModulePreloadOptions { interface ModulePreloadOptions {
/** /**
* Whether to inject a module preload polyfill. * 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 * 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 * 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 * 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 * defined ones in `include`, it is better to disable this option to let the
* browser process more requests in parallel. * browser process more requests in parallel.
* @default true * @default true
@ -2923,7 +2860,6 @@ type LightningCSSOptions = {
pseudoClasses?: PseudoClasses pseudoClasses?: PseudoClasses
unusedSymbols?: string[] unusedSymbols?: string[]
cssModules?: CSSModulesConfig cssModules?: CSSModulesConfig
errorRecovery?: boolean
} }
interface CSSOptions { interface CSSOptions {
@ -3426,18 +3362,6 @@ interface LegacyOptions {
* https://github.com/vitejs/vite/discussions/14697. * https://github.com/vitejs/vite/discussions/14697.
*/ */
proxySsrExternalModules?: boolean; 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 { interface ResolvedWorkerOptions {
format: 'es' | 'iife'; format: 'es' | 'iife';
@ -3479,17 +3403,6 @@ type ResolvedConfig = Readonly<Omit<UserConfig, 'plugins' | 'css' | 'assetsInclu
worker: ResolvedWorkerOptions; worker: ResolvedWorkerOptions;
appType: AppType; appType: AppType;
experimental: ExperimentalOptions; 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>; } & PluginHookUtils>;
interface PluginHookUtils { interface PluginHookUtils {
getSortedPlugins: <K extends keyof Plugin>(hookName: K) => PluginWithRequiredHook<K>[]; 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. * 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 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 loadEnv(mode: string, envDir: string, prefixes?: string | string[]): Record<string, string>;
declare function resolveEnvPrefix({ envPrefix, }: UserConfig): string[]; declare function resolveEnvPrefix({ envPrefix, }: UserConfig): string[];
@ -3624,4 +3535,4 @@ declare class ServerHMRConnector implements HMRRuntimeConnection {
onUpdate(handler: (payload: HMRPayload) => void): void; 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 };

391
node_modules/vite/dist/node/index.js generated vendored
View File

@ -1,6 +1,6 @@
export { parseAst, parseAstAsync } from 'rollup/parseAst'; export { parseAst, parseAstAsync } from 'rollup/parseAst';
import { i as isInNodeModules, a as arraify } 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, 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'; 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 version } from './constants.js';
export { version as esbuildVersion } from 'esbuild'; export { version as esbuildVersion } from 'esbuild';
import { existsSync, readFileSync } from 'node:fs'; import { existsSync, readFileSync } from 'node:fs';
@ -11,25 +11,23 @@ import 'node:url';
import 'node:util'; import 'node:util';
import 'node:perf_hooks'; import 'node:perf_hooks';
import 'node:module'; import 'node:module';
import 'node:crypto';
import 'tty'; import 'tty';
import 'path'; import 'path';
import 'fs'; import 'fs';
import 'node:events'; import 'events';
import 'node:stream'; import 'assert';
import 'node:string_decoder';
import 'node:child_process'; import 'node:child_process';
import 'node:http'; import 'node:http';
import 'node:https'; import 'node:https';
import 'util'; import 'util';
import 'net'; import 'net';
import 'events';
import 'url'; import 'url';
import 'http'; import 'http';
import 'stream'; import 'stream';
import 'os'; import 'os';
import 'child_process'; import 'child_process';
import 'node:os'; import 'node:os';
import 'node:crypto';
import 'node:dns'; import 'node:dns';
import 'crypto'; import 'crypto';
import 'module'; import 'module';
@ -37,225 +35,238 @@ import 'node:assert';
import 'node:v8'; import 'node:v8';
import 'node:worker_threads'; import 'node:worker_threads';
import 'node:buffer'; import 'node:buffer';
import 'node:events';
import 'querystring'; import 'querystring';
import 'node:readline'; import 'node:readline';
import 'zlib'; import 'zlib';
import 'buffer'; import 'buffer';
import 'https'; import 'https';
import 'tls'; import 'tls';
import 'node:net';
import 'assert';
import 'node:zlib'; import 'node:zlib';
const CSS_LANGS_RE = ( // This file will be built for both ESM and CJS. Avoid relying on other modules as possible.
// eslint-disable-next-line regexp/no-unused-capturing-group // copy from constants.ts
/\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/ 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); 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 { class SplitVendorChunkCache {
cache; cache;
constructor() { constructor() {
this.cache = /* @__PURE__ */ new Map(); this.cache = 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";
} }
}; 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 = []) { function staticImportedByEntry(id, getModuleInfo, cache, importStack = []) {
if (cache.has(id)) { if (cache.has(id)) {
return cache.get(id); return cache.get(id);
} }
if (importStack.includes(id)) { if (importStack.includes(id)) {
cache.set(id, false); // circular deps!
return false; cache.set(id, false);
} return false;
const mod = getModuleInfo(id); }
if (!mod) { const mod = getModuleInfo(id);
cache.set(id, false); if (!mod) {
return false; cache.set(id, false);
} return false;
if (mod.isEntry) { }
cache.set(id, true); if (mod.isEntry) {
return true; cache.set(id, true);
} return true;
const someImporterIs = mod.importers.some( }
(importer) => staticImportedByEntry( const someImporterIs = mod.importers.some((importer) => staticImportedByEntry(importer, getModuleInfo, cache, importStack.concat(id)));
importer, cache.set(id, someImporterIs);
getModuleInfo, return someImporterIs;
cache,
importStack.concat(id)
)
);
cache.set(id, someImporterIs);
return someImporterIs;
} }
/**
* @deprecated use build.rollupOptions.output.manualChunks or framework specific configuration
*/
function splitVendorChunkPlugin() { function splitVendorChunkPlugin() {
const caches = []; const caches = [];
function createSplitVendorChunk(output, config) { function createSplitVendorChunk(output, config) {
const cache = new SplitVendorChunkCache(); const cache = new SplitVendorChunkCache();
caches.push(cache); caches.push(cache);
const build = config.build ?? {}; const build = config.build ?? {};
const format = output?.format; const format = output?.format;
if (!build.ssr && !build.lib && format !== "umd" && format !== "iife") { if (!build.ssr && !build.lib && format !== 'umd' && format !== 'iife') {
return splitVendorChunk({ cache }); 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;
}
}
} }
} 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 { class ServerHMRBroadcasterClient {
constructor(hmrChannel) { hmrChannel;
this.hmrChannel = hmrChannel; 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];
} }
if (payload.type !== "custom") { send(...args) {
throw new Error( let payload;
"Cannot send non-custom events from the client to the server." 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 { class ServerHMRConnector {
handlers = []; handlers = [];
hmrChannel; hmrChannel;
hmrClient; hmrClient;
connected = false; connected = false;
constructor(server) { constructor(server) {
const hmrChannel = server.hot?.channels.find( const hmrChannel = server.hot?.channels.find((c) => c.name === 'ssr');
(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.");
if (!hmrChannel) { }
throw new Error( this.hmrClient = new ServerHMRBroadcasterClient(hmrChannel);
"Your version of Vite doesn't support HMR during SSR. Please, use Vite 5.1 or higher." 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) { function createHMROptions(server, options) {
if (server.config.server.hmr === false || options.hmr === false) { if (server.config.server.hmr === false || options.hmr === false) {
return false; return false;
} }
const connection = new ServerHMRConnector(server); const connection = new ServerHMRConnector(server);
return { return {
connection, connection,
logger: options.hmr?.logger logger: options.hmr?.logger,
}; };
} }
const prepareStackTrace = { const prepareStackTrace = {
retrieveFile(id) { retrieveFile(id) {
if (existsSync(id)) { if (existsSync(id)) {
return readFileSync(id, "utf-8"); return readFileSync(id, 'utf-8');
} }
} },
}; };
function resolveSourceMapOptions(options) { function resolveSourceMapOptions(options) {
if (options.sourcemapInterceptor != null) { if (options.sourcemapInterceptor != null) {
if (options.sourcemapInterceptor === "prepareStackTrace") { if (options.sourcemapInterceptor === 'prepareStackTrace') {
return prepareStackTrace; return prepareStackTrace;
}
if (typeof options.sourcemapInterceptor === 'object') {
return { ...prepareStackTrace, ...options.sourcemapInterceptor };
}
return options.sourcemapInterceptor;
} }
if (typeof options.sourcemapInterceptor === "object") { if (typeof process !== 'undefined' && 'setSourceMapsEnabled' in process) {
return { ...prepareStackTrace, ...options.sourcemapInterceptor }; return 'node';
} }
return options.sourcemapInterceptor; return prepareStackTrace;
}
if (typeof process !== "undefined" && "setSourceMapsEnabled" in process) {
return "node";
}
return prepareStackTrace;
} }
/**
* Create an instance of the Vite SSR runtime that support HMR.
* @experimental
*/
async function createViteRuntime(server, options = {}) { async function createViteRuntime(server, options = {}) {
const hmr = createHMROptions(server, options); const hmr = createHMROptions(server, options);
return new ViteRuntime( return new ViteRuntime({
{ ...options,
...options, root: server.config.root,
root: server.config.root, fetchModule: server.ssrFetchModule,
fetchModule: server.ssrFetchModule, hmr,
hmr, sourcemapInterceptor: resolveSourceMapOptions(options),
sourcemapInterceptor: resolveSourceMapOptions(options) }, options.runner || new ESModulesRunner());
},
options.runner || new ESModulesRunner()
);
} }
export { ServerHMRConnector, createViteRuntime, isCSSRequest, splitVendorChunk, splitVendorChunkPlugin }; export { ServerHMRConnector, createViteRuntime, isCSSRequest, splitVendorChunk, splitVendorChunkPlugin };

View File

@ -51,22 +51,24 @@ function normalizeString(path, allowAboveRoot) {
char = "/"; char = "/";
} }
if (char === "/") { if (char === "/") {
if (!(lastSlash === index - 1 || dots === 1)) if (dots === 2) { if (!(lastSlash === index - 1 || dots === 1))
if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") { if (dots === 2) {
if (res.length > 2) { if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") {
const lastSlashIndex = res.lastIndexOf("/"); if (res.length > 2) {
lastSlashIndex === -1 ? (res = "", lastSegmentLength = 0) : (res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/")), lastSlash = index, dots = 0; const lastSlashIndex = res.lastIndexOf("/");
continue; lastSlashIndex === -1 ? (res = "", lastSegmentLength = 0) : (res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/")), lastSlash = index, dots = 0;
} else if (res.length > 0) { continue;
res = "", lastSegmentLength = 0, lastSlash = index, dots = 0; } else if (res.length > 0) {
continue; res = "", lastSegmentLength = 0, lastSlash = index, dots = 0;
continue;
}
} }
} allowAboveRoot && (res += res.length > 0 ? "/.." : "..", lastSegmentLength = 2);
allowAboveRoot && (res += res.length > 0 ? "/.." : "..", lastSegmentLength = 2); } else
} else res.length > 0 ? res += `/${path.slice(lastSlash + 1, index)}` : res = path.slice(lastSlash + 1, index), lastSegmentLength = index - lastSlash - 1;
res.length > 0 ? res += `/${path.slice(lastSlash + 1, index)}` : res = path.slice(lastSlash + 1, index), lastSegmentLength = index - lastSlash - 1;
lastSlash = index, dots = 0; lastSlash = index, dots = 0;
} else char === "." && dots !== -1 ? ++dots : dots = -1; } else
char === "." && dots !== -1 ? ++dots : dots = -1;
} }
return res; return res;
} }
@ -94,46 +96,38 @@ for (let i = 0; i < chars.length; i++) {
const c = chars.charCodeAt(i); const c = chars.charCodeAt(i);
intToChar[i] = c, charToInt[c] = 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; let value = 0, shift = 0, integer = 0;
do { do {
const c = reader.next(); const c = mappings.charCodeAt(pos++);
integer = charToInt[c], value |= (integer & 31) << shift, shift += 5; integer = charToInt[c], value |= (integer & 31) << shift, shift += 5;
} while (integer & 32); } while (integer & 32);
const shouldNegate = value & 1; 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) { function hasMoreVlq(mappings, i, length) {
return reader.pos >= max ? !1 : reader.peek() !== comma; return i >= length ? !1 : mappings.charCodeAt(i) !== 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 sort(line) { function sort(line) {
line.sort(sortComparator); 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; 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 { class DecodedMap {
constructor(map, from) { map;
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)
);
}
_encoded; _encoded;
_decoded; _decoded;
_decodedMemo; _decodedMemo;
@ -220,6 +208,11 @@ class DecodedMap {
version; version;
names = []; names = [];
resolvedSources; 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() { function memoizedState() {
return { return {
@ -232,9 +225,7 @@ function getOriginalPosition(map, needle) {
const result = originalPositionFor(map, needle); const result = originalPositionFor(map, needle);
return result.column == null ? null : result; return result.column == null ? null : result;
} }
const VITE_RUNTIME_SOURCEMAPPING_REGEXP = new RegExp( const VITE_RUNTIME_SOURCEMAPPING_REGEXP = new RegExp(`//# ${SOURCEMAPPING_URL}=data:application/json;base64,(.+)`);
`//# ${SOURCEMAPPING_URL}=data:application/json;base64,(.+)`
);
class ModuleCacheMap extends Map { class ModuleCacheMap extends Map {
root; root;
constructor(root, entries) { constructor(root, entries) {
@ -276,16 +267,17 @@ class ModuleCacheMap extends Map {
const module = this.get(id); 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(); module.evaluated = !1, module.meta = void 0, module.map = void 0, module.promise = void 0, module.exports = void 0, module.imports?.clear();
} }
isImported({ isImported({ importedId, importedBy }, seen = /* @__PURE__ */ new Set()) {
importedId, if (importedId = this.normalize(importedId), importedBy = this.normalize(importedBy), importedBy === importedId)
importedBy return !0;
}, seen = /* @__PURE__ */ new Set()) { if (seen.has(importedId))
if (importedId = this.normalize(importedId), importedBy = this.normalize(importedBy), importedBy === importedId) return !0; return !1;
if (seen.has(importedId)) return !1;
seen.add(importedId); seen.add(importedId);
const importers = this.getByModuleId(importedId)?.importers; const importers = this.getByModuleId(importedId)?.importers;
if (!importers) return !1; if (!importers)
if (importers.has(importedBy)) return !0; return !1;
if (importers.has(importedBy))
return !0;
for (const importer of importers) for (const importer of importers)
if (this.isImported({ if (this.isImported({
importedBy, importedBy,
@ -300,7 +292,8 @@ class ModuleCacheMap extends Map {
invalidateDepTree(ids, invalidated = /* @__PURE__ */ new Set()) { invalidateDepTree(ids, invalidated = /* @__PURE__ */ new Set()) {
for (const _id of ids) { for (const _id of ids) {
const id = this.normalize(_id); const id = this.normalize(_id);
if (invalidated.has(id)) continue; if (invalidated.has(id))
continue;
invalidated.add(id); invalidated.add(id);
const mod = super.get(id); const mod = super.get(id);
mod?.importers && this.invalidateDepTree(mod.importers, invalidated), super.delete(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()) { invalidateSubDepTree(ids, invalidated = /* @__PURE__ */ new Set()) {
for (const _id of ids) { for (const _id of ids) {
const id = this.normalize(_id); const id = this.normalize(_id);
if (invalidated.has(id)) continue; if (invalidated.has(id))
continue;
invalidated.add(id); invalidated.add(id);
const subIds = Array.from(super.entries()).filter(([, mod]) => mod.importers?.has(id)).map(([key]) => key); const subIds = Array.from(super.entries()).filter(([, mod]) => mod.importers?.has(id)).map(([key]) => key);
subIds.length && this.invalidateSubDepTree(subIds, invalidated), super.delete(id); subIds.length && this.invalidateSubDepTree(subIds, invalidated), super.delete(id);
@ -322,21 +316,28 @@ class ModuleCacheMap extends Map {
} }
getSourceMap(moduleId) { getSourceMap(moduleId) {
const mod = this.get(moduleId); const mod = this.get(moduleId);
if (mod.map) return mod.map; if (mod.map)
if (!mod.meta || !("code" in mod.meta)) return null; return mod.map;
const mapString = VITE_RUNTIME_SOURCEMAPPING_REGEXP.exec(mod.meta.code)?.[1]; if (!mod.meta || !("code" in mod.meta))
if (!mapString) return null; 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]; const baseFile = mod.meta.file || moduleId.split("?")[0];
return mod.map = new DecodedMap(JSON.parse(decodeBase64(mapString)), baseFile), mod.map; return mod.map = new DecodedMap(JSON.parse(decodeBase64(mapString)), baseFile), mod.map;
} }
} }
const prefixedBuiltins = /* @__PURE__ */ new Set(["node:test"]); const prefixedBuiltins = /* @__PURE__ */ new Set(["node:test"]);
function normalizeModuleId(file, root) { 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(/^\/+/, "/"); let unixFile = slash(file).replace(/^\/@fs\//, isWindows ? "" : "/").replace(/^node:/, "").replace(/^\/+/, "/");
return unixFile.startsWith(root) && (unixFile = unixFile.slice(root.length - 1)), unixFile.replace(/^file:\//, "/"); return unixFile.startsWith(root) && (unixFile = unixFile.slice(root.length - 1)), unixFile.replace(/^file:\//, "/");
} }
class HMRContext { class HMRContext {
hmrClient;
ownerPath;
newListeners;
constructor(hmrClient, ownerPath) { constructor(hmrClient, ownerPath) {
this.hmrClient = hmrClient, this.ownerPath = ownerPath, hmrClient.dataMap.has(ownerPath) || hmrClient.dataMap.set(ownerPath, {}); this.hmrClient = hmrClient, this.ownerPath = ownerPath, hmrClient.dataMap.has(ownerPath) || hmrClient.dataMap.set(ownerPath, {});
const mod = hmrClient.hotModulesMap.get(ownerPath); const mod = hmrClient.hotModulesMap.get(ownerPath);
@ -345,14 +346,10 @@ class HMRContext {
if (staleListeners) if (staleListeners)
for (const [event, staleFns] of staleListeners) { for (const [event, staleFns] of staleListeners) {
const listeners = hmrClient.customListenersMap.get(event); const listeners = hmrClient.customListenersMap.get(event);
listeners && hmrClient.customListenersMap.set( listeners && hmrClient.customListenersMap.set(event, listeners.filter((l) => !staleFns.includes(l)));
event,
listeners.filter((l) => !staleFns.includes(l))
);
} }
this.newListeners = /* @__PURE__ */ new Map(), hmrClient.ctxToListenersMap.set(ownerPath, this.newListeners); this.newListeners = /* @__PURE__ */ new Map(), hmrClient.ctxToListenersMap.set(ownerPath, this.newListeners);
} }
newListeners;
get data() { get data() {
return this.hmrClient.dataMap.get(this.ownerPath); return this.hmrClient.dataMap.get(this.ownerPath);
} }
@ -385,9 +382,7 @@ class HMRContext {
this.hmrClient.notifyListeners("vite:invalidate", { this.hmrClient.notifyListeners("vite:invalidate", {
path: this.ownerPath, path: this.ownerPath,
message message
}), this.send("vite:invalidate", { path: this.ownerPath, message }), this.hmrClient.logger.debug( }), this.send("vite:invalidate", { path: this.ownerPath, message }), this.hmrClient.logger.debug(`[vite] invalidate ${this.ownerPath}${message ? `: ${message}` : ""}`);
`[vite] invalidate ${this.ownerPath}${message ? `: ${message}` : ""}`
);
} }
on(event, cb) { on(event, cb) {
const addToMap = (map) => { const addToMap = (map) => {
@ -411,9 +406,7 @@ class HMRContext {
removeFromMap(this.hmrClient.customListenersMap), removeFromMap(this.newListeners); removeFromMap(this.hmrClient.customListenersMap), removeFromMap(this.newListeners);
} }
send(event, data) { send(event, data) {
this.hmrClient.messenger.send( this.hmrClient.messenger.send(JSON.stringify({ type: "custom", event, data }));
JSON.stringify({ type: "custom", event, data })
);
} }
acceptDeps(deps, callback = () => { acceptDeps(deps, callback = () => {
}) { }) {
@ -428,6 +421,7 @@ class HMRContext {
} }
} }
class HMRMessenger { class HMRMessenger {
connection;
constructor(connection) { constructor(connection) {
this.connection = connection; this.connection = connection;
} }
@ -440,9 +434,8 @@ class HMRMessenger {
} }
} }
class HMRClient { class HMRClient {
constructor(logger, connection, importUpdatedModule) { logger;
this.logger = logger, this.importUpdatedModule = importUpdatedModule, this.messenger = new HMRMessenger(connection); importUpdatedModule;
}
hotModulesMap = /* @__PURE__ */ new Map(); hotModulesMap = /* @__PURE__ */ new Map();
disposeMap = /* @__PURE__ */ new Map(); disposeMap = /* @__PURE__ */ new Map();
pruneMap = /* @__PURE__ */ new Map(); pruneMap = /* @__PURE__ */ new Map();
@ -450,6 +443,9 @@ class HMRClient {
customListenersMap = /* @__PURE__ */ new Map(); customListenersMap = /* @__PURE__ */ new Map();
ctxToListenersMap = /* @__PURE__ */ new Map(); ctxToListenersMap = /* @__PURE__ */ new Map();
messenger; messenger;
constructor(logger, connection, importUpdatedModule) {
this.logger = logger, this.importUpdatedModule = importUpdatedModule, this.messenger = new HMRMessenger(connection);
}
async notifyListeners(event, data) { async notifyListeners(event, data) {
const cbs = this.customListenersMap.get(event); const cbs = this.customListenersMap.get(event);
cbs && await Promise.allSettled(cbs.map((cb) => cb(data))); 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 // but they may have left behind side effects that need to be cleaned up
// (.e.g style injections) // (.e.g style injections)
async prunePaths(paths) { async prunePaths(paths) {
await Promise.all( await Promise.all(paths.map((path) => {
paths.map((path) => { const disposer = this.disposeMap.get(path);
const disposer = this.disposeMap.get(path); if (disposer)
if (disposer) return disposer(this.dataMap.get(path)); return disposer(this.dataMap.get(path));
}) })), paths.forEach((path) => {
), paths.forEach((path) => {
const fn = this.pruneMap.get(path); const fn = this.pruneMap.get(path);
fn && fn(this.dataMap.get(path)); fn && fn(this.dataMap.get(path));
}); });
} }
warnFailedUpdate(err, path) { warnFailedUpdate(err, path) {
err.message.includes("fetch") || this.logger.error(err), this.logger.error( 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)`);
`[hmr] Failed to reload ${path}. This could be due to syntax errors or importing non-existent modules. (see errors above)`
);
} }
updateQueue = []; updateQueue = [];
pendingUpdateQueue = !1; pendingUpdateQueue = !1;
@ -495,9 +488,7 @@ class HMRClient {
if (!mod) if (!mod)
return; return;
let fetchedModule; let fetchedModule;
const isSelfUpdate = path === acceptedPath, qualifiedCallbacks = mod.callbacks.filter( const isSelfUpdate = path === acceptedPath, qualifiedCallbacks = mod.callbacks.filter(({ deps }) => deps.includes(acceptedPath));
({ deps }) => deps.includes(acceptedPath)
);
if (isSelfUpdate || qualifiedCallbacks.length > 0) { if (isSelfUpdate || qualifiedCallbacks.length > 0) {
const disposer = this.disposeMap.get(acceptedPath); const disposer = this.disposeMap.get(acceptedPath);
disposer && await disposer(this.dataMap.get(acceptedPath)); disposer && await disposer(this.dataMap.get(acceptedPath));
@ -509,22 +500,18 @@ class HMRClient {
} }
return () => { return () => {
for (const { deps, fn } of qualifiedCallbacks) for (const { deps, fn } of qualifiedCallbacks)
fn( fn(deps.map((dep) => dep === acceptedPath ? fetchedModule : void 0));
deps.map((dep) => dep === acceptedPath ? fetchedModule : void 0)
);
const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`; const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`;
this.logger.debug(`[vite] hot updated: ${loggedPath}`); this.logger.debug(`[vite] hot updated: ${loggedPath}`);
}; };
} }
} }
function analyzeImportedModDifference(mod, rawId, moduleType, metadata) { 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)); const missingBindings = metadata.importedNames.filter((s) => !(s in mod));
if (missingBindings.length) { if (missingBindings.length) {
const lastBinding = missingBindings[missingBindings.length - 1]; const lastBinding = missingBindings[missingBindings.length - 1];
throw moduleType === "module" ? new SyntaxError( 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.
`[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.
CommonJS modules can always be imported via the default export, for example using: CommonJS modules can always be imported via the default export, for example using:
import pkg from '${rawId}'; 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 = () => { 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 = { }, silentConsole = {
debug: noop, debug: noop,
@ -550,28 +546,23 @@ async function handleHMRPayload(runtime, payload) {
hmrClient.logger.debug("[vite] connected."), hmrClient.messenger.flush(); hmrClient.logger.debug("[vite] connected."), hmrClient.messenger.flush();
break; break;
case "update": case "update":
await hmrClient.notifyListeners("vite:beforeUpdate", payload), await Promise.all( await hmrClient.notifyListeners("vite:beforeUpdate", payload), await Promise.all(payload.updates.map(async (update) => {
payload.updates.map(async (update) => { if (update.type === "js-update")
if (update.type === "js-update") return update.acceptedPath = unwrapId(update.acceptedPath), update.path = unwrapId(update.path), hmrClient.queueUpdate(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.");
hmrClient.logger.error( })), await hmrClient.notifyListeners("vite:afterUpdate", payload);
"[vite] css hmr is not supported in runtime mode."
);
})
), await hmrClient.notifyListeners("vite:afterUpdate", payload);
break; break;
case "custom": { case "custom": {
await hmrClient.notifyListeners(payload.event, payload.data); await hmrClient.notifyListeners(payload.event, payload.data);
break; break;
} }
case "full-reload": { case "full-reload": {
const { triggeredBy } = payload, clearEntrypoints = triggeredBy ? [...runtime.entrypoints].filter( const { triggeredBy } = payload, clearEntrypoints = triggeredBy ? [...runtime.entrypoints].filter((entrypoint) => runtime.moduleCache.isImported({
(entrypoint) => runtime.moduleCache.isImported({ importedId: triggeredBy,
importedId: triggeredBy, importedBy: entrypoint
importedBy: entrypoint })) : [...runtime.entrypoints];
}) if (!clearEntrypoints.length)
) : [...runtime.entrypoints]; break;
if (!clearEntrypoints.length) break;
hmrClient.logger.debug("[vite] program reload"), await hmrClient.notifyListeners("vite:beforeFullReload", payload), runtime.moduleCache.clear(); hmrClient.logger.debug("[vite] program reload"), await hmrClient.notifyListeners("vite:beforeFullReload", payload), runtime.moduleCache.clear();
for (const id of clearEntrypoints) for (const id of clearEntrypoints)
await runtime.executeUrl(id); await runtime.executeUrl(id);
@ -583,11 +574,9 @@ async function handleHMRPayload(runtime, payload) {
case "error": { case "error": {
await hmrClient.notifyListeners("vite:error", payload); await hmrClient.notifyListeners("vite:error", payload);
const err = payload.err; const err = payload.err;
hmrClient.logger.error( hmrClient.logger.error(`[vite] Internal Server Error
`[vite] Internal Server Error
${err.message} ${err.message}
${err.stack}` ${err.stack}`);
);
break; break;
} }
default: 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) => { const sourceMapCache = {}, fileContentsCache = {}, moduleGraphs = /* @__PURE__ */ new Set(), retrieveFileHandlers = /* @__PURE__ */ new Set(), retrieveSourceMapHandlers = /* @__PURE__ */ new Set(), createExecHandlers = (handlers) => (...args) => {
for (const handler of handlers) { for (const handler of handlers) {
const result = handler(...args); const result = handler(...args);
if (result) return result; if (result)
return result;
} }
return null; return null;
}, retrieveFileFromHandlers = createExecHandlers(retrieveFileHandlers), retrieveSourceMapFromHandlers = createExecHandlers( }, retrieveFileFromHandlers = createExecHandlers(retrieveFileHandlers), retrieveSourceMapFromHandlers = createExecHandlers(retrieveSourceMapHandlers);
retrieveSourceMapHandlers
);
let overridden = !1; let overridden = !1;
const originalPrepare = Error.prepareStackTrace; const originalPrepare = Error.prepareStackTrace;
function resetInterceptor(runtime, options) { 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); 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) { function supportRelativeURL(file, url) {
if (!file) return url; if (!file)
return url;
const dir = posixDirname(slash(file)), match = /^\w+:\/\/[^/]*/.exec(dir); const dir = posixDirname(slash(file)), match = /^\w+:\/\/[^/]*/.exec(dir);
let protocol = match ? match[0] : ""; let protocol = match ? match[0] : "";
const startPath = dir.slice(protocol.length); const startPath = dir.slice(protocol.length);
@ -652,24 +641,29 @@ function getRuntimeSourceMap(position) {
return null; return null;
} }
function retrieveFile(path) { function retrieveFile(path) {
if (path in fileContentsCache) return fileContentsCache[path]; if (path in fileContentsCache)
return fileContentsCache[path];
const content = retrieveFileFromHandlers(path); const content = retrieveFileFromHandlers(path);
return typeof content == "string" ? (fileContentsCache[path] = content, content) : null; return typeof content == "string" ? (fileContentsCache[path] = content, content) : null;
} }
function retrieveSourceMapURL(source) { function retrieveSourceMapURL(source) {
const fileData = retrieveFile(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; const re = /\/\/[@#]\s*sourceMappingURL=([^\s'"]+)\s*$|\/\*[@#]\s*sourceMappingURL=[^\s*'"]+\s*\*\/\s*$/gm;
let lastMatch, match; let lastMatch, match;
for (; match = re.exec(fileData); ) lastMatch = match; for (; match = re.exec(fileData); )
lastMatch = match;
return lastMatch ? lastMatch[1] : null; return lastMatch ? lastMatch[1] : null;
} }
const reSourceMap = /^data:application\/json[^,]+base64,/; const reSourceMap = /^data:application\/json[^,]+base64,/;
function retrieveSourceMap(source) { function retrieveSourceMap(source) {
const urlAndMap = retrieveSourceMapFromHandlers(source); const urlAndMap = retrieveSourceMapFromHandlers(source);
if (urlAndMap) return urlAndMap; if (urlAndMap)
return urlAndMap;
let sourceMappingURL = retrieveSourceMapURL(source); let sourceMappingURL = retrieveSourceMapURL(source);
if (!sourceMappingURL) return null; if (!sourceMappingURL)
return null;
let sourceMapData; let sourceMapData;
if (reSourceMap.test(sourceMappingURL)) { if (reSourceMap.test(sourceMappingURL)) {
const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(",") + 1); const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(",") + 1);
@ -682,7 +676,8 @@ function retrieveSourceMap(source) {
} : null; } : null;
} }
function mapSourcePosition(position) { function mapSourcePosition(position) {
if (!position.source) return position; if (!position.source)
return position;
let sourceMap = getRuntimeSourceMap(position); let sourceMap = getRuntimeSourceMap(position);
if (sourceMap || (sourceMap = sourceMapCache[position.source]), !sourceMap) { if (sourceMap || (sourceMap = sourceMapCache[position.source]), !sourceMap) {
const urlAndMap = retrieveSourceMap(position.source); const urlAndMap = retrieveSourceMap(position.source);
@ -690,10 +685,7 @@ function mapSourcePosition(position) {
const url = urlAndMap.url; const url = urlAndMap.url;
sourceMap = sourceMapCache[position.source] = { sourceMap = sourceMapCache[position.source] = {
url, url,
map: new DecodedMap( map: new DecodedMap(typeof urlAndMap.map == "string" ? JSON.parse(urlAndMap.map) : urlAndMap.map, url)
typeof urlAndMap.map == "string" ? JSON.parse(urlAndMap.map) : urlAndMap.map,
url
)
}; };
const contents = sourceMap.map?.map.sourcesContent; const contents = sourceMap.map?.map.sourcesContent;
sourceMap.map && contents && sourceMap.map.resolvedSources.forEach((source, i) => { sourceMap.map && contents && sourceMap.map.resolvedSources.forEach((source, i) => {
@ -712,10 +704,7 @@ function mapSourcePosition(position) {
if (sourceMap && sourceMap.map && sourceMap.url) { if (sourceMap && sourceMap.map && sourceMap.url) {
const originalPosition = getOriginalPosition(sourceMap.map, position); const originalPosition = getOriginalPosition(sourceMap.map, position);
if (originalPosition && originalPosition.source != null) if (originalPosition && originalPosition.source != null)
return originalPosition.source = supportRelativeURL( return originalPosition.source = supportRelativeURL(sourceMap.url, originalPosition.source), sourceMap.vite && (originalPosition._vite = !0), originalPosition;
sourceMap.url,
originalPosition.source
), sourceMap.vite && (originalPosition._vite = !0), originalPosition;
} }
return position; return position;
} }
@ -749,14 +738,13 @@ function CallSiteToString() {
const functionName = this.getFunctionName(); const functionName = this.getFunctionName();
let addSuffix = !0; let addSuffix = !0;
const isConstructor = this.isConstructor(); const isConstructor = this.isConstructor();
if (this.isToplevel() || isConstructor) if (!(this.isToplevel() || isConstructor)) {
isConstructor ? line += `new ${functionName || "<anonymous>"}` : functionName ? line += functionName : (line += fileLocation, addSuffix = !1);
else {
let typeName = this.getTypeName(); let typeName = this.getTypeName();
typeName === "[object Object]" && (typeName = "null"); typeName === "[object Object]" && (typeName = "null");
const methodName = this.getMethodName(); 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>"}`; 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; return addSuffix && (line += ` (${fileLocation})`), line;
} }
function cloneCallSite(frame) { function cloneCallSite(frame) {
@ -813,29 +801,18 @@ function prepareStackTrace(error, stack) {
function enableSourceMapSupport(runtime) { function enableSourceMapSupport(runtime) {
if (runtime.options.sourcemapInterceptor === "node") { if (runtime.options.sourcemapInterceptor === "node") {
if (typeof process > "u") if (typeof process > "u")
throw new TypeError( throw new TypeError(`Cannot use "sourcemapInterceptor: 'node'" because global "process" variable is not available.`);
`Cannot use "sourcemapInterceptor: 'node'" because global "process" variable is not available.`
);
if (typeof process.setSourceMapsEnabled != "function") if (typeof process.setSourceMapsEnabled != "function")
throw new TypeError( throw new TypeError(`Cannot use "sourcemapInterceptor: 'node'" because "process.setSourceMapsEnabled" function is not available. Please use Node >= 16.6.0.`);
`Cannot use "sourcemapInterceptor: 'node'" because "process.setSourceMapsEnabled" function is not available. Please use Node >= 16.6.0.`
);
const isEnabledAlready = process.sourceMapsEnabled ?? !1; const isEnabledAlready = process.sourceMapsEnabled ?? !1;
return process.setSourceMapsEnabled(!0), () => !isEnabledAlready && process.setSourceMapsEnabled(!1); return process.setSourceMapsEnabled(!0), () => !isEnabledAlready && process.setSourceMapsEnabled(!1);
} }
return interceptStackTrace( return interceptStackTrace(runtime, typeof runtime.options.sourcemapInterceptor == "object" ? runtime.options.sourcemapInterceptor : void 0);
runtime,
typeof runtime.options.sourcemapInterceptor == "object" ? runtime.options.sourcemapInterceptor : void 0
);
} }
class ViteRuntime { class ViteRuntime {
constructor(options, runner, debug) { options;
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( runner;
options.hmr.logger === !1 ? silentConsole : options.hmr.logger || console, debug;
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));
}
/** /**
* Holds the cache of modules * Holds the cache of modules
* Keys of the map are ids * Keys of the map are ids
@ -847,13 +824,14 @@ class ViteRuntime {
fileToIdMap = /* @__PURE__ */ new Map(); fileToIdMap = /* @__PURE__ */ new Map();
envProxy = new Proxy({}, { envProxy = new Proxy({}, {
get(_, p) { get(_, p) {
throw new Error( throw new Error(`[vite-runtime] Dynamic access of "import.meta.env" is not supported. Please, use "import.meta.env.${String(p)}" instead.`);
`[vite-runtime] Dynamic access of "import.meta.env" is not supported. Please, use "import.meta.env.${String(p)}" instead.`
);
} }
}); });
_destroyed = !1; _destroyed = !1;
_resetSourceMapSupport; _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. * URL to execute. Accepts file path, server path or id relative to the root.
*/ */
@ -913,7 +891,7 @@ class ViteRuntime {
if (!("externalize" in fetchResult)) if (!("externalize" in fetchResult))
return exports; return exports;
const { id, type } = fetchResult; 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) { async cachedRequest(id, fetchedModule, callstack = [], metadata) {
const moduleId = fetchedModule.id; const moduleId = fetchedModule.id;
@ -926,10 +904,8 @@ class ViteRuntime {
const getStack = () => `stack: const getStack = () => `stack:
${[...callstack, moduleId].reverse().map((p) => ` - ${p}`).join(` ${[...callstack, moduleId].reverse().map((p) => ` - ${p}`).join(`
`)}`; `)}`;
this.debug( this.debug(`[vite-runtime] module ${moduleId} takes over 2s to load.
`[vite-runtime] module ${moduleId} takes over 2s to load. ${getStack()}`);
${getStack()}`
);
}, 2e3)); }, 2e3));
try { try {
if (mod.promise) if (mod.promise)
@ -972,9 +948,7 @@ ${getStack()}`
const { code, file } = fetchResult; const { code, file } = fetchResult;
if (code == null) { if (code == null) {
const importer = callstack[callstack.length - 2]; const importer = callstack[callstack.length - 2];
throw new Error( throw new Error(`[vite-runtime] Failed to load "${id}"${importer ? ` imported from ${importer}` : ""}`);
`[vite-runtime] Failed to load "${id}"${importer ? ` imported from ${importer}` : ""}`
);
} }
const modulePath = cleanUrl(file || moduleId), href = posixPathToFileHref(modulePath), filename = modulePath, dirname2 = posixDirname(modulePath), meta = { const modulePath = cleanUrl(file || moduleId), href = posixPathToFileHref(modulePath), filename = modulePath, dirname2 = posixDirname(modulePath), meta = {
filename: isWindows ? toWindowsPath(filename) : filename, filename: isWindows ? toWindowsPath(filename) : filename,
@ -982,9 +956,7 @@ ${getStack()}`
url: href, url: href,
env: this.envProxy, env: this.envProxy,
resolve(id2, parent) { resolve(id2, parent) {
throw new Error( throw new Error('[vite-runtime] "import.meta.resolve" is not supported.');
'[vite-runtime] "import.meta.resolve" is not supported.'
);
}, },
// should be replaced during transformation // should be replaced during transformation
glob() { glob() {
@ -1042,13 +1014,7 @@ class ESModulesRunner {
ssrExportAllKey, ssrExportAllKey,
// source map should already be inlined by Vite // source map should already be inlined by Vite
'"use strict";' + code '"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) { runExternalModule(filepath) {
return import(filepath); return import(filepath);

34
node_modules/vite/index.cjs generated vendored
View File

@ -1,9 +1,12 @@
/* eslint-disable no-restricted-globals */
warnCjsUsage() warnCjsUsage()
// type utils // type utils
module.exports.defineConfig = (config) => config module.exports.defineConfig = (config) => config
// proxy cjs utils (sync functions) // 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')) Object.assign(module.exports, require('./dist/node-cjs/publicUtils.cjs'))
// async functions, can be redirect from ESM build // async functions, can be redirect from ESM build
@ -25,36 +28,11 @@ asyncFunctions.forEach((name) => {
function warnCjsUsage() { function warnCjsUsage() {
if (process.env.VITE_CJS_IGNORE_WARNING) return 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` const yellow = (str) => `\u001b[33m${str}\u001b[39m`
console.warn( const log = process.env.VITE_CJS_TRACE ? console.trace : console.warn
log(
yellow( 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'),
)
}
} }

2
node_modules/vite/index.d.cts generated vendored
View File

@ -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 declare const module: any

54
node_modules/vite/package.json generated vendored
View File

@ -1,6 +1,6 @@
{ {
"name": "vite", "name": "vite",
"version": "5.4.21", "version": "5.2.14",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"author": "Evan You", "author": "Evan You",
@ -68,30 +68,33 @@
"bugs": { "bugs": {
"url": "https://github.com/vitejs/vite/issues" "url": "https://github.com/vitejs/vite/issues"
}, },
"homepage": "https://vite.dev", "homepage": "https://vitejs.dev",
"funding": "https://github.com/vitejs/vite?sponsor=1", "funding": "https://github.com/vitejs/vite?sponsor=1",
"//": "READ CONTRIBUTING.md to understand what to put under deps vs. devDeps!", "//": "READ CONTRIBUTING.md to understand what to put under deps vs. devDeps!",
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.20.1",
"postcss": "^8.4.43", "postcss": "^8.4.38",
"rollup": "^4.20.0" "rollup": "^4.13.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"fsevents": "~2.3.3" "fsevents": "~2.3.3"
}, },
"devDependencies": { "devDependencies": {
"@ampproject/remapping": "^2.3.0", "@ampproject/remapping": "^2.3.0",
"@babel/parser": "^7.25.6", "@babel/parser": "^7.24.6",
"@jridgewell/trace-mapping": "^0.3.25", "@jridgewell/trace-mapping": "^0.3.25",
"@polka/compression": "^1.0.0-next.25", "@polka/compression": "^1.0.0-next.25",
"@rollup/plugin-alias": "^5.1.0", "@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-dynamic-import-vars": "^2.1.2",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "15.2.3", "@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@rollup/pluginutils": "^5.1.0", "@rollup/pluginutils": "^5.1.0",
"@types/escape-html": "^1.0.4", "@types/escape-html": "^1.0.4",
"@types/pnpapi": "^0.0.5", "@types/pnpapi": "^0.0.5",
"acorn": "^8.11.3",
"acorn-walk": "^8.3.2",
"artichokie": "^0.2.1", "artichokie": "^0.2.1",
"cac": "^6.7.14", "cac": "^6.7.14",
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
@ -99,21 +102,21 @@
"convert-source-map": "^2.0.0", "convert-source-map": "^2.0.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"debug": "^4.3.6", "debug": "^4.3.4",
"dep-types": "link:./src/types", "dep-types": "link:./src/types",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"dotenv-expand": "^11.0.6", "dotenv-expand": "^11.0.6",
"es-module-lexer": "^1.5.4", "es-module-lexer": "^1.5.3",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"estree-walker": "^3.0.3", "estree-walker": "^3.0.3",
"etag": "^1.8.1", "etag": "^1.8.1",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"launch-editor-middleware": "^2.9.1", "launch-editor-middleware": "^2.6.1",
"lightningcss": "^1.26.0", "lightningcss": "^1.25.1",
"magic-string": "^0.30.11", "magic-string": "^0.30.10",
"micromatch": "^4.0.8", "micromatch": "^4.0.7",
"mlly": "^1.7.1", "mlly": "^1.7.0",
"mrmime": "^2.0.0", "mrmime": "^2.0.0",
"open": "^8.4.2", "open": "^8.4.2",
"parse5": "^7.1.2", "parse5": "^7.1.2",
@ -127,25 +130,23 @@
"resolve.exports": "^2.0.2", "resolve.exports": "^2.0.2",
"rollup-plugin-dts": "^6.1.1", "rollup-plugin-dts": "^6.1.1",
"rollup-plugin-esbuild": "^6.1.1", "rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-license": "^3.5.2", "rollup-plugin-license": "^3.4.0",
"sass": "^1.77.8", "sass": "^1.77.2",
"sass-embedded": "^1.77.8",
"sirv": "^2.0.4", "sirv": "^2.0.4",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"strip-ansi": "^7.1.0", "strip-ansi": "^7.1.0",
"strip-literal": "^2.1.0", "strip-literal": "^2.1.0",
"tsconfck": "^3.1.4", "tsconfck": "^3.0.3",
"tslib": "^2.7.0", "tslib": "^2.6.2",
"types": "link:./types", "types": "link:./types",
"ufo": "^1.5.4", "ufo": "^1.5.3",
"ws": "^8.18.0" "ws": "^8.17.0"
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0", "@types/node": "^18.0.0 || >=20.0.0",
"less": "*", "less": "*",
"lightningcss": "^1.21.0", "lightningcss": "^1.21.0",
"sass": "*", "sass": "*",
"sass-embedded": "*",
"stylus": "*", "stylus": "*",
"sugarss": "*", "sugarss": "*",
"terser": "^5.4.0" "terser": "^5.4.0"
@ -157,9 +158,6 @@
"sass": { "sass": {
"optional": true "optional": true
}, },
"sass-embedded": {
"optional": true
},
"stylus": { "stylus": {
"optional": true "optional": true
}, },
@ -177,12 +175,12 @@
} }
}, },
"scripts": { "scripts": {
"dev": "tsx scripts/dev.ts", "dev": "rimraf dist && pnpm run build-bundle -w",
"build": "rimraf dist && run-s build-bundle build-types", "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": "run-s build-types-temp build-types-roll build-types-check",
"build-types-temp": "tsc --emitDeclarationOnly --outDir temp -p src/node", "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", "build-types-check": "tsc --project tsconfig.check.json",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "eslint --cache --ext .ts src/**", "lint": "eslint --cache --ext .ts src/**",

26
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"axios": "^1.7.0", "axios": "^1.7.0",
"echarts": "^6.0.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.0", "vue": "^3.4.0",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
@ -1552,6 +1553,16 @@
"node": ">= 0.4" "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": { "node_modules/entities": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
@ -2529,6 +2540,12 @@
"node": ">=14.0.0" "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": { "node_modules/type-detect": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz", "resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz",
@ -2882,6 +2899,15 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "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"
}
} }
} }
} }

View File

@ -5,7 +5,7 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"dev:simple": "node dev-server.mjs", "dev:simple": "node dev-server.mjs",
"build": "vue-tsc -b && vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest", "test": "vitest",
"test:e2e": "playwright test" "test:e2e": "playwright test"
@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"axios": "^1.7.0", "axios": "^1.7.0",
"echarts": "^6.0.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.0", "vue": "^3.4.0",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"

57
src/api/audit.ts Normal file
View File

@ -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'
})
}

View File

@ -2,17 +2,17 @@ import request from '@/utils/request'
import type { LoginRequest, LoginResponse } from '@/types' import type { LoginRequest, LoginResponse } from '@/types'
export const login = (data: LoginRequest) => { export const login = (data: LoginRequest) => {
return request.post<LoginResponse>('/auth/login', data) return request.post<LoginResponse>('/api/auth/login', data)
} }
export const logout = () => { export const logout = () => {
return request.post('/auth/logout') return request.post('/api/auth/logout')
} }
export const getCurrentUser = () => { export const getCurrentUser = () => {
return request.get('/auth/me') return request.get('/api/auth/me')
} }
export const refreshToken = () => { export const refreshToken = () => {
return request.post('/auth/refresh') return request.post('/api/auth/refresh')
} }

93
src/api/energy.ts Normal file
View File

@ -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 }
})
}

106
src/api/equipment-health.ts Normal file
View File

@ -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}`)
}

77
src/api/equipment.ts Normal file
View File

@ -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 }
})
}

View File

@ -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}`)
}

156
src/api/maintenance.ts Normal file
View File

@ -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`
})
}

View File

@ -1,26 +1,121 @@
import request from '@/utils/request' import request from '@/utils/request'
import type { Project } from '@/types' 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 = () => { export const getProjects = () => {
return request.get<Project[]>('/projects') return request.get<Project[]>('/api/mdm/projects/all')
} }
// 获取项目详情
export const getProject = (id: string) => { export const getProject = (id: string) => {
return request.get<Project>(`/projects/${id}`) return request.get<Project>(`/api/mdm/projects/${id}`)
} }
// 根据编码获取项目
export const getProjectByCode = (code: string) => { 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>) => { 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>) => { 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) => { 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 })
} }

View File

@ -1,5 +1,5 @@
import request from '@/utils/request' import request from '@/utils/request'
import type { Role } from '@/types' import type { Role, Permission } from '@/types'
export const getRoles = () => { export const getRoles = () => {
return request.get<Role[]>('/api/roles') return request.get<Role[]>('/api/roles')
@ -9,6 +9,10 @@ export const getRole = (id: string) => {
return request.get<Role>(`/api/roles/${id}`) 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) => { export const getRolesByProject = (projectId: string) => {
return request.get<Role[]>(`/api/roles/project/${projectId}`) return request.get<Role[]>(`/api/roles/project/${projectId}`)
} }
@ -36,3 +40,7 @@ export const getUserRoles = (userId: string) => {
export const removeRoleFromUser = (userId: string, roleId: string) => { export const removeRoleFromUser = (userId: string, roleId: string) => {
return request.delete(`/api/users/${userId}/roles/${roleId}`) return request.delete(`/api/users/${userId}/roles/${roleId}`)
} }
export const getRoleUsers = (roleId: string) => {
return request.get(`/api/roles/${roleId}/users`)
}

38
src/api/space.ts Normal file
View File

@ -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}`)
}

148
src/api/sparepart.ts Normal file
View File

@ -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`)
}

16
src/api/system.ts Normal file
View File

@ -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
})
}

View File

@ -2,12 +2,18 @@
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
interface Props { interface Props {
title: string title?: string
showBack?: boolean showBack?: boolean
actions?: { icon: string; text: string; onClick?: () => void }[] actions?: { icon: string; text: string; onClick?: () => void }[]
} }
defineProps<Props>() const props = withDefaults(defineProps<Props>(), {
title: ''
})
defineEmits<{
(e: 'back'): void
}>()
const router = useRouter() 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"/> <path d="M12.5 15L7.5 10L12.5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
</button> </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>
<div v-if="actions && actions.length > 0" class="page-header-actions"> <div v-if="actions && actions.length > 0" class="page-header-actions">
<button <button
@ -36,6 +45,7 @@ const handleBack = () => {
{{ action.text }} {{ action.text }}
</button> </button>
</div> </div>
<slot name="actions"></slot>
</div> </div>
</template> </template>

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
interface Props { interface Props {
current?: number current?: number
@ -34,16 +35,18 @@ const handleChange = (page: number, size: number) => {
<template> <template>
<div class="table-pagination"> <div class="table-pagination">
<a-pagination <a-config-provider :locale="zhCN">
v-model:current="current" <a-pagination
:total="total" v-model:current="current"
:page-size="pageSize" :total="total"
:show-size-changer="true" :page-size="pageSize"
:show-quick-jumper="true" :show-size-changer="true"
:page-size-options="pageSizes.map(String)" :show-quick-jumper="true"
:show-total="(total: number) => `共 ${total} 条`" :page-size-options="pageSizes.map(String)"
@change="handleChange" :show-total="(total: number) => `共 ${total} 条`"
/> @change="handleChange"
/>
</a-config-provider>
</div> </div>
</template> </template>

View File

@ -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>

View File

@ -1,18 +1,20 @@
<script setup lang="ts"> <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 { interface Props {
status: string status: string
map?: Record<string, { color: string; label: string }> map?: Record<string, { color: string; label: string; icon?: string }>
defaultColor?: string defaultColor?: string
defaultLabel?: string defaultLabel?: string
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
map: () => ({ map: () => ({
ACTIVE: { color: 'success', label: '正常' }, ACTIVE: { color: 'success', label: '正常', icon: 'check' },
LOCKED: { color: 'warning', label: '锁定' }, ENABLED: { color: 'success', label: '启用', icon: 'check' },
DISABLED: { color: 'error', label: '禁用' } LOCKED: { color: 'warning', label: '锁定', icon: 'warning' },
DISABLED: { color: 'error', label: '禁用', icon: 'close' }
}), }),
defaultColor: 'default', defaultColor: 'default',
defaultLabel: '' defaultLabel: ''
@ -33,25 +35,43 @@ const bgMap: Record<string, string> = {
} }
const currentStatus = computed(() => { 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 tagColor = computed(() => colorMap[currentStatus.value.color] || colorMap.default)
const tagBg = computed(() => bgMap[currentStatus.value.color] || bgMap.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> </script>
<template> <template>
<span class="status-tag" :style="{ color: tagColor, backgroundColor: tagBg }"> <span class="status-tag" :style="{ color: tagColor, backgroundColor: tagBg }">
<component :is="renderIcon" />
{{ currentStatus.label }} {{ currentStatus.label }}
</span> </span>
</template> </template>
<style scoped> <style scoped>
.status-tag { .status-tag {
display: inline-block; display: inline-flex;
align-items: center;
padding: 2px 8px; padding: 2px 8px;
font-size: 12px; font-size: 12px;
border-radius: 4px; border-radius: 4px;
white-space: nowrap; white-space: nowrap;
line-height: 1.5;
} }
</style> </style>

View File

@ -1,92 +1,201 @@
<script setup lang="ts"> <script setup lang="ts">
import { EditOutlined, DeleteOutlined } from '@ant-design/icons-vue' import { EllipsisOutlined } from '@ant-design/icons-vue'
import { Popconfirm } from 'ant-design-vue' import { Dropdown, Menu, Popconfirm } from 'ant-design-vue'
import { computed } from 'vue'
interface ActionItem { interface ActionItem {
key: string key: string
label: string label: string
icon?: any
danger?: boolean danger?: boolean
} }
interface Props { interface Props {
actions?: ActionItem[] actions?: ActionItem[]
showView?: boolean
showEdit?: boolean showEdit?: boolean
showDelete?: boolean showDelete?: boolean
viewText?: string
editText?: string editText?: string
deleteText?: string deleteText?: string
deleteTitle?: string deleteTitle?: string
deleteDescription?: 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<{ const emit = defineEmits<{
(e: 'edit'): void (e: 'edit'): void
(e: 'delete'): void (e: 'delete'): void
(e: 'view'): void
(e: 'action', key: string): void (e: 'action', key: string): void
}>() }>()
const handleAction = (key: string) => { // + actions +
emit('action', key) 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
})
// 332
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> </script>
<template> <template>
<div class="table-actions"> <span class="table-actions">
<!-- 自定义操作 --> <!-- 可见按钮 -->
<template v-for="action in actions" :key="action.key"> <template v-for="action in visibleActions" :key="action.key">
<a-button <!-- 删除按钮需要 Popconfirm -->
v-if="action.danger" <Popconfirm
type="link" v-if="action.key === 'delete'"
danger :title="deleteTitle"
size="small" :description="deleteDescription"
@click="handleAction(action.key)" ok-text="确认"
cancel-text="取消"
@confirm="handleDeleteConfirm"
> >
<component v-if="action.icon" :is="action.icon" /> <a-button type="link" danger size="small" class="table-action-btn">
{{ action.label }} {{ action.label }}
</a-button> </a-button>
</Popconfirm>
<!-- 普通按钮 -->
<a-button <a-button
v-else v-else
type="link" type="link"
size="small" size="small"
@click="handleAction(action.key)" class="table-action-btn"
@click="handleFixedClick(action.key)"
> >
<component v-if="action.icon" :is="action.icon" />
{{ action.label }} {{ action.label }}
</a-button> </a-button>
</template> </template>
<!-- 编辑按钮 --> <!-- 更多操作下拉菜单 -->
<a-button <Dropdown v-if="hasMoreActions" placement="bottomRight" :overlay-style="{ minWidth: '80px' }">
v-if="showEdit" <a-button type="link" size="small" class="table-action-btn more-btn">
type="link" <EllipsisOutlined />
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 }}
</a-button> </a-button>
</Popconfirm> <template #overlay>
</div> <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> </template>
<style scoped> <style scoped>
.table-actions { .table-actions {
display: flex; display: inline-flex;
align-items: center; 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> </style>

View File

@ -63,3 +63,6 @@ export { default as PasswordItem } from './PasswordItem/index.vue'
// 业务组件 - 详情 // 业务组件 - 详情
export { default as DescriptionList } from './DescriptionList/index.vue' export { default as DescriptionList } from './DescriptionList/index.vue'
export { default as ProfileCard } from './ProfileCard/index.vue' export { default as ProfileCard } from './ProfileCard/index.vue'
// 业务组件 - 空间
export { default as SpaceTree } from './SpaceTree/index.vue'

View File

@ -45,11 +45,107 @@ const router = createRouter({
component: () => import('@/views/system/Audit.vue'), component: () => import('@/views/system/Audit.vue'),
meta: { title: '审计日志' } meta: { title: '审计日志' }
}, },
{
path: 'system/settings',
name: 'Settings',
component: () => import('@/views/system/Settings.vue'),
meta: { title: '系统设置' }
},
{ {
path: 'project/list', path: 'project/list',
name: 'ProjectList', name: 'ProjectList',
component: () => import('@/views/project/List.vue'), component: () => import('@/views/project/List.vue'),
meta: { title: '项目管理' } 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: '备件出库' }
} }
] ]
} }

View File

@ -148,27 +148,58 @@ interface Props {
</template> </template>
``` ```
#### TableActions 行操作 #### TableActions 行操作(统一组件)
**使用方式:**
```vue ```vue
<template> <!-- 基础用法:编辑 + 删除 -->
<div class="table-actions"> <TableActions @edit="handleEdit(record)" @delete="handleDelete(record.id)" />
<a-button type="link" size="small" @click="handleEdit(record)">
<EditOutlined /> 编辑 <!-- 带查看按钮 -->
</a-button> <TableActions show-view @view="handleView(record)" @edit="handleEdit(record)" @delete="handleDelete(record.id)" />
<a-popconfirm
title="确认删除?" <!-- 自定义操作 -->
ok-text="确认" <TableActions :actions="[{ key: 'export', label: '导出', danger: false }]" @action="handleAction" />
cancel-text="取消"
@confirm="handleDelete(record)"
>
<a-button type="link" danger size="small">
<DeleteOutlined /> 删除
</a-button>
</a-popconfirm>
</div>
</template>
``` ```
**属性说明:**
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| 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 详情组件 ### 4. Detail 详情组件
@ -804,10 +835,12 @@ const handleChange = (value: string) => {
#### TableActions 表格行操作 #### TableActions 表格行操作
| 状态 | 功能 | 说明 | | 状态 | 功能 | 说明 |
|------|------|------| |------|------|------|
| ✅ 已支持 | 查看按钮 | showView |
| ✅ 已支持 | 编辑按钮 | showEdit | | ✅ 已支持 | 编辑按钮 | showEdit |
| ✅ 已支持 | 删除按钮 | showDelete | | ✅ 已支持 | 删除按钮 | showDelete |
| ✅ 已支持 | 自定义操作 | actions prop | | ✅ 已支持 | 自定义操作 | actions prop |
| ✅ 已支持 | 删除确认 | Popconfirm | | ✅ 已支持 | 删除确认 | Popconfirm |
| ✅ 已支持 | 按钮文本配置 | viewText/editText/deleteText |
| 🔲 待开发 | 更多操作 | more-actions dropdown | | 🔲 待开发 | 更多操作 | more-actions dropdown |
| 🔲 待开发 | 成功反馈 | success-message | | 🔲 待开发 | 成功反馈 | success-message |
| 🔲 待开发 | 二次确认配置 | confirm-title/description | | 🔲 待开发 | 二次确认配置 | confirm-title/description |

View File

@ -55,23 +55,17 @@ export interface Project {
name: string name: string
description?: string description?: string
address?: string address?: string
projectType?: 'RESIDENTIAL' | 'OFFICE' | 'INDUSTRIAL_PARK'
province?: string province?: string
city?: string city?: string
district?: string district?: string
status: 'ACTIVE' | 'DISABLED' status: 'ACTIVE' | 'DISABLED' | 'PENDING' | 'ARCHIVED'
createdAt?: string
updatedAt?: string
} }
export interface SpaceNode { // 导出项目相关类型
id: string export * from './project'
code: string
name: string // 导出空间相关类型
projectCode: string export * from './space'
nodeType: string
parentCode?: string
building?: string
unit?: string
floor?: string
roomNumber?: string
area?: number
status: 'ACTIVE' | 'DISABLED'
}

125
src/types/project.ts Normal file
View File

@ -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
}

141
src/types/space.ts Normal file
View File

@ -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
}

View File

@ -2,7 +2,7 @@ import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'ax
import type { ApiResponse } from '@/types' import type { ApiResponse } from '@/types'
const request: AxiosInstance = axios.create({ 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 timeout: 10000
}) })

View File

@ -15,17 +15,55 @@ import {
UserOutlined UserOutlined
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
import { Col, Row } from 'ant-design-vue' import { Col, Row } from 'ant-design-vue'
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const router = useRouter() const router = useRouter()
const stats = [ const stats = [
{ label: '用户总数', value: '1,286', change: '+12.5%', up: true, icon: UserOutlined }, { label: '用户总数', value: 1286, change: '+12.5%', up: true, icon: UserOutlined },
{ label: '角色总数', value: '8', change: '-', up: true, icon: TeamOutlined }, { label: '角色总数', value: 8, change: '-', up: true, icon: TeamOutlined },
{ label: '项目总数', value: '24', change: '+8.3%', up: true, icon: ProjectOutlined }, { label: '项目总数', value: 24, change: '+8.3%', up: true, icon: ProjectOutlined },
{ label: '空间节点', value: '156', change: '-2.1%', up: false, icon: ApartmentOutlined } { 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 = [ const todos = [
{ title: '待处理工单', count: 12 }, { title: '待处理工单', count: 12 },
{ title: '待审核报修', count: 5 }, { title: '待审核报修', count: 5 },
@ -48,6 +86,38 @@ const notices = [
] ]
const chartData = [65, 78, 52, 91, 68, 85, 73] 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> </script>
<template> <template>
@ -60,13 +130,15 @@ const chartData = [65, 78, 52, 91, 68, 85, 73]
<!-- 统计卡片 --> <!-- 统计卡片 -->
<div class="stats-row"> <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"> <div class="stat-icon">
<component :is="s.icon" /> <component :is="s.icon" />
</div> </div>
<div class="stat-content"> <div class="stat-content">
<div class="stat-label">{{ s.label }}</div> <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'"> <div v-if="s.change !== '-'" class="stat-change" :class="s.up ? 'up' : 'down'">
<component :is="s.up ? ArrowUpOutlined : ArrowDownOutlined" /> <component :is="s.up ? ArrowUpOutlined : ArrowDownOutlined" />
{{ s.change }} {{ s.change }}
@ -84,7 +156,7 @@ const chartData = [65, 78, 52, 91, 68, 85, 73]
</h3> </h3>
<div class="chart"> <div class="chart">
<div v-for="(v, i) in chartData" :key="i" class="bar-item"> <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> <span class="bar-label">{{ ['一', '二', '三', '四', '五', '六', '日'][i] }}</span>
</div> </div>
</div> </div>
@ -186,4 +258,12 @@ const chartData = [65, 78, 52, 91, 68, 85, 73]
justify-content: center; justify-content: center;
font-size: 12px; font-size: 12px;
} }
.stat-value {
transition: color 0.3s ease;
}
.stat-value.counting {
color: #1890ff;
}
</style> </style>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { h } from 'vue' import { h, computed } from 'vue'
import { RouterView, useRouter } from 'vue-router' import { RouterView, useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { Layout, Menu, Button } from 'ant-design-vue' import { Layout, Menu, Button } from 'ant-design-vue'
import type { MenuProps } from 'ant-design-vue' import type { MenuProps } from 'ant-design-vue'
@ -10,21 +10,83 @@ import {
TeamOutlined, TeamOutlined,
AppstoreOutlined, AppstoreOutlined,
BuildOutlined, BuildOutlined,
HeatMapOutlined,
LogoutOutlined, LogoutOutlined,
AuditOutlined AuditOutlined,
SettingOutlined,
ToolOutlined
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
const { Header, Sider, Content } = Layout const { Header, Sider, Content } = Layout
const router = useRouter() const router = useRouter()
const route = useRoute()
const userStore = useUserStore() const userStore = useUserStore()
const selectedKeys = computed(() => [route.path])
const menuItems: MenuProps['items'] = [ const menuItems: MenuProps['items'] = [
{ key: '/dashboard', icon: () => h(DashboardOutlined), label: '仪表盘' }, {
{ key: '/system/users', icon: () => h(UserOutlined), label: '用户管理' }, key: 'workbench',
{ key: '/system/roles', icon: () => h(TeamOutlined), label: '角色管理' }, label: '工作台',
{ key: '/system/permissions', icon: () => h(AppstoreOutlined), label: '权限管理' }, type: 'group',
{ key: '/system/audit', icon: () => h(AuditOutlined), label: '审计日志' }, children: [
{ key: '/project/list', icon: () => h(BuildOutlined), label: '项目管理' } { 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) => { const handleMenuClick = (e: any) => {
@ -45,6 +107,7 @@ const handleLogout = async () => {
theme="dark" theme="dark"
mode="inline" mode="inline"
:items="menuItems" :items="menuItems"
:selectedKeys="selectedKeys"
@click="handleMenuClick" @click="handleMenuClick"
/> />
</Sider> </Sider>
@ -72,4 +135,9 @@ const handleLogout = async () => {
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: bold;
} }
/* 分组标题颜色比菜单项淡 */
:deep(.ant-menu-item-group-title) {
color: rgba(255, 255, 255, 0.45) !important;
}
</style> </style>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,59 +1,226 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, reactive, onMounted, computed } from 'vue'
import { Table, Button, Drawer, Form, Input, Select, Space, Popconfirm, message } from 'ant-design-vue' import { useRouter } from 'vue-router'
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue' 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 { getProjects, createProject, updateProject, deleteProject } from '@/api/project' 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 { Project } from '@/types'
import type { ProjectQuery, ProjectStatus, ProjectFormData, PageResponse, ProjectType } from '@/types/project'
import { ProjectStatusMap, ProjectTypeMap } from '@/types/project'
interface ProjectFormData { const router = useRouter()
id?: string
code?: string //
name?: string const formatDate = (date: string | Date) => {
description?: string if (!date) return '-'
address?: string const d = new Date(date)
province?: string const year = d.getFullYear()
city?: string const month = String(d.getMonth() + 1).padStart(2, '0')
district?: string const day = String(d.getDate()).padStart(2, '0')
status?: string 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: 'name', key: 'name', width: 200 },
{ title: '类型', dataIndex: 'projectType', key: 'projectType', width: 100 },
{ title: '地址', dataIndex: 'address', key: 'address', ellipsis: true }, { title: '地址', dataIndex: 'address', key: 'address', ellipsis: true },
{ title: '状态', dataIndex: 'status', key: 'status', width: 80 }, { title: '状态', dataIndex: 'status', key: 'status', width: 100 },
{ title: '操作', key: 'action', width: 120, fixed: 'right' } { 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 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 drawerVisible = ref(false)
const drawerTitle = ref('') const drawerTitle = ref('')
const formRef = ref() const formRef = ref()
const submitting = ref(false) const submitting = ref(false)
const formState = ref<ProjectFormData>({ //
id: '', const viewDrawerVisible = ref(false)
code: '', 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: '', name: '',
description: '', description: '',
address: '', 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: '', province: '',
city: '', city: '',
district: '', district: '',
status: 'ACTIVE' status: 'ACTIVE'
}) })
const statusOptions = [ //
{ value: 'ACTIVE', label: '正常', color: 'success' }, const handleNameClick = async (record: Project) => {
{ value: 'DISABLED', label: '禁用', color: 'error' } 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 () => { const fetchProjects = async () => {
loading.value = true loading.value = true
try { try {
const res = await getProjects() const params: ProjectQuery = {
projects.value = res.data ...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 { } catch {
message.error('获取项目列表失败') message.error('获取项目列表失败')
} finally { } finally {
@ -61,14 +228,34 @@ const fetchProjects = async () => {
} }
} }
const handleAdd = () => { //
drawerTitle.value = '新增项目' const handleSearch = () => {
formState.value = { pagination.current = 1
id: '', fetchProjects()
code: '', }
//
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: '', name: '',
description: '', description: '',
address: '', address: '',
projectType: 'RESIDENTIAL',
province: '', province: '',
city: '', city: '',
district: '', district: '',
@ -77,22 +264,58 @@ const handleAdd = () => {
drawerVisible.value = true drawerVisible.value = true
} }
const handleEdit = (record: Project) => { //
drawerTitle.value = '编辑项目' const handleQuickEdit = async (record: Project) => {
formState.value = { editProject.value = record
id: record.id, editActiveTab.value = 'info'
code: record.code, editFormState.value = {
name: record.name, name: record.name,
description: record.description || '', description: record.description || '',
address: record.address || '', address: record.address || '',
projectType: record.projectType || 'RESIDENTIAL',
province: record.province || '', province: record.province || '',
city: record.city || '', city: record.city || '',
district: record.district || '', 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) => { const handleDelete = async (id: string) => {
try { try {
await deleteProject(id) await deleteProject(id)
@ -103,47 +326,57 @@ const handleDelete = async (id: string) => {
} }
} }
const handleSubmit = async () => { //
const handleCreateSubmit = async () => {
try { try {
await formRef.value.validate()
submitting.value = true submitting.value = true
await createProject(createFormState.value)
if (formState.value.id) { message.success('创建成功')
await updateProject(formState.value.id, formState.value)
message.success('更新成功')
} else {
await createProject(formState.value)
message.success('创建成功')
}
drawerVisible.value = false drawerVisible.value = false
fetchProjects() fetchProjects()
} catch (error: any) { } catch (error: any) {
if (error.errorFields) return message.error('创建失败')
message.error('操作失败')
} finally { } finally {
submitting.value = false submitting.value = false
} }
} }
const handleClose = () => { const handleEditSubmit = async () => {
formRef.value?.resetFields() try {
drawerVisible.value = false 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> = { const statusTagMap = {
ACTIVE: 'success', ACTIVE: { color: 'success', label: '正常', icon: 'check' },
DISABLED: 'error' INACTIVE: { color: 'error', label: '禁用', icon: 'close' },
} DRAFT: { color: 'warning', label: '草稿', icon: 'warning' },
return map[status] || 'default' ARCHIVED: { color: 'default', label: '归档', icon: 'minus' }
} }
const getStatusLabel = (status: string) => { //
const map: Record<string, string> = { const canToggleStatus = (status: ProjectStatus) => {
ACTIVE: '正常', return status === 'ACTIVE' || status === 'INACTIVE'
DISABLED: '禁用' }
//
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) onMounted(fetchProjects)
@ -161,91 +394,391 @@ onMounted(fetchProjects)
</div> </div>
</div> </div>
<!-- 表格 --> <!-- 筛选区 -->
<div class="table-card"> <div class="filter-bar">
<Table <Space>
:columns="columns" <Input
:data-source="projects" v-model:value="queryParams.keyword"
:loading="loading" placeholder="搜索项目名称/编码"
:row-key="(record: Project) => record.id" style="width: 240px"
:pagination="{ pageSize: 10, showSizeChanger: true, showTotal: (total: number) => `共 ${total} 条` }" allow-clear
> @press-enter="handleSearch"
<template #bodyCell="{ column, record }"> />
<template v-if="column.key === 'status'"> <Select
<a-tag :color="getStatusColor(record.status)"> v-model:value="queryParams.status"
{{ getStatusLabel(record.status) }} placeholder="请选择状态"
</a-tag> allow-clear
</template> style="width: 150px"
<template v-else-if="column.key === 'action'"> :options="statusOptions"
<Space> />
<Button type="link" size="small" @click="handleEdit(record)"> <Button type="primary" @click="handleSearch">
<EditOutlined /> 编辑 <SearchOutlined /> 查询
</Button> </Button>
<Popconfirm <Button @click="handleReset">
title="确认删除" <ReloadOutlined /> 重置
description="删除后不可恢复,是否继续?" </Button>
ok-text="确认" </Space>
cancel-text="取消"
@confirm="handleDelete(record.id)"
>
<Button type="link" danger size="small">
<DeleteOutlined /> 删除
</Button>
</Popconfirm>
</Space>
</template>
</template>
</Table>
</div> </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 <Drawer
v-model:open="drawerVisible" v-model:open="drawerVisible"
:title="drawerTitle" title="新增项目"
width="560px" width="560px"
:footer-style="{ textAlign: 'right' }" :footer-style="{ textAlign: 'right' }"
@close="handleClose" @close="drawerVisible = false"
> >
<Form <Form
ref="formRef" :model="createFormState"
:model="formState"
layout="vertical" layout="vertical"
:rules="{ :rules="{
code: [{ required: true, message: '请输入项目编码' }],
name: [{ 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"> <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>
<Form.Item label="描述" name="description"> <Form.Item label="描述" name="description">
<Input.TextArea v-model:value="formState.description" placeholder="请输入描述" :rows="2" /> <Input.TextArea v-model:value="createFormState.description" placeholder="请输入描述" :rows="2" />
</Form.Item>
<Form.Item label="地址" name="address">
<Input v-model:value="formState.address" placeholder="请输入详细地址" />
</Form.Item> </Form.Item>
<Form.Item label="省份" name="province"> <Form.Item label="省份" name="province">
<Input v-model:value="formState.province" placeholder="请输入省份" /> <Input v-model:value="createFormState.province" placeholder="请输入省份" />
</Form.Item> </Form.Item>
<Form.Item label="城市" name="city"> <Form.Item label="城市" name="city">
<Input v-model:value="formState.city" placeholder="请输入城市" /> <Input v-model:value="createFormState.city" placeholder="请输入城市" />
</Form.Item> </Form.Item>
<Form.Item label="区县" name="district"> <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>
<Form.Item label="状态" name="status"> <Form.Item label="状态" name="status">
<Select v-model:value="formState.status" :options="statusOptions" /> <Select v-model:value="createFormState.status" :options="statusOptions" />
</Form.Item> </Form.Item>
</Form> </Form>
<template #footer> <template #footer>
<Space> <Space>
<Button @click="handleClose">取消</Button> <Button @click="drawerVisible = false">取消</Button>
<Button type="primary" :loading="submitting" @click="handleSubmit">确定</Button> <Button type="primary" :loading="submitting" @click="handleCreateSubmit">确定</Button>
</Space> </Space>
</template> </template>
</Drawer> </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> </div>
</template> </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>

View File

@ -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>

374
src/views/space/Space.vue Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,200 +1,334 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { Table, Button, Space, Input, Select, DatePicker, Tag, message } from 'ant-design-vue' import { Table, Button, Space, Input, Select, DatePicker, Tag, message, ConfigProvider } from 'ant-design-vue'
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-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 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'
// dayjs.locale('zh-cn')
interface AuditLog {
id: string
time: string
operator: string
type: 'PERMISSION' | 'ROLE' | 'PROJECT'
content: string
target: string
ip: string
}
//
const columns = [ const columns = [
{ title: '时间', dataIndex: 'time', key: 'time', width: 180 }, { title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 170 },
{ title: '操作用户', dataIndex: 'operator', key: 'operator', width: 120 }, { title: '操作用户', dataIndex: 'username', key: 'username', width: 100 },
{ title: '操作类型', dataIndex: 'type', key: 'type', width: 100 }, { title: '功能模块', dataIndex: 'module', key: 'module', width: 100 },
{ title: '操作内容', dataIndex: 'content', key: 'content', ellipsis: true }, { title: '操作类型', dataIndex: 'action', key: 'action', width: 90 },
{ title: '目标对象', dataIndex: 'target', key: 'target', width: 150 }, { title: '操作描述', dataIndex: 'operation', key: 'operation', width: 200 },
{ title: 'IP地址', dataIndex: 'ip', key: 'ip', width: 140 } { 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 logs = ref<AuditLog[]>([])
const loading = ref(false) const loading = ref(false)
const pagination = ref({
// current: 1,
const filters = ref({ pageSize: 10,
type: undefined as string | undefined, total: 0
dateRange: [] as [dayjs.Dayjs, dayjs.Dayjs] | null,
operator: ''
}) })
// const stats = ref({
const typeOptions = [ total: 0,
{ value: 'PERMISSION', label: '权限变更' }, retentionDays: 30
{ value: 'ROLE', label: '角色分配' }, })
{ value: 'PROJECT', label: '项目参与' }
]
// const moduleOptions = ref<{ value: string; label: string }[]>([])
const mockLogs: AuditLog[] = [ const actionOptions = ref<{ value: string; label: string }[]>([])
{
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 filters = ref({
const getTypeColor = (type: string) => { module: undefined as string | undefined,
const map: Record<string, string> = { action: undefined as string | undefined,
PERMISSION: 'blue', username: '',
ROLE: 'green', dateRange: null as [Dayjs, Dayjs] | null
PROJECT: 'orange' })
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 loadActions = async () => {
const map: Record<string, string> = { try {
PERMISSION: '权限变更', const res = await getAuditActions()
ROLE: '角色分配', actionOptions.value = res.data.data || []
PROJECT: '项目参与' } 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 loadStats = async () => {
const loadData = () => { try {
const res = await getAuditStats()
stats.value = res.data.data || { total: 0, retentionDays: 30 }
} catch {
// ignore
}
}
const loadData = async () => {
loading.value = true loading.value = true
// API try {
setTimeout(() => { const params: any = {
logs.value = mockLogs.filter((log) => { page: pagination.value.current - 1,
// size: pagination.value.pageSize
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.module) {
// params.module = filters.value.module
if (filters.value.dateRange && filters.value.dateRange.length === 2) { }
const logDate = dayjs(log.time).startOf('day') if (filters.value.action) {
const [start, end] = filters.value.dateRange params.action = filters.value.action
if (logDate.isBefore(start, 'day') || logDate.isAfter(end, 'day')) return false }
} if (filters.value.username) {
return true 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 loading.value = false
}, 300) }
} }
// const handleTableChange = (pag: any) => {
const handleReset = () => { pagination.value.current = pag.current
filters.value = { pagination.value.pageSize = pag.pageSize
type: undefined,
dateRange: null,
operator: ''
}
loadData() 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> </script>
<template> <template>
<ConfigProvider :locale="zhCN">
<div class="page-container"> <div class="page-container">
<!-- 页面标题 --> <!-- 页面标题 -->
<div class="page-header"> <div class="page-header">
<h2 class="page-title">操作审计日志</h2> <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>
<!-- 筛选区 --> <FilterBar>
<div class="filter-bar">
<Space wrap> <Space wrap>
<Select <Select
v-model:value="filters.type" v-model:value="filters.module"
placeholder="操作类型" placeholder="功能模块"
:options="typeOptions" :options="moduleOptions"
allow-clear allow-clear
style="width: 140px" style="width: 140px"
/> />
<DatePicker.RangePicker v-model:value="filters.dateRange" style="width: 260px" /> <Select
<Input v-model:value="filters.action"
v-model:value="filters.operator" placeholder="操作类型"
placeholder="操作用户" :options="actionOptions"
allow-clear
style="width: 140px" 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 /> 查询 <SearchOutlined /> 查询
</Button> </Button>
<Button @click="handleReset"> <Button @click="handleReset">
<ReloadOutlined /> 重置 <ReloadOutlined /> 重置
</Button> </Button>
</Space> </Space>
</div> </FilterBar>
<!-- 表格 --> <TableCard>
<div class="table-card"> <TableToolbar @refresh="loadData" />
<Table
<a-table
:columns="columns" :columns="columns"
:data-source="logs" :data-source="logs"
:loading="loading" :loading="loading"
:row-key="(record: AuditLog) => record.id" :row-key="(record: AuditLog) => record.id"
:pagination="{ pageSize: 10, showSizeChanger: true, showTotal: (total: number) => `共 ${total} 条` }" :pagination="false"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'"> <template v-if="column.key === 'module'">
<Tag :color="getTypeColor(record.type)"> {{ getModuleLabel(record.module) }}
{{ getTypeLabel(record.type) }} </template>
<template v-else-if="column.key === 'action'">
<Tag :color="getActionColor(record.action)">
{{ getActionLabel(record.action) }}
</Tag> </Tag>
</template> </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> </template>
</Table> </a-table>
</div>
<Pagination
v-model:current="pagination.current"
v-model:pageSize="pagination.pageSize"
:total="pagination.total"
@change="handleTableChange"
/>
</TableCard>
</div> </div>
</ConfigProvider>
</template> </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