20260902173133
This commit is contained in:
1 parent
6b9cda5424
commit
db7813a2d2
16 files changed
+1350
-675
No files matched your search
@@ -19,9 +19,24 @@ import {
|
||||
RefreshCw,
|
||||
Languages,
|
||||
} from '@lucide/vue'
|
||||
import { Button, Checkbox, Input, Select, Switch, Splitter, SplitterPanel, Tree, Modal, Message, Form, FormItem, Dropdown } from '@/components/ui'
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
Select,
|
||||
Switch,
|
||||
Splitter,
|
||||
SplitterPanel,
|
||||
Tree,
|
||||
Modal,
|
||||
Message,
|
||||
Form,
|
||||
FormItem,
|
||||
Dropdown,
|
||||
} from '@/components/ui'
|
||||
import IconPicker from '@/components/icon-picker/IconPicker.vue'
|
||||
import FmsTable from '@/components/fms-table/FmsTable.vue'
|
||||
import { loadDataApi, saveObjectApi, nextIdApi } from '@/services/api'
|
||||
import { loadDataApi, loadDataBySqlApi, saveObjectApi, nextIdApi } from '@/services/api'
|
||||
import {
|
||||
buildTree,
|
||||
sortTree,
|
||||
@@ -290,6 +305,24 @@ async function onMenuDrop({ dragKey, dropKey, dropPosition }) {
|
||||
}
|
||||
|
||||
/* ================= 新增菜单 ================= */
|
||||
/** 从树节点提取纯菜单行(剔除 children/isLeaf/icon 等树注入字段),用于未保存节点切回时重建草稿 */
|
||||
function menuRowOf(node) {
|
||||
return {
|
||||
b_id: node.b_id,
|
||||
b_code: node.b_code,
|
||||
b_parent_id: node.b_parent_id ? String(node.b_parent_id) : null,
|
||||
b_name: node.b_name,
|
||||
b_i18n: node.b_i18n ?? null,
|
||||
b_menu_type: node.b_menu_type,
|
||||
b_route: node.b_route ?? null,
|
||||
b_module_id: node.b_module_id ?? null,
|
||||
b_power_code: node.b_power_code ?? null,
|
||||
b_icon: node.b_icon ?? null,
|
||||
b_xh: node.b_xh,
|
||||
b_canuse: node.b_canuse,
|
||||
}
|
||||
}
|
||||
|
||||
async function addMenu(parentId, menuTypeValue = 'page') {
|
||||
const res = await nextIdApi()
|
||||
const newId = String(res.data)
|
||||
@@ -400,16 +433,17 @@ async function loadMenuConfig(menuId) {
|
||||
}
|
||||
i18nValues.value = i18nValuesInit
|
||||
originalI18nValues.value = { ...i18nValuesInit }
|
||||
// 操作/按钮权限点:按 b_power_type='action' + b_module_id=菜单 id 加载
|
||||
// 操作/按钮权限点:仅 page/data/external 才有(目录是导航容器,无按钮权限)
|
||||
powersLoading.value = true
|
||||
try {
|
||||
const powerRes = menu
|
||||
? await loadDataApi(
|
||||
's_power',
|
||||
`b_power_type = 'action' AND b_module_id = ${id}`,
|
||||
'b_xh ASC, b_id ASC',
|
||||
)
|
||||
: { data: [] }
|
||||
const powerRes =
|
||||
menu && menu.b_menu_type !== 'directory'
|
||||
? await loadDataApi(
|
||||
's_power',
|
||||
`b_power_type = 'action' AND b_module_id = ${id}`,
|
||||
'b_xh ASC, b_id ASC',
|
||||
)
|
||||
: { data: [] }
|
||||
powers.value = (powerRes.data || []).map((row) => ({
|
||||
b_id: row.b_id,
|
||||
b_code: row.b_code,
|
||||
@@ -446,6 +480,26 @@ watch(menuSelectedId, (id) => {
|
||||
drafts.value = null
|
||||
return
|
||||
}
|
||||
// 未落库的新节点:临时 id 在库里查不到行,不能走 loadMenuConfig
|
||||
// (会得到 menu 为 null 的「真值」drafts,模板按 null 渲染直接崩溃)。
|
||||
// 从树节点重建草稿继续编辑;originals 与之一致(未编辑态)。
|
||||
if (unsavedMenuIds.has(String(id))) {
|
||||
const node = findTreeNode(menuTreeData.value, String(id))
|
||||
configLoading.value = false
|
||||
if (!node) {
|
||||
loadedMenuId.value = null
|
||||
drafts.value = null
|
||||
return
|
||||
}
|
||||
drafts.value = cloneData({ menu: menuRowOf(node) })
|
||||
originals.value = cloneData(drafts.value)
|
||||
i18nValues.value = { 'zh-CN': node.b_name || '' }
|
||||
originalI18nValues.value = { ...i18nValues.value }
|
||||
powers.value = []
|
||||
powersBaseline.value = '[]'
|
||||
loadedMenuId.value = String(id)
|
||||
return
|
||||
}
|
||||
loadedMenuId.value = null
|
||||
drafts.value = null
|
||||
void loadMenuConfig(id)
|
||||
@@ -459,7 +513,10 @@ function hasUnsavedChanges() {
|
||||
if (!drafts.value || !originals.value) return false
|
||||
if (JSON.stringify(drafts.value.menu) !== JSON.stringify(originals.value.menu)) return true
|
||||
// 多语言非默认语言变化视为未保存
|
||||
if (nonDefaultLanguages.some((l) => i18nValues.value[l.code] !== originalI18nValues.value[l.code])) return true
|
||||
if (
|
||||
nonDefaultLanguages.some((l) => i18nValues.value[l.code] !== originalI18nValues.value[l.code])
|
||||
)
|
||||
return true
|
||||
// 操作/按钮权限点变化视为未保存
|
||||
const current = JSON.stringify(powers.value)
|
||||
if (current !== powersBaseline.value) return true
|
||||
@@ -514,7 +571,8 @@ const formRef = ref(null)
|
||||
|
||||
async function saveMenu() {
|
||||
if (!selectedMenu.value || busy.value) return false
|
||||
if (loadedMenuId.value == null || String(loadedMenuId.value) !== String(menuSelectedId.value)) return false
|
||||
if (loadedMenuId.value == null || String(loadedMenuId.value) !== String(menuSelectedId.value))
|
||||
return false
|
||||
const menuId = String(selectedMenu.value.b_id)
|
||||
|
||||
// Validate form
|
||||
@@ -535,8 +593,14 @@ async function saveMenu() {
|
||||
const menuI18nKey = `menu.${menuData.b_code}`
|
||||
menuData.b_i18n = menuI18nKey
|
||||
|
||||
// 权限编码留空时按 菜单编码 自动生成(权限跟着菜单走)
|
||||
if (!menuData.b_power_code) menuData.b_power_code = `menu.${menuData.b_code}`
|
||||
// 目录是纯导航容器:可见性由子菜单决定,自身不需要访问权限编码,也不生成权限点。
|
||||
// 仅 page/data/external 需要访问权限编码;留空时按菜单编码自动生成(权限跟着菜单走)
|
||||
const menuIsDirectory = menuData.b_menu_type === 'directory'
|
||||
if (menuIsDirectory) {
|
||||
menuData.b_power_code = null
|
||||
} else if (!menuData.b_power_code) {
|
||||
menuData.b_power_code = `menu.${menuData.b_code}`
|
||||
}
|
||||
|
||||
const isNew = unsavedMenuIds.has(menuId)
|
||||
const req = { table: 's_menu', key_field: 'b_id' }
|
||||
@@ -568,15 +632,32 @@ async function saveMenu() {
|
||||
reqs.push(i18nReq)
|
||||
}
|
||||
|
||||
// 目录是纯导航容器:自身不需要访问权限编码与权限点。
|
||||
// 若该菜单此前(或改为目录前)生成过权限记录,保存时一并清理,避免孤儿数据:
|
||||
// - 访问权限(s_power.b_power_type='menu',按原权限编码关联)
|
||||
// - 操作/按钮权限(s_power.b_power_type='action',按 b_module_id=菜单 id 关联)
|
||||
if (menuIsDirectory) {
|
||||
const prevPowerCode = originals.value?.menu?.b_power_code
|
||||
if (prevPowerCode) {
|
||||
reqs.push({
|
||||
table: 's_power',
|
||||
key_field: 'b_id',
|
||||
deletes: [{ b_power_type: 'menu', b_resource: prevPowerCode }],
|
||||
})
|
||||
}
|
||||
reqs.push({
|
||||
table: 's_power',
|
||||
key_field: 'b_id',
|
||||
deletes: [{ b_power_type: 'action', b_module_id: menuId }],
|
||||
})
|
||||
}
|
||||
|
||||
// 访问权限点(s_power.b_power_type='menu',自动同步)
|
||||
const powerCode = (menuData.b_power_code || '').trim()
|
||||
if (powerCode) {
|
||||
// 查是否有已存在的访问权限(按 b_resource = 菜单权限编码)
|
||||
const accessPowerRes = menuData.b_id
|
||||
? await loadDataApi(
|
||||
's_power',
|
||||
`b_power_type = 'menu' AND b_resource = '${powerCode}'`,
|
||||
)
|
||||
? await loadDataApi('s_power', `b_power_type = 'menu' AND b_resource = '${powerCode}'`)
|
||||
: { data: [] }
|
||||
const existingAccess = accessPowerRes.data?.[0]
|
||||
if (existingAccess) {
|
||||
@@ -620,63 +701,66 @@ async function saveMenu() {
|
||||
}
|
||||
|
||||
// 操作/按钮权限点(s_power.b_power_type='action',diff 保存)
|
||||
const baselineIds = new Set()
|
||||
try {
|
||||
for (const r of JSON.parse(powersBaseline.value)) {
|
||||
if (r.b_id) baselineIds.add(String(r.b_id))
|
||||
// 目录是导航容器,不保存操作/按钮权限点
|
||||
if (!menuIsDirectory && powers.value.length > 0) {
|
||||
const baselineIds = new Set()
|
||||
try {
|
||||
for (const r of JSON.parse(powersBaseline.value)) {
|
||||
if (r.b_id) baselineIds.add(String(r.b_id))
|
||||
}
|
||||
} catch {
|
||||
/* 空基线 */
|
||||
}
|
||||
} catch { /* 空基线 */ }
|
||||
const currentIds = new Set(
|
||||
powers.value.filter((r) => r.b_id).map((r) => String(r.b_id)),
|
||||
)
|
||||
// 被删除的行
|
||||
for (const id of baselineIds) {
|
||||
if (!currentIds.has(id)) {
|
||||
reqs.push({ table: 's_power', key_field: 'b_id', deletes: [{ b_id: id }] })
|
||||
const currentIds = new Set(powers.value.filter((r) => r.b_id).map((r) => String(r.b_id)))
|
||||
// 被删除的行
|
||||
for (const id of baselineIds) {
|
||||
if (!currentIds.has(id)) {
|
||||
reqs.push({ table: 's_power', key_field: 'b_id', deletes: [{ b_id: id }] })
|
||||
}
|
||||
}
|
||||
}
|
||||
// 新增/更新的行
|
||||
for (const row of powers.value) {
|
||||
if (!row.b_code || !String(row.b_code).trim()) continue // 未填编码的行不保存
|
||||
// 基线中已存在的 id 为真实主键 → 更新;临时 id(负数,未入库)→ 新增
|
||||
if (row.b_id && baselineIds.has(String(row.b_id))) {
|
||||
reqs.push({
|
||||
table: 's_power',
|
||||
key_field: 'b_id',
|
||||
updates: [
|
||||
{
|
||||
b_id: row.b_id,
|
||||
b_power_type: 'action',
|
||||
b_code: row.b_code,
|
||||
b_name: row.b_name,
|
||||
b_resource: row.b_code,
|
||||
b_operation: 'execute',
|
||||
b_module_id: menuId,
|
||||
b_field_id: null,
|
||||
b_canuse: Number(row.b_canuse) === 1 ? 1 : 0,
|
||||
},
|
||||
],
|
||||
})
|
||||
} else {
|
||||
// 新增行需要分配主键
|
||||
const newId = String((await nextIdApi()).data)
|
||||
reqs.push({
|
||||
table: 's_power',
|
||||
key_field: 'b_id',
|
||||
inserts: [
|
||||
{
|
||||
b_id: newId,
|
||||
b_power_type: 'action',
|
||||
b_code: row.b_code,
|
||||
b_name: row.b_name,
|
||||
b_resource: row.b_code,
|
||||
b_operation: 'execute',
|
||||
b_module_id: menuId,
|
||||
b_field_id: null,
|
||||
b_canuse: Number(row.b_canuse) === 1 ? 1 : 0,
|
||||
},
|
||||
],
|
||||
})
|
||||
// 新增/更新的行
|
||||
for (const row of powers.value) {
|
||||
if (!row.b_code || !String(row.b_code).trim()) continue // 未填编码的行不保存
|
||||
// 基线中已存在的 id 为真实主键 → 更新;临时 id(负数,未入库)→ 新增
|
||||
if (row.b_id && baselineIds.has(String(row.b_id))) {
|
||||
reqs.push({
|
||||
table: 's_power',
|
||||
key_field: 'b_id',
|
||||
updates: [
|
||||
{
|
||||
b_id: row.b_id,
|
||||
b_power_type: 'action',
|
||||
b_code: row.b_code,
|
||||
b_name: row.b_name,
|
||||
b_resource: row.b_code,
|
||||
b_operation: 'execute',
|
||||
b_module_id: menuId,
|
||||
b_field_id: null,
|
||||
b_canuse: Number(row.b_canuse) === 1 ? 1 : 0,
|
||||
},
|
||||
],
|
||||
})
|
||||
} else {
|
||||
// 新增行需要分配主键
|
||||
const newId = String((await nextIdApi()).data)
|
||||
reqs.push({
|
||||
table: 's_power',
|
||||
key_field: 'b_id',
|
||||
inserts: [
|
||||
{
|
||||
b_id: newId,
|
||||
b_power_type: 'action',
|
||||
b_code: row.b_code,
|
||||
b_name: row.b_name,
|
||||
b_resource: row.b_code,
|
||||
b_operation: 'execute',
|
||||
b_module_id: menuId,
|
||||
b_field_id: null,
|
||||
b_canuse: Number(row.b_canuse) === 1 ? 1 : 0,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,6 +819,7 @@ function onMenuTypeChange(v) {
|
||||
if (v === 'directory') {
|
||||
drafts.value.menu.b_route = null
|
||||
drafts.value.menu.b_module_id = null
|
||||
drafts.value.menu.b_power_code = null
|
||||
} else if (v === 'page') {
|
||||
drafts.value.menu.b_module_id = null
|
||||
} else if (v === 'data') {
|
||||
@@ -758,7 +843,9 @@ async function onToggleEnable(node, checked) {
|
||||
// 未保存草稿仅本地,落库时生效
|
||||
if (unsavedMenuIds.has(id)) return
|
||||
try {
|
||||
await saveObjectApi([{ table: 's_menu', key_field: 'b_id', updates: [{ b_id: id, b_canuse: newVal }] }])
|
||||
await saveObjectApi([
|
||||
{ table: 's_menu', key_field: 'b_id', updates: [{ b_id: id, b_canuse: newVal }] },
|
||||
])
|
||||
Message.success(newVal ? '菜单已启用' : '菜单已停用')
|
||||
} catch (e) {
|
||||
node.b_canuse = newVal === 1 ? 0 : 1
|
||||
@@ -776,44 +863,34 @@ const powers = ref([])
|
||||
const powersBaseline = ref('')
|
||||
const powersLoading = ref(false)
|
||||
|
||||
/** 权限点表格列配置 */
|
||||
/** 权限点表格列配置
|
||||
* 列宽铺满约定(库文档「列宽铺满容器hack方式」):固定列设 width,
|
||||
* 弹性列(权限名称)设 minWidth 占满剩余宽度,并在 colResizable 中禁用其拖拽。 */
|
||||
const powerColumns = shallowRef([
|
||||
{
|
||||
dataIndex: 'b_code',
|
||||
title: '权限编码',
|
||||
width: 170,
|
||||
editor: { type: 'input', placeholder: '如 add' },
|
||||
width: 150,
|
||||
editor: { type: 'input' },
|
||||
},
|
||||
{
|
||||
dataIndex: 'b_name',
|
||||
title: '权限名称',
|
||||
width: 170,
|
||||
editor: { type: 'input', placeholder: '如 新增' },
|
||||
},
|
||||
{
|
||||
dataIndex: 'b_power_type',
|
||||
title: '类型',
|
||||
width: 130,
|
||||
editor: {
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '操作权限', value: 'action' },
|
||||
{ label: '按钮权限', value: 'button' },
|
||||
],
|
||||
},
|
||||
minWidth: 150,
|
||||
editor: { type: 'input' },
|
||||
},
|
||||
{
|
||||
dataIndex: 'b_canuse',
|
||||
title: '启用',
|
||||
width: 70,
|
||||
align: 'center',
|
||||
width: 70,
|
||||
editor: { type: 'checkbox' },
|
||||
},
|
||||
{
|
||||
dataIndex: 'actions',
|
||||
title: '操作',
|
||||
width: 70,
|
||||
align: 'center',
|
||||
width: 70,
|
||||
actions: [
|
||||
{
|
||||
type: 'delete',
|
||||
@@ -885,6 +962,81 @@ function batchDeletePowers() {
|
||||
Message.success(`已删除 ${keys.length} 条权限`)
|
||||
}
|
||||
|
||||
/* ================= 权限来源库(全库聚合,快速导入) ================= */
|
||||
/** 全库去重的操作权限(s_power.b_power_type='action',按编码聚合) */
|
||||
const powerLibrary = ref([])
|
||||
const powerLibraryLoading = ref(false)
|
||||
/** 来源库勾选的权限编码 */
|
||||
const selectedLibCodes = ref([])
|
||||
/** 来源库表格引用与行选择 */
|
||||
const libTableRef = ref(null)
|
||||
|
||||
const powerLibraryColumns = shallowRef([
|
||||
// 列宽铺满约定同 powerColumns:编码固定宽,名称弹性占满
|
||||
{ dataIndex: 'b_code', title: '权限编码', width: 100 },
|
||||
{ dataIndex: 'b_name', title: '权限名称', minWidth: 80 },
|
||||
])
|
||||
|
||||
const libRowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedLibCodes.value,
|
||||
onChange: (keys) => {
|
||||
selectedLibCodes.value = keys
|
||||
},
|
||||
}))
|
||||
|
||||
/** 聚合全库出现的操作权限(编码/名称,去重) */
|
||||
async function loadPowerLibrary() {
|
||||
powerLibraryLoading.value = true
|
||||
try {
|
||||
// 参考旧系统 module-powervalue:把全库权限按编码聚合为"来源库"
|
||||
const res = await loadDataBySqlApi(
|
||||
`SELECT b_code, MAX(b_name) AS b_name FROM s_power
|
||||
WHERE b_power_type = 'action' AND b_code IS NOT NULL AND b_code <> ''
|
||||
GROUP BY b_code ORDER BY b_code`,
|
||||
)
|
||||
powerLibrary.value = (res.data || []).map((row) => ({
|
||||
b_code: row.b_code,
|
||||
b_name: row.b_name,
|
||||
}))
|
||||
selectedLibCodes.value = []
|
||||
} catch (e) {
|
||||
Message.error('加载权限库失败:' + (e?.message || e))
|
||||
powerLibrary.value = []
|
||||
} finally {
|
||||
powerLibraryLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 把来源库勾选的权限导入当前菜单(跳过已存在编码) */
|
||||
function importSelectedPowers() {
|
||||
const codes = selectedLibCodes.value
|
||||
if (!codes.length) {
|
||||
Message.warning('请先在权限库中勾选要导入的权限')
|
||||
return
|
||||
}
|
||||
const existing = new Set(powers.value.map((r) => String(r.b_code || '').trim()))
|
||||
const added = []
|
||||
for (const code of codes) {
|
||||
if (existing.has(String(code).trim())) continue
|
||||
const lib = powerLibrary.value.find((r) => String(r.b_code) === String(code))
|
||||
added.push({
|
||||
b_id: createTempId(),
|
||||
b_code: lib?.b_code || code,
|
||||
b_name: lib?.b_name || '',
|
||||
b_power_type: 'action',
|
||||
b_canuse: 1,
|
||||
})
|
||||
existing.add(String(code).trim())
|
||||
}
|
||||
if (!added.length) {
|
||||
Message.warning('勾选的权限均已存在,无需导入')
|
||||
return
|
||||
}
|
||||
powers.value = [...powers.value, ...added]
|
||||
selectedLibCodes.value = []
|
||||
Message.success(`已导入 ${added.length} 条权限`)
|
||||
}
|
||||
|
||||
/* ================= 加载模块列表(供 data 类型下拉选择) ================= */
|
||||
const moduleOptions = ref([])
|
||||
let moduleOptionsLoaded = false
|
||||
@@ -892,7 +1044,11 @@ 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', 'query')", 'b_xh ASC, b_id ASC')
|
||||
const res = await loadDataApi(
|
||||
's_module',
|
||||
"b_canuse = 1 AND b_module_type IN ('data', 'query')",
|
||||
'b_xh ASC, b_id ASC',
|
||||
)
|
||||
moduleOptions.value = (res.data || []).map((m) => ({
|
||||
label: `[${m.b_code}] ${m.b_name}`,
|
||||
value: String(m.b_id),
|
||||
@@ -937,6 +1093,7 @@ async function onConfirmOk() {
|
||||
/* ================= 初始化 ================= */
|
||||
loadMenuTree()
|
||||
loadModuleOptions()
|
||||
loadPowerLibrary()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -946,7 +1103,7 @@ loadModuleOptions()
|
||||
<SplitterPanel :default-size="248" :min="180" :max="420">
|
||||
<aside class="mm-tree-panel">
|
||||
<div class="mm-tree-header">
|
||||
<Input v-model="menuKeyword" placeholder="搜索菜单" allow-clear class="mm-tree-search">
|
||||
<Input v-model="menuKeyword" allow-clear class="mm-tree-search">
|
||||
<template #prefix><Search :size="14" class="mm-tree-search__icon" /></template>
|
||||
</Input>
|
||||
<div class="mm-tree-header__actions">
|
||||
@@ -960,7 +1117,12 @@ loadModuleOptions()
|
||||
<RefreshCw :size="14" />
|
||||
</Button>
|
||||
<Dropdown :items="toolbarAddOptions" @click="onToolbarAdd">
|
||||
<Button type="ghost" class="mm-tree-header__icon-btn" title="新增菜单" aria-label="新增菜单">
|
||||
<Button
|
||||
type="ghost"
|
||||
class="mm-tree-header__icon-btn"
|
||||
title="新增菜单"
|
||||
aria-label="新增菜单"
|
||||
>
|
||||
<Plus :size="14" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
@@ -1027,30 +1189,35 @@ loadModuleOptions()
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 编辑面板 -->
|
||||
<div class="mm-body__inner">
|
||||
<div v-if="drafts" class="mm-config-content">
|
||||
<div class="mm-config-pane">
|
||||
<Form
|
||||
<!-- 主体:三块 Splitter 分割 -->
|
||||
<div v-if="drafts" class="mm-edit-body">
|
||||
<Splitter class="mm-edit-splitter">
|
||||
<SplitterPanel :default-size="360">
|
||||
<div class="mm-form-col__card">
|
||||
<div class="mm-form-col__head">
|
||||
<div class="mm-pane__title">菜单配置</div>
|
||||
</div>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="drafts.menu"
|
||||
:rules="{
|
||||
b_code: [{ required: true, message: '请填写菜单编码' }],
|
||||
b_name: [{ required: true, message: '请填写菜单名称' }],
|
||||
b_menu_type: [{ required: true, message: '请选择菜单类型' }],
|
||||
b_route: isPage || isExternal ? [{ required: true, message: '请填写路由地址' }] : [],
|
||||
b_route:
|
||||
isPage || isExternal
|
||||
? [{ required: true, message: '请填写路由地址' }]
|
||||
: [],
|
||||
b_module_id: isData ? [{ required: true, message: '请选择绑定模块' }] : [],
|
||||
}"
|
||||
layout="vertical"
|
||||
:columns="3"
|
||||
max-width="720px"
|
||||
:gap="20"
|
||||
>
|
||||
<FormItem label="菜单名称" name="b_name" required>
|
||||
<div class="mm-name-field">
|
||||
<Input
|
||||
class="mm-name-field__input"
|
||||
:model-value="drafts.menu.b_name"
|
||||
placeholder="请输入菜单名称(简体中文)"
|
||||
@update:model-value="(v) => setField('b_name', v)"
|
||||
/>
|
||||
<Button
|
||||
@@ -1066,7 +1233,6 @@ loadModuleOptions()
|
||||
<FormItem label="菜单编码" name="b_code" required>
|
||||
<Input
|
||||
:model-value="drafts.menu.b_code"
|
||||
placeholder="请输入菜单编码,全局唯一"
|
||||
@update:model-value="(v) => setField('b_code', v)"
|
||||
/>
|
||||
</FormItem>
|
||||
@@ -1074,17 +1240,15 @@ loadModuleOptions()
|
||||
<Select
|
||||
:model-value="drafts.menu.b_menu_type"
|
||||
:options="menuTypeOptions"
|
||||
placeholder="请选择菜单类型"
|
||||
@update:model-value="onMenuTypeChange"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="上级菜单">
|
||||
<FormItem label="上级菜单" name="b_parent_id">
|
||||
<Input :model-value="parentMenuName || '根节点(顶级菜单)'" readonly />
|
||||
</FormItem>
|
||||
<FormItem label="图标" name="b_icon">
|
||||
<Input
|
||||
<IconPicker
|
||||
:model-value="drafts.menu.b_icon"
|
||||
placeholder="lucide 图标名,如 Settings"
|
||||
@update:model-value="(v) => setField('b_icon', v)"
|
||||
/>
|
||||
</FormItem>
|
||||
@@ -1099,7 +1263,6 @@ loadModuleOptions()
|
||||
<FormItem v-if="isPage" label="路由地址" name="b_route" required>
|
||||
<Input
|
||||
:model-value="drafts.menu.b_route"
|
||||
placeholder="如 /system/module-management"
|
||||
@update:model-value="(v) => setField('b_route', v)"
|
||||
/>
|
||||
</FormItem>
|
||||
@@ -1108,7 +1271,6 @@ loadModuleOptions()
|
||||
<Select
|
||||
:model-value="drafts.menu.b_module_id"
|
||||
:options="moduleOptions"
|
||||
placeholder="请选择绑定的数据/查询模块"
|
||||
@update:model-value="(v) => setField('b_module_id', v)"
|
||||
/>
|
||||
</FormItem>
|
||||
@@ -1116,7 +1278,6 @@ loadModuleOptions()
|
||||
<FormItem v-if="isExternal" label="外链地址" name="b_route" required>
|
||||
<Input
|
||||
:model-value="drafts.menu.b_route"
|
||||
placeholder="如 https://example.com"
|
||||
@update:model-value="(v) => setField('b_route', v)"
|
||||
/>
|
||||
</FormItem>
|
||||
@@ -1126,12 +1287,11 @@ loadModuleOptions()
|
||||
@update:model-value="(v) => setField('b_canuse', v ? 1 : 0)"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="访问权限编码" name="b_power_code">
|
||||
<FormItem v-if="!isDirectory" label="访问权限编码" name="b_power_code">
|
||||
<div class="mm-power-code-field">
|
||||
<Input
|
||||
class="mm-power-code-field__input"
|
||||
:model-value="drafts.menu.b_power_code"
|
||||
placeholder="留空保存时自动生成 menu.编码"
|
||||
@update:model-value="(v) => setField('b_power_code', v)"
|
||||
/>
|
||||
<Button type="outline" size="small" @click="autoPowerCode">
|
||||
@@ -1140,51 +1300,99 @@ loadModuleOptions()
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</SplitterPanel>
|
||||
|
||||
<!-- 权限点:访问(自动)+ 操作/按钮 -->
|
||||
<div class="mm-config-pane">
|
||||
<div class="mm-pane__title">权限点</div>
|
||||
<!-- 访问权限:随菜单自动同步(只读展示) -->
|
||||
<div class="mm-access-bar">
|
||||
<span class="mm-access-bar__tag">访问</span>
|
||||
<span class="mm-access-bar__code">{{ accessPowerDesc || '(待生成)' }}</span>
|
||||
<span class="mm-access-bar__hint">自动同步,随菜单保存</span>
|
||||
<!-- 权限面板:仅 page/data/external(目录无权限) -->
|
||||
<SplitterPanel v-if="!isDirectory">
|
||||
<div class="mm-power-card">
|
||||
<div class="mm-power-card__head">
|
||||
<div class="mm-pane__title">当前菜单权限</div>
|
||||
<div class="mm-power-toolbar">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="powersLoading"
|
||||
@click="addPowerRow"
|
||||
>
|
||||
<Plus :size="12" /> 新增
|
||||
</Button>
|
||||
<Button
|
||||
type="danger"
|
||||
size="small"
|
||||
:disabled="powersLoading || !selectedPowerKeys.length"
|
||||
@click="batchDeletePowers"
|
||||
>
|
||||
<Trash2 :size="12" /> 删除{{
|
||||
selectedPowerKeys.length ? `(${selectedPowerKeys.length})` : ''
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="powersLoading" class="mm-power-loading">加载中...</div>
|
||||
<div v-else class="mm-power-table-wrap">
|
||||
<FmsTable
|
||||
ref="powerTableRef"
|
||||
v-model:columns="powerColumns"
|
||||
:data-source="powers"
|
||||
:editable="true"
|
||||
row-key="b_id"
|
||||
:row-selection="rowSelection"
|
||||
:col-resizable="{ disabled: (col) => col.dataIndex === 'b_name' }"
|
||||
@change="() => {}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mm-power-toolbar">
|
||||
<Button type="primary" size="small" :disabled="powersLoading" @click="addPowerRow">
|
||||
<Plus :size="12" /> 新增权限
|
||||
</Button>
|
||||
<Button
|
||||
type="danger"
|
||||
size="small"
|
||||
:disabled="powersLoading || !selectedPowerKeys.length"
|
||||
@click="batchDeletePowers"
|
||||
>
|
||||
<Trash2 :size="12" /> 批量删除{{ selectedPowerKeys.length ? `(${selectedPowerKeys.length})` : '' }}
|
||||
</Button>
|
||||
<span class="mm-power-toolbar__hint">操作/按钮权限用于页面的按钮级控制。</span>
|
||||
</SplitterPanel>
|
||||
|
||||
<!-- 权限来源库:全库聚合,快速导入 -->
|
||||
<SplitterPanel v-if="!isDirectory" :default-size="260">
|
||||
<div class="mm-power-card">
|
||||
<div class="mm-power-card__head">
|
||||
<div class="mm-pane__title">权限库</div>
|
||||
<div class="mm-power-toolbar">
|
||||
<Button
|
||||
type="outline"
|
||||
size="small"
|
||||
:loading="powerLibraryLoading"
|
||||
@click="loadPowerLibrary"
|
||||
>
|
||||
<RefreshCw :size="12" /> 刷新
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="!selectedLibCodes.length"
|
||||
@click="importSelectedPowers"
|
||||
>
|
||||
<Plus :size="12" /> 导入选中{{
|
||||
selectedLibCodes.length ? `(${selectedLibCodes.length})` : ''
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="powerLibraryLoading" class="mm-power-loading">加载中...</div>
|
||||
<div v-else class="mm-lib-table-wrap">
|
||||
<FmsTable
|
||||
ref="libTableRef"
|
||||
v-model:columns="powerLibraryColumns"
|
||||
:data-source="powerLibrary"
|
||||
:seq="false"
|
||||
row-key="b_code"
|
||||
:row-selection="libRowSelection"
|
||||
:col-resizable="{ disabled: (col) => col.dataIndex === 'b_name' }"
|
||||
@change="() => {}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="powersLoading" class="mm-power-loading">加载中...</div>
|
||||
<div v-else class="mm-power-table-wrap">
|
||||
<FmsTable
|
||||
ref="powerTableRef"
|
||||
v-model:columns="powerColumns"
|
||||
:data-source="powers"
|
||||
:editable="true"
|
||||
row-key="b_id"
|
||||
:row-selection="rowSelection"
|
||||
@change="() => {}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="configLoading" class="mm-config-state">
|
||||
<span class="mm-config-state__spinner"></span>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<div v-else class="mm-config-state">暂无配置数据</div>
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</div>
|
||||
<div v-else-if="configLoading" class="mm-config-state">
|
||||
<span class="mm-config-state__spinner"></span>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<div v-else class="mm-config-state">暂无配置数据</div>
|
||||
</template>
|
||||
<div v-else class="mm-main__empty">
|
||||
<FolderTree :size="32" />
|
||||
@@ -1205,7 +1413,9 @@ loadModuleOptions()
|
||||
>
|
||||
<p style="margin: 0; line-height: 1.7; color: var(--fms-text)">{{ confirmState?.content }}</p>
|
||||
<template #footer>
|
||||
<Button type="outline" :disabled="confirmSaving" @click="confirmVisible = false">取消</Button>
|
||||
<Button type="outline" :disabled="confirmSaving" @click="confirmVisible = false"
|
||||
>取消</Button
|
||||
>
|
||||
<Button
|
||||
:type="confirmState?.okType || 'primary'"
|
||||
:loading="confirmSaving"
|
||||
@@ -1397,19 +1607,6 @@ loadModuleOptions()
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 各配置区块(多语言 / 权限)间距 */
|
||||
.mm-config-pane + .mm-config-pane {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* 区块标题 */
|
||||
.mm-pane__title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--fms-text);
|
||||
}
|
||||
|
||||
/* 菜单名称 + 多语言图标按钮 */
|
||||
.mm-name-field {
|
||||
display: flex;
|
||||
@@ -1432,7 +1629,6 @@ loadModuleOptions()
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.mm-power-code-field__input {
|
||||
@@ -1440,69 +1636,6 @@ loadModuleOptions()
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 权限点列表 */
|
||||
.mm-power-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mm-power-toolbar__hint {
|
||||
font-size: 12px;
|
||||
color: var(--fms-text-secondary);
|
||||
}
|
||||
|
||||
.mm-power-loading {
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 访问权限条(自动同步展示) */
|
||||
.mm-access-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
padding: 6px 10px;
|
||||
border: 1px dashed var(--fms-border);
|
||||
border-radius: var(--fms-control-radius);
|
||||
background: var(--fms-secondary);
|
||||
font-size: 12px;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.mm-access-bar__tag {
|
||||
flex-shrink: 0;
|
||||
padding: 0 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--fms-primary, #1677ff);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mm-access-bar__code {
|
||||
flex-shrink: 0;
|
||||
font-weight: 600;
|
||||
color: var(--fms-text);
|
||||
}
|
||||
|
||||
.mm-access-bar__hint {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--fms-text-secondary);
|
||||
}
|
||||
|
||||
/* 权限点 FmsTable 容器(固定高度,FmsTable 需要显式高度渲染) */
|
||||
.mm-power-table-wrap {
|
||||
max-width: 720px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
/* ---- 右侧主区 ---- */
|
||||
.mm-main {
|
||||
display: flex;
|
||||
@@ -1513,10 +1646,7 @@ loadModuleOptions()
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
box-shadow: var(--fms-shadow);
|
||||
padding: 8px 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mm-main__empty {
|
||||
@@ -1528,6 +1658,9 @@ loadModuleOptions()
|
||||
flex: 1;
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 14px;
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
box-shadow: var(--fms-shadow);
|
||||
|
||||
svg {
|
||||
color: var(--fms-text-secondary);
|
||||
@@ -1540,24 +1673,123 @@ loadModuleOptions()
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 8px 0;
|
||||
flex-shrink: 0;
|
||||
padding: 8px 10px;
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
}
|
||||
|
||||
/* ---- 内容区 ---- */
|
||||
.mm-body__inner {
|
||||
/* ---- 主体三块 Splitter ---- */
|
||||
.mm-edit-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mm-config-content {
|
||||
.mm-edit-splitter :deep(> .fms-splitter-panel) {
|
||||
overflow: hidden;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
/* 第一个面板(菜单配置)去左边距;最后一个面板(权限库)去右边距 */
|
||||
.mm-edit-splitter :deep(> .fms-splitter-panel:first-child) {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.mm-edit-splitter :deep(> .fms-splitter-panel:last-child) {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* 左栏:表单卡片 */
|
||||
.mm-form-col__card {
|
||||
height: 100%;
|
||||
padding: 8px 10px;
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
box-shadow: var(--fms-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 左卡片头部 */
|
||||
.mm-form-col__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 0;
|
||||
|
||||
.mm-pane__title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 右栏:权限卡片 */
|
||||
.mm-power-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 8px 10px;
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
box-shadow: var(--fms-shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mm-power-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.mm-pane__title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 权限点 FmsTable 容器(撑满卡片剩余高度) */
|
||||
.mm-power-table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mm-config-pane {
|
||||
max-width: 720px;
|
||||
/* 来源库表格容器(撑满卡片剩余高度) */
|
||||
.mm-lib-table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 权限点列表工具条 */
|
||||
.mm-power-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mm-power-loading {
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 区块标题 */
|
||||
.mm-pane__title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--fms-text);
|
||||
}
|
||||
|
||||
/* 各配置区块间距 */
|
||||
.mm-config-pane + .mm-config-pane {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.mm-config-state {
|
||||
@@ -1566,7 +1798,7 @@ loadModuleOptions()
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -1585,4 +1817,4 @@ loadModuleOptions()
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
-1
@@ -467,7 +467,6 @@ const treeFeatures = computed(() => ({
|
||||
:loading="loading"
|
||||
:data-columns="dataColumns"
|
||||
:active-key="activeKey"
|
||||
:row-drag-handle="false"
|
||||
@back="switchView('design')"
|
||||
@row-click="onDataRowClick"
|
||||
@change="() => emit('update', localRows.value)"
|
||||
|
||||
-3
@@ -12,8 +12,6 @@ defineProps({
|
||||
dataColumns: { type: Array, required: true },
|
||||
/** 高亮行 id(行点击由壳组件更新) */
|
||||
activeKey: { type: [String, Number], default: null },
|
||||
/** 是否显示行拖拽把手(表格编辑器关闭,避免与列宽拖拽视觉混淆) */
|
||||
rowDragHandle: { type: Boolean, default: true },
|
||||
/** 是否显示「返回设计视图」按钮(数据视图常驻的宿主传 false) */
|
||||
showBack: { type: Boolean, default: true },
|
||||
})
|
||||
@@ -32,7 +30,6 @@ const emit = defineEmits(['back', 'row-click', 'change', 'row-order-change'])
|
||||
:data-source="rows"
|
||||
:editable="!loading"
|
||||
:row-draggable="!loading"
|
||||
:row-drag-handle="rowDragHandle"
|
||||
row-key="b_id"
|
||||
:active-row-key="activeKey"
|
||||
@row-click="(_, row) => emit('row-click', row)"
|
||||
|
||||
@@ -38,12 +38,17 @@ import {
|
||||
Message,
|
||||
} from '@/components/ui'
|
||||
import { loadDataApi, saveObjectApi, nextIdApi, describeApi } from '@/services/api'
|
||||
import { buildTree, sortTree, findTreeNode, collectAllKeys, expandAncestors, filterTreeByText, insertTreeNode, removeTreeNode } from '@/utils/tree'
|
||||
import {
|
||||
buildModuleI18nKey,
|
||||
buildModuleItemI18nKey,
|
||||
syncModuleItemI18nKeys,
|
||||
} from '@/utils/i18n'
|
||||
buildTree,
|
||||
sortTree,
|
||||
findTreeNode,
|
||||
collectAllKeys,
|
||||
expandAncestors,
|
||||
filterTreeByText,
|
||||
insertTreeNode,
|
||||
removeTreeNode,
|
||||
} from '@/utils/tree'
|
||||
import { buildModuleI18nKey, buildModuleItemI18nKey, syncModuleItemI18nKeys } from '@/utils/i18n'
|
||||
import { createTempId } from '@/utils/tempId'
|
||||
import {
|
||||
cloneData,
|
||||
@@ -176,7 +181,11 @@ function validateAutoCode() {
|
||||
|
||||
async function saveModule() {
|
||||
if (!selectedModule.value || busy.value) return false
|
||||
if (loadedModuleId.value == null || String(loadedModuleId.value) !== String(moduleSelectedId.value)) return false
|
||||
if (
|
||||
loadedModuleId.value == null ||
|
||||
String(loadedModuleId.value) !== String(moduleSelectedId.value)
|
||||
)
|
||||
return false
|
||||
const moduleId = String(selectedModule.value.b_id)
|
||||
configSaving.value = true
|
||||
try {
|
||||
@@ -207,7 +216,10 @@ async function saveModule() {
|
||||
moduleData.b_i18n = buildModuleI18nKey(moduleData.b_module_type, moduleCode)
|
||||
|
||||
const isNew = unsavedModuleIds.has(moduleId)
|
||||
if (moduleData && (isNew || JSON.stringify(moduleData) !== JSON.stringify(originals.value?.module))) {
|
||||
if (
|
||||
moduleData &&
|
||||
(isNew || JSON.stringify(moduleData) !== JSON.stringify(originals.value?.module))
|
||||
) {
|
||||
const moduleRequest = { table: 's_module', key_field: 'b_id' }
|
||||
if (isNew) moduleRequest.inserts = [moduleData]
|
||||
else moduleRequest.updates = [moduleData]
|
||||
@@ -348,7 +360,9 @@ watch(moduleKeyword, (kw) => {
|
||||
moduleExpandedKeys.value = kw.trim() ? collectAllKeys(moduleTreeData.value) : []
|
||||
})
|
||||
|
||||
const filteredModuleTree = computed(() => filterTreeByText(moduleTreeData.value, moduleKeyword.value))
|
||||
const filteredModuleTree = computed(() =>
|
||||
filterTreeByText(moduleTreeData.value, moduleKeyword.value),
|
||||
)
|
||||
|
||||
// 是否存在「已保存」的子模块(未保存草稿子树不算,可随父一并移除)
|
||||
function hasSavedModuleDescendant(node) {
|
||||
@@ -404,6 +418,27 @@ function onModuleContextMenuClick({ item, node }) {
|
||||
else if (item.key === 'delete') void deleteModule(parentId)
|
||||
}
|
||||
|
||||
/** 从树节点提取纯模块行(剔除 children/isLeaf/icon 等树注入字段),用于未保存模块切回时重建草稿 */
|
||||
function moduleRowOf(node) {
|
||||
return {
|
||||
b_id: node.b_id,
|
||||
b_code: node.b_code,
|
||||
b_parent_id: node.b_parent_id ? String(node.b_parent_id) : null,
|
||||
b_name: node.b_name,
|
||||
b_i18n: node.b_i18n ?? null,
|
||||
b_module_type: node.b_module_type,
|
||||
b_viewtable: node.b_viewtable ?? null,
|
||||
b_savetable: node.b_savetable ?? null,
|
||||
b_keyfield: node.b_keyfield ?? null,
|
||||
b_orderfield: node.b_orderfield ?? null,
|
||||
b_delete_policy: node.b_delete_policy ?? null,
|
||||
b_edit_mode: node.b_edit_mode ?? null,
|
||||
b_canuse: node.b_canuse,
|
||||
b_xh: node.b_xh,
|
||||
b_bz: node.b_bz ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
async function addModule(parentId, moduleType) {
|
||||
const res = await nextIdApi()
|
||||
const newId = String(res.data)
|
||||
@@ -627,7 +662,8 @@ async function loadModuleConfig(moduleId) {
|
||||
loadDataApi('s_autocode', `b_module_id = ${id}`, 'b_id ASC'),
|
||||
loadDataApi('s_i18n', `b_module_id = ${id}`, 'b_key ASC, b_locale ASC'),
|
||||
])
|
||||
;[moduleRes, fieldsRes, groupsRes, listRes, editRes, queryRes, autoCodeRes, i18nRes] = responses
|
||||
;[moduleRes, fieldsRes, groupsRes, listRes, editRes, queryRes, autoCodeRes, i18nRes] =
|
||||
responses
|
||||
} else if (type === 'query') {
|
||||
const responses = await Promise.all([
|
||||
loadDataApi('s_module', `b_id = ${id}`),
|
||||
@@ -657,7 +693,10 @@ async function loadModuleConfig(moduleId) {
|
||||
// 只取原始扁平行传给 ModuleI18nPanel,由面板自持 pivot/reconcile/保存 diff
|
||||
rawI18nRows.value = i18nRes
|
||||
? (i18nRes.data || []).map((row) => ({
|
||||
b_id: row.b_id, b_key: row.b_key, b_locale: row.b_locale, b_value: row.b_value,
|
||||
b_id: row.b_id,
|
||||
b_key: row.b_key,
|
||||
b_locale: row.b_locale,
|
||||
b_value: row.b_value,
|
||||
}))
|
||||
: []
|
||||
// 列表配置不再自动补全:行存在即展示,删除即真删,由用户通过字段库手动管理(与查询配置一致)。
|
||||
@@ -687,6 +726,30 @@ watch(moduleSelectedId, (id) => {
|
||||
rawI18nRows.value = []
|
||||
return
|
||||
}
|
||||
// 未落库的新模块:临时 id 查库无行,不走 loadModuleConfig(会得到 module 为 null 的草稿)。
|
||||
// 从树节点重建草稿(与 addModule 的初始空草稿同构),子配置(字段/分组等)从空开始。
|
||||
if (unsavedModuleIds.has(String(id))) {
|
||||
const node = findTreeNode(moduleTreeData.value, String(id))
|
||||
configLoading.value = false
|
||||
if (!node) {
|
||||
loadedModuleId.value = null
|
||||
drafts.value = null
|
||||
return
|
||||
}
|
||||
drafts.value = cloneData({
|
||||
module: moduleRowOf(node),
|
||||
fields: [],
|
||||
groups: [],
|
||||
listConfig: [],
|
||||
editConfig: [],
|
||||
queryConfig: [],
|
||||
autoCode: [],
|
||||
})
|
||||
rawI18nRows.value = []
|
||||
originals.value = cloneData(drafts.value)
|
||||
loadedModuleId.value = String(id)
|
||||
return
|
||||
}
|
||||
// 清空旧草稿,让右侧进入加载态(否则旧数据残留,v-if="drafts" 仍为真,看不到 loading)
|
||||
loadedModuleId.value = null
|
||||
drafts.value = null
|
||||
@@ -702,16 +765,10 @@ const switchSaving = ref(false)
|
||||
// 是否有未保存的修改(仅切换时按需检查,不做实时监听)
|
||||
function hasUnsavedChanges() {
|
||||
if (!drafts.value || !originals.value) return false
|
||||
const keys = [
|
||||
'module',
|
||||
'fields',
|
||||
'groups',
|
||||
'listConfig',
|
||||
'editConfig',
|
||||
'queryConfig',
|
||||
'autoCode',
|
||||
]
|
||||
return keys.some((key) => JSON.stringify(drafts.value[key]) !== JSON.stringify(originals.value[key]))
|
||||
const keys = ['module', 'fields', 'groups', 'listConfig', 'editConfig', 'queryConfig', 'autoCode']
|
||||
return keys.some(
|
||||
(key) => JSON.stringify(drafts.value[key]) !== JSON.stringify(originals.value[key]),
|
||||
)
|
||||
}
|
||||
|
||||
function onSwitchCancel() {
|
||||
@@ -827,7 +884,8 @@ function buildSyncPlan(module, columns) {
|
||||
for (const existing of drafts.value.fields || []) {
|
||||
if (!viewColumnSet.has(String(existing.b_field).toLowerCase())) {
|
||||
removedIds.add(String(existing.b_id))
|
||||
const key = existing.b_i18n || buildModuleItemI18nKey('field', module.b_code, existing.b_field)
|
||||
const key =
|
||||
existing.b_i18n || buildModuleItemI18nKey('field', module.b_code, existing.b_field)
|
||||
if (key) removedI18nIds.add(`field:${key}`)
|
||||
}
|
||||
}
|
||||
@@ -927,209 +985,224 @@ loadModuleTree()
|
||||
<SplitterPanel :default-size="248" :min="180" :max="420">
|
||||
<aside class="mm-tree-panel">
|
||||
<div class="mm-tree-header">
|
||||
<Input v-model="moduleKeyword" placeholder="搜索模块" allow-clear class="mm-tree-search">
|
||||
<Input
|
||||
v-model="moduleKeyword"
|
||||
placeholder="搜索模块"
|
||||
allow-clear
|
||||
class="mm-tree-search"
|
||||
>
|
||||
<template #prefix><Search :size="14" class="mm-tree-search__icon" /></template>
|
||||
</Input>
|
||||
<div class="mm-tree-header__actions">
|
||||
<Dropdown :items="moduleNewItems" @click="onModuleNewClick">
|
||||
<Button type="ghost" class="mm-tree-header__icon-btn" title="新建模块" aria-label="新建模块">
|
||||
<Button
|
||||
type="ghost"
|
||||
class="mm-tree-header__icon-btn"
|
||||
title="新建模块"
|
||||
aria-label="新建模块"
|
||||
>
|
||||
<Plus :size="14" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mm-tree-panel__scroll">
|
||||
<div v-if="moduleTreeLoading" class="mm-tree-empty">加载中...</div>
|
||||
<Tree
|
||||
v-else
|
||||
:tree-data="filteredModuleTree"
|
||||
:field-names="{ key: 'b_id', title: 'b_name', children: 'children' }"
|
||||
:expanded-keys="moduleExpandedKeys"
|
||||
@update:expanded-keys="(keys) => (moduleExpandedKeys = keys)"
|
||||
:selected-keys="moduleSelectedId ? [moduleSelectedId] : []"
|
||||
:context-menu="buildModuleContextMenu"
|
||||
:draggable="!moduleKeyword.trim()"
|
||||
@select="onModuleSelect"
|
||||
@context-menu-click="onModuleContextMenuClick"
|
||||
@drop="onModuleDrop"
|
||||
>
|
||||
<template #title="{ node }">
|
||||
<span class="mm-tree-node">
|
||||
<span
|
||||
class="mm-tree-node__name"
|
||||
:class="{ 'is-disabled': Number(node.b_canuse) !== 1 }"
|
||||
>
|
||||
{{ node.b_name || node.b_code }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</Tree>
|
||||
</div>
|
||||
<div class="mm-tree-panel__scroll">
|
||||
<div v-if="moduleTreeLoading" class="mm-tree-empty">加载中...</div>
|
||||
<Tree
|
||||
v-else
|
||||
:tree-data="filteredModuleTree"
|
||||
:field-names="{ key: 'b_id', title: 'b_name', children: 'children' }"
|
||||
:expanded-keys="moduleExpandedKeys"
|
||||
@update:expanded-keys="(keys) => (moduleExpandedKeys = keys)"
|
||||
:selected-keys="moduleSelectedId ? [moduleSelectedId] : []"
|
||||
:context-menu="buildModuleContextMenu"
|
||||
:draggable="!moduleKeyword.trim()"
|
||||
@select="onModuleSelect"
|
||||
@context-menu-click="onModuleContextMenuClick"
|
||||
@drop="onModuleDrop"
|
||||
>
|
||||
<template #title="{ node }">
|
||||
<span class="mm-tree-node">
|
||||
<span
|
||||
class="mm-tree-node__name"
|
||||
:class="{ 'is-disabled': Number(node.b_canuse) !== 1 }"
|
||||
>
|
||||
{{ node.b_name || node.b_code }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</Tree>
|
||||
</div>
|
||||
</aside>
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel>
|
||||
<main class="mm-main">
|
||||
<template v-if="selectedModule">
|
||||
<!-- 顶部通栏操作:保存 / 同步字段(全 tab 生效,不随内容区切换) -->
|
||||
<div class="mm-main-toolbar">
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="configSaving"
|
||||
:disabled="configLoading || !drafts"
|
||||
@click="saveModule"
|
||||
>
|
||||
<Save :size="14" /> 保存
|
||||
</Button>
|
||||
<Button
|
||||
v-if="isDataModule || isQueryModule"
|
||||
type="outline"
|
||||
:disabled="busy"
|
||||
title="按数据库视图结构对齐字段定义"
|
||||
@click="syncFields"
|
||||
>
|
||||
同步字段
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
v-if="drafts"
|
||||
v-model="activeSection"
|
||||
nav-only
|
||||
class="mm-config-nav"
|
||||
:items="configNavItems"
|
||||
aria-label="模块配置导航"
|
||||
>
|
||||
<template #label="{ item }">
|
||||
<component :is="item.icon" :size="14" class="mm-config-nav__icon" />
|
||||
<span class="mm-config-nav__label">{{ item.label }}</span>
|
||||
<span v-if="item.badge" class="mm-config-nav__badge">{{ item.badge }}</span>
|
||||
</template>
|
||||
</Tabs>
|
||||
|
||||
<div class="mm-body__inner">
|
||||
<div v-if="drafts" class="mm-config-content">
|
||||
<div v-show="activeSection === 'basic'" class="mm-config-pane">
|
||||
<ModuleBasicPanel
|
||||
ref="basicPanelRef"
|
||||
:data="drafts?.module"
|
||||
:parent-name="parentModuleName"
|
||||
:loading="busy"
|
||||
@update="(v) => onModuleUpdate('module', v)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
<main class="mm-main">
|
||||
<template v-if="selectedModule">
|
||||
<!-- 顶部通栏操作:保存 / 同步字段(全 tab 生效,不随内容区切换) -->
|
||||
<div class="mm-main-toolbar">
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="configSaving"
|
||||
:disabled="configLoading || !drafts"
|
||||
@click="saveModule"
|
||||
>
|
||||
<Save :size="14" /> 保存
|
||||
</Button>
|
||||
<Button
|
||||
v-if="isDataModule || isQueryModule"
|
||||
v-show="activeSection === 'fields'"
|
||||
class="mm-config-pane"
|
||||
type="outline"
|
||||
:disabled="busy"
|
||||
title="按数据库视图结构对齐字段定义"
|
||||
@click="syncFields"
|
||||
>
|
||||
<ModuleFieldsPanel
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:module-code="drafts?.module?.b_code || ''"
|
||||
:fields="drafts?.fields"
|
||||
:edit-config="drafts?.editConfig"
|
||||
:list-config="drafts?.listConfig"
|
||||
:query-config="drafts?.queryConfig"
|
||||
:module-type="moduleType"
|
||||
:loading="busy"
|
||||
@update="onFieldsUpdate"
|
||||
@update-edit-config="(v) => onModuleUpdate('editConfig', v)"
|
||||
@update-list-config="(v) => onModuleUpdate('listConfig', v)"
|
||||
@update-query-config="(v) => onModuleUpdate('queryConfig', v)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isDataModule || isQueryModule"
|
||||
v-show="activeSection === 'list'"
|
||||
class="mm-config-pane"
|
||||
>
|
||||
<ModuleListConfigPanel
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:list-config="drafts?.listConfig"
|
||||
:fields="drafts?.fields"
|
||||
:groups="drafts?.groups"
|
||||
:loading="busy"
|
||||
@update="(v) => onModuleUpdate('listConfig', v)"
|
||||
@update-groups="onGroupsUpdate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isDataModule" v-show="activeSection === 'edit'" class="mm-config-pane">
|
||||
<component
|
||||
:is="activeEditPanel"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:edit-config="drafts?.editConfig"
|
||||
:list-config="drafts?.listConfig"
|
||||
:fields="drafts?.fields"
|
||||
:groups="drafts?.groups"
|
||||
:edit-mode="editMode"
|
||||
:active="activeSection === 'edit'"
|
||||
:loading="busy"
|
||||
@update="(v) => onModuleUpdate('editConfig', v)"
|
||||
@update:edit-mode="
|
||||
(v) => onModuleUpdate('module', { ...(drafts?.module || {}), b_edit_mode: v })
|
||||
"
|
||||
@update-groups="onGroupsUpdate"
|
||||
@update-list-config="(v) => onModuleUpdate('listConfig', v)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isDataModule || isQueryModule"
|
||||
v-show="activeSection === 'query'"
|
||||
class="mm-config-pane"
|
||||
>
|
||||
<ModuleQueryConfigPanel
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:query-config="drafts?.queryConfig"
|
||||
:fields="drafts?.fields"
|
||||
:loading="busy"
|
||||
@update="(v) => onModuleUpdate('queryConfig', v)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isDataModule" v-show="activeSection === 'auto-code'" class="mm-config-pane">
|
||||
<ModuleAutoCodePanel
|
||||
ref="autoCodePanelRef"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:auto-code="drafts?.autoCode"
|
||||
:fields="drafts?.fields"
|
||||
:loading="busy"
|
||||
@update="onAutoCodeUpdate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="moduleType && moduleType !== 'category'"
|
||||
v-show="activeSection === 'i18n'"
|
||||
class="mm-config-pane"
|
||||
>
|
||||
<ModuleI18nPanel
|
||||
ref="i18nPanelRef"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:module="drafts?.module"
|
||||
:fields="drafts?.fields"
|
||||
:groups="drafts?.groups"
|
||||
:raw-rows="rawI18nRows"
|
||||
:loading="busy"
|
||||
/>
|
||||
</div>
|
||||
同步字段
|
||||
</Button>
|
||||
</div>
|
||||
<!-- 加载中/无数据占位:避免配置区一片空白 -->
|
||||
<div v-else-if="configLoading" class="mm-config-state">
|
||||
<span class="mm-config-state__spinner"></span>
|
||||
<span>加载中...</span>
|
||||
|
||||
<div class="mm-body__inner">
|
||||
<Tabs
|
||||
v-if="drafts"
|
||||
v-model="activeSection"
|
||||
nav-only
|
||||
class="mm-config-nav"
|
||||
:items="configNavItems"
|
||||
aria-label="模块配置导航"
|
||||
>
|
||||
<template #label="{ item }">
|
||||
<component :is="item.icon" :size="14" class="mm-config-nav__icon" />
|
||||
<span class="mm-config-nav__label">{{ item.label }}</span>
|
||||
<span v-if="item.badge" class="mm-config-nav__badge">{{ item.badge }}</span>
|
||||
</template>
|
||||
</Tabs>
|
||||
|
||||
<div v-if="drafts" class="mm-config-content">
|
||||
<div v-show="activeSection === 'basic'" class="mm-config-pane">
|
||||
<ModuleBasicPanel
|
||||
ref="basicPanelRef"
|
||||
:data="drafts?.module"
|
||||
:parent-name="parentModuleName"
|
||||
:loading="busy"
|
||||
@update="(v) => onModuleUpdate('module', v)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isDataModule || isQueryModule"
|
||||
v-show="activeSection === 'fields'"
|
||||
class="mm-config-pane"
|
||||
>
|
||||
<ModuleFieldsPanel
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:module-code="drafts?.module?.b_code || ''"
|
||||
:fields="drafts?.fields"
|
||||
:edit-config="drafts?.editConfig"
|
||||
:list-config="drafts?.listConfig"
|
||||
:query-config="drafts?.queryConfig"
|
||||
:module-type="moduleType"
|
||||
:loading="busy"
|
||||
@update="onFieldsUpdate"
|
||||
@update-edit-config="(v) => onModuleUpdate('editConfig', v)"
|
||||
@update-list-config="(v) => onModuleUpdate('listConfig', v)"
|
||||
@update-query-config="(v) => onModuleUpdate('queryConfig', v)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isDataModule || isQueryModule"
|
||||
v-show="activeSection === 'list'"
|
||||
class="mm-config-pane"
|
||||
>
|
||||
<ModuleListConfigPanel
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:list-config="drafts?.listConfig"
|
||||
:fields="drafts?.fields"
|
||||
:groups="drafts?.groups"
|
||||
:loading="busy"
|
||||
@update="(v) => onModuleUpdate('listConfig', v)"
|
||||
@update-groups="onGroupsUpdate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isDataModule" v-show="activeSection === 'edit'" class="mm-config-pane">
|
||||
<component
|
||||
:is="activeEditPanel"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:edit-config="drafts?.editConfig"
|
||||
:list-config="drafts?.listConfig"
|
||||
:fields="drafts?.fields"
|
||||
:groups="drafts?.groups"
|
||||
:edit-mode="editMode"
|
||||
:active="activeSection === 'edit'"
|
||||
:loading="busy"
|
||||
@update="(v) => onModuleUpdate('editConfig', v)"
|
||||
@update:edit-mode="
|
||||
(v) =>
|
||||
onModuleUpdate('module', { ...(drafts?.module || {}), b_edit_mode: v })
|
||||
"
|
||||
@update-groups="onGroupsUpdate"
|
||||
@update-list-config="(v) => onModuleUpdate('listConfig', v)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isDataModule || isQueryModule"
|
||||
v-show="activeSection === 'query'"
|
||||
class="mm-config-pane"
|
||||
>
|
||||
<ModuleQueryConfigPanel
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:query-config="drafts?.queryConfig"
|
||||
:fields="drafts?.fields"
|
||||
:loading="busy"
|
||||
@update="(v) => onModuleUpdate('queryConfig', v)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isDataModule"
|
||||
v-show="activeSection === 'auto-code'"
|
||||
class="mm-config-pane"
|
||||
>
|
||||
<ModuleAutoCodePanel
|
||||
ref="autoCodePanelRef"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:auto-code="drafts?.autoCode"
|
||||
:fields="drafts?.fields"
|
||||
:loading="busy"
|
||||
@update="onAutoCodeUpdate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="moduleType && moduleType !== 'category'"
|
||||
v-show="activeSection === 'i18n'"
|
||||
class="mm-config-pane"
|
||||
>
|
||||
<ModuleI18nPanel
|
||||
ref="i18nPanelRef"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:module="drafts?.module"
|
||||
:fields="drafts?.fields"
|
||||
:groups="drafts?.groups"
|
||||
:raw-rows="rawI18nRows"
|
||||
:loading="busy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 加载中/无数据占位:避免配置区一片空白 -->
|
||||
<div v-else-if="configLoading" class="mm-config-state">
|
||||
<span class="mm-config-state__spinner"></span>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<div v-else class="mm-config-state">暂无配置数据</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="mm-main__empty">
|
||||
<FolderTree :size="32" />
|
||||
<span>请选择左侧模块节点</span>
|
||||
</div>
|
||||
<div v-else class="mm-config-state">暂无配置数据</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="mm-main__empty">
|
||||
<FolderTree :size="32" />
|
||||
<span>请选择左侧模块节点</span>
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</div>
|
||||
@@ -1144,7 +1217,9 @@ loadModuleTree()
|
||||
>
|
||||
<p style="margin: 0; line-height: 1.7; color: var(--fms-text)">{{ confirmState?.content }}</p>
|
||||
<template #footer>
|
||||
<Button type="outline" :disabled="confirmSaving" @click="confirmVisible = false">取消</Button>
|
||||
<Button type="outline" :disabled="confirmSaving" @click="confirmVisible = false"
|
||||
>取消</Button
|
||||
>
|
||||
<Button
|
||||
:type="confirmState?.okType || 'primary'"
|
||||
:loading="confirmSaving"
|
||||
@@ -1320,10 +1395,7 @@ loadModuleTree()
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
/* 灰底画布上的白卡:与左侧树卡用灰缝分隔 */
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
box-shadow: var(--fms-shadow);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mm-main__empty {
|
||||
@@ -1335,6 +1407,13 @@ loadModuleTree()
|
||||
flex: 1;
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 13px;
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
box-shadow: var(--fms-shadow);
|
||||
|
||||
svg {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部通栏操作条:保存 / 同步字段(位于 tabs 之上,全 tab 生效) */
|
||||
@@ -1344,6 +1423,8 @@ loadModuleTree()
|
||||
gap: 8px;
|
||||
flex: none;
|
||||
padding: 8px 10px;
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
}
|
||||
|
||||
/* ---- 右侧:配置工作区 ---- */
|
||||
@@ -1352,16 +1433,21 @@ loadModuleTree()
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
box-shadow: var(--fms-shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ---- 配置导航:横向 Tab 导航(line 类型) ---- */
|
||||
/* ---- 配置导航:横向 Tab 导航(line 类型,使用默认 tabs 底边线 ---- */
|
||||
.mm-config-nav.tabs {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* 页签高度(42px):仅作用于本页 tab 项,不改变组件内部样式,也不影响按钮 */
|
||||
/* 页签高度:仅作用于本页 tab 项,不改变组件内部样式,也不影响按钮 */
|
||||
.mm-config-nav.tabs :deep(.tabs-tab) {
|
||||
height: 42px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
/* 标签内布局:图标 + 文字 + badge */
|
||||
|
||||
Reference in new issue
Block a user