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

@@ -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 {