20260824172723
This commit is contained in:
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;
|
||||
}
|
||||
|
||||
/* 配置区加载中 / 空态占位 */
|
||||
|
||||
Reference in new issue
Block a user