20260807173051

This commit is contained in:
oneao committed 2026-08-07 17:30:51 +08:00
1 parent 6938deef39
commit a07b1b6fdb
14 files changed
+441 -270

No files matched your search

+3
View File
@@ -2,6 +2,9 @@
import zhCN from "antdv-next/locale/zh_CN"; import zhCN from "antdv-next/locale/zh_CN";
const themeConfig = { const themeConfig = {
token: {
splitBarSize: 1,
},
}; };
@@ -81,12 +81,11 @@ const routeInstanceKey = computed(
} }
.app-content { .app-content {
width: 100%;
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
padding: 6px 0; padding: 12px 0;
background: #ffffff; background: #ffffff;
} }
@@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed, onMounted, ref, watch } from "vue"; import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { ChevronDown, House } from "@lucide/vue"; import { ChevronDown, House } from "@lucide/vue";
import { useAppStore } from "@/stores/app"; import { useAppStore } from "@/stores/app";
@@ -16,6 +16,41 @@ const permissionStore = usePermissionStore();
const openKeys = ref([]); const openKeys = ref([]);
const effectiveCollapsed = computed(() => appStore.sidebarCollapsed); const effectiveCollapsed = computed(() => appStore.sidebarCollapsed);
/* ---- 拖拽调整宽度 ---- */
const MIN_WIDTH = 180;
const MAX_WIDTH = 400;
const sidebarWidth = ref(220);
const isResizing = ref(false);
function onResizeStart(e) {
e.preventDefault();
isResizing.value = true;
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
}
function onResizeMove(e) {
if (!isResizing.value) return;
sidebarWidth.value = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, e.clientX));
}
function onResizeEnd() {
if (!isResizing.value) return;
isResizing.value = false;
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
onMounted(() => {
document.addEventListener("mousemove", onResizeMove);
document.addEventListener("mouseup", onResizeEnd);
});
onBeforeUnmount(() => {
document.removeEventListener("mousemove", onResizeMove);
document.removeEventListener("mouseup", onResizeEnd);
});
function resolveIcon(iconName, fallback) { function resolveIcon(iconName, fallback) {
return getIcon(iconName) || fallback; return getIcon(iconName) || fallback;
} }
@@ -174,16 +209,16 @@ onMounted(() => {
</script> </script>
<template> <template>
<a-layout-sider <div class="sidebar-outer" :style="{ width: effectiveCollapsed ? '56px' : sidebarWidth + 'px' }">
class="app-sider" <a-layout-sider
:collapsed="effectiveCollapsed" class="app-sider"
:trigger="null" :collapsed="effectiveCollapsed"
:collapsed-width="56" :trigger="null"
collapsible :collapsed-width="56"
resizable collapsible
theme="light" theme="light"
width="220" width="100%"
> >
<!-- 品牌区 --> <!-- 品牌区 -->
<div class="sidebar-header" :class="{ collapsed: effectiveCollapsed }"> <div class="sidebar-header" :class="{ collapsed: effectiveCollapsed }">
<div class="sidebar-brand"> <div class="sidebar-brand">
@@ -263,22 +298,54 @@ onMounted(() => {
</template> </template>
</nav> </nav>
</a-layout-sider> </a-layout-sider>
<div
v-if="!effectiveCollapsed"
class="sidebar-resize-handle"
@mousedown="onResizeStart"
/>
</div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
/* ============================================================
* Sidebar 外层容器(控制宽度)
* ============================================================ */
.sidebar-outer {
position: relative;
display: flex;
flex: none;
height: 100%;
}
/* ---- 拖拽手柄 ---- */
.sidebar-resize-handle {
position: absolute;
top: 0;
right: -2px;
bottom: 0;
width: 4px;
z-index: 20;
cursor: col-resize;
background: transparent;
transition: background-color 0.15s ease;
&:hover {
background: rgb(0 0 0 / 6%);
}
}
/* ============================================================ /* ============================================================
* Sidebar 容器 * Sidebar 容器
* ============================================================ */ * ============================================================ */
.app-sider { .app-sider {
width: 100% !important;
max-width: none !important;
min-width: 0 !important;
flex: none !important;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
border-right: 1px solid transparent;
background: #ffffff; background: #ffffff;
transition: border-color 0.2s ease;
&:hover {
border-right-color: var(--border, #e4e4e7);
}
:deep(.ant-layout-sider-children) { :deep(.ant-layout-sider-children) {
display: flex; display: flex;
@@ -387,8 +454,8 @@ onMounted(() => {
color: #3f3f46; color: #3f3f46;
font-weight: 500; font-weight: 500;
box-shadow: box-shadow:
0 1px 3px rgb(0 0 0 / 8%), 0 1px 3px rgb(0 0 0 / 4%),
0 2px 4px rgb(0 0 0 / 6%); 0 2px 4px rgb(0 0 0 / 3%);
} }
&.collapsed { &.collapsed {
@@ -212,17 +212,18 @@ function handleTabKeydown(event, index) {
<style lang="scss" scoped> <style lang="scss" scoped>
.tab-header { .tab-header {
position: relative;
z-index: 15; z-index: 15;
display: flex; display: flex;
flex: none; flex: none;
height: 34px; height: 38px;
align-items: center; align-items: center;
gap: 4px; gap: 2px;
padding: 3px 0; padding: 4px 0;
background: #ffffff; background: transparent;
} }
/* ---- 通用小按钮 ---- */ /* ---- 滚动按钮(ghost 风格) ---- */
.tab-btn { .tab-btn {
display: grid; display: grid;
flex: none; flex: none;
@@ -230,7 +231,7 @@ function handleTabKeydown(event, index) {
height: 28px; height: 28px;
padding: 0; padding: 0;
place-items: center; place-items: center;
border: 1px solid #e4e4e7; border: 0;
border-radius: 6px; border-radius: 6px;
color: #71717a; color: #71717a;
background: transparent; background: transparent;
@@ -262,8 +263,7 @@ function handleTabKeydown(event, index) {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
gap: 6px; gap: 6px;
padding: 2px 0; padding: 0;
margin: -2px 0;
overflow-x: auto; overflow-x: auto;
overflow-y: visible; overflow-y: visible;
overscroll-behavior-inline: contain; overscroll-behavior-inline: contain;
@@ -274,35 +274,41 @@ function handleTabKeydown(event, index) {
} }
} }
/* ---- 标签项 ---- */ /* ---- 标签项(应用 tab 药丸式) ---- */
.tab-item { .tab-item {
display: flex; display: flex;
flex: none; flex: none;
min-width: 64px; min-width: 72px;
height: 28px; height: 30px;
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;
border-radius: 6px; border-radius: 8px;
background: #f4f4f5; background: transparent;
color: #71717a; color: #71717a;
transition: transition:
background-color 0.15s ease, background-color 0.15s ease,
color 0.15s ease; color 0.15s ease;
background: #f4f4f5;
&:hover { &:hover {
background: #f4f4f5; background: #ebebeb;
color: #3f3f46; color: #3f3f46;
} }
/* 选中态:实心深色药丸,应用 tab 的醒目语言 */
&--active { &--active {
background: #f4f4f5; color: #ffffff;
color: #3f3f46;
font-weight: 500; font-weight: 500;
box-shadow: 0 1px 3px rgb(0 0 0 / 6%), 0 1px 2px rgb(0 0 0 / 4%); background: #18181b;
&:hover { &:hover {
background: #f4f4f5; background: #27272a;
color: #3f3f46; color: #ffffff;
}
.tab-item__close:hover {
background: rgb(255 255 255 / 12%);
} }
} }
@@ -323,27 +329,30 @@ function handleTabKeydown(event, index) {
justify-content: center; justify-content: center;
padding: 0 8px; padding: 0 8px;
font-size: 13px; font-size: 13px;
overflow: hidden;
span { span {
white-space: nowrap; white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
} }
&__close { &__close {
display: grid; display: grid;
width: 18px;
flex: none; flex: none;
width: 22px;
padding: 0; padding: 0;
place-items: center; place-items: center;
border-radius: 4px; border-radius: 4px;
&:hover { &:hover {
background: rgb(0 0 0 / 6%); background: rgb(0 0 0 / 8%);
} }
.lucide { .lucide {
width: 10px; width: 12px;
height: 10px; height: 12px;
stroke-width: 2; stroke-width: 2;
} }
} }
@@ -89,7 +89,7 @@ function handleClick() {
background: #f4f4f5; background: #f4f4f5;
color: #3f3f46; color: #3f3f46;
font-weight: 500; font-weight: 500;
box-shadow: 0 1px 3px rgb(0 0 0 / 8%), 0 2px 4px rgb(0 0 0 / 6%); box-shadow: 0 1px 3px rgb(0 0 0 / 4%), 0 2px 4px rgb(0 0 0 / 3%);
} }
/* ---- 可展开的父级按钮 ---- */ /* ---- 可展开的父级按钮 ---- */
@@ -34,6 +34,12 @@ const localRow = ref(null);
const enabled = computed(() => Number(localRow.value?.b_canuse) === 1); const enabled = computed(() => Number(localRow.value?.b_canuse) === 1);
function toggleEnabled(checked) {
if (!localRow.value) return;
localRow.value.b_canuse = checked ? 1 : 0;
notifyUpdate();
}
const fieldOptions = computed(() => { const fieldOptions = computed(() => {
const options = []; const options = [];
for (const field of props.fields || []) { for (const field of props.fields || []) {
@@ -114,6 +120,13 @@ const preview = computed(() => {
<div class="panel-body"> <div class="panel-body">
<a-form layout="vertical" class="auto-code-form" :disabled="loading"> <a-form layout="vertical" class="auto-code-form" :disabled="loading">
<template v-if="localRow"> <template v-if="localRow">
<a-form-item label="启用自动编码">
<a-switch
:checked="enabled"
:disabled="loading"
@update:checked="toggleEnabled"
/>
</a-form-item>
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="8"> <a-col :span="8">
<a-form-item label="业务编号字段" :required="enabled"> <a-form-item label="业务编号字段" :required="enabled">
@@ -210,7 +223,6 @@ const preview = computed(() => {
.panel-body { .panel-body {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
padding: 16px 20px;
} }
.auto-code-form { .auto-code-form {
@@ -262,7 +262,6 @@ function onI18nSaved(key) {
.panel-body { .panel-body {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
padding: 16px 20px;
} }
.basic-form { .basic-form {
@@ -213,13 +213,12 @@ const columns = shallowRef([
display: flex; display: flex;
flex: none; flex: none;
gap: 8px; gap: 8px;
padding: 12px 20px 0; padding: 0 0 8px;
} }
.panel-body { .panel-body {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
padding: 12px 20px 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -24,7 +24,7 @@ const props = defineProps({
loading: { type: Boolean, default: false }, loading: { type: Boolean, default: false },
}); });
const emit = defineEmits(["update", "i18nSaved"]); const emit = defineEmits(["update", "i18nSaved", "syncFields"]);
const localFields = ref([]); const localFields = ref([]);
const activeKey = ref(null); const activeKey = ref(null);
@@ -153,6 +153,9 @@ const columns = ref([
<template> <template>
<div class="fields-panel"> <div class="fields-panel">
<div class="panel-toolbar">
<a-button :disabled="loading" @click="emit('syncFields')">同步字段</a-button>
</div>
<div class="panel-body"> <div class="panel-body">
<FmsTable <FmsTable
v-model:columns="columns" v-model:columns="columns"
@@ -175,10 +178,16 @@ const columns = ref([
height: 100%; height: 100%;
} }
.panel-toolbar {
display: flex;
flex: none;
gap: 8px;
padding: 0 0 8px;
}
.panel-body { .panel-body {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
padding: 12px 20px 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -487,13 +487,12 @@ const columns = computed(() => {
display: flex; display: flex;
flex: none; flex: none;
gap: 8px; gap: 8px;
padding: 12px 20px 0; padding: 0 0 8px;
} }
.panel-body { .panel-body {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
padding: 12px 20px 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -198,13 +198,12 @@ const columns = shallowRef([
display: flex; display: flex;
flex: none; flex: none;
gap: 8px; gap: 8px;
padding: 12px 20px 0; padding: 0 0 8px;
} }
.panel-body { .panel-body {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
padding: 12px 20px 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -253,13 +253,12 @@ const columns = ref([
display: flex; display: flex;
flex: none; flex: none;
gap: 8px; gap: 8px;
padding: 12px 20px 0; padding: 0 0 8px;
} }
.panel-body { .panel-body {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
padding: 12px 20px 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -154,13 +154,12 @@ const columns = shallowRef([
display: flex; display: flex;
flex: none; flex: none;
gap: 8px; gap: 8px;
padding: 12px 20px 0; padding: 0 0 8px;
} }
.panel-body { .panel-body {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
padding: 12px 20px 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed, h, ref, watch } from "vue"; import { computed, h, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { message, Modal } from "antdv-next"; import { message, Modal } from "antdv-next";
import { Plus, Trash2 } from "@lucide/vue"; import { Plus, Trash2 } from "@lucide/vue";
import FmsLoadingOverlay from "@/components/FmsLoadingOverlay.vue"; import FmsLoadingOverlay from "@/components/FmsLoadingOverlay.vue";
@@ -42,6 +42,25 @@ const expandedTreeKeys = ref([]);
const selectedModuleId = ref(null); const selectedModuleId = ref(null);
const skipLoad = ref(false); const skipLoad = ref(false);
const activeTab = ref("basic"); const activeTab = ref("basic");
const treeSearch = ref("");
function filterTree(nodes, keyword) {
if (!keyword) return nodes;
const kw = keyword.toLowerCase();
const result = [];
for (const node of nodes) {
const title = (node.b_name || node.b_code || node.title || "").toLowerCase();
const children = filterTree(node.children || [], kw);
if (title.includes(kw) || children.length) {
result.push({ ...node, children });
}
}
return result;
}
const filteredTreeData = computed(() => {
return filterTree(treeData.value, treeSearch.value);
});
function buildTreeContextMenu(node) { function buildTreeContextMenu(node) {
const items = []; const items = [];
const iconSize = 14; const iconSize = 14;
@@ -123,6 +142,46 @@ const busy = computed(() => configLoading.value || configSaving.value || configS
const syncDisabled = computed( const syncDisabled = computed(
() => busy.value || !String(selectedModule.value?.b_viewtable || "").trim(), () => busy.value || !String(selectedModule.value?.b_viewtable || "").trim(),
); );
/* ---- 左侧面板拖拽调整宽度 — 抄 AppSidebar 的 sidebar-resize-handle ---- */
const MIN_SIDEBAR = 210;
const MAX_SIDEBAR = 440;
const leftPanelWidth = ref(260);
const isResizing = ref(false);
let resizeStartX = 0;
let resizeStartWidth = 0;
function onResizeStart(e) {
e.preventDefault();
isResizing.value = true;
resizeStartX = e.clientX;
resizeStartWidth = leftPanelWidth.value;
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
}
function onResizeMove(e) {
if (!isResizing.value) return;
const delta = e.clientX - resizeStartX;
leftPanelWidth.value = Math.min(MAX_SIDEBAR, Math.max(MIN_SIDEBAR, resizeStartWidth + delta));
}
function onResizeEnd() {
if (!isResizing.value) return;
isResizing.value = false;
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
onMounted(() => {
document.addEventListener("mousemove", onResizeMove);
document.addEventListener("mouseup", onResizeEnd);
});
onBeforeUnmount(() => {
document.removeEventListener("mousemove", onResizeMove);
document.removeEventListener("mouseup", onResizeEnd);
});
// 构建受管多语言项。managedSource 提供模块/字段/权限的受管键来源, // 构建受管多语言项。managedSource 提供模块/字段/权限的受管键来源,
// plainSource 提供分组/列表标题等非受管键来源(手动填写,需即时反馈)。 // plainSource 提供分组/列表标题等非受管键来源(手动填写,需即时反馈)。
function buildI18nItems(managedSource, plainSource) { function buildI18nItems(managedSource, plainSource) {
@@ -1040,185 +1099,167 @@ void permissionStore
<template> <template>
<div class="mm-page"> <div class="mm-page">
<a-splitter class="mm-splitter" lazy> <!-- 左侧模块树 -->
<a-splitter-panel :default-size="260" :min="210" :max="440" collapsible> <aside class="mm-sidebar" :style="{ width: leftPanelWidth + 'px' }">
<!-- 左侧模块树 --> <div class="mm-tree-search">
<aside class="mm-sidebar"> <a-input-search
v-model:value="treeSearch"
allow-clear
/>
</div>
<div class="mm-tree-stage"> <div class="mm-tree-stage">
<div v-if="treeLoading" class="mm-tree-empty"> <div v-if="treeLoading" class="mm-tree-empty">
<a-spin /> <a-spin />
</div> </div>
<FmsTree <div v-else class="mm-tree-scroll">
v-else <FmsTree
:disabled="configSaving" :disabled="configSaving"
:tree-data="treeData" :tree-data="filteredTreeData"
v-model:expanded-keys="expandedTreeKeys" v-model:expanded-keys="expandedTreeKeys"
:selected-keys="selectedModuleId ? [selectedModuleId] : []" :selected-keys="selectedModuleId ? [selectedModuleId] : []"
:context-menu="buildTreeContextMenu" :context-menu="buildTreeContextMenu"
:context-menu-disabled="busy" :context-menu-disabled="busy"
@select="onTreeSelect" @select="onTreeSelect"
@menu-click="onTreeMenuClick" @menu-click="onTreeMenuClick"
> >
<template #title="node"> <template #title="node">
<span class="mm-tree-node"> <span class="mm-tree-node">
<span>{{ <span>{{
appStore.t(node.b_i18n, node.b_name || node.b_code || node.title) appStore.t(node.b_i18n, node.b_name || node.b_code || node.title)
}}</span> }}</span>
</span> </span>
</template> </template>
</FmsTree> </FmsTree>
</div>
</div> </div>
</aside> </aside>
</a-splitter-panel> <div class="mm-resize-handle" @mousedown="onResizeStart" />
<a-splitter-panel :min="720">
<!-- 右侧详情 --> <!-- 右侧详情 -->
<main class="mm-main"> <main class="mm-main">
<FmsLoadingOverlay :loading="configLoading"> <FmsLoadingOverlay :loading="configLoading">
<template v-if="selectedModule"> <template v-if="selectedModule">
<!-- 页面头部 --> <!-- 顶部栏:tabs + 开关 + 操作 -->
<div class="mm-header"> <div class="mm-tab-bar">
<a-space> <nav class="mm-tab-nav">
<button
v-for="tab in visibleTabs"
:key="tab.key"
type="button"
class="mm-tab-nav__item"
:class="{ active: activeTab === tab.key }"
@click="activeTab = tab.key"
>
{{ tab.tab }}
</button>
</nav>
<div class="mm-tab-bar__right">
<label class="mm-switch"><span>启用</span><a-checkbox
:checked="drafts.module?.b_canuse === 1"
:disabled="busy"
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
/></label>
<template v-if="drafts.module?.b_module_type !== 'data'">
<label class="mm-switch"><span>显示菜单</span><a-checkbox
:checked="drafts.module?.b_canmenu === 1"
:disabled="busy || ['data', 'data_category'].includes(drafts.module?.b_module_type)"
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
/></label>
</template>
<a-button <a-button
v-if="canManage('update')" v-if="canManage('update')"
type="primary" type="primary"
:loading="configSaving" :loading="configSaving"
:disabled="configLoading || configSyncing" :disabled="configLoading || configSyncing"
@click="saveAll" @click="saveAll"
>保存</a-button
> >
<a-button 保存
v-if="selectedModule.b_module_type === 'data' && canManage('sync')"
:loading="configSyncing"
:disabled="syncDisabled"
@click="syncFields"
>
同步字段
</a-button> </a-button>
</a-space>
<div class="mm-header-center">
<a-space size="middle">
<span>启用</span>
<a-checkbox
:checked="drafts.module?.b_canuse === 1"
:disabled="busy"
@update:checked="(v) => onSetField('b_canuse', v ? 1 : 0)"
/>
<template v-if="drafts.module?.b_module_type !== 'data'">
<a-divider type="vertical" />
<span>显示菜单</span>
<a-checkbox
:checked="drafts.module?.b_canmenu === 1"
:disabled="
busy || ['data', 'data_category'].includes(drafts.module?.b_module_type)
"
@update:checked="(v) => onSetField('b_canmenu', v ? 1 : 0)"
/>
</template>
<template v-if="drafts.module?.b_module_type === 'data'">
<a-divider type="vertical" />
<span>自动编码</span>
<a-checkbox
:checked="autoCodeEnabled"
:disabled="busy"
@update:checked="setAutoCodeEnabled"
/>
</template>
</a-space>
</div> </div>
</div> </div>
<!-- Tab 面板 --> <!-- 面板内容 -->
<div class="mm-body"> <div class="mm-body">
<a-tabs v-model:active-key="activeTab" class="mm-tabs"> <ModuleBasicPanel
<a-tab-pane v-if="activeTab === 'basic'"
v-for="tab in visibleTabs" :module-id="String(selectedModule.b_id)"
:key="tab.key" :data="drafts.module"
:tab="tab.tab" :parent-name="parentModuleName"
:force-render="tab.key === 'i18n'" :loading="busy"
> @update="(v) => onUpdate('module', v)"
<ModuleBasicPanel @i18n-saved="onI18nSaved"
v-if="tab.key === 'basic'" />
:module-id="String(selectedModule.b_id)" <ModuleFieldsPanel
:data="drafts.module" v-else-if="activeTab === 'fields'"
:parent-name="parentModuleName" :module-id="String(selectedModule.b_id)"
:loading="busy" :module-code="selectedModule?.b_code || ''"
@update="(v) => onUpdate('module', v)" :fields="drafts.fields"
@i18n-saved="onI18nSaved" :loading="busy"
/> @update="onFieldsUpdate"
<ModuleFieldsPanel @i18n-saved="onI18nSaved"
v-else-if="tab.key === 'fields'" @sync-fields="syncFields"
:module-id="String(selectedModule.b_id)" />
:module-code="selectedModule?.b_code || ''" <ModuleListConfigPanel
:fields="drafts.fields" v-else-if="activeTab === 'list'"
:loading="busy" :module-id="String(selectedModule.b_id)"
@update="onFieldsUpdate" :list-config="drafts.listConfig"
@i18n-saved="onI18nSaved" :edit-config="drafts.editConfig"
/> :query-config="drafts.queryConfig"
<ModuleListConfigPanel :fields="drafts.fields"
v-else-if="tab.key === 'list'" :groups="drafts.groups"
:module-id="String(selectedModule.b_id)" :referenced-group-ids="referencedGroupIds"
:list-config="drafts.listConfig" :loading="busy"
:edit-config="drafts.editConfig" @update="(v) => onUpdate('listConfig', v)"
:query-config="drafts.queryConfig" @groups-saved="onGroupsSaved"
:fields="drafts.fields" @sync-edit="(v) => onUpdate('editConfig', v)"
:groups="drafts.groups" @sync-query="(v) => onUpdate('queryConfig', v)"
:referenced-group-ids="referencedGroupIds" />
:loading="busy" <ModuleEditConfigPanel
@update="(v) => onUpdate('listConfig', v)" v-else-if="activeTab === 'edit'"
@groups-saved="onGroupsSaved" :module-id="String(selectedModule.b_id)"
@sync-edit="(v) => onUpdate('editConfig', v)" :edit-config="drafts.editConfig"
@sync-query="(v) => onUpdate('queryConfig', v)" :list-config="drafts.listConfig"
/> :query-config="drafts.queryConfig"
<ModuleEditConfigPanel :fields="drafts.fields"
v-else-if="tab.key === 'edit'" :groups="drafts.groups"
:module-id="String(selectedModule.b_id)" :referenced-group-ids="referencedGroupIds"
:edit-config="drafts.editConfig" :loading="busy"
:list-config="drafts.listConfig" @update="(v) => onUpdate('editConfig', v)"
:query-config="drafts.queryConfig" @groups-saved="onGroupsSaved"
:fields="drafts.fields" @sync-list="(v) => onUpdate('listConfig', v)"
:groups="drafts.groups" @sync-query="(v) => onUpdate('queryConfig', v)"
:referenced-group-ids="referencedGroupIds" />
:loading="busy" <ModuleQueryConfigPanel
@update="(v) => onUpdate('editConfig', v)" v-else-if="activeTab === 'query'"
@groups-saved="onGroupsSaved" :module-id="String(selectedModule.b_id)"
@sync-list="(v) => onUpdate('listConfig', v)" :query-config="drafts.queryConfig"
@sync-query="(v) => onUpdate('queryConfig', v)" :fields="drafts.fields"
/> :loading="busy"
<ModuleQueryConfigPanel @update="(v) => onUpdate('queryConfig', v)"
v-else-if="tab.key === 'query'" />
:module-id="String(selectedModule.b_id)" <ModuleAutoCodePanel
:query-config="drafts.queryConfig" v-else-if="activeTab === 'auto-code'"
:fields="drafts.fields" :module-id="String(selectedModule.b_id)"
:loading="busy" :auto-code="drafts.autoCode"
@update="(v) => onUpdate('queryConfig', v)" :fields="drafts.fields"
/> :loading="busy"
<ModuleAutoCodePanel @update="onAutoCodeUpdate"
v-else-if="tab.key === 'auto-code'" />
:module-id="String(selectedModule.b_id)" <ModulePowersPanel
:auto-code="drafts.autoCode" v-else-if="activeTab === 'powers'"
:fields="drafts.fields" :module-id="String(selectedModule.b_id)"
:loading="busy" :module-code="selectedModule?.b_code || ''"
@update="onAutoCodeUpdate" :powers="drafts.powers"
/> :loading="busy"
<ModulePowersPanel @update="(v) => onUpdate('powers', v)"
v-else-if="tab.key === 'powers'" @i18n-saved="onI18nSaved"
:module-id="String(selectedModule.b_id)" />
:module-code="selectedModule?.b_code || ''" <ModuleI18nPanel
:powers="drafts.powers" v-else-if="activeTab === 'i18n'"
:loading="busy" :ref="setI18nPanelRef"
@update="(v) => onUpdate('powers', v)" :module-id="String(selectedModule.b_id)"
@i18n-saved="onI18nSaved" :i18n-items="i18nItems"
/> :loading="busy"
<ModuleI18nPanel />
v-else-if="tab.key === 'i18n'"
:ref="setI18nPanelRef"
:module-id="String(selectedModule.b_id)"
:i18n-items="i18nItems"
:loading="busy"
/>
</a-tab-pane>
</a-tabs>
</div> </div>
</template> </template>
@@ -1228,27 +1269,35 @@ void permissionStore
</div> </div>
</FmsLoadingOverlay> </FmsLoadingOverlay>
</main> </main>
</a-splitter-panel>
</a-splitter>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.mm-page, .mm-page {
.mm-splitter { display: flex;
width: 100%; width: 100%;
height: 100%; height: 100%;
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
background: #ffffff;
} }
.mm-page { /* 拖拽手柄 — 抄 AppSidebar 的 sidebar-resize-handle */
background: var(--fms-bg); .mm-resize-handle {
flex: none;
width: 4px;
cursor: col-resize;
background: transparent;
transition: background-color 0.15s ease;
&:hover {
background: rgb(0 0 0 / 6%);
}
} }
.mm-sidebar { .mm-sidebar {
display: flex; display: flex;
width: 100%; flex: none;
height: 100%; height: 100%;
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
@@ -1256,13 +1305,24 @@ void permissionStore
background: var(--fms-surface); background: var(--fms-surface);
} }
.mm-tree-search {
flex: none;
padding: 8px 0;
}
.mm-tree-stage { .mm-tree-stage {
display: flex; display: flex;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow: auto; overflow: hidden;
flex-direction: column; flex-direction: column;
padding: 12px; padding-bottom: 8px;
}
.mm-tree-scroll {
flex: 1;
min-height: 0;
overflow: auto;
} }
.mm-tree-empty { .mm-tree-empty {
@@ -1284,68 +1344,86 @@ void permissionStore
.mm-main { .mm-main {
display: flex; display: flex;
width: 100%; flex: 1;
height: 100%; height: 100%;
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
flex-direction: column; flex-direction: column;
overflow: hidden;
background: var(--fms-surface); background: var(--fms-surface);
} }
.mm-header { /* ---- 顶部栏:tabs + 开关 + 操作 ---- */
.mm-tab-bar {
display: flex; display: flex;
align-items: center;
flex: none; flex: none;
gap: 12px; align-items: center;
padding: 12px 20px; height: 44px;
background: var(--fms-surface); padding: 0 20px;
border-bottom: 1px solid var(--fms-border);
&__right {
display: flex;
flex: none;
align-items: center;
gap: 12px;
}
} }
.mm-header-center { .mm-switch {
flex: 1; display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #71717a;
cursor: pointer;
}
/* ---- 自定义 tab 导航 ---- */
.mm-tab-nav {
display: flex; display: flex;
justify-content: center; flex: 1;
min-width: 0;
align-items: center;
gap: 2px;
&__item {
display: inline-flex;
height: 32px;
align-items: center;
padding: 0 12px;
border: 0;
border-radius: 6px;
background: transparent;
color: #71717a;
cursor: pointer;
font-size: 13px;
transition:
background-color 0.15s ease,
color 0.15s ease;
&:hover {
background: #f4f4f5;
color: #3f3f46;
}
&.active {
background: #f4f4f5;
color: #0a0a0a;
font-weight: 500;
&:hover {
background: #ebebeb;
color: #0a0a0a;
}
}
}
} }
.mm-body { .mm-body {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
} padding: 16px 20px 0;
.mm-tabs {
height: 100%;
display: flex;
flex-direction: column;
:deep(.ant-tabs-nav) {
margin-bottom: 0;
padding: 0 20px;
background: var(--fms-surface);
}
:deep(.ant-tabs-content-holder) {
flex: 1;
min-height: 0;
overflow: hidden;
}
:deep(.ant-tabs-content) {
flex: 1;
min-height: 0;
height: 100%;
overflow: hidden;
}
:deep(.ant-tabs-tabpane) {
height: 100%;
}
:deep(.ant-tabs-body),
:deep(.ant-tabs-body-top) {
height: 100%;
}
} }
.mm-loading { .mm-loading {