Files
workspace/code/g3soft-erp/web/src/views/module/menu-management/ModuleBindPanel.vue
T
2026-10-15 17:31:35 +08:00

180 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>