20260901222055

This commit is contained in:
oneao committed 2026-09-01 22:20:55 +08:00
1 parent 53aba8b956
commit 6b9cda5424
12 files changed
+2129 -275

No files matched your search

@@ -0,0 +1,111 @@
<script setup>
/**
* 多语言名称编辑弹窗(通用组件)。
* 用于编辑一个对象在不同语言下的名称,支持任意语言列表。
*
* 用法:
* <I18nNameModal
* :open="open"
* :languages="I18N_LANGUAGES"
* :model-value="{ 'zh-CN': '名称', 'en-US': 'Name' }"
* @update:model-value="(v) => writeBack(v)"
* @update:open="(v) => (open = v)"
* />
*/
import { ref, watch } from 'vue'
import { Modal, Button, Input } from '@/components/ui'
defineOptions({ inheritAttrs: false })
const props = defineProps({
/** 受控打开状态 */
open: { type: Boolean, default: false },
/** 弹窗标题 */
title: { type: String, default: '多语言名称' },
/** 语言列表 [{ code, name, default }] */
languages: { type: Array, default: () => [] },
/** 各语言值 { [code]: value } */
modelValue: { type: Object, default: () => ({}) },
/** 底部提示文字 */
hint: { type: String, default: '' },
})
const emit = defineEmits(['update:open', 'update:modelValue', 'confirm'])
/** 本地草稿:打开时快照,确定时提交 */
const draft = ref({})
watch(
() => props.open,
(open) => {
if (open) draft.value = { ...props.modelValue }
},
)
function onValueChange(lang, value) {
draft.value = { ...draft.value, [lang.code]: value }
}
function onOk() {
emit('update:modelValue', { ...draft.value })
emit('confirm', { ...draft.value })
emit('update:open', false)
}
function onCancel() {
emit('update:open', false)
}
</script>
<template>
<Modal :open="open" :title="title" :width="520" :mask-closable="false" @cancel="onCancel">
<div class="i18n-name-modal">
<div class="i18n-name-row" v-for="lang in languages" :key="lang.code">
<span class="i18n-name-row__label">{{ lang.name }}</span>
<Input
class="i18n-name-row__input"
:model-value="draft[lang.code] || ''"
:placeholder="`请输入${lang.name}名称`"
@update:model-value="(v) => onValueChange(lang, v)"
/>
</div>
<p v-if="hint" class="i18n-name-modal__hint">{{ hint }}</p>
</div>
<template #footer>
<Button type="outline" @click="onCancel">取消</Button>
<Button type="primary" @click="onOk">确定</Button>
</template>
</Modal>
</template>
<style scoped lang="scss">
.i18n-name-modal {
display: flex;
flex-direction: column;
gap: 10px;
}
.i18n-name-row {
display: flex;
align-items: center;
gap: 10px;
max-width: 480px;
}
.i18n-name-row__label {
flex-shrink: 0;
width: 80px;
font-size: 13px;
color: var(--fms-text-secondary);
}
.i18n-name-row__input {
flex: 1;
min-width: 0;
}
.i18n-name-modal__hint {
margin: 0;
font-size: 12px;
line-height: 1.7;
color: var(--fms-text-secondary);
}
</style>
@@ -56,6 +56,7 @@ const navMain = computed(() => {
icon: Settings,
items: [
{ title: '模块管理', url: '/system/module-management' },
{ title: '菜单管理', url: '/system/menu-management' },
],
})
+7
View File
@@ -101,6 +101,13 @@ const router = createRouter({
component: () => import('@/views/system/module-management/index.vue'),
meta: { title: '模块管理', componentName: 'ModuleManagement' },
},
// 菜单管理:占位页面,待实现
{
path: '/system/menu-management',
name: 'system-menu-management',
component: () => import('@/views/system/menu-management/index.vue'),
meta: { title: '菜单管理', componentName: 'MenuManagement' },
},
// 业务页面后续在此追加,配置 meta.moduleId 后自动走权限校验
],
},
+1 -1
View File
@@ -107,7 +107,7 @@
/* 栅格(grid):默认水平/垂直间距(不随主题变化) */
--fms-grid-gutter: 0px;
--fms-splitter-bar-size: 1px;
--fms-splitter-bar-size: 3px;
--fms-splitter-bar-size-hover: 4px;
--fms-splitter-handle-size: 20px;
File diff suppressed because it is too large. Load diff
@@ -201,8 +201,8 @@ watch(
function buildColumns() {
const baseColumns = [
{ dataIndex: 'b_field', title: '字段名', width: 150 },
{ dataIndex: 'b_name', title: '字段名称', width: 140, editor: { type: 'input' } },
{ dataIndex: 'b_field', title: '字段名', width: 150, sorter: true },
{ dataIndex: 'b_name', title: '字段名称', width: 140, sorter: true, editor: { type: 'input' } },
{
dataIndex: 'b_type',
title: '控件类型',
@@ -251,6 +251,7 @@ function buildColumns() {
<FmsTable
v-model:columns="columns"
:data-source="localFields"
:sort-config="{ emptyToBottom: true, stringLocaleCompare: true }"
:editable="!loading"
row-key="b_id"
:active-row-key="activeKey"
@@ -8,7 +8,7 @@
*/
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { Plus, Trash2 } from '@lucide/vue'
import { Button, Input } from '@/components/ui'
import { Button } from '@/components/ui'
import FmsTable from '@/components/fms-table/FmsTable.vue'
import { createTempId } from '@/utils/tempId'
import {
@@ -24,8 +24,6 @@ const props = defineProps({
module: { type: Object, default: null },
/** 字段草稿(b_field / b_name / b_i18n),受管键依赖它 */
fields: { type: Array, default: () => [] },
/** 权限草稿(b_code / b_name),受管键依赖它 */
powers: { type: Array, default: () => [] },
/** 分组草稿(b_id / b_title / b_i18n),受管键依赖它 */
groups: { type: Array, default: () => [] },
/** index.vue 加载的原始 s_i18n 扁平行([{b_id,b_key,b_locale,b_value}]) */
@@ -49,13 +47,12 @@ const defaultLocale = computed(() => langOptions.value[0]?.code || '')
const localRows = ref([])
/** 保存 diff 基线:加载时的原始扁平行 */
const originalsFlat = ref([])
const searchKey = ref('')
const activeKey = ref(null)
const tableRef = ref(null)
const selectedRowKeys = ref([])
/* ================= 受管键收集 ================= */
// 模块 / 字段 / 权限 / 分组的翻译键是系统自动生成的,必须始终存在:
// 模块 / 字段 / 分组的翻译键是系统自动生成的,必须始终存在:
// 收集这些键(含默认名称),供 reconcile 补行、跟随名称、清理失效键。
const finalI18nItems = computed(() => {
const module = props.module
@@ -74,10 +71,6 @@ const finalI18nItems = computed(() => {
const key = buildModuleItemI18nKey('field', module.b_code, field.b_field)
if (key) items.push({ key, managed: true, defaultValue: field.b_name || '', id: `field:${key}` })
}
for (const power of props.powers || []) {
const key = buildModuleItemI18nKey('power', module.b_code, power.b_code)
if (key) items.push({ key, managed: true, defaultValue: power.b_name || '', id: `power:${key}` })
}
// 分组标题多语言:b_i18n 为用户手填 key(空则运行时直接用标题原文,不纳入受管)
for (const group of props.groups || []) {
const key = String(group.b_i18n || '').trim()
@@ -236,18 +229,13 @@ function batchDelete() {
if (activeKey.value && delIds.has(String(activeKey.value))) activeKey.value = null
}
const filteredRows = computed(() => {
const kw = String(searchKey.value || '').trim().toLowerCase()
if (!kw) return localRows.value
return localRows.value.filter((row) => String(row.b_key || '').toLowerCase().includes(kw))
})
const columns = computed(() => {
const cols = [
{
dataIndex: 'b_key',
title: '多语言键',
width: 260,
sorter: true,
editor: { type: 'input', disabled: (row) => isManagedRow(row) },
},
]
@@ -256,6 +244,8 @@ const columns = computed(() => {
dataIndex: lang.code,
title: lang.name,
width: 200,
// 默认语言列(简体中文)开启排序
sorter: lang.code === defaultLocale.value,
editor: { type: 'input' },
})
}
@@ -344,9 +334,6 @@ defineExpose({ buildSaveRequest, applyInsertedIds, removeByManagedIds })
<template>
<div class="mm-i18n">
<div class="mm-panel__toolbar">
<div class="mm-i18n__search">
<Input v-model="searchKey" placeholder="按多语言键过滤" allow-clear />
</div>
<Button type="primary" :disabled="loading" @click="addRow"><Plus :size="14" /> 新增多语言</Button>
<Button type="danger" :disabled="loading || !selectedRowKeys.length" @click="batchDelete">
<Trash2 :size="14" /> 批量删除{{ selectedRowKeys.length ? `(${selectedRowKeys.length})` : '' }}
@@ -356,7 +343,8 @@ defineExpose({ buildSaveRequest, applyInsertedIds, removeByManagedIds })
<FmsTable
ref="tableRef"
:columns="columns"
:data-source="filteredRows"
:data-source="localRows"
:sort-config="{ emptyToBottom: true, stringLocaleCompare: true }"
:editable="!loading"
:row-selection="rowSelection"
row-key="_id"
@@ -382,10 +370,6 @@ defineExpose({ buildSaveRequest, applyInsertedIds, removeByManagedIds })
padding: 0 0 8px;
}
.mm-i18n__search {
width: 300px;
}
.mm-panel__body {
display: flex;
flex-direction: column;
@@ -1,172 +0,0 @@
<script setup>
/**
* 权限配置面板(s_power):模块操作权限点(b_power_type=action)。
* i18n 键按 power.{moduleCode}.{powerCode} 自动同步。
*/
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
import { Plus, Trash2 } from '@lucide/vue'
import { Message, Button } from '@/components/ui'
import FmsTable from '@/components/fms-table/FmsTable.vue'
import { syncModuleItemI18nKeys } from '@/utils/i18n'
import { createTempId } from '@/utils/tempId'
const props = defineProps({
moduleId: { type: String, required: true },
moduleCode: { type: String, default: '' },
powers: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
/** 权限点类型:模块操作 action / 菜单访问 menu */
powerType: { type: String, default: 'action' },
/** 资源编码(b_resource):默认取 moduleCode */
resource: { type: String, default: '' },
/** i18n 键前缀:power / menu */
i18nPrefix: { type: String, default: 'power' },
})
const emit = defineEmits(['update'])
const localPowers = ref([])
const activeKey = ref(null)
const tableRef = ref(null)
watch(
() => [props.powers, props.moduleCode],
([val, moduleCode]) => {
if (val === localPowers.value) return
const rows = val?.map((row) => ({ ...row })) || []
const changed = syncModuleItemI18nKeys(rows, props.i18nPrefix, moduleCode, 'b_code')
localPowers.value = rows
if (changed) emit('update', rows)
},
{ immediate: true },
)
async function addRow() {
if (props.loading) return
const newId = createTempId()
const newRow = {
b_id: newId,
b_module_id: props.moduleId,
b_code: '',
b_name: '',
b_i18n: null,
b_power_type: props.powerType,
b_resource: props.resource || props.moduleCode || null,
b_operation: props.powerType === 'menu' ? 'access' : 'execute',
b_xh: (localPowers.value.length + 1) * 10,
b_canuse: 1,
}
// stk-table 对同一引用数组的 push 不敏感(内部 shallowRef + 引用 watch),必须换新引用才会渲染新行
localPowers.value = [...localPowers.value, newRow]
emit('update', localPowers.value)
activeKey.value = String(newId)
await nextTick()
tableRef.value?.editRow?.(String(newId), 'b_code')
}
function deleteRow(record) {
if (props.loading) return
localPowers.value = localPowers.value.filter((r) => r.b_id !== record.b_id)
selectedRowKeys.value = selectedRowKeys.value.filter((x) => x !== String(record.b_id))
emit('update', localPowers.value)
}
const selectedRowKeys = ref([])
const rowSelection = computed(() => ({
selectedRowKeys: selectedRowKeys.value,
onChange: (keys) => {
selectedRowKeys.value = keys
},
}))
function batchDelete() {
if (props.loading) return
const keys = selectedRowKeys.value
if (!keys.length) return
const delSet = new Set(keys)
localPowers.value = localPowers.value.filter((r) => !delSet.has(String(r.b_id)))
selectedRowKeys.value = []
if (activeKey.value && delSet.has(activeKey.value)) activeKey.value = ''
emit('update', localPowers.value)
Message.success(`已删除 ${keys.length} 条权限`)
}
function onCellChange() {
syncModuleItemI18nKeys(localPowers.value, props.i18nPrefix, props.moduleCode, 'b_code')
emit('update', localPowers.value)
}
const columns = shallowRef([
{ dataIndex: 'b_code', title: '操作编码', width: 160, editor: { type: 'input' } },
{ dataIndex: 'b_name', title: '操作名称', width: 160, editor: { type: 'input' } },
{ dataIndex: 'b_i18n', title: '多语言键', width: 200 },
{ dataIndex: 'b_xh', title: '排序', width: 90, editor: { type: 'input', inputType: 'number' } },
{
dataIndex: 'b_canuse',
title: '启用',
width: 70,
align: 'center',
editor: { type: 'checkbox' },
},
{
dataIndex: 'actions',
title: '操作',
width: 70,
actions: [
{
type: 'delete',
label: '删除',
onClick: (row) => deleteRow(row),
},
],
},
])
</script>
<template>
<div class="mm-panel">
<div class="mm-panel__toolbar">
<Button type="primary" :disabled="loading" @click="addRow"><Plus :size="14" /> 新增权限</Button>
<Button type="danger" :disabled="loading || !selectedRowKeys.length" @click="batchDelete">
<Trash2 :size="14" /> 批量删除{{ selectedRowKeys.length ? `(${selectedRowKeys.length})` : '' }}
</Button>
</div>
<div class="mm-panel__body">
<FmsTable
ref="tableRef"
v-model:columns="columns"
:data-source="localPowers"
:editable="!loading"
row-key="b_id"
:active-row-key="activeKey"
:row-selection="rowSelection"
@row-click="(_, row) => (activeKey = String(row.b_id))"
@change="onCellChange"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.mm-panel {
display: flex;
flex-direction: column;
min-height: 0;
flex: 1;
}
.mm-panel__toolbar {
display: flex;
flex: none;
flex-wrap: wrap;
gap: 8px;
padding: 0 0 8px;
}
.mm-panel__body {
display: flex;
flex-direction: column;
min-height: 0;
flex: 1;
}
</style>
@@ -266,7 +266,8 @@ const columns = shallowRef([
</template>
<style lang="scss" scoped>
/* 双栏布局:左字段库(FieldLibrary 自带右侧分割线与内边距)+ 右主区 */
/* 双栏布局:左字段库(FieldLibrary 自带右侧分割线与内边距)+ 右主区;
外框与列表配置(TableDesignEditor .mm-edit)一致 */
.mm-qdesign {
display: grid;
grid-template-columns: 184px minmax(0, 1fr);
@@ -274,6 +275,9 @@ const columns = shallowRef([
min-height: 0;
min-width: 0;
background: var(--fms-card);
border: 1px solid var(--fms-border);
border-radius: var(--fms-control-radius);
overflow: hidden;
}
.mm-qdesign__main {
@@ -1,7 +1,7 @@
<script setup>
/**
* 模块管理:模块配置(单分区)。
* 模块配置:左侧模块树 + 右侧分节面板(基础/字段/列表/编辑/查询/自动编码/权限/多语言)。
* 模块配置:左侧模块树 + 右侧分节面板(基础/字段/列表/编辑/查询/自动编码/多语言)。
* 数据直接经通用 API(loadDataApi / saveObjectApi / nextIdApi / describeApi)读写元数据表。
* 模块树 / 草稿加载与脏检查 / 保存编排逻辑直接内联在下方;确认弹窗状态机仅本页使用。
* 多语言面板(i18n)数据由 ModuleI18nPanel 自持 —— 本文件只加载原始扁平行(rawI18nRows)传入,
@@ -22,7 +22,6 @@ import {
List,
Pencil,
Hash,
Shield,
Globe,
Info,
FolderTree,
@@ -61,7 +60,6 @@ import ModuleListConfigPanel from './ModuleListConfigPanel.vue'
import ModuleEditPanel from './ModuleEditPanel.vue'
import ModuleQueryConfigPanel from './ModuleQueryConfigPanel.vue'
import ModuleAutoCodePanel from './ModuleAutoCodePanel.vue'
import ModulePowersPanel from './ModulePowersPanel.vue'
import ModuleI18nPanel from './ModuleI18nPanel.vue'
/* ================= 共享状态(index.vue 组装后传入各 composable) ================= */
@@ -138,20 +136,14 @@ const configGroups = computed(() => {
uiItems.push({ key: 'query', label: '查询配置', icon: Search })
groups.push({ key: 'display', label: '界面配置', items: uiItems })
const capabilityItems = []
// 业务能力:仅数据模块有自动编码
if (isDataModule.value) {
capabilityItems.push({
key: 'auto-code',
label: '自动编码',
icon: Hash,
groups.push({
key: 'capability',
label: '业务能力',
items: [{ key: 'auto-code', label: '自动编码', icon: Hash }],
})
}
capabilityItems.push({
key: 'powers',
label: '权限',
icon: Shield,
})
groups.push({ key: 'capability', label: '业务能力', items: capabilityItems })
}
if (moduleType.value && moduleType.value !== 'category') {
@@ -212,7 +204,6 @@ async function saveModule() {
if (!moduleData) return false
const moduleCode = String(moduleData.b_code || '').trim()
syncModuleItemI18nKeys(drafts.value.fields, 'field', moduleCode, 'b_field')
syncModuleItemI18nKeys(drafts.value.powers, 'power', moduleCode, 'b_code')
moduleData.b_i18n = buildModuleI18nKey(moduleData.b_module_type, moduleCode)
const isNew = unsavedModuleIds.has(moduleId)
@@ -230,7 +221,6 @@ async function saveModule() {
['s_field_edit', 'editConfig'],
['s_field_query', 'queryConfig'],
['s_autocode', 'autoCode'],
['s_power', 'powers'],
]
// 收集字段/分组的临时 id → 真实 id 映射,用于跨表外键重映射
const fieldIdMap = new Map()
@@ -459,7 +449,6 @@ async function addModule(parentId, moduleType) {
editConfig: [],
queryConfig: [],
autoCode: [],
powers: [],
})
rawI18nRows.value = []
originals.value = cloneData(drafts.value)
@@ -523,13 +512,110 @@ async function deleteModule(id) {
})
}
/* ================= 排序(拖拽) ================= */
// 在树中查找节点所在的父数组及索引
function findNodePosition(nodes, id) {
for (let i = 0; i < nodes.length; i++) {
if (String(nodes[i].b_id) === String(id)) return { list: nodes, index: i }
if (nodes[i].children) {
const found = findNodePosition(nodes[i].children, id)
if (found) return found
}
}
return null
}
// 重排某组兄弟的 b_xh(10 起步、步长 10),收集变化行
function renumberSiblings(list, updatedRows) {
list.forEach((s, i) => {
const nextXh = (i + 1) * 10
if (s.b_xh !== nextXh) {
s.b_xh = nextXh
updatedRows.push({ b_id: s.b_id, b_xh: nextXh })
}
})
}
async function onModuleDrop({ dragKey, dropKey, dropPosition }) {
if (moduleKeyword.value.trim()) return // 过滤状态下禁止拖拽
const dragId = String(dragKey)
const dropId = String(dropKey)
if (dragId === dropId) return
const dragPos = findNodePosition(moduleTreeData.value, dragId)
const dropPos = findNodePosition(moduleTreeData.value, dropId)
if (!dragPos || !dropPos) return
// 先记录落点节点信息,再从原位置移除被拖节点
const dropNode = dropPos.list[dropPos.index]
const dropParentId = dropNode.b_parent_id ?? null
const [moved] = dragPos.list.splice(dragPos.index, 1)
const updatedRows = []
const renumberSets = new Set()
if (dropPosition === 'inside') {
// 成为落点节点的子节点
moved.b_parent_id = dropNode.b_id
if (!dropNode.children) {
dropNode.children = []
dropNode.isLeaf = false
}
dropNode.children.push(moved)
renumberSets.add(dropNode.children)
renumberSets.add(dragPos.list)
} else {
// 同级插入:落点节点前 / 后
moved.b_parent_id = dropParentId
let targetList = dropPos.list
let targetIndex = dropPos.index + (dropPosition === 'after' ? 1 : 0)
// 同一组内拖拽且被拖节点在落点之前:移除后索引需前移一位
if (dragPos.list === dropPos.list && dragPos.index < dropPos.index) {
targetIndex -= 1
}
targetList.splice(targetIndex, 0, moved)
renumberSets.add(targetList)
}
renumberSets.forEach((list) => renumberSiblings(list, updatedRows))
// 若当前编辑的节点排序号/父级变化,同步到草稿(未保存节点落库时随 inserts 一起生效)
if (drafts.value?.module) {
const draftRow = updatedRows.find((r) => String(r.b_id) === String(drafts.value.module.b_id))
if (draftRow) drafts.value.module.b_xh = draftRow.b_xh
if (String(drafts.value.module.b_id) === dragId) {
drafts.value.module.b_parent_id = moved.b_parent_id
}
}
if (!updatedRows.length) return
// 未保存草稿不能通过 updates 持久化,仅本地排序,落库后自然生效
const persistedRows = updatedRows.filter((r) => !unsavedModuleIds.has(String(r.b_id)))
if (!persistedRows.length) return
try {
const reqs = persistedRows.map((r) => ({
table: 's_module',
key_field: 'b_id',
updates: [r],
}))
await saveObjectApi(reqs)
Message.success('排序已更新')
} catch (e) {
Message.error('排序更新失败:' + (e?.message || e))
// Reload tree on failure to restore correct order
await loadModuleTree()
}
}
/* ================= 草稿加载 ================= */
async function loadModuleConfig(moduleId) {
const id = String(moduleId)
const type = findTreeNode(moduleTreeData.value, id)?.b_module_type
configLoading.value = true
try {
let moduleRes, fieldsRes, groupsRes, listRes, editRes, queryRes, autoCodeRes, powersRes, i18nRes
let moduleRes, fieldsRes, groupsRes, listRes, editRes, queryRes, autoCodeRes, i18nRes
if (type === 'data') {
const responses = await Promise.all([
loadDataApi('s_module', `b_id = ${id}`),
@@ -539,10 +625,9 @@ async function loadModuleConfig(moduleId) {
loadDataApi('s_field_edit', `b_module_id = ${id}`, 'b_xh ASC, b_id ASC'),
loadDataApi('s_field_query', `b_module_id = ${id}`, 'b_xh ASC, b_id ASC'),
loadDataApi('s_autocode', `b_module_id = ${id}`, 'b_id ASC'),
loadDataApi('s_power', `b_module_id = ${id} AND b_power_type = 'action'`, 'b_xh ASC, b_id ASC'),
loadDataApi('s_i18n', `b_module_id = ${id}`, 'b_key ASC, b_locale ASC'),
])
;[moduleRes, fieldsRes, groupsRes, listRes, editRes, queryRes, autoCodeRes, powersRes, i18nRes] = responses
;[moduleRes, fieldsRes, groupsRes, listRes, editRes, queryRes, autoCodeRes, i18nRes] = responses
} else if (type === 'query') {
const responses = await Promise.all([
loadDataApi('s_module', `b_id = ${id}`),
@@ -550,10 +635,9 @@ async function loadModuleConfig(moduleId) {
loadDataApi('s_field_group', `b_module_id = ${id}`, 'b_xh ASC, b_id ASC'),
loadDataApi('s_field_view', `b_module_id = ${id}`, 'b_xh ASC, b_id ASC'),
loadDataApi('s_field_query', `b_module_id = ${id}`, 'b_xh ASC, b_id ASC'),
loadDataApi('s_power', `b_module_id = ${id} AND b_power_type = 'action'`, 'b_xh ASC, b_id ASC'),
loadDataApi('s_i18n', `b_module_id = ${id}`, 'b_key ASC, b_locale ASC'),
])
;[moduleRes, fieldsRes, groupsRes, listRes, queryRes, powersRes, i18nRes] = responses
;[moduleRes, fieldsRes, groupsRes, listRes, queryRes, i18nRes] = responses
} else {
moduleRes = await loadDataApi('s_module', `b_id = ${id}`)
}
@@ -566,7 +650,6 @@ async function loadModuleConfig(moduleId) {
editConfig: editRes?.data || [],
queryConfig: queryRes?.data || [],
autoCode: autoCodeRes?.data || [],
powers: powersRes?.data || [],
}
originals.value = original
drafts.value = cloneData(original)
@@ -627,7 +710,6 @@ function hasUnsavedChanges() {
'editConfig',
'queryConfig',
'autoCode',
'powers',
]
return keys.some((key) => JSON.stringify(drafts.value[key]) !== JSON.stringify(originals.value[key]))
}
@@ -866,8 +948,10 @@ loadModuleTree()
@update:expanded-keys="(keys) => (moduleExpandedKeys = keys)"
:selected-keys="moduleSelectedId ? [moduleSelectedId] : []"
:context-menu="buildModuleContextMenu"
:draggable="!moduleKeyword.trim()"
@select="onModuleSelect"
@context-menu-click="onModuleContextMenuClick"
@drop="onModuleDrop"
>
<template #title="{ node }">
<span class="mm-tree-node">
@@ -1017,20 +1101,6 @@ loadModuleTree()
/>
</div>
<div
v-if="isDataModule || isQueryModule"
v-show="activeSection === 'powers'"
class="mm-config-pane"
>
<ModulePowersPanel
:module-id="String(selectedModule.b_id)"
:module-code="drafts?.module?.b_code || ''"
:powers="drafts?.powers"
:loading="busy"
@update="(v) => onModuleUpdate('powers', v)"
/>
</div>
<div
v-if="moduleType && moduleType !== 'category'"
v-show="activeSection === 'i18n'"
@@ -1041,7 +1111,6 @@ loadModuleTree()
:module-id="String(selectedModule.b_id)"
:module="drafts?.module"
:fields="drafts?.fields"
:powers="drafts?.powers"
:groups="drafts?.groups"
:raw-rows="rawI18nRows"
:loading="busy"
@@ -1120,14 +1189,20 @@ loadModuleTree()
/* Splitter 适配:
* - 面板内容各自内部滚动,避免双滚动条
* - 面板左右留 3px,两张白卡之间形成灰缝(3 + 分隔条 6 + 3 = 12px),无需边框线
* - 隐藏分隔条常显的 1px 线(hover 时的手柄仍会显示,可拖拽调宽) */
* - 常显线平时透明,hover / 拖拽时显示整条 100% 高度的细线 + 手柄提示 */
.mm-body-splitter :deep(> .fms-splitter-panel) {
overflow: hidden;
padding: 0 3px;
}
.mm-body-splitter :deep(.fms-splitter-bar::before) {
display: none;
opacity: 0;
}
.mm-body-splitter :deep(.fms-splitter-bar:hover::before),
.mm-body-splitter :deep(.fms-splitter-bar-active::before),
.mm-body-splitter :deep(.fms-splitter-bar:focus-visible::before) {
opacity: 1;
}
/* ---- 左树顶部 header:搜索框在左,操作在右,无边框无标题 ---- */