20260826224445
This commit is contained in:
1 parent
d750e04eee
commit
60cacfa8fb
9 files changed
+3609
-2083
No files matched your search
@@ -13,6 +13,7 @@ dist-ssr
|
|||||||
dist-verify
|
dist-verify
|
||||||
dist-check
|
dist-check
|
||||||
dist-check2
|
dist-check2
|
||||||
|
dist-check3
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
|
|||||||
@@ -108,9 +108,15 @@ function toggleFullscreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 遮罩点击关闭:wrap 铺满视口且层级高于 mask,事件实际落在 wrap 上;
|
// 遮罩点击关闭:wrap 铺满视口且层级高于 mask,事件实际落在 wrap 上;
|
||||||
// 只响应点击 wrap 自身(面板外空白/遮罩区域),点击面板内部不关闭
|
// 只响应点击 wrap 自身(面板外空白/遮罩区域),点击面板内部不关闭。
|
||||||
|
// 记录 mousedown 是否发生在面板内:若在面板内按下(如拖选输入框文本)后
|
||||||
|
// 拖出到遮罩再松开,click.target 会回溯为共同祖先 wrap,此时不应误判为点击遮罩。
|
||||||
|
let maskDownInPanel = false
|
||||||
|
function onWrapMouseDown(event) {
|
||||||
|
maskDownInPanel = !!panelRef.value?.contains(event.target)
|
||||||
|
}
|
||||||
function onWrapClick(event) {
|
function onWrapClick(event) {
|
||||||
if (event.target === event.currentTarget && props.maskClosable) {
|
if (event.target === event.currentTarget && props.maskClosable && !maskDownInPanel) {
|
||||||
requestClose()
|
requestClose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,6 +214,7 @@ onBeforeUnmount(() => {
|
|||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
class="fms-modal-wrap"
|
class="fms-modal-wrap"
|
||||||
:class="{ 'fms-modal-centered': centered }"
|
:class="{ 'fms-modal-centered': centered }"
|
||||||
|
@mousedown="onWrapMouseDown"
|
||||||
@click="onWrapClick"
|
@click="onWrapClick"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -26,10 +26,13 @@
|
|||||||
box-shadow 0.15s ease;
|
box-shadow 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* hover:仅内容区域背景变化(箭头/缩进区不变),整行保持 pointer 提示可点 */
|
||||||
.tree-node:hover:not(.is-disabled) {
|
.tree-node:hover:not(.is-disabled) {
|
||||||
background: var(--fms-secondary-hover);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.tree-node:hover:not(.is-disabled) .tree-node-content {
|
||||||
|
background: var(--fms-secondary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
/* 键盘/鼠标聚焦:focus-visible 描边,避免鼠标点击也出现描边 */
|
/* 键盘/鼠标聚焦:focus-visible 描边,避免鼠标点击也出现描边 */
|
||||||
.tree-node:focus-visible {
|
.tree-node:focus-visible {
|
||||||
@@ -37,12 +40,12 @@
|
|||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 选中:主色淡底,明显区分于灰底页面;文字保持默认色。
|
/* 选中:仅内容区域主色淡底(整行 hover 背景、箭头/缩进区不受影响)。
|
||||||
比例取 5%:深色主题(如 #0f172b)在白底上约 #f3f3f4,接近 hover 浅色 #f1f5f9;
|
比例取 5%:深色主题(如 #0f172b)在白底上约 #f3f3f4,接近 hover 浅色 #f1f5f9;
|
||||||
比例过高(10%)时深色主题会算出 #e7e7e9 等偏深灰,显得脏。
|
比例过高(10%)时深色主题会算出 #e7e7e9 等偏深灰,显得脏。
|
||||||
注意 hover 规则 specificity 更高,需连同 hover 一起覆盖,避免悬停时选中态被盖掉 */
|
注意 hover 规则 specificity 更高,需连同 hover 一起覆盖,避免悬停时选中态被盖掉 */
|
||||||
.tree-node.is-selected,
|
.tree-node.is-selected .tree-node-content,
|
||||||
.tree-node.is-selected:hover:not(.is-disabled) {
|
.tree-node.is-selected:hover:not(.is-disabled) .tree-node-content {
|
||||||
background: color-mix(in srgb, var(--fms-primary) 5%, transparent);
|
background: color-mix(in srgb, var(--fms-primary) 5%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,14 +81,15 @@
|
|||||||
}
|
}
|
||||||
.tree-node-switcher:hover:not(.is-placeholder) {
|
.tree-node-switcher:hover:not(.is-placeholder) {
|
||||||
background: color-mix(in srgb, var(--fms-primary) 10%, transparent);
|
background: color-mix(in srgb, var(--fms-primary) 10%, transparent);
|
||||||
color: var(--fms-primary);
|
|
||||||
}
|
}
|
||||||
.tree-node-switcher.is-placeholder {
|
.tree-node-switcher.is-placeholder {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
/* 展开/收起箭头:黑色(跟随主题文字色),hover 仅显示淡底提示、箭头不变色 */
|
||||||
.tree-switcher-icon {
|
.tree-switcher-icon {
|
||||||
width: var(--fms-tree-icon-size);
|
width: var(--fms-tree-icon-size);
|
||||||
height: 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);
|
transition: transform 0.2s cubic-bezier(0.2, 0.8, 0.4, 1);
|
||||||
}
|
}
|
||||||
.tree-switcher-icon.is-open {
|
.tree-switcher-icon.is-open {
|
||||||
@@ -115,9 +119,11 @@
|
|||||||
.tree-node-content {
|
.tree-node-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
/* 父容器 .tree-node 仅有 min-height(无显式 height),height:100% 无法解析,
|
||||||
|
故用 flex 交叉轴拉伸占满整行实际高度(选中背景/圆角才能覆盖整行) */
|
||||||
|
align-self: stretch;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
|
||||||
padding: 0 var(--fms-tree-title-padding-x);
|
padding: 0 var(--fms-tree-title-padding-x);
|
||||||
border-radius: var(--fms-control-radius);
|
border-radius: var(--fms-control-radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -160,13 +166,8 @@
|
|||||||
width: var(--fms-tree-grip-size);
|
width: var(--fms-tree-grip-size);
|
||||||
height: var(--fms-tree-grip-size);
|
height: var(--fms-tree-grip-size);
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
color: var(--fms-text-secondary);
|
color: #b0b7c3;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
}
|
|
||||||
.tree-node:hover .tree-node-grip {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
.tree-node-grip:active {
|
.tree-node-grip:active {
|
||||||
cursor: grabbing;
|
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',
|
'CalendarDays', 'CalendarClock', 'Clock', 'Timer', 'History', 'AlarmClock',
|
||||||
'Search', 'Filter', 'Eye', 'EyeOff', 'Lock', 'Unlock', 'Key', 'KeyRound',
|
'Search', 'Filter', 'Eye', 'EyeOff', 'Lock', 'Unlock', 'Key', 'KeyRound',
|
||||||
'Plus', 'Minus', 'Pencil', 'Trash2', 'Copy', 'Save', 'Upload', 'Download',
|
'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',
|
'Printer', 'Mail', 'Phone', 'MapPin', 'Globe', 'Globe2', 'Languages', 'Translate',
|
||||||
'BookOpen', 'BookMarked', 'GraduationCap', 'Award', 'Star', 'Heart', 'Flag',
|
'BookOpen', 'BookMarked', 'GraduationCap', 'Award', 'Star', 'Heart', 'Flag',
|
||||||
'Check', 'CheckCircle2', 'XCircle', 'AlertCircle', 'AlertTriangle', 'Info',
|
'Check', 'CheckCircle2', 'XCircle', 'AlertCircle', 'AlertTriangle', 'Info',
|
||||||
|
|||||||
+211
-1553
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 Message from '@/components/ui/message/message-manager'
|
||||||
import Button from '@/components/ui/button/button.vue'
|
import Button from '@/components/ui/button/button.vue'
|
||||||
import FmsTable from '@/components/fms-table/FmsTable.vue'
|
import FmsTable from '@/components/fms-table/FmsTable.vue'
|
||||||
import FieldGroupManager from './FieldGroupManager.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
moduleId: { type: String, required: true },
|
moduleId: { type: String, required: true },
|
||||||
@@ -15,14 +14,12 @@ const props = defineProps({
|
|||||||
queryConfig: { type: Array, default: () => [] },
|
queryConfig: { type: Array, default: () => [] },
|
||||||
fields: { type: Array, default: () => [] },
|
fields: { type: Array, default: () => [] },
|
||||||
groups: { type: Array, default: () => [] },
|
groups: { type: Array, default: () => [] },
|
||||||
referencedGroupIds: { type: Array, default: () => [] },
|
|
||||||
loading: { type: Boolean, default: false },
|
loading: { type: Boolean, default: false },
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update', 'groupsSaved', 'syncEdit', 'syncQuery'])
|
const emit = defineEmits(['update', 'syncEdit', 'syncQuery'])
|
||||||
|
|
||||||
const localList = ref([])
|
const localList = ref([])
|
||||||
const groupManagerVisible = ref(false)
|
|
||||||
const activeKey = ref(null)
|
const activeKey = ref(null)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -139,9 +136,6 @@ const columns = shallowRef([
|
|||||||
<template>
|
<template>
|
||||||
<div class="mm-panel">
|
<div class="mm-panel">
|
||||||
<div class="mm-panel__toolbar">
|
<div class="mm-panel__toolbar">
|
||||||
<Button type="outline" :disabled="loading" @click="groupManagerVisible = true">
|
|
||||||
分组管理
|
|
||||||
</Button>
|
|
||||||
<Button type="outline" :disabled="loading || !editConfig.length" @click="syncToEdit">
|
<Button type="outline" :disabled="loading || !editConfig.length" @click="syncToEdit">
|
||||||
同步到编辑显示
|
同步到编辑显示
|
||||||
</Button>
|
</Button>
|
||||||
@@ -162,13 +156,6 @@ const columns = shallowRef([
|
|||||||
@row-order-change="onRowOrderChange"
|
@row-order-change="onRowOrderChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FieldGroupManager
|
|
||||||
v-model:open="groupManagerVisible"
|
|
||||||
:module-id="moduleId"
|
|
||||||
:groups="groups"
|
|
||||||
:referenced-group-ids="referencedGroupIds"
|
|
||||||
@saved="(groups, idMapping) => emit('groupsSaved', groups, idMapping)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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 ModuleBasicPanel from './components/ModuleBasicPanel.vue'
|
||||||
import ModuleFieldsPanel from './components/ModuleFieldsPanel.vue'
|
import ModuleFieldsPanel from './components/ModuleFieldsPanel.vue'
|
||||||
import ModuleListConfigPanel from './components/ModuleListConfigPanel.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 ModuleQueryConfigPanel from './components/ModuleQueryConfigPanel.vue'
|
||||||
import ModuleAutoCodePanel from './components/ModuleAutoCodePanel.vue'
|
import ModuleAutoCodePanel from './components/ModuleAutoCodePanel.vue'
|
||||||
import ModulePowersPanel from './components/ModulePowersPanel.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() {
|
function onModuleI18nSaved() {
|
||||||
i18nPanelRef.value?.refreshKey?.()
|
i18nPanelRef.value?.refreshKey?.()
|
||||||
}
|
}
|
||||||
@@ -679,7 +670,7 @@ function onModuleNewClick({ key }) {
|
|||||||
|
|
||||||
/* ================= 右侧分组配置导航 ================= */
|
/* ================= 右侧分组配置导航 ================= */
|
||||||
// 配置导航分组:概览 / 数据结构 / 界面配置 / 业务能力 / 高级设置。
|
// 配置导航分组:概览 / 数据结构 / 界面配置 / 业务能力 / 高级设置。
|
||||||
// 每个导航项唯一激活(activeSection);「分组管理」点击时同时打开分组管理弹窗。
|
// 每个导航项唯一激活(activeSection)。
|
||||||
const configGroups = computed(() => {
|
const configGroups = computed(() => {
|
||||||
const groups = []
|
const groups = []
|
||||||
groups.push({
|
groups.push({
|
||||||
@@ -753,17 +744,6 @@ const configNavItems = computed(() =>
|
|||||||
configGroups.value.flatMap((group) => group.items.map((item) => ({ ...item }))),
|
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 子表表格)决定「表单设计」面板的形态与导航标签
|
// 编辑方式(form 主表表单 / table 子表表格)决定「表单设计」面板的形态与导航标签
|
||||||
const editMode = computed(() => drafts.value?.module?.b_edit_mode || 'form')
|
const editMode = computed(() => drafts.value?.module?.b_edit_mode || 'form')
|
||||||
const editNavLabel = computed(() => (editMode.value === 'table' ? '表格设计' : '表单设计'))
|
const editNavLabel = computed(() => (editMode.value === 'table' ? '表格设计' : '表单设计'))
|
||||||
@@ -915,6 +895,9 @@ async function saveModule() {
|
|||||||
['s_autocode', 'autoCode'],
|
['s_autocode', 'autoCode'],
|
||||||
['s_power', 'powers'],
|
['s_power', 'powers'],
|
||||||
]
|
]
|
||||||
|
// 收集字段/分组的临时 id → 真实 id 映射,用于跨表外键重映射
|
||||||
|
const fieldIdMap = new Map()
|
||||||
|
const groupIdMap = new Map()
|
||||||
for (const [table, section] of subTables) {
|
for (const [table, section] of subTables) {
|
||||||
const current = drafts.value[section] || []
|
const current = drafts.value[section] || []
|
||||||
const previous = originals.value?.[section] || []
|
const previous = originals.value?.[section] || []
|
||||||
@@ -927,11 +910,25 @@ async function saveModule() {
|
|||||||
}
|
}
|
||||||
const req = tableChange(table, changes)
|
const req = tableChange(table, changes)
|
||||||
if (hasChanges(req)) {
|
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)
|
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
|
const i18nPanel = i18nPanelRef.value
|
||||||
if (i18nPanel && typeof i18nPanel.reconcileManagedRows === 'function') {
|
if (i18nPanel && typeof i18nPanel.reconcileManagedRows === 'function') {
|
||||||
i18nPanel.reconcileManagedRows(finalI18nItems.value)
|
i18nPanel.reconcileManagedRows(finalI18nItems.value)
|
||||||
@@ -1098,24 +1095,41 @@ loadModuleTree()
|
|||||||
:query-config="drafts?.queryConfig"
|
:query-config="drafts?.queryConfig"
|
||||||
:fields="drafts?.fields"
|
:fields="drafts?.fields"
|
||||||
:groups="drafts?.groups"
|
:groups="drafts?.groups"
|
||||||
:referenced-group-ids="referencedGroupIds"
|
|
||||||
:loading="busy"
|
:loading="busy"
|
||||||
@update="(v) => onModuleUpdate('listConfig', v)"
|
@update="(v) => onModuleUpdate('listConfig', v)"
|
||||||
@groups-saved="onGroupsSaved"
|
|
||||||
@sync-edit="(v) => onModuleUpdate('editConfig', v)"
|
@sync-edit="(v) => onModuleUpdate('editConfig', v)"
|
||||||
@sync-query="(v) => onModuleUpdate('queryConfig', v)"
|
@sync-query="(v) => onModuleUpdate('queryConfig', v)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isDataModule" v-show="activeSection === 'edit'" class="mm-config-pane">
|
<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)"
|
:module-id="String(selectedModule.b_id)"
|
||||||
:edit-config="drafts?.editConfig"
|
:edit-config="drafts?.editConfig"
|
||||||
:list-config="drafts?.listConfig"
|
:list-config="drafts?.listConfig"
|
||||||
:query-config="drafts?.queryConfig"
|
:query-config="drafts?.queryConfig"
|
||||||
:fields="drafts?.fields"
|
:fields="drafts?.fields"
|
||||||
:groups="drafts?.groups"
|
:groups="drafts?.groups"
|
||||||
:referenced-group-ids="referencedGroupIds"
|
|
||||||
:edit-mode="editMode"
|
:edit-mode="editMode"
|
||||||
:active="activeSection === 'edit'"
|
:active="activeSection === 'edit'"
|
||||||
:loading="busy"
|
:loading="busy"
|
||||||
@@ -1124,7 +1138,6 @@ loadModuleTree()
|
|||||||
(v) => onModuleUpdate('module', { ...(drafts?.module || {}), b_edit_mode: v })
|
(v) => onModuleUpdate('module', { ...(drafts?.module || {}), b_edit_mode: v })
|
||||||
"
|
"
|
||||||
@update-groups="(v) => onModuleUpdate('groups', v)"
|
@update-groups="(v) => onModuleUpdate('groups', v)"
|
||||||
@groups-saved="onGroupsSaved"
|
|
||||||
@sync-list="(v) => onModuleUpdate('listConfig', v)"
|
@sync-list="(v) => onModuleUpdate('listConfig', v)"
|
||||||
@sync-query="(v) => onModuleUpdate('queryConfig', v)"
|
@sync-query="(v) => onModuleUpdate('queryConfig', v)"
|
||||||
/>
|
/>
|
||||||
@@ -1419,11 +1432,16 @@ loadModuleTree()
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 配置导航:横向 Tab 导航(line 类型,不作内部覆盖,组件默认样式即可) ---- */
|
/* ---- 配置导航:横向 Tab 导航(line 类型) ---- */
|
||||||
.mm-config-nav.tabs {
|
.mm-config-nav.tabs {
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 页签高度(42px):仅作用于本页 tab 项,不改变组件内部样式,也不影响按钮 */
|
||||||
|
.mm-config-nav.tabs :deep(.tabs-tab) {
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 标签内布局:图标 + 文字 + badge */
|
/* 标签内布局:图标 + 文字 + badge */
|
||||||
.mm-config-nav__icon {
|
.mm-config-nav__icon {
|
||||||
flex: none;
|
flex: none;
|
||||||
|
|||||||
Reference in new issue
Block a user