20260824172723
This commit is contained in:
1 parent
791d1e01d7
commit
97aea665f9
10 files changed
+443
-723
No files matched your search
+113
-295
@@ -38,8 +38,6 @@ import { createTempId } from '@/utils/tempId'
|
||||
|
||||
const props = defineProps({
|
||||
moduleId: { type: String, required: true },
|
||||
/** 模块名称(仅用于画布标题展示) */
|
||||
moduleName: { type: String, default: '' },
|
||||
editConfig: { type: Array, default: () => [] },
|
||||
listConfig: { type: Array, default: () => [] },
|
||||
queryConfig: { type: Array, default: () => [] },
|
||||
@@ -62,11 +60,9 @@ const viewMode = ref('design') // 'design' | 'data'
|
||||
const selectedRowKey = ref(null)
|
||||
const dragPayload = ref(null)
|
||||
const dragSourceId = ref(null)
|
||||
// 拖拽对齐提示线:{ fieldId, pos: 'before' | 'after' }
|
||||
// 拖拽对齐提示:{ fieldId, pos: 'before' | 'after' | 'line-before' | 'newline' }
|
||||
const dragLine = ref(null)
|
||||
// 悬停行的几何高亮框(相对 sheet 的绝对定位矩形,把整行字段圈起来,明确「行」的概念)
|
||||
const lineRect = ref(null)
|
||||
// 拖拽悬停中的分区 key(用于分组高亮)
|
||||
// 拖拽悬停中的分区 key(用于分组放置逻辑)
|
||||
const dragOverSection = ref(null)
|
||||
const lastDeleted = ref(null)
|
||||
let undoTimer = null
|
||||
@@ -455,8 +451,6 @@ function toggleFormDirection() {
|
||||
)
|
||||
}
|
||||
|
||||
const moduleTitle = computed(() => props.moduleName || '编辑表单')
|
||||
|
||||
// 字段库:展示模块的全部字段(含未加入表单的字段),点击定位到画布卡片
|
||||
const libraryFields = computed(() => props.fields || [])
|
||||
|
||||
@@ -666,33 +660,20 @@ function selectField(field) {
|
||||
})
|
||||
}
|
||||
|
||||
/* ================= 画布拖拽(vuedraggable / SortableJS) =================
|
||||
注意:Sortable 的 end 事件只在拖拽「源列表」触发,跨列表拖拽的目标列表只收到 add/sort。
|
||||
/* ================= 画布拖拽(vuedraggable / SortableJS fallback 模式) =================
|
||||
注意:启用 force-fallback 后拖拽完全基于鼠标事件模拟,不触发原生 HTML5 拖拽——
|
||||
滚动条在拖拽期间可正常滚动,边缘自动滚动(auto-scroll)也正常生效。
|
||||
Sortable 的 end 事件只在拖拽「源列表」触发,跨列表拖拽的目标列表只收到 add/sort。
|
||||
因此字段库拖入画布必须在 onLibDragEnd(源=字段库)提交,画布内拖拽在 onFieldDragEnd 提交。 */
|
||||
|
||||
/* 跟手标题条(阿里式拖拽):浏览器原生 HTML5 拖影是半透明的且透明度不受 CSS 控制,
|
||||
这里把原生拖影换成 1×1 透明占位隐藏掉,再自绘一个只带字段名的小标签跟随鼠标移动——
|
||||
不拖整张卡片,拖的是「标题条」;落点由目标卡片对应方向的边框高亮表达。
|
||||
native 拖拽本身(dragover/drop 事件)不受影响,落区检测照常工作 */
|
||||
/* 跟手标题条(阿里式拖拽):自绘一个只带字段名的小标签跟随鼠标移动——
|
||||
不拖整张卡片,拖的是「标题条」;落点由目标卡片对应方向的边框高亮表达 */
|
||||
let dragShadowEl = null
|
||||
let dragShadowOffsetX = 0
|
||||
let dragShadowOffsetY = 0
|
||||
|
||||
/* 原生 dragstart(捕获阶段,先于 SortableJS 处理) */
|
||||
function onNativeDragStart(evt) {
|
||||
if (!evt.dataTransfer || evt.defaultPrevented) return
|
||||
const card =
|
||||
evt.target instanceof Element
|
||||
? evt.target.closest('.mm-fdesign__field, .mm-fdesign__lib-item')
|
||||
: null
|
||||
if (!card) return
|
||||
// 1) 用 1×1 透明占位替换浏览器默认的半透明拖影
|
||||
const blank = document.createElement('div')
|
||||
blank.style.cssText = 'position:fixed;top:0;left:0;width:1px;height:1px;opacity:0.01;pointer-events:none'
|
||||
document.body.appendChild(blank)
|
||||
evt.dataTransfer.setDragImage(blank, 0, 0)
|
||||
requestAnimationFrame(() => blank.remove())
|
||||
// 2) 自绘「标题条」跟手:字段类型标记(如有)+ 字段名
|
||||
/* 从被拖卡片 DOM 提取字段名,创建跟手标题条 */
|
||||
function createDragBadge(card) {
|
||||
removeDragShadow()
|
||||
const nameEl = card.querySelector('.mm-fdesign__field-name, .mm-fdesign__lib-name')
|
||||
const markEl = card.querySelector('.mm-fdesign__mark')
|
||||
@@ -711,14 +692,15 @@ function onNativeDragStart(evt) {
|
||||
badge.appendChild(name)
|
||||
document.body.appendChild(badge)
|
||||
dragShadowEl = badge
|
||||
// 让标签右上角跟手(斜下方 12px 悬挂),像「拎着」一个小标签
|
||||
// 让标签右上角跟手(斜下方 14px 悬挂),像「拎着」一个小标签
|
||||
dragShadowOffsetX = 14
|
||||
dragShadowOffsetY = 14
|
||||
positionDragShadow(evt.clientX, evt.clientY)
|
||||
}
|
||||
|
||||
function onDragShadowMove(evt) {
|
||||
positionDragShadow(evt.clientX, evt.clientY)
|
||||
// fallback 模式下没有原生 dragover,放置意图检测改在 mousemove 中同步进行
|
||||
updateDragIntent(evt)
|
||||
}
|
||||
|
||||
function positionDragShadow(clientX, clientY) {
|
||||
@@ -733,32 +715,50 @@ function removeDragShadow() {
|
||||
}
|
||||
}
|
||||
|
||||
/* 拖拽期间全局禁用文本选中(fallback 拖拽用鼠标模拟,mousemove 会触发文本框选),
|
||||
结束后恢复——body 级 user-select 覆盖整个页面,防止拖到任意文字上都出现蓝色高亮 */
|
||||
function disableTextSelection() {
|
||||
document.body.style.userSelect = 'none'
|
||||
document.body.style.webkitUserSelect = 'none'
|
||||
}
|
||||
|
||||
function enableTextSelection() {
|
||||
document.body.style.userSelect = ''
|
||||
document.body.style.webkitUserSelect = ''
|
||||
}
|
||||
|
||||
function onFieldDragStart(evt) {
|
||||
dragPayload.value = { kind: 'card' }
|
||||
dragSourceId.value = String(evt?.item?.dataset?.fieldId || '')
|
||||
createDragBadge(evt.item)
|
||||
window.addEventListener('keydown', onDragKeydown)
|
||||
window.addEventListener('drag', onDragShadowMove)
|
||||
window.addEventListener('mousemove', onDragShadowMove)
|
||||
disableTextSelection()
|
||||
}
|
||||
|
||||
// SortableJS onMove:仅用于布局冻结(禁止拖拽中实时重排 DOM,主流表单设计器的做法)。
|
||||
// 放置意图的记录改由原生 dragover(onCanvasDragOver)承担:SortableJS 在目标插入位置
|
||||
// 等于被拖元素当前位置时会跳过 move 回调(如往「紧邻的上一行」拖拽),会导致意图丢失。
|
||||
// 放置意图的记录由 mousemove(onDragShadowMove → updateDragIntent)承担。
|
||||
function onFieldMove() {
|
||||
return false
|
||||
}
|
||||
|
||||
/* 放置意图记录:原生 dragover 每次指针移动都触发,不受 SortableJS 内部决策影响。
|
||||
/* 放置意图记录:mousemove 每次指针移动都触发(fallback 模式替代原生 dragover)。
|
||||
语义:上/下/左/右四条边各占 50%(中线分界),程序按「比例最近边缘」判断鼠标
|
||||
偏向哪条边——上 → 该行上方新行,下 → 该行下方新行,左 → 插到该字段前,
|
||||
右 → 插到该字段后;完全居中默认并入该行末尾。
|
||||
注意:必须用捕获阶段监听——SortableJS 在冒泡阶段对 dragover/drop 调用了
|
||||
stopPropagation,冒泡监听挂在祖先容器上会永远收不到事件。 */
|
||||
function onCanvasDragOver(evt) {
|
||||
右 → 插到该字段后;完全居中默认并入该行末尾。 */
|
||||
function updateDragIntent(evt) {
|
||||
if (!dragPayload.value) return
|
||||
const hoverEl = evt.target instanceof Element ? evt.target : null
|
||||
if (!hoverEl || hoverEl.closest('.sortable-fallback')) return // 忽略拖拽跟随影子
|
||||
|
||||
/* 鼠标离开画布区域:清除放置意图,松手时按取消处理(避免把字段误移到最后悬停的分组) */
|
||||
const canvasWrap = hoverEl.closest('.mm-fdesign__canvas-wrap')
|
||||
if (!canvasWrap) {
|
||||
dragLine.value = null
|
||||
dragOverSection.value = null
|
||||
return
|
||||
}
|
||||
|
||||
const sectionEl = hoverEl.closest('[data-section-key]')
|
||||
dragOverSection.value = sectionEl ? sectionEl.getAttribute('data-section-key') : null
|
||||
|
||||
@@ -821,15 +821,6 @@ function onCanvasDragOver(evt) {
|
||||
dragLine.value = { fieldId, pos }
|
||||
}
|
||||
|
||||
/* 拖拽离开画布区域:清除放置意图,松手时按取消处理(避免把字段误移到最后悬停的分组) */
|
||||
function onCanvasDragLeave(evt) {
|
||||
if (!dragPayload.value) return
|
||||
const toEl = evt.relatedTarget
|
||||
if (toEl instanceof Element && toEl.closest('.mm-fdesign__canvas-wrap')) return // 仍在画布内
|
||||
dragLine.value = null
|
||||
dragOverSection.value = null
|
||||
}
|
||||
|
||||
/* 鼠标坐标命中的「行」(按卡片 top 聚类成视觉行),返回行矩形 + 行内字段顺序。
|
||||
用于鼠标落在卡片间隙 / 行内空白时仍能按整行区域判定上/下新行 */
|
||||
function rowRectAt(clientX, clientY, sectionEl) {
|
||||
@@ -886,7 +877,7 @@ function dropTipText(pos) {
|
||||
}
|
||||
|
||||
/* 同行意图(before/after)时:被悬停字段所在视觉行的全体成员 fieldId。
|
||||
整行卡片一起高亮,而不是只亮被悬停的那一张,让用户明确「是并入这一行」 */
|
||||
整行卡片一起保持显眼(拖拽变淡时不被压暗),让用户明确「是并入这一行」 */
|
||||
const dragLineRowIds = computed(() => {
|
||||
const line = dragLine.value
|
||||
if (!line || (line.pos !== 'before' && line.pos !== 'after')) return null
|
||||
@@ -896,45 +887,6 @@ const dragLineRowIds = computed(() => {
|
||||
return new Set(collectLineIndexes(rows, idx).idxs.map((i) => String(rows[i].b_field_id)))
|
||||
})
|
||||
|
||||
/* 悬停行高亮框:根据 dragLine 实时计算目标视觉行的几何矩形(相对 sheet),
|
||||
用虚线框把整行字段圈起来,用户一眼看清「这一行」的边界,以及新行插入的横贯线位置 */
|
||||
let lastLineKey = ''
|
||||
watch(
|
||||
() => dragLine.value,
|
||||
(line) => {
|
||||
const key = line ? `${line.fieldId}:${line.pos}` : ''
|
||||
if (key === lastLineKey) return // 意图未变化,不重复计算
|
||||
lastLineKey = key
|
||||
if (!line) {
|
||||
lineRect.value = null
|
||||
return
|
||||
}
|
||||
const rows = localEdit.value
|
||||
const idx = rows.findIndex((r) => String(r.b_field_id) === String(line.fieldId))
|
||||
const sheetEl = document.querySelector('.mm-fdesign__sheet')
|
||||
if (idx < 0 || !sheetEl) {
|
||||
lineRect.value = null
|
||||
return
|
||||
}
|
||||
const els = collectLineIndexes(rows, idx).idxs
|
||||
.map((i) => document.querySelector(`.mm-fdesign__field[data-row-id="${rows[i].b_id}"]`))
|
||||
.filter(Boolean)
|
||||
if (!els.length) {
|
||||
lineRect.value = null
|
||||
return
|
||||
}
|
||||
const rects = els.map((el) => el.getBoundingClientRect())
|
||||
const sheetRect = sheetEl.getBoundingClientRect()
|
||||
lineRect.value = {
|
||||
top: Math.min(...rects.map((r) => r.top)) - sheetRect.top - 4,
|
||||
left: Math.min(...rects.map((r) => r.left)) - sheetRect.left - 4,
|
||||
width: Math.max(...rects.map((r) => r.right)) - Math.min(...rects.map((r) => r.left)) + 8,
|
||||
height: Math.max(...rects.map((r) => r.bottom)) - Math.min(...rects.map((r) => r.top)) + 8,
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
|
||||
// 目标分组段的末尾插入位置(rows 为移除被拖字段后的列表)
|
||||
function sectionEndIndex(rows, gid) {
|
||||
let last = -1
|
||||
@@ -967,9 +919,9 @@ function distributeLine(rows, memberIdxs) {
|
||||
完全根据拖拽悬停记录的意图(dragLine / dragOverSection)计算新顺序与宽度 */
|
||||
function onFieldDragEnd(evt) {
|
||||
window.removeEventListener('keydown', onDragKeydown)
|
||||
window.removeEventListener('drag', onDragShadowMove)
|
||||
window.removeEventListener('mousemove', onDragShadowMove)
|
||||
removeDragShadow()
|
||||
enableTextSelection()
|
||||
const rowId = String(evt?.item?.dataset?.fieldId || '')
|
||||
const linePos = dragLine.value
|
||||
const overKey = dragOverSection.value
|
||||
@@ -1235,16 +1187,17 @@ function togglePair(row) {
|
||||
function onLibDragStart(evt) {
|
||||
dragPayload.value = { kind: 'library', fieldId: String(evt?.item?.dataset?.fieldId || '') }
|
||||
dragSourceId.value = 'lib'
|
||||
createDragBadge(evt.item)
|
||||
window.addEventListener('keydown', onDragKeydown)
|
||||
window.addEventListener('drag', onDragShadowMove)
|
||||
window.addEventListener('mousemove', onDragShadowMove)
|
||||
disableTextSelection()
|
||||
}
|
||||
|
||||
function onLibDragEnd() {
|
||||
window.removeEventListener('keydown', onDragKeydown)
|
||||
window.removeEventListener('drag', onDragShadowMove)
|
||||
window.removeEventListener('mousemove', onDragShadowMove)
|
||||
removeDragShadow()
|
||||
enableTextSelection()
|
||||
const payload = dragPayload.value
|
||||
const linePos = dragLine.value
|
||||
const overKey = dragOverSection.value
|
||||
@@ -1456,6 +1409,10 @@ function onDesignerKeydown(event) {
|
||||
onMounted(() => window.addEventListener('keydown', onDesignerKeydown))
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', onDesignerKeydown)
|
||||
window.removeEventListener('keydown', onDragKeydown)
|
||||
window.removeEventListener('mousemove', onDragShadowMove)
|
||||
removeDragShadow()
|
||||
enableTextSelection()
|
||||
clearTimeout(undoTimer)
|
||||
})
|
||||
</script>
|
||||
@@ -1518,11 +1475,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<!-- ================= 设计视图 ================= -->
|
||||
<div
|
||||
v-show="viewMode === 'design'"
|
||||
class="mm-fdesign"
|
||||
@dragstart.capture="onNativeDragStart"
|
||||
>
|
||||
<div v-show="viewMode === 'design'" class="mm-fdesign">
|
||||
<aside class="mm-fdesign__lib">
|
||||
<div class="mm-fdesign__subhead">
|
||||
<strong>字段库</strong>
|
||||
@@ -1547,6 +1500,8 @@ onBeforeUnmount(() => {
|
||||
:animation="150"
|
||||
ghost-class="mm-fdesign__lib-ghost"
|
||||
:fallback-on-body="true"
|
||||
:force-fallback="true"
|
||||
fallback-class="mm-fdesign__drag-fallback"
|
||||
@start="onLibDragStart"
|
||||
@end="onLibDragEnd"
|
||||
>
|
||||
@@ -1599,8 +1554,6 @@ onBeforeUnmount(() => {
|
||||
<div
|
||||
class="mm-fdesign__canvas-wrap"
|
||||
:class="{ 'is-drag-active': dragPayload }"
|
||||
@dragover.capture="onCanvasDragOver"
|
||||
@dragleave.capture="onCanvasDragLeave"
|
||||
>
|
||||
<div class="mm-fdesign__canvas">
|
||||
<div class="mm-fdesign__canvas-note">
|
||||
@@ -1631,32 +1584,6 @@ onBeforeUnmount(() => {
|
||||
:class="{ 'is-horizontal': formDirection === 'horizontal' }"
|
||||
@click="selectRow(null)"
|
||||
>
|
||||
<div class="mm-fdesign__sheet-head">
|
||||
<div>
|
||||
<strong>{{ moduleTitle }}</strong>
|
||||
<span>编辑表单 · 展示当前草稿布局</span>
|
||||
</div>
|
||||
<span class="mm-fdesign__badge">编辑态</span>
|
||||
</div>
|
||||
|
||||
<!-- 拖拽悬停行高亮框:虚线框包住整行字段,明确「行」边界与插入横贯线位置 -->
|
||||
<div
|
||||
v-if="lineRect"
|
||||
class="mm-fdesign__line-highlight"
|
||||
:class="{
|
||||
'is-line-before': dragLine && dragLine.pos === 'line-before',
|
||||
'is-newline': dragLine && dragLine.pos === 'newline',
|
||||
'is-before': dragLine && dragLine.pos === 'before',
|
||||
'is-after': dragLine && dragLine.pos === 'after',
|
||||
}"
|
||||
:style="{
|
||||
top: `${lineRect.top}px`,
|
||||
left: `${lineRect.left}px`,
|
||||
width: `${lineRect.width}px`,
|
||||
height: `${lineRect.height}px`,
|
||||
}"
|
||||
/>
|
||||
|
||||
<template v-if="canvasSections.length">
|
||||
<div
|
||||
v-for="section in canvasSections"
|
||||
@@ -1664,7 +1591,6 @@ onBeforeUnmount(() => {
|
||||
class="mm-fdesign__section"
|
||||
:data-section-key="section.key"
|
||||
:class="{
|
||||
'is-over': dragPayload && dragOverSection === section.key,
|
||||
'is-horizontal': section.group?.b_layout_direction === 'horizontal',
|
||||
}"
|
||||
>
|
||||
@@ -1697,6 +1623,8 @@ onBeforeUnmount(() => {
|
||||
ghost-class="mm-fdesign__sortable-ghost"
|
||||
:drag-class="'mm-fdesign__sortable-drag'"
|
||||
:fallback-on-body="true"
|
||||
:force-fallback="true"
|
||||
fallback-class="mm-fdesign__drag-fallback"
|
||||
:filter="'.mm-fdesign__field-toolbar, button'"
|
||||
:prevent-on-filter="true"
|
||||
:move="onFieldMove"
|
||||
@@ -1788,21 +1716,15 @@ onBeforeUnmount(() => {
|
||||
class="mm-fdesign__ctrl"
|
||||
type="text"
|
||||
disabled
|
||||
:placeholder="placeholderOf(row)"
|
||||
/>
|
||||
<textarea
|
||||
v-else-if="fieldType(row) === 'textarea'"
|
||||
class="mm-fdesign__ctrl"
|
||||
disabled
|
||||
:style="{ height: `${heightOf(row)}px` }"
|
||||
:placeholder="placeholderOf(row)"
|
||||
/>
|
||||
<select
|
||||
v-else-if="fieldType(row) === 'select'"
|
||||
class="mm-fdesign__ctrl"
|
||||
disabled
|
||||
>
|
||||
<option value="">{{ placeholderOf(row) }}</option>
|
||||
<select v-else-if="fieldType(row) === 'select'" class="mm-fdesign__ctrl" disabled>
|
||||
<option value=""></option>
|
||||
</select>
|
||||
<div
|
||||
v-else-if="fieldType(row) === 'checkbox'"
|
||||
@@ -1828,15 +1750,8 @@ onBeforeUnmount(() => {
|
||||
class="mm-fdesign__ctrl"
|
||||
type="number"
|
||||
disabled
|
||||
placeholder="0"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
class="mm-fdesign__ctrl"
|
||||
type="text"
|
||||
disabled
|
||||
:placeholder="placeholderOf(row)"
|
||||
/>
|
||||
<input v-else class="mm-fdesign__ctrl" type="text" disabled />
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedRowKey === String(row.b_id)"
|
||||
@@ -1893,6 +1808,8 @@ onBeforeUnmount(() => {
|
||||
:animation="150"
|
||||
ghost-class="mm-fdesign__sortable-ghost"
|
||||
:fallback-on-body="true"
|
||||
:force-fallback="true"
|
||||
fallback-class="mm-fdesign__drag-fallback"
|
||||
:move="onFieldMove"
|
||||
@start="onFieldDragStart"
|
||||
@end="onFieldDragEnd"
|
||||
@@ -2441,9 +2358,9 @@ onBeforeUnmount(() => {
|
||||
|
||||
.mm-fdesign__sheet {
|
||||
position: relative; /* 拖拽行高亮框的绝对定位参照 */
|
||||
width: 80%;
|
||||
min-width: 80%;
|
||||
max-width: none;
|
||||
/* 纸面宽度自适应:宽屏时 90% 上限 960px,窄屏时跟随容器尽量占满,
|
||||
避免小屏幕上画布被固定百分比挤压(配置导航已改横向,画布空间更充足) */
|
||||
width: min(90%, 960px);
|
||||
margin: 0 auto;
|
||||
padding: 16px 18px 20px;
|
||||
border: 1px solid var(--fms-border);
|
||||
@@ -2451,17 +2368,13 @@ onBeforeUnmount(() => {
|
||||
background: var(--fms-card);
|
||||
box-shadow: 0 3px 14px rgba(39, 54, 70, 0.06);
|
||||
|
||||
/* 表单级左右布局:分组作为单元横向排列,可换行;表单头部始终占满整行 */
|
||||
/* 表单级左右布局:分组作为单元横向排列,可换行 */
|
||||
&.is-horizontal {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
|
||||
.mm-fdesign__sheet-head {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.mm-fdesign__section {
|
||||
flex: 1 1 340px;
|
||||
min-width: 0;
|
||||
@@ -2470,81 +2383,12 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 拖拽悬停行高亮框:单层细虚线框包住整行字段,明确「行」单元;
|
||||
新行意图时插入侧加粗为实线横贯整行(卡片级横线已移除,不重复) */
|
||||
.mm-fdesign__line-highlight {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
pointer-events: none;
|
||||
border: 1px dashed var(--fms-primary);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--fms-primary) 4%, transparent);
|
||||
|
||||
/* 新行意图:插入侧边框加粗成实线横贯整行(50% 色带移至被悬停卡片,与左右半区遮罩同款) */
|
||||
&.is-line-before {
|
||||
border-top: 2px solid var(--fms-primary);
|
||||
}
|
||||
|
||||
&.is-newline {
|
||||
border-bottom: 2px solid var(--fms-primary);
|
||||
}
|
||||
|
||||
/* 同行意图:左/右半侧加粗提示插入侧 */
|
||||
&.is-before {
|
||||
border-left: 3px solid var(--fms-primary);
|
||||
}
|
||||
|
||||
&.is-after {
|
||||
border-right: 3px solid var(--fms-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.mm-fdesign__sheet-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 14px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: var(--fms-text);
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.mm-fdesign__badge {
|
||||
flex: none;
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
background: #e7f6ed;
|
||||
color: #146c41;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.mm-fdesign__section {
|
||||
margin-top: 12px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* 拖拽悬停:目标分组栅格虚线高亮 */
|
||||
&.is-over > .mm-fdesign__grid {
|
||||
outline: 2px dashed var(--fms-primary);
|
||||
outline-offset: 2px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 分组停用标记 */
|
||||
@@ -2622,6 +2466,41 @@ onBeforeUnmount(() => {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* 拖拽中:禁用卡片边框/阴影过渡动画——鼠标在卡片边缘快速移动时
|
||||
before/after 等意图频繁切换,0.15s 过渡会让高亮条「追赶」造成抖动 */
|
||||
.mm-fdesign__canvas-wrap.is-drag-active .mm-fdesign__field {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* 拖拽中:目标卡片只保留插入方向的单侧边框高亮(is-drop-* 的 inset 阴影)。
|
||||
隐藏 hover 边框、选中虚线边框与外发光——避免整圈边框同时亮起干扰判断;
|
||||
被拖卡片(.is-dragging / .mm-fdesign__sortable-drag)保持原样 */
|
||||
.mm-fdesign__canvas-wrap.is-drag-active
|
||||
.mm-fdesign__field:not(.is-dragging):not(.mm-fdesign__sortable-drag) {
|
||||
border-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.mm-fdesign__canvas-wrap.is-drag-active
|
||||
.mm-fdesign__field.is-drop-before:not(.is-dragging):not(.mm-fdesign__sortable-drag) {
|
||||
box-shadow: inset 2px 0 0 0 var(--fms-primary) !important;
|
||||
}
|
||||
|
||||
.mm-fdesign__canvas-wrap.is-drag-active
|
||||
.mm-fdesign__field.is-drop-after:not(.is-dragging):not(.mm-fdesign__sortable-drag) {
|
||||
box-shadow: inset -2px 0 0 0 var(--fms-primary) !important;
|
||||
}
|
||||
|
||||
.mm-fdesign__canvas-wrap.is-drag-active
|
||||
.mm-fdesign__field.is-drop-line-before:not(.is-dragging):not(.mm-fdesign__sortable-drag) {
|
||||
box-shadow: inset 0 2px 0 0 var(--fms-primary) !important;
|
||||
}
|
||||
|
||||
.mm-fdesign__canvas-wrap.is-drag-active
|
||||
.mm-fdesign__field.is-drop-newline:not(.is-dragging):not(.mm-fdesign__sortable-drag) {
|
||||
box-shadow: inset 0 -2px 0 0 var(--fms-primary) !important;
|
||||
}
|
||||
|
||||
/* 拖拽中:隐藏卡片上的操作按钮(上移/下移/删除),避免干扰拖拽焦点 */
|
||||
.mm-fdesign__canvas-wrap.is-drag-active .mm-fdesign__field-toolbar {
|
||||
display: none;
|
||||
@@ -2635,6 +2514,9 @@ onBeforeUnmount(() => {
|
||||
border-radius: 6px;
|
||||
background: var(--fms-card);
|
||||
cursor: grab;
|
||||
/* 拖拽时禁用文本选中,避免 label 文字像鼠标框选一样出现蓝色高亮 */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s,
|
||||
@@ -2661,87 +2543,13 @@ onBeforeUnmount(() => {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* 拖拽悬停目标行成员:仅极淡底色(行边界由整行高亮框承担,不再逐卡描边) */
|
||||
&.is-drop-hover {
|
||||
background: color-mix(in srgb, var(--fms-primary) 5%, var(--fms-card));
|
||||
}
|
||||
|
||||
/* 同行配对第一选择:虚线边框 + 外发光,提示等待第二个字段 */
|
||||
&.is-pairing {
|
||||
border: 1px dashed var(--fms-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--fms-primary) 18%, transparent);
|
||||
}
|
||||
|
||||
/* 同行合并意图的左右半区遮罩(::before/::after 基座,仅 before/after 意图显示) */
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
height: 0;
|
||||
border-top: 2px solid var(--fms-primary);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: -5px;
|
||||
}
|
||||
|
||||
/* 同行合并意图:目标卡片左/右半区高亮遮罩(操作文字由 .mm-fdesign__drop-tip 气泡承载)。
|
||||
上/下方新行意图不再画卡片级横线——整行高亮框的实线边已表达插入位置,避免重复 */
|
||||
&.is-drop-before::before,
|
||||
&.is-drop-after::after {
|
||||
content: '';
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: auto;
|
||||
width: 50%;
|
||||
height: auto;
|
||||
border: none;
|
||||
border-radius: 5px 0 0 5px;
|
||||
background: color-mix(in srgb, var(--fms-primary) 14%, transparent);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.is-drop-after::after {
|
||||
left: auto;
|
||||
right: 0;
|
||||
border-radius: 0 5px 5px 0;
|
||||
}
|
||||
|
||||
/* 上方/下方新行:被悬停卡片内上半/下半部半区遮罩(与左右半区同款,50% 高度,内部填充) */
|
||||
&.is-drop-line-before::before,
|
||||
&.is-drop-newline::after {
|
||||
top: auto;
|
||||
bottom: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: auto;
|
||||
height: 50%;
|
||||
border: none;
|
||||
border-radius: 5px 5px 0 0;
|
||||
background: color-mix(in srgb, var(--fms-primary) 14%, transparent);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.is-drop-line-before::before {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&.is-drop-newline::after {
|
||||
bottom: 0;
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
/* 阿里式:目标卡片对应方向的边框高亮(inset 阴影不撑布局,拖拽中优先于选中外圈) */
|
||||
/* ---- 拖拽插入方向高亮:目标卡片对应侧边框高亮(阿里式 inset 阴影,不撑布局) ---- */
|
||||
&.is-drop-before {
|
||||
box-shadow: inset 2px 0 0 0 var(--fms-primary);
|
||||
}
|
||||
@@ -2937,7 +2745,7 @@ textarea.mm-fdesign__ctrl {
|
||||
/* ---- vuedraggable / SortableJS 拖拽视觉 ---- */
|
||||
/* 幽灵占位:SortableJS 把 ghostClass 加在原卡片上(布局冻结,原位不动),
|
||||
显示为灰色占位块(内部控件隐藏),一眼看出「这里被拖走了」;
|
||||
跟手的是拖拽标题条(onNativeDragStart 自绘,见 .mm-fdesign__drag-badge) */
|
||||
跟手的是拖拽标题条(createDragBadge 自绘,见 .mm-fdesign__drag-badge) */
|
||||
.mm-fdesign__sortable-ghost,
|
||||
.mm-fdesign__lib-ghost {
|
||||
opacity: 1;
|
||||
@@ -2950,7 +2758,7 @@ textarea.mm-fdesign__ctrl {
|
||||
}
|
||||
|
||||
/* 阿里式拖拽标题条:跟手只带字段名的小标签(不拖整卡),
|
||||
位置/层级由 onNativeDragStart 内联控制,这里负责外观 */
|
||||
位置/层级由 createDragBadge 内联控制,这里负责外观 */
|
||||
.mm-fdesign__drag-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -2968,6 +2776,8 @@ textarea.mm-fdesign__ctrl {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.mm-fdesign__drag-badge-mark {
|
||||
@@ -3454,3 +3264,11 @@ textarea.mm-fdesign__ctrl {
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<!-- fallback 克隆(SortableJS 在 body 上生成的整卡克隆)不在 scoped 作用域内,需全局隐藏:
|
||||
拖动时只保留自绘的跟手标题条(.mm-fdesign__drag-badge),不显示整卡克隆 -->
|
||||
<style lang="scss">
|
||||
.mm-fdesign__drag-fallback {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -59,7 +59,6 @@ import ModuleAutoCodePanel from './components/ModuleAutoCodePanel.vue'
|
||||
import ModulePowersPanel from './components/ModulePowersPanel.vue'
|
||||
import ModuleI18nPanel from './components/ModuleI18nPanel.vue'
|
||||
import MenuDetailPanel from './components/MenuDetailPanel.vue'
|
||||
import FieldGroupManager from './components/FieldGroupManager.vue'
|
||||
|
||||
/* ================= 顶部切换(Tabs 默认样式) ================= */
|
||||
const activeZone = ref('module')
|
||||
@@ -324,7 +323,9 @@ watch(moduleSelectedId, (id) => {
|
||||
drafts.value = null
|
||||
return
|
||||
}
|
||||
// 清空旧草稿,让右侧进入加载态(否则旧数据残留,v-if="drafts" 仍为真,看不到 loading)
|
||||
loadedModuleId.value = null
|
||||
drafts.value = null
|
||||
void loadModuleConfig(id)
|
||||
})
|
||||
|
||||
@@ -705,10 +706,7 @@ const configGroups = computed(() => {
|
||||
groups.push({
|
||||
key: 'structure',
|
||||
label: '数据结构',
|
||||
items: [
|
||||
{ key: 'fields', label: '字段定义', icon: Table2 },
|
||||
{ key: 'groups', label: '分组管理', icon: FolderTree },
|
||||
],
|
||||
items: [{ key: 'fields', label: '字段定义', icon: Table2 }],
|
||||
})
|
||||
|
||||
// 界面配置:表单设计(数据模块)→ 列表配置 → 查询配置
|
||||
@@ -764,18 +762,10 @@ const configGroups = computed(() => {
|
||||
return groups
|
||||
})
|
||||
|
||||
// 分组管理弹窗(导航「分组管理」项触发,保存后与列表/编辑面板共享同一分组草稿)
|
||||
const groupManagerVisible = ref(false)
|
||||
|
||||
function onNavItemClick(item) {
|
||||
activeSection.value = item.key
|
||||
groupManagerVisible.value = item.key === 'groups'
|
||||
}
|
||||
|
||||
// 离开「分组管理」分区时自动关闭弹窗,避免悬挂
|
||||
watch(activeSection, (key) => {
|
||||
if (key !== 'groups') groupManagerVisible.value = false
|
||||
})
|
||||
/* 横向胶囊 Tabs 的数据源:展平 configGroups 的分组为平铺 items */
|
||||
const configNavItems = computed(() =>
|
||||
configGroups.value.flatMap((group) => group.items.map((item) => ({ ...item }))),
|
||||
)
|
||||
|
||||
const referencedGroupIds = computed(() => {
|
||||
const ids = new Set()
|
||||
@@ -1337,6 +1327,11 @@ const menuConfigGroups = computed(() => {
|
||||
return groups
|
||||
})
|
||||
|
||||
/* 菜单横向胶囊 Tabs 的数据源 */
|
||||
const menuConfigNavItems = computed(() =>
|
||||
menuConfigGroups.value.flatMap((group) => group.items.map((item) => ({ ...item }))),
|
||||
)
|
||||
|
||||
// 重载当前菜单:若有未保存修改先确认放弃
|
||||
function reloadMenu() {
|
||||
if (!selectedMenu.value || menuSaving.value) return
|
||||
@@ -1432,55 +1427,40 @@ loadMenuTree()
|
||||
</aside>
|
||||
</SplitterPanel>
|
||||
|
||||
<!-- 配置导航:独立中栏 -->
|
||||
<SplitterPanel :default-size="158" :min="140" :max="220">
|
||||
<nav v-if="drafts" class="mm-config-nav" aria-label="模块配置导航">
|
||||
<template v-for="group in configGroups" :key="group.key">
|
||||
<div class="mm-config-nav__group">
|
||||
<div class="mm-config-nav__group-title">{{ group.label }}</div>
|
||||
<button
|
||||
v-for="item in group.items"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="mm-config-nav__item"
|
||||
:class="{ 'is-active': activeSection === item.key }"
|
||||
@click="onNavItemClick(item)"
|
||||
>
|
||||
<component :is="item.icon" :size="14" class="mm-config-nav__icon" />
|
||||
<span class="mm-config-nav__label">{{ item.label }}</span>
|
||||
<span v-if="item.badge" class="mm-config-nav__badge">{{ item.badge }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</nav>
|
||||
<div v-else class="mm-config-nav-empty">请选择左侧模块节点</div>
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel>
|
||||
<main class="mm-main">
|
||||
<template v-if="selectedModule">
|
||||
<div class="mm-header">
|
||||
<div class="mm-header__info">
|
||||
<component :is="moduleTypeIcon" :size="16" class="mm-header__icon" />
|
||||
<span class="mm-header__name">{{ selectedModule.b_name || selectedModule.b_code }}</span>
|
||||
<span class="mm-header__type">{{ moduleTypeLabel }}</span>
|
||||
<span v-if="moduleBreadcrumb.length > 1" class="mm-header__crumb">
|
||||
{{ moduleBreadcrumb.slice(0, -1).join(' / ') }}
|
||||
</span>
|
||||
<span v-if="unsavedModuleIds.has(String(selectedModule.b_id))" class="mm-header__unsaved">
|
||||
未保存
|
||||
</span>
|
||||
</div>
|
||||
<div class="mm-header__actions">
|
||||
<span class="mm-header__hint">Ctrl + S 保存</span>
|
||||
<Button type="outline" :disabled="busy || !selectedModule" @click="reloadModule">
|
||||
<RefreshCw :size="14" /> 重载
|
||||
</Button>
|
||||
<Button type="primary" :loading="configSaving" :disabled="configLoading" @click="saveModule">
|
||||
<Save :size="14" /> 保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs
|
||||
v-if="drafts"
|
||||
v-model="activeSection"
|
||||
type="pill"
|
||||
nav-only
|
||||
class="mm-config-nav"
|
||||
:items="configNavItems"
|
||||
aria-label="模块配置导航"
|
||||
>
|
||||
<template #label="{ item }">
|
||||
<component :is="item.icon" :size="14" class="mm-config-nav__icon" />
|
||||
<span class="mm-config-nav__label">{{ item.label }}</span>
|
||||
<span v-if="item.badge" class="mm-config-nav__badge">{{ item.badge }}</span>
|
||||
</template>
|
||||
<template #rightExtra>
|
||||
<div class="mm-header__actions">
|
||||
<span class="mm-header__hint">Ctrl + S 保存</span>
|
||||
<Button type="outline" :disabled="busy || !selectedModule" @click="reloadModule">
|
||||
<RefreshCw :size="14" /> 重载
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="configSaving"
|
||||
:disabled="configLoading"
|
||||
@click="saveModule"
|
||||
>
|
||||
<Save :size="14" /> 保存
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Tabs>
|
||||
|
||||
<div class="mm-body__inner">
|
||||
<div v-if="drafts" class="mm-config-content">
|
||||
@@ -1514,23 +1494,6 @@ loadMenuTree()
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isDataModule || isQueryModule"
|
||||
v-show="activeSection === 'groups'"
|
||||
class="mm-config-pane"
|
||||
>
|
||||
<div class="mm-groups-pane">
|
||||
<FolderTree :size="22" class="mm-groups-pane__icon" />
|
||||
<p class="mm-groups-pane__text">
|
||||
字段分组用于组织编辑表单与列表的展示结构,当前共
|
||||
{{ drafts?.groups?.length ?? 0 }} 个分组。分组的新增、排序与启停通过分组管理弹窗维护,保存后即时生效。
|
||||
</p>
|
||||
<Button type="outline" :disabled="busy" @click="groupManagerVisible = true">
|
||||
打开分组管理
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isDataModule || isQueryModule"
|
||||
v-show="activeSection === 'list'"
|
||||
@@ -1555,7 +1518,6 @@ loadMenuTree()
|
||||
<div v-if="isDataModule" v-show="activeSection === 'edit'" class="mm-config-pane">
|
||||
<ModuleEditConfigPanel
|
||||
:module-id="String(selectedModule.b_id)"
|
||||
:module-name="selectedModule.b_name || selectedModule.b_code"
|
||||
:edit-config="drafts?.editConfig"
|
||||
:list-config="drafts?.listConfig"
|
||||
:query-config="drafts?.queryConfig"
|
||||
@@ -1640,14 +1602,6 @@ loadMenuTree()
|
||||
</main>
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
<!-- 分组管理弹窗:导航「分组管理」项触发;与列表/编辑面板共用同一份分组草稿 -->
|
||||
<FieldGroupManager
|
||||
v-model:open="groupManagerVisible"
|
||||
:module-id="String(selectedModule?.b_id || '')"
|
||||
:groups="drafts?.groups"
|
||||
:referenced-group-ids="referencedGroupIds"
|
||||
@saved="onGroupsSaved"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ================ 菜单配置 ================ -->
|
||||
@@ -1704,59 +1658,35 @@ loadMenuTree()
|
||||
</aside>
|
||||
</SplitterPanel>
|
||||
|
||||
<!-- 配置导航:独立中栏(与模块配置统一三栏结构) -->
|
||||
<SplitterPanel :default-size="158" :min="140" :max="220">
|
||||
<nav
|
||||
v-if="activeZone === 'menu' && selectedMenu"
|
||||
class="mm-config-nav"
|
||||
aria-label="菜单配置导航"
|
||||
>
|
||||
<template v-for="group in menuConfigGroups" :key="group.key">
|
||||
<div class="mm-config-nav__group">
|
||||
<div class="mm-config-nav__group-title">{{ group.label }}</div>
|
||||
<button
|
||||
v-for="item in group.items"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="mm-config-nav__item"
|
||||
:class="{ 'is-active': menuActiveSection === item.key }"
|
||||
@click="menuActiveSection = item.key"
|
||||
>
|
||||
<component :is="item.icon" :size="14" class="mm-config-nav__icon" />
|
||||
<span class="mm-config-nav__label">{{ item.label }}</span>
|
||||
<span v-if="item.badge" class="mm-config-nav__badge">{{ item.badge }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</nav>
|
||||
<div v-else-if="activeZone === 'menu'" class="mm-config-nav-empty">请选择左侧菜单节点</div>
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel>
|
||||
<main class="mm-main">
|
||||
<template v-if="selectedMenu">
|
||||
<div class="mm-header">
|
||||
<div class="mm-header__info">
|
||||
<component :is="menuTypeIcon" :size="16" class="mm-header__icon" />
|
||||
<span class="mm-header__name">{{ selectedMenu.b_name || selectedMenu.b_code }}</span>
|
||||
<span class="mm-header__type">{{ menuTypeLabel(selectedMenu.b_menu_type) }}</span>
|
||||
<span v-if="menuBreadcrumb.length > 1" class="mm-header__crumb">
|
||||
{{ menuBreadcrumb.slice(0, -1).join(' / ') }}
|
||||
</span>
|
||||
<span v-if="unsavedMenuIds.has(String(selectedMenu.b_id))" class="mm-header__unsaved">
|
||||
未保存
|
||||
</span>
|
||||
</div>
|
||||
<div class="mm-header__actions">
|
||||
<span class="mm-header__hint">Ctrl + S 保存</span>
|
||||
<Button type="outline" :disabled="menuSaving" @click="reloadMenu">
|
||||
<RefreshCw :size="14" /> 重载
|
||||
</Button>
|
||||
<Button type="primary" :loading="menuSaving" @click="saveMenu">
|
||||
<Save :size="14" /> 保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs
|
||||
v-if="menuDrafts"
|
||||
v-model="menuActiveSection"
|
||||
type="pill"
|
||||
nav-only
|
||||
class="mm-config-nav"
|
||||
:items="menuConfigNavItems"
|
||||
aria-label="菜单配置导航"
|
||||
>
|
||||
<template #label="{ item }">
|
||||
<component :is="item.icon" :size="14" class="mm-config-nav__icon" />
|
||||
<span class="mm-config-nav__label">{{ item.label }}</span>
|
||||
<span v-if="item.badge" class="mm-config-nav__badge">{{ item.badge }}</span>
|
||||
</template>
|
||||
<template #rightExtra>
|
||||
<div class="mm-header__actions">
|
||||
<span class="mm-header__hint">Ctrl + S 保存</span>
|
||||
<Button type="outline" :disabled="menuSaving" @click="reloadMenu">
|
||||
<RefreshCw :size="14" /> 重载
|
||||
</Button>
|
||||
<Button type="primary" :loading="menuSaving" @click="saveMenu">
|
||||
<Save :size="14" /> 保存
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Tabs>
|
||||
|
||||
<div class="mm-body__inner">
|
||||
<MenuDetailPanel
|
||||
@@ -1856,8 +1786,8 @@ loadMenuTree()
|
||||
}
|
||||
|
||||
/* ---- 左树顶部 header:标题在左,新增/刷新图标在右。
|
||||
与右侧 .mm-header 同规格:padding 10px + 内容 28px(图标按钮与 Button 等高)
|
||||
= 48px,两侧 header 的底边框在同一水平线对齐。
|
||||
与右侧配置导航同规格:padding 6px + 内容 28px(图标按钮与 Button 等高)
|
||||
= 40px,两侧 header 的底边框在同一水平线对齐。
|
||||
负 margin 抵消面板 8px padding,使 border-bottom 横跨面板全宽。 */
|
||||
.mm-tree-header {
|
||||
display: flex;
|
||||
@@ -1865,7 +1795,7 @@ loadMenuTree()
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
margin: -8px -8px 0;
|
||||
padding: 10px;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
@@ -1998,72 +1928,7 @@ loadMenuTree()
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mm-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex: none;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.mm-header__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mm-header__icon {
|
||||
flex: none;
|
||||
color: var(--fms-text-secondary);
|
||||
}
|
||||
|
||||
.mm-header__crumb {
|
||||
max-width: 240px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
color: var(--fms-text-secondary);
|
||||
|
||||
&::before {
|
||||
content: '‹ ';
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.mm-header__name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--fms-text);
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mm-header__type {
|
||||
flex: none;
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
background: var(--fms-secondary);
|
||||
color: var(--fms-text-secondary);
|
||||
}
|
||||
|
||||
.mm-header__unsaved {
|
||||
flex: none;
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
/* 操作按钮组(Tabs 的 rightExtra 插槽内) */
|
||||
.mm-header__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2086,94 +1951,47 @@ loadMenuTree()
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 中栏配置导航:独立 Splitter 面板内撑满,右侧竖线分隔工作区 */
|
||||
.mm-config-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 10px 8px;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid var(--fms-border);
|
||||
background: var(--fms-card);
|
||||
/* ---- 配置导航:横向胶囊式 Tabs,位于 header 内左侧,操作按钮贴右 ---- */
|
||||
/* ---- 配置导航:横向胶囊式 Tabs,位于工作区顶部,操作按钮在 rightExtra 插槽 ---- */
|
||||
.mm-config-nav.tabs {
|
||||
flex: none;
|
||||
/* 与左侧树 header 的分割线对齐:左右两栏 header 同高同线,视觉统一 */
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.mm-config-nav-empty {
|
||||
display: flex;
|
||||
.mm-config-nav :deep(.tabs-nav) {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
/* 标签列表由 Scrollbar 组件接管滚动,这里只保持垂直居中 */
|
||||
.mm-config-nav :deep(.tabs-nav-list) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
border-right: 1px solid var(--fms-border);
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mm-config-nav__group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mm-config-nav__group-title {
|
||||
padding: 2px 8px 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--fms-text-secondary);
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.mm-config-nav__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
padding: 5px 8px;
|
||||
border: none;
|
||||
border-radius: var(--fms-control-radius);
|
||||
background: transparent;
|
||||
color: var(--fms-text);
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--fms-secondary);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: color-mix(in srgb, var(--fms-primary) 10%, transparent);
|
||||
color: var(--fms-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
/* rightExtra 操作区与标签在同一行垂直居中 */
|
||||
.mm-config-nav :deep(.tabs-nav-extra) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
/* 标签内布局:图标 + 文字 + badge */
|
||||
.mm-config-nav__icon {
|
||||
flex: none;
|
||||
color: var(--fms-text-secondary);
|
||||
}
|
||||
|
||||
.mm-config-nav__item.is-active .mm-config-nav__icon {
|
||||
.mm-config-nav .tabs-tab-active .mm-config-nav__icon {
|
||||
color: var(--fms-primary);
|
||||
}
|
||||
|
||||
.mm-config-nav__label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mm-config-nav__badge {
|
||||
flex: none;
|
||||
padding: 0 5px;
|
||||
border-radius: 999px;
|
||||
background: var(--fms-secondary);
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
@@ -2185,7 +2003,6 @@ loadMenuTree()
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 12px 16px 16px;
|
||||
}
|
||||
|
||||
.mm-config-pane {
|
||||
@@ -2195,34 +2012,8 @@ loadMenuTree()
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 分组管理占位页(弹窗由中栏导航触发) */
|
||||
.mm-groups-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-height: 180px;
|
||||
padding: 24px;
|
||||
border: 1px dashed var(--fms-border);
|
||||
border-radius: var(--fms-control-radius);
|
||||
background: var(--fms-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mm-groups-pane__icon {
|
||||
color: var(--fms-text-secondary);
|
||||
}
|
||||
|
||||
.mm-groups-pane__text {
|
||||
max-width: 440px;
|
||||
margin: 0;
|
||||
color: var(--fms-text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
/* 滚动容器内边距:内容与滚动边缘保持间距,聚焦光环(3px)不被 overflow: auto 裁剪 */
|
||||
padding: 12px 16px 16px;
|
||||
}
|
||||
|
||||
/* 配置区加载中 / 空态占位 */
|
||||
|
||||
Reference in new issue
Block a user