20260824172723

This commit is contained in:
oneao committed 2026-08-24 17:27:24 +08:00
1 parent 791d1e01d7
commit 97aea665f9
10 files changed
+443 -723

No files matched your search

@@ -59,7 +59,6 @@ import ModuleAutoCodePanel from './components/ModuleAutoCodePanel.vue'
import ModulePowersPanel from './components/ModulePowersPanel.vue'
import ModuleI18nPanel from './components/ModuleI18nPanel.vue'
import MenuDetailPanel from './components/MenuDetailPanel.vue'
import FieldGroupManager from './components/FieldGroupManager.vue'
/* ================= 顶部切换(Tabs 默认样式) ================= */
const activeZone = ref('module')
@@ -324,7 +323,9 @@ watch(moduleSelectedId, (id) => {
drafts.value = null
return
}
// 清空旧草稿,让右侧进入加载态(否则旧数据残留,v-if="drafts" 仍为真,看不到 loading)
loadedModuleId.value = null
drafts.value = null
void loadModuleConfig(id)
})
@@ -705,10 +706,7 @@ const configGroups = computed(() => {
groups.push({
key: 'structure',
label: '数据结构',
items: [
{ key: 'fields', label: '字段定义', icon: Table2 },
{ key: 'groups', label: '分组管理', icon: FolderTree },
],
items: [{ key: 'fields', label: '字段定义', icon: Table2 }],
})
// 界面配置:表单设计(数据模块)→ 列表配置 → 查询配置
@@ -764,18 +762,10 @@ const configGroups = computed(() => {
return groups
})
// 分组管理弹窗(导航「分组管理」项触发,保存后与列表/编辑面板共享同一分组草稿)
const groupManagerVisible = ref(false)
function onNavItemClick(item) {
activeSection.value = item.key
groupManagerVisible.value = item.key === 'groups'
}
// 离开「分组管理」分区时自动关闭弹窗,避免悬挂
watch(activeSection, (key) => {
if (key !== 'groups') groupManagerVisible.value = false
})
/* 横向胶囊 Tabs 的数据源:展平 configGroups 的分组为平铺 items */
const configNavItems = computed(() =>
configGroups.value.flatMap((group) => group.items.map((item) => ({ ...item }))),
)
const referencedGroupIds = computed(() => {
const ids = new Set()
@@ -1337,6 +1327,11 @@ const menuConfigGroups = computed(() => {
return groups
})
/* 菜单横向胶囊 Tabs 的数据源 */
const menuConfigNavItems = computed(() =>
menuConfigGroups.value.flatMap((group) => group.items.map((item) => ({ ...item }))),
)
// 重载当前菜单:若有未保存修改先确认放弃
function reloadMenu() {
if (!selectedMenu.value || menuSaving.value) return
@@ -1432,55 +1427,40 @@ loadMenuTree()
</aside>
</SplitterPanel>
<!-- 配置导航:独立中栏 -->
<SplitterPanel :default-size="158" :min="140" :max="220">
<nav v-if="drafts" class="mm-config-nav" aria-label="模块配置导航">
<template v-for="group in configGroups" :key="group.key">
<div class="mm-config-nav__group">
<div class="mm-config-nav__group-title">{{ group.label }}</div>
<button
v-for="item in group.items"
:key="item.key"
type="button"
class="mm-config-nav__item"
:class="{ 'is-active': activeSection === item.key }"
@click="onNavItemClick(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>
</button>
</div>
</template>
</nav>
<div v-else class="mm-config-nav-empty">请选择左侧模块节点</div>
</SplitterPanel>
<SplitterPanel>
<main class="mm-main">
<template v-if="selectedModule">
<div class="mm-header">
<div class="mm-header__info">
<component :is="moduleTypeIcon" :size="16" class="mm-header__icon" />
<span class="mm-header__name">{{ selectedModule.b_name || selectedModule.b_code }}</span>
<span class="mm-header__type">{{ moduleTypeLabel }}</span>
<span v-if="moduleBreadcrumb.length > 1" class="mm-header__crumb">
{{ moduleBreadcrumb.slice(0, -1).join(' / ') }}
</span>
<span v-if="unsavedModuleIds.has(String(selectedModule.b_id))" class="mm-header__unsaved">
未保存
</span>
</div>
<div class="mm-header__actions">
<span class="mm-header__hint">Ctrl + S 保存</span>
<Button type="outline" :disabled="busy || !selectedModule" @click="reloadModule">
<RefreshCw :size="14" /> 重载
</Button>
<Button type="primary" :loading="configSaving" :disabled="configLoading" @click="saveModule">
<Save :size="14" /> 保存
</Button>
</div>
</div>
<Tabs
v-if="drafts"
v-model="activeSection"
type="pill"
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>
<template #rightExtra>
<div class="mm-header__actions">
<span class="mm-header__hint">Ctrl + S 保存</span>
<Button type="outline" :disabled="busy || !selectedModule" @click="reloadModule">
<RefreshCw :size="14" /> 重载
</Button>
<Button
type="primary"
:loading="configSaving"
:disabled="configLoading"
@click="saveModule"
>
<Save :size="14" /> 保存
</Button>
</div>
</template>
</Tabs>
<div class="mm-body__inner">
<div v-if="drafts" class="mm-config-content">
@@ -1514,23 +1494,6 @@ loadMenuTree()
/>
</div>
<div
v-if="isDataModule || isQueryModule"
v-show="activeSection === 'groups'"
class="mm-config-pane"
>
<div class="mm-groups-pane">
<FolderTree :size="22" class="mm-groups-pane__icon" />
<p class="mm-groups-pane__text">
字段分组用于组织编辑表单与列表的展示结构,当前共
{{ drafts?.groups?.length ?? 0 }} 个分组。分组的新增、排序与启停通过分组管理弹窗维护,保存后即时生效。
</p>
<Button type="outline" :disabled="busy" @click="groupManagerVisible = true">
打开分组管理
</Button>
</div>
</div>
<div
v-if="isDataModule || isQueryModule"
v-show="activeSection === 'list'"
@@ -1555,7 +1518,6 @@ loadMenuTree()
<div v-if="isDataModule" v-show="activeSection === 'edit'" class="mm-config-pane">
<ModuleEditConfigPanel
:module-id="String(selectedModule.b_id)"
:module-name="selectedModule.b_name || selectedModule.b_code"
:edit-config="drafts?.editConfig"
:list-config="drafts?.listConfig"
:query-config="drafts?.queryConfig"
@@ -1640,14 +1602,6 @@ loadMenuTree()
</main>
</SplitterPanel>
</Splitter>
<!-- 分组管理弹窗:导航「分组管理」项触发;与列表/编辑面板共用同一份分组草稿 -->
<FieldGroupManager
v-model:open="groupManagerVisible"
:module-id="String(selectedModule?.b_id || '')"
:groups="drafts?.groups"
:referenced-group-ids="referencedGroupIds"
@saved="onGroupsSaved"
/>
</div>
<!-- ================ 菜单配置 ================ -->
@@ -1704,59 +1658,35 @@ loadMenuTree()
</aside>
</SplitterPanel>
<!-- 配置导航:独立中栏(与模块配置统一三栏结构) -->
<SplitterPanel :default-size="158" :min="140" :max="220">
<nav
v-if="activeZone === 'menu' && selectedMenu"
class="mm-config-nav"
aria-label="菜单配置导航"
>
<template v-for="group in menuConfigGroups" :key="group.key">
<div class="mm-config-nav__group">
<div class="mm-config-nav__group-title">{{ group.label }}</div>
<button
v-for="item in group.items"
:key="item.key"
type="button"
class="mm-config-nav__item"
:class="{ 'is-active': menuActiveSection === item.key }"
@click="menuActiveSection = item.key"
>
<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>
</button>
</div>
</template>
</nav>
<div v-else-if="activeZone === 'menu'" class="mm-config-nav-empty">请选择左侧菜单节点</div>
</SplitterPanel>
<SplitterPanel>
<main class="mm-main">
<template v-if="selectedMenu">
<div class="mm-header">
<div class="mm-header__info">
<component :is="menuTypeIcon" :size="16" class="mm-header__icon" />
<span class="mm-header__name">{{ selectedMenu.b_name || selectedMenu.b_code }}</span>
<span class="mm-header__type">{{ menuTypeLabel(selectedMenu.b_menu_type) }}</span>
<span v-if="menuBreadcrumb.length > 1" class="mm-header__crumb">
{{ menuBreadcrumb.slice(0, -1).join(' / ') }}
</span>
<span v-if="unsavedMenuIds.has(String(selectedMenu.b_id))" class="mm-header__unsaved">
未保存
</span>
</div>
<div class="mm-header__actions">
<span class="mm-header__hint">Ctrl + S 保存</span>
<Button type="outline" :disabled="menuSaving" @click="reloadMenu">
<RefreshCw :size="14" /> 重载
</Button>
<Button type="primary" :loading="menuSaving" @click="saveMenu">
<Save :size="14" /> 保存
</Button>
</div>
</div>
<Tabs
v-if="menuDrafts"
v-model="menuActiveSection"
type="pill"
nav-only
class="mm-config-nav"
:items="menuConfigNavItems"
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>
<template #rightExtra>
<div class="mm-header__actions">
<span class="mm-header__hint">Ctrl + S 保存</span>
<Button type="outline" :disabled="menuSaving" @click="reloadMenu">
<RefreshCw :size="14" /> 重载
</Button>
<Button type="primary" :loading="menuSaving" @click="saveMenu">
<Save :size="14" /> 保存
</Button>
</div>
</template>
</Tabs>
<div class="mm-body__inner">
<MenuDetailPanel
@@ -1856,8 +1786,8 @@ loadMenuTree()
}
/* ---- 左树顶部 header:标题在左,新增/刷新图标在右。
与右侧 .mm-header 同规格:padding 10px + 内容 28px(图标按钮与 Button 等高)
= 48px,两侧 header 的底边框在同一水平线对齐。
与右侧配置导航同规格:padding 6px + 内容 28px(图标按钮与 Button 等高)
= 40px,两侧 header 的底边框在同一水平线对齐。
负 margin 抵消面板 8px padding,使 border-bottom 横跨面板全宽。 */
.mm-tree-header {
display: flex;
@@ -1865,7 +1795,7 @@ loadMenuTree()
justify-content: space-between;
gap: 6px;
margin: -8px -8px 0;
padding: 10px;
padding: 6px;
border-bottom: 1px solid var(--fms-border);
}
@@ -1998,72 +1928,7 @@ loadMenuTree()
font-size: 13px;
}
.mm-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex: none;
padding: 10px;
border-bottom: 1px solid var(--fms-border);
}
.mm-header__info {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.mm-header__icon {
flex: none;
color: var(--fms-text-secondary);
}
.mm-header__crumb {
max-width: 240px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
color: var(--fms-text-secondary);
&::before {
content: '‹ ';
opacity: 0.6;
}
}
.mm-header__name {
font-size: 15px;
font-weight: 600;
color: var(--fms-text);
line-height: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mm-header__type {
flex: none;
padding: 1px 8px;
border-radius: 999px;
font-size: 12px;
line-height: 18px;
background: var(--fms-secondary);
color: var(--fms-text-secondary);
}
.mm-header__unsaved {
flex: none;
padding: 1px 8px;
border-radius: 999px;
font-size: 12px;
line-height: 18px;
background: #fef3c7;
color: #b45309;
}
/* 操作按钮组(Tabs 的 rightExtra 插槽内) */
.mm-header__actions {
display: flex;
align-items: center;
@@ -2086,94 +1951,47 @@ loadMenuTree()
min-height: 0;
}
/* 中栏配置导航:独立 Splitter 面板内撑满,右侧竖线分隔工作区 */
.mm-config-nav {
display: flex;
flex-direction: column;
gap: 2px;
width: 100%;
height: 100%;
min-height: 0;
padding: 10px 8px;
overflow-y: auto;
border-right: 1px solid var(--fms-border);
background: var(--fms-card);
/* ---- 配置导航:横向胶囊式 Tabs,位于 header 内左侧,操作按钮贴右 ---- */
/* ---- 配置导航:横向胶囊式 Tabs,位于工作区顶部,操作按钮在 rightExtra 插槽 ---- */
.mm-config-nav.tabs {
flex: none;
/* 与左侧树 header 的分割线对齐:左右两栏 header 同高同线,视觉统一 */
border-bottom: 1px solid var(--fms-border);
}
.mm-config-nav-empty {
display: flex;
.mm-config-nav :deep(.tabs-nav) {
padding: 6px;
}
/* 标签列表由 Scrollbar 组件接管滚动,这里只保持垂直居中 */
.mm-config-nav :deep(.tabs-nav-list) {
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 16px;
border-right: 1px solid var(--fms-border);
color: var(--fms-text-secondary);
font-size: 12px;
text-align: center;
}
.mm-config-nav__group {
margin-bottom: 12px;
}
.mm-config-nav__group-title {
padding: 2px 8px 4px;
font-size: 11px;
font-weight: 600;
color: var(--fms-text-secondary);
letter-spacing: 0.4px;
}
.mm-config-nav__item {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
padding: 5px 8px;
border: none;
border-radius: var(--fms-control-radius);
background: transparent;
color: var(--fms-text);
font-size: 13px;
text-align: left;
cursor: pointer;
&:hover {
background: var(--fms-secondary);
}
&.is-active {
background: color-mix(in srgb, var(--fms-primary) 10%, transparent);
color: var(--fms-primary);
font-weight: 500;
}
/* rightExtra 操作区与标签在同一行垂直居中 */
.mm-config-nav :deep(.tabs-nav-extra) {
margin-top: 0;
margin-bottom: 0;
padding-right: 10px;
}
/* 标签内布局:图标 + 文字 + badge */
.mm-config-nav__icon {
flex: none;
color: var(--fms-text-secondary);
}
.mm-config-nav__item.is-active .mm-config-nav__icon {
.mm-config-nav .tabs-tab-active .mm-config-nav__icon {
color: var(--fms-primary);
}
.mm-config-nav__label {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mm-config-nav__badge {
flex: none;
padding: 0 5px;
border-radius: 999px;
background: var(--fms-secondary);
color: var(--fms-text-secondary);
font-size: 11px;
font-size: 10px;
line-height: 16px;
}
@@ -2185,7 +2003,6 @@ loadMenuTree()
display: flex;
flex-direction: column;
overflow: hidden;
padding: 12px 16px 16px;
}
.mm-config-pane {
@@ -2195,34 +2012,8 @@ loadMenuTree()
display: flex;
flex-direction: column;
overflow: auto;
}
/* 分组管理占位页(弹窗由中栏导航触发) */
.mm-groups-pane {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
flex: 1;
min-height: 180px;
padding: 24px;
border: 1px dashed var(--fms-border);
border-radius: var(--fms-control-radius);
background: var(--fms-secondary);
text-align: center;
}
.mm-groups-pane__icon {
color: var(--fms-text-secondary);
}
.mm-groups-pane__text {
max-width: 440px;
margin: 0;
color: var(--fms-text-secondary);
font-size: 13px;
line-height: 1.7;
/* 滚动容器内边距:内容与滚动边缘保持间距,聚焦光环(3px)不被 overflow: auto 裁剪 */
padding: 12px 16px 16px;
}
/* 配置区加载中 / 空态占位 */