Files
shrimp-admin/src/components/ShrimpPowerTree.vue
T
wc24 2b33bca681 v0.3.0: 单 sources 入口 + 路径后缀覆盖约定
破坏性重构。框架对外只暴露一个 `sources` glob 入口,消费侧任何 .vue
都按文件路径后缀匹配同后缀覆盖:

- shrimp/components/<name>.vue 同后缀覆盖框架内置(login/layout/topbar/ShrimpTable/...)
- shrimp/components/fields/Field<X>.vue 注册类型 <x>(首字母小写)
- shrimp/module/<key>.vue 业务模块

变更:
- 删除 customFields / moduleSources / overrideField / registerField 旧入口
- 删除 fieldOverrides Map,字段表纯靠 glob 扫描注册
- 框架内 .vue 对可覆盖 peer 改用 defineAsyncComponent(resolveVue(...))
  (login → ShrimpLangSwitch / layout → sideM/navMenu/topbar/userbar/logo /
   ShrimpTable → ShrimpEditDrawer/ShrimpFilterDrawer/...)
- views/ 全部并入 components/,App.vue 上提到 src/ 根
- LangSwitch → ShrimpLangSwitch,PowerTree → ShrimpPowerTree
- module/FieldRelativeTime.vue → components/fields/FieldRelativeTime.vue
- module/user.vue 改用 #column-updatedAt slot 替代旧的 overrideField

字段类型约定从 'field' + Name 改为 lowercase-first(FieldString → 'string'),
内置和消费侧统一。
2026-05-25 15:05:16 +08:00

123 lines
3.7 KiB
Vue

<template>
<div class="power-tree-wrap">
<el-tree ref="treeRef" :data="treeData" show-checkbox node-key="key" default-expand-all
:props="{ label: 'label', children: 'children' }" @check="onCheck" />
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch, nextTick } from 'vue'
import { vueAdmin } from '../VueAdmin'
import type { IMenuItem, ILabel } from '../IShrimpType'
interface IPowerItem {
key: string
label: ILabel
}
const props = defineProps<{
modelValue: string[]
allPowers: IPowerItem[]
menuTree: IMenuItem[]
}>()
const emit = defineEmits<{
'update:modelValue': [value: string[]]
}>()
const treeRef = ref<any>(null)
interface PowerNode { key: string; label: string; children?: PowerNode[] }
const MENU_PREFIX = '_menu:'
const treeData = computed(() => {
function walkMenu(items: IMenuItem[]): PowerNode[] {
return items.reduce<PowerNode[]>((acc, item) => {
if (item.moduleKey) {
const modulePowers = props.allPowers.filter(p => p.key === item.moduleKey)
const children = props.allPowers
.filter(p => p.key.startsWith(item.moduleKey + '.'))
.map(p => ({ key: p.key, label: vueAdmin.t(p.label) }))
// 模块本身(view 权限)
if (modulePowers.length > 0) {
children.unshift({
key: item.moduleKey,
label: vueAdmin.t({ t: "power.view" }),
})
}
const node: PowerNode = {
key: MENU_PREFIX + item.moduleKey,
label: vueAdmin.t(item.label),
children: children.length ? children : undefined,
}
acc.push(node)
} else if (item.children?.length) {
const children = walkMenu(item.children)
if (children.length) {
acc.push({
key: MENU_PREFIX + (item.name || ''),
label: vueAdmin.t(item.label),
children,
})
}
}
return acc
}, [])
}
return walkMenu(props.menuTree)
})
function onCheck(data: any, state: any) {
const allKeys: string[] = state.checkedKeys
let keys = allKeys.filter((k: string) => !k.startsWith(MENU_PREFIX))
const toggledKey: string = data.key
// 菜单分组节点的子节点已由 el-tree 自动处理,直接返回
if (toggledKey.startsWith(MENU_PREFIX)) {
emit('update:modelValue', keys)
return
}
const checked = new Set(keys)
const isChecked = checked.has(toggledKey)
if (isChecked) {
// 勾选子权限时,自动勾选所有祖先
let ancestor = toggledKey
while (ancestor.includes('.')) {
ancestor = ancestor.substring(0, ancestor.lastIndexOf('.'))
checked.add(ancestor)
}
} else {
// 取消父权限时,自动取消所有后代
const prefix = toggledKey + '.'
keys = keys.filter(key => !key.startsWith(prefix))
emit('update:modelValue', keys)
return
}
emit('update:modelValue', [...checked])
}
watch(() => [props.modelValue, props.allPowers], () => {
nextTick(() => treeRef.value?.setCheckedKeys(props.modelValue || []))
}, { immediate: true })
</script>
<style scoped>
.power-tree-wrap {
border: 1px solid var(--el-border-color);
border-radius: 4px;
background: var(--el-bg-color);
padding: 8px 12px;
width: 100%;
}
.power-tree-wrap :deep(.el-tree) {
background: transparent;
}
</style>