diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/README.md b/README.md index 45c0bb2..b677c59 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,48 @@ # shrimp-admin +Shrimp 框架前端 —— 一种**经典解决方案**(不是框架本身),消费 [shrimp-server](http://git.zeromvc.com/wc24/shrimp-server) 暴露的 `ShrimpServer` 路由契约 + `ShrimpTable` 数据形状契约,基于 **Vue 3 + Element Plus** 实现一套可直接用的后台壳。 + +如果你想换 UI 库或者换前端技术栈,直接照 shrimp-server 的约定再写一份就行,不依赖本包。 + +## 安装 + +```bash +# 推荐 HTTP +npm i git+http://git.zeromvc.com/wc24/shrimp-admin.git#v0.1.0 +``` + +shrimp-admin 是**源码 ship**(.ts + .vue 直接发),由消费侧 Vite 编译。 + +## 使用 + +```ts +// main.ts +import { createApp } from 'vue' +import { createShrimpAdmin, App } from 'shrimp-admin' +import 'shrimp-admin/style' + +const app = createApp(App) +app.use(createShrimpAdmin({ + config: { + API_URL: '/admin-api', + TIMEOUT: 10000, + }, + moduleSources: import.meta.glob('./module/**/*.vue'), + customFields: import.meta.glob('./module/fields/Field*.vue'), +})) +app.mount('#app') +``` + +## 消费的契约 + +参见 shrimp-server README `## 约定` 章节。本包消费以下条件契约: + +- **ShrimpTable**: list 响应 `{list,total,page,pageSize,totalPages}` / 字段 type 枚举 / populate `${key}_label` +- **ShrimpServer**: HTTP 路由 / JWT Bearer / IUserAccessor 返回的 powers +- **默认 Field*.vue**: 字段 type 必须是内置枚举之一,或通过 `customFields` 注册 + +## 扩展点 + +- `customFields` — 业务字段组件(`Field.vue`) +- `moduleSources` — 业务模块组件(`module/.vue` / `module//.vue`) +- `vueAdmin.registerField(type, comp)` / `overrideField(moduleKey, fieldKey, comp)` — 运行时也可 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..556e1ec --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1542 @@ +{ + "name": "shrimp-admin", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "shrimp-admin", + "version": "0.1.0", + "dependencies": { + "shrimp-server": "git+http://git.zeromvc.com/wc24/shrimp-server.git#v0.5.0" + }, + "devDependencies": { + "@element-plus/icons-vue": "^2.1.0", + "@types/node": "^20.0.0", + "axios": "^1.3.4", + "element-plus": "^2.10.4", + "typescript": "^5.3.0", + "vite": "^4.4.5", + "vue": "^3.3.4", + "vue-i18n": "^9.1.9", + "vue-router": "^4.1.6" + }, + "peerDependencies": { + "@element-plus/icons-vue": "^2.0.0", + "axios": "^1.0.0", + "element-plus": "^2.0.0", + "vue": "^3.3.0", + "vue-i18n": "^9.0.0", + "vue-router": "^4.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@intlify/core-base": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz", + "integrity": "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@intlify/message-compiler": "9.14.5", + "@intlify/shared": "9.14.5" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.5.tgz", + "integrity": "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@intlify/shared": "9.14.5", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.5.tgz", + "integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.14", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.34.tgz", + "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.34.tgz", + "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", + "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/runtime-core": "3.5.34", + "@vue/shared": "3.5.34", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.34.tgz", + "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "vue": "3.5.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.14.0.tgz", + "integrity": "sha512-POgH+TtoreaEKWqYYAVQyE6i8rQMEFqAEublyF29dBA5yASWPLKY6EzfeqBTr2Uv26mPss4vSrMrNPyaK7LX5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "14.3.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.2.8" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rollup": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz", + "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/shrimp-server": { + "version": "0.5.0", + "resolved": "git+http://git.zeromvc.com/wc24/shrimp-server.git#ca283c673356d20ad89d49a8efb3663bdd2eb918", + "peerDependencies": { + "bcrypt": "^5 || ^6", + "express": "^4 || ^5", + "jsonwebtoken": "^8 || ^9", + "mongoose": "^6 || ^7 || ^8" + }, + "peerDependenciesMeta": { + "bcrypt": { + "optional": true + }, + "express": { + "optional": true + }, + "jsonwebtoken": { + "optional": true + }, + "mongoose": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "4.5.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", + "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", + "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-sfc": "3.5.34", + "@vue/runtime-dom": "3.5.34", + "@vue/server-renderer": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.1.tgz", + "integrity": "sha512-pu58kqxmVyEH6VfNYW1UyEfR3XAnJ27ZXT3yzXxxpjLxVzAbyC35Zk/nm/RMs7ijWnJNSd9fWkeex2OhUsx3MA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-i18n": { + "version": "9.14.5", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.5.tgz", + "integrity": "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==", + "deprecated": "v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html", + "dev": true, + "license": "MIT", + "dependencies": { + "@intlify/core-base": "9.14.5", + "@intlify/shared": "9.14.5", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4508585 --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "shrimp-admin", + "version": "0.1.0", + "private": true, + "description": "Shrimp 框架前端 — 消费 ShrimpServer + ShrimpTable 契约的经典 Vue3 + Element Plus 实现", + "main": "src/index.ts", + "types": "src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + }, + "./style": "./src/style/style.scss", + "./package.json": "./package.json" + }, + "files": [ + "src", + "README.md" + ], + "dependencies": { + "shrimp-server": "git+http://git.zeromvc.com/wc24/shrimp-server.git#v0.5.0" + }, + "peerDependencies": { + "vue": "^3.3.0", + "vue-router": "^4.0.0", + "vue-i18n": "^9.0.0", + "axios": "^1.0.0", + "element-plus": "^2.0.0", + "@element-plus/icons-vue": "^2.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.3.0", + "vite": "^4.4.5", + "vue": "^3.3.4", + "vue-router": "^4.1.6", + "vue-i18n": "^9.1.9", + "axios": "^1.3.4", + "element-plus": "^2.10.4", + "@element-plus/icons-vue": "^2.1.0" + } +} diff --git a/src/ShrimpClient.ts b/src/ShrimpClient.ts new file mode 100644 index 0000000..11ab58c --- /dev/null +++ b/src/ShrimpClient.ts @@ -0,0 +1,372 @@ +/** + * ShrimpClient — Shrimp 框架前端基类 + * 框架无关层,包含协议通信、模块管理、菜单树操作 + */ +import { ShrimpCode, ShrimpRequestFn, IShrimpResponse, IModuleInfo, IMenuItem, ILoginResult, IMeResult, IShrimpClient, ILabel } from 'shrimp-server/types' + +/** tip 展示接口(由框架层实现,如 Element Plus)。msg 是 ILabel,实现层负责翻译后展示 */ +export interface IShrimpTipper { + success(msg: ILabel): void + error(msg: ILabel): void + alert(msg: ILabel): Promise +} + +/** 路由注册信息,子类 addRoute 的参数 */ +export interface IRouteInfo { + moduleKey: string + name: string + path?: string + label: ILabel + events: string[] + /** 组件相对 module 目录的路径(不含 .vue),不传则默认 moduleKey。用于子模块嵌套目录定位 */ + componentPath?: string + /** 子模块跳转按钮图标(element-plus icon 名,仅 sub 模块有意义) */ + icon?: string +} + +class ShrimpResponseError extends Error { + constructor(public data: IShrimpResponse) { + super(data.code.toString()) + this.name = 'ShrimpResponseError' + } +} +/** + * 处理 Shrimp 响应中的 tip 和 log + * @returns 处理后的响应数据,或抛出错误 + */ +export function handleShrimpResponse( + body: IShrimpResponse, + tipper: IShrimpTipper, + onUnauthorized?: () => void, +): any { + if (body.log) { + console.warn('[Shrimp]', body.log) + } + + if (body.code === ShrimpCode.OK) { + if (body.tip) tipper.success(body.tip) + return body.data + } + + // 业务错误:根据 code 决定展示方式 + if (body.tip) { + if (body.code === ShrimpCode.UNAUTHORIZED) { + tipper.alert(body.tip).then(() => onUnauthorized?.()) + } else { + tipper.error(body.tip) + } + } else { + tipper.alert("").then(() => onUnauthorized?.()) + // tipper.error(t('base.fail')) + } + + const err = new ShrimpResponseError(body) as any + err.code = body.code + err.handled = true + throw err +} + +/** + * 根据协议类型生成类型安全的 API 调用器 + * 用法: + * const userApi = getCaller(request, "user") + * const subApi = getCaller(request, ["parent", "sub"]) + */ +export function getCaller

>(request: ShrimpRequestFn, moduleKey: string | string[]): P { + const segments = Array.isArray(moduleKey) ? moduleKey : [moduleKey] + return new Proxy({} as any, { + get: (target, prop, receiver) => { + if (typeof prop === 'symbol' || prop === 'valueOf' || prop === 'toString') { + return Reflect.get(target, prop, receiver) + } + return (...args: any[]) => request([...segments, prop as string], ...args) + } + }) +} + +/** + * ShrimpClient 基类 + * 封装与 Shrimp 协议相关的公共方法,框架无关 + * 业务层(如 HandAdmin)继承此类并实现底层请求 + */ +export abstract class ShrimpClient implements IShrimpClient { + /** 当前用户的模块权限列表 */ + moduleInfo: IModuleInfo[] = [] + + /** 当前用户的菜单树(含层级结构) */ + menuTree: IMenuItem[] = [] + + /** 全局选项(/me 返回的 options) */ + globalOptions: Record = {} + + private callers: Map = new Map() + + /** 子类必须实现:底层 HTTP 请求 */ + abstract rawRequest(method: string, url: string, data?: any): Promise + + /** 子类必须实现:路由注册(框架特定,如 Vue Router) */ + abstract registerRoute(info: IRouteInfo): void + + // ---- Shrimp RPC ---- + + /** Shrimp RPC 请求(POST /moduleKey/eventName) */ + async doRequest(path: string[], data?: any[]): Promise { + return this.rawRequest('POST', '/' + path.join('/'), data) + } + + /** 标准 Shrimp 请求函数(过滤 undefined 参数),供 getCaller 使用 */ + async shrimpRequest(path: string[], ...args: any[]): Promise { + const filteredArgs = args.filter(arg => arg !== undefined) + return await this.doRequest(path, filteredArgs.length > 0 ? filteredArgs : undefined) + } + + // ---- REST 接口 ---- + + /** 获取当前用户信息(权限 + 菜单) */ + async fetchMe(): Promise { + return this.rawRequest('GET', '/me') + } + + /** 用户登录 */ + async fetchLogin(data: { username: string; password: string }): Promise { + return this.rawRequest('POST', '/login', data) + } + + /** 获取后台配置 */ + async fetchConfig(): Promise> { + return this.rawRequest>('GET', '/config') + } + + // ---- 用户信息 ---- + + /** 获取当前用户信息(moduleInfo + menuTree)并注册路由 */ + async loadUserInfo(): Promise { + const data = await this.fetchMe() + this.moduleInfo = data.moduleInfo + this.menuTree = data.menuTree + this.globalOptions = data.options || {} + if (this.menuTree.length > 0) { + this.registerRoutesFromMenuTree(this.menuTree) + } else { + this.registerRoutesFromModuleInfo(this.moduleInfo) + } + // 子模块永远走 moduleInfo 递归注册(不进 menu) + this.registerSubModuleRoutes(this.moduleInfo) + return data + } + + // ---- 路由注册 ---- + + /** + * 根据 moduleInfo 扁平注册路由 + * 适用于无层级菜单的场景:moduleKey 即路由名,path 为 /moduleKey + * 子模块(带 parent)不会作为顶层路由,由 registerSubModuleRoutes 处理 + */ + registerRoutesFromModuleInfo(moduleInfo: IModuleInfo[]) { + moduleInfo.forEach(info => { + if (info.parent) return + this.registerRoute({ + moduleKey: info.moduleKey, + name: info.moduleKey, + label: info.label, + path: '/' + info.moduleKey, + events: info.events, + }) + }) + } + + /** + * 根据 menuTree 层级注册路由 + * 遍历菜单树,对每个带 moduleKey 的项调用 addRoute + */ + registerRoutesFromMenuTree(menuTree: IMenuItem[]) { + const walk = (items: IMenuItem[]) => { + items.forEach(item => { + if (item.moduleKey) { + this.registerRoute({ + moduleKey: item.moduleKey, + name: item.name, + path: item.path, + label: item.label, + events: this.getEvents(item.moduleKey), + }) + } + if (item.children) { + walk(item.children) + } + }) + } + walk(menuTree) + } + + /** + * 注册子模块路由(扁平 moduleInfo,通过 parent 链接) + * 子模块路径 = //,组件文件 = @/module//.vue + */ + registerSubModuleRoutes(moduleInfo: IModuleInfo[]) { + const byKey = new Map(moduleInfo.map(m => [m.moduleKey, m])) + const buildSegments = (info: IModuleInfo): string[] => { + const segs = [info.moduleKey] + let p = info.parent + while (p) { + segs.unshift(p) + p = byKey.get(p)?.parent + } + return segs + } + moduleInfo.forEach(info => { + if (!info.parent) return + const segments = buildSegments(info) + this.registerRoute({ + moduleKey: info.moduleKey, + name: segments.join('.'), + path: '/' + segments.join('/'), + label: info.label, + events: info.events, + componentPath: segments.join('/'), + icon: info.icon, + }) + }) + } + + // ---- 菜单树操作 ---- + + /** 获取第一个菜单路径(用于默认跳转) */ + getFirstMenuPath(): string { + const findFirst = (items: IMenuItem[]): string | null => { + for (const item of items) { + if (item.path) return item.path + if (item.moduleKey) return '/' + item.moduleKey + if (item.children) { + const found = findFirst(item.children) + if (found) return found + } + } + return null + } + const path = findFirst(this.menuTree) + if (path) return path + const top = this.moduleInfo.find(m => !m.parent) + return '/' + (top?.moduleKey || '') + } + + /** 根据 path 获取所在顶级菜单的索引 */ + getMenuIndex(path: string): number { + const findIndex = (items: IMenuItem[], targetPath: string): number => { + for (let i = 0; i < items.length; i++) { + const item = items[i] + if (item.path === targetPath || '/' + item.name === targetPath) return i + if (item.children) { + const childIndex = findIndex(item.children, targetPath) + if (childIndex !== -1) return i + } + } + return -1 + } + return findIndex(this.menuTree, path) + } + + /** 判断是否为分组菜单(有 children) */ + isGroupMenu(item?: IMenuItem): boolean { + return item != null && item.children != null && item.children.length > 0 + } + + /** 根据 path 生成面包屑路径(支持子模块: 走 moduleInfo.parent 链) */ + getBreadcrumb(path: string): { key: string; label: ILabel; path?: string }[] { + const result: { key: string; label: ILabel; path?: string }[] = [] + const findInMenu = (items: IMenuItem[], targetPath: string, out: typeof result): boolean => { + for (const item of items) { + const itemPath = item.path || '/' + item.name + if (itemPath === targetPath) { + out.unshift({ key: item.name, label: item.label, path: itemPath }) + return true + } + if (item.children) { + if (findInMenu(item.children, targetPath, out)) { + out.unshift({ key: item.name, label: item.label, path: item.path }) + return true + } + } + } + return false + } + if (findInMenu(this.menuTree, path, result)) return result + + // 子模块 fallback: 沿 moduleInfo.parent 链构建,带累积路径 + const segments = path.split('/').filter(Boolean) + if (segments.length === 0) return result + const byKey = new Map(this.moduleInfo.map(m => [m.moduleKey, m])) + const chain: IModuleInfo[] = [] + let info = byKey.get(segments[segments.length - 1]) + while (info) { + chain.unshift(info) + info = info.parent ? byKey.get(info.parent) : undefined + } + if (chain.length === 0) return result + + // 顶层模块如果在 menu 分组下, 先把分组上下文放进来 + findInMenu(this.menuTree, '/' + chain[0].moduleKey, result) + const lastKey = result.length > 0 ? result[result.length - 1].key : '' + let acc = '' + chain.forEach(m => { + acc += '/' + m.moduleKey + if (m.moduleKey === lastKey) return // 已被 menu 添加 + result.push({ key: m.moduleKey, label: m.label, path: acc }) + }) + return result + } + + // ---- 模块权限 ---- + + /** 获取模块的权限事件列表 */ + getEvents(moduleKey: string): string[] { + const mod = this.moduleInfo.find(m => m.moduleKey === moduleKey) + return mod?.events || [] + } + + /** 判断是否有某个事件的权限 */ + hasEvent(moduleKey: string, eventName: string): boolean { + return this.getEvents(moduleKey).includes(eventName) + } + + /** 获取绑定到指定模块的权限检查器:const can = client.can('shopAdmin'); can('apply') */ + can(moduleKey: string): (event: string) => boolean { + return (event: string) => this.hasEvent(moduleKey, event) + } + + // ---- 模块 API ---- + + /** 获取类型安全的模块 API(带缓存,自动沿 parent 链解析嵌套模块路径) */ + getModule

>(moduleKey: string): P { + let caller = this.callers.get(moduleKey) + if (!caller) { + const segments = this.resolveModulePath(moduleKey) + caller = getCaller

(this.shrimpRequest.bind(this), segments) + this.callers.set(moduleKey, caller) + } + return caller + } + + /** 从 moduleInfo 沿 parent 链回溯出完整路径(根→自身);找不到时退化为单段 */ + private resolveModulePath(moduleKey: string): string[] { + const byKey = new Map(this.moduleInfo.map(m => [m.moduleKey, m])) + const info = byKey.get(moduleKey) + if (!info) return [moduleKey] + const segs = [info.moduleKey] + let p = info.parent + while (p) { + segs.unshift(p) + p = byKey.get(p)?.parent + } + return segs + } + + /** 清除模块 API 缓存 */ + clearModuleCache(moduleKey?: string) { + if (moduleKey) { + this.callers.delete(moduleKey) + } else { + this.callers.clear() + } + } +} diff --git a/src/VueAdmin.ts b/src/VueAdmin.ts new file mode 100644 index 0000000..4b681cc --- /dev/null +++ b/src/VueAdmin.ts @@ -0,0 +1,395 @@ +import * as elIcons from '@element-plus/icons-vue' +import { type App, type Component, type Plugin, defineAsyncComponent, reactive } from 'vue' +import axios from 'axios' +import { ElMessage, ElMessageBox } from 'element-plus' +import ElementPlus from 'element-plus' +import { createRouter, createWebHashHistory, Router } from 'vue-router' +import zh_cn from './locales/zh-cn.json' +import en from './locales/en.json' +import { createI18n, I18n } from 'vue-i18n' +import { IShrimpResponse, ILabel } from 'shrimp-server/types' + +import { ShrimpClient, handleShrimpResponse, type IShrimpTipper, type IRouteInfo } from './ShrimpClient' +import FieldString from './components/fields/FieldString.vue' + +// ============== Options ============== + +export interface ShrimpAdminConfig { + API_URL: string + TIMEOUT?: number + HEADERS?: Record + APP_VER?: string + LS_ENCRYPTION?: string + LS_ENCRYPTION_key?: string +} + +export interface ShrimpAdminOptions { + config: ShrimpAdminConfig + /** 业务模块组件:`import.meta.glob('./module/**\/*.vue')` 的结果 */ + moduleSources?: Record Promise<{ default: Component } | Component>> + /** 业务字段组件:`import.meta.glob('./module/fields/Field*.vue')`,文件名 FieldXxx → type='fieldXxx' */ + customFields?: Record Promise<{ default: Component } | Component>> + /** 语言资源,与框架自带的合并(消费侧同 key 覆盖框架默认) */ + locales?: Record + /** 语言列表,默认 [{ lang: 'zh-cn', name: '简体中文' }] */ + langList?: { lang: string; name: string }[] +} + +// ============== 字段组件注册表 ============== + +const fieldRegistry = new Map() +const fieldOverrides = new Map() + +fieldRegistry.set('string', FieldString) +fieldRegistry.set('boolean', defineAsyncComponent(() => import('./components/fields/FieldBoolean.vue'))) +fieldRegistry.set('date', defineAsyncComponent(() => import('./components/fields/FieldDate.vue'))) +fieldRegistry.set('number', defineAsyncComponent(() => import('./components/fields/FieldNumber.vue'))) +fieldRegistry.set('password', defineAsyncComponent(() => import('./components/fields/FieldPassword.vue'))) +fieldRegistry.set('relation', defineAsyncComponent(() => import('./components/fields/FieldRelation.vue'))) +fieldRegistry.set('select', defineAsyncComponent(() => import('./components/fields/FieldSelect.vue'))) +fieldRegistry.set('textarea', defineAsyncComponent(() => import('./components/fields/FieldTextarea.vue'))) +fieldRegistry.set('json', defineAsyncComponent(() => import('./components/fields/FieldJson.vue'))) +fieldRegistry.set('tab', defineAsyncComponent(() => import('./components/fields/FieldTab.vue'))) + +// 框架自带模块组件 — vite glob 相对本文件解析,扫到的是 shrimp-admin 内部的 module/ +const internalModules = import.meta.glob('./module/**/*.vue') + +// 业务模块由消费侧通过 createShrimpAdmin({ moduleSources }) 注册 +const externalModules: Record Promise> = {} + +function resolveModuleComponent(componentPath: string): (() => Promise) | null { + const suffix = `/module/${componentPath}.vue` + // 业务优先(允许覆盖同名内置) + for (const key in externalModules) { + if (key.endsWith(suffix)) return externalModules[key] + } + for (const key in internalModules) { + if (key.endsWith(suffix)) return internalModules[key] as any + } + return null +} + +// ============== LocalData ============== + +class LocalData { + set(key: string, data: any) { localStorage.setItem(key, JSON.stringify(data)) } + get(key: string) { + try { + const value = localStorage.getItem(key) + return value ? JSON.parse(value) : null + } catch { return null } + } + remove(key: string) { localStorage.removeItem(key) } + clear() { localStorage.clear() } +} + +// ============== VueAdmin ============== + +export class VueAdmin extends ShrimpClient { + router!: Router + localData = new LocalData() + i18n!: I18n + adminConfig: Record = reactive({}) + userName = '' + config: ShrimpAdminConfig + langList: { lang: string; name: string }[] + + registerField(type: string, comp: Component) { fieldRegistry.set(type, comp) } + + overrideField(moduleKey: string, fieldKey: string, comp: Component) { + const registryKey = `__override_${moduleKey}_${fieldKey}` + fieldRegistry.set(registryKey, comp) + fieldOverrides.set(`${moduleKey}.${fieldKey}`, registryKey) + } + + getFieldComponent(moduleKey: string, type: string, fieldKey?: string): Component { + if (fieldKey) { + const override = fieldOverrides.get(`${moduleKey}.${fieldKey}`) + if (override && fieldRegistry.has(override)) return fieldRegistry.get(override)! + } + return fieldRegistry.get(type) || FieldString + } + + state = reactive({ isMobile: false }) + + private _token: string | null = null + get token() { return this._token } + set token(value: string | null) { + this.localData.set("token", value) + this._token = value + } + + get langCount() { return this.langList.length } + + get locale(): string { return (this.i18n.global.locale as any).value } + set locale(value: string) { + (this.i18n.global.locale as any).value = value + this.localData.set("lang", value) + } + + toggleLang() { + if (this.langCount <= 1) return + const idx = this.langList.findIndex(l => l.lang === this.locale) + const next = this.langList[(idx + 1) % this.langCount] + this.locale = next.lang + } + + get currentLangName() { + return this.langList.find(l => l.lang === this.locale)?.name || this.locale + } + + request: ReturnType + + constructor(opts: ShrimpAdminOptions) { + super() + this.config = opts.config + this.langList = opts.langList || [{ lang: 'zh-cn', name: '简体中文' }] + + // 业务模块/字段注册 + if (opts.moduleSources) Object.assign(externalModules, opts.moduleSources) + if (opts.customFields) { + for (const path in opts.customFields) { + const match = path.match(/\/Field(.+)\.vue$/) + if (match) { + const name = 'field' + match[1] + fieldRegistry.set(name, defineAsyncComponent(opts.customFields[path] as any)) + } + } + } + + this.request = axios.create({ + baseURL: this.config.API_URL, + timeout: this.config.TIMEOUT ?? 10000, + }) + this.initHttp() + this.initI18n(opts.locales) + this.initRouter() + this.initMobileCheck() + this._token = this.localData.get("token") + } + + initI18n(extraLocales?: Record) { + const merged: Record = { + 'zh-cn': { ...zh_cn, ...(extraLocales?.['zh-cn'] || {}) }, + 'en': { ...en, ...(extraLocales?.['en'] || {}) }, + } + if (extraLocales) { + for (const key in extraLocales) { + if (!(key in merged)) merged[key] = extraLocales[key] + } + } + this.i18n = createI18n({ + locale: this.localData.get("lang") || 'zh-cn', + globalInjection: true, + legacy: false, + messages: merged, + }) + } + + t(value?: ILabel | null): string { + if (value == null) return '' + if (typeof value === 'string') return value + const v = value as any + if ('t' in v) return (this.i18n.global.t as any)(v.t, this.recurseParams(v.params)) + if ('tc' in v) return ((this.i18n.global as any).t)(v.tc, this.recurseParams(v.params), v.count) + if ('d' in v) return (this.i18n.global.d as any)(v.d, v.format) + if ('n' in v) return (this.i18n.global.n as any)(v.n, v.format) + return v[this.locale] || '' + } + + private recurseParams(params?: Record): Record { + if (!params) return {} + const out: Record = {} + for (const [k, val] of Object.entries(params)) { + out[k] = (val !== null && typeof val === 'object') ? this.t(val as ILabel) : val + } + return out + } + + async loadUserInfo() { + const data = await this.fetchMe() + this.userName = (data as any).nickname || '' + this.moduleInfo = data.moduleInfo + this.menuTree = data.menuTree + this.globalOptions = data.options || {} + if (this.menuTree.length > 0) { + this.registerRoutesFromMenuTree(this.menuTree) + } else { + this.registerRoutesFromModuleInfo(this.moduleInfo) + } + this.registerSubModuleRoutes(this.moduleInfo) + return data + } + + async rawRequest(method: string, url: string, data?: any): Promise { + const res = await this.request({ method, url, data }) + return res.data as T + } + + registerRoute(info: IRouteInfo) { + const componentPath = info.componentPath || info.moduleKey + const importer = resolveModuleComponent(componentPath) + if (!importer) { + console.warn(`[shrimp-admin] module component not found: module/${componentPath}.vue`) + return + } + this.router.addRoute("layout", { + path: info.path || "/" + info.name, + name: info.name, + component: importer, + props: { moduleKey: info.moduleKey, events: info.events }, + meta: { title: info.label } + }) + } + + initRouter() { + this.router = createRouter({ + history: createWebHashHistory(), + routes: [ + { + name: "layout", + path: "/", + component: () => import('./views/layout.vue'), + children: [] + }, + { + path: "/login", + component: () => import('./views/login.vue'), + meta: { title: "t:login.signIn" } + }, + { + path: "/:pathMatch(.*)", + component: () => import('./views/404.vue'), + }, + ], + }) + + this.router.afterEach((to) => { + if (to.meta?.title) { + document.title = this.t(to.meta.title as string) + } else { + document.title = this.adminConfig.title || 'Admin' + } + }) + + this.router.beforeEach(async (to, _from, next) => { + if (to.path === "/login") { + next() + } else if (!this.token) { + next({ path: '/login' }) + } else if (this.moduleInfo.length === 0) { + try { + await this.loadUserInfo() + const firstPath = this.getFirstMenuPath() + next(to.path === "/" + ? { path: firstPath, replace: true } + : { ...to, replace: true }) + } catch { + this.token = null + next({ path: '/login' }) + } + } else { + next() + } + }) + } + + tipper: IShrimpTipper = { + success: (msg) => ElMessage.success(this.t(msg)), + error: (msg) => ElMessage.error(this.t(msg)), + alert: (msg) => ElMessageBox.alert(this.t(msg), { + confirmButtonText: (this.i18n.global.t as any)('base.confirm'), + showClose: true, + }), + } + + initHttp() { + this.request.interceptors.request.use((reqConfig) => { + if (this.token) { + reqConfig.headers.Authorization = `Bearer ${this.token}` + } + return reqConfig + }) + + this.request.interceptors.response.use( + (res) => { + const body = res.data as IShrimpResponse + if (body.code === undefined) return res + try { + if (body.tip) body.tip = this.t(body.tip) + res.data = handleShrimpResponse(body, this.tipper, () => { + this.token = null + this.router.push('/login') + }) + return res + } catch (err) { + return Promise.reject(err) + } + }, + (error) => { + ElMessage.error(this.t('t:base.netError')) + throw error + } + ) + } + + initMobileCheck() { + const check = () => { this.state.isMobile = document.body.clientWidth <= 992 } + window.addEventListener('resize', check) + check() + } + + fullscreen() { + const doc = document as any + const isFull = !!(doc.webkitIsFullScreen || doc.mozFullScreen || doc.msFullscreenElement || doc.fullscreenElement) + if (isFull) { + doc.exitFullscreen?.() || doc.msExitFullscreen?.() || doc.mozCancelFullScreen?.() || doc.webkitExitFullscreen?.() + } else { + const el = doc.documentElement + el.requestFullscreen?.() || el.msRequestFullscreen?.() || el.mozRequestFullScreen?.() || el.webkitRequestFullscreen?.() + } + } + + clearCache() { this.localData.clear() } + + reset() { + this.token = null + location.reload() + } +} + +// ============== 单例 + factory ============== + +let _instance: VueAdmin | null = null + +export const vueAdmin = new Proxy({} as VueAdmin, { + get(_t, prop) { + if (!_instance) throw new Error('[shrimp-admin] vueAdmin accessed before createShrimpAdmin() was called') + return Reflect.get(_instance, prop, _instance) + }, + set(_t, prop, value) { + if (!_instance) throw new Error('[shrimp-admin] vueAdmin accessed before createShrimpAdmin() was called') + return Reflect.set(_instance, prop, value, _instance) + } +}) + +export function createShrimpAdmin(opts: ShrimpAdminOptions): Plugin { + if (_instance) { + console.warn('[shrimp-admin] createShrimpAdmin called more than once; reusing existing instance') + return shrimpAdminPlugin + } + _instance = new VueAdmin(opts) + return shrimpAdminPlugin +} + +const shrimpAdminPlugin: Plugin = { + install(app: App) { + if (!_instance) throw new Error('[shrimp-admin] install called before createShrimpAdmin()') + app.use(ElementPlus) + app.use(_instance.i18n) + app.use(_instance.router) + for (const key in elIcons) { + app.component(`ElIcon${key}`, (elIcons as any)[key]) + } + } +} diff --git a/src/assets/icons/Lang.vue b/src/assets/icons/Lang.vue new file mode 100644 index 0000000..30a0b87 --- /dev/null +++ b/src/assets/icons/Lang.vue @@ -0,0 +1,21 @@ + diff --git a/src/components/LangSwitch.vue b/src/components/LangSwitch.vue new file mode 100644 index 0000000..34af72b --- /dev/null +++ b/src/components/LangSwitch.vue @@ -0,0 +1,47 @@ + + + + + + + diff --git a/src/components/PowerTree.vue b/src/components/PowerTree.vue new file mode 100644 index 0000000..64ebe59 --- /dev/null +++ b/src/components/PowerTree.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/src/components/ShrimpEditDrawer.vue b/src/components/ShrimpEditDrawer.vue new file mode 100644 index 0000000..847d61e --- /dev/null +++ b/src/components/ShrimpEditDrawer.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/src/components/ShrimpFilterDrawer.vue b/src/components/ShrimpFilterDrawer.vue new file mode 100644 index 0000000..4b4d3e8 --- /dev/null +++ b/src/components/ShrimpFilterDrawer.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/src/components/ShrimpFilterTrigger.vue b/src/components/ShrimpFilterTrigger.vue new file mode 100644 index 0000000..623464e --- /dev/null +++ b/src/components/ShrimpFilterTrigger.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/components/ShrimpSearchInput.vue b/src/components/ShrimpSearchInput.vue new file mode 100644 index 0000000..6f1ba6b --- /dev/null +++ b/src/components/ShrimpSearchInput.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/src/components/ShrimpTable.vue b/src/components/ShrimpTable.vue new file mode 100644 index 0000000..a9164d5 --- /dev/null +++ b/src/components/ShrimpTable.vue @@ -0,0 +1,583 @@ + + + + + diff --git a/src/components/fields/FieldBoolean.vue b/src/components/fields/FieldBoolean.vue new file mode 100644 index 0000000..136cb85 --- /dev/null +++ b/src/components/fields/FieldBoolean.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/components/fields/FieldDate.vue b/src/components/fields/FieldDate.vue new file mode 100644 index 0000000..bc01fff --- /dev/null +++ b/src/components/fields/FieldDate.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/src/components/fields/FieldJson.vue b/src/components/fields/FieldJson.vue new file mode 100644 index 0000000..cc686a1 --- /dev/null +++ b/src/components/fields/FieldJson.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/components/fields/FieldNumber.vue b/src/components/fields/FieldNumber.vue new file mode 100644 index 0000000..fc865eb --- /dev/null +++ b/src/components/fields/FieldNumber.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/src/components/fields/FieldPassword.vue b/src/components/fields/FieldPassword.vue new file mode 100644 index 0000000..439e324 --- /dev/null +++ b/src/components/fields/FieldPassword.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/components/fields/FieldRelation.vue b/src/components/fields/FieldRelation.vue new file mode 100644 index 0000000..96bf214 --- /dev/null +++ b/src/components/fields/FieldRelation.vue @@ -0,0 +1,94 @@ + + + diff --git a/src/components/fields/FieldSelect.vue b/src/components/fields/FieldSelect.vue new file mode 100644 index 0000000..796c679 --- /dev/null +++ b/src/components/fields/FieldSelect.vue @@ -0,0 +1,65 @@ + + + diff --git a/src/components/fields/FieldString.vue b/src/components/fields/FieldString.vue new file mode 100644 index 0000000..0f56c78 --- /dev/null +++ b/src/components/fields/FieldString.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/src/components/fields/FieldTab.vue b/src/components/fields/FieldTab.vue new file mode 100644 index 0000000..41bffb3 --- /dev/null +++ b/src/components/fields/FieldTab.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/src/components/fields/FieldTextarea.vue b/src/components/fields/FieldTextarea.vue new file mode 100644 index 0000000..206e5a0 --- /dev/null +++ b/src/components/fields/FieldTextarea.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/src/composables/useTableUrlSync.ts b/src/composables/useTableUrlSync.ts new file mode 100644 index 0000000..8c1460a --- /dev/null +++ b/src/composables/useTableUrlSync.ts @@ -0,0 +1,98 @@ +import { ref, watch } from 'vue' +import { useRoute, useRouter } from 'vue-router' + +export interface UseTableUrlSyncOptions { + pageSize: number +} + +export function useTableUrlSync(options: UseTableUrlSyncOptions) { + const route = useRoute() + const router = useRouter() + + const currentPage = ref(1) + const currentPageSize = ref(options.pageSize) + const keyword = ref('') + const searchKeyword = ref('') + const currentSortBy = ref('') + const currentSortOrder = ref<'asc' | 'desc'>('asc') + const filterData = ref>({}) + + function restoreFromQuery() { + const q = route.query + if (q.page) currentPage.value = Number(q.page) || 1 + if (q.pageSize) currentPageSize.value = Number(q.pageSize) || options.pageSize + if (q.keyword) { + keyword.value = String(q.keyword) + searchKeyword.value = String(q.keyword) + } + if (q.sortBy) currentSortBy.value = String(q.sortBy) + if (q.sortOrder && (q.sortOrder === 'asc' || q.sortOrder === 'desc')) { + currentSortOrder.value = q.sortOrder + } + if (q.filter) { + try { + const parsed = JSON.parse(String(q.filter)) + if (typeof parsed === 'object' && parsed !== null) { + filterData.value = parsed + } + } catch { /* ignore invalid JSON */ } + } + } + + // 表格自身管理的 query key,sync 时只覆盖这几个,其它保留(如 ?id= 子表父记录、自定义参数) + const TABLE_QUERY_KEYS = ['page', 'pageSize', 'keyword', 'sortBy', 'sortOrder', 'filter'] + + function syncToQuery() { + const preserved: Record = {} + for (const [k, v] of Object.entries(route.query)) { + if (!TABLE_QUERY_KEYS.includes(k)) preserved[k] = v + } + const query: Record = { ...preserved } + if (currentPage.value > 1) query.page = String(currentPage.value) + if (currentPageSize.value !== options.pageSize) query.pageSize = String(currentPageSize.value) + if (searchKeyword.value) query.keyword = searchKeyword.value + if (currentSortBy.value) { + query.sortBy = currentSortBy.value + query.sortOrder = currentSortOrder.value + } + const cleanFilter: Record = {} + for (const [key, val] of Object.entries(filterData.value)) { + if (val === '' || val === null || val === undefined) continue + if (Array.isArray(val) && val.length === 0) continue + cleanFilter[key] = val + } + if (Object.keys(cleanFilter).length > 0) { + query.filter = JSON.stringify(cleanFilter) + } + router.replace({ query }) + } + + let isRestoring = false + + watch( + [currentPage, currentPageSize, searchKeyword, currentSortBy, currentSortOrder, filterData], + () => { + if (!isRestoring) { + syncToQuery() + } + }, + { deep: true } + ) + + function restore() { + isRestoring = true + restoreFromQuery() + isRestoring = false + } + + return { + currentPage, + currentPageSize, + keyword, + searchKeyword, + currentSortBy, + currentSortOrder, + filterData, + restore, + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..063177c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,30 @@ +// ============== 主入口 ============== +// +// 消费侧用法见 README.md。本文件只做 re-export。 + +export { + VueAdmin, + vueAdmin, + createShrimpAdmin, + type ShrimpAdminOptions, + type ShrimpAdminConfig, +} from './VueAdmin' + +export { + ShrimpClient, + handleShrimpResponse, + type IShrimpTipper, + type IRouteInfo, +} from './ShrimpClient' + +// 框架组件 — 业务页面想直接复用就 import 这些 +export { default as ShrimpTable } from './components/ShrimpTable.vue' +export { default as ShrimpEditDrawer } from './components/ShrimpEditDrawer.vue' +export { default as ShrimpFilterDrawer } from './components/ShrimpFilterDrawer.vue' +export { default as ShrimpFilterTrigger } from './components/ShrimpFilterTrigger.vue' +export { default as ShrimpSearchInput } from './components/ShrimpSearchInput.vue' +export { default as PowerTree } from './components/PowerTree.vue' +export { default as LangSwitch } from './components/LangSwitch.vue' + +// 应用入口视图(消费侧 createApp 用) +export { default as App } from './views/App.vue' diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..19815a3 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,132 @@ +{ + "base": { + "name": "Name", + "add": "Add", + "edit": "Edit", + "delete": "Delete", + "search": "Search", + "filter": "Filter", + "reset": "Reset", + "confirm": "Confirm", + "cancel": "Cancel", + "close": "Close", + "save": "Save", + "submit": "Submit", + "operation": "Operation", + "exportExcel": "Export Excel", + "importExcel": "Import Excel", + "success": "Success", + "fail": "Failed", + "noNavMenu": "No submenu", + "clearCache": "Clear Cache", + "logout": "Logout", + "netError": "Network error", + "disabled": "Disabled", + "enabled": "Enabled", + "pressEnter": "Press Enter to add", + "expand": "Expand", + "collapse": "Collapse" + }, + "user": { + "username": "Username", + "password": "Password", + "nickname": "Nickname", + "roleId": "Role", + "active": "Active", + "createdAt": "Created At", + "updatedAt": "Updated At", + "menuManage": "User", + "operationLog": "Operation Log" + }, + "operationLog": { + "ip": "IP", + "method": "Action", + "args": "Args", + "success": "Success", + "errorMsg": "Error", + "createdAt": "Time" + }, + "role": { + "name": "Role Name", + "description": "Description", + "isSuperAdmin": "Super Admin", + "powers": "Permissions", + "createdAt": "Created At", + "updatedAt": "Updated At", + "menuManage": "Role", + "menuList": "Role List", + "empty": "No roles", + "addRole": "Add Role", + "confirmDelete": "Are you sure you want to delete this role?", + "selectOrCreate": "Please select or create a role", + "selectAll": "Select All", + "newRole": "New Role" + }, + "table": { + "addSuccess": "Added successfully", + "editSuccess": "Updated successfully", + "confirmDelete": "Are you sure you want to delete this record?", + "confirmClear": "Are you sure you want to clear all data? This cannot be undone!", + "yes": "Yes", + "no": "No", + "unlimited": "Any", + "startTime": "Start Time", + "endTime": "End Time", + "min": "Min", + "max": "Max", + "justNow": "Just now", + "minutesAgo": "{n} min ago", + "hoursAgo": "{n} hr ago", + "daysAgo": "{n} days ago", + "textareaMultipleNotSupported": "Textarea does not support multiple selection", + "passwordMultipleNotSupported": "Password does not support multiple selection", + "pressEnterForNumber": "Type number and press Enter" + }, + "login": { + "userPlaceholder": "Username / Phone / Email", + "PWPlaceholder": "Please enter password", + "signIn": "Sign In", + "userError": "Please enter username", + "PWError": "Please enter password" + }, + "power": { + "user": "User", + "role": "Role", + "zerobase": "Zero Base", + "add": "Add", + "edit": "Edit", + "delete": "Delete", + "list": "List", + "get": "Detail", + "importExcel": "Import Excel", + "exportExcel": "Export Excel", + "view": "View", + "clear": "Clear", + "export": "Export", + "import": "Import" + }, + "shrimp": { + "loginFailed": "Incorrect username or password", + "userNotFound": "User not found", + "relogin": "Please log in again", + "userDisabled": "User has been disabled", + "emptyPath": "Request path cannot be empty", + "forbidden": "Insufficient permissions", + "serverError": "Server error", + "cannotDisableSelf": "Cannot disable yourself", + "cannotDeleteSelf": "Cannot delete yourself", + "cannotChangeOwnRole": "Cannot change your own role", + "cannotEditOwnRole": "Cannot edit your own role", + "cannotDeleteOwnRole": "Cannot delete your own role", + "cannotClearAllUsers": "Cannot clear all users", + "cannotClearAllRoles": "Cannot clear all roles", + "fieldRequired": "Field \"{field}\" is required", + "fieldTypeError": "Field \"{field}\" has invalid type", + "fieldInvalid": "Field \"{field}\" has invalid value", + "fieldMin": "Field \"{field}\" minimum value is {min}", + "fieldMax": "Field \"{field}\" maximum value is {max}", + "fieldMinLength": "Field \"{field}\" minimum length is {min}", + "fieldMaxLength": "Field \"{field}\" maximum length is {max}", + "invalidJson": "Invalid JSON format" + } +} diff --git a/src/locales/zh-cn.json b/src/locales/zh-cn.json new file mode 100644 index 0000000..30f44ac --- /dev/null +++ b/src/locales/zh-cn.json @@ -0,0 +1,130 @@ +{ + "base": { + "name": "名称", + "add": "新增", + "edit": "编辑", + "delete": "删除", + "search": "搜索", + "filter": "筛选", + "reset": "重置", + "confirm": "确定", + "cancel": "取消", + "close": "关闭", + "save": "保存", + "submit": "提交", + "operation": "操作", + "exportExcel": "导出Excel", + "importExcel": "导入Excel", + "success": "成功", + "fail": "失败", + "noNavMenu": "没有子菜单", + "clearCache": "清除缓存", + "logout": "退出登录", + "netError": "网络出错", + "disabled": "禁用", + "enabled": "启用", + "pressEnter": "按回车添加", + "back": "返回", + "refresh": "刷新", + "notFound": "未找到数据", + "expand": "展开", + "collapse": "收起" + }, + "user": { + "username": "用户名", + "password": "密码", + "nickname": "昵称", + "roleId": "角色", + "active": "启用状态", + "createdAt": "创建时间", + "updatedAt": "更新时间", + "operationLog": "操作日志" + }, + "operationLog": { + "ip": "IP", + "method": "操作", + "args": "参数", + "success": "成功", + "errorMsg": "错误信息", + "createdAt": "时间" + }, + "role": { + "name": "角色名", + "description": "描述", + "isSuperAdmin": "超级管理员", + "powers": "权限", + "createdAt": "创建时间", + "updatedAt": "更新时间", + "menuList": "角色列表", + "empty": "暂无角色", + "addRole": "新增角色", + "confirmDelete": "确定删除此角色吗?", + "selectOrCreate": "请选择或新建角色", + "selectAll": "全选", + "newRole": "新角色" + }, + "table": { + "addSuccess": "新增成功", + "editSuccess": "编辑成功", + "confirmDelete": "确定删除此记录吗?", + "confirmClear": "确定清空数据吗?此操作不可恢复!", + "yes": "是", + "no": "否", + "unlimited": "不限", + "startTime": "开始时间", + "endTime": "结束时间", + "min": "最小值", + "max": "最大值", + "justNow": "刚刚", + "minutesAgo": "{n} 分钟前", + "hoursAgo": "{n} 小时前", + "daysAgo": "{n} 天前", + "textareaMultipleNotSupported": "文本域暂不支持多选", + "passwordMultipleNotSupported": "密码暂不支持多选", + "pressEnterForNumber": "输入数字后按回车" + }, + "login": { + "userPlaceholder": "用户名 / 手机 / 邮箱", + "PWPlaceholder": "请输入密码", + "signIn": "登录", + "userError": "请输入用户名", + "PWError": "请输入密码" + }, + "power": { + "add": "新增", + "edit": "编辑", + "delete": "删除", + "list": "列表", + "get": "详情", + "importExcel": "导入Excel", + "exportExcel": "导出Excel", + "view": "查看", + "clear": "清除", + "export": "导出", + "import": "导入" + }, + "shrimp": { + "loginFailed": "用户名或密码错误", + "userNotFound": "用户不存在", + "relogin": "请重新登录", + "userDisabled": "用户已被禁用", + "emptyPath": "请求路径不能为空", + "forbidden": "权限不足", + "serverError": "服务器错误", + "cannotDisableSelf": "不能禁用自己", + "cannotDeleteSelf": "不能删除自己", + "cannotChangeOwnRole": "不能修改自己的角色", + "cannotEditOwnRole": "不能编辑自己的角色", + "cannotDeleteOwnRole": "不能删除自己的角色", + "cannotClearAllUsers": "不能清空所有用户", + "cannotClearAllRoles": "不能清空所有角色", + "fieldRequired": "字段\"{field}\"不能为空", + "fieldTypeError": "字段\"{field}\"类型错误", + "fieldInvalid": "字段\"{field}\"值不合法", + "fieldMin": "字段\"{field}\"最小值为 {min}", + "fieldMax": "字段\"{field}\"最大值为 {max}", + "fieldMinLength": "字段\"{field}\"最小长度为 {min}", + "fieldMaxLength": "字段\"{field}\"最大长度为 {max}", + "invalidJson": "JSON格式错误" + } +} diff --git a/src/module/FieldRelativeTime.vue b/src/module/FieldRelativeTime.vue new file mode 100644 index 0000000..1fdf623 --- /dev/null +++ b/src/module/FieldRelativeTime.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/module/role.vue b/src/module/role.vue new file mode 100644 index 0000000..77cc9ff --- /dev/null +++ b/src/module/role.vue @@ -0,0 +1,238 @@ + + + + + diff --git a/src/module/user.vue b/src/module/user.vue new file mode 100644 index 0000000..bbbb72a --- /dev/null +++ b/src/module/user.vue @@ -0,0 +1,14 @@ + + + + + diff --git a/src/module/user/operationLog.vue b/src/module/user/operationLog.vue new file mode 100644 index 0000000..a125b77 --- /dev/null +++ b/src/module/user/operationLog.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/src/shims.d.ts b/src/shims.d.ts new file mode 100644 index 0000000..1a9fbc3 --- /dev/null +++ b/src/shims.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/src/style/app.scss b/src/style/app.scss new file mode 100644 index 0000000..8bdf1b5 --- /dev/null +++ b/src/style/app.scss @@ -0,0 +1,521 @@ +/* 全局 */ +#app, +body, +html { + width: 100%; + height: 100%; + background-color: #f6f8f9; + font-size: 12px; + font-family: "sans-serif"; +} + +a { + color: #333; + text-decoration: none; +} + +a:hover, +a:focus { + color: #000; + text-decoration: none; +} + +a:link { + text-decoration: none; +} + +a:-webkit-any-link { + text-decoration: none; +} + +a, +button, +input, +textarea { + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + box-sizing: border-box; + outline: none !important; + -webkit-appearance: none; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + outline: none; +} + +/* 大布局样式 */ +.aminui { + display: flex; + flex-flow: column; +} + +.aminui-wrapper { + display: flex; + flex: 1; + overflow: auto; +} + +/* 全局滚动条样式 */ +.scrollabel { + -webkit-overflow-scrolling: touch; +} + +::-webkit-scrollbar { + width: 5px; + height: 5px; +} + +::-webkit-scrollbar-thumb { + background-color: rgba(50, 50, 50, 0.3); +} + +::-webkit-scrollbar-thumb:hover { + background-color: rgba(50, 50, 50, 0.6); +} + +::-webkit-scrollbar-track { + background-color: rgba(50, 50, 50, 0.1); +} + +::-webkit-scrollbar-track:hover { + background-color: rgba(50, 50, 50, 0.2); +} + +/*布局设置*/ +.layout-setting { + position: fixed; + width: 40px; + height: 40px; + border-radius: 3px 0 0 3px; + bottom: 100px; + right: 0px; + z-index: 100; + background: #409EFF; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.layout-setting i { + font-size: 18px; + color: #fff; +} + +/* 头部 */ +.adminui-header { + height: 58px; + background: #222b45; + color: #fff; + display: flex; + justify-content: space-between; +} + +.adminui-header-left { + display: flex; + align-items: center; + padding-left: 20px; +} + +.adminui-header-right { + display: flex; + align-items: center; +} + +.adminui-header .logo-bar { + font-size: 20px; + font-weight: bold; + display: flex; + align-items: center; +} + +.adminui-header .logo-bar .logo { + margin-right: 10px; + width: 35px; + height: 35px; +} + +.adminui-header .nav { + display: flex; + height: 100%; + margin-left: 40px; +} + +.adminui-header .nav li { + padding: 0 10px; + margin: 0 10px 0 0; + font-size: 14px; + color: rgba(255, 255, 255, 0.6); + list-style: none; + height: 100%; + display: flex; + align-items: center; + cursor: pointer; +} + +.adminui-header .nav li i { + margin-right: 5px; +} + +.adminui-header .nav li:hover { + color: #fff; +} + +.adminui-header .nav li.active { + background: rgba(255, 255, 255, 0.1); + color: #fff; +} + +.adminui-header .user-bar .panel-item:hover { + background: rgba(255, 255, 255, 0.1) !important; +} + +.adminui-header .user-bar .user label { + color: #fff; +} + +/* 左侧菜单 */ +.aminui-side-split { + width: 65px; + flex-shrink: 0; + background: #222b45; + display: flex; + flex-flow: column; +} + +.aminui-side-split-top { + height: 49px; +} + +.aminui-side-split-top a { + display: inline-block; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.aminui-side-split-top .logo { + height: 30px; + vertical-align: bottom; +} + +.adminui-side-split-scroll { + display: flex; + flex-direction: column; + overflow: hidden; + height: 100%; + flex: 1; +} + +.aminui-side-split li { + cursor: pointer; + width: 65px; + height: 65px; + color: #fff; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.aminui-side-split li i { + font-size: 18px; +} + +.aminui-side-split li p { + margin-top: 5px; + padding: 0 4px; + font-size: 12px; + line-height: 1.2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; +} + +.aminui-side-split li:hover { + background: rgba(255, 255, 255, 0.1); +} + +.aminui-side-split li.active { + background: #409EFF; +} + +.adminui-side-split-scroll::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255, 0.4); + border-radius: 5px; +} + +.adminui-side-split-scroll::-webkit-scrollbar-thumb:hover { + background-color: rgba(255, 255, 255, 0.5); +} + +.adminui-side-split-scroll::-webkit-scrollbar-track { + background-color: rgba(255, 255, 255, 0); +} + +.adminui-side-split-scroll::-webkit-scrollbar-track:hover { + background-color: rgba(255, 255, 255, 0); +} + +.aminui-side { + display: flex; + flex-flow: column; + flex-shrink: 0; + width: 210px; + background: #fff; + box-shadow: 2px 0 8px 0 rgba(29, 35, 41, .05); + border-right: 1px solid #e6e6e6; + transition: width 0.3s; +} + +.adminui-side-top { + border-bottom: 1px solid #ebeef5; + height: 50px; + line-height: 50px; +} + +.adminui-side-top h2 { + padding: 0 20px; + font-size: 17px; + color: #3c4a54; +} + +.adminui-side-scroll { + overflow: auto; + overflow-x: hidden; + flex: 1; +} + +.adminui-side-bottom { + border-top: 1px solid #ebeef5; + height: 51px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.adminui-side-bottom i { + font-size: 16px; +} + +.adminui-side-bottom:hover { + color: var(--el-color-primary); +} + +.aminui-side.isCollapse { + width: 65px; +} + +.el-menu .menu-tag { + position: absolute; + height: 18px; + line-height: 18px; + background: var(--el-color-danger); + font-size: 12px; + color: #fff; + right: 20px; + border-radius: 18px; + padding: 0 6px; +} + +.el-menu .el-sub-menu__title .menu-tag { + right: 40px; +} + +.el-menu--horizontal>li .menu-tag { + display: none; +} + +/* 右侧内容 */ +.aminui-body { + flex: 1; + display: flex; + flex-flow: column; +} + +.adminui-topbar { + height: 50px; + border-bottom: 1px solid #ebeef5; + background: #fff; + box-shadow: 0 1px 4px rgba(0, 21, 41, .08); + display: flex; + justify-content: space-between; +} + +.adminui-topbar .left-panel { + display: flex; + align-items: center; +} + +.adminui-topbar .right-panel { + display: flex; + align-items: center; +} + +.right-panel-search { + display: flex; + align-items: center; +} + +.right-panel-search>*+* { + margin-left: 10px; +} + +.adminui-tags { + height: 35px; + background: #fff; + border-bottom: 1px solid #e6e6e6; +} + +.adminui-tags ul { + display: flex; + overflow: hidden; +} + +.adminui-tags li { + cursor: pointer; + display: inline-block; + float: left; + height: 34px; + line-height: 34px; + position: relative; + flex-shrink: 0; +} + +.adminui-tags li::after { + content: " "; + width: 1px; + height: 100%; + position: absolute; + right: 0px; + background-image: linear-gradient(#fff, #e6e6e6); +} + +.adminui-tags li a { + display: inline-block; + padding: 0 10px; + width: 100%; + height: 100%; + color: #999; + text-decoration: none; + display: flex; + align-items: center; +} + +.adminui-tags li i { + margin-left: 10px; + border-radius: 3px; + width: 18px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; +} + +.adminui-tags li i:hover { + background: rgba(0, 0, 0, .2); + color: #fff; +} + +.adminui-tags li:hover { + background: #ecf5ff; +} + +.adminui-tags li.active { + background: #409EFF; +} + +.adminui-tags li.active a { + color: #fff; +} + +.adminui-tags li.sortable-ghost { + opacity: 0; +} + +.adminui-main { + overflow: hidden; + background-color: #f6f8f9; + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; +} + +/* 让路由内容铺满 .adminui-main,使内部的滚动容器(如 ShrimpTable 的 .el-main)能拿到正确高度 */ +.adminui-main > * { + flex: 1; + min-height: 0; +} + +/*页面最大化*/ +.aminui.main-maximize { + .main-maximize-exit { + display: block; + } + + .aminui-side-split, + .aminui-side, + .adminui-header, + .adminui-topbar, + .adminui-tags { + display: none; + } +} + +.main-maximize-exit { + display: none; + position: fixed; + z-index: 3000; + top: -20px; + left: 50%; + margin-left: -20px; + border-radius: 50%; + width: 40px; + height: 40px; + cursor: pointer; + background: rgba(0, 0, 0, 0.2); + text-align: center; +} + +.main-maximize-exit i { + font-size: 14px; + margin-top: 22px; + color: #fff; +} + +.main-maximize-exit:hover { + background: rgba(0, 0, 0, 0.4); +} + +/*定宽页面*/ +.sc-page { + width: 1230px; + margin: 0 auto; +} + +.el-drawer .el-form-item { + display: block; +} + +// .el-drawer.rtl {width: min(600px, 100%)!important;} +//新增全局字体样式 +.el-tag+.el-tag { + margin-left: 5px; +} + +.el-button+.el-button { + margin-left: 5px; +} + +.el-table .el-table__cell { + padding: 5px; +} \ No newline at end of file diff --git a/src/style/dark.scss b/src/style/dark.scss new file mode 100644 index 0000000..2f551f5 --- /dev/null +++ b/src/style/dark.scss @@ -0,0 +1,42 @@ +@use 'element-plus/theme-chalk/src/dark/css-vars.scss'; + +html.dark { + //变量 + --el-text-color-primary: #d0d0d0; + --el-color-primary-dark-2: var(--el-color-primary-light-2) !important; + --el-color-primary-light-9: var(--el-color-primary-dark-8) !important; + --el-color-primary-light-8: var(--el-color-primary-dark-7) !important; + --el-color-primary-light-7: var(--el-color-primary-dark-6) !important; + --el-color-primary-light-5: var(--el-color-primary-dark-4) !important; + --el-color-primary-light-3: var(--el-color-primary-dark-3) !important; + + //背景 + #app {background: var(--el-bg-color);} + + //登录背景 + .login_bg {background: var(--el-bg-color);} + + //框架 + .adminui-header {background: var(--el-bg-color-overlay);border-bottom: 1px solid var(--el-border-color-light);height:59px;} + .aminui-side-split {background: var(--el-bg-color);} + .aminui-side-split li {color: var(--el-text-color-primary);} + .aminui-side {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);} + .adminui-side-top, .adminui-side-bottom {border-color: var(--el-border-color-light);} + .adminui-side-top h2 {color: var(--el-text-color-primary);} + .adminui-topbar, .adminui-tags {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);} + .adminui-main {background: var(--el-bg-color);} + .drawerBG {background: var(--el-bg-color);} + .adminui-header-menu .el-menu {--el-menu-bg-color:var(--el-bg-color-overlay) !important;--el-menu-hover-bg-color: #171819 !important;} + .adminui-header-menu .el-menu .el-sub-menu__title {background-color:transparent !important;} + + //全局滚动条样式 + ::-webkit-scrollbar-thumb {background-color: rgba(163, 166, 173, 0.3);} + ::-webkit-scrollbar-thumb:hover {background-color: rgba(163, 166, 173, 0.5);} + + //组件 + .el-header, .el-main.nopadding, .el-footer {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);} + .el-main {background: var(--el-bg-color);} + .el-aside {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);} + .el-table .el-table__body-wrapper {background: var(--el-bg-color);} + .el-table th.is-sortable:hover {background: #111;} +} diff --git a/src/style/fix.scss b/src/style/fix.scss new file mode 100644 index 0000000..4b2f08c --- /dev/null +++ b/src/style/fix.scss @@ -0,0 +1,91 @@ +/* 覆盖element-plus样式 */ + +:root { + --el-color-primary: #409EFF; + --el-color-primary-light-1: #53a7ff; + --el-color-primary-light-2: #66b1ff; + --el-color-primary-light-3: #79bbff; + --el-color-primary-light-4: #8cc4ff; + --el-color-primary-light-5: #9fceff; + --el-color-primary-light-6: #b2d8ff; + --el-color-primary-light-7: #c5e1ff; + --el-color-primary-light-8: #d8ebff; + --el-color-primary-light-9: #ebf5ff; + --el-color-primary-dark-1: #398ee5; + --el-color-primary-dark-2: #337ecc; + --el-color-primary-dark-3: #2c6eb2; + --el-color-primary-dark-4: #265e99; + --el-color-primary-dark-5: #204f7f; + --el-color-primary-dark-6: #193f66; + --el-color-primary-dark-7: #132f4c; + --el-color-primary-dark-8: #0c1f32; + --el-color-primary-dark-9: #060f19; +} + +.el-menu {border: none!important;} +.el-menu .el-menu-item a {color: inherit;text-decoration: none;display: block;width:100%;height:100%;position: absolute;top:0px;left:0px;} +.el-form-item-msg {font-size: 12px;color: #999;clear: both;width: 100%;} +.el-container {height: 100%;} +.el-aside {border-right: 1px solid var(--el-border-color-light);} +.el-container + .el-aside {border-right: 0;border-left: 1px solid var(--el-border-color-light);} +.el-header {background: #fff;border-bottom: 1px solid var(--el-border-color-light);padding:13px 15px;display: flex;align-items: center;} +.el-header .left-panel {display: flex;align-items: center;} +.el-header .right-panel {display: flex;align-items: center;} +.el-header .right-panel > * + * {margin-left:10px;} +.el-footer {background: #fff;border-top: 1px solid var(--el-border-color-light);padding:13px 15px;} +.el-main {padding:15px;} +.el-main.nopadding {padding:0;background: #fff;} +.el-drawer__body {overflow: auto;padding:0;} +.el-popconfirm__main {margin: 14px 0;} +// .el-card__header {border-bottom: 0;font-size: 17px;font-weight: bold;padding:15px 20px 0px 20px;} +// .el-card__header {padding:5px 20px 5px 20px;} +.el-dialog__title {font-size: 17px;font-weight: bold;} +.el-drawer__header>:first-child {font-size: 17px;font-weight: bold;} +.el-tree.menu .el-tree-node__content {height:36px;} +.el-tree.menu .el-tree-node__content .el-tree-node__label .icon {margin-right: 5px;} +.el-progress__text {font-size: 12px!important;} +.el-progress__text i {font-size: 14.4px!important;} +.el-step.is-horizontal .el-step__line {height:1px;} +.el-step__title {font-size: 14px;} +.drawerBG {background: #f6f8f9;} +.el-button+.el-dropdown {margin-left: 10px;} +.el-button-group+.el-dropdown {margin-left: 10px;} +.el-button-group+.el-button-group {margin-left: 10px;} +.el-tabs__nav-wrap::after {height: 1px;} +.el-table th.is-sortable {transition: .1s;} +.el-table th.is-sortable:hover {background: #eee;} +.el-table .el-table__body-wrapper {background: #f6f8f9;} +.el-col .el-card {margin-bottom: 15px;} +.el-main {flex-basis: 100%;} +.el-main > .scTable .el-table--border::before {display: none;} +.el-main > .scTable .el-table--border::after {display: none;} +.el-main > .scTable .el-table--border .el-table__inner-wrapper::after {display: none;} +.el-main > .scTable .el-table__border-left-patch {display: none;} +.el-main > .scTable .el-table--border .el-table__inner-wrapper tr:first-child td:first-child {border-left: 0;} +.el-main > .scTable .el-table--border .el-table__inner-wrapper tr:first-child th:first-child {border-left: 0;} +.el-table.el-table--large {font-size: 14px;} +.el-table.el-table--small {font-size: 12px;} +.el-table {font-size: 12px;} +.el-radio-button__inner {font-size: 12px;} +.el-checkbox-button__inner {font-size: 12px;} +.el-sub-menu .el-icon {font-size: 17px;} +.el-sub-menu .el-sub-menu__icon-arrow {font-size: 12px;} + +.aminui-side-split li.active {background-color: var(--el-color-primary);} +.adminui-tags li:hover {background-color: var(--el-color-primary-light-9);} +.adminui-tags li.active {background-color: var(--el-color-primary)!important;} +.contextmenu li:hover {background-color: var(--el-color-primary-light-9)!important;color: var(--el-color-primary-light-2)!important;} +.data-box .item-background {background-color: var(--el-color-primary)!important;} +.layout-setting,.diy-grid-setting {background-color: var(--el-color-primary)!important;} + +/* 全部禁用el-tag动画 */ +.el-tag {transition: all 0s !important;} + +/* 覆盖tinymce样式 */ +.sceditor .tox-tinymce {border: 1px solid #DCDFE6;border-radius: 0;} +body .tox-tinymce-aux {z-index: 5700;} + +//新增 +.el-table .cell{ + word-break: break-word; +} \ No newline at end of file diff --git a/src/style/media.scss b/src/style/media.scss new file mode 100644 index 0000000..559198c --- /dev/null +++ b/src/style/media.scss @@ -0,0 +1,174 @@ +@media (max-width: 992px) { + + // 移动端样式覆盖 + .el-form-item { + display: block; + } + + .el-form-item__label { + margin: 5px !important; + } + + .el-dialog { + width: 90% !important; + } + + .el-dialog.is-fullscreen { + width: 100% !important; + } + + .el-drawer.rtl { + width: min(400px, 100%) !important; + } + + .el-form-item__content { + margin: 5px !important; + } + // .arrayInput{ + // width: 100% !important; + // } + + .adminui-main { + >.el-container { + display: block; + height: auto; + } + + >.el-container>.el-aside { + width: 100% !important; + border: 0 + } + } + + .scTable { + + .el-table, + .el-table__body-wrapper { + display: block !important; + height: auto !important; + } + + .el-scrollbar__wrap { + height: auto !important; + } + + .scTable-page { + padding: 0 5px !important; + } + + .el-pagination__total, + .el-pagination__jump, + .el-pagination__sizes { + display: none !important; + } + } + + .headerPublic { + height: auto !important; + display: block; + + .left-panel { + overflow: auto; + } + + .left-panel::-webkit-scrollbar { + display: none; + } + + .right-panel { + display: block; + border-top: 1px solid var(--el-border-color-light); + margin-top: 15px; + } + + .right-panel .right-panel-search { + display: block; + } + + .right-panel .right-panel-search>* { + width: 100%; + margin: 0; + margin-top: 15px; + } + } + + .adminui-main>.el-container>*:first-child:not(.el-aside):not(.el-header) { + border: 0; + margin-top: 0; + } + + .adminui-main>.el-container>*:first-child:not(.el-aside):not(.el-header)+.el-aside { + margin-top: 0; + } + + .adminui-main>.el-container>.el-aside { + border-bottom: 1px solid var(--el-border-color-light) !important; + } + + .adminui-main>.el-container>.el-container { + border-top: 1px solid var(--el-border-color-light); + border-bottom: 1px solid var(--el-border-color-light); + margin-top: 15px; + } + + .adminui-main>.el-container>.el-container+.el-aside { + border-top: 1px solid var(--el-border-color-light); + margin-top: 15px; + } + + .adminui-main>.el-container>.el-header { + @extend .headerPublic; + } + + .adminui-main>.el-container>.el-main.nopadding { + border-top: 1px solid var(--el-border-color-light); + border-bottom: 1px solid var(--el-border-color-light); + margin-top: 15px; + } + + .adminui-main>.el-container>.el-main+.el-aside { + border-left: 0 !important; + border-top: 1px solid var(--el-border-color-light); + margin-top: 15px; + } + + .adminui-main>.el-container>.el-footer { + margin-top: 15px; + border-bottom: 1px solid var(--el-border-color-light); + } + + .adminui-main>.el-container>.el-container>.el-header { + @extend .headerPublic + } + + .adminui-main>.el-container>.el-container>.el-header .left-panel { + display: block; + } + + .adminui-main>.el-container>.el-container>.el-header .right-panel { + display: block; + margin-top: 15px; + } + + .sc-page { + width: 100%; + margin: 0; + } + + .common-main .el-form { + width: 100% !important; + } + + .common-header-logo label { + display: none; + } + + .common-header-title { + display: none; + } + + //手机端没有左右边距 新增 + .el-main { + padding: 15px 0px; + } +} \ No newline at end of file diff --git a/src/style/pages.scss b/src/style/pages.scss new file mode 100644 index 0000000..564c8ca --- /dev/null +++ b/src/style/pages.scss @@ -0,0 +1,44 @@ +/* USERCENTER */ +.page-user { + .user-info-top {text-align: center;} + .user-info-top h2 {font-size: 18px;margin-top: 5px;} + .user-info-top p {margin: 8px 0 10px 0;} + .menu {background: none;} + .menu .el-menu-item {font-size: 12px;--el-menu-item-height:50px;} + .menu .el-menu-item-group {border-top: 1px solid var(--el-border-color-light);} + .menu .el-menu-item-group:first-child {border: 0;} +} + +/*static-table*/ +.static-table {border-collapse: collapse;width: 100%;font-size: 14px;margin-bottom: 45px;line-height: 1.5em;} +.static-table th {text-align: left;white-space: nowrap;color: #909399;font-weight: 400;border-bottom: 1px solid #dcdfe6;padding: 15px;max-width: 250px;} +.static-table td {border-bottom: 1px solid #dcdfe6;padding: 15px;max-width: 250px;color: #606266;} + +/*header-tabs*/ +.header-tabs {padding:10px 0 0 0;display:block;border:0!important;height:50px;background: none;} +.header-tabs .el-tabs__header {padding-left:10px;margin: 0;} +.header-tabs .el-tabs__content {display: none;} +.header-tabs .el-tabs__nav {border-radius: 0 !important;} +.header-tabs .el-tabs__item {font-size: 13px;} +.header-tabs .el-tabs__item.is-active {background-color: var(--el-bg-color-overlay);} + +/*common-page*/ +.common-page {} +.common-header-left {display: flex;align-items: center;} +.common-header-logo {display: flex;align-items: center;} +.common-header-logo img {height:30px;margin-right: 10px;vertical-align: bottom;} +.common-header-logo label {font-size: 20px;} +.common-header-title {font-size: 16px;border-left: 1px solid var(--el-border-color-light);margin-left: 15px;padding-left: 15px;} +.common-header-right {display: flex;align-items: center;} +.common-header-right a {font-size: 14px;color: var(--el-color-primary);cursor: pointer;} +.common-header-right a:hover {color: var(--el-color-primary-light-3);} +.common-container {max-width: 1240px;margin:30px auto 30px auto;} +.common-main {padding:20px;} +.common-title {font-size: 26px;margin-bottom: 20px;font-weight: normal;} +.common-main .el-form {width: 500px;margin:30px auto;} +.common-main .el-steps .el-step__title {font-size: 14px;} +.common-main .el-steps .el-step__icon {border: 1px solid;} +.common-main .yzm {display: flex;width: 100%;} +.common-main .yzm .el-button {margin-left: 10px;} +.common-main .link {color: var(--el-color-primary);cursor: pointer;} +.common-main .link:hover {color: var(--el-color-primary-light-3);} diff --git a/src/style/style.scss b/src/style/style.scss new file mode 100644 index 0000000..467ac17 --- /dev/null +++ b/src/style/style.scss @@ -0,0 +1,6 @@ +@use 'app.scss'; +@use 'fix.scss'; +@use 'pages.scss'; +@use 'media.scss'; +@use 'dark.scss'; +@use 'zd.scss'; diff --git a/src/style/zd.scss b/src/style/zd.scss new file mode 100644 index 0000000..6f20cee --- /dev/null +++ b/src/style/zd.scss @@ -0,0 +1,48 @@ +.zd-flex-center { + display: flex; + justify-content: center; + align-items: center; +} + +.zd-flex-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.zd-flex-column { + display: flex; + flex-direction: column; +} + +.zd-padding-0 { + padding: 0px; +} + +.zd-padding-1 { + padding: 1px; +} + +.zd-padding-2 { + padding: 2px; +} + +.zd-padding-3 { + padding: 3px; +} + +.zd-padding-5 { + padding: 5px; +} + +.zd-padding-8 { + padding: 8px; +} + +.zd-padding-10 { + padding: 10px; +} + +.zd-aside+.zd-aside { + margin-left: 10px; +} \ No newline at end of file diff --git a/src/views/404.vue b/src/views/404.vue new file mode 100644 index 0000000..e6836a5 --- /dev/null +++ b/src/views/404.vue @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/src/views/App.vue b/src/views/App.vue new file mode 100644 index 0000000..10e19ec --- /dev/null +++ b/src/views/App.vue @@ -0,0 +1,24 @@ + + + + diff --git a/src/views/layout.vue b/src/views/layout.vue new file mode 100644 index 0000000..81179d3 --- /dev/null +++ b/src/views/layout.vue @@ -0,0 +1,110 @@ + + + + diff --git a/src/views/login.vue b/src/views/login.vue new file mode 100644 index 0000000..c608829 --- /dev/null +++ b/src/views/login.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/src/views/logo.vue b/src/views/logo.vue new file mode 100644 index 0000000..79264bc --- /dev/null +++ b/src/views/logo.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/src/views/navMenu.vue b/src/views/navMenu.vue new file mode 100644 index 0000000..4fa5b05 --- /dev/null +++ b/src/views/navMenu.vue @@ -0,0 +1,37 @@ + + + diff --git a/src/views/sideM.vue b/src/views/sideM.vue new file mode 100644 index 0000000..db4dde1 --- /dev/null +++ b/src/views/sideM.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/src/views/topbar.vue b/src/views/topbar.vue new file mode 100644 index 0000000..2a4dad6 --- /dev/null +++ b/src/views/topbar.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/src/views/userbar.vue b/src/views/userbar.vue new file mode 100644 index 0000000..89a1010 --- /dev/null +++ b/src/views/userbar.vue @@ -0,0 +1,90 @@ + + + + + + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0741dc5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "bundler", + "strict": true, + "jsx": "preserve", + "esModuleInterop": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "lib": ["dom", "esnext"], + "types": ["vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts"] +}