This commit is contained in:
oneao committed 2026-10-15 17:31:35 +08:00
1 parent b4498e5350
commit bf9dca19de
47 files changed
+8592 -313

No files matched your search

@@ -0,0 +1,179 @@
<script setup lang="ts">
/**
* 绑定模块面板(menu-management 右侧,与「菜单配置」并排)。
*
* 为什么是常驻面板而不是弹层:绑定是菜单属性的一部分,配完要能一眼看到「这个菜单挂了哪些模块」;
* 弹层则每次都要「打开 → 勾 → 确定」,确认前看不到已绑关系,也容易漏点确定白配一次。
*
* 交互:按模块树勾选当前页面使用的模块。勾分类节点(module)会自动级联勾选其下全部模块
* —— 由 G3Tree 的 checkable 非严格模式提供(点父级整棵子树开 / 关,半选态自动计算)。
* 落库只登记 data / virtual 模块编码:module 类型只是模块树上的分类,不参与绑定,
* 回写模型前统一过滤;树上的勾选草稿(含分类节点)只存在于本组件。
*
* 数据契约:v-model 双向绑定已绑模块编码数组。差异比对与落库(s_menu_module)
* 由父级负责 —— 本面板只维护草稿,随菜单的「保存」一起提交。
*
* 尚未移植:fms-vue 该面板右侧还有「权限点表 + 权限库」两块(就地增删 s_power、
* 从历史动作库导入)。本文件先只做模块绑定,那两块随权限相关步骤补。
*/
import { computed, ref, watch } from 'vue'
import { G3Tag } from '@g3soft/ui'
import type { TreeKey } from '@g3soft/ui'
import BizTree from '@/components/biz-tree/BizTree.vue'
import type { TreeRow } from '@/utils/tree'
import {
filterBindableModules,
isBindableModule,
MENU_BIND_MENU_TYPE,
moduleBindSignature,
} from './utils'
const props = withDefaults(
defineProps<{
/** 模块树(module 分类 + data / virtual 数据模块),父级加载一次后传入 */
treeData?: TreeRow[]
/** 模块编码 → b_module_type;缺少映射的键按可绑定处理,避免误删历史绑定 */
moduleTypes?: Record<string, string>
/** 菜单类型(directory / page / external):只有页面菜单绑定模块 */
menuType?: string
/** 当前菜单标识:切换菜单时清掉组件内的勾选草稿 */
menuId?: string
/** 模块清单加载中 */
loading?: boolean
}>(),
{
treeData: () => [],
// 默认值必须是内联写法:defineProps 会被提升到 setup 之外,
// 引用模块级常量(即使是 const)编译期直接报错(checkInvalidScopeReference)。
// 这里的括号包裹空对象字面量:写成返回空表达式的箭头函数会让 props 变 undefined。
moduleTypes: () => ({}),
menuType: '',
menuId: '',
loading: false,
},
)
/** 已绑模块编码(s_menu_module 草稿,父级持有并随菜单保存一并提交) */
const modules = defineModel<string[]>({ default: () => [] })
/** 仅页面菜单绑定模块:目录 / 外链换一句说明,不渲染可勾的树 */
const canBind = computed(() => props.menuType === MENU_BIND_MENU_TYPE)
/** 不可绑定的原因(目录与外链分别说明,不共用一句含糊的「不可用」) */
const stateText = computed(() =>
props.menuType === 'directory' ? '目录不绑定模块' : '外链不绑定模块',
)
/**
* 树上的勾选草稿:含被勾中的分类节点(非严格模式下点父级会把整棵子树的键回传)。
* 外部值变化(切换菜单 / 重载 / 保存后清空)时按签名重置;本组件自身的回写签名一致,
* 因此不会重置 —— 刚点亮的分类节点不会因为回写模型而闪掉。
*/
const draftChecked = ref<string[]>([])
watch(
() => modules.value,
(value) => {
const next = (value || []).map(String)
if (moduleBindSignature(next) === moduleBindSignature(draftChecked.value)) return
draftChecked.value = next
},
{ immediate: true },
)
/** 勾选变化:树上记全量(含分类节点),回写模型只留可绑定的 data / virtual */
function onCheckChange(keys: TreeKey[]): void {
draftChecked.value = keys.map(String)
modules.value = filterBindableModules(keys, props.moduleTypes)
}
/** 已绑数量(只算可绑定的,与落库口径一致) */
const boundCount = computed(
() =>
(modules.value || []).filter((key) => isBindableModule(props.moduleTypes[String(key)]))
.length,
)
/**
* 切换菜单:上一个菜单的勾选草稿不该留下。
* 未保存改动由父级在切换前用「有未保存改动」提示拦下,确认放弃后才走到这里。
*/
watch(
() => props.menuId,
() => {
draftChecked.value = []
},
)
</script>
<template>
<div class="mm-bind">
<div class="mm-bind__head">
<span class="mm-bind__title">绑定模块</span>
<!-- 数字是已绑数量;不写「已绑 3 个模块」这类解释性文案 -->
<G3Tag v-if="canBind && boundCount" variant="secondary">{{ boundCount }}</G3Tag>
</div>
<div class="mm-bind__body">
<div v-if="!canBind" class="mm-bind__state">{{ stateText }}</div>
<div v-else class="mm-bind__tree">
<!-- 不设 default-expand-all:整棵树默认收起,由用户按需逐层点开 -->
<BizTree
v-model:checked-keys="draftChecked"
:tree-data="treeData"
:loading="loading"
checkable
@update:checked-keys="onCheckChange"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
/* 白卡 + 卡头常驻 + 正文自己滚动,与「菜单配置」卡同构 */
.mm-bind {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--g3-color-bg-container, #fff);
border-radius: var(--g3-radius-base, 0.5rem);
}
.mm-bind__head {
display: flex;
align-items: center;
gap: 6px;
flex: none;
padding: 10px 14px;
border-bottom: 1px solid var(--g3-color-border, #e5e6eb);
}
.mm-bind__title {
font-size: 13px;
font-weight: 600;
color: var(--g3-color-text, #1f2329);
}
.mm-bind__body {
flex: 1;
min-height: 0;
}
/* 模块树:撑满面板高度,长树在自己区域内滚动而不是把卡片顶高 */
.mm-bind__tree {
height: 100%;
overflow: auto;
padding: 6px 8px;
}
/* 目录 / 外链没有模块绑定:内容区中间给一句说明,空着一片反而像没加载出来 */
.mm-bind__state {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--g3-color-text-tertiary, #86909c);
font-size: 13px;
}
</style>