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