-
-
-
-
+
删除列
+
+
+
+
+
+ 分组带
+
+
-
-
-
状态
-
-
-
必填
-
+
+
+
+ 第 {{ selectedBand.level + 1 }} 级
+
+
+
+ {{ selectedBand.span }}
+
-
-
只读
-
+
-
-
禁用
-
+
操作
+
+
+
+
+
+
+
+
+ 选择一列或一个表头分组带进行配置
-
- 显示
-
-
-
-
操作
-
-
+
+
+
@@ -1874,8 +1814,7 @@ onBeforeUnmount(() => {
}
/* ---- 中间列顶部 header(左 = 编辑方式/布局;中 = 视图切换;右 = 操作),
- * 底部 1px 分割线与画布分隔。左侧控件增多后不再绝对居中视图切换,
- * 改为左右两翼弹性伸缩、中间固定,避免重叠 ---- */
+ * 底部 1px 分割线与画布分隔 ---- */
.mm-edit__head {
display: flex;
align-items: center;
@@ -1885,7 +1824,6 @@ onBeforeUnmount(() => {
border-bottom: 1px solid var(--fms-border);
}
-/* 视图切换:左/右两翼 flex 伸缩挤出中间位,保证居中 */
.mm-edit__head-center {
flex: none;
display: inline-flex;
@@ -1921,7 +1859,6 @@ onBeforeUnmount(() => {
background: var(--fms-secondary);
}
-/* 编辑方式分段按钮(主表表单 / 子表表格) */
.mm-edit__mode-btn {
height: 26px;
padding: 0 12px;
@@ -2000,7 +1937,7 @@ onBeforeUnmount(() => {
font-size: 12px;
font-weight: 600;
line-height: 1;
- padding-bottom: 1px; /* 问号字形重心略偏上,下移 1px 视觉居中 */
+ padding-bottom: 1px;
cursor: help;
transition:
border-color 0.15s,
@@ -2064,41 +2001,38 @@ onBeforeUnmount(() => {
}
}
-/* ---- 设计视图:工作区模型(三栏统一灰底,白色表单纸面居中浮起) ---- */
+/* ---- 设计视图:左字段库 | 主区(上预览表 / 下分组树+属性) ---- */
.mm-fdesign {
display: grid;
- grid-template-columns: 184px minmax(0, 1fr) 280px;
+ grid-template-columns: 184px minmax(0, 1fr);
flex: 1;
min-height: 0;
min-width: 0;
- /* 全白三栏 + 分割线:左/右 1px 竖线分隔,header 在中间列顶部
- 字段库略窄、属性面板略宽,突出字段配置 */
background: var(--fms-card);
}
-/* 属性面板折叠时收窄为展开条 */
-.mm-fdesign.is-inspector-collapsed {
- grid-template-columns: 196px minmax(0, 1fr) 32px;
-}
-
-/* 画布区:中间列主体(header 下方),浅灰底与左右白面板区分,
- * 字段控件透明排布在灰底上,通过输入框自带边框区分 */
-.mm-fdesign__canvas-wrap {
- flex: 1;
- min-width: 0;
+/* 预览区:最上面的实时表格,只拖列宽 */
+.mm-fdesign__preview {
+ flex: 0 0 auto;
+ max-height: 46%;
min-height: 0;
overflow: auto;
+ border-bottom: 1px solid var(--fms-border);
background: var(--fms-background);
}
-.mm-fdesign__canvas {
- min-width: 100%;
- min-height: 100%;
- padding: 10px;
- box-sizing: border-box;
+/* 关键处理区:分组树(多级)| 属性面板 */
+.mm-fdesign__config {
+ flex: 1;
+ min-height: 0;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 280px;
+}
+
+.mm-fdesign__config.is-inspector-collapsed {
+ grid-template-columns: minmax(0, 1fr) 32px;
}
-/* 左栏:白底 + 右分割线(与中间列分隔) */
.mm-fdesign__lib {
border-right: 1px solid var(--fms-border);
overflow: auto;
@@ -2106,7 +2040,6 @@ onBeforeUnmount(() => {
background: var(--fms-card);
}
-/* 中间列:flex column(header + 画布) */
.mm-fdesign__main {
display: flex;
flex-direction: column;
@@ -2114,7 +2047,74 @@ onBeforeUnmount(() => {
min-height: 0;
}
-/* 右栏:白底 + 左分割线(与中间列分隔) */
+/* 分组树面板 */
+.mm-fdesign__tree-panel {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ min-height: 0;
+ padding: 8px;
+ border-right: 1px solid var(--fms-border);
+ overflow: hidden;
+}
+
+.mm-fdesign__tree-head {
+ flex: none;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 2px 4px 8px;
+ color: var(--fms-text);
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.mm-fdesign__tree-head-meta {
+ color: var(--fms-text-secondary);
+ font-size: 10px;
+ font-weight: 400;
+}
+
+.mm-fdesign__tree-scroll {
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+}
+
+.mm-fdesign__tree-empty {
+ padding: 12px 8px;
+ color: var(--fms-text-secondary);
+ font-size: 11px;
+ text-align: center;
+}
+
+.mm-fdesign__tree-title {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+
+ &.is-disabled {
+ color: var(--fms-text-disabled, #9ca3af);
+ }
+}
+
+.mm-fdesign__tree-label {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.mm-fdesign__tree-meta {
+ margin-left: auto;
+ flex: none;
+ color: var(--fms-text-secondary);
+ font-size: 10px;
+ font-weight: 400;
+}
+
.mm-fdesign__inspector {
border-left: 1px solid var(--fms-border);
overflow: auto;
@@ -2122,7 +2122,6 @@ onBeforeUnmount(() => {
background: var(--fms-card);
}
-/* 折叠态:面板内容隐藏,仅保留展开按钮 */
.mm-fdesign__lib.is-collapsed,
.mm-fdesign__inspector.is-collapsed {
padding: 0;
@@ -2132,7 +2131,6 @@ onBeforeUnmount(() => {
overflow: hidden;
}
-/* 折叠后的展开按钮:占满折叠列,图标置于顶部 */
.mm-fdesign__panel-expand {
display: flex;
align-items: flex-start;
@@ -2172,20 +2170,6 @@ onBeforeUnmount(() => {
font-size: 10px;
}
-/* 「源已移除」告警徽标:供后续若字段来源缺失时高亮使用(仅样式层) */
-.mm-fdesign__stale-tag {
- display: inline-flex;
- align-items: center;
- padding: 0 5px;
- border: 1px solid color-mix(in srgb, var(--fms-danger, #f5222d) 45%, transparent);
- border-radius: var(--fms-control-radius, 4px);
- background: color-mix(in srgb, var(--fms-danger, #f5222d) 10%, var(--fms-card));
- color: var(--fms-danger, #f5222d);
- font-size: 10px;
- line-height: 16px;
-}
-
-/* 面板头部折叠按钮 */
.mm-fdesign__collapse {
display: inline-flex;
align-items: center;
@@ -2208,6 +2192,7 @@ onBeforeUnmount(() => {
}
}
+/* ---- 左栏字段库 ---- */
.mm-fdesign__lib-search {
width: 100%;
height: 26px;
@@ -2265,7 +2250,6 @@ onBeforeUnmount(() => {
background: color-mix(in srgb, var(--fms-primary) 14%, var(--fms-card));
color: var(--fms-primary);
}
- /* 已加入表单的字段:不弱化视觉,与未加入字段一致(仅靠右侧 ✓ 标记区分) */
}
.mm-fdesign__lib-name {
@@ -2297,78 +2281,19 @@ onBeforeUnmount(() => {
text-align: center;
}
-/* ---- 画布 ---- */
-/* 透明画布:去掉白色纸面,让浅灰工作区底色透出,白色分组卡片直接浮于其上 */
-.mm-fdesign__sheet {
- position: relative; /* 拖拽行高亮框的绝对定位参照 */
- min-height: 120px;
-}
-
-/* ---- 分组卡片(主表 / 子表统一,参考 demo.html 分组样式) ---- */
-.mm-fdesign__section,
-.mm-tdesign__section {
- margin-top: 12px;
- border: 1px solid var(--fms-border);
- border-radius: 8px;
- background: var(--fms-card);
- box-shadow: 0 1px 2px rgba(39, 54, 70, 0.05);
- /* 不用 overflow:hidden —— 表单字段的悬浮工具栏/拖拽提示需浮出卡片上缘 */
- transition:
- border-color 0.15s,
- box-shadow 0.15s;
-
- &:hover {
- border-color: color-mix(in srgb, var(--fms-primary) 30%, var(--fms-border));
- box-shadow: 0 2px 8px rgba(39, 54, 70, 0.06);
- }
-
- &:first-of-type {
- margin-top: 0;
- }
-
- /* 未分组伪分区:虚线框 + 白底(与其他分组一致,不做特殊处理) */
- &.is-ungrouped {
- border-style: dashed;
- background: var(--fms-card);
- }
-}
-
-/* 分组标题行(主表 / 子表统一):白底 header 条,与卡片圆角裁剪 */
-.mm-fdesign__section-title,
-.mm-tdesign__section-title {
+/* ---- 空态 / 分组带标题 ---- */
+.mm-fdesign__empty {
display: flex;
align-items: center;
- gap: 8px;
- padding: 7px 12px;
- border-bottom: 1px solid var(--fms-border);
- color: var(--fms-text);
- font-size: 12px;
- font-weight: 600;
+ justify-content: center;
+ min-height: 80px;
+ color: var(--fms-text-secondary);
+ font-size: 11px;
}
-/* 分组名称:撑满剩余空间,超出省略 */
-.mm-fdesign__section-name {
- flex: 1;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-/* 分组名内联编辑:悬停提示可点击重命名 */
-.mm-fdesign__section-name.is-editable {
- cursor: text;
- border-bottom: 1px dashed transparent;
-
- &:hover {
- border-bottom-color: var(--fms-text-placeholder, #9ca3af);
- }
-}
-
-/* 分组名编辑输入框 + 确认/取消 */
.mm-fdesign__section-edit {
- flex: 0 1 200px;
- min-width: 120px;
+ flex: 1 1 auto;
+ min-width: 90px;
height: 24px;
padding: 0 6px;
border: 1px solid var(--fms-primary);
@@ -2412,288 +2337,180 @@ onBeforeUnmount(() => {
}
}
-.mm-fdesign__grid {
- display: grid;
- grid-template-columns: repeat(24, minmax(0, 1fr));
- gap: 6px;
- /* 空分组也保留可放置高度,避免拖拽热区为零 */
- min-height: 46px;
-}
-
-/* 表单分组内容区:标题条下方留白 */
-.mm-fdesign__section-body {
- padding: 10px 12px;
-}
-
-/* 空分组占位(draggable footer):无边框,仅一行提示文字 */
-.mm-fdesign__grid-empty {
- grid-column: span 24;
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 40px;
- color: var(--fms-text-secondary);
- font-size: 11px;
- pointer-events: none;
-}
-
-/* 拖拽中:除被拖卡片、悬停目标行、跟随光标的拖拽克隆与幽灵占位外,
- 其余卡片整体变淡(突出拖拽焦点) */
-.mm-fdesign__canvas-wrap.is-drag-active
- .mm-fdesign__field:not(.is-dragging):not(.is-drop-hover):not(.mm-fdesign__sortable-drag):not(
- .mm-fdesign__sortable-ghost
- ) {
- 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;
-}
-
-/* ---- 画布字段:所见即所得的真实控件(默认无背景无边框,直接排在白纸上;
- * hover 淡主色边框,选中虚线框;内部控件(input/checkbox 等)自带边框样式可区分) ---- */
-.mm-fdesign__field {
+/* ---- 子表:多级表头预览表格 ---- */
+.mm-tdesign__canvas {
position: relative;
- padding: 5px 8px 6px;
- border: 1px solid transparent;
- border-radius: 6px;
+ min-width: 100%;
+ min-height: 100%;
+ padding: 10px;
+ box-sizing: border-box;
+}
+
+.mm-tdesign__table {
+ border-collapse: separate;
+ border-spacing: 0;
+ width: max-content;
+ table-layout: fixed;
+ border: 1px solid var(--fms-border);
+ border-radius: 8px;
background: var(--fms-card);
- cursor: grab;
- /* 拖拽时禁用文本选中,避免 label 文字像鼠标框选一样出现蓝色高亮 */
+ box-shadow: 0 1px 2px rgba(39, 54, 70, 0.05);
+ /* 不用 overflow:hidden:会裁剪选中列悬浮在表头上方的工具栏 */
+}
+
+/* ---- 表头分组带(band,纯展示) ---- */
+.mm-tdesign__band {
+ padding: 6px 10px;
+ border: 1px solid var(--fms-border);
+ background: var(--fms-secondary);
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--fms-text);
+ text-align: center;
+ vertical-align: middle;
user-select: none;
-webkit-user-select: none;
transition:
border-color 0.15s,
box-shadow 0.15s,
- opacity 0.2s;
+ background 0.15s;
&:hover {
- border-color: color-mix(in srgb, var(--fms-primary) 35%, transparent);
+ background: color-mix(in srgb, var(--fms-primary) 6%, var(--fms-secondary));
}
&.is-selected {
- border-style: dashed;
- border-color: var(--fms-primary);
- box-shadow: none;
+ background: color-mix(in srgb, var(--fms-primary) 12%, var(--fms-secondary));
+ box-shadow: inset 0 0 0 1.5px var(--fms-primary);
}
- /* 正在拖拽的源字段:保持完整显眼(其余卡片整体变淡),带淡主题色底提示 */
- &.is-dragging {
- opacity: 1;
- background: color-mix(in srgb, var(--fms-primary) 6%, var(--fms-card));
- }
-
- /* 隐藏字段:降透明度 + 角标提示(运行时不渲染) */
- &.is-invisible {
- opacity: 0.55;
- }
-
- /* ---- 拖拽插入方向高亮:目标卡片对应侧边框高亮(阿里式 inset 阴影,不撑布局) ---- */
- &.is-drop-before {
- box-shadow: inset 2px 0 0 0 var(--fms-primary);
- }
-
- &.is-drop-after {
- box-shadow: inset -2px 0 0 0 var(--fms-primary);
- }
-
- &.is-drop-line-before {
- box-shadow: inset 0 2px 0 0 var(--fms-primary);
- }
-
- &.is-drop-newline {
- box-shadow: inset 0 -2px 0 0 var(--fms-primary);
+ &.is-disabled {
+ opacity: 0.5;
}
}
-/* 拖拽操作提示气泡:实时告知「松手后插入到哪个位置」。
- 必须高于自绘跟手卡片(body 上 z-index 2147483000),否则光标附近会被跟手卡盖住 */
-.mm-fdesign__drop-tip {
- position: absolute;
- z-index: 2147483001;
- pointer-events: none;
- padding: 3px 10px;
- border-radius: 12px;
- background: var(--fms-primary);
- color: #fff;
- font-size: 11px;
- font-weight: 600;
- line-height: 1.4;
- white-space: nowrap;
- box-shadow: 0 2px 8px color-mix(in srgb, var(--fms-primary) 35%, transparent);
- left: 50%;
- transform: translateX(-50%);
-
- /* 上方 / 下方新行:悬浮在卡片外侧边缘 */
- &.is-line-before {
- top: -30px;
- }
-
- &.is-newline {
- bottom: -30px;
- }
-
- /* 同行合并:贴在卡片左/右缘内侧(配合半区遮罩) */
- &.is-before {
- left: 4px;
- top: 50%;
- transform: translateY(-50%);
- }
-
- &.is-after {
- left: auto;
- right: 4px;
- top: 50%;
- transform: translateY(-50%);
- }
-}
-
-.mm-fdesign__field-label {
- display: flex;
- align-items: center;
- gap: 4px;
- margin-bottom: 4px;
- font-size: 11px;
- font-weight: 600;
- color: var(--fms-text);
-}
-
-.mm-fdesign__field-name {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.mm-fdesign__field-control {
- min-height: 26px;
-}
-
-/* 画布内真实控件(纯视觉预览:不置 disabled,避免灰态;pointer-events:none 穿透到卡片本体,
- tabindex=-1 防止 Tab 聚焦) */
-.mm-fdesign__ctrl {
- width: 100%;
- height: 26px;
- padding: 0 8px;
- border: 1px solid var(--fms-border);
- border-radius: 4px;
- background: var(--fms-card);
- color: var(--fms-text);
- font-size: 11px;
- font-family: inherit;
- outline: none;
- pointer-events: none;
-
- &::placeholder {
- color: var(--fms-text-placeholder, #9ca3af);
- }
-}
-
-textarea.mm-fdesign__ctrl {
- height: auto;
- padding: 5px 8px;
- resize: none;
- line-height: 1.5;
-}
-
-.mm-fdesign__ctrl-checkbox {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- height: 26px;
- padding: 0 4px;
- font-size: 11px;
- color: var(--fms-text);
- pointer-events: none; /* 纯预览,点击穿透到卡片 */
-}
-
-/* 选中悬浮工具栏 */
-.mm-fdesign__field-toolbar {
- position: absolute;
- top: -11px;
- right: 6px;
- display: inline-flex;
- align-items: center;
- gap: 1px;
- padding: 2px;
- border-radius: 5px;
- border: 1px solid var(--fms-border);
- background: var(--fms-card);
- box-shadow: 0 1px 4px rgba(39, 54, 70, 0.12);
-}
-
-.mm-fdesign__tool-btn {
+.mm-tdesign__band-title {
display: inline-flex;
align-items: center;
justify-content: center;
- width: 20px;
- height: 20px;
- padding: 0;
- border: none;
- border-radius: 3px;
- background: transparent;
- color: var(--fms-text-secondary);
- cursor: pointer;
+ gap: 6px;
+ min-width: 0;
+}
+
+/* ---- 列头 cell ---- */
+.mm-tdesign__cell {
+ position: relative;
+ padding: 5px 8px;
+ border: 1px solid var(--fms-border);
+ background: #fafbfc;
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--fms-text);
+ text-align: left;
+ vertical-align: middle;
+ user-select: none;
+ -webkit-user-select: none;
+ transition:
+ border-color 0.15s,
+ box-shadow 0.15s,
+ background 0.15s;
&:hover {
- background: var(--fms-secondary);
- color: var(--fms-text);
+ background: color-mix(in srgb, var(--fms-primary) 6%, #fafbfc);
}
- /* 配对按钮激活(已选中第一个字段) */
- &.is-active {
- background: color-mix(in srgb, var(--fms-primary) 16%, transparent);
- color: var(--fms-primary);
+ &.is-selected {
+ background: color-mix(in srgb, var(--fms-primary) 10%, #fff);
+ box-shadow: inset 0 0 0 1.5px var(--fms-primary);
}
- &.is-danger:hover {
- color: #d95757;
+ &.is-invisible {
+ opacity: 0.55;
}
}
+.mm-tdesign__cell-head {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ min-width: 0;
+}
+
+.mm-tdesign__cell--slot {
+ background: transparent;
+ border-style: dashed;
+ color: var(--fms-text-disabled, #9ca3af);
+ font-weight: 400;
+ text-align: center;
+}
+
+.mm-tdesign__slot-text {
+ font-size: 14px;
+ line-height: 1;
+}
+
+.mm-tdesign__col-width {
+ flex: none;
+ color: color-mix(in srgb, var(--fms-text-secondary) 70%, var(--fms-text));
+ font-size: 10px;
+ font-weight: 400;
+ font-variant-numeric: tabular-nums;
+}
+
+/* 列宽拖拽手柄:hover 常显一条竖线,拖拽命中区稍宽 */
+.mm-tdesign__resizer {
+ position: absolute;
+ top: 0;
+ right: -1px;
+ bottom: 0;
+ width: 7px;
+ cursor: col-resize;
+ z-index: 3;
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 4px;
+ bottom: 4px;
+ right: 3px;
+ width: 1px;
+ background: transparent;
+ transition: background 0.15s;
+ }
+
+ &:hover::after {
+ background: color-mix(in srgb, var(--fms-primary) 45%, transparent);
+ }
+
+ &:hover {
+ background: color-mix(in srgb, var(--fms-primary) 10%, transparent);
+ }
+}
+
+/* ---- 预览 mock 数据行 ---- */
+.mm-tdesign__mock {
+ padding: 4px 8px;
+ border: 1px solid var(--fms-border);
+ background: var(--fms-card);
+ font-size: 11px;
+ color: var(--fms-text-secondary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &.is-muted {
+ color: var(--fms-text-placeholder, #9ca3af);
+ font-style: italic;
+ }
+}
+
+/* ---- 通用小件:必填 / 隐藏标记 ---- */
.mm-fdesign__required {
color: #d95757;
font-style: normal;
margin-left: 2px;
}
-/* 隐藏字段角标:默认隐藏,hover / 选中时浮现(隐藏卡片本身已整体降透明度) */
.mm-fdesign__invisible-mark {
margin-left: auto;
padding: 0 5px;
@@ -2707,72 +2524,32 @@ textarea.mm-fdesign__ctrl {
transition: opacity 0.15s;
}
-.mm-fdesign__field:hover .mm-fdesign__invisible-mark,
-.mm-fdesign__field.is-selected .mm-fdesign__invisible-mark,
-.mm-fdesign__field.is-invisible .mm-fdesign__invisible-mark,
-.mm-tdesign__col:hover .mm-fdesign__invisible-mark,
-.mm-tdesign__col.is-selected .mm-fdesign__invisible-mark,
-.mm-tdesign__col.is-invisible .mm-fdesign__invisible-mark {
+.mm-tdesign__cell:hover .mm-fdesign__invisible-mark,
+.mm-tdesign__cell.is-selected .mm-fdesign__invisible-mark,
+.mm-tdesign__cell.is-invisible .mm-fdesign__invisible-mark {
opacity: 1;
}
-/* 幽灵占位:SortableJS 把 ghostClass 加在原卡片上(布局冻结,原位不动),
- 显示为灰色占位块(内部控件隐藏),一眼看出「这里被拖走了」;
- 跟手的是自绘跟手卡片(createDragBadge 生成,样式见全局 .mm-fdesign__drag-badge) */
-.mm-fdesign__sortable-ghost,
-.mm-fdesign__lib-ghost {
- opacity: 1;
- background: color-mix(in srgb, var(--fms-border) 55%, transparent) !important;
- border-radius: 6px;
-
- > * {
- visibility: hidden;
- }
-}
-
-/* 正在拖拽的卡片:置顶 + 适中阴影(避免过重显得脏) */
-.mm-fdesign__sortable-drag {
- opacity: 0.95;
- box-shadow: 0 8px 20px rgba(39, 54, 70, 0.16) !important;
- z-index: 1000;
-}
-
-/* 空画布拖入容器:无边框,仅保留可放置高度 */
-.mm-fdesign__empty-list {
- width: min(880px, 100%);
- margin: 0 auto;
- min-height: 120px;
-}
-
-.mm-fdesign__empty {
- /* 空画布拖入区:无边框,仅一行提示文字 */
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 80px;
- color: var(--fms-text-secondary);
- font-size: 11px;
-
- &.is-over {
- border: 1px dashed var(--fms-primary);
- border-radius: 5px;
- background: color-mix(in srgb, var(--fms-primary) 8%, transparent);
- }
-}
-
/* ---- 属性面板 ---- */
.mm-fdesign__inspector .mm-fdesign__subhead {
padding-bottom: 8px;
margin-bottom: 2px;
}
+.mm-fdesign__insp-empty {
+ padding: 24px 8px;
+ color: var(--fms-text-secondary);
+ font-size: 11px;
+ text-align: center;
+ line-height: 1.7;
+}
+
.mm-fdesign__prop-group {
margin-top: 14px;
color: var(--fms-text);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
- /* 前竖线:与左侧分组小标题统一左对齐 */
border-left: 3px solid var(--fms-primary);
padding-left: 8px;
@@ -2798,54 +2575,6 @@ textarea.mm-fdesign__ctrl {
gap: 8px;
}
-/* 布局组只有栅格宽度一项时(非文本域字段)占满整行 */
-.mm-fdesign__prop--full {
- grid-column: span 2;
-}
-
-/* 栅格宽度 / 文本域高度:Select + 数字输入框横排 */
-.mm-fdesign__span-control,
-.mm-fdesign__height-control {
- display: flex;
- gap: 4px;
-
- :deep(.select-wrapper) {
- flex: 1;
- min-width: 0;
- }
-
- input[type='number'] {
- flex: none;
- width: 48px;
- height: 26px;
- padding: 0 4px;
- border: 1px solid var(--fms-border);
- border-radius: 4px;
- background: var(--fms-card);
- color: var(--fms-text);
- font-size: 11px;
- text-align: center;
- outline: none;
-
- &:focus {
- border-color: var(--fms-primary);
- }
-
- &:disabled {
- opacity: 0.5;
- }
- }
-}
-
-/* 栅格宽度:去掉下拉后数字输入框即主控件,填充剩余宽度(列宽控件仍用下拉,不受影响) */
-.mm-fdesign__span-control input#mm-fdesign-colspan {
- flex: 1;
- width: auto;
- min-width: 0;
- padding: 0 8px;
- text-align: left;
-}
-
.mm-fdesign__prop-static {
display: flex;
align-items: center;
@@ -2879,6 +2608,62 @@ textarea.mm-fdesign__ctrl {
}
}
+.mm-fdesign__span-control {
+ display: flex;
+ gap: 4px;
+
+ :deep(.select-wrapper) {
+ flex: 1;
+ min-width: 0;
+ }
+
+ input[type='number'] {
+ flex: none;
+ width: 48px;
+ height: 26px;
+ padding: 0 4px;
+ border: 1px solid var(--fms-border);
+ border-radius: 4px;
+ background: var(--fms-card);
+ color: var(--fms-text);
+ font-size: 11px;
+ text-align: center;
+ outline: none;
+
+ &:focus {
+ border-color: var(--fms-primary);
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ }
+ }
+}
+
+.mm-fdesign__step {
+ flex: none;
+ width: 22px;
+ height: 26px;
+ padding: 0;
+ border: 1px solid var(--fms-border);
+ border-radius: 4px;
+ background: var(--fms-card);
+ color: var(--fms-text-secondary);
+ font-size: 12px;
+ line-height: 1;
+ cursor: pointer;
+
+ &:hover:not(:disabled) {
+ border-color: var(--fms-primary);
+ color: var(--fms-primary);
+ }
+
+ &:disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+ }
+}
+
.mm-fdesign__toggles {
margin-top: 10px;
}
@@ -2931,6 +2716,11 @@ textarea.mm-fdesign__ctrl {
width: 100%;
}
+.mm-fdesign__band-action {
+ margin-top: 8px;
+ width: 100%;
+}
+
/* ---- 数据视图 ---- */
.mm-edit__data {
display: flex;
@@ -2953,373 +2743,4 @@ textarea.mm-fdesign__ctrl {
min-height: 0;
flex: 1;
}
-
-/* ---- 分组标题行:右侧操作(字段/列计数 + 停用标记 + 删除),主表/子表统一 ---- */
-.mm-edit__section-meta {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- flex: none;
- /* 无标题分组时标题名不渲染,meta 仍靠右 */
- margin-left: auto;
- margin-left: auto;
-}
-
-.mm-edit__section-count {
- color: var(--fms-text-secondary);
- font-size: 11px;
- font-weight: 400;
- font-variant-numeric: tabular-nums;
-}
-
-/* 画布分组删除按钮(标题行右侧):图标常驻显示 */
-.mm-fdesign__section-del {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 22px;
- height: 22px;
- padding: 0;
- border: 1px solid transparent;
- border-radius: var(--fms-control-radius, 5px);
- background: transparent;
- color: var(--fms-text-placeholder, #9ca3af);
- cursor: pointer;
- transition:
- background 0.15s,
- color 0.15s,
- border-color 0.15s;
-
- &:hover {
- background: color-mix(in srgb, var(--fms-danger, #d95757) 10%, var(--fms-card));
- color: var(--fms-danger, #d95757);
- border-color: color-mix(in srgb, var(--fms-danger, #d95757) 35%, transparent);
- }
-}
-
-/* 分组标题行编辑按钮(✏ 修改标题):图标常驻显示 */
-.mm-fdesign__section-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 22px;
- height: 22px;
- padding: 0;
- border: 1px solid transparent;
- border-radius: var(--fms-control-radius, 5px);
- background: transparent;
- color: var(--fms-text-placeholder, #9ca3af);
- cursor: pointer;
- transition:
- background 0.15s,
- color 0.15s,
- border-color 0.15s;
-
- &:hover {
- background: var(--fms-secondary-hover, #f1f5f9);
- color: var(--fms-text);
- border-color: var(--fms-border);
- }
-}
-
-/* 分组标题为空时的占位:灰色「空」,点击可设置标题 */
-.mm-fdesign__section-name.is-empty {
- color: var(--fms-text-placeholder, #9ca3af);
- cursor: text;
-}
-
-/* 分组拖拽手柄(标题行左侧):拖动调整分组顺序 */
-.mm-fdesign__section-grip {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- flex: none;
- width: 22px;
- height: 22px;
- padding: 0;
- border: 1px solid transparent;
- border-radius: var(--fms-control-radius, 5px);
- background: transparent;
- color: var(--fms-text-placeholder, #9ca3af);
- cursor: grab;
- transition:
- background 0.15s,
- color 0.15s;
-
- &:hover {
- background: var(--fms-secondary-hover, #f1f5f9);
- color: var(--fms-text);
- }
-
- &:active {
- cursor: grabbing;
- }
-}
-
-/* 分组卡片拖拽占位 */
-.mm-fdesign__section-ghost {
- opacity: 0.4;
-}
-
-/* 分组级左右布局:标题在左(竖排),字段画布在右;仅影响单个分组 */
-.mm-fdesign__section.is-horizontal {
- display: flex;
- align-items: stretch;
- gap: 0;
-
- .mm-fdesign__section-title {
- flex: none;
- width: 56px;
- margin-bottom: 0;
- padding: 10px 0;
- border-bottom: none;
- flex-direction: column;
- align-items: center;
- gap: 8px;
-
- .mm-fdesign__section-name {
- writing-mode: vertical-rl;
- letter-spacing: 0.2em;
- }
-
- .mm-edit__section-meta {
- flex-direction: column;
- align-items: center;
- margin-left: 0;
- gap: 4px;
- }
- }
-
- .mm-fdesign__section-body {
- flex: 1;
- min-width: 0;
- }
-}
-
-/* ---- 卡片标签行快捷开关(必填 / 显示)----
- 默认隐藏,仅在卡片 hover 或选中时浮现,降低常驻视觉噪音;
- 图标灰色=未开启,主题色=开启 */
-.mm-fdesign__label-chips {
- display: inline-flex;
- align-items: center;
- gap: 3px;
- margin-left: 4px;
- opacity: 0;
- transition: opacity 0.15s;
-}
-
-.mm-fdesign__field:hover .mm-fdesign__label-chips,
-.mm-fdesign__field.is-selected .mm-fdesign__label-chips,
-.mm-tdesign__col:hover .mm-fdesign__label-chips,
-.mm-tdesign__col.is-selected .mm-fdesign__label-chips {
- opacity: 1;
-}
-
-.mm-fdesign__label-chip {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 18px;
- height: 18px;
- padding: 0;
- border: none;
- border-radius: 4px;
- background: color-mix(in srgb, var(--fms-primary) 5%, transparent);
- color: var(--fms-text-disabled, #9ca3af);
- cursor: pointer;
- transition:
- background 0.15s,
- color 0.15s;
-
- &:hover {
- background: color-mix(in srgb, var(--fms-primary) 16%, transparent);
- }
-
- &.is-on {
- color: var(--fms-primary);
- background: color-mix(in srgb, var(--fms-primary) 16%, transparent);
- }
-}
-
-/* ---- 栅格宽度:步进按钮 + 本行占用合计 ---- */
-.mm-fdesign__step {
- flex: none;
- width: 22px;
- height: 26px;
- padding: 0;
- border: 1px solid var(--fms-border);
- border-radius: 4px;
- background: var(--fms-card);
- color: var(--fms-text-secondary);
- font-size: 12px;
- line-height: 1;
- cursor: pointer;
-
- &:hover:not(:disabled) {
- border-color: var(--fms-primary);
- color: var(--fms-primary);
- }
-
- &:disabled {
- opacity: 0.45;
- cursor: not-allowed;
- }
-}
-
-/* ---- 表格(子表)设计:列画布(透明,白色分组卡片直接浮于灰底) ---- */
-.mm-tdesign__sheet {
- position: relative;
- min-height: 120px;
-}
-
-.mm-tdesign__col {
- display: flex;
- align-items: center;
- gap: 8px;
- min-height: 38px;
- padding: 4px 8px;
- border: 1px solid transparent; /* 默认不显示边框线 */
- border-radius: 6px;
- background: var(--fms-card);
- cursor: grab;
- user-select: none;
- -webkit-user-select: none;
- transition:
- border-color 0.15s,
- box-shadow 0.15s;
-
- &:hover {
- border-color: color-mix(in srgb, var(--fms-primary) 35%, transparent);
- }
-
- &.is-selected {
- border-style: dashed;
- border-color: var(--fms-primary);
- box-shadow: none;
- }
-
- &.is-invisible {
- opacity: 0.55;
- }
-
- .mm-fdesign__field-name {
- flex: 0 1 auto;
- }
-}
-
-/* 拖拽手柄:默认隐藏,hover / 选中时浮现;整卡本身可拖拽(cursor 提示保留在卡片上) */
-.mm-tdesign__drag-handle {
- flex: none;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 20px;
- height: 20px;
- color: var(--fms-text-secondary);
- cursor: grab;
- opacity: 0;
- transition: opacity 0.15s;
-}
-
-.mm-tdesign__col {
- cursor: grab;
-}
-
-.mm-tdesign__col:hover .mm-tdesign__drag-handle,
-.mm-tdesign__col.is-selected .mm-tdesign__drag-handle {
- opacity: 0.7;
-}
-
-.mm-tdesign__col-meta {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- margin-left: auto;
- flex: none;
-}
-
-.mm-tdesign__col-type {
- padding: 0 6px;
- border-radius: 4px;
- background: var(--fms-secondary);
- color: color-mix(in srgb, var(--fms-text-secondary) 65%, var(--fms-text));
- font-size: 10px;
- line-height: 16px;
-}
-
-.mm-tdesign__col-width {
- color: color-mix(in srgb, var(--fms-text-secondary) 70%, var(--fms-text));
- font-size: 11px;
- font-variant-numeric: tabular-nums;
-}
-
-.mm-tdesign__col-del {
- flex: none;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- padding: 0;
- border: none;
- border-radius: 5px;
- background: transparent;
- color: var(--fms-text-disabled, #9ca3af);
- cursor: pointer;
- opacity: 0;
- transition: opacity 0.15s;
-
- &:hover {
- background: #fdecec;
- color: #d95757;
- }
-}
-
-.mm-tdesign__col:hover .mm-tdesign__col-del,
-.mm-tdesign__col.is-selected .mm-tdesign__col-del {
- opacity: 1;
-}
-
-.mm-tdesign__empty {
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 56px;
- color: var(--fms-text-secondary);
- font-size: 11px;
-}
-
-
-
-
diff --git a/code/fms/fms-vue/src/views/system/module-management/index.vue b/code/fms/fms-vue/src/views/system/module-management/index.vue
index 6e52c607..2c55f7f5 100644
--- a/code/fms/fms-vue/src/views/system/module-management/index.vue
+++ b/code/fms/fms-vue/src/views/system/module-management/index.vue
@@ -24,15 +24,17 @@ import {
Info,
FolderTree,
} from '@lucide/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 Input from '@/components/ui/input/input.vue'
-import Tree from '@/components/ui/tree/tree.vue'
-import Dropdown from '@/components/ui/dropdown/dropdown.vue'
-import Splitter from '@/components/ui/splitter/splitter.vue'
-import SplitterPanel from '@/components/ui/splitter/panel.vue'
-import Tabs from '@/components/ui/tabs/tabs.vue'
+import {
+ Message,
+ Modal,
+ Button,
+ Input,
+ Tree,
+ Dropdown,
+ Splitter,
+ SplitterPanel,
+ Tabs,
+} from '@/components/ui'
import { loadDataApi, saveObjectApi, nextIdApi, describeApi } from '@/services/api'
import { buildTree, sortTree } from '@/utils/tree'
import { createTempId } from '@/utils/tempId'
diff --git a/code/fms/fms-vue/src/views/test/TestPage.vue b/code/fms/fms-vue/src/views/test/TestPage.vue
index f5c85cb3..73a878b8 100644
--- a/code/fms/fms-vue/src/views/test/TestPage.vue
+++ b/code/fms/fms-vue/src/views/test/TestPage.vue
@@ -3,7 +3,7 @@ import { computed } from 'vue'
import { useRoute } from 'vue-router'
defineOptions({ name: 'TestPage' })
-import Button from '@/components/ui/button/button.vue'
+import { Button } from '@/components/ui'
const route = useRoute()
const title = computed(() => route.meta?.title ?? '测试页')
diff --git a/code/fms/fms-vue/tests/fms-table-virtual.spec.js b/code/fms/fms-vue/tests/fms-table-virtual.spec.js
index 79f39727..6f307c03 100644
--- a/code/fms/fms-vue/tests/fms-table-virtual.spec.js
+++ b/code/fms/fms-vue/tests/fms-table-virtual.spec.js
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { nextTick } from 'vue'
-import { registerFeature, useAreaSelection } from 'stk-table-vue'
+import { registerFeature, useAreaSelection, StkTable } from 'stk-table-vue'
import FmsTable from '@/components/fms-table/FmsTable.vue'
// 与 main.js 一致:注册 stk-table 的按需 feature(行编辑依赖 area-selection)
@@ -150,4 +150,33 @@ describe('fms-table 虚拟滚动验证', () => {
expect(editor.exists()).toBe(true)
wrapper.unmount()
})
+
+ // 列宽拖拽回归:库 useColResize 在 mouseUp 时「原地改写」传入的列对象 width 并
+ // emit update:columns(同一批对象的新数组)。未绑定 v-model:columns 的父组件
+ // 不会回灌新数组,displayColumns 必须自行换新数组引用,库才能重算出新列宽。
+ // 复现方式:拿到库实际收到的 displayColumns,原地改第 1 列宽度后以同批对象
+ // emit update:columns,断言表头渲染出新宽度。
+ it('拖拽列宽后(未绑定 v-model:columns),新列宽立即生效不丢失', async () => {
+ const columns = makeColumns()
+ const wrapper = mount(FmsTable, {
+ props: { columns, dataSource: makeRows(20), rowKey: 'id', seq: false },
+ attachTo: document.body,
+ })
+ await nextTick()
+ await new Promise((r) => setTimeout(r, 100))
+
+ const stkTable = wrapper.findComponent(StkTable)
+ const displayColumns = stkTable.props('columns')
+ const target = displayColumns.find((c) => c.dataIndex === 'c1')
+ target.width = '333px' // 模拟 useColResize mouseUp 的原地改写
+
+ await stkTable.vm.$emit('update:columns', displayColumns.slice())
+ await nextTick()
+ await new Promise((r) => setTimeout(r, 100))
+
+ // 表头 th 宽度经 --cw CSS 变量渲染;virtualX 模式下取自 __W__(dealColumns 重算产物)
+ const th = wrapper.findAll('thead th').find((el) => el.attributes('data-col-key') === 'c1')
+ expect(th.attributes('style')).toContain('333px')
+ wrapper.unmount()
+ })
})
diff --git a/code/fms/问题.md b/code/fms/问题.md
deleted file mode 100644
index 9b449bc6..00000000
--- a/code/fms/问题.md
+++ /dev/null
@@ -1,115 +0,0 @@
-# 代码审查问题清单
-
-> 审查范围:`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) | 功能缺失 |
\ No newline at end of file
diff --git a/code/test/form-js b/code/test/form-js
new file mode 160000
index 00000000..d00e32ab
--- /dev/null
+++ b/code/test/form-js
@@ -0,0 +1 @@
+Subproject commit d00e32ab9237404e54f4897b19342fe7c7ed0efd