20260901222055
This commit is contained in:
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' },
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@@ -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 后自动走权限校验
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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:搜索框在左,操作在右,无边框无标题 ---- */
|
||||
|
||||
Reference in new issue
Block a user