20260826224445

This commit is contained in:
oneao committed 2026-08-26 22:44:45 +08:00
1 parent d750e04eee
commit 60cacfa8fb
9 files changed
+3650 -2124

No files matched your search

@@ -108,9 +108,15 @@ function toggleFullscreen() {
}
// 遮罩点击关闭:wrap 铺满视口且层级高于 mask,事件实际落在 wrap 上;
// 只响应点击 wrap 自身(面板外空白/遮罩区域),点击面板内部不关闭
// 只响应点击 wrap 自身(面板外空白/遮罩区域),点击面板内部不关闭。
// 记录 mousedown 是否发生在面板内:若在面板内按下(如拖选输入框文本)后
// 拖出到遮罩再松开,click.target 会回溯为共同祖先 wrap,此时不应误判为点击遮罩。
let maskDownInPanel = false
function onWrapMouseDown(event) {
maskDownInPanel = !!panelRef.value?.contains(event.target)
}
function onWrapClick(event) {
if (event.target === event.currentTarget && props.maskClosable) {
if (event.target === event.currentTarget && props.maskClosable && !maskDownInPanel) {
requestClose()
}
}
@@ -208,6 +214,7 @@ onBeforeUnmount(() => {
v-if="isOpen"
class="fms-modal-wrap"
:class="{ 'fms-modal-centered': centered }"
@mousedown="onWrapMouseDown"
@click="onWrapClick"
>
<div
@@ -26,10 +26,13 @@
box-shadow 0.15s ease;
}
/* hover:仅内容区域背景变化(箭头/缩进区不变),整行保持 pointer 提示可点 */
.tree-node:hover:not(.is-disabled) {
background: var(--fms-secondary-hover);
cursor: pointer;
}
.tree-node:hover:not(.is-disabled) .tree-node-content {
background: var(--fms-secondary-hover);
}
/* 键盘/鼠标聚焦:focus-visible 描边,避免鼠标点击也出现描边 */
.tree-node:focus-visible {
@@ -37,12 +40,12 @@
outline-offset: -2px;
}
/* 选中:主色淡底,明显区分于灰底页面;文字保持默认色。
/* 选中:仅内容区域主色淡底(整行 hover 背景、箭头/缩进区不受影响)。
比例取 5%:深色主题(如 #0f172b)在白底上约 #f3f3f4,接近 hover 浅色 #f1f5f9;
比例过高(10%)时深色主题会算出 #e7e7e9 等偏深灰,显得脏。
注意 hover 规则 specificity 更高,需连同 hover 一起覆盖,避免悬停时选中态被盖掉 */
.tree-node.is-selected,
.tree-node.is-selected:hover:not(.is-disabled) {
.tree-node.is-selected .tree-node-content,
.tree-node.is-selected:hover:not(.is-disabled) .tree-node-content {
background: color-mix(in srgb, var(--fms-primary) 5%, transparent);
}
@@ -78,14 +81,15 @@
}
.tree-node-switcher:hover:not(.is-placeholder) {
background: color-mix(in srgb, var(--fms-primary) 10%, transparent);
color: var(--fms-primary);
}
.tree-node-switcher.is-placeholder {
cursor: default;
}
/* 展开/收起箭头:黑色(跟随主题文字色),hover 仅显示淡底提示、箭头不变色 */
.tree-switcher-icon {
width: var(--fms-tree-icon-size);
height: var(--fms-tree-icon-size);
color: var(--fms-text);
transition: transform 0.2s cubic-bezier(0.2, 0.8, 0.4, 1);
}
.tree-switcher-icon.is-open {
@@ -115,9 +119,11 @@
.tree-node-content {
flex: 1;
min-width: 0;
/* 父容器 .tree-node 仅有 min-height(无显式 height),height:100% 无法解析,
故用 flex 交叉轴拉伸占满整行实际高度(选中背景/圆角才能覆盖整行) */
align-self: stretch;
display: flex;
align-items: center;
height: 100%;
padding: 0 var(--fms-tree-title-padding-x);
border-radius: var(--fms-control-radius);
overflow: hidden;
@@ -160,13 +166,8 @@
width: var(--fms-tree-grip-size);
height: var(--fms-tree-grip-size);
margin-right: 4px;
color: var(--fms-text-secondary);
color: #b0b7c3;
cursor: grab;
opacity: 0;
transition: opacity 0.15s;
}
.tree-node:hover .tree-node-grip {
opacity: 1;
}
.tree-node-grip:active {
cursor: grabbing;
@@ -1,471 +0,0 @@
<script setup>
/**
* 字段分组管理弹窗:管理 s_field_group 树形分组。
* 简化交互:顶部输入框回车即新增(选中节点则挂到其下,未选中为顶级),
* 树节点行内直接 启用 / 重命名 / 删除,无需右侧属性面板。
* 独立保存到后端,保存后 emit("saved", groups, idMapping),父级用 idMapping
* 重映射列表/编辑配置中的 b_group_id(临时 ID → 雪花 ID)。
*/
import { computed, nextTick, ref, watch } from 'vue'
import Message from '@/components/ui/message/message-manager'
import Modal from '@/components/ui/modal/modal.vue'
import Button from '@/components/ui/button/button.vue'
import Switch from '@/components/ui/switch/switch.vue'
import Tree from '@/components/ui/tree/tree.vue'
import { Plus, Pencil, Trash2, ArrowUp, ArrowDown } from '@lucide/vue'
import { buildTree } from '@/utils/tree'
import { createTempId } from '@/utils/tempId'
import { cloneData, diffRows, allocateTemporaryIds, tableChange, hasChanges } from '@/utils/dataChanges'
import { saveObjectApi } from '@/services/api'
const props = defineProps({
open: { type: Boolean, default: false },
moduleId: { type: String, required: true },
groups: { type: Array, default: () => [] },
referencedGroupIds: { type: Array, default: () => [] },
})
const emit = defineEmits(['update:open', 'saved'])
const localGroups = ref([])
const selectedGroupId = ref(null)
const saving = ref(false)
/* 顶部输入即新增 */
const newTitle = ref('')
const addInputRef = ref(null)
/* 行内重命名 */
const editingId = ref(null)
const renameValue = ref('')
const renameInputRef = ref(null)
watch(
() => props.groups,
(val) => {
localGroups.value = cloneData(val || [])
},
{ immediate: true },
)
const treeData = computed(() => buildTree(localGroups.value))
const selectedGroup = computed(() =>
localGroups.value.find((g) => String(g.b_id) === String(selectedGroupId.value)),
)
function onTreeSelect(keys) {
selectedGroupId.value = keys.length ? String(keys[0]) : null
}
/* 新增:输入名称回车即添加;选中节点时挂到其下,否则为顶级。
新增后不自动选中新节点,便于连续新增到同一父级下。 */
function handleAdd() {
const title = newTitle.value.trim()
if (!title) return
localGroups.value.push({
b_id: createTempId(),
b_module_id: props.moduleId,
b_parent_id: selectedGroupId.value || null,
b_title: title,
b_xh: (localGroups.value.length + 1) * 10,
b_canuse: 1,
b_layout_direction: 'vertical',
})
newTitle.value = ''
nextTick(() => addInputRef.value?.focus?.())
}
function onAddEnter(event) {
if (event.isComposing) return
handleAdd()
}
/* 行内重命名 */
function startRename(node) {
editingId.value = String(node.b_id)
renameValue.value = node.b_title || ''
nextTick(() => renameInputRef.value?.focus?.())
}
function commitRename(node) {
if (editingId.value !== String(node.b_id)) return
const title = renameValue.value.trim()
if (title) node.b_title = title
editingId.value = null
}
function cancelRename() {
editingId.value = null
}
function onRenameEnter(event, node) {
if (event.isComposing) return
commitRename(node)
}
/* 行内启用切换 */
function toggleEnable(node) {
node.b_canuse = Number(node.b_canuse) === 1 ? 0 : 1
}
function toggleLayout(node) {
node.b_layout_direction = node.b_layout_direction === 'horizontal' ? 'vertical' : 'horizontal'
}
/* 同级排序:上移/下移(交换同级顺序并重新编号 b_xh,随保存提交) */
function moveGroup(node, delta) {
if (!node) return
const parentId = node.b_parent_id == null ? null : String(node.b_parent_id)
const siblings = localGroups.value.filter(
(g) => (g.b_parent_id == null ? null : String(g.b_parent_id)) === parentId,
)
siblings.sort((a, b) => (a.b_xh || 0) - (b.b_xh || 0))
const idx = siblings.findIndex((g) => String(g.b_id) === String(node.b_id))
const target = idx + delta
if (idx < 0 || target < 0 || target >= siblings.length) return
const [moved] = siblings.splice(idx, 1)
siblings.splice(target, 0, moved)
siblings.forEach((g, i) => {
g.b_xh = (i + 1) * 10
})
}
/* 删除:有子分组 / 仍被引用时禁止 */
function deleteGroup(node) {
if (!node) return
const id = String(node.b_id)
const hasChildren = localGroups.value.some(
(g) => String(g.b_parent_id) === id && String(g.b_id) !== id,
)
if (hasChildren) {
Message.warning('该分组下还有子分组,请先删除子分组')
return
}
if (props.referencedGroupIds.some((gid) => String(gid) === id)) {
Message.warning('该分组仍被列表或编辑配置引用,请先清空引用')
return
}
localGroups.value = localGroups.value.filter((g) => String(g.b_id) !== id)
if (String(selectedGroupId.value) === id) selectedGroupId.value = null
}
async function save() {
if (saving.value) return
const changes = diffRows(localGroups.value, cloneData(props.groups || []))
const req = tableChange('s_field_group', changes)
if (!hasChanges(req)) {
Message.info('没有需要保存的变更')
emit('update:open', false)
emit('saved', localGroups.value, new Map())
return
}
saving.value = true
try {
const mapping = await allocateTemporaryIds([req.inserts || [], req.updates || []])
await saveObjectApi([req])
Message.success('分组已保存')
emit('update:open', false)
emit('saved', localGroups.value, mapping)
} catch (err) {
Message.error('保存失败:' + (err?.message || err))
} finally {
saving.value = false
}
}
</script>
<template>
<Modal
:open="open"
title="分组管理"
width="520"
ok-text="保存"
:confirm-loading="saving"
:resizable="false"
@update:open="(v) => emit('update:open', v)"
@ok="save"
>
<div class="mm-groups">
<div class="mm-groups__head">
<div class="mm-groups__head-title">分组结构</div>
<span class="mm-groups__head-count">共 {{ localGroups.length }} 个分组</span>
</div>
<div class="mm-groups__add">
<input
ref="addInputRef"
class="mm-groups__input"
:value="newTitle"
placeholder="输入分组名称,回车或点「新增」"
@input="(e) => (newTitle = e.target.value)"
@keydown.enter="onAddEnter"
@keydown.esc="newTitle = ''"
/>
<Button type="primary" @click="handleAdd">
<Plus :size="14" /> 新增
</Button>
</div>
<p class="mm-groups__hint">
{{ selectedGroup ? `将添加到「${selectedGroup.b_title || '未命名分组'}」下` : '将添加为顶级分组' }}
</p>
<div class="mm-groups__scroll">
<Tree
:tree-data="treeData"
:field-names="{ key: 'b_id', title: 'b_title', children: 'children' }"
default-expand-all
:selected-keys="selectedGroupId ? [selectedGroupId] : []"
@select="onTreeSelect"
>
<template #empty>暂无分组,输入名称后回车即可创建</template>
<template #title="{ node }">
<span class="mm-groups__node">
<input
v-if="editingId === String(node.b_id)"
ref="renameInputRef"
class="mm-groups__rename"
:value="renameValue"
@click.stop
@input="(e) => (renameValue = e.target.value)"
@keydown.enter.stop.prevent="(e) => onRenameEnter(e, node)"
@keydown.esc.stop.prevent="cancelRename"
@blur="commitRename(node)"
/>
<template v-else>
<span
class="mm-groups__node-name"
:class="{ 'is-disabled': Number(node.b_canuse) !== 1 }"
>
{{ node.b_title || '未命名分组' }}
</span>
<span class="mm-groups__node-actions" @click.stop>
<button
type="button"
class="mm-groups__node-btn"
title="上移"
aria-label="上移分组"
@click="moveGroup(node, -1)"
>
<ArrowUp :size="13" />
</button>
<button
type="button"
class="mm-groups__node-btn"
title="下移"
aria-label="下移分组"
@click="moveGroup(node, 1)"
>
<ArrowDown :size="13" />
</button>
<span class="mm-groups__node-divider"></span>
<Switch
:model-value="Number(node.b_canuse) === 1"
title="启用/停用"
aria-label="启用/停用"
@change="() => toggleEnable(node)"
/>
<button
type="button"
class="mm-groups__node-btn"
title="重命名"
aria-label="重命名"
@click="startRename(node)"
>
<Pencil :size="13" />
</button>
<button
type="button"
class="mm-groups__node-btn"
:title="node.b_layout_direction === 'horizontal' ? '切换为上下布局' : '切换为左右布局'"
:aria-label="node.b_layout_direction === 'horizontal' ? '切换为上下布局' : '切换为左右布局'"
@click="toggleLayout(node)"
>
{{ node.b_layout_direction === 'horizontal' ? '左右' : '上下' }}
</button>
<button
type="button"
class="mm-groups__node-btn is-danger"
title="删除分组"
aria-label="删除分组"
@click="deleteGroup(node)"
>
<Trash2 :size="13" />
</button>
</span>
</template>
</span>
</template>
</Tree>
</div>
</div>
</Modal>
</template>
<style lang="scss" scoped>
.mm-groups {
display: flex;
flex-direction: column;
gap: 8px;
height: 400px;
min-height: 0;
}
/* ---- 顶部摘要 ---- */
.mm-groups__head {
display: flex;
align-items: center;
justify-content: space-between;
flex: none;
padding: 2px 2px 0;
}
.mm-groups__head-title {
font-size: 13px;
font-weight: 600;
color: var(--fms-text);
}
.mm-groups__head-count {
font-size: 12px;
color: var(--fms-text-secondary);
}
/* ---- 顶部输入即新增 ---- */
.mm-groups__add {
display: flex;
gap: 6px;
flex: none;
}
.mm-groups__input {
flex: 1;
min-width: 0;
height: var(--fms-control-height);
padding: 0 10px;
border: 1px solid var(--fms-border);
border-radius: var(--fms-control-radius);
background: var(--fms-card);
color: var(--fms-text);
font-size: 13px;
font-family: inherit;
outline: none;
&:focus {
border-color: var(--fms-primary);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--fms-primary) 12%, transparent);
}
&::placeholder {
color: var(--fms-text-placeholder, #9ca3af);
}
}
.mm-groups__hint {
flex: none;
margin: 0;
color: var(--fms-text-secondary);
font-size: 12px;
}
/* ---- 树 ---- */
.mm-groups__scroll {
flex: 1;
min-height: 0;
overflow: auto;
padding: 6px;
border: 1px solid var(--fms-border);
border-radius: var(--fms-control-radius);
background: var(--fms-secondary-hover);
}
/* 节点行:名称撑满,操作靠右;hover 时显示完整操作 */
.mm-groups__node {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
min-width: 0;
padding: 3px 6px;
border-radius: 6px;
transition: background 0.15s;
&:hover {
background: var(--fms-card);
}
}
.mm-groups__node-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
&.is-disabled {
color: var(--fms-text-disabled, #9ca3af);
}
}
.mm-groups__node-actions {
display: inline-flex;
align-items: center;
gap: 2px;
flex: none;
}
/* 移动按钮与其它操作的分隔线 */
.mm-groups__node-divider {
width: 1px;
height: 14px;
margin: 0 2px;
background: var(--fms-border);
}
.mm-groups__node-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
padding: 0;
border: 1px solid transparent;
border-radius: 5px;
background: transparent;
color: var(--fms-text-secondary);
cursor: pointer;
transition:
background 0.15s,
color 0.15s,
border-color 0.15s;
&:hover {
background: var(--fms-secondary);
color: var(--fms-text);
border-color: var(--fms-border);
}
&.is-danger:hover {
background: #fdecec;
color: #d95757;
border-color: #f5c6c6;
}
}
/* 行内重命名输入框 */
.mm-groups__rename {
width: 100%;
height: 24px;
padding: 0 6px;
border: 1px solid var(--fms-primary);
border-radius: 4px;
background: var(--fms-card);
color: var(--fms-text);
font-size: 12px;
font-family: inherit;
outline: none;
&:focus {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--fms-primary) 12%, transparent);
}
}
</style>
@@ -23,7 +23,7 @@ const ICON_NAMES = [
'CalendarDays', 'CalendarClock', 'Clock', 'Timer', 'History', 'AlarmClock',
'Search', 'Filter', 'Eye', 'EyeOff', 'Lock', 'Unlock', 'Key', 'KeyRound',
'Plus', 'Minus', 'Pencil', 'Trash2', 'Copy', 'Save', 'Upload', 'Download',
'RefreshCw', 'RotateCw', 'Undo2', 'Redo2', 'ExternalLink', 'Link', 'Share2',
'RefreshCw', 'RotateCw', 'ExternalLink', 'Link', 'Share2',
'Printer', 'Mail', 'Phone', 'MapPin', 'Globe', 'Globe2', 'Languages', 'Translate',
'BookOpen', 'BookMarked', 'GraduationCap', 'Award', 'Star', 'Heart', 'Flag',
'Check', 'CheckCircle2', 'XCircle', 'AlertCircle', 'AlertTriangle', 'Info',
@@ -6,7 +6,6 @@ import { computed, ref, shallowRef, watch } from 'vue'
import Message from '@/components/ui/message/message-manager'
import Button from '@/components/ui/button/button.vue'
import FmsTable from '@/components/fms-table/FmsTable.vue'
import FieldGroupManager from './FieldGroupManager.vue'
const props = defineProps({
moduleId: { type: String, required: true },
@@ -15,14 +14,12 @@ const props = defineProps({
queryConfig: { type: Array, default: () => [] },
fields: { type: Array, default: () => [] },
groups: { type: Array, default: () => [] },
referencedGroupIds: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
})
const emit = defineEmits(['update', 'groupsSaved', 'syncEdit', 'syncQuery'])
const emit = defineEmits(['update', 'syncEdit', 'syncQuery'])
const localList = ref([])
const groupManagerVisible = ref(false)
const activeKey = ref(null)
watch(
@@ -139,9 +136,6 @@ const columns = shallowRef([
<template>
<div class="mm-panel">
<div class="mm-panel__toolbar">
<Button type="outline" :disabled="loading" @click="groupManagerVisible = true">
分组管理
</Button>
<Button type="outline" :disabled="loading || !editConfig.length" @click="syncToEdit">
同步到编辑显示
</Button>
@@ -162,13 +156,6 @@ const columns = shallowRef([
@row-order-change="onRowOrderChange"
/>
</div>
<FieldGroupManager
v-model:open="groupManagerVisible"
:module-id="moduleId"
:groups="groups"
:referenced-group-ids="referencedGroupIds"
@saved="(groups, idMapping) => emit('groupsSaved', groups, idMapping)"
/>
</div>
</template>
@@ -48,7 +48,8 @@ import { buildModuleI18nKey, buildModuleItemI18nKey, syncModuleItemI18nKeys } fr
import ModuleBasicPanel from './components/ModuleBasicPanel.vue'
import ModuleFieldsPanel from './components/ModuleFieldsPanel.vue'
import ModuleListConfigPanel from './components/ModuleListConfigPanel.vue'
import ModuleEditConfigPanel from './components/ModuleEditConfigPanel.vue'
import ModuleFormEditPanel from './components/ModuleFormEditPanel.vue'
import ModuleTableEditPanel from './components/ModuleTableEditPanel.vue'
import ModuleQueryConfigPanel from './components/ModuleQueryConfigPanel.vue'
import ModuleAutoCodePanel from './components/ModuleAutoCodePanel.vue'
import ModulePowersPanel from './components/ModulePowersPanel.vue'
@@ -629,16 +630,6 @@ function onAutoCodeUpdate(rows) {
}
}
function onGroupsSaved(newGroups, idMapping) {
if (!drafts.value) return
if (idMapping && idMapping.size) {
remapForeignKeys(drafts.value.listConfig || [], ['b_group_id'], idMapping)
remapForeignKeys(drafts.value.editConfig || [], ['b_group_id'], idMapping)
}
drafts.value.groups = cloneData(newGroups)
originals.value.groups = cloneData(newGroups)
}
function onModuleI18nSaved() {
i18nPanelRef.value?.refreshKey?.()
}
@@ -679,7 +670,7 @@ function onModuleNewClick({ key }) {
/* ================= 右侧分组配置导航 ================= */
// 配置导航分组:概览 / 数据结构 / 界面配置 / 业务能力 / 高级设置。
// 每个导航项唯一激活(activeSection);「分组管理」点击时同时打开分组管理弹窗。
// 每个导航项唯一激活(activeSection)。
const configGroups = computed(() => {
const groups = []
groups.push({
@@ -753,17 +744,6 @@ const configNavItems = computed(() =>
configGroups.value.flatMap((group) => group.items.map((item) => ({ ...item }))),
)
const referencedGroupIds = computed(() => {
const ids = new Set()
for (const row of drafts.value?.listConfig || []) {
if (row.b_group_id != null) ids.add(String(row.b_group_id))
}
for (const row of drafts.value?.editConfig || []) {
if (row.b_group_id != null) ids.add(String(row.b_group_id))
}
return Array.from(ids)
})
// 编辑方式(form 主表表单 / table 子表表格)决定「表单设计」面板的形态与导航标签
const editMode = computed(() => drafts.value?.module?.b_edit_mode || 'form')
const editNavLabel = computed(() => (editMode.value === 'table' ? '表格设计' : '表单设计'))
@@ -915,6 +895,9 @@ async function saveModule() {
['s_autocode', 'autoCode'],
['s_power', 'powers'],
]
// 收集字段/分组的临时 id → 真实 id 映射,用于跨表外键重映射
const fieldIdMap = new Map()
const groupIdMap = new Map()
for (const [table, section] of subTables) {
const current = drafts.value[section] || []
const previous = originals.value?.[section] || []
@@ -927,11 +910,25 @@ async function saveModule() {
}
const req = tableChange(table, changes)
if (hasChanges(req)) {
await allocateTemporaryIds([req.inserts || [], req.updates || []])
const mapping = await allocateTemporaryIds([req.inserts || [], req.updates || []])
if (table === 's_field') mapping.forEach((v, k) => fieldIdMap.set(k, v))
if (table === 's_field_group') mapping.forEach((v, k) => groupIdMap.set(k, v))
saveReqs.push(req)
}
}
// 统一重映射跨表外键:列表/编辑/查询配置中引用字段、分组的临时 id 换成真实 id
if (fieldIdMap.size) {
for (const section of ['listConfig', 'editConfig', 'queryConfig']) {
remapForeignKeys(drafts.value[section] || [], ['b_field_id'], fieldIdMap)
}
}
if (groupIdMap.size) {
for (const section of ['listConfig', 'editConfig', 'queryConfig']) {
remapForeignKeys(drafts.value[section] || [], ['b_group_id'], groupIdMap)
}
}
const i18nPanel = i18nPanelRef.value
if (i18nPanel && typeof i18nPanel.reconcileManagedRows === 'function') {
i18nPanel.reconcileManagedRows(finalI18nItems.value)
@@ -1098,24 +1095,41 @@ loadModuleTree()
:query-config="drafts?.queryConfig"
:fields="drafts?.fields"
:groups="drafts?.groups"
:referenced-group-ids="referencedGroupIds"
:loading="busy"
@update="(v) => onModuleUpdate('listConfig', v)"
@groups-saved="onGroupsSaved"
@sync-edit="(v) => onModuleUpdate('editConfig', v)"
@sync-query="(v) => onModuleUpdate('queryConfig', v)"
/>
</div>
<div v-if="isDataModule" v-show="activeSection === 'edit'" class="mm-config-pane">
<ModuleEditConfigPanel
<ModuleTableEditPanel
v-if="editMode === 'table'"
:module-id="String(selectedModule.b_id)"
:edit-config="drafts?.editConfig"
:list-config="drafts?.listConfig"
:query-config="drafts?.queryConfig"
: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="(v) => onModuleUpdate('groups', v)"
@sync-list="(v) => onModuleUpdate('listConfig', v)"
@sync-query="(v) => onModuleUpdate('queryConfig', v)"
/>
<ModuleFormEditPanel
v-else
:module-id="String(selectedModule.b_id)"
:edit-config="drafts?.editConfig"
:list-config="drafts?.listConfig"
:query-config="drafts?.queryConfig"
:fields="drafts?.fields"
:groups="drafts?.groups"
:referenced-group-ids="referencedGroupIds"
:edit-mode="editMode"
:active="activeSection === 'edit'"
:loading="busy"
@@ -1124,7 +1138,6 @@ loadModuleTree()
(v) => onModuleUpdate('module', { ...(drafts?.module || {}), b_edit_mode: v })
"
@update-groups="(v) => onModuleUpdate('groups', v)"
@groups-saved="onGroupsSaved"
@sync-list="(v) => onModuleUpdate('listConfig', v)"
@sync-query="(v) => onModuleUpdate('queryConfig', v)"
/>
@@ -1419,11 +1432,16 @@ loadModuleTree()
min-height: 0;
}
/* ---- 配置导航:横向 Tab 导航(line 类型,不作内部覆盖,组件默认样式即可) ---- */
/* ---- 配置导航:横向 Tab 导航(line 类型) ---- */
.mm-config-nav.tabs {
flex: none;
}
/* 页签高度(42px):仅作用于本页 tab 项,不改变组件内部样式,也不影响按钮 */
.mm-config-nav.tabs :deep(.tabs-tab) {
height: 42px;
}
/* 标签内布局:图标 + 文字 + badge */
.mm-config-nav__icon {
flex: none;