Compare commits
4 Commits
169ccc8632
...
74d8e669c0
| Author | SHA1 | Date | |
|---|---|---|---|
| 74d8e669c0 | |||
| ef60ebf8b2 | |||
| 516b204b38 | |||
| ca3d697941 |
14
.vscode/extensions.json
vendored
Normal file
14
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"antfu.unocss",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"kisstkondoros.vscode-gutter-preview",
|
||||
"mariusalchimavicius.json-to-ts",
|
||||
"mhutchie.git-graph",
|
||||
"sdras.vue-vscode-snippets",
|
||||
"vue.volar",
|
||||
"vue.vscode-typescript-vue-plugin"
|
||||
]
|
||||
}
|
||||
22
.vscode/launch.json
vendored
Normal file
22
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Vue Debugger",
|
||||
"url": "http://localhost:9527",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "TS Debugger",
|
||||
"runtimeExecutable": "tsx",
|
||||
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"],
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
}
|
||||
]
|
||||
}
|
||||
31
.vscode/settings.json
vendored
Normal file
31
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.formatOnSave": false,
|
||||
"eslint.validate": [
|
||||
"html",
|
||||
"css",
|
||||
"scss",
|
||||
"json",
|
||||
"jsonc",
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue"
|
||||
],
|
||||
"i18n-ally.displayLanguage": "zh-cn",
|
||||
"i18n-ally.enabledParsers": ["ts"],
|
||||
"i18n-ally.enabledFrameworks": ["vue"],
|
||||
"i18n-ally.editor.preferEditor": true,
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": ["src/locales/langs"],
|
||||
"i18n-ally.parsers.typescript.compilerOptions": {
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"prettier.enable": false,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"unocss.root": ["./"]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generatedAt": "2026-03-25T08:15:18.925Z",
|
||||
"generatedAt": "2026-03-27T05:39:32.467Z",
|
||||
"description": "Frontend visible page resource whitelist for backend route/menu configuration.",
|
||||
"rules": {
|
||||
"directoryComponent": "layout.base",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineConfig } from '@soybeanjs/eslint-config';
|
||||
import tsEslintPlugin from '@typescript-eslint/eslint-plugin';
|
||||
|
||||
export default defineConfig(
|
||||
{ vue: true, unocss: true },
|
||||
@@ -20,5 +21,22 @@ export default defineConfig(
|
||||
],
|
||||
'unocss/order-attributify': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx', '**/*.vue'],
|
||||
plugins: {
|
||||
'@typescript-eslint': tsEslintPlugin
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
varsIgnorePattern: '^_',
|
||||
argsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
"cleanup": "sa cleanup",
|
||||
"commit": "sa git-commit",
|
||||
"commit:zh": "sa git-commit -l=zh-cn",
|
||||
"create-route": "sa gen-route",
|
||||
"dev": "vite --mode dev",
|
||||
"dev:prod": "vite --mode prod",
|
||||
"create-route": "sa gen-route",
|
||||
"gen:page-resource-manifest": "node --experimental-strip-types scripts/generate-page-resource-manifest.mjs",
|
||||
"gen-route": "node --experimental-strip-types scripts/generate-elegant-router.mjs",
|
||||
"gen:page-resource-manifest": "node --experimental-strip-types scripts/generate-page-resource-manifest.mjs",
|
||||
"lint": "eslint . --fix",
|
||||
"prepare": "simple-git-hooks",
|
||||
"preview": "vite preview",
|
||||
@@ -62,6 +62,7 @@
|
||||
"echarts": "6.0.0",
|
||||
"element-plus": "^2.11.1",
|
||||
"jsbarcode": "3.12.1",
|
||||
"jsencrypt": "^3.5.4",
|
||||
"json5": "2.2.3",
|
||||
"nprogress": "0.2.0",
|
||||
"pinia": "3.0.3",
|
||||
|
||||
@@ -11,7 +11,6 @@ export const locales = {
|
||||
},
|
||||
gitCommitTypes: [
|
||||
['feat', '新功能'],
|
||||
['feat-wip', '开发中的功能,比如某功能的部分代码'],
|
||||
['fix', '修复Bug'],
|
||||
['docs', '只涉及文档更新'],
|
||||
['typo', '代码或文档勘误,比如错误拼写'],
|
||||
@@ -49,7 +48,6 @@ export const locales = {
|
||||
},
|
||||
gitCommitTypes: [
|
||||
['feat', 'A new feature'],
|
||||
['feat-wip', 'Features in development, such as partial code for a certain feature'],
|
||||
['fix', 'A bug fix'],
|
||||
['docs', 'Documentation only changes'],
|
||||
['typo', 'Code or document corrections, such as spelling errors'],
|
||||
|
||||
56
pnpm-lock.yaml
generated
56
pnpm-lock.yaml
generated
@@ -83,6 +83,9 @@ importers:
|
||||
jsbarcode:
|
||||
specifier: 3.12.1
|
||||
version: 3.12.1
|
||||
jsencrypt:
|
||||
specifier: ^3.5.4
|
||||
version: 3.5.4
|
||||
json5:
|
||||
specifier: 2.2.3
|
||||
version: 2.2.3
|
||||
@@ -2227,8 +2230,8 @@ packages:
|
||||
resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
baseline-browser-mapping@2.10.12:
|
||||
resolution: {integrity: sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==}
|
||||
baseline-browser-mapping@2.10.10:
|
||||
resolution: {integrity: sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -2248,11 +2251,11 @@ packages:
|
||||
boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
brace-expansion@1.1.13:
|
||||
resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==}
|
||||
brace-expansion@1.1.12:
|
||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||
|
||||
brace-expansion@2.0.3:
|
||||
resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==}
|
||||
brace-expansion@2.0.2:
|
||||
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
||||
|
||||
brace-expansion@5.0.5:
|
||||
resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==}
|
||||
@@ -2326,8 +2329,8 @@ packages:
|
||||
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
caniuse-lite@1.0.30001782:
|
||||
resolution: {integrity: sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==}
|
||||
caniuse-lite@1.0.30001781:
|
||||
resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==}
|
||||
|
||||
cfb@1.2.2:
|
||||
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
|
||||
@@ -2424,8 +2427,8 @@ packages:
|
||||
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
comment-parser@1.4.6:
|
||||
resolution: {integrity: sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==}
|
||||
comment-parser@1.4.5:
|
||||
resolution: {integrity: sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
component-emitter@1.3.1:
|
||||
@@ -2839,8 +2842,8 @@ packages:
|
||||
echarts@6.0.0:
|
||||
resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
|
||||
|
||||
electron-to-chromium@1.5.328:
|
||||
resolution: {integrity: sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==}
|
||||
electron-to-chromium@1.5.325:
|
||||
resolution: {integrity: sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==}
|
||||
|
||||
element-plus@2.13.6:
|
||||
resolution: {integrity: sha512-XHgwXr8Fjz6i+6BaqFhAbae/dJbG7bBAAlHrY3pWL7dpj+JcqcOyKYt4Oy5KP86FQwS1k4uIZDjCx2FyUR5lDg==}
|
||||
@@ -3754,6 +3757,9 @@ packages:
|
||||
jsbarcode@3.12.1:
|
||||
resolution: {integrity: sha512-QZQSqIknC2Rr/YOUyOkCBqsoiBAOTYK+7yNN3JsqfoUtJtkazxNw1dmPpxuv7VVvqW13kA3/mKiLq+s/e3o9hQ==}
|
||||
|
||||
jsencrypt@3.5.4:
|
||||
resolution: {integrity: sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA==}
|
||||
|
||||
jsesc@3.0.2:
|
||||
resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -7496,7 +7502,7 @@ snapshots:
|
||||
mixin-deep: 1.3.2
|
||||
pascalcase: 0.1.1
|
||||
|
||||
baseline-browser-mapping@2.10.12: {}
|
||||
baseline-browser-mapping@2.10.10: {}
|
||||
|
||||
big.js@5.2.2: {}
|
||||
|
||||
@@ -7508,12 +7514,12 @@ snapshots:
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
brace-expansion@1.1.13:
|
||||
brace-expansion@1.1.12:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
concat-map: 0.0.1
|
||||
|
||||
brace-expansion@2.0.3:
|
||||
brace-expansion@2.0.2:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
@@ -7542,9 +7548,9 @@ snapshots:
|
||||
|
||||
browserslist@4.28.1:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.10.12
|
||||
caniuse-lite: 1.0.30001782
|
||||
electron-to-chromium: 1.5.328
|
||||
baseline-browser-mapping: 2.10.10
|
||||
caniuse-lite: 1.0.30001781
|
||||
electron-to-chromium: 1.5.325
|
||||
node-releases: 2.0.36
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||
|
||||
@@ -7624,7 +7630,7 @@ snapshots:
|
||||
|
||||
camelcase@6.3.0: {}
|
||||
|
||||
caniuse-lite@1.0.30001782: {}
|
||||
caniuse-lite@1.0.30001781: {}
|
||||
|
||||
cfb@1.2.2:
|
||||
dependencies:
|
||||
@@ -7731,7 +7737,7 @@ snapshots:
|
||||
|
||||
commander@7.2.0: {}
|
||||
|
||||
comment-parser@1.4.6: {}
|
||||
comment-parser@1.4.5: {}
|
||||
|
||||
component-emitter@1.3.1: {}
|
||||
|
||||
@@ -8133,7 +8139,7 @@ snapshots:
|
||||
tslib: 2.3.0
|
||||
zrender: 6.0.0
|
||||
|
||||
electron-to-chromium@1.5.328: {}
|
||||
electron-to-chromium@1.5.325: {}
|
||||
|
||||
element-plus@2.13.6(typescript@5.8.3)(vue@3.5.20(typescript@5.8.3)):
|
||||
dependencies:
|
||||
@@ -8348,7 +8354,7 @@ snapshots:
|
||||
eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.57.2(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.34.0(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.57.2
|
||||
comment-parser: 1.4.6
|
||||
comment-parser: 1.4.5
|
||||
debug: 4.4.3
|
||||
eslint: 9.34.0(jiti@2.6.1)
|
||||
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
|
||||
@@ -9172,6 +9178,8 @@ snapshots:
|
||||
|
||||
jsbarcode@3.12.1: {}
|
||||
|
||||
jsencrypt@3.5.4: {}
|
||||
|
||||
jsesc@3.0.2: {}
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
@@ -9365,11 +9373,11 @@ snapshots:
|
||||
|
||||
minimatch@3.1.5:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.13
|
||||
brace-expansion: 1.1.12
|
||||
|
||||
minimatch@9.0.9:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.3
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimist@1.2.0: {}
|
||||
|
||||
|
||||
@@ -2,12 +2,11 @@ import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { setupElegantRouter } from '../build/plugins/router.ts';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
const currentFilePath = fileURLToPath(import.meta.url);
|
||||
const currentDirPath = path.dirname(currentFilePath);
|
||||
const rootDir = path.resolve(currentDirPath, '..');
|
||||
|
||||
const generatedFiles = [
|
||||
path.resolve(rootDir, 'src/router/elegant/imports.ts'),
|
||||
@@ -36,17 +35,25 @@ function isGenerated(before, after) {
|
||||
async function waitForGeneration(beforeMtimes, timeoutMs = 10000) {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
async function poll() {
|
||||
const afterMtimes = await getGeneratedFileMtimes();
|
||||
|
||||
if (isGenerated(beforeMtimes, afterMtimes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
if (Date.now() - startTime >= timeoutMs) {
|
||||
throw new Error('Timed out while waiting for elegant-router generated files.');
|
||||
}
|
||||
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
await poll();
|
||||
}
|
||||
|
||||
throw new Error('Timed out while waiting for elegant-router generated files.');
|
||||
await poll();
|
||||
}
|
||||
|
||||
process.chdir(rootDir);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { customRoutes } from '../src/router/routes/custom-routes.ts';
|
||||
import { generatedRoutes } from '../src/router/elegant/routes.ts';
|
||||
import zhCn from '../src/locales/langs/zh-cn.ts';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
const currentFilePath = fileURLToPath(import.meta.url);
|
||||
const currentDirPath = path.dirname(currentFilePath);
|
||||
const rootDir = path.resolve(currentDirPath, '..');
|
||||
const outputPath = path.resolve(rootDir, 'docs/frontend-page-resource-manifest.json');
|
||||
const excludedPageResourceRouteNames = new Set(['exception_403', 'exception_404', 'exception_500']);
|
||||
const excludedPageResourcePathPrefixes = ['/function/', '/plugin/'];
|
||||
@@ -120,54 +119,76 @@ function getRouteLabel(route) {
|
||||
return route.meta?.title || route.name;
|
||||
}
|
||||
|
||||
function getPageResourceMeta(route) {
|
||||
const hideInMenu = Boolean(route.meta?.hideInMenu);
|
||||
const order = getNumberMetaValue(route.meta?.order);
|
||||
const fixedIndexInTab = getNumberMetaValue(route.meta?.fixedIndexInTab);
|
||||
|
||||
return {
|
||||
title: getRouteLabel(route),
|
||||
i18nKey: route.meta?.i18nKey || null,
|
||||
icon: route.meta?.icon || null,
|
||||
localIcon: route.meta?.localIcon || null,
|
||||
order,
|
||||
keepAlive: Boolean(route.meta?.keepAlive),
|
||||
hideInMenu,
|
||||
activeMenu: route.meta?.activeMenu || null,
|
||||
multiTab: Boolean(route.meta?.multiTab),
|
||||
fixedIndexInTab
|
||||
};
|
||||
}
|
||||
|
||||
function shouldCollectPageResource(route, pageComponent) {
|
||||
if (!pageComponent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (route.meta?.hideInMenu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !shouldExcludePageResource(route);
|
||||
}
|
||||
|
||||
function createPageResourceItem(route, options) {
|
||||
const { pageComponent, parentName, source } = options;
|
||||
const props = normalizeRouteProps(route.props);
|
||||
const meta = getPageResourceMeta(route);
|
||||
|
||||
return {
|
||||
name: route.name,
|
||||
path: route.path,
|
||||
component: pageComponent,
|
||||
title: meta.title,
|
||||
routeTitle: route.meta?.title || route.name,
|
||||
i18nKey: meta.i18nKey,
|
||||
icon: meta.icon,
|
||||
localIcon: meta.localIcon,
|
||||
order: meta.order,
|
||||
hideInMenu: meta.hideInMenu,
|
||||
keepAlive: meta.keepAlive,
|
||||
activeMenu: meta.activeMenu,
|
||||
multiTab: meta.multiTab,
|
||||
fixedIndexInTab: meta.fixedIndexInTab,
|
||||
redirect: route.redirect || null,
|
||||
props,
|
||||
meta,
|
||||
parentName,
|
||||
pageType: getPageType(pageComponent),
|
||||
source
|
||||
};
|
||||
}
|
||||
|
||||
function collectPageResources(routes, options, items) {
|
||||
const { parentName = null, source } = options;
|
||||
const orderedRoutes = sortRoutes(routes);
|
||||
|
||||
for (const route of orderedRoutes) {
|
||||
const pageComponent = getPageComponent(route.component);
|
||||
const hideInMenu = Boolean(route.meta?.hideInMenu);
|
||||
|
||||
// The backend page-node whitelist only contains menu-visible pages.
|
||||
if (pageComponent && !hideInMenu && !shouldExcludePageResource(route)) {
|
||||
const order = getNumberMetaValue(route.meta?.order);
|
||||
const fixedIndexInTab = getNumberMetaValue(route.meta?.fixedIndexInTab);
|
||||
const props = normalizeRouteProps(route.props);
|
||||
const meta = {
|
||||
title: getRouteLabel(route),
|
||||
i18nKey: route.meta?.i18nKey || null,
|
||||
icon: route.meta?.icon || null,
|
||||
localIcon: route.meta?.localIcon || null,
|
||||
order,
|
||||
keepAlive: Boolean(route.meta?.keepAlive),
|
||||
hideInMenu,
|
||||
activeMenu: route.meta?.activeMenu || null,
|
||||
multiTab: Boolean(route.meta?.multiTab),
|
||||
fixedIndexInTab
|
||||
};
|
||||
|
||||
items.push({
|
||||
name: route.name,
|
||||
path: route.path,
|
||||
component: pageComponent,
|
||||
title: meta.title,
|
||||
routeTitle: route.meta?.title || route.name,
|
||||
i18nKey: meta.i18nKey,
|
||||
icon: meta.icon,
|
||||
localIcon: meta.localIcon,
|
||||
order,
|
||||
hideInMenu,
|
||||
keepAlive: meta.keepAlive,
|
||||
activeMenu: meta.activeMenu,
|
||||
multiTab: meta.multiTab,
|
||||
fixedIndexInTab,
|
||||
redirect: route.redirect || null,
|
||||
props,
|
||||
meta,
|
||||
parentName,
|
||||
pageType: getPageType(pageComponent),
|
||||
source
|
||||
});
|
||||
if (shouldCollectPageResource(route, pageComponent)) {
|
||||
items.push(createPageResourceItem(route, { pageComponent, parentName, source }));
|
||||
}
|
||||
|
||||
if (route.children?.length) {
|
||||
@@ -179,8 +200,16 @@ function collectPageResources(routes, options, items) {
|
||||
function getPageResources() {
|
||||
const items = [];
|
||||
|
||||
collectPageResources(customRoutes.filter(route => !isConstantRoute(route)), { source: 'custom' }, items);
|
||||
collectPageResources(generatedRoutes.filter(route => !isConstantRoute(route)), { source: 'generated' }, items);
|
||||
collectPageResources(
|
||||
customRoutes.filter(route => !isConstantRoute(route)),
|
||||
{ source: 'custom' },
|
||||
items
|
||||
);
|
||||
collectPageResources(
|
||||
generatedRoutes.filter(route => !isConstantRoute(route)),
|
||||
{ source: 'generated' },
|
||||
items
|
||||
);
|
||||
|
||||
const uniqueItems = Array.from(new Map(items.map(item => [item.name, item])).values());
|
||||
|
||||
|
||||
@@ -68,10 +68,10 @@ export const menuRouteKindRecord: Record<Api.SystemManage.MenuRouteKind, App.I18
|
||||
|
||||
export const menuRouteKindOptions = transformRecordToOption(menuRouteKindRecord);
|
||||
|
||||
export const postTypeRecord: Record<Api.SystemManage.PostType, App.I18n.I18nKey> = {
|
||||
management: 'page.system.post.type.management',
|
||||
technical: 'page.system.post.type.technical',
|
||||
business: 'page.system.post.type.business'
|
||||
export const postTypeRecord: Record<Api.SystemManage.PostType, string> = {
|
||||
management: '管理岗',
|
||||
technical: '技术岗',
|
||||
business: '业务岗'
|
||||
};
|
||||
|
||||
export const postTypeOptions = transformRecordToOption(postTypeRecord);
|
||||
|
||||
@@ -40,12 +40,6 @@ const { isFullscreen, toggle } = useFullscreen();
|
||||
<div>
|
||||
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
|
||||
</div>
|
||||
<LangSwitch
|
||||
v-if="themeStore.header.multilingual.visible"
|
||||
:lang="appStore.locale"
|
||||
:lang-options="appStore.localeOptions"
|
||||
@change-lang="appStore.changeLocale"
|
||||
/>
|
||||
<ThemeSchemaSwitch
|
||||
:theme-schema="themeStore.themeScheme"
|
||||
:is-dark="themeStore.darkMode"
|
||||
|
||||
@@ -121,10 +121,7 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra
|
||||
placeholder="CN-RDMS"
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem key="9" :label="$t('theme.header.multilingual.visible')">
|
||||
<ElSwitch v-model="themeStore.header.multilingual.visible" />
|
||||
</SettingItem>
|
||||
<SettingItem key="10" :label="$t('theme.header.globalSearch.visible')">
|
||||
<SettingItem key="9" :label="$t('theme.header.globalSearch.visible')">
|
||||
<ElSwitch v-model="themeStore.header.globalSearch.visible" />
|
||||
</SettingItem>
|
||||
</TransitionGroup>
|
||||
|
||||
@@ -630,33 +630,6 @@ const local: App.I18n.Schema = {
|
||||
editType: '编辑字典类型',
|
||||
addData: '新增字典数据',
|
||||
editData: '编辑字典数据'
|
||||
},
|
||||
post: {
|
||||
title: '岗位列表',
|
||||
postName: '岗位名称',
|
||||
postCode: '岗位编码',
|
||||
postType: '岗位类型',
|
||||
levelRank: '岗位职级',
|
||||
sort: '排序',
|
||||
postStatus: '岗位状态',
|
||||
remark: '备注',
|
||||
createTime: '创建时间',
|
||||
form: {
|
||||
postName: '请输入岗位名称',
|
||||
postCode: '请输入岗位编码',
|
||||
postType: '请选择岗位类型',
|
||||
levelRank: '请输入岗位职级',
|
||||
sort: '请输入排序',
|
||||
postStatus: '请选择岗位状态',
|
||||
remark: '请输入备注'
|
||||
},
|
||||
addPost: '新增岗位',
|
||||
editPost: '编辑岗位',
|
||||
type: {
|
||||
management: '管理类',
|
||||
technical: '技术类',
|
||||
business: '业务类'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -244,8 +244,8 @@ export function fetchGetPostSimpleList() {
|
||||
}
|
||||
|
||||
/** 获取岗位分页 */
|
||||
export function fetchGetPostPage(params?: any) {
|
||||
return request({
|
||||
export function fetchGetPostPage(params?: Api.SystemManage.PostSearchParams) {
|
||||
return request<Api.SystemManage.PostList>({
|
||||
url: `${POST_PREFIX}/page`,
|
||||
method: 'get',
|
||||
params
|
||||
@@ -254,7 +254,7 @@ export function fetchGetPostPage(params?: any) {
|
||||
|
||||
/** 获取岗位详情 */
|
||||
export function fetchGetPost(id: number) {
|
||||
return request({
|
||||
return request<Api.SystemManage.Post>({
|
||||
url: `${POST_PREFIX}/get`,
|
||||
method: 'get',
|
||||
params: { id }
|
||||
@@ -262,8 +262,8 @@ export function fetchGetPost(id: number) {
|
||||
}
|
||||
|
||||
/** 创建岗位 */
|
||||
export function fetchCreatePost(data: any) {
|
||||
return request({
|
||||
export function fetchCreatePost(data: Api.SystemManage.SavePostParams) {
|
||||
return request<number>({
|
||||
url: `${POST_PREFIX}/create`,
|
||||
method: 'post',
|
||||
data
|
||||
@@ -271,8 +271,8 @@ export function fetchCreatePost(data: any) {
|
||||
}
|
||||
|
||||
/** 更新岗位 */
|
||||
export function fetchUpdatePost(data: { id: number } & any) {
|
||||
return request({
|
||||
export function fetchUpdatePost(data: { id: number } & Api.SystemManage.SavePostParams) {
|
||||
return request<boolean>({
|
||||
url: `${POST_PREFIX}/update`,
|
||||
method: 'put',
|
||||
data
|
||||
@@ -281,7 +281,7 @@ export function fetchUpdatePost(data: { id: number } & any) {
|
||||
|
||||
/** 删除岗位 */
|
||||
export function fetchDeletePost(id: number) {
|
||||
return request({
|
||||
return request<boolean>({
|
||||
url: `${POST_PREFIX}/delete`,
|
||||
method: 'delete',
|
||||
params: { id }
|
||||
@@ -290,7 +290,7 @@ export function fetchDeletePost(id: number) {
|
||||
|
||||
/** 批量删除岗位 */
|
||||
export function fetchBatchDeletePost(ids: number[]) {
|
||||
return request({
|
||||
return request<boolean>({
|
||||
url: `${POST_PREFIX}/delete-list?${createBatchDeleteQuery(ids)}`,
|
||||
method: 'delete'
|
||||
});
|
||||
|
||||
29
src/service/api/user-preference.ts
Normal file
29
src/service/api/user-preference.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||
import { request } from '../request';
|
||||
|
||||
const USER_PREFERENCE_THEME_PREFIX = `${SYSTEM_SERVICE_PREFIX}/user-preference/theme`;
|
||||
|
||||
/** 获取当前登录用户的主题偏好覆盖项 */
|
||||
export function fetchGetUserPreferenceTheme() {
|
||||
return request<Api.UserPreference.ThemeSettings>({
|
||||
url: USER_PREFERENCE_THEME_PREFIX,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/** 保存当前登录用户的主题偏好覆盖项 */
|
||||
export function fetchSaveUserPreferenceTheme(data: Api.UserPreference.ThemeSettings) {
|
||||
return request<boolean>({
|
||||
url: USER_PREFERENCE_THEME_PREFIX,
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/** 重置当前登录用户的主题偏好覆盖项 */
|
||||
export function fetchResetUserPreferenceTheme() {
|
||||
return request<boolean>({
|
||||
url: USER_PREFERENCE_THEME_PREFIX,
|
||||
method: 'delete'
|
||||
});
|
||||
}
|
||||
121
src/service/request/api-encrypt.ts
Normal file
121
src/service/request/api-encrypt.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { InternalAxiosRequestConfig } from 'axios';
|
||||
import { JSEncrypt } from 'jsencrypt';
|
||||
import { SYSTEM_SERVICE_PREFIX } from '@/constants/service';
|
||||
|
||||
const API_ENCRYPT_HEADER = 'X-Api-Encrypt';
|
||||
const API_ENCRYPT_HEADER_VALUE = 'true';
|
||||
|
||||
const API_ENCRYPT_CONFIG_ERROR_MSG = '前端加密配置异常,请联系管理员';
|
||||
const API_ENCRYPT_REQUEST_ERROR_MSG = '请求加密失败,请刷新页面后重试';
|
||||
|
||||
const API_ENCRYPT_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2kobVo5aSDWvmWZIKeL
|
||||
G7dowOLTwjdWIwy7+UmTBw3e6vJW5BNEjrnW7kiqeT97VQj2V6MMmaJufpeegACG
|
||||
AmhuTnG83kVLQeiXL5rlPUmdNPM8O89gSM3iMzLSUhn+rvAaHFXjKNu2xssodYn1
|
||||
F26SlVO1ewwS82AAwEPSaotL7Kq8Qxg7vmZty6RcEjp7/OaYAtHfva3uewiGMp11
|
||||
ZkywKPleQ3nT7HHjQgAckbNZFMhTMMqDzW5oI3KSm3sA+pWsUfRrZxUf2ws358/F
|
||||
KewDbbhwj3u731NbXlO+WUfv3FvbdhktXtU/15FC0b+Tx+YHIUhkNTRzuIpiG7+X
|
||||
cwIDAQAB
|
||||
-----END PUBLIC KEY-----`;
|
||||
|
||||
const API_ENCRYPT_RULE_MAP = {
|
||||
[`POST ${SYSTEM_SERVICE_PREFIX}/auth/login`]: ['password'],
|
||||
[`POST ${SYSTEM_SERVICE_PREFIX}/auth/register`]: ['password'],
|
||||
[`PUT ${SYSTEM_SERVICE_PREFIX}/user/profile/update-password`]: ['oldPassword', 'newPassword'],
|
||||
[`POST ${SYSTEM_SERVICE_PREFIX}/user/create`]: ['password'],
|
||||
[`PUT ${SYSTEM_SERVICE_PREFIX}/user/update-password`]: ['password']
|
||||
} as const satisfies Record<string, readonly string[]>;
|
||||
|
||||
type ApiEncryptRuleKey = keyof typeof API_ENCRYPT_RULE_MAP;
|
||||
|
||||
interface ApiEncryptRequestConfig extends InternalAxiosRequestConfig {
|
||||
apiEncryptProcessed?: boolean;
|
||||
}
|
||||
|
||||
let encryptor: JSEncrypt | null = null;
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return Object.prototype.toString.call(value) === '[object Object]';
|
||||
}
|
||||
|
||||
function normalizeRequestPath(url: string) {
|
||||
try {
|
||||
return new URL(url, 'http://localhost').pathname;
|
||||
} catch {
|
||||
return url.split('?')[0] || '';
|
||||
}
|
||||
}
|
||||
|
||||
function getApiEncryptRule(config: InternalAxiosRequestConfig) {
|
||||
const method = config.method?.toUpperCase();
|
||||
const url = config.url;
|
||||
|
||||
if (!method || !url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ruleKey = `${method} ${normalizeRequestPath(url)}` as ApiEncryptRuleKey;
|
||||
|
||||
return API_ENCRYPT_RULE_MAP[ruleKey] ?? null;
|
||||
}
|
||||
|
||||
function getEncryptor() {
|
||||
if (encryptor) {
|
||||
return encryptor;
|
||||
}
|
||||
|
||||
const publicKey = API_ENCRYPT_PUBLIC_KEY.trim();
|
||||
|
||||
if (!publicKey.includes('BEGIN PUBLIC KEY') || !publicKey.includes('END PUBLIC KEY')) {
|
||||
throw new Error(API_ENCRYPT_CONFIG_ERROR_MSG);
|
||||
}
|
||||
|
||||
encryptor = new JSEncrypt();
|
||||
encryptor.setPublicKey(publicKey);
|
||||
|
||||
return encryptor;
|
||||
}
|
||||
|
||||
function encryptFieldValue(value: unknown) {
|
||||
if (typeof value !== 'string' || value.length === 0) {
|
||||
throw new Error(API_ENCRYPT_REQUEST_ERROR_MSG);
|
||||
}
|
||||
|
||||
const encryptedValue = getEncryptor().encrypt(value);
|
||||
|
||||
if (!encryptedValue) {
|
||||
throw new Error(API_ENCRYPT_REQUEST_ERROR_MSG);
|
||||
}
|
||||
|
||||
return encryptedValue;
|
||||
}
|
||||
|
||||
export function applyApiEncrypt(config: InternalAxiosRequestConfig) {
|
||||
const encryptConfig = config as ApiEncryptRequestConfig;
|
||||
|
||||
if (encryptConfig.apiEncryptProcessed) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const encryptFields = getApiEncryptRule(config);
|
||||
|
||||
if (!encryptFields) {
|
||||
return config;
|
||||
}
|
||||
|
||||
if (!isPlainObject(config.data)) {
|
||||
throw new Error(API_ENCRYPT_REQUEST_ERROR_MSG);
|
||||
}
|
||||
|
||||
const nextData = { ...config.data };
|
||||
|
||||
encryptFields.forEach(field => {
|
||||
nextData[field] = encryptFieldValue(nextData[field]);
|
||||
});
|
||||
|
||||
config.data = nextData;
|
||||
config.headers.set(API_ENCRYPT_HEADER, API_ENCRYPT_HEADER_VALUE);
|
||||
encryptConfig.apiEncryptProcessed = true;
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useAuthStore } from '@/store/modules/auth';
|
||||
import { localStg } from '@/utils/storage';
|
||||
import { getServiceBaseURL } from '@/utils/service';
|
||||
import { $t } from '@/locales';
|
||||
import { applyApiEncrypt } from './api-encrypt';
|
||||
import { getAuthorization, handleExpiredRequest, showErrorMsg } from './shared';
|
||||
import type { RequestInstanceState } from './type';
|
||||
|
||||
@@ -28,6 +29,7 @@ export const request = createFlatRequest(
|
||||
async onRequest(config) {
|
||||
const Authorization = getAuthorization();
|
||||
Object.assign(config.headers, { Authorization });
|
||||
applyApiEncrypt(config);
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
@@ -98,9 +98,7 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
|
||||
)
|
||||
);
|
||||
|
||||
for (const routeKey of routeKeysToReset) {
|
||||
await routeStore.resetRouteCache(routeKey);
|
||||
}
|
||||
await Promise.all(routeKeysToReset.map(routeKey => routeStore.resetRouteCache(routeKey)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,7 +28,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
||||
showIcon: true
|
||||
},
|
||||
multilingual: {
|
||||
visible: true
|
||||
visible: false
|
||||
},
|
||||
globalSearch: {
|
||||
visible: true
|
||||
|
||||
3
src/typings/api/system-manage.d.ts
vendored
3
src/typings/api/system-manage.d.ts
vendored
@@ -200,8 +200,7 @@ declare namespace Api {
|
||||
|
||||
type PostSimpleList = PostSimple[];
|
||||
|
||||
type PostSearchParams = CommonType.RecordNullable<Pick<Post, 'name' | 'code' | 'postType' | 'status'>> &
|
||||
PageParams;
|
||||
type PostSearchParams = CommonType.RecordNullable<Pick<Post, 'name' | 'code' | 'postType' | 'status'>> & PageParams;
|
||||
|
||||
type SavePostParams = Pick<Post, 'name' | 'code' | 'postType' | 'levelRank' | 'sort' | 'status'> & {
|
||||
remark?: string | null;
|
||||
|
||||
9
src/typings/api/user-preference.d.ts
vendored
Normal file
9
src/typings/api/user-preference.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
declare namespace Api {
|
||||
namespace UserPreference {
|
||||
type ThemeSettingsValue<T> = {
|
||||
[K in keyof T]?: T[K] extends Record<string, unknown> ? ThemeSettingsValue<T[K]> : T[K];
|
||||
};
|
||||
|
||||
type ThemeSettings = ThemeSettingsValue<App.Theme.ThemeSetting>;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { computed } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
import { getPaletteColorByNumber, mixColor } from '@sa/color';
|
||||
import { loginModuleRecord } from '@/constants/app';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { $t } from '@/locales';
|
||||
import PwdLogin from './modules/pwd-login.vue';
|
||||
@@ -18,7 +17,6 @@ interface Props {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
interface LoginModule {
|
||||
@@ -61,13 +59,6 @@ const bgColor = computed(() => {
|
||||
class="text-20px lt-sm:text-18px"
|
||||
@switch="themeStore.toggleThemeScheme"
|
||||
/>
|
||||
<LangSwitch
|
||||
v-if="themeStore.header.multilingual.visible"
|
||||
:lang="appStore.locale"
|
||||
:lang-options="appStore.localeOptions"
|
||||
:show-tooltip="false"
|
||||
@change-lang="appStore.changeLocale"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<main class="pt-15px">
|
||||
|
||||
@@ -346,10 +346,6 @@ const pageResourceOptions = pageResourceItems.map(item => ({
|
||||
const selectedPageResource = computed(() => pageResourceMap.get(model.value.pageResourcePath) ?? null);
|
||||
|
||||
const displayRoutePath = computed(() => {
|
||||
if (selectedPageResource.value) {
|
||||
return selectedPageResource.value.path;
|
||||
}
|
||||
|
||||
return joinRoutePaths(currentParentFullPath.value, model.value.path);
|
||||
});
|
||||
|
||||
@@ -357,14 +353,6 @@ const hasCompatibleViewRouteData = computed(() =>
|
||||
Boolean(getNullableText(model.value.component) && getNullableText(model.value.path))
|
||||
);
|
||||
|
||||
const isSelectedPageResourceCompatible = computed(() => {
|
||||
if (!selectedPageResource.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(getRelativeRoutePath(selectedPageResource.value.path, currentParentFullPath.value));
|
||||
});
|
||||
|
||||
const rules = computed(() => {
|
||||
const pathRule: RuleFormItem = {
|
||||
message: $t('page.system.menu.form.path'),
|
||||
@@ -403,11 +391,6 @@ const rules = computed(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSelectedPageResourceCompatible.value) {
|
||||
callback(new Error($t('page.system.menu.form.pageResourceParentMismatch')));
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
};
|
||||
@@ -569,14 +552,16 @@ const parentTreeOptions = computed<ParentTreeOption[]>(() => {
|
||||
];
|
||||
});
|
||||
|
||||
function filterParentTreeNode(value: string, data: ParentTreeOption) {
|
||||
function filterParentTreeNode(value: string, data: { label?: string }) {
|
||||
const keyword = value.trim().toLowerCase();
|
||||
|
||||
if (!keyword) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return data.label.toLowerCase().includes(keyword);
|
||||
return String(data.label ?? '')
|
||||
.toLowerCase()
|
||||
.includes(keyword);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
@@ -642,7 +627,13 @@ function syncViewRouteFields() {
|
||||
return;
|
||||
}
|
||||
|
||||
model.value.path = getRelativeRoutePath(pageResource.path, currentParentFullPath.value);
|
||||
const nextRelativePath =
|
||||
getRelativeRoutePath(pageResource.path, currentParentFullPath.value) ||
|
||||
normalizeRoutePart(model.value.path) ||
|
||||
normalizeRoutePart(pageResource.path).split('/').filter(Boolean).at(-1) ||
|
||||
'';
|
||||
|
||||
model.value.path = nextRelativePath;
|
||||
model.value.component = pageResource.component;
|
||||
model.value.componentName = pageResource.name;
|
||||
model.value.routePropsJson = stringifyRouteProps(pageResource.props as Record<string, unknown> | null);
|
||||
@@ -867,8 +858,11 @@ watch(
|
||||
|
||||
watch(
|
||||
() => model.value.parentId,
|
||||
() => {
|
||||
async () => {
|
||||
syncCurrentRouteFields();
|
||||
|
||||
await nextTick();
|
||||
clearFormValidation();
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -4,11 +4,9 @@ import type { TableInstance } from 'element-plus';
|
||||
import { ElButton, ElPopconfirm, ElTag } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import { useBoolean } from '@sa/hooks';
|
||||
import { commonStatusRecord } from '@/constants/business';
|
||||
import { fetchBatchDeletePost, fetchDeletePost, fetchGetPostPage } from '@/service/api';
|
||||
import { useUIPaginatedTable } from '@/hooks/common/table';
|
||||
import BusinessTableActionCell from '@/components/custom/business-table-action-cell';
|
||||
import { $t } from '@/locales';
|
||||
import PostOperateDialog from './modules/post-operate-dialog.vue';
|
||||
import PostSearch from './modules/post-search.vue';
|
||||
|
||||
@@ -56,7 +54,21 @@ function getStatusTagType(status: Api.SystemManage.CommonStatus): UI.ThemeColor
|
||||
}
|
||||
|
||||
function getStatusLabel(status: Api.SystemManage.CommonStatus) {
|
||||
return $t(commonStatusRecord[status]);
|
||||
return status === 0 ? '启用' : '停用';
|
||||
}
|
||||
|
||||
function getPostTypeLabel(type?: Api.SystemManage.PostType | null) {
|
||||
if (!type) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
const postTypeLabelMap: Record<Api.SystemManage.PostType, string> = {
|
||||
management: '管理岗',
|
||||
technical: '技术岗',
|
||||
business: '业务岗'
|
||||
};
|
||||
|
||||
return postTypeLabelMap[type];
|
||||
}
|
||||
|
||||
const searchParams = reactive(getInitSearchParams());
|
||||
@@ -76,41 +88,47 @@ const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagi
|
||||
},
|
||||
columns: () => [
|
||||
{ prop: 'selection', type: 'selection', width: 48 },
|
||||
{ prop: 'index', type: 'index', label: $t('common.index'), width: 64 },
|
||||
{ prop: 'name', label: $t('page.system.post.postName'), minWidth: 160, showOverflowTooltip: true },
|
||||
{ prop: 'code', label: $t('page.system.post.postCode'), minWidth: 180, showOverflowTooltip: true },
|
||||
{ prop: 'index', type: 'index', label: '序号', width: 64 },
|
||||
{ prop: 'name', label: '岗位名称', minWidth: 160, showOverflowTooltip: true },
|
||||
{ prop: 'code', label: '岗位编码', minWidth: 180, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'postType',
|
||||
label: $t('page.system.post.postType'),
|
||||
label: '岗位类型',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: row => row.postType ? $t(`page.system.post.type.${row.postType}`) : '--'
|
||||
formatter: row => getPostTypeLabel(row.postType)
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: $t('page.system.post.postStatus'),
|
||||
label: '岗位状态',
|
||||
width: 110,
|
||||
align: 'center',
|
||||
formatter: row => <ElTag type={getStatusTagType(row.status)}>{getStatusLabel(row.status)}</ElTag>
|
||||
},
|
||||
{ prop: 'levelRank', label: $t('page.system.post.levelRank'), width: 100, align: 'center', formatter: row => row.levelRank ?? '--' },
|
||||
{ prop: 'sort', label: $t('page.system.post.sort'), width: 90, align: 'center' },
|
||||
{
|
||||
prop: 'levelRank',
|
||||
label: '岗位职级',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
formatter: row => String(row.levelRank ?? '--')
|
||||
},
|
||||
{ prop: 'sort', label: '排序', width: 90, align: 'center' },
|
||||
{
|
||||
prop: 'remark',
|
||||
label: $t('page.system.post.remark'),
|
||||
label: '备注',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: row => row.remark || '--'
|
||||
},
|
||||
{
|
||||
prop: 'createTime',
|
||||
label: $t('page.system.post.createTime'),
|
||||
label: '创建时间',
|
||||
minWidth: 170,
|
||||
formatter: row => formatTime(row.createTime)
|
||||
},
|
||||
{
|
||||
prop: 'operate',
|
||||
label: $t('common.operate'),
|
||||
label: '操作',
|
||||
width: 196,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
@@ -119,13 +137,13 @@ const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagi
|
||||
actions={[
|
||||
{
|
||||
key: 'edit',
|
||||
label: $t('common.edit'),
|
||||
label: '编辑',
|
||||
buttonType: 'primary',
|
||||
onClick: () => openEdit(row)
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: $t('common.delete'),
|
||||
label: '删除',
|
||||
buttonType: 'danger',
|
||||
onClick: () => handleDeleteAction(row)
|
||||
}
|
||||
@@ -159,16 +177,16 @@ async function handleDelete(item: Api.SystemManage.Post) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success($t('common.deleteSuccess'));
|
||||
window.$message?.success('删除成功');
|
||||
|
||||
await reloadPostTable();
|
||||
}
|
||||
|
||||
async function handleDeleteAction(row: Api.SystemManage.Post) {
|
||||
try {
|
||||
await window.$messageBox?.confirm($t('common.confirmDelete'), $t('common.warning'), {
|
||||
confirmButtonText: $t('common.confirm'),
|
||||
cancelButtonText: $t('common.cancel'),
|
||||
await window.$messageBox?.confirm('确认删除当前岗位吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
} catch {
|
||||
@@ -189,7 +207,7 @@ async function handleBatchDelete() {
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success($t('common.deleteSuccess'));
|
||||
window.$message?.success('删除成功');
|
||||
await reloadPostTable();
|
||||
}
|
||||
|
||||
@@ -223,11 +241,11 @@ function handleSubmitted(postId: number) {
|
||||
<div class="flex-col-stretch gap-16px overflow-hidden">
|
||||
<PostSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
|
||||
|
||||
<ElCard class="card-wrapper flex-1-hidden" body-class="post-table-card-body">
|
||||
<ElCard class="flex-1-hidden card-wrapper" body-class="post-table-card-body">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-12px">
|
||||
<div class="flex items-center gap-10px">
|
||||
<p>{{ $t('page.system.post.title') }}</p>
|
||||
<p>岗位列表</p>
|
||||
<ElTag effect="plain">{{ mobilePagination.total || data.length }}</ElTag>
|
||||
</div>
|
||||
<TableHeaderOperation
|
||||
@@ -241,15 +259,15 @@ function handleSubmitted(postId: number) {
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.add') }}
|
||||
新增
|
||||
</ElButton>
|
||||
<ElPopconfirm :title="$t('common.confirmDelete')" @confirm="handleBatchDelete">
|
||||
<ElPopconfirm title="确认删除选中的岗位吗?" @confirm="handleBatchDelete">
|
||||
<template #reference>
|
||||
<ElButton type="danger" plain :disabled="postCheckedRowKeys.length === 0">
|
||||
<template #icon>
|
||||
<icon-ic-round-delete class="text-icon" />
|
||||
</template>
|
||||
{{ $t('common.batchDelete') }}
|
||||
批量删除
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElPopconfirm>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { commonStatusOptions } from '@/constants/business';
|
||||
import { postTypeOptions } from '@/constants/business';
|
||||
import { fetchCreatePost, fetchGetPost, fetchUpdatePost } from '@/service/api';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({ name: 'PostOperateDialog' });
|
||||
|
||||
@@ -30,11 +28,15 @@ const { createRequiredRule } = useFormRules();
|
||||
const detailLoading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const isEdit = computed(() => props.operateType === 'edit');
|
||||
const statusOptions = [
|
||||
{ label: '启用', value: 0 as const },
|
||||
{ label: '停用', value: 1 as const }
|
||||
];
|
||||
|
||||
const title = computed(() => {
|
||||
const titleMap: Record<UI.TableOperateType, string> = {
|
||||
add: $t('page.system.post.addPost'),
|
||||
edit: $t('page.system.post.editPost')
|
||||
add: '新增岗位',
|
||||
edit: '编辑岗位'
|
||||
};
|
||||
|
||||
return titleMap[props.operateType];
|
||||
@@ -57,11 +59,11 @@ function createDefaultModel(): Model {
|
||||
}
|
||||
|
||||
const rules = {
|
||||
name: createRequiredRule($t('page.system.post.form.postName')),
|
||||
code: createRequiredRule($t('page.system.post.form.postCode')),
|
||||
postType: createRequiredRule($t('page.system.post.form.postType')),
|
||||
sort: createRequiredRule($t('page.system.post.form.sort')),
|
||||
status: createRequiredRule($t('page.system.post.form.postStatus'))
|
||||
name: createRequiredRule('请输入岗位名称'),
|
||||
code: createRequiredRule('请输入岗位编码'),
|
||||
postType: createRequiredRule('请选择岗位类型'),
|
||||
sort: createRequiredRule('请输入排序'),
|
||||
status: createRequiredRule('请选择岗位状态')
|
||||
} satisfies Record<string, App.Global.FormRule>;
|
||||
|
||||
function closeModal() {
|
||||
@@ -113,7 +115,7 @@ async function handleSubmit() {
|
||||
|
||||
const request =
|
||||
isEdit.value && props.rowData
|
||||
? await fetchUpdatePost({id: props.rowData.id, ...submitData})
|
||||
? await fetchUpdatePost({ id: props.rowData.id, ...submitData })
|
||||
: await fetchCreatePost(submitData);
|
||||
|
||||
const { error, data } = await request;
|
||||
@@ -126,7 +128,7 @@ async function handleSubmit() {
|
||||
|
||||
const postId = isEdit.value && props.rowData ? props.rowData.id : Number(data);
|
||||
|
||||
window.$message?.success($t(isEdit.value ? 'common.updateSuccess' : 'common.addSuccess'));
|
||||
window.$message?.success(isEdit.value ? '修改成功' : '新增成功');
|
||||
|
||||
closeModal();
|
||||
emit('submitted', postId);
|
||||
@@ -152,59 +154,44 @@ watch(visible, value => {
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="$t('page.system.post.postName')" prop="name">
|
||||
<ElInput v-model="model.name" :placeholder="$t('page.system.post.form.postName')" />
|
||||
<ElFormItem label="岗位名称" prop="name">
|
||||
<ElInput v-model="model.name" placeholder="请输入岗位名称" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="$t('page.system.post.postCode')" prop="code">
|
||||
<ElInput v-model="model.code" :placeholder="$t('page.system.post.form.postCode')" />
|
||||
<ElFormItem label="岗位编码" prop="code">
|
||||
<ElInput v-model="model.code" placeholder="请输入岗位编码" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="$t('page.system.post.postType')" prop="postType">
|
||||
<ElSelect v-model="model.postType" class="w-full" :placeholder="$t('page.system.post.form.postType')">
|
||||
<ElOption v-for="{ label, value } in postTypeOptions" :key="value" :label="$t(label)" :value="value" />
|
||||
<ElFormItem label="岗位类型" prop="postType">
|
||||
<ElSelect v-model="model.postType" class="w-full" placeholder="请选择岗位类型">
|
||||
<ElOption v-for="{ label, value } in postTypeOptions" :key="value" :label="label" :value="value" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="$t('page.system.post.levelRank')" prop="levelRank">
|
||||
<ElInputNumber
|
||||
v-model="model.levelRank"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
:placeholder="$t('page.system.post.form.levelRank')"
|
||||
/>
|
||||
<ElFormItem label="岗位职级" prop="levelRank">
|
||||
<ElInputNumber v-model="model.levelRank" class="w-full" :min="0" placeholder="请输入岗位职级" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="$t('page.system.post.sort')" prop="sort">
|
||||
<ElInputNumber
|
||||
v-model="model.sort"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
:placeholder="$t('page.system.post.form.sort')"
|
||||
/>
|
||||
<ElFormItem label="排序" prop="sort">
|
||||
<ElInputNumber v-model="model.sort" class="w-full" :min="0" placeholder="请输入排序" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="$t('page.system.post.postStatus')" prop="status">
|
||||
<ElFormItem label="岗位状态" prop="status">
|
||||
<ElRadioGroup v-model="model.status" class="business-form-radio-group">
|
||||
<ElRadio v-for="{ label, value } in commonStatusOptions" :key="value" :value="value">
|
||||
{{ $t(label) }}
|
||||
<ElRadio v-for="{ label, value } in statusOptions" :key="value" :value="value">
|
||||
{{ label }}
|
||||
</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem :label="$t('page.system.post.remark')" prop="remark">
|
||||
<ElInput
|
||||
v-model="model.remark"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
:placeholder="$t('page.system.post.form.remark')"
|
||||
/>
|
||||
<ElFormItem label="备注" prop="remark">
|
||||
<ElInput v-model="model.remark" type="textarea" :rows="4" placeholder="请输入备注" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { commonStatusOptions } from '@/constants/business';
|
||||
import { postTypeOptions } from '@/constants/business';
|
||||
import TableSearchPanel from '@/components/custom/table-search-panel.vue';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({ name: 'PostSearch' });
|
||||
|
||||
@@ -14,6 +12,11 @@ const emit = defineEmits<{
|
||||
|
||||
const model = defineModel<Api.SystemManage.PostSearchParams>('model', { required: true });
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '启用', value: 0 as const },
|
||||
{ label: '停用', value: 1 as const }
|
||||
];
|
||||
|
||||
const keyword = computed({
|
||||
get() {
|
||||
return model.value.name ?? model.value.code ?? '';
|
||||
@@ -37,21 +40,21 @@ function search() {
|
||||
<template>
|
||||
<TableSearchPanel :model="model" :action-col-lg="8" @reset="reset" @search="search">
|
||||
<ElCol :lg="8" :md="12" :sm="12">
|
||||
<ElFormItem :label="$t('page.system.post.postName')" prop="name">
|
||||
<ElInput v-model="keyword" clearable :placeholder="$t('page.system.post.form.postName')" />
|
||||
<ElFormItem label="岗位名称" prop="name">
|
||||
<ElInput v-model="keyword" clearable placeholder="请输入岗位名称或岗位编码" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :lg="8" :md="12" :sm="12">
|
||||
<ElFormItem :label="$t('page.system.post.postType')" prop="postType">
|
||||
<ElSelect v-model="model.postType" clearable :placeholder="$t('page.system.post.form.postType')">
|
||||
<ElOption v-for="{ label, value } in postTypeOptions" :key="value" :label="$t(label)" :value="value" />
|
||||
<ElFormItem label="岗位类型" prop="postType">
|
||||
<ElSelect v-model="model.postType" clearable placeholder="请选择岗位类型">
|
||||
<ElOption v-for="{ label, value } in postTypeOptions" :key="value" :label="label" :value="value" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :lg="8" :md="12" :sm="12">
|
||||
<ElFormItem :label="$t('page.system.post.postStatus')" prop="status">
|
||||
<ElSelect v-model="model.status" clearable :placeholder="$t('page.system.post.form.postStatus')">
|
||||
<ElOption v-for="{ label, value } in commonStatusOptions" :key="value" :label="$t(label)" :value="value" />
|
||||
<ElFormItem label="岗位状态" prop="status">
|
||||
<ElSelect v-model="model.status" clearable placeholder="请选择岗位状态">
|
||||
<ElOption v-for="{ label, value } in statusOptions" :key="value" :label="label" :value="value" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
|
||||
@@ -100,7 +100,10 @@ watch(visible, async value => {
|
||||
:scrollbar="false"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top" autocomplete="off">
|
||||
<input class="business-form-autofill-guard" type="text" name="fake-username" autocomplete="username" />
|
||||
<input class="business-form-autofill-guard" type="password" name="fake-password" autocomplete="new-password" />
|
||||
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem :label="$t('page.system.user.userName')">
|
||||
@@ -109,14 +112,22 @@ watch(visible, async value => {
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem :label="$t('page.system.user.newPassword')" prop="password">
|
||||
<ElInput v-model="model.password" show-password :placeholder="$t('page.system.user.form.newPassword')" />
|
||||
<ElInput
|
||||
v-model="model.password"
|
||||
name="system-user-reset-password"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
:placeholder="$t('page.system.user.form.newPassword')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem :label="$t('page.system.user.confirmPassword')" prop="confirmPassword">
|
||||
<ElInput
|
||||
v-model="model.confirmPassword"
|
||||
name="system-user-reset-confirm-password"
|
||||
show-password
|
||||
autocomplete="new-password"
|
||||
:placeholder="$t('page.system.user.form.confirmPassword')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
@@ -125,3 +136,19 @@ watch(visible, async value => {
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.business-form-autofill-guard {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -29,12 +29,13 @@ const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required
|
||||
<TableSearchPanel
|
||||
:model="model"
|
||||
:disabled="disabled"
|
||||
:action-col-lg="8"
|
||||
:action-col-md="8"
|
||||
:action-col-lg="24"
|
||||
:action-col-md="24"
|
||||
:action-col-sm="24"
|
||||
@reset="$emit('reset')"
|
||||
@search="$emit('search')"
|
||||
>
|
||||
<ElCol :lg="8" :md="8" :sm="12">
|
||||
<ElCol :lg="6" :md="8" :sm="12">
|
||||
<ElFormItem :label="$t('page.system.user.userName')" prop="username">
|
||||
<ElInput
|
||||
v-model="model.username"
|
||||
@@ -44,7 +45,7 @@ const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :lg="8" :md="8" :sm="12">
|
||||
<ElCol :lg="6" :md="8" :sm="12">
|
||||
<ElFormItem :label="$t('page.system.user.userPhone')" prop="mobile">
|
||||
<ElInput
|
||||
v-model="model.mobile"
|
||||
@@ -54,7 +55,7 @@ const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :lg="8" :md="8" :sm="12">
|
||||
<ElCol :lg="6" :md="8" :sm="12">
|
||||
<ElFormItem :label="$t('page.system.user.userStatus')" prop="status">
|
||||
<ElSelect
|
||||
v-model="model.status"
|
||||
@@ -71,21 +72,18 @@ const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
|
||||
<template #extra>
|
||||
<ElCol :lg="8" :md="8" :sm="12">
|
||||
<ElFormItem :label="$t('page.system.user.userRole')" prop="roleId">
|
||||
<ElSelect
|
||||
v-model="model.roleId"
|
||||
clearable
|
||||
filterable
|
||||
:disabled="disabled"
|
||||
:placeholder="$t('page.system.user.form.userRole')"
|
||||
>
|
||||
<ElOption v-for="item in roleOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</template>
|
||||
<ElCol :lg="6" :md="8" :sm="12">
|
||||
<ElFormItem :label="$t('page.system.user.userRole')" prop="roleId">
|
||||
<ElSelect
|
||||
v-model="model.roleId"
|
||||
clearable
|
||||
filterable
|
||||
:disabled="disabled"
|
||||
:placeholder="$t('page.system.user.form.userRole')"
|
||||
>
|
||||
<ElOption v-for="item in roleOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</TableSearchPanel>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user