style(projects): 微调
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# 生产环境的后端服务地址
|
# 生产环境的后端服务地址
|
||||||
VITE_SERVICE_BASE_URL=https://mock.apifox.cn/m1/3109515-0-default
|
VITE_SERVICE_BASE_URL=
|
||||||
|
|
||||||
# 生产环境下的其他后端服务地址
|
# 生产环境下的其他后端服务地址
|
||||||
VITE_OTHER_SERVICE_BASE_URL= `{
|
VITE_OTHER_SERVICE_BASE_URL= `{
|
||||||
|
|||||||
3
src/typings/components.d.ts
vendored
3
src/typings/components.d.ts
vendored
@@ -119,11 +119,9 @@ declare module 'vue' {
|
|||||||
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
|
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
|
||||||
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
|
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
|
||||||
IconMdiCheck: typeof import('~icons/mdi/check')['default']
|
IconMdiCheck: typeof import('~icons/mdi/check')['default']
|
||||||
IconMdiCheckCircleOutline: typeof import('~icons/mdi/check-circle-outline')['default']
|
|
||||||
IconMdiChevronDoubleDown: typeof import('~icons/mdi/chevron-double-down')['default']
|
IconMdiChevronDoubleDown: typeof import('~icons/mdi/chevron-double-down')['default']
|
||||||
IconMdiChevronDoubleUp: typeof import('~icons/mdi/chevron-double-up')['default']
|
IconMdiChevronDoubleUp: typeof import('~icons/mdi/chevron-double-up')['default']
|
||||||
IconMdiCloseCircle: typeof import('~icons/mdi/close-circle')['default']
|
IconMdiCloseCircle: typeof import('~icons/mdi/close-circle')['default']
|
||||||
IconMdiCloseCircleOutline: typeof import('~icons/mdi/close-circle-outline')['default']
|
|
||||||
IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
|
IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
|
||||||
IconMdiDownload: typeof import('~icons/mdi/download')['default']
|
IconMdiDownload: typeof import('~icons/mdi/download')['default']
|
||||||
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
|
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
|
||||||
@@ -135,7 +133,6 @@ declare module 'vue' {
|
|||||||
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
|
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
|
||||||
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
|
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
|
||||||
IconMdiLinkVariant: typeof import('~icons/mdi/link-variant')['default']
|
IconMdiLinkVariant: typeof import('~icons/mdi/link-variant')['default']
|
||||||
IconMdiPackageDown: typeof import('~icons/mdi/package-down')['default']
|
|
||||||
IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline')['default']
|
IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline')['default']
|
||||||
IconMdiPlus: typeof import('~icons/mdi/plus')['default']
|
IconMdiPlus: typeof import('~icons/mdi/plus')['default']
|
||||||
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ interface FormModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const model = ref<FormModel>({
|
const model = ref<FormModel>({
|
||||||
userName: 'admin',
|
userName: '',
|
||||||
password: 'admin123'
|
password: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
|
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { TreeInstance } from 'element-plus';
|
|||||||
import { menuTypeRecord } from '@/constants/business';
|
import { menuTypeRecord } from '@/constants/business';
|
||||||
import { fetchAssignRoleMenus, fetchGetRoleMenuIds } from '@/service/api';
|
import { fetchAssignRoleMenus, fetchGetRoleMenuIds } from '@/service/api';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import { normalizeRoleMenuCheckedIds, resolveRoleMenuSubmitIds } from './role-resource-tree';
|
import { normalizeRoleMenuCheckedIds } from './role-resource-tree';
|
||||||
|
|
||||||
defineOptions({ name: 'RoleResourcePanel' });
|
defineOptions({ name: 'RoleResourcePanel' });
|
||||||
|
|
||||||
@@ -28,7 +28,6 @@ const submitting = ref(false);
|
|||||||
const filterKeyword = ref('');
|
const filterKeyword = ref('');
|
||||||
const checkedKeys = ref<string[]>([]);
|
const checkedKeys = ref<string[]>([]);
|
||||||
const baselineMenuIds = ref<string[]>([]);
|
const baselineMenuIds = ref<string[]>([]);
|
||||||
const dirtyMenuIds = ref<Set<string>>(new Set());
|
|
||||||
|
|
||||||
const disabled = computed(() => !props.role || props.role.status === 1);
|
const disabled = computed(() => !props.role || props.role.status === 1);
|
||||||
const checkedCount = computed(() => checkedKeys.value.length);
|
const checkedCount = computed(() => checkedKeys.value.length);
|
||||||
@@ -107,7 +106,6 @@ function collectExpandableNodeIds(nodes: Api.SystemManage.MenuSimple[]) {
|
|||||||
async function loadRoleMenus() {
|
async function loadRoleMenus() {
|
||||||
if (!props.role) {
|
if (!props.role) {
|
||||||
baselineMenuIds.value = [];
|
baselineMenuIds.value = [];
|
||||||
dirtyMenuIds.value = new Set();
|
|
||||||
await applyCheckedKeys([]);
|
await applyCheckedKeys([]);
|
||||||
treeRef.value?.filter(filterKeyword.value);
|
treeRef.value?.filter(filterKeyword.value);
|
||||||
return;
|
return;
|
||||||
@@ -121,7 +119,6 @@ async function loadRoleMenus() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
baselineMenuIds.value = [];
|
baselineMenuIds.value = [];
|
||||||
dirtyMenuIds.value = new Set();
|
|
||||||
await applyCheckedKeys([]);
|
await applyCheckedKeys([]);
|
||||||
treeRef.value?.filter(filterKeyword.value);
|
treeRef.value?.filter(filterKeyword.value);
|
||||||
return;
|
return;
|
||||||
@@ -133,13 +130,11 @@ async function loadRoleMenus() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
baselineMenuIds.value = normalizedMenuIds;
|
baselineMenuIds.value = normalizedMenuIds;
|
||||||
dirtyMenuIds.value = new Set();
|
|
||||||
await applyCheckedKeys(normalizedMenuIds);
|
await applyCheckedKeys(normalizedMenuIds);
|
||||||
treeRef.value?.filter(filterKeyword.value);
|
treeRef.value?.filter(filterKeyword.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCheck(data: Api.SystemManage.MenuSimple) {
|
function handleCheck() {
|
||||||
dirtyMenuIds.value = new Set([...dirtyMenuIds.value, data.id]);
|
|
||||||
syncCheckedKeys();
|
syncCheckedKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,13 +143,8 @@ async function handleSave() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkedMenuIds = (treeRef.value?.getCheckedKeys(false) as string[]) ?? [];
|
// 直接提交树上完整勾选的节点(getCheckedKeys(false) 自动排除半选父节点),保证“所见即所得”。
|
||||||
const menuIds = resolveRoleMenuSubmitIds({
|
const menuIds = (treeRef.value?.getCheckedKeys(false) as string[]) ?? [];
|
||||||
menuTree: props.menuTree,
|
|
||||||
baselineIds: baselineMenuIds.value,
|
|
||||||
dirtyIds: [...dirtyMenuIds.value],
|
|
||||||
checkedIds: checkedMenuIds
|
|
||||||
});
|
|
||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
|
|
||||||
@@ -170,7 +160,6 @@ async function handleSave() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
baselineMenuIds.value = [...menuIds];
|
baselineMenuIds.value = [...menuIds];
|
||||||
dirtyMenuIds.value = new Set();
|
|
||||||
syncCheckedKeys();
|
syncCheckedKeys();
|
||||||
|
|
||||||
window.$message?.success($t('common.modifySuccess'));
|
window.$message?.success($t('common.modifySuccess'));
|
||||||
|
|||||||
@@ -3,13 +3,6 @@ export type RoleResourceTreeNode = {
|
|||||||
children?: RoleResourceTreeNode[] | null;
|
children?: RoleResourceTreeNode[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ResolveRoleMenuSubmitIdsInput = {
|
|
||||||
menuTree: RoleResourceTreeNode[];
|
|
||||||
baselineIds: string[];
|
|
||||||
dirtyIds: string[];
|
|
||||||
checkedIds: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type NormalizeRoleMenuCheckedIdsInput = {
|
type NormalizeRoleMenuCheckedIdsInput = {
|
||||||
menuTree: RoleResourceTreeNode[];
|
menuTree: RoleResourceTreeNode[];
|
||||||
checkedIds: string[];
|
checkedIds: string[];
|
||||||
@@ -17,7 +10,6 @@ type NormalizeRoleMenuCheckedIdsInput = {
|
|||||||
|
|
||||||
type TreeIndex = {
|
type TreeIndex = {
|
||||||
orderedIds: string[];
|
orderedIds: string[];
|
||||||
parentById: Map<string, string | null>;
|
|
||||||
subtreeIdsById: Map<string, string[]>;
|
subtreeIdsById: Map<string, string[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,104 +46,33 @@ export function normalizeRoleMenuCheckedIds(input: NormalizeRoleMenuCheckedIdsIn
|
|||||||
return sortIdsByTreeOrder(treeIndex.orderedIds, normalizedIdSet);
|
return sortIdsByTreeOrder(treeIndex.orderedIds, normalizedIdSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveRoleMenuSubmitIds(input: ResolveRoleMenuSubmitIdsInput) {
|
|
||||||
const baselineIds = normalizeIds(input.baselineIds);
|
|
||||||
|
|
||||||
if (!input.dirtyIds.length) {
|
|
||||||
return baselineIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
const treeIndex = buildTreeIndex(input.menuTree);
|
|
||||||
const affectedIds = collectAffectedIds(treeIndex, baselineIds, normalizeIds(input.dirtyIds));
|
|
||||||
|
|
||||||
if (!affectedIds.size) {
|
|
||||||
return baselineIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextIdSet = new Set<string>();
|
|
||||||
|
|
||||||
baselineIds.forEach(id => {
|
|
||||||
if (!affectedIds.has(id)) {
|
|
||||||
nextIdSet.add(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 半选父节点只用于树态展示,提交它会把整棵子树误当成完整授权。
|
|
||||||
normalizeIds(input.checkedIds).forEach(id => {
|
|
||||||
if (affectedIds.has(id)) {
|
|
||||||
nextIdSet.add(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return sortIdsByTreeOrder(treeIndex.orderedIds, nextIdSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeIds(ids: string[]) {
|
function normalizeIds(ids: string[]) {
|
||||||
return [...new Set(ids.map(id => String(id).trim()).filter(Boolean))];
|
return [...new Set(ids.map(id => String(id).trim()).filter(Boolean))];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTreeIndex(nodes: RoleResourceTreeNode[]) {
|
function buildTreeIndex(nodes: RoleResourceTreeNode[]) {
|
||||||
const orderedIds: string[] = [];
|
const orderedIds: string[] = [];
|
||||||
const parentById = new Map<string, string | null>();
|
|
||||||
const subtreeIdsById = new Map<string, string[]>();
|
const subtreeIdsById = new Map<string, string[]>();
|
||||||
|
|
||||||
const walk = (items: RoleResourceTreeNode[], parentId: string | null) => {
|
const walk = (items: RoleResourceTreeNode[]) => {
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
orderedIds.push(item.id);
|
orderedIds.push(item.id);
|
||||||
parentById.set(item.id, parentId);
|
|
||||||
|
|
||||||
const childIds = item.children?.length ? walk(item.children, item.id) : [];
|
const childIds = item.children?.length ? walk(item.children) : [];
|
||||||
subtreeIdsById.set(item.id, [item.id, ...childIds]);
|
subtreeIdsById.set(item.id, [item.id, ...childIds]);
|
||||||
});
|
});
|
||||||
|
|
||||||
return items.flatMap(item => subtreeIdsById.get(item.id) ?? [item.id]);
|
return items.flatMap(item => subtreeIdsById.get(item.id) ?? [item.id]);
|
||||||
};
|
};
|
||||||
|
|
||||||
walk(nodes, null);
|
walk(nodes);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
orderedIds,
|
orderedIds,
|
||||||
parentById,
|
|
||||||
subtreeIdsById
|
subtreeIdsById
|
||||||
} satisfies TreeIndex;
|
} satisfies TreeIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectAffectedIds(treeIndex: TreeIndex, baselineIds: string[], dirtyIds: string[]) {
|
|
||||||
const affectedIds = new Set<string>();
|
|
||||||
const baselineIdSet = new Set(baselineIds);
|
|
||||||
|
|
||||||
dirtyIds.forEach(dirtyId => {
|
|
||||||
const subtreeIds = treeIndex.subtreeIdsById.get(dirtyId) ?? [dirtyId];
|
|
||||||
subtreeIds.forEach(id => affectedIds.add(id));
|
|
||||||
|
|
||||||
const ancestors = collectAncestorIds(treeIndex.parentById, dirtyId);
|
|
||||||
|
|
||||||
ancestors.forEach(ancestorId => {
|
|
||||||
if (!baselineIdSet.has(ancestorId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ancestorSubtreeIds = treeIndex.subtreeIdsById.get(ancestorId) ?? [ancestorId];
|
|
||||||
ancestorSubtreeIds.forEach(id => affectedIds.add(id));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return affectedIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectAncestorIds(parentById: Map<string, string | null>, nodeId: string) {
|
|
||||||
const ancestorIds: string[] = [];
|
|
||||||
|
|
||||||
let currentId: string | null | undefined = nodeId;
|
|
||||||
|
|
||||||
while (currentId) {
|
|
||||||
ancestorIds.push(currentId);
|
|
||||||
currentId = parentById.get(currentId) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ancestorIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortIdsByTreeOrder(orderedIds: string[], idSet: Set<string>) {
|
function sortIdsByTreeOrder(orderedIds: string[], idSet: Set<string>) {
|
||||||
const sortedIds: string[] = [];
|
const sortedIds: string[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -32,65 +32,6 @@ const menuTree: MenuNode[] = [
|
|||||||
const normalizeRoleMenuCheckedIds = (roleResourceTree as { normalizeRoleMenuCheckedIds?: NormalizeRoleMenuCheckedIds })
|
const normalizeRoleMenuCheckedIds = (roleResourceTree as { normalizeRoleMenuCheckedIds?: NormalizeRoleMenuCheckedIds })
|
||||||
.normalizeRoleMenuCheckedIds;
|
.normalizeRoleMenuCheckedIds;
|
||||||
|
|
||||||
const { resolveRoleMenuSubmitIds } = roleResourceTree;
|
|
||||||
|
|
||||||
describe('resolveRoleMenuSubmitIds', () => {
|
|
||||||
it('keeps original ids when there is no user interaction', () => {
|
|
||||||
const result = resolveRoleMenuSubmitIds({
|
|
||||||
menuTree,
|
|
||||||
baselineIds: ['weekly', 'monthly'],
|
|
||||||
dirtyIds: [],
|
|
||||||
checkedIds: ['personal', 'weekly', 'monthly']
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepEqual(result, ['weekly', 'monthly']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves untouched branches when another branch changes', () => {
|
|
||||||
const result = resolveRoleMenuSubmitIds({
|
|
||||||
menuTree,
|
|
||||||
baselineIds: ['weekly', 'monthly'],
|
|
||||||
dirtyIds: ['stateMachine'],
|
|
||||||
checkedIds: ['personal', 'weekly', 'monthly', 'stateMachine']
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepEqual(result, ['weekly', 'monthly', 'stateMachine']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('recomputes the whole dirty branch instead of expanding unrelated baseline ids', () => {
|
|
||||||
const result = resolveRoleMenuSubmitIds({
|
|
||||||
menuTree,
|
|
||||||
baselineIds: ['personal'],
|
|
||||||
dirtyIds: ['weekly'],
|
|
||||||
checkedIds: ['personal', 'weekly', 'weeklyDetail']
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepEqual(result, ['personal', 'weekly', 'weeklyDetail']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not expand untouched sibling branches under the same ancestor', () => {
|
|
||||||
const result = resolveRoleMenuSubmitIds({
|
|
||||||
menuTree,
|
|
||||||
baselineIds: ['monthly'],
|
|
||||||
dirtyIds: ['weeklyDetail'],
|
|
||||||
checkedIds: ['personal', 'weekly', 'weeklyDetail', 'monthly', 'monthlyDetail']
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepEqual(result, ['weeklyDetail', 'monthly']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not submit half-checked parent ids when a fully authorized branch becomes partial', () => {
|
|
||||||
const result = resolveRoleMenuSubmitIds({
|
|
||||||
menuTree,
|
|
||||||
baselineIds: ['personal'],
|
|
||||||
dirtyIds: ['monthlyDetail'],
|
|
||||||
checkedIds: ['weekly', 'weeklyDetail']
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepEqual(result, ['weekly', 'weeklyDetail']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('normalizeRoleMenuCheckedIds', () => {
|
describe('normalizeRoleMenuCheckedIds', () => {
|
||||||
it('removes partially covered parent ids before tree rendering', () => {
|
it('removes partially covered parent ids before tree rendering', () => {
|
||||||
assert.equal(typeof normalizeRoleMenuCheckedIds, 'function');
|
assert.equal(typeof normalizeRoleMenuCheckedIds, 'function');
|
||||||
|
|||||||
Reference in New Issue
Block a user