20260914223013
This commit is contained in:
1 parent
549c8bd2eb
commit
63514476d7
60 files changed
+6478
-2004
No files matched your search
@@ -34,6 +34,7 @@ import UnsavedChangesModal from '@/components/UnsavedChangesModal.vue'
|
||||
import I18nNameModal from '@/components/I18nNameModal.vue'
|
||||
import MenuTreePanel from './MenuTreePanel.vue'
|
||||
import MenuBasicPanel from './MenuBasicPanel.vue'
|
||||
import ModulePickerModal from './ModulePickerModal.vue'
|
||||
import {
|
||||
addRowUpdate,
|
||||
applyMenuPosition,
|
||||
@@ -51,6 +52,11 @@ function sqlText(value) {
|
||||
return `N'${String(value ?? '').replace(/'/g, "''")}'`
|
||||
}
|
||||
|
||||
/** 编码数组的顺序无关签名(比较模块绑定集合是否变化) */
|
||||
function sortedKey(list) {
|
||||
return (list || []).map(String).toSorted().join(',')
|
||||
}
|
||||
|
||||
/* ================= 多语言配置(s_i18n_type 驱动) ================= */
|
||||
// 菜单多语言:默认语言(b_default=1)名称直接存 b_name;其余语言存 s_i18n(b_key=menu.编码)。
|
||||
// 语言清单来自数据库(stores/i18n),不再写死 zh-CN/en-US。
|
||||
@@ -105,6 +111,13 @@ const i18n_org = ref([])
|
||||
/** 多语言名称编辑弹窗 */
|
||||
const i18nModalVisible = ref(false)
|
||||
|
||||
/** 绑定模块草稿(s_menu_module 的模块编码数组,随菜单保存一并提交) */
|
||||
const menuModules = ref([])
|
||||
/** 绑定模块加载基线(保存时行级 diffRows 的原始集合) */
|
||||
const menuModules_org = ref([])
|
||||
/** 模块绑定选择弹层 */
|
||||
const modulePickerOpen = ref(false)
|
||||
|
||||
/** 菜单基础信息面板(保存前校验入口) */
|
||||
const basicPanelRef = ref(null)
|
||||
|
||||
@@ -154,37 +167,6 @@ async function loadMenuTree() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具栏「刷新」:loadMenuTree 会用库里数据整体重建树,
|
||||
* 未保存的新建菜单会凭空消失、当前草稿也会与新树脱节,故有未保存内容时先确认。
|
||||
*/
|
||||
function requestReloadTree() {
|
||||
const hasDraftNode = [...unsavedMenuIds].some((id) => findTreeNode(menuTreeData.value, id))
|
||||
// 选中未保存节点时,maindata 与 maindata_org 一致(未编辑态),hasUnsavedChanges 判不出来,需单独看
|
||||
const editingDraftNode =
|
||||
!!menuSelectedId.value && unsavedMenuIds.has(String(menuSelectedId.value))
|
||||
const dirty = loadedMenuId.value && hasUnsavedChanges()
|
||||
if (!hasDraftNode && !dirty) {
|
||||
void loadMenuTree()
|
||||
return
|
||||
}
|
||||
showConfirm({
|
||||
title: '刷新菜单树',
|
||||
content:
|
||||
'刷新会用数据库数据重建整棵树,当前未保存的修改与未保存的新建菜单都将丢失。确定继续吗?',
|
||||
okText: '刷新',
|
||||
onOk: async () => {
|
||||
// 选中项若为未保存节点,刷新后它必然不在树上:先清空选中,避免右侧残留悬空草稿。
|
||||
// 已保存节点则记录 id,树刷新后连同草稿一起重载,保持左右两侧一致。
|
||||
const reloadId =
|
||||
editingDraftNode || !menuSelectedId.value ? null : String(menuSelectedId.value)
|
||||
if (editingDraftNode) menuSelectedId.value = null
|
||||
await loadMenuTree()
|
||||
if (reloadId) await loadMenuConfig(reloadId)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 工具栏「重载」:重载会放弃当前未保存的改动,先确认 */
|
||||
function requestReloadConfig() {
|
||||
const id = menuSelectedId.value
|
||||
@@ -342,7 +324,6 @@ async function addMenu(parentId, menuTypeValue = 'page') {
|
||||
b_i18n: null,
|
||||
b_menu_type: menuTypeValue,
|
||||
b_route: null,
|
||||
b_module_id: null,
|
||||
b_icon: null,
|
||||
b_xh: 10,
|
||||
b_canuse: 1,
|
||||
@@ -397,6 +378,8 @@ async function deleteMenu(id) {
|
||||
// s_i18n 无菜单归属列,按资源键删该菜单的全部语言行
|
||||
const reqs = [
|
||||
{ table: 's_i18n', key_field: 'b_key', deletes: [{ b_key: buildMenuI18nKey(menuId) }] },
|
||||
// 模块绑定关系随菜单删除(b_menu_id 为复合主键首列,按它批量删)
|
||||
{ table: 's_menu_module', key_field: 'b_menu_id', deletes: [{ b_menu_id: menuId }] },
|
||||
]
|
||||
reqs.push({ table: 's_menu', key_field: 'b_id', deletes: [{ b_id: menuId }] })
|
||||
await saveObjectApi(reqs)
|
||||
@@ -444,6 +427,15 @@ async function loadMenuConfig(menuId) {
|
||||
i18n_values.value = i18nInitValues
|
||||
i18n_values_org.value = { ...i18nInitValues }
|
||||
i18n_org.value = i18n_org_rows
|
||||
// 模块绑定(s_menu_module):行身份 (b_menu_id, b_module_id),只登记 data / virtual 模块
|
||||
const moduleRes = await loadDataApi(
|
||||
's_menu_module',
|
||||
`b_menu_id = ${sqlText(id)}`,
|
||||
'b_xh ASC, b_module_id ASC',
|
||||
)
|
||||
if (String(menuSelectedId.value) !== id) return
|
||||
menuModules_org.value = (moduleRes.data || []).map((row) => String(row.b_module_id))
|
||||
menuModules.value = [...menuModules_org.value]
|
||||
loadedMenuId.value = id
|
||||
} catch (e) {
|
||||
if (String(menuSelectedId.value) === String(menuId)) {
|
||||
@@ -466,6 +458,8 @@ watch(menuSelectedId, (id) => {
|
||||
configLoading.value = false
|
||||
loadedMenuId.value = null
|
||||
maindata.value = null
|
||||
menuModules.value = []
|
||||
menuModules_org.value = []
|
||||
return
|
||||
}
|
||||
// 未落库的新节点:临时 id 在库里查不到行,不能走 loadMenuConfig
|
||||
@@ -484,6 +478,8 @@ watch(menuSelectedId, (id) => {
|
||||
i18n_values.value = { [defaultLocale.value]: node.b_name || '' }
|
||||
i18n_values_org.value = { ...i18n_values.value }
|
||||
i18n_org.value = []
|
||||
menuModules.value = []
|
||||
menuModules_org.value = []
|
||||
loadedMenuId.value = String(id)
|
||||
return
|
||||
}
|
||||
@@ -507,12 +503,14 @@ function hasUnsavedChanges() {
|
||||
)
|
||||
)
|
||||
return true
|
||||
// 模块绑定变化视为未保存(顺序无关)
|
||||
if (sortedKey(menuModules.value) !== sortedKey(menuModules_org.value)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的「有未保存改动就先确认」闸门:无改动直接执行 action,有改动弹窗等待用户选择。
|
||||
* 所有会覆盖当前草稿的操作(切换节点 / 新建菜单 / 刷新树)都必须经此,
|
||||
* 所有会覆盖当前草稿的操作(切换节点 / 新建菜单)都必须经此,
|
||||
* 否则会出现「某条路径静默丢弃编辑」的不一致。
|
||||
*/
|
||||
function confirmIfUnsaved(action) {
|
||||
@@ -594,9 +592,9 @@ async function beforeSave() {
|
||||
)
|
||||
return false
|
||||
}
|
||||
// 绑定模块非必填:页面菜单可不绑定业务模块,留空即保存为空
|
||||
// 路由 / 模块按菜单类型收敛:目录、外链不绑模块(清空草稿,保存时按 diff 删除关系行)
|
||||
if (!typeRule.route && menuData.b_route) menuData.b_route = null
|
||||
if (!typeRule.module && menuData.b_module_id) menuData.b_module_id = null
|
||||
if (!typeRule.module && menuModules.value.length) menuModules.value = []
|
||||
|
||||
// 字符串业务键:菜单 b_id 即编码,创建后不可修改;保存时统一去除首尾空格
|
||||
const menuCode = String(menuData.b_id || '').trim()
|
||||
@@ -681,6 +679,32 @@ async function buildSaveData() {
|
||||
reqs.push(i18nReq)
|
||||
}
|
||||
|
||||
// 模块绑定(s_menu_module):行身份 (b_menu_id, b_module_id),只提交增删行;
|
||||
// b_xh 记录页面内模块顺序(按当前勾选顺序 10 起步),顺序变化会产生 update,可接受。
|
||||
const toModuleRows = (list) =>
|
||||
list.map((moduleId, index) => ({
|
||||
b_menu_id: menuId,
|
||||
b_module_id: String(moduleId),
|
||||
b_xh: (index + 1) * 10,
|
||||
b_canuse: 1,
|
||||
}))
|
||||
const moduleChanges = diffRows(
|
||||
toModuleRows(menuModules.value),
|
||||
toModuleRows(menuModules_org.value),
|
||||
{ keyFields: ['b_menu_id', 'b_module_id'] },
|
||||
)
|
||||
if (
|
||||
moduleChanges.inserts.length ||
|
||||
moduleChanges.updates.length ||
|
||||
moduleChanges.deletes.length
|
||||
) {
|
||||
const moduleReq = { table: 's_menu_module', key_field: 'b_menu_id,b_module_id' }
|
||||
if (moduleChanges.inserts.length) moduleReq.inserts = moduleChanges.inserts
|
||||
if (moduleChanges.updates.length) moduleReq.updates = moduleChanges.updates
|
||||
if (moduleChanges.deletes.length) moduleReq.deletes = moduleChanges.deletes
|
||||
reqs.push(moduleReq)
|
||||
}
|
||||
|
||||
return reqs.length ? reqs : null
|
||||
}
|
||||
|
||||
@@ -787,22 +811,24 @@ async function onToggleEnable({ node, checked }) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 加载模块列表(供页面类型下拉选择) ================= */
|
||||
const moduleOptions = ref([])
|
||||
/* ================= 加载模块清单(绑定模块选择器) ================= */
|
||||
/** 模块平铺行(s_module 启用行;已绑模块名称展示与类型过滤用) */
|
||||
const moduleRows = ref([])
|
||||
/** 模块树(module 分类 + data / virtual 数据模块;绑定弹层勾选用) */
|
||||
const moduleTree = ref([])
|
||||
/** 模块编码 → 模块类型(弹层确定时过滤掉 module 分类节点) */
|
||||
const moduleTypes = computed(() =>
|
||||
Object.fromEntries(moduleRows.value.map((m) => [String(m.b_id), String(m.b_module_type)])),
|
||||
)
|
||||
let moduleOptionsLoaded = false
|
||||
|
||||
async function loadModuleOptions() {
|
||||
if (moduleOptionsLoaded) return
|
||||
try {
|
||||
const res = await loadDataApi(
|
||||
's_module',
|
||||
"b_canuse = 1 AND b_module_type IN ('data', 'virtual')",
|
||||
'b_xh ASC, b_id ASC',
|
||||
)
|
||||
moduleOptions.value = (res.data || []).map((m) => ({
|
||||
label: `[${m.b_id}] ${m.b_name}`,
|
||||
value: String(m.b_id),
|
||||
}))
|
||||
const res = await loadDataApi('s_module', 'b_canuse = 1', 'b_path ASC, b_xh ASC, b_id ASC')
|
||||
const rows = res.data || []
|
||||
moduleRows.value = rows
|
||||
moduleTree.value = sortTree(buildTree(rows.map((row) => ({ ...row }))))
|
||||
moduleOptionsLoaded = true
|
||||
} catch (e) {
|
||||
// Silently fail; modules will be empty
|
||||
@@ -860,7 +886,6 @@ loadModuleOptions()
|
||||
@add="onMenuAdd"
|
||||
@delete="(id) => void deleteMenu(id)"
|
||||
@drop="(payload) => void onMenuDrop(payload)"
|
||||
@refresh="requestReloadTree"
|
||||
@toggle-enable="(payload) => void onToggleEnable(payload)"
|
||||
/>
|
||||
</SplitterPanel>
|
||||
@@ -894,10 +919,12 @@ loadModuleOptions()
|
||||
<MenuBasicPanel
|
||||
ref="basicPanelRef"
|
||||
v-model="maindata"
|
||||
v-model:modules="menuModules"
|
||||
:type-locked="menuTypeLocked"
|
||||
:module-options="moduleOptions"
|
||||
:module-rows="moduleRows"
|
||||
:loading="busy"
|
||||
@open-i18n="i18nModalVisible = true"
|
||||
@open-module-picker="modulePickerOpen = true"
|
||||
/>
|
||||
</div>
|
||||
</SplitterPanel>
|
||||
@@ -941,6 +968,16 @@ loadModuleOptions()
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<!-- 绑定模块选择(模块树多选:勾分类节点自动级联其下全部模块) -->
|
||||
<ModulePickerModal
|
||||
:open="modulePickerOpen"
|
||||
:tree-data="moduleTree"
|
||||
:checked-keys="menuModules"
|
||||
:module-types="moduleTypes"
|
||||
@update:open="(v) => (modulePickerOpen = v)"
|
||||
@confirm="menuModules = $event"
|
||||
/>
|
||||
|
||||
<!-- 多语言名称编辑(通用组件,与模块配置共用) -->
|
||||
<I18nNameModal
|
||||
:open="i18nModalVisible"
|
||||
|
||||
Reference in new issue
Block a user