fix(components): 菜单管理调整
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
10919
pnpm-lock.yaml
generated
Normal file
10919
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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());
|
||||
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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