20260826173130
This commit is contained in:
1 parent
9957757b5d
commit
0bbbbd3ecc
15 files changed
+2417
-819
No files matched your search
+1007
File diff suppressed because it is too large.
Load diff
@@ -121,6 +121,8 @@
|
||||
|
||||
.tabs-top .tabs-nav-extra:last-child {
|
||||
margin-left: 8px;
|
||||
/* 右侧操作区与导航栏右边缘留白(组件默认,使用方无需再覆盖) */
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.tabs-left .tabs-nav-extra:first-child,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, defineComponent, h, ref, watch } from 'vue'
|
||||
import { computed, defineComponent, h, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import Splitter from '@/components/ui/splitter/splitter.vue'
|
||||
@@ -57,7 +57,6 @@ watch(
|
||||
)
|
||||
|
||||
/* ---------- 侧栏宽度(展开态可拖拽调宽,由 Splitter 接管) ---------- */
|
||||
const SIDEBAR_MIN = 180
|
||||
const SIDEBAR_MAX = 400
|
||||
const SIDEBAR_DEFAULT = 220
|
||||
// 折叠态:48px(对齐 shadcn sidebar 默认 3rem,仅容图标)
|
||||
@@ -76,6 +75,68 @@ function onSplitterResize(sizes) {
|
||||
const width = Math.round(Number(sizes[0]))
|
||||
if (Number.isFinite(width)) sidebarWidth.value = width
|
||||
}
|
||||
|
||||
/* 展开/折叠逻辑:
|
||||
* - 折叠:侧栏固定 48px(不可拖拽)
|
||||
* - 展开:恢复为拖拽宽度;但若上次被拖得过窄(< 默认 220),自动恢复默认宽度,
|
||||
* 避免「拖窄 → 折叠 → 再展开」后侧栏变成一条细缝。
|
||||
* 用 watch 监听折叠态,无论从顶栏按钮还是菜单点击触发展开都生效 */
|
||||
watch(
|
||||
() => appStore.sidebarCollapsed,
|
||||
(collapsed) => {
|
||||
if (!collapsed && sidebarWidth.value < SIDEBAR_DEFAULT) {
|
||||
sidebarWidth.value = SIDEBAR_DEFAULT
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/* 顶栏折叠/展开按钮的点击逻辑(经 provide 提供给 AppTopbar):
|
||||
* - 折叠态:展开(宽度由上方 watch 自动恢复)
|
||||
* - 展开态且宽度已被拖到很窄(< 默认 220):不折叠,直接展开到默认宽度
|
||||
* ——用户把侧栏拖没了以后点按钮,期望的是"恢复宽度"而不是"再折叠"
|
||||
* - 其余情况:正常折叠(48px) */
|
||||
function toggleSidebar() {
|
||||
if (appStore.sidebarCollapsed) {
|
||||
appStore.setSidebar(false)
|
||||
return
|
||||
}
|
||||
if (sidebarWidth.value < SIDEBAR_DEFAULT) {
|
||||
sidebarWidth.value = SIDEBAR_DEFAULT
|
||||
return
|
||||
}
|
||||
appStore.setSidebar(true)
|
||||
}
|
||||
provide('toggleSidebar', toggleSidebar)
|
||||
|
||||
/* ---------- 内容可视高度(页面撑满用) ----------
|
||||
* 实测 .fms-content 可视高度(clientHeight - 纵向 padding),写入 CSS 变量
|
||||
* --fms-content-height(随 DOM 继承给页面所有后代)。
|
||||
* 需要「固定高度 + 内部滚动」的页面(如工具栏固定、表格内部滚动)直接写:
|
||||
* height: var(--fms-content-height)
|
||||
* 替代 calc(100vh - 116px) 这类魔数——顶栏/页签/padding 等布局内部变化时自动跟随,
|
||||
* 页面无需改动。 */
|
||||
const contentRef = ref(null)
|
||||
let contentResizeObserver = null
|
||||
|
||||
function syncContentHeight() {
|
||||
const el = contentRef.value
|
||||
if (!el) return
|
||||
const style = getComputedStyle(el)
|
||||
const height =
|
||||
el.clientHeight - parseFloat(style.paddingTop || '0') - parseFloat(style.paddingBottom || '0')
|
||||
el.style.setProperty('--fms-content-height', `${height}px`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// ResizeObserver 首次 observe 会立即回调一次,初始值无需额外同步
|
||||
contentResizeObserver = new ResizeObserver(syncContentHeight)
|
||||
if (contentRef.value) contentResizeObserver.observe(contentRef.value)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
contentResizeObserver?.disconnect()
|
||||
contentResizeObserver = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -89,10 +150,11 @@ function onSplitterResize(sizes) {
|
||||
@resize-start="dragging = true"
|
||||
@resize-end="dragging = false"
|
||||
>
|
||||
<!-- 侧栏面板:折叠态固定 48px 且不可调整 -->
|
||||
<!-- 侧栏面板:紧贴左/上/下边缘(无 padding),侧栏卡片右侧阴影与主区分隔。
|
||||
拖拽无最小宽度限制(可拖到任意窄),仅限制最大宽度 -->
|
||||
<SplitterPanel
|
||||
class="fms-sidebar-panel"
|
||||
:size="sidebarSize"
|
||||
:min="appStore.sidebarCollapsed ? undefined : SIDEBAR_MIN"
|
||||
:max="appStore.sidebarCollapsed ? undefined : SIDEBAR_MAX"
|
||||
:resizable="!appStore.sidebarCollapsed"
|
||||
>
|
||||
@@ -105,7 +167,7 @@ function onSplitterResize(sizes) {
|
||||
<AppTopbar />
|
||||
|
||||
<div class="fms-main">
|
||||
<div class="fms-content">
|
||||
<div ref="contentRef" class="fms-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="page" mode="out-in">
|
||||
<div :key="route.fullPath" class="fms-page-wrap">
|
||||
@@ -167,19 +229,28 @@ function onSplitterResize(sizes) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 非折叠态:分隔条命中区改为悬浮(width: 0 + 负 margin),不占 flex 宽度。
|
||||
* 否则侧栏内容右 padding(8px) + 分隔条占位(6px) 叠加成 14px 空隙,
|
||||
* 而右侧仅 8px,两侧不对称(折叠态因 display:none 掩盖了此问题)。
|
||||
* 注意同样只能用 > 限定为布局自身的直接子级分隔条:
|
||||
* 用后代选择器时 scoped :deep 会把主面板内嵌套 Splitter 的分隔条
|
||||
* 一并压成 0 宽,导致内层(如 FmsFile)分隔条不可见、无法拖拽 */
|
||||
/* 非折叠态:分隔条悬浮命中区(width: 0 + margin: 0,不参与 flex 布局)。
|
||||
* 注意:原实现用负 margin(0 -3px)做悬浮命中区,但 flex 中负 margin 会把相邻面板
|
||||
* 拉近 6px,抵消主区面板 padding-left 8px → 侧栏白卡与右侧内容(顶栏/灰画布)只剩
|
||||
* 2px 灰缝,视觉上"紧贴"。命中区改由绝对定位的 ::after 扩展,布局不再受影响。 */
|
||||
.fms-layout .fms-layout-splitter :deep(> .fms-splitter-bar) {
|
||||
width: 0;
|
||||
margin: 0 calc(var(--fms-splitter-trigger-size) / -2);
|
||||
margin: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 命中区:绝对定位在分隔条两侧各扩展 3px(共 6px),不占 flex 空间,
|
||||
* 且能正常触发 bar 的 pointerdown 拖拽 */
|
||||
.fms-layout .fms-layout-splitter :deep(> .fms-splitter-bar)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: calc(var(--fms-splitter-trigger-size) / -2);
|
||||
width: var(--fms-splitter-trigger-size);
|
||||
}
|
||||
|
||||
/* 侧栏面板宽度过渡:折叠/展开切换平滑;拖拽时禁用保证跟手 */
|
||||
.fms-layout-splitter .fms-splitter-panel {
|
||||
transition: flex-basis 180ms ease;
|
||||
@@ -189,22 +260,25 @@ function onSplitterResize(sizes) {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* 主区面板:四周留 8px 间距(含左侧),与侧栏拉开标准间隙。
|
||||
* 折叠态(窄侧栏 48px)左侧归 0,避免过窄时两侧都留间隙 */
|
||||
/* 主区面板:只留左右 8px padding(顶栏/内容区紧贴上边缘)。
|
||||
* 折叠态与展开态一致,保持 8px 左灰缝,避免侧栏折叠时与右侧紧贴 */
|
||||
.fms-main-panel {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.fms-layout.is-collapsed .fms-main-panel {
|
||||
padding: 8px 8px 8px 0;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
/* 覆盖 Splitter 面板默认 overflow: auto(scoped 同名规则特异性更高,需带 .fms-layout 提权),
|
||||
* 让主卡片左侧阴影完整绘制到侧栏背景上(对齐 old2 的 z-index 效果) */
|
||||
* 让顶栏/卡片阴影与圆角完整绘制 */
|
||||
.fms-layout .fms-splitter-panel.fms-main-panel {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* 侧栏面板同样需要 overflow: visible:
|
||||
* 否则默认 overflow: auto 会把侧栏卡片的右侧投影(box-shadow 向右投射 4px)
|
||||
* 裁掉,侧栏与右侧内容之间失去可见分隔,看起来"紧贴" */
|
||||
.fms-layout .fms-splitter-panel.fms-sidebar-panel {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* 主区纵向列:顶栏在灰底上独立成条,下方白卡与其留 8px 间距 */
|
||||
.fms-main-col {
|
||||
display: flex;
|
||||
@@ -215,8 +289,9 @@ function onSplitterResize(sizes) {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 主区内容容器:白底承载内容,与透明侧栏(灰底)形成明显区分;
|
||||
* 圆角 + 阴影让白卡在灰底上浮起,层次清晰 */
|
||||
/* 主区内容容器:灰底画布(--fms-background),不再是白卡。
|
||||
* 页面在此画布上自行放置白卡(background: var(--fms-card) + 圆角 + 阴影),
|
||||
* 靠「灰底 + 白卡」形成层次,避免用大量边框线分隔 */
|
||||
.fms-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -224,20 +299,28 @@ function onSplitterResize(sizes) {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
box-shadow: var(--fms-shadow);
|
||||
background: var(--fms-background);
|
||||
}
|
||||
|
||||
/* 内容区:白卡内滚动区,顶部由 .fms-tabs 的底边线分隔。
|
||||
* flex column:让页面根可用 flex:1 撑满可视高度(百分比高度链在滚动容器内不可靠) */
|
||||
/* 内容区:灰底画布内的滚动区(顶部由 .fms-tabs 的底边线分隔)。
|
||||
* flex column:让页面根可用 flex:1 撑满可视高度(百分比高度链在滚动容器内不可靠)。
|
||||
* overflow: auto hidden —— 只允许纵向滚动,禁止横向滚动条:
|
||||
* 侧栏折叠/展开过渡(flex-basis 180ms)瞬间内容宽度会短暂超出容器,
|
||||
* 若允许 x 滚动会出现滚动条闪现;页面内容的横向滚动应由内部容器(表格/splitter)处理。
|
||||
* 页面需要「固定高度 + 内部滚动」时用 height: var(--fms-content-height)
|
||||
* (由布局实测写入,随顶栏/页签/padding 变化自动跟随),不要手写 calc(100vh - xx) 魔数。
|
||||
* 页面内部的分层白卡:background: var(--fms-card) + border-radius + box-shadow,
|
||||
* 卡间用 margin/gap 留出灰缝,不要用 border 画分隔线 */
|
||||
.fms-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
padding: 12px 16px 16px;
|
||||
overflow: auto hidden;
|
||||
/* 只保留上下 padding(8px),左右为 0:
|
||||
* 页面内容紧贴灰画布左右边缘(与主区面板的左右 8px 间距配合),
|
||||
* 让页面白卡尽可能占满横向空间 */
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* 页面过渡外层包裹:稳定的 DOM 根,供 transition 的 out-in 模式直接管理,
|
||||
|
||||
@@ -158,6 +158,14 @@ function toNavItem(node) {
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
/* 白色侧栏,紧贴左/上/下边缘(父面板无 padding、无圆角)。
|
||||
* 右侧投影参考 soybean-admin 的 sider 设计:
|
||||
* box-shadow: 2px 0 8px 0 rgb(29, 35, 41, 0.05)
|
||||
* 2px 右偏移、8px 模糊、5% 透明度 —— 极淡克制的细腻投影,紧贴右缘自然分隔,
|
||||
* 不用浓阴影(会显脏)。深色模式下 --fms-card 自动变为深灰卡,无需额外覆盖 */
|
||||
background: var(--fms-card);
|
||||
box-shadow: 2px 0 4px 0 rgb(29, 35, 41, 0.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 团队切换(对齐 SidebarHeader p-2 + TeamSwitcher h-12) */
|
||||
@@ -210,6 +218,7 @@ function toNavItem(node) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
/* 主色底 + 白图标(与白侧栏形成反差,保持醒目) */
|
||||
background: var(--fms-primary);
|
||||
color: var(--fms-primary-text);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { computed, inject, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Moon,
|
||||
Palette,
|
||||
PanelLeft,
|
||||
PanelRight,
|
||||
Sun,
|
||||
} from '@lucide/vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -27,6 +28,11 @@ const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
/* 侧栏折叠/展开按钮逻辑由布局(DefaultLayout)提供:
|
||||
* 折叠态 → 展开并恢复宽度;展开态但被拖得很窄 → 展开到默认宽度(不折叠);
|
||||
* 其余 → 正常折叠。兜底为 store 的简单切换 */
|
||||
const toggleSidebar = inject('toggleSidebar', () => appStore.toggleSidebar())
|
||||
|
||||
/* 深浅色切换:原 ThemeToggle 组件逻辑内联,按钮样式复用 .fms-icon-btn */
|
||||
const isDark = computed(() => appStore.darkMode)
|
||||
function toggleDark() {
|
||||
@@ -95,15 +101,15 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
|
||||
|
||||
<template>
|
||||
<header class="fms-topbar">
|
||||
<!-- 左侧:折叠按钮 -->
|
||||
<!-- 左侧:折叠/展开按钮(图标随状态切换:折叠态显示"展开"图标) -->
|
||||
<div class="fms-topbar-left">
|
||||
<Button
|
||||
type="ghost"
|
||||
class="fms-icon-btn"
|
||||
:title="appStore.sidebarCollapsed ? '展开侧栏' : '折叠侧栏'"
|
||||
@click="appStore.toggleSidebar()"
|
||||
@click="toggleSidebar()"
|
||||
>
|
||||
<PanelLeft />
|
||||
<component :is="appStore.sidebarCollapsed ? PanelRight : PanelLeft" />
|
||||
</Button>
|
||||
|
||||
<span class="fms-separator" aria-hidden="true" />
|
||||
@@ -212,10 +218,12 @@ onBeforeUnmount(() => document.removeEventListener('fullscreenchange', syncFulls
|
||||
gap: 8px;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
/* 顶栏独立成一块:与 .fms-main 同款白底 + 圆角 + 同款投影,与下方白卡并列分层 */
|
||||
/* 顶栏紧贴上边缘(主区面板上下无 padding);左下+右下圆角。
|
||||
* 向下投影参考 soybean-admin 的 header 设计(0 1px 2px rgb(0,21,41,0.08)),
|
||||
* 配合底圆角让白卡从灰画布上微微浮起,与侧栏的"右侧投影"角色互补 */
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
box-shadow: var(--fms-shadow);
|
||||
border-radius: 0 0 var(--fms-control-radius) var(--fms-control-radius);
|
||||
box-shadow: 0 2px 4px rgb(0, 21, 41, 0.08);
|
||||
}
|
||||
|
||||
.fms-topbar-left,
|
||||
|
||||
@@ -218,8 +218,7 @@ function onToggle() {
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
/* 激活高亮:主色淡底 + 主色文字(与树选中态一致)。
|
||||
侧栏为浅灰底时白底+阴影会形成突兀的白块,改用主色淡底融入浅灰侧栏 */
|
||||
/* 激活高亮:主色淡底 + 主色文字(与树选中态一致) */
|
||||
.fms-nav-link.is-active {
|
||||
background: color-mix(in srgb, var(--fms-primary) 10%, transparent);
|
||||
color: var(--fms-primary);
|
||||
|
||||
@@ -149,9 +149,9 @@ const totalCells = computed(() => columns.value.length * dataSource.value.length
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
/* 页面高度 = fms-content 可视内容高度(100vh - 顶栏40 - 布局gap8 - 页签40 - 内容padding 12+16),
|
||||
/* 高度 = 内容区可视高度(由布局实测写入 --fms-content-height,自动跟随顶栏/页签/padding),
|
||||
让滚动条落在表格内部而不是整个页面滚动 */
|
||||
height: calc(100vh - 116px);
|
||||
height: var(--fms-content-height, calc(100vh - 116px));
|
||||
color: var(--fms-text);
|
||||
}
|
||||
|
||||
@@ -204,16 +204,16 @@ const totalCells = computed(() => columns.value.length * dataSource.value.length
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ---- demo 卡片:工具栏 + 表格 ---- */
|
||||
/* ---- demo 卡片:工具栏 + 表格(灰底画布上的白卡,靠阴影分层,无边框线) ---- */
|
||||
.demo-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 8px;
|
||||
background: var(--fms-card);
|
||||
box-shadow: var(--fms-shadow);
|
||||
}
|
||||
|
||||
.demo-toolbar {
|
||||
|
||||
@@ -70,9 +70,9 @@ const counter = ref(0)
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: var(--fms-control-radius);
|
||||
background: var(--fms-card);
|
||||
box-shadow: var(--fms-shadow);
|
||||
}
|
||||
|
||||
.detail-hint {
|
||||
|
||||
@@ -193,9 +193,9 @@ function clearEvents() {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: var(--fms-control-radius);
|
||||
background: var(--fms-card);
|
||||
box-shadow: var(--fms-shadow);
|
||||
|
||||
&--grow {
|
||||
flex: 1;
|
||||
|
||||
+90
-8
@@ -12,7 +12,7 @@ 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 } from '@lucide/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'
|
||||
@@ -112,6 +112,24 @@ 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
|
||||
@@ -168,6 +186,10 @@ async function save() {
|
||||
@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"
|
||||
@@ -216,6 +238,25 @@ async function save() {
|
||||
{{ 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="启用/停用"
|
||||
@@ -268,6 +309,26 @@ async function save() {
|
||||
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;
|
||||
@@ -310,20 +371,26 @@ async function save() {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 8px;
|
||||
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: 1px 0;
|
||||
padding: 3px 6px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: var(--fms-card);
|
||||
}
|
||||
}
|
||||
|
||||
.mm-groups__node-name {
|
||||
@@ -346,26 +413,41 @@ async function save() {
|
||||
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: 20px;
|
||||
height: 20px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+996
-734
File diff suppressed because it is too large.
Load diff
@@ -363,7 +363,11 @@ async function onSwitchSave() {
|
||||
switchSaving.value = true
|
||||
try {
|
||||
const ok = await saveModule()
|
||||
if (!ok) return
|
||||
if (!ok) {
|
||||
// 保存失败(原因已由 saveModule 内部提示):留在当前对象,弹窗不关闭,可重试或放弃
|
||||
Message.warning('保存未完成,已留在当前对象,请处理后再切换')
|
||||
return
|
||||
}
|
||||
moduleSelectedId.value = state.pendingId
|
||||
switchConfirm.value = null
|
||||
} finally {
|
||||
@@ -527,6 +531,26 @@ async function deleteModule(id) {
|
||||
if (powers.length) {
|
||||
reqs.push({ table: 's_power', key_field: 'b_id', deletes: powers.map((r) => ({ b_id: r.b_id })) })
|
||||
}
|
||||
// 多语言(s_i18n 无 b_module_id 字段,需按模块编码匹配 b_key 前缀)。
|
||||
// 若不清理,删除后残留的孤儿键会被同编码新模块的多语言面板加载,
|
||||
// 进而产生指向已不存在记录的 UPDATE,导致后端「修改记录数异常: 0」。
|
||||
const moduleCode = String(module.b_code || '').trim()
|
||||
if (moduleCode) {
|
||||
const prefix = module.b_module_type === 'category' ? 'menu' : 'module'
|
||||
const i18nRes = await loadDataApi(
|
||||
's_i18n',
|
||||
`b_key = '${prefix}.${moduleCode}' OR b_key LIKE '${prefix}.${moduleCode}.%' ` +
|
||||
`OR b_key LIKE 'field.${moduleCode}.%' OR b_key LIKE 'power.${moduleCode}.%'`,
|
||||
)
|
||||
const i18nRows = i18nRes.data || []
|
||||
if (i18nRows.length) {
|
||||
reqs.push({
|
||||
table: 's_i18n',
|
||||
key_field: 'b_id',
|
||||
deletes: i18nRows.map((r) => ({ b_id: r.b_id })),
|
||||
})
|
||||
}
|
||||
}
|
||||
reqs.push({ table: 's_module', key_field: 'b_id', deletes: [{ b_id: id }] })
|
||||
await saveObjectApi(reqs)
|
||||
// 本地移除整棵子树(未保存子孙随父一并清理),并清除未保存标记
|
||||
@@ -1003,7 +1027,6 @@ loadModuleTree()
|
||||
<Tabs
|
||||
v-if="drafts"
|
||||
v-model="activeSection"
|
||||
type="pill"
|
||||
nav-only
|
||||
class="mm-config-nav"
|
||||
:items="configNavItems"
|
||||
@@ -1235,9 +1258,17 @@ loadModuleTree()
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Splitter 适配:面板内容各自内部滚动,避免双滚动条 */
|
||||
/* Splitter 适配:
|
||||
* - 面板内容各自内部滚动,避免双滚动条
|
||||
* - 面板左右留 3px,两张白卡之间形成灰缝(3 + 分隔条 6 + 3 = 12px),无需边框线
|
||||
* - 隐藏分隔条常显的 1px 线(hover 时的手柄仍会显示,可拖拽调宽) */
|
||||
.mm-body-splitter :deep(> .fms-splitter-panel) {
|
||||
overflow: hidden;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.mm-body-splitter :deep(.fms-splitter-bar::before) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ---- 左树顶部 header:搜索框在左,操作在右,无边框无标题 ---- */
|
||||
@@ -1296,6 +1327,11 @@ loadModuleTree()
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 8px;
|
||||
/* 灰底画布上的白卡:白底 + 圆角 + 阴影,与右侧内容卡用灰缝分隔 */
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
box-shadow: var(--fms-shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mm-tree-panel__scroll {
|
||||
@@ -1350,6 +1386,10 @@ loadModuleTree()
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
/* 灰底画布上的白卡:与左侧树卡用灰缝分隔 */
|
||||
background: var(--fms-card);
|
||||
border-radius: var(--fms-control-radius);
|
||||
box-shadow: var(--fms-shadow);
|
||||
}
|
||||
|
||||
.mm-main__empty {
|
||||
@@ -1379,28 +1419,9 @@ loadModuleTree()
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ---- 配置导航:横向胶囊式 Tabs,位于 header 内左侧,操作按钮贴右 ---- */
|
||||
/* ---- 配置导航:横向胶囊式 Tabs,位于工作区顶部,操作按钮在 rightExtra 插槽 ---- */
|
||||
/* ---- 配置导航:横向 Tab 导航(line 类型,不作内部覆盖,组件默认样式即可) ---- */
|
||||
.mm-config-nav.tabs {
|
||||
flex: none;
|
||||
/* 与左侧树 header 的分割线对齐:左右两栏 header 同高同线,视觉统一 */
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.mm-config-nav :deep(.tabs-nav) {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
/* 标签列表由 Scrollbar 组件接管滚动,这里只保持垂直居中 */
|
||||
.mm-config-nav :deep(.tabs-nav-list) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* rightExtra 操作区与标签在同一行垂直居中 */
|
||||
.mm-config-nav :deep(.tabs-nav-extra) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
/* 标签内布局:图标 + 文字 + badge */
|
||||
|
||||
@@ -62,9 +62,9 @@ const index = computed(() => Number(route.params.id ?? 0))
|
||||
|
||||
.fms-test-page__card {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: var(--fms-control-radius);
|
||||
background: var(--fms-card);
|
||||
box-shadow: var(--fms-shadow);
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
@@ -1152,6 +1152,10 @@ describe('ModuleEditConfigPanel(表单设计视图)', () => {
|
||||
expect(wrapper.text()).toContain('未分组')
|
||||
// 属性面板初始未选中字段(子标题显示"未选中")
|
||||
expect(wrapper.find('.mm-fdesign__inspector .mm-fdesign__subhead').text()).toContain('未选中')
|
||||
// 左栏只放字段:不显示「字段库」标题与布局组件入口
|
||||
expect(wrapper.text()).not.toContain('字段库')
|
||||
expect(wrapper.text()).not.toContain('布局组件')
|
||||
expect(wrapper.text()).not.toContain('分组容器')
|
||||
// 数据视图表格默认隐藏
|
||||
expect(wrapper.find('.mm-edit__data').attributes('style')).toContain('display: none')
|
||||
})
|
||||
@@ -1220,8 +1224,8 @@ describe('ModuleEditConfigPanel(表单设计视图)', () => {
|
||||
]
|
||||
const wrapper = mountEdit({ fields: extraFields })
|
||||
await nextTick()
|
||||
// 字段库渲染全部字段项(4 字段 + 2 布局组件)
|
||||
expect(wrapper.findAll('.mm-fdesign__lib-item').length).toBe(6)
|
||||
// 字段库渲染全部字段项(4 个字段,无布局组件)
|
||||
expect(wrapper.findAll('.mm-fdesign__lib-item').length).toBe(4)
|
||||
// 画布分组渲染现有字段
|
||||
expect(wrapper.findAll('.mm-fdesign__field').length).toBe(3)
|
||||
})
|
||||
@@ -2354,7 +2358,7 @@ describe('ModuleEditConfigPanel(表单设计视图)', () => {
|
||||
const subtitles = wrapper.findAll('.mm-fdesign__lib-subtitle').map((s) => s.text())
|
||||
// 首次出现顺序:输入框(input)、金额(money)、下拉选择(select)、文本域(textarea)
|
||||
expect(subtitles).toEqual(['输入框', '金额', '下拉选择', '文本域'])
|
||||
expect(wrapper.findAll('.mm-fdesign__lib-item').length).toBe(6) // 4 字段 + 2 布局组件
|
||||
expect(wrapper.findAll('.mm-fdesign__lib-item').length).toBe(4) // 4 字段,无布局组件
|
||||
})
|
||||
|
||||
it('智能偏向判定:按比例最近边缘,鼠标偏向哪条边就插到哪边', async () => {
|
||||
@@ -2710,13 +2714,16 @@ describe('ModuleEditConfigPanel(子表表格设计)', () => {
|
||||
|
||||
// 带一个分组(g1 金额信息)的挂载:f1/f2 在组内,f3 未分组
|
||||
|
||||
it('头部编辑方式下拉:显示表单设计/表格设计,切换 emit update:editMode', async () => {
|
||||
it('头部编辑方式分段控件:主表表单/子表表格,切换 emit update:editMode', async () => {
|
||||
const wrapper = mountTableEdit()
|
||||
await nextTick()
|
||||
// 第一个 Select 为头部编辑方式(modelValue = props.editMode)
|
||||
const modeSelect = wrapper.findAllComponents({ name: 'Select' })[0]
|
||||
expect(modeSelect.props('modelValue')).toBe('table')
|
||||
modeSelect.vm.$emit('update:model-value', 'form')
|
||||
const modeBtns = wrapper.findAll('.mm-edit__mode-btn')
|
||||
expect(modeBtns).toHaveLength(2)
|
||||
expect(modeBtns[0].text()).toContain('主表表单')
|
||||
expect(modeBtns[1].text()).toContain('子表表格')
|
||||
// 当前 editMode=table → 第二个按钮激活
|
||||
expect(modeBtns[1].classes()).toContain('is-active')
|
||||
await modeBtns[0].trigger('click')
|
||||
await nextTick()
|
||||
expect(wrapper.emitted('update:editMode').at(-1)[0]).toBe('form')
|
||||
})
|
||||
@@ -2915,7 +2922,10 @@ describe('ModuleEditConfigPanel(子表表格设计)', () => {
|
||||
}
|
||||
|
||||
function previewHandle(fieldId) {
|
||||
return document.body.querySelector(`.mm-tdesign__resize-handle[data-field-id="${fieldId}"]`)
|
||||
// 限定在预览弹窗内查询列宽手柄,避免命中残留 DOM
|
||||
return document.body.querySelector(
|
||||
`.mm-tdesign__preview-wrap .mm-tdesign__resize-handle[data-field-id="${fieldId}"]`,
|
||||
)
|
||||
}
|
||||
|
||||
it('预览弹窗拖拽调整列宽:实时更新表头,松手后 emit update(b_width)', async () => {
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
# 代码审查问题清单
|
||||
|
||||
> 审查范围:`ModuleEditConfigPanel.vue`(4510 行)、`index.vue`、`FieldGroupManager.vue`、`FmsTable.vue`
|
||||
> 日期:2026-08-26
|
||||
|
||||
---
|
||||
|
||||
## 一、功能性 bug(会真实出错)
|
||||
|
||||
### 1. 子表预览弹窗没有打开入口
|
||||
**P1**
|
||||
- `previewOpen` 定义于 `ModuleEditConfigPanel.vue:611`,弹窗渲染于 1998–2087 行
|
||||
- 全文件检索不到任何设置 `previewOpen = true` 的地方,也没有「预览」按钮
|
||||
- 弹窗 `:open` 永远为 false → **子表预览功能不可达**
|
||||
- `tableColumnRows`/`previewGroupHeader`/列宽拖拽手柄那一整套 WYSIWYG 预览全被闲置
|
||||
|
||||
### 2. `libGroup`/`columnGridGroup` 分组配置不一致(跨分组拖拽可能失效)
|
||||
- 字段库:`libGroup = { name: 'form', pull: 'clone', put: false }`(1461 行)
|
||||
- 子表列网格:`columnGridGroup = { name: 'form' }`(1464 行)→ 子表列之间可以互相移动
|
||||
- **主表画布网格:内联 `:group="{ name: 'form' }"`(2165 行),默认 `pull: true`** → 字段库(`put: false`)往主表画布放 → 会被 SortableJS 拒绝
|
||||
- 主表模式其实有自己的 `onLibDragEnd`(1368 行)兜底逻辑,注释也说"源=字段库,目标列表只收到 add/sort"
|
||||
- 这是一个「看起来会被拒、实际靠 onLibDragEnd 兜底」的双轨实现——逻辑相当绕,一旦某个 onMove/onStart/onEnd 时序变化就会暴露。**主表拖入的可靠性完全押在 sortable 事件顺序上**,比较脆弱。
|
||||
|
||||
### 3. 分组标题重命名有 blur/点击竞态(很可能被覆盖丢改动)
|
||||
- `commitEditGroupTitle` 同时绑定在输入框 `@blur` 和确认按钮 `@mousedown.prevent`(1806、1816、1824 行)
|
||||
- 在按钮上按下鼠标 → `@mousedown.prevent` 阻止了默认行为,不会先触发 blur,顺序基本 OK
|
||||
- 真正的坑是:**直接点「删除分组」或拖拽到其它分组时**,输入框先失焦提交、随后 `updateGroups`/`syncList` 把 props.groups 换成新数组,但 `groupTitleDraft` 只在 commit 里清。整体靠时序兜底,多步操作间容易丢改动(详见第 11 条联动问题)。
|
||||
|
||||
### 4. 表格模式下列宽与 `b_height` 语义混乱
|
||||
**P0**
|
||||
- `formDataColumns`(315 行)里 `b_height` 是「文本域高度」,但 `b_height` 会被 `normalizedRow` 统一 `Number()` 化(160 行);表格模式 `tableDataColumns` 没有 b_height 列
|
||||
- **`b_width` 在 diff/save 时会被完整提交**——它既不是后端字段也不是 UI 控件,只是一次拖拽调宽的临时状态,随保存写库会造成脏数据(后端 `s_field_edit` 若没有该列,`update` 会因"字段不存在"报错或写进多余列)
|
||||
- **栅格宽度口径不一致**:主表设计视图用「24 栅格」,但 `COLSPAN_OPTIONS`(428 行)写的是 `整行 24/24 / 2/3 行 16/24 / 半行 12/24…`——文本写着 24 栅格,但 `spanOf` 支持 1~24,`b_colspan` 默认 24。如果后端 `b_colspan` 存的是 12 制(计划文档 §10.2 说"12 栅格"),前端按 24 制渲染会导致整行永远多一倍宽。需要和后端表结构核对,目前前后端口径不一致的风险很高。
|
||||
|
||||
---
|
||||
|
||||
## 二、交互体验问题
|
||||
|
||||
### 5. 字段库「点击=添加」与「拖拽=添加」语义不一致
|
||||
**P1**
|
||||
- 点击字段库项 → `selectField`(862 行):**已加入的字段再点击会被移除**(867 行)
|
||||
- 拖拽字段到画布 → 已在表单中会被 `Message.warning` 拒绝(1386 行)
|
||||
- 同一动作两种结果,用户容易误删字段。建议点击已加入项改为「定位选中」,把移除收敛到 Delete/删除按钮。
|
||||
|
||||
### 6. 拖拽意图判定在窄卡片上极易误判
|
||||
- `updateDragIntent`(959 行)按「鼠标到卡片四条边的归一化最近距离」决定插入方向(1024-1030 行)
|
||||
- 字段卡片通常只有 1/2~1/4 行宽(约 150~300px),宽高比很大,**左右方向几乎总是胜出** → 用户想「拖到下一行」时总是落在同行前后
|
||||
- 加上 hover 自身只给上/下(998-1009 行),两种判定阈值不一致,操作学习成本高、误操作率高
|
||||
|
||||
### 7. 拖拽期间 `dragLine` 只清不锁
|
||||
- Esc 取消只清 `dragLine`/`dragOverSection`(125-129 行),SortableJS 的拖拽会话仍继续
|
||||
- **中途 Esc 后继续移动,`onDragShadowMove` 会重新计算 intent**,表现为"取消失效"
|
||||
|
||||
### 8. 撤销栈是「操作级」而非「原子级」,且深度受限
|
||||
- `pushHistory` 只压 `snapshotRows`(浅拷贝数组),相邻重复快照去重(99 行)
|
||||
- `onColspanInput`(1503)每次 input 事件压栈 → 连续输入数字产生 N 个撤销点
|
||||
- `onWidthInput`(533)同样逐键压栈
|
||||
- 列宽拖拽 `startColumnResize` 压一次但 `onColumnResizeMove` 每帧改 `row.b_width`,mouseup 时才 emit → **撤销栈里的中间宽度和最终宽度不一致**,Ctrl+Z 会跳到拖拽中间值
|
||||
- 这些组合下 30 条 HISTORY_LIMIT 很快就耗尽,且撤销点不直观
|
||||
|
||||
### 9. 删除字段的「撤销提示条」有 8 秒超时但无进度
|
||||
- `undoTimer` 8 秒后清 `lastDeleted`(1539 行),提示条消失**且无倒计时/动画提示**
|
||||
- 若用户晚于 8 秒想恢复,只能靠 Ctrl+Z(且此时 Ctrl+Z 撤销的是整表快照,会连带恢复已删除的字段布局,可能更糟)
|
||||
|
||||
### 10. 分组方向(左右/上下)开关被隐藏
|
||||
- `toggleLayout` 定义在 `FieldGroupManager.vue:111`,但只有「分组管理」弹窗里每个节点有该按钮(280-283 行)
|
||||
- 主画布上只有 hover 才出现的分组删除按钮,**画布上没有任何分组方向切换入口**——用户必须打开弹窗才能调左右布局,体验割裂
|
||||
|
||||
### 11. 删除分组直接改动 listConfig/editConfig,但不进撤销栈
|
||||
- `deleteGroupDirect`(760 行)会改 `props.listConfig`(`syncList`)并把字段移未分组
|
||||
- 但这些改动**不推入 history**,Ctrl+Z 无法回退分组删除本身(只能回退字段归属)
|
||||
|
||||
---
|
||||
|
||||
## 三、一致性与可维护性
|
||||
|
||||
### 12. `formDirection`/`globalDirection` 死代码
|
||||
**P0**
|
||||
- `formDirection = computed(() => globalDirection.value)`(644 行)引用的 `globalDirection` 全文件不存在
|
||||
- **该 computed 会在首次求值时抛 ReferenceError**,任何路径访问 `formDirection` 都会崩
|
||||
- 目前没人引用它所以没炸,但这是颗定时炸弹
|
||||
|
||||
### 13. 大量字段/状态重复定义
|
||||
- `FIELD_TYPE_LABELS`(416)在 `libraryGroups`(667)和 `controlTypeLabel`(468)各用一次,类型集散落在 `placeholderOf`(481)、`defaultWidthOf`(512)、`SAMPLE_TEXT`(628)、`field-control` 模板(2254-2298)四处,新增类型要改五个地方
|
||||
- 属性面板(主表 2527、子表 2385)几乎整段重复,只有宽度/高度两处不同
|
||||
- `COLSPAN_OPTIONS` 只在数据视图用(363 行 `editor: { type: 'select', options: () => COLSPAN_OPTIONS }`),**设计视图属性面板用的是 `mm-fdesign__span-control` 数字输入框**,两处口径(24 制 vs 数字 1-24)分开维护
|
||||
|
||||
### 14. `placeholderOf` 只读字段被忽略
|
||||
- `placeholderOf`(481 行)返回默认占位符,但 `placeholder` 在画布控件上是**只读展示**(`tabindex="-1"` + `pointer-events:none`)
|
||||
- 属性面板里根本没有占位符编辑入口(只有数据视图的 FmsTable 有 `b_placeholder` 列)
|
||||
- 即**设计视图里无法设置占位符**——功能存在但 UI 缺失
|
||||
|
||||
### 15. 快捷键在「预览(数据视图)」视图下静默失效
|
||||
- `onDesignerKeydown` 要求 `viewMode === 'design'`(1569 行),但数据视图的 FmsTable 里 Ctrl+C/V/Z 会被浏览器/表格接管
|
||||
- 设计视图的复制/撤销/Delete 快捷键全不生效,且没有任何提示(帮助浮层只在设计视图头部)
|
||||
- 用户切到数据视图按 Delete 想删字段会直接没反应
|
||||
|
||||
### 16. 无保存时「重载」与「切换」的确认文案不一致
|
||||
- `index.vue` 用两个不同的弹窗:重载用通用 `showConfirm`(236-244),切换用专门的 `switchConfirm`(332-341、1221-1236)
|
||||
- 两个弹窗行为基本一致但代码/文案/按钮布局重复
|
||||
- 切换弹窗缺「保存并切换失败时的错误提示」路径:`onSwitchSave` 里 `saveModule()` 返回 false 时直接留在弹窗,没有给用户反馈为什么失败
|
||||
|
||||
---
|
||||
|
||||
## 建议优先级
|
||||
|
||||
| 优先级 | 事项 | 理由 |
|
||||
|--------|------|------|
|
||||
| P0 | 修 `formDirection`/`globalDirection` 死代码(#12) | 一旦有人引用就崩 |
|
||||
| P0 | 统一 24/12 栅格口径,核对 `b_colspan` 后端字段(#4) | 前后端不一致=整行渲染错误 |
|
||||
| P1 | 给子表预览加入口或移除死代码(#1) | 功能不可达 |
|
||||
| P1 | 撤销栈改「原子操作」+ 修复列宽拖拽中间态(#8) | 直接影响日常编辑 |
|
||||
| P1 | 字段库点击/拖拽语义统一(#5) | 防误删 |
|
||||
| P2 | 拖拽意图判定、Esc 取消锁定、删除分组入撤销栈等(#6/#7/#11) | 打磨体验 |
|
||||
| P2 | 占位符编辑入口、分组方向画布入口、数据视图快捷键(#14/#10/#15) | 功能缺失 |
|
||||
Reference in new issue
Block a user