20260826224445
This commit is contained in:
1 parent
d750e04eee
commit
60cacfa8fb
9 files changed
+3650
-2124
No files matched your search
@@ -108,9 +108,15 @@ function toggleFullscreen() {
|
||||
}
|
||||
|
||||
// 遮罩点击关闭:wrap 铺满视口且层级高于 mask,事件实际落在 wrap 上;
|
||||
// 只响应点击 wrap 自身(面板外空白/遮罩区域),点击面板内部不关闭
|
||||
// 只响应点击 wrap 自身(面板外空白/遮罩区域),点击面板内部不关闭。
|
||||
// 记录 mousedown 是否发生在面板内:若在面板内按下(如拖选输入框文本)后
|
||||
// 拖出到遮罩再松开,click.target 会回溯为共同祖先 wrap,此时不应误判为点击遮罩。
|
||||
let maskDownInPanel = false
|
||||
function onWrapMouseDown(event) {
|
||||
maskDownInPanel = !!panelRef.value?.contains(event.target)
|
||||
}
|
||||
function onWrapClick(event) {
|
||||
if (event.target === event.currentTarget && props.maskClosable) {
|
||||
if (event.target === event.currentTarget && props.maskClosable && !maskDownInPanel) {
|
||||
requestClose()
|
||||
}
|
||||
}
|
||||
@@ -208,6 +214,7 @@ onBeforeUnmount(() => {
|
||||
v-if="isOpen"
|
||||
class="fms-modal-wrap"
|
||||
:class="{ 'fms-modal-centered': centered }"
|
||||
@mousedown="onWrapMouseDown"
|
||||
@click="onWrapClick"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -26,10 +26,13 @@
|
||||
box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
/* hover:仅内容区域背景变化(箭头/缩进区不变),整行保持 pointer 提示可点 */
|
||||
.tree-node:hover:not(.is-disabled) {
|
||||
background: var(--fms-secondary-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
.tree-node:hover:not(.is-disabled) .tree-node-content {
|
||||
background: var(--fms-secondary-hover);
|
||||
}
|
||||
|
||||
/* 键盘/鼠标聚焦:focus-visible 描边,避免鼠标点击也出现描边 */
|
||||
.tree-node:focus-visible {
|
||||
@@ -37,12 +40,12 @@
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* 选中:主色淡底,明显区分于灰底页面;文字保持默认色。
|
||||
/* 选中:仅内容区域主色淡底(整行 hover 背景、箭头/缩进区不受影响)。
|
||||
比例取 5%:深色主题(如 #0f172b)在白底上约 #f3f3f4,接近 hover 浅色 #f1f5f9;
|
||||
比例过高(10%)时深色主题会算出 #e7e7e9 等偏深灰,显得脏。
|
||||
注意 hover 规则 specificity 更高,需连同 hover 一起覆盖,避免悬停时选中态被盖掉 */
|
||||
.tree-node.is-selected,
|
||||
.tree-node.is-selected:hover:not(.is-disabled) {
|
||||
.tree-node.is-selected .tree-node-content,
|
||||
.tree-node.is-selected:hover:not(.is-disabled) .tree-node-content {
|
||||
background: color-mix(in srgb, var(--fms-primary) 5%, transparent);
|
||||
}
|
||||
|
||||
@@ -78,14 +81,15 @@
|
||||
}
|
||||
.tree-node-switcher:hover:not(.is-placeholder) {
|
||||
background: color-mix(in srgb, var(--fms-primary) 10%, transparent);
|
||||
color: var(--fms-primary);
|
||||
}
|
||||
.tree-node-switcher.is-placeholder {
|
||||
cursor: default;
|
||||
}
|
||||
/* 展开/收起箭头:黑色(跟随主题文字色),hover 仅显示淡底提示、箭头不变色 */
|
||||
.tree-switcher-icon {
|
||||
width: var(--fms-tree-icon-size);
|
||||
height: var(--fms-tree-icon-size);
|
||||
color: var(--fms-text);
|
||||
transition: transform 0.2s cubic-bezier(0.2, 0.8, 0.4, 1);
|
||||
}
|
||||
.tree-switcher-icon.is-open {
|
||||
@@ -115,9 +119,11 @@
|
||||
.tree-node-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
/* 父容器 .tree-node 仅有 min-height(无显式 height),height:100% 无法解析,
|
||||
故用 flex 交叉轴拉伸占满整行实际高度(选中背景/圆角才能覆盖整行) */
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 var(--fms-tree-title-padding-x);
|
||||
border-radius: var(--fms-control-radius);
|
||||
overflow: hidden;
|
||||
@@ -160,13 +166,8 @@
|
||||
width: var(--fms-tree-grip-size);
|
||||
height: var(--fms-tree-grip-size);
|
||||
margin-right: 4px;
|
||||
color: var(--fms-text-secondary);
|
||||
color: #b0b7c3;
|
||||
cursor: grab;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.tree-node:hover .tree-node-grip {
|
||||
opacity: 1;
|
||||
}
|
||||
.tree-node-grip:active {
|
||||
cursor: grabbing;
|
||||
|
||||
@@ -1,471 +0,0 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 字段分组管理弹窗:管理 s_field_group 树形分组。
|
||||
* 简化交互:顶部输入框回车即新增(选中节点则挂到其下,未选中为顶级),
|
||||
* 树节点行内直接 启用 / 重命名 / 删除,无需右侧属性面板。
|
||||
* 独立保存到后端,保存后 emit("saved", groups, idMapping),父级用 idMapping
|
||||
* 重映射列表/编辑配置中的 b_group_id(临时 ID → 雪花 ID)。
|
||||
*/
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import Message from '@/components/ui/message/message-manager'
|
||||
import Modal from '@/components/ui/modal/modal.vue'
|
||||
import Button from '@/components/ui/button/button.vue'
|
||||
import Switch from '@/components/ui/switch/switch.vue'
|
||||
import Tree from '@/components/ui/tree/tree.vue'
|
||||
import { Plus, Pencil, Trash2, ArrowUp, ArrowDown } from '@lucide/vue'
|
||||
import { buildTree } from '@/utils/tree'
|
||||
import { createTempId } from '@/utils/tempId'
|
||||
import { cloneData, diffRows, allocateTemporaryIds, tableChange, hasChanges } from '@/utils/dataChanges'
|
||||
import { saveObjectApi } from '@/services/api'
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
moduleId: { type: String, required: true },
|
||||
groups: { type: Array, default: () => [] },
|
||||
referencedGroupIds: { type: Array, default: () => [] },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:open', 'saved'])
|
||||
|
||||
const localGroups = ref([])
|
||||
const selectedGroupId = ref(null)
|
||||
const saving = ref(false)
|
||||
|
||||
/* 顶部输入即新增 */
|
||||
const newTitle = ref('')
|
||||
const addInputRef = ref(null)
|
||||
|
||||
/* 行内重命名 */
|
||||
const editingId = ref(null)
|
||||
const renameValue = ref('')
|
||||
const renameInputRef = ref(null)
|
||||
|
||||
watch(
|
||||
() => props.groups,
|
||||
(val) => {
|
||||
localGroups.value = cloneData(val || [])
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const treeData = computed(() => buildTree(localGroups.value))
|
||||
const selectedGroup = computed(() =>
|
||||
localGroups.value.find((g) => String(g.b_id) === String(selectedGroupId.value)),
|
||||
)
|
||||
|
||||
function onTreeSelect(keys) {
|
||||
selectedGroupId.value = keys.length ? String(keys[0]) : null
|
||||
}
|
||||
|
||||
/* 新增:输入名称回车即添加;选中节点时挂到其下,否则为顶级。
|
||||
新增后不自动选中新节点,便于连续新增到同一父级下。 */
|
||||
function handleAdd() {
|
||||
const title = newTitle.value.trim()
|
||||
if (!title) return
|
||||
localGroups.value.push({
|
||||
b_id: createTempId(),
|
||||
b_module_id: props.moduleId,
|
||||
b_parent_id: selectedGroupId.value || null,
|
||||
b_title: title,
|
||||
b_xh: (localGroups.value.length + 1) * 10,
|
||||
b_canuse: 1,
|
||||
b_layout_direction: 'vertical',
|
||||
})
|
||||
newTitle.value = ''
|
||||
nextTick(() => addInputRef.value?.focus?.())
|
||||
}
|
||||
|
||||
function onAddEnter(event) {
|
||||
if (event.isComposing) return
|
||||
handleAdd()
|
||||
}
|
||||
|
||||
/* 行内重命名 */
|
||||
function startRename(node) {
|
||||
editingId.value = String(node.b_id)
|
||||
renameValue.value = node.b_title || ''
|
||||
nextTick(() => renameInputRef.value?.focus?.())
|
||||
}
|
||||
|
||||
function commitRename(node) {
|
||||
if (editingId.value !== String(node.b_id)) return
|
||||
const title = renameValue.value.trim()
|
||||
if (title) node.b_title = title
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
function cancelRename() {
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
function onRenameEnter(event, node) {
|
||||
if (event.isComposing) return
|
||||
commitRename(node)
|
||||
}
|
||||
|
||||
/* 行内启用切换 */
|
||||
function toggleEnable(node) {
|
||||
node.b_canuse = Number(node.b_canuse) === 1 ? 0 : 1
|
||||
}
|
||||
|
||||
function toggleLayout(node) {
|
||||
node.b_layout_direction = node.b_layout_direction === 'horizontal' ? 'vertical' : 'horizontal'
|
||||
}
|
||||
|
||||
/* 同级排序:上移/下移(交换同级顺序并重新编号 b_xh,随保存提交) */
|
||||
function moveGroup(node, delta) {
|
||||
if (!node) return
|
||||
const parentId = node.b_parent_id == null ? null : String(node.b_parent_id)
|
||||
const siblings = localGroups.value.filter(
|
||||
(g) => (g.b_parent_id == null ? null : String(g.b_parent_id)) === parentId,
|
||||
)
|
||||
siblings.sort((a, b) => (a.b_xh || 0) - (b.b_xh || 0))
|
||||
const idx = siblings.findIndex((g) => String(g.b_id) === String(node.b_id))
|
||||
const target = idx + delta
|
||||
if (idx < 0 || target < 0 || target >= siblings.length) return
|
||||
const [moved] = siblings.splice(idx, 1)
|
||||
siblings.splice(target, 0, moved)
|
||||
siblings.forEach((g, i) => {
|
||||
g.b_xh = (i + 1) * 10
|
||||
})
|
||||
}
|
||||
|
||||
/* 删除:有子分组 / 仍被引用时禁止 */
|
||||
function deleteGroup(node) {
|
||||
if (!node) return
|
||||
const id = String(node.b_id)
|
||||
const hasChildren = localGroups.value.some(
|
||||
(g) => String(g.b_parent_id) === id && String(g.b_id) !== id,
|
||||
)
|
||||
if (hasChildren) {
|
||||
Message.warning('该分组下还有子分组,请先删除子分组')
|
||||
return
|
||||
}
|
||||
if (props.referencedGroupIds.some((gid) => String(gid) === id)) {
|
||||
Message.warning('该分组仍被列表或编辑配置引用,请先清空引用')
|
||||
return
|
||||
}
|
||||
localGroups.value = localGroups.value.filter((g) => String(g.b_id) !== id)
|
||||
if (String(selectedGroupId.value) === id) selectedGroupId.value = null
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (saving.value) return
|
||||
const changes = diffRows(localGroups.value, cloneData(props.groups || []))
|
||||
const req = tableChange('s_field_group', changes)
|
||||
if (!hasChanges(req)) {
|
||||
Message.info('没有需要保存的变更')
|
||||
emit('update:open', false)
|
||||
emit('saved', localGroups.value, new Map())
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const mapping = await allocateTemporaryIds([req.inserts || [], req.updates || []])
|
||||
await saveObjectApi([req])
|
||||
Message.success('分组已保存')
|
||||
emit('update:open', false)
|
||||
emit('saved', localGroups.value, mapping)
|
||||
} catch (err) {
|
||||
Message.error('保存失败:' + (err?.message || err))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:open="open"
|
||||
title="分组管理"
|
||||
width="520"
|
||||
ok-text="保存"
|
||||
:confirm-loading="saving"
|
||||
:resizable="false"
|
||||
@update:open="(v) => emit('update:open', v)"
|
||||
@ok="save"
|
||||
>
|
||||
<div class="mm-groups">
|
||||
<div class="mm-groups__head">
|
||||
<div class="mm-groups__head-title">分组结构</div>
|
||||
<span class="mm-groups__head-count">共 {{ localGroups.length }} 个分组</span>
|
||||
</div>
|
||||
<div class="mm-groups__add">
|
||||
<input
|
||||
ref="addInputRef"
|
||||
class="mm-groups__input"
|
||||
:value="newTitle"
|
||||
placeholder="输入分组名称,回车或点「新增」"
|
||||
@input="(e) => (newTitle = e.target.value)"
|
||||
@keydown.enter="onAddEnter"
|
||||
@keydown.esc="newTitle = ''"
|
||||
/>
|
||||
<Button type="primary" @click="handleAdd">
|
||||
<Plus :size="14" /> 新增
|
||||
</Button>
|
||||
</div>
|
||||
<p class="mm-groups__hint">
|
||||
{{ selectedGroup ? `将添加到「${selectedGroup.b_title || '未命名分组'}」下` : '将添加为顶级分组' }}
|
||||
</p>
|
||||
|
||||
<div class="mm-groups__scroll">
|
||||
<Tree
|
||||
:tree-data="treeData"
|
||||
:field-names="{ key: 'b_id', title: 'b_title', children: 'children' }"
|
||||
default-expand-all
|
||||
:selected-keys="selectedGroupId ? [selectedGroupId] : []"
|
||||
@select="onTreeSelect"
|
||||
>
|
||||
<template #empty>暂无分组,输入名称后回车即可创建</template>
|
||||
<template #title="{ node }">
|
||||
<span class="mm-groups__node">
|
||||
<input
|
||||
v-if="editingId === String(node.b_id)"
|
||||
ref="renameInputRef"
|
||||
class="mm-groups__rename"
|
||||
:value="renameValue"
|
||||
@click.stop
|
||||
@input="(e) => (renameValue = e.target.value)"
|
||||
@keydown.enter.stop.prevent="(e) => onRenameEnter(e, node)"
|
||||
@keydown.esc.stop.prevent="cancelRename"
|
||||
@blur="commitRename(node)"
|
||||
/>
|
||||
<template v-else>
|
||||
<span
|
||||
class="mm-groups__node-name"
|
||||
:class="{ 'is-disabled': Number(node.b_canuse) !== 1 }"
|
||||
>
|
||||
{{ node.b_title || '未命名分组' }}
|
||||
</span>
|
||||
<span class="mm-groups__node-actions" @click.stop>
|
||||
<button
|
||||
type="button"
|
||||
class="mm-groups__node-btn"
|
||||
title="上移"
|
||||
aria-label="上移分组"
|
||||
@click="moveGroup(node, -1)"
|
||||
>
|
||||
<ArrowUp :size="13" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="mm-groups__node-btn"
|
||||
title="下移"
|
||||
aria-label="下移分组"
|
||||
@click="moveGroup(node, 1)"
|
||||
>
|
||||
<ArrowDown :size="13" />
|
||||
</button>
|
||||
<span class="mm-groups__node-divider"></span>
|
||||
<Switch
|
||||
:model-value="Number(node.b_canuse) === 1"
|
||||
title="启用/停用"
|
||||
aria-label="启用/停用"
|
||||
@change="() => toggleEnable(node)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="mm-groups__node-btn"
|
||||
title="重命名"
|
||||
aria-label="重命名"
|
||||
@click="startRename(node)"
|
||||
>
|
||||
<Pencil :size="13" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="mm-groups__node-btn"
|
||||
:title="node.b_layout_direction === 'horizontal' ? '切换为上下布局' : '切换为左右布局'"
|
||||
:aria-label="node.b_layout_direction === 'horizontal' ? '切换为上下布局' : '切换为左右布局'"
|
||||
@click="toggleLayout(node)"
|
||||
>
|
||||
{{ node.b_layout_direction === 'horizontal' ? '左右' : '上下' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="mm-groups__node-btn is-danger"
|
||||
title="删除分组"
|
||||
aria-label="删除分组"
|
||||
@click="deleteGroup(node)"
|
||||
>
|
||||
<Trash2 :size="13" />
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
</Tree>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mm-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
height: 400px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ---- 顶部摘要 ---- */
|
||||
.mm-groups__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: none;
|
||||
padding: 2px 2px 0;
|
||||
}
|
||||
|
||||
.mm-groups__head-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--fms-text);
|
||||
}
|
||||
|
||||
.mm-groups__head-count {
|
||||
font-size: 12px;
|
||||
color: var(--fms-text-secondary);
|
||||
}
|
||||
|
||||
/* ---- 顶部输入即新增 ---- */
|
||||
.mm-groups__add {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.mm-groups__input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: var(--fms-control-height);
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: var(--fms-control-radius);
|
||||
background: var(--fms-card);
|
||||
color: var(--fms-text);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--fms-primary);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--fms-primary) 12%, transparent);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--fms-text-placeholder, #9ca3af);
|
||||
}
|
||||
}
|
||||
|
||||
.mm-groups__hint {
|
||||
flex: none;
|
||||
margin: 0;
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ---- 树 ---- */
|
||||
.mm-groups__scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: var(--fms-control-radius);
|
||||
background: var(--fms-secondary-hover);
|
||||
}
|
||||
|
||||
/* 节点行:名称撑满,操作靠右;hover 时显示完整操作 */
|
||||
.mm-groups__node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 3px 6px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: var(--fms-card);
|
||||
}
|
||||
}
|
||||
|
||||
.mm-groups__node-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
|
||||
&.is-disabled {
|
||||
color: var(--fms-text-disabled, #9ca3af);
|
||||
}
|
||||
}
|
||||
|
||||
.mm-groups__node-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* 移动按钮与其它操作的分隔线 */
|
||||
.mm-groups__node-divider {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
margin: 0 2px;
|
||||
background: var(--fms-border);
|
||||
}
|
||||
|
||||
.mm-groups__node-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: var(--fms-text-secondary);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s,
|
||||
border-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: var(--fms-secondary);
|
||||
color: var(--fms-text);
|
||||
border-color: var(--fms-border);
|
||||
}
|
||||
|
||||
&.is-danger:hover {
|
||||
background: #fdecec;
|
||||
color: #d95757;
|
||||
border-color: #f5c6c6;
|
||||
}
|
||||
}
|
||||
|
||||
/* 行内重命名输入框 */
|
||||
.mm-groups__rename {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
padding: 0 6px;
|
||||
border: 1px solid var(--fms-primary);
|
||||
border-radius: 4px;
|
||||
background: var(--fms-card);
|
||||
color: var(--fms-text);
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--fms-primary) 12%, transparent);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -23,7 +23,7 @@ const ICON_NAMES = [
|
||||
'CalendarDays', 'CalendarClock', 'Clock', 'Timer', 'History', 'AlarmClock',
|
||||
'Search', 'Filter', 'Eye', 'EyeOff', 'Lock', 'Unlock', 'Key', 'KeyRound',
|
||||
'Plus', 'Minus', 'Pencil', 'Trash2', 'Copy', 'Save', 'Upload', 'Download',
|
||||
'RefreshCw', 'RotateCw', 'Undo2', 'Redo2', 'ExternalLink', 'Link', 'Share2',
|
||||
'RefreshCw', 'RotateCw', 'ExternalLink', 'Link', 'Share2',
|
||||
'Printer', 'Mail', 'Phone', 'MapPin', 'Globe', 'Globe2', 'Languages', 'Translate',
|
||||
'BookOpen', 'BookMarked', 'GraduationCap', 'Award', 'Star', 'Heart', 'Flag',
|
||||
'Check', 'CheckCircle2', 'XCircle', 'AlertCircle', 'AlertTriangle', 'Info',
|
||||
|
||||
+252
-1594
File diff suppressed because it is too large.
Load diff
+1
-14
@@ -6,7 +6,6 @@ import { computed, ref, shallowRef, watch } from 'vue'
|
||||
import Message from '@/components/ui/message/message-manager'
|
||||
import Button from '@/components/ui/button/button.vue'
|
||||
import FmsTable from '@/components/fms-table/FmsTable.vue'
|
||||
import FieldGroupManager from './FieldGroupManager.vue'
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
@@ -15,14 +14,12 @@ const props = defineProps({
|
||||
queryConfig: { type: Array, default: () => [] },
|
||||
fields: { type: Array, default: () => [] },
|
||||
groups: { type: Array, default: () => [] },
|
||||
referencedGroupIds: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update', 'groupsSaved', 'syncEdit', 'syncQuery'])
|
||||
const emit = defineEmits(['update', 'syncEdit', 'syncQuery'])
|
||||
|
||||
const localList = ref([])
|
||||
const groupManagerVisible = ref(false)
|
||||
const activeKey = ref(null)
|
||||
|
||||
watch(
|
||||
@@ -139,9 +136,6 @@ const columns = shallowRef([
|
||||
<template>
|
||||
<div class="mm-panel">
|
||||
<div class="mm-panel__toolbar">
|
||||
<Button type="outline" :disabled="loading" @click="groupManagerVisible = true">
|
||||
分组管理
|
||||
</Button>
|
||||
<Button type="outline" :disabled="loading || !editConfig.length" @click="syncToEdit">
|
||||
同步到编辑显示
|
||||
</Button>
|
||||
@@ -162,13 +156,6 @@ const columns = shallowRef([
|
||||
@row-order-change="onRowOrderChange"
|
||||
/>
|
||||
</div>
|
||||
<FieldGroupManager
|
||||
v-model:open="groupManagerVisible"
|
||||
:module-id="moduleId"
|
||||
:groups="groups"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
@saved="(groups, idMapping) => emit('groupsSaved', groups, idMapping)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
+3325
File diff suppressed because it is too large.
Load diff
@@ -48,7 +48,8 @@ import { buildModuleI18nKey, buildModuleItemI18nKey, syncModuleItemI18nKeys } fr
|
||||
import ModuleBasicPanel from './components/ModuleBasicPanel.vue'
|
||||
import ModuleFieldsPanel from './components/ModuleFieldsPanel.vue'
|
||||
import ModuleListConfigPanel from './components/ModuleListConfigPanel.vue'
|
||||
import ModuleEditConfigPanel from './components/ModuleEditConfigPanel.vue'
|
||||
import ModuleFormEditPanel from './components/ModuleFormEditPanel.vue'
|
||||
import ModuleTableEditPanel from './components/ModuleTableEditPanel.vue'
|
||||
import ModuleQueryConfigPanel from './components/ModuleQueryConfigPanel.vue'
|
||||
import ModuleAutoCodePanel from './components/ModuleAutoCodePanel.vue'
|
||||
import ModulePowersPanel from './components/ModulePowersPanel.vue'
|
||||
@@ -629,16 +630,6 @@ function onAutoCodeUpdate(rows) {
|
||||
}
|
||||
}
|
||||
|
||||
function onGroupsSaved(newGroups, idMapping) {
|
||||
if (!drafts.value) return
|
||||
if (idMapping && idMapping.size) {
|
||||
remapForeignKeys(drafts.value.listConfig || [], ['b_group_id'], idMapping)
|
||||
remapForeignKeys(drafts.value.editConfig || [], ['b_group_id'], idMapping)
|
||||
}
|
||||
drafts.value.groups = cloneData(newGroups)
|
||||
originals.value.groups = cloneData(newGroups)
|
||||
}
|
||||
|
||||
function onModuleI18nSaved() {
|
||||
i18nPanelRef.value?.refreshKey?.()
|
||||
}
|
||||
@@ -679,7 +670,7 @@ function onModuleNewClick({ key }) {
|
||||
|
||||
/* ================= 右侧分组配置导航 ================= */
|
||||
// 配置导航分组:概览 / 数据结构 / 界面配置 / 业务能力 / 高级设置。
|
||||
// 每个导航项唯一激活(activeSection);「分组管理」点击时同时打开分组管理弹窗。
|
||||
// 每个导航项唯一激活(activeSection)。
|
||||
const configGroups = computed(() => {
|
||||
const groups = []
|
||||
groups.push({
|
||||
@@ -753,17 +744,6 @@ const configNavItems = computed(() =>
|
||||
configGroups.value.flatMap((group) => group.items.map((item) => ({ ...item }))),
|
||||
)
|
||||
|
||||
const referencedGroupIds = computed(() => {
|
||||
const ids = new Set()
|
||||
for (const row of drafts.value?.listConfig || []) {
|
||||
if (row.b_group_id != null) ids.add(String(row.b_group_id))
|
||||
}
|
||||
for (const row of drafts.value?.editConfig || []) {
|
||||
if (row.b_group_id != null) ids.add(String(row.b_group_id))
|
||||
}
|
||||
return Array.from(ids)
|
||||
})
|
||||
|
||||
// 编辑方式(form 主表表单 / table 子表表格)决定「表单设计」面板的形态与导航标签
|
||||
const editMode = computed(() => drafts.value?.module?.b_edit_mode || 'form')
|
||||
const editNavLabel = computed(() => (editMode.value === 'table' ? '表格设计' : '表单设计'))
|
||||
@@ -915,6 +895,9 @@ async function saveModule() {
|
||||
['s_autocode', 'autoCode'],
|
||||
['s_power', 'powers'],
|
||||
]
|
||||
// 收集字段/分组的临时 id → 真实 id 映射,用于跨表外键重映射
|
||||
const fieldIdMap = new Map()
|
||||
const groupIdMap = new Map()
|
||||
for (const [table, section] of subTables) {
|
||||
const current = drafts.value[section] || []
|
||||
const previous = originals.value?.[section] || []
|
||||
@@ -927,11 +910,25 @@ async function saveModule() {
|
||||
}
|
||||
const req = tableChange(table, changes)
|
||||
if (hasChanges(req)) {
|
||||
await allocateTemporaryIds([req.inserts || [], req.updates || []])
|
||||
const mapping = await allocateTemporaryIds([req.inserts || [], req.updates || []])
|
||||
if (table === 's_field') mapping.forEach((v, k) => fieldIdMap.set(k, v))
|
||||
if (table === 's_field_group') mapping.forEach((v, k) => groupIdMap.set(k, v))
|
||||
saveReqs.push(req)
|
||||
}
|
||||
}
|
||||
|
||||
// 统一重映射跨表外键:列表/编辑/查询配置中引用字段、分组的临时 id 换成真实 id
|
||||
if (fieldIdMap.size) {
|
||||
for (const section of ['listConfig', 'editConfig', 'queryConfig']) {
|
||||
remapForeignKeys(drafts.value[section] || [], ['b_field_id'], fieldIdMap)
|
||||
}
|
||||
}
|
||||
if (groupIdMap.size) {
|
||||
for (const section of ['listConfig', 'editConfig', 'queryConfig']) {
|
||||
remapForeignKeys(drafts.value[section] || [], ['b_group_id'], groupIdMap)
|
||||
}
|
||||
}
|
||||
|
||||
const i18nPanel = i18nPanelRef.value
|
||||
if (i18nPanel && typeof i18nPanel.reconcileManagedRows === 'function') {
|
||||
i18nPanel.reconcileManagedRows(finalI18nItems.value)
|
||||
@@ -1098,24 +1095,41 @@ loadModuleTree()
|
||||
:query-config="drafts?.queryConfig"
|
||||
:fields="drafts?.fields"
|
||||
:groups="drafts?.groups"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
:loading="busy"
|
||||
@update="(v) => onModuleUpdate('listConfig', v)"
|
||||
@groups-saved="onGroupsSaved"
|
||||
@sync-edit="(v) => onModuleUpdate('editConfig', v)"
|
||||
@sync-query="(v) => onModuleUpdate('queryConfig', v)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isDataModule" v-show="activeSection === 'edit'" class="mm-config-pane">
|
||||
<ModuleEditConfigPanel
|
||||
<ModuleTableEditPanel
|
||||
v-if="editMode === 'table'"
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:edit-config="drafts?.editConfig"
|
||||
:list-config="drafts?.listConfig"
|
||||
:query-config="drafts?.queryConfig"
|
||||
:fields="drafts?.fields"
|
||||
:groups="drafts?.groups"
|
||||
:edit-mode="editMode"
|
||||
:active="activeSection === 'edit'"
|
||||
:loading="busy"
|
||||
@update="(v) => onModuleUpdate('editConfig', v)"
|
||||
@update:edit-mode="
|
||||
(v) => onModuleUpdate('module', { ...(drafts?.module || {}), b_edit_mode: v })
|
||||
"
|
||||
@update-groups="(v) => onModuleUpdate('groups', v)"
|
||||
@sync-list="(v) => onModuleUpdate('listConfig', v)"
|
||||
@sync-query="(v) => onModuleUpdate('queryConfig', v)"
|
||||
/>
|
||||
<ModuleFormEditPanel
|
||||
v-else
|
||||
: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"
|
||||
:edit-mode="editMode"
|
||||
:active="activeSection === 'edit'"
|
||||
:loading="busy"
|
||||
@@ -1124,7 +1138,6 @@ loadModuleTree()
|
||||
(v) => onModuleUpdate('module', { ...(drafts?.module || {}), b_edit_mode: v })
|
||||
"
|
||||
@update-groups="(v) => onModuleUpdate('groups', v)"
|
||||
@groups-saved="onGroupsSaved"
|
||||
@sync-list="(v) => onModuleUpdate('listConfig', v)"
|
||||
@sync-query="(v) => onModuleUpdate('queryConfig', v)"
|
||||
/>
|
||||
@@ -1419,11 +1432,16 @@ loadModuleTree()
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ---- 配置导航:横向 Tab 导航(line 类型,不作内部覆盖,组件默认样式即可) ---- */
|
||||
/* ---- 配置导航:横向 Tab 导航(line 类型) ---- */
|
||||
.mm-config-nav.tabs {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* 页签高度(42px):仅作用于本页 tab 项,不改变组件内部样式,也不影响按钮 */
|
||||
.mm-config-nav.tabs :deep(.tabs-tab) {
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
/* 标签内布局:图标 + 文字 + badge */
|
||||
.mm-config-nav__icon {
|
||||
flex: none;
|
||||
|
||||
Reference in new issue
Block a user