@done-coding/admin-core 0.28.1-alpha.0 → 0.29.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/es/bridge/layout.mjs +31 -2
  2. package/es/components/app-layout/AppAside.vue.mjs +1 -1
  3. package/es/components/app-layout/AppCollapseToggle.vue.mjs +1 -1
  4. package/es/components/app-layout/AppFooter.vue.mjs +1 -1
  5. package/es/components/app-layout/AppHeader.vue.mjs +1 -1
  6. package/es/components/app-layout/AppHeader.vue2.mjs +1 -1
  7. package/es/components/app-layout/AppSidebar.vue.mjs +1 -1
  8. package/es/components/app-layout/AppSidebar.vue2.mjs +60 -34
  9. package/es/components/data-view/DataGridView.vue.mjs +1 -1
  10. package/es/components/data-view/DataGridView.vue2.mjs +30 -6
  11. package/es/components/data-view/DataListView.vue.mjs +1 -1
  12. package/es/components/data-view/DataListView.vue2.mjs +38 -14
  13. package/es/components/data-view/DataListViewItem.vue.mjs +1 -1
  14. package/es/components/data-view/DataListViewItem.vue2.mjs +23 -4
  15. package/es/components/data-view/InfiniteListView.vue.mjs +1 -1
  16. package/es/components/data-view/InfiniteListView.vue2.mjs +9 -3
  17. package/es/components/data-view/use-grid-layout.mjs +46 -0
  18. package/es/components/data-view/use-masonry.mjs +65 -0
  19. package/es/components/display/BadgeMark.vue.mjs +1 -1
  20. package/es/components/display/BadgeMark.vue2.mjs +11 -3
  21. package/es/components/display/HeightProvider.vue.mjs +17 -3
  22. package/es/components/display/use-badge.mjs +22 -13
  23. package/es/components/form/use-upload-state.mjs +33 -11
  24. package/es/components/page-layout/AppPageDetail.vue.mjs +1 -1
  25. package/es/components/page-layout/AppPageDetail.vue2.mjs +12 -2
  26. package/es/components/page-layout/AppPageListDetailLayout.vue.mjs +11 -4
  27. package/es/components/page-layout/AppPageListDetailSheet.vue.mjs +8 -1
  28. package/es/components/page-layout/AppPageListDetailSplit.vue.mjs +1 -1
  29. package/es/components/page-layout/AppPageListDetailSplit.vue2.mjs +8 -1
  30. package/es/components/table/TableMain.vue.mjs +1 -1
  31. package/es/components/table/TableMain.vue2.mjs +8 -5
  32. package/es/components/view-layout/ViewLayout.vue.mjs +1 -1
  33. package/es/components/view-layout/ViewLayout.vue2.mjs +68 -20
  34. package/es/hooks/use-surface.mjs +4 -1
  35. package/es/hooks/use-theme-apply.mjs +78 -0
  36. package/es/index.mjs +118 -120
  37. package/es/store/app.mjs +25 -0
  38. package/es/style.css +120 -95
  39. package/package.json +2 -2
  40. package/src/bridge/docs/README.md +31 -0
  41. package/src/components/app-layout/docs/README-AppAside.md +11 -0
  42. package/src/components/app-layout/docs/README-AppFooter.md +11 -1
  43. package/src/components/app-layout/docs/README-AppHeader.md +16 -0
  44. package/src/components/app-layout/docs/README-AppSidebar.md +18 -0
  45. package/src/components/data-view/README.md +2 -1
  46. package/src/components/data-view/docs/README-DataGridView.md +21 -1
  47. package/src/components/data-view/docs/README-DataListView.md +65 -3
  48. package/src/components/data-view/docs/README-InfiniteListView.md +17 -0
  49. package/src/components/display/README.md +1 -1
  50. package/src/components/display/docs/README-BadgeMark.md +52 -10
  51. package/src/components/display/docs/README-HeightProvider.md +14 -8
  52. package/src/components/form/docs/README-FormUpload.md +79 -0
  53. package/src/components/form/docs/README-FormUploadImage.md +19 -0
  54. package/src/components/form/docs/README-FormUploadVideo.md +19 -0
  55. package/src/components/page-layout/README.md +15 -0
  56. package/src/components/page-layout/docs/README-AppPageDetail.md +3 -0
  57. package/src/components/page-layout/docs/README-AppPageListDetailLayout.md +33 -0
  58. package/src/components/table/docs/README-TableMain.md +17 -0
  59. package/src/components/view-layout/README.md +2 -2
  60. package/src/components/view-layout/docs/README-ViewLayout.md +97 -6
  61. package/types/bridge/badge.d.ts +3 -1
  62. package/types/bridge/layout.d.ts +42 -5
  63. package/types/components/app-layout/AppSidebar.vue.d.ts +286 -2
  64. package/types/components/app-layout/types.d.ts +1 -1
  65. package/types/components/data-view/DataListViewItem.vue.d.ts +12 -7
  66. package/types/components/data-view/types.d.ts +76 -0
  67. package/types/components/data-view/use-grid-layout.d.ts +42 -0
  68. package/types/components/data-view/use-masonry.d.ts +44 -0
  69. package/types/components/display/BadgeMark.vue.d.ts +5 -5
  70. package/types/components/display/HeightProvider.vue.d.ts +1 -0
  71. package/types/components/display/use-badge.d.ts +6 -0
  72. package/types/components/page-layout/AppPageDetail.vue.d.ts +18 -0
  73. package/types/components/page-layout/AppPageListDetailLayout.vue.d.ts +20 -0
  74. package/types/components/page-layout/AppPageListDetailSheet.vue.d.ts +4 -0
  75. package/types/components/page-layout/AppPageListDetailSplit.vue.d.ts +4 -0
  76. package/types/components/table/types.d.ts +8 -0
  77. package/types/components/view-layout/ViewLayout.vue.d.ts +30 -0
  78. package/types/components/view-layout/types.d.ts +12 -0
  79. package/types/hooks/use-surface.d.ts +11 -0
  80. package/types/index.d.ts +0 -1
  81. package/types/injectInfo.json.d.ts +1 -1
  82. package/es/components/icon/IconHammer.vue.mjs +0 -26
  83. package/es/components/icon/IconHammer.vue2.mjs +0 -4
  84. package/src/components/icon/README.md +0 -38
  85. package/types/components/icon/IconHammer.vue.d.ts +0 -2
  86. package/types/components/icon/index.d.ts +0 -7
@@ -22,7 +22,8 @@
22
22
  - **消费入口**:
23
23
  - `DataListView`:走 `TableMain` `customView` prop + `#custom-view-item` 插槽(TableMain `customView.value ? markRaw(DataListView) : markRaw(ElTable)` 择一渲染;ListLayout 已转发该插槽,类型 `DataListViewItemScope<T>`)
24
24
  - `DataGridView`:走 `ListLayout` `viewMode="grid"`(`viewMode="table"` 时 ListLayout 渲染 TableMain,内部 `customView` 仍切 ElTable/DataListView——切换在 ListLayout 层,两容器数据路径不同构故不共存于 TableMain 内)
25
- - **DataListViewItem**:由 DataListView 内部 `v-for` 使用,消费方不直接使用;[MUST NOT] 写死视觉样式(卡片阴影/圆角归消费方)、[MUST NOT] 内置选中状态机
25
+ - **DataListViewItem**:由 DataListView 内部 `v-for` 使用,消费方不直接使用;承载状态钩子 + **可选卡壳**(`card`,只出面);[MUST NOT] 写死**状态**视觉(`.is-active` / `.is-selected` 归消费方)、[MUST NOT] 内置选中状态机
26
+ > 立场 2026-09-17 收窄:原话是「[MUST NOT] 写死卡片视觉」,与内置卡壳打架 ⇒ 收窄为「不写死**状态**视觉」。
26
27
 
27
28
  ## 分层(A 底座 / B 容器)
28
29
 
@@ -12,6 +12,7 @@
12
12
  - **数据路径与 TableMain 不同构**:TableMain = 分页替换当前页;DataGridView = **存量累加**(滚动到底 append 下一页,直到 hasMore=false)。两者是两套数据状态机,故**不共存于同一组件内**——切换在 `ListLayout` 的 `viewMode` 层
13
13
  - 泛型三参:`T` 行 / `SQ` 静态查询参数 / `F` 注入对象类型;`query` 变化时清空存量从第 1 页重新累加
14
14
  - 内部结构:数据容器(本组件)→ InfiniteListView(IO 哨兵触发 + 虚拟化 + 底部三态)→ gridColumns 卡片网格
15
+ - **瀑布流不在本组件**:虚拟化按列数 chunk 成**等高行**,与瀑布流「卡片不等高、跨行装箱」天然冲突 ⇒ 该能力落在分页路径 `DataListView`(无虚拟化)
15
16
 
16
17
  ## 快速上手(最小可用)
17
18
 
@@ -96,7 +97,9 @@
96
97
  | `rowKey` | `Extract<keyof T,string> \| ((row:T)=>string)`(必传) | — | 行/卡唯一键 |
97
98
  | `query` | `SQ` | — | 静态查询参数;变化时清空存量从第 1 页重新累加 |
98
99
  | `pageSizeInit` | `number` | `20` | 每批加载条数(批次固定,运行期不改) |
99
- | `gridColumns` | `number \| Partial<Record<Breakpoint, number>>` | `4` | 定列数(`repeat(N, 1fr)`);对象 = el-col span 制断点配置(24 栅格,列数 = 24 / span,未配档回落更小档) |
100
+ | `gridColumns` | `number \| Partial<Record<Breakpoint, number>>` | `4` | 定列数(`repeat(N, 1fr)`);对象 = el-col span 制断点配置(24 栅格,列数 = 24 / span,未配档回落更小档)。⚠️ **不支持** `DataListView` 那档 `{ min }` 流体列宽——本组件按列数 chunk 成等长行做虚拟化,流体列数是浏览器算的、JS 拿不到 |
101
+ | `gap` | `number` | 模块档(默认 `8`) | item 间距(px,横纵同值)。缺省取间距阶梯模块档,随 gap 基数与密度缩放。⚠️ **原写死 `12`**(呼吸档的值,档位用错),见 migrations |
102
+ | `card` | `boolean` | `true` | 内置卡壳(border/radius/padding)。**壳已从插槽 fallback 内移到 `.data-grid-view__cell`** ⇒ 给了 `#custom-view-item` 时也有壳。自带卡片视觉时关掉 |
100
103
  | `maxHeight` | `number \| string` | `"100vh"` | 数据区最大高度;数字 px / 字符串 CSS 长度;默认满视口高(非 refine 兜底限高) |
101
104
  | `refine` | `boolean` | `true` | 精细化布局:无 maxHeight 时按 viewport 高减工具栏/底部得出数据区高 |
102
105
  | `refineReduceHeight` | `number` | `0` | refine 模式预留高度减量 |
@@ -145,6 +148,23 @@
145
148
  - **独立使用成本**:不传 `toolbar`/搜索区时只有网格本体;完整列表页 [MUST] 走 `ListLayout`(搜索/头部/操作/粘性共享)
146
149
  - **列插槽无 `#<column.prop>`**:网格卡片是"一卡多列"形态,非表格"一列一格";卡片渲染走 `#custom-view-item` 或默认 column 配置
147
150
 
151
+ ## 面与面不叠(`itemCard`)
152
+
153
+ 项自带卡片时,容器**只撤面色、不撤呼吸与圆角**:
154
+
155
+ ```
156
+ 容器面(--el-bg-color) + 卡片面(--el-bg-color) = 同一个色
157
+ ⇒ gap 漏出的就是与卡片同色的容器面,视觉上「一整片白 + 若干 1px 细框」
158
+ ```
159
+
160
+ - 判据 `itemCard`:`TableMain` 缺省跟随 **`customView`**(该态下内层 `DataListView` 的 `card`
161
+ 默认开)、`DataGridView` 缺省跟随 **`card`**、`InfiniteListView` 缺省 **`false`**
162
+ (它没有内置卡壳、卡由消费方自画,core 无从判断 ⇒ 自画卡时 [MUST] 显式传 `itemCard`)。
163
+ - 🔴 判据 [MUST NOT] 收窄成「`card === true`」—— 那会整条漏掉 `InfiniteListView`。
164
+ - 🔴 **撤色不撤呼吸**:呼吸计入高度账(`surfacePad` 与可用高扣减同源),
165
+ 一并撤会连带改 refine 的 reserve、整页溢出。
166
+ - 阴性:`card: false` / `itemCard: false` 时容器**照常出面**(别把非卡片态的面一起干掉)。
167
+
148
168
  ## 关联
149
169
 
150
170
  - 门面:`ListLayout`(`viewMode="grid"`;`viewMode="table"` 渲染 TableMain,内部 customView 切 ElTable/DataListView)
@@ -1,6 +1,7 @@
1
1
  # DataListView(数据视图列表)
2
2
 
3
3
  > 卡片/自定义渲染的列表视图:外部传入数据与列配置,无请求、无分页;TableMain `customView` 模式的渲染载体。
4
+ > **自带网格能力**:列数(断点表 / 流体)· item 间距 · 内置卡壳 · 瀑布流(opt-in)—— 消费方 [MUST NOT] 再手写 `:deep(.data-list-view){display:grid;…}`。
4
5
  > 范式页:`apps/reference/src/pages/data-view/custom-view/Index.vue`
5
6
  > ⚠️ 内部件:仅供 TableMain 内部 import,[MUST NOT] 对外导出(不进 core 顶层 export)
6
7
 
@@ -10,6 +11,8 @@
10
11
  - **何时不用**:标准表格用 `TableMain` / `ElTable`;DataListView 不直接对外消费
11
12
  - 泛型 `<T, F extends Record<string, any>>`:行类型 / 注入上下文类型
12
13
  - 数据契约:`data` 外部传入(当前页数据),组件无请求/分页逻辑
14
+ - **网格能力与 `DataGridView` 同源**:`gridColumns` 的断点语义**逐字一致**(共用 `resolveGridColumnsByBreakpoint`),
15
+ [MUST NOT] 在本路径另造一套 —— 两套断点心智会让同一页切 viewMode 时列数跳
13
16
  - 接线:TableMain `customView.value ? markRaw(DataListView) : markRaw(ElTable)`;ListLayout 转发 `#custom-view-item` 插槽(类型 `DataListViewItemScope<T>`)
14
17
 
15
18
  ## 快速上手(最小可用)
@@ -35,6 +38,14 @@
35
38
  - **内部件不进顶层 export:仅供 TableMain 内部 import,[MUST NOT] 对外导出 / 全局注册 / install。** 业务 [MUST] 走 `TableMain` `customView` 模式消费——本组件无请求/分页,`data` 须外部传入(当前页数据),脱离 TableMain 直接消费没有数据来源。
36
39
  - **`maxHeight` / `minHeight`:默认 `undefined`(自然高度)即够用。** 仅当限高滚动(`maxHeight` 传入容器 `overflowY: auto`)或配合 TableMain `fillHeight` 顶分页器到底(`minHeight`)才设。
37
40
  - **`selectable`:默认 `false` 即够用。** 仅当卡片需要选中/批量操作能力才开——开启后才有 `$type_selection` 键。
41
+ - **`gridColumns`:默认 `undefined` = 不成网格**(单列流式,与本能力引入前一致)。要多列才传:
42
+ 数字 = 定列数;对象 = el-col span 制断点表(与 `DataGridView` 同语义);`{ min: N }` = **流体列宽**
43
+ (`repeat(auto-fill, minmax(Npx, 1fr))`,列数由容器宽自然得出,不需要断点表)。
44
+ - **`gap`:默认取间距阶梯的\*\*模块档\*\***(`APP_LAYOUT_GAP_CONFIG.size × 密度 × 1`,默认 8)。
45
+ `grid` 的 gap 是**卡与卡之间** = 模块档的定义值,[MUST NOT] 在消费侧写死像素补偿。
46
+ - **`card`:默认 `true`**(内置卡壳:border + radius + padding,全走 token)。
47
+ 自带卡片视觉时传 `false` 关掉,[MUST NOT] 在自己的卡上再画一层边框叠上去。
48
+ - **`masonry`:默认 `false`**(瀑布流需要 JS 量高,按「需要 JS 就默认不开」的规则关着)。
38
49
  - **`rowKey` / `columns` / `getRenderCtxParams` 必填契约**:行唯一键、列配置、列 render 上下文缺一不可。
39
50
  - **完整能力演示**(选中框列 + 行号列 + 字段列 + 操作列全接线):`apps/reference/src/pages/data-view/custom-view/Index.vue`——能力展示,非推荐默认。
40
51
 
@@ -51,6 +62,49 @@
51
62
 
52
63
  关掉传 `:active-scroll="false"`。
53
64
 
65
+ ## 网格能力(列数 / 间距 / 卡壳 / 瀑布流)
66
+
67
+ ```html
68
+ <!-- 断点表:与 DataGridView 逐字同语义(24 栅格 span 制) -->
69
+ <TableMain :customView="customView" :custom-view-props="{ gridColumns: { xs: 24, sm: 12, md: 8, lg: 6 } }" />
70
+
71
+ <!-- 流体列宽:列数由容器宽自然得出(消费方 2026-09-02 拍板过的形态) -->
72
+ <TableMain :custom-view-props="{ gridColumns: { min: 189 } }" />
73
+ ```
74
+
75
+ | `gridColumns` 形态 | 产出 | 何时用 |
76
+ | --- | --- | --- |
77
+ | 不传(默认) | 不成网格(单列流式) | 普通列表 |
78
+ | `number` | `repeat(N, 1fr)` | 列数固定 |
79
+ | `{ xs, sm, md, lg, xl }` | 按断点算列数(span 制,24 栅格;未配档回落 ≤ 当前断点最近一档) | 要按视口分档 |
80
+ | `{ min: N }` | `repeat(auto-fill, minmax(Npx, 1fr))` | **列数按容器宽流体**、不想钉死 |
81
+
82
+ 🔴 **`{ min }` 这一档只有本组件有,`DataGridView` 没有** —— 它按 `gridColumns` 把存量
83
+ **chunk 成等长的行**做虚拟化,而流体档的列数是浏览器算的、JS 拿不到 ⇒ chunk 无从下手。
84
+ 这是实现约束不是遗漏,[MUST NOT] 为「对齐」把它硬塞进 `DataGridView`。
85
+
86
+ ### 内置卡壳(`card`,默认开)
87
+
88
+ 壳落在**已有的包裹层** `.data-list-view-item` 上(加 `is-card` 类),[MUST NOT] 另包一层:
89
+ DOM 层级恒定 ⇒ 消费方写的 `:deep()` 选择器不会随 prop 变;瀑布流的跨度也「量谁写谁」。
90
+
91
+ 壳**只出面**(border / radius / padding,全走 token)。
92
+ `.is-active` / `.is-selected` **仍是空钩子** —— 本族的立场从「[MUST NOT] 写死卡片视觉」
93
+ **收窄为**「[MUST NOT] 写死**状态**视觉」,两句话不再打架。
94
+
95
+ ### 瀑布流(`masonry`,默认关)
96
+
97
+ 开启后走 `grid-auto-rows: <行单位>` + 每项 `grid-row-end: span N`(N 由实测卡高算),
98
+ **顺序仍是行优先(横向)** —— 这正是纯 CSS `columns` 做不到的那点(它竖着填,
99
+ 按时间/热度排的列表会被打乱)。
100
+
101
+ - **需要 JS ⇒ 默认关**(规则:需要 JS 就默认不开)。
102
+ - 与 `gridColumns` 的 `{ min }` / 断点表都能叠。
103
+ - `DataGridView` **不提供**本能力:它的虚拟化按列数 chunk 成**等高行**,而瀑布流要求
104
+ 卡片不等高、跨行装箱,两者天然冲突。分页路径没有虚拟化,是它更自然的落点。
105
+ - 量不到高度时每项退**兜底跨度**(CSS 给的),表现为「不够好看」而**不会重叠**——
106
+ 增强失败 [MUST NOT] 变成布局坏掉。
107
+
54
108
  ## API
55
109
  > ⚠️ API 以 types.ts 与组件源码为真相源;与文档冲突时以源码为准。
56
110
 
@@ -63,6 +117,10 @@
63
117
  | `columns` | `DataViewColumn<T>[]`(必填) | 无 | 列配置(Pick 自 ElTableColumnProps 的 `prop` / `label` / `type` / `render`),内部归一为 `fieldComponentMap` |
64
118
  | `maxHeight` | `number` | `undefined` | 传入时容器 `overflowY: auto` 限高滚动 |
65
119
  | `minHeight` | `number` | `undefined` | 传入时撑满至此高(配合 TableMain `fillHeight` 顶分页器到底) |
120
+ | `gridColumns` | `number \| Partial<Record<Breakpoint, number>> \| { min: number }` | `undefined` | 列数:数字 = 定列数;对象 = el-col span 制断点表(与 `DataGridView` 同语义);`{ min }` = 流体列宽 `repeat(auto-fill, minmax(Npx,1fr))`。不传 = 不成网格 |
121
+ | `gap` | `number` | 模块档(默认 `8`) | item 间距(px)。缺省取间距阶梯模块档,随 gap 基数与密度缩放 |
122
+ | `card` | `boolean` | `true` | 内置卡壳(border/radius/padding,走 token;落在 `.data-list-view-item` 上)。自带卡片视觉时关掉 |
123
+ | `masonry` | `boolean` | `false` | 瀑布流装箱(需要 JS 量高,故默认关)。顺序仍行优先 |
66
124
  | `selectable` | `boolean` | `false` | 启用选中能力 |
67
125
  | `getRenderCtxParams` | `() => Pick<TableColumnDefaultScope<T, F>, "injectInfo" \| "exposeInfo">`(必填) | 无 | 列 render 上下文(必填契约) |
68
126
 
@@ -98,7 +156,9 @@
98
156
  - **无请求/分页**:`data` 须外部传入(当前页数据),勿期待组件内部取数
99
157
  - **刷新清选中**:`data` 引用变化即清空选中——跨页 / 刷新后选中状态不保留
100
158
  - **未实现方法抛错**:Expose 面与 TableInstance 对齐但 10 个方法为「not supported」桩(表格特有行为在自定义视图无意义)——含 `setCurrentRow`,故 TableMain 在 `customView` 态 [MUST NOT] 调它同步高亮(active 由本组件自持)
101
- - **`.is-active` / `.is-selected` 是空钩子类**:本族 [MUST NOT] 写死卡片视觉,选中 / 当前项的样式由消费方覆写
159
+ - **`.is-active` / `.is-selected` 是空钩子类**:本族 [MUST NOT] 写死**状态**视觉,选中 / 当前项的样式由消费方覆写。
160
+ ⚠️ 这条立场 2026-09-17 **收窄过**:内置卡壳(`card`,默认开)只出**面**(border/radius/padding),
161
+ 状态视觉仍是空钩子 —— [MUST NOT] 拿旧话「不写死卡片视觉」去否掉卡壳,也 [MUST NOT] 反过来在钩子里写死状态色
102
162
  - **列键依赖归一规则**:消费 `fieldComponentMap` 只用 `prop` / `$type_index` / `$type_selection` 三系键,`type: "expand"` 列不产出
103
163
 
104
164
  ## DataListViewItem(列表项容器)
@@ -111,6 +171,8 @@
111
171
  | --- | --- | --- | --- |
112
172
  | `selected` | `boolean`(必填) | 无 | 是否选中(勾选语义,由 DataListView 传入)→ `.is-selected` |
113
173
  | `active` | `boolean` | `false` | 是否为「当前项」(active,与勾选无关)→ `.is-active` |
174
+ | `card` | `boolean` | `false` | 是否套内置卡壳 → `.is-card`(border/radius/padding 走 token)。由 `DataListView` 下发,默认值以它为准 |
175
+ | `spanRows` | `number` | `undefined` | 瀑布流跨度(`grid-row-end: span N`);仅 `masonry` 开启时由 `DataListView` 下发 |
114
176
 
115
177
  ### Emits / Expose
116
178
 
@@ -124,8 +186,8 @@
124
186
 
125
187
  ### 定位注释(.vue 头)
126
188
 
127
- - 仅承载选中态 class 钩子(`.is-selected`)+ 项内容插槽
128
- - [MUST NOT] 写死视觉样式(卡片阴影 / 圆角归消费方)
189
+ - 承载选中 / 当前项 class 钩子 + 可选卡壳(`card`)+ 项内容插槽
190
+ - [MUST NOT] 写死**状态**视觉(`.is-active` / `.is-selected` 归消费方);卡壳只出面,不碰状态
129
191
  - [MUST NOT] 内置选中状态机
130
192
 
131
193
  ## 关联
@@ -112,6 +112,23 @@
112
112
  - **`pullRefresh` 默认关**:桌面 Web 无手势语境,[MUST NOT] 依赖下拉刷新手势;用刷新按钮/自动刷新
113
113
  - **触发防抖**:`load-more` 在 loading/loadingMore/error/finished 任一态下不触发——外部 [MUST] 及时置 loading/loadingMore 防重复请求
114
114
 
115
+ ## 面与面不叠(`itemCard`)
116
+
117
+ 项自带卡片时,容器**只撤面色、不撤呼吸与圆角**:
118
+
119
+ ```
120
+ 容器面(--el-bg-color) + 卡片面(--el-bg-color) = 同一个色
121
+ ⇒ gap 漏出的就是与卡片同色的容器面,视觉上「一整片白 + 若干 1px 细框」
122
+ ```
123
+
124
+ - 判据 `itemCard`:`TableMain` 缺省跟随 **`customView`**(该态下内层 `DataListView` 的 `card`
125
+ 默认开)、`DataGridView` 缺省跟随 **`card`**、`InfiniteListView` 缺省 **`false`**
126
+ (它没有内置卡壳、卡由消费方自画,core 无从判断 ⇒ 自画卡时 [MUST] 显式传 `itemCard`)。
127
+ - 🔴 判据 [MUST NOT] 收窄成「`card === true`」—— 那会整条漏掉 `InfiniteListView`。
128
+ - 🔴 **撤色不撤呼吸**:呼吸计入高度账(`surfacePad` 与可用高扣减同源),
129
+ 一并撤会连带改 refine 的 reserve、整页溢出。
130
+ - 阴性:`card: false` / `itemCard: false` 时容器**照常出面**(别把非卡片态的面一起干掉)。
131
+
115
132
  ## 关联
116
133
 
117
134
  - 数据容器:`DataGridView`(内部按 gridColumns chunk 行后喂本组件)
@@ -10,7 +10,7 @@
10
10
  - [ActionBtnGroup](./docs/README-ActionBtnGroup.md) — 配置式按钮组(渲染三态)
11
11
  - [ActionConfirm](./docs/README-ActionConfirm.md) — 确认原语(扁平,submitFn 返 Promise 才关)
12
12
  - [WatchSize](./docs/README-WatchSize.md) — 尺寸监听容器(mode / observeResize / debounce)
13
- - [HeightProvider](./docs/README-HeightProvider.md) — 纯数值高度预算节点(available = viewportHeight − reserve
13
+ - [HeightProvider](./docs/README-HeightProvider.md) — 纯数值高度预算节点(available = viewportHeight − reserve;reserve = #header 高 + #footer 高)
14
14
  - [BooleanTag](./docs/README-BooleanTag.md) — boolean 只读彩色标签
15
15
  - [ShadowClone](./docs/README-ShadowClone.md) — 影分身(零 DOM 输出双渲染位:本体就地 + 分身 teleport,目标晚渲染自动等待)
16
16
  - [DragFloat](./docs/README-DragFloat.md) — 可拖拽悬浮容器(宿主容器内约束 + 点击拖拽消歧 + 呼吸光影;首个消费者 = FormMain AI 填充触发器)
@@ -102,15 +102,44 @@ bridge.badge.set("/order/list", { kind: "new" });
102
102
 
103
103
  文案与语义色由 core 统一定(跨 app 一致);语义档解析到 `--el-color-*`,**主题风格 / 亮暗切换自动跟随**(规则 14,[MUST NOT] 写死色值)。
104
104
 
105
- | kind | 文案 | 语义色 | 为什么 |
106
- | --- | --- | --- | --- |
107
- | `count` | 数字 | `danger` 红 | 待办数量**要人行动**,未读红点是全行业最强共识 |
108
- | `new` | | `success` 绿 | 新功能上线是**正面资讯**(Atlassian success 档即覆盖 "added")。[MUST NOT] 复用红——会与 `count` 撞色,扫视时分不清「有新东西」和「有待办」 |
109
- | `updated` | 更新 | `primary` 主色 | 中性被动通知,不喧宾夺主 |
110
- | `beta` | 内测 | `warning` 橙 | 能用但**不稳定需谨慎**,正是 warning 语义 |
111
- | `wip` | 开发 | `info` 灰 | 尚不完善,中性、不引导点击。**仅提示,不拦点击** |
112
- | `custom` | 自定 | 缺省 `primary` | 内置档不够用时才用;`type` 只收语义档 |
113
- | `anchor` | —— | —— | **伪标 / 纯锚点**:不渲染角标,只留 DOM 挂点供引导等消费方定位 |
105
+ | kind | 气泡文案 | 图标(轮廓原型) | 语义色 | 为什么 |
106
+ | --- | --- | --- | --- | --- |
107
+ | `count` | —— | **数字**(唯一非图标档) | `danger` 红 | 待办数量要人行动,未读红点是全行业最强共识 |
108
+ | `new` | 新上线 | `Sunny`(中心圆 + 放射,**团**) | `success` 绿 | 正面资讯。[MUST NOT] 复用红——会与 `count` 撞色 |
109
+ | `updated` | 已更新 | `RefreshRight`(闭合**环** + 箭头) | `primary` 主色 | 中性被动通知,不喧宾夺主 |
110
+ | `beta` | 内测中 | `Opportunity`(钟形**灯泡**) | `warning` 橙 | 能用但不稳定需谨慎 |
111
+ | `wip` | 开发中 | `MoonNight`(**月牙**,`iconScale` 0.8) | `info` 灰 | 尚不完善,中性、不引导点击。**仅提示,不拦点击** |
112
+ | `planned` | 规划中 | `Calendar`(**方框 + 两耳**) | `info` | 还没开工,比 `wip` 更靠前。**不进遮挡档** |
113
+ | `deprecated` | 待下线 | `Warning`(**三角**) | `danger` | 「要迁走」需要用户采取行动 |
114
+ | `locked` | 未开通 | `Lock`(**拱 + 方体**) | `warning` 橙 | 权限边界,**唯一拦点击的档** |
115
+ | `custom` | —— | 自定义文案 | 缺省 `primary` | 内置档不够用时才用;`type` 只收语义档 |
116
+ | `anchor` | —— | —— | —— | **伪标 / 纯锚点**:不渲染角标,只留 DOM 挂点供引导等消费方定位 |
117
+
118
+ ### 🔴 内置档**一律图标 + 悬浮气泡**(新增 kind 前先读这条)
119
+
120
+ 依据是**角标逃逸量**:绝对定位下逃逸 = 角标宽 − 13px。文字档三字词宽 ≈50px、逃逸 36px
121
+ **会压到水平相邻的宿主**(实测右缘顶到下一个按钮);图标档角标宽 ≈24px、逃逸 ≈11px,
122
+ 只压进邻居的 padding。资讯三档当初留文字是**容忍**(2 字逃逸 24px 在安全线内)不是偏好 ⇒
123
+ v6odvs 起一并图标化,更安全。
124
+
125
+ **代价 [MUST] 知情**:三档从「扫一眼就懂」退成「要悬浮才懂」——
126
+ 故 `label` 气泡 [MUST NOT] 省,语义不能随文字一起消失。
127
+
128
+ #### 选图标的第一判据是**轮廓原型**,不是像不像
129
+
130
+ 镂空态下图标是语义色线条,**没有面、没有色差**可借 ⇒ 区分度只剩形状;12px 的尺度下笔画
131
+ 细节根本不成立。⇒ 七档 [MUST] 落在**七个不同的轮廓原型**上(团 / 环 / 灯泡 / 月牙 /
132
+ 方框耳 / 三角 / 拱方),[MUST] 并排截图看,[MUST NOT] 只看单档。
133
+
134
+ 两处刻意避开的撞车,[MUST NOT] 改回去:
135
+ - `new` **不取 `Star`** —— `wip` 的 `MoonNight` 自带小星星,12px 下会互相干扰;
136
+ - `beta` **不取 `Aim`(靶心)** —— 同心圆与 `Sunny` 同属「圆团」,七档里会出现两个团。
137
+
138
+ #### `iconScale`(预设里的**逐档**字段)
139
+
140
+ `MoonNight` 视觉重量比其余档满 ⇒ 取 `0.8`。走 `font-size`(`ElIcon` 尺寸本就是 `1em`,
141
+ 缩字号即等比缩,且不动角标盒的布局尺寸)。
142
+ 🔴 [MUST NOT] 提成全局常量或只给某一档打补丁 —— 全图标化之后后续还会有别的档要同样处理。
114
143
 
115
144
  ### `anchor`:只要挂点、不要角标
116
145
 
@@ -139,7 +168,20 @@ bridge.badge.set("/order/list", { kind: "new" });
139
168
  - **`custom` 传色值字面量**:`type` [MUST] 取语义档(`primary`/`success`/`warning`/`danger`/`info`),写死色跨主题不响应(规则 14)
140
169
  - **包纯文字却用默认 `corner`**:角标会悬在文字上方并溢出相邻行
141
170
  - **指望它做聚合**:本组件只按 key 查表渲染,core [MUST NOT] 把子级的标上卷到父级;聚合由 app 自算后往父级 key 写 entry
142
- - **角标文字色恒为白**(EP `--el-color-white`,主题不覆盖):若某主题的语义色很浅,白字对比度会不足——这是 EP 层面的约束
171
+ - **以为角标是实心底白字**:**已改镂空**(见下节)。旧描述作废,[MUST NOT] 照旧假设
172
+
173
+ ## 镂空(outline)观感
174
+
175
+ 角标走**镂空**:浅底 + 语义色 1px 边框 + 语义色文字 / 图标(原先是语义色实心底 + 白字)。
176
+
177
+ - 落点在 `use-theme-apply` 的 `EP_STRUCTURAL_CSS`(与其余 EP 结构性修正同源)——
178
+ 角标由 `ElBadge` 渲染在它自己的 DOM 上,[MUST NOT] 在 `BadgeMark.vue` 写 scoped(打不中)。
179
+ - 颜色全走 `--el-color-<档>` / `--dc-core-*` 双源(规则 14),[MUST NOT] 写死。
180
+ - **底不是纯透明**:角标叠在宿主内容上,全透会透出下面的字 ⇒ 用 `--el-bg-color`(面色)打底。
181
+ - 图标档随之从白色变**语义色线条**(`currentColor`)。
182
+
183
+ 要退回实心:在消费方侧覆盖 `.el-badge__content`(`background-color` / `color` / `border`)。
184
+ [MUST NOT] 指望有 prop 开关 —— 观感是全局一致口径,不做成逐处可调。
143
185
 
144
186
  ## 关联
145
187
 
@@ -1,13 +1,13 @@
1
1
  # HeightProvider(视口高度供给)
2
2
 
3
- > 视口高度供给:传入 viewportHeight,按 #header 实际高度(WatchSize 测高)扣减 reserve,向 #default 插槽提供 available 高度——页面容器高度链的源头。
3
+ > 视口高度供给:传入 viewportHeight,按 #header + #footer 两个固定区的实际高度(各自 WatchSize 测高)累加成 reserve 扣减,向 #default 插槽提供 available 高度——页面容器高度链的源头。
4
4
  > 范式页:无独立范式页(内嵌于 TabsMain refine 精细流等容器;核心实证:`packages/core/src/components/display/__tests__/HeightProvider.test.ts`)
5
5
 
6
6
  ## 定位
7
7
 
8
8
  - **何时用**:需要「视口高 − 页头高 → 内容可用高」计算的容器源头
9
9
  - **何时不用**:页签容器直接用 `TabsMain`(内部已接 HeightProvider,无需手动组合)
10
- - 不 inject/provide;Fragment 双根(header + default 并行渲染)
10
+ - 不 inject/provide;Fragment **多根**(header + default + footer 并行渲染,未传的槽不渲染)
11
11
 
12
12
  ## 快速上手(最小可用)
13
13
 
@@ -17,19 +17,23 @@
17
17
  <HeightProvider :viewportHeight="480">
18
18
  <template #header>...</template>
19
19
  <template #default="{ viewportHeight }">...</template>
20
+ <template #footer>...</template>
20
21
  </HeightProvider>
21
22
  ```
22
23
 
23
24
  **要点**:
24
25
 
25
26
  - `#default` scope `{ viewportHeight: available }`,available = max(viewportHeight - reserve, minHeight ?? 0)
26
- - `#header` WatchSize 测高得 reserve——header 高度变化自动回算
27
+ - **`reserve = #header + #footer 高`**,两槽各经一个 `WatchSize` 测高——任一区高度变化自动回算
28
+ - 两个槽都是**按需渲染**(`v-if="$slots.x"`):不传即不渲染、也不占 reserve,[MUST NOT] 传一个空 `<template>` 进来占位
29
+ - 渲染顺序 = `header → default → footer`(DOM 序即视觉序;本组件不做定位,"固定在底部"由父级的 flex 列方向给出)
27
30
  - [MUST NOT] 给 WatchSize 设 min-height: available(reserve↔available 反馈环 → 首帧震荡)
28
31
 
29
32
  ## 能力边界 / 按需使用
30
33
 
31
34
  - **默认场景:被容器组件内嵌消费,业务 [MUST NOT] 手动组合。** `TabsMain` refine 精细流内部即包 HeightProvider、`ListLayout` 已接管高度链——常规页签 / 列表页无需直用本组件。
32
- - **业务直用判据**:自定义容器需要「视口高 − 页头高 → 内容可用高」计算,且现有容器组件(TabsMain / ListLayout)接不上时才直用——形态:有固定 `#header` 区 + `#default` 需精确撑满到视口底;`#header` WatchSize 量高,header 高度变化自动回算。
35
+ - **业务直用判据**:自定义容器需要「视口高 − 固定区高 → 内容可用高」计算,且现有容器组件(TabsMain / ListLayout)接不上时才直用——形态:有固定 `#header` / `#footer` 区 + `#default` 需精确撑满到视口底;两区经 WatchSize 量高,高度变化自动回算。
36
+ - **`#footer`:仅当需要「不随内容滚的底部固定区」才传。** 只想把内容排在后面(自然流)就别用本组件,直接写在 `#default` 之后——传进 `#footer` 意味着它要从可用高里扣掉。
33
37
  - **`minHeight`:默认 `0` 即够用。** 仅当内容需要保底高度才设;⚠️ 超 viewportHeight 会撑破父容器——有意设计,[MUST NOT] 当 bug 修。
34
38
  - **`viewportHeight` 必填**:由父级传入(AppBody 高度链 / 显式值),本组件不 inject/provide。
35
39
  - **完整能力演示**:无独立范式页——核心实证 `packages/core/src/components/display/__tests__/HeightProvider.test.ts` + `TabsMain` refine 精细流消费(`apps/reference/src/pages/display/tabs/`)。
@@ -55,7 +59,8 @@
55
59
  | 槽 | scope | 语义 |
56
60
  | --- | --- | --- |
57
61
  | `#default` | `{ viewportHeight: available }` | 内容区(available = max(viewportHeight - reserve, minHeight ?? 0)) |
58
- | `#header` | 无 | 页头区(经 WatchSize 测高得 reserve |
62
+ | `#header` | 无 | 页头固定区(经 WatchSize 测高,计入 reserve);不传不渲染 |
63
+ | `#footer` | 无 | 页脚固定区(经 WatchSize 测高,计入 reserve);不传不渲染 |
59
64
 
60
65
  ### Expose
61
66
 
@@ -65,10 +70,11 @@
65
70
 
66
71
  - **WatchSize min-height: available 反馈环**:[MUST NOT]——reserve↔available 互算 → 首帧震荡
67
72
  - **minHeight 超 viewportHeight**:会撑破父容器——有意设计,[MUST NOT] 当 bug 修
68
- - **Fragment 双根**:不能直接作为单根子组件传 class/style 到外部
73
+ - **Fragment 多根**:不能直接作为单根子组件传 class/style 到外部(`TabsRefineFlow` 为此包了一层透明 div)
74
+ - **把不该扣的东西塞进 `#footer`**:[MUST NOT]——凡进这个槽的都会从 `#default` 的可用高里扣掉。随内容滚的尾部内容属于内容,写在 `#default` 里
69
75
 
70
76
  ## 关联
71
77
 
72
- - 内部:`WatchSize`(#header 量高)
73
- - 下游:`TabsMain`(refine 精细流内部包 HeightProvider)
78
+ - 内部:`WatchSize`(#header / #footer 各一个,量高得 reserve)
79
+ - 下游:`TabsMain`(refine 精细流内部包 HeightProvider)/ `ViewLayout`(toolbar 进 #header、`#bottom` 插槽进 #footer
74
80
  - 核心实证:`packages/core/src/components/display/__tests__/HeightProvider.test.ts`
@@ -91,6 +91,82 @@
91
91
 
92
92
 
93
93
 
94
+ ## 🔴 受控语义:外部拒绝变更时,组件回到外部给的值
95
+
96
+ `modelValue` 是**唯一真相源**,内部的 `fileList`(喂给 `ElUpload` 的那份)**恒与它对齐**:
97
+
98
+ ```
99
+ 对齐 = 以 modelValue 的 url 集合为准增删
100
+ [MUST NOT] 拿「fileList 是不是空的」当门槛 —— 那会让
101
+ 「外部不接受这次变更」和「外部接受了、只是值恰好相同」在组件眼里长得一模一样,
102
+ 而受控组件恰恰靠前者工作。
103
+ ```
104
+
105
+ 由此三条行为都成立(`["A"] → ["B"]` 换图 / `["A"] → []` 清空 / `[] → ["A"]` 填充),
106
+ 且**受控方不回写时组件回到空态** —— 上传完外部仍给 `[]`,列表就清回空、触发区还在、`✕` 可用。
107
+
108
+ ### 🔴 对齐的驱动源是**三个**,不是只有 `modelValue`
109
+
110
+ 「外部拒绝这次变更」时 `modelValue` 的**引用根本不变** ⇒ 只 watch 它,那一档永远不跑。
111
+ 故对齐挂在三源上(`flush: "post"`,同 tick 内齐变只收敛一次、无闪):
112
+
113
+ | 源 | 兜的是哪一路 |
114
+ | --- | --- |
115
+ | `modelValue` | 外部改值(含清空 / 原地换值) |
116
+ | `fileList` | EP 经 `v-model:file-list` **绕过 `modelValue` 直接塞条目** |
117
+ | 在途数归零 | EP 上传成功是**就地改条目**(`response` / `status`),数组引用可能不变、watch 不到 |
118
+
119
+ `flush: "post"` [MUST] 保留:上传成功那一瞬,「外部接受了」与「外部拒绝了」长得一模一样,
120
+ **只有等值传播完才分得开**。
121
+
122
+ ### ⚠️ 对齐**保住在途项**
123
+
124
+ 上传期间那一条**不在 `modelValue` 里**(它还没有最终 url)。对齐按「**有无最终 url**」划线:
125
+
126
+ | | 判据 | 对齐时 |
127
+ | --- | --- | --- |
128
+ | 已完成 / 回显项 | `file.response ?? file.url` **有值** | 按 `modelValue` 的 url 集合增删;命中的**复用原条目**(不重建,避免闪烁) |
129
+ | 在途项 | 两处皆无 | **原样保留**,排在已完成项之后 |
130
+
131
+ 🔴 **`blob:` 不算最终 url**:`list-type="picture-card"`(媒体三件的默认形态)下
132
+ EP 会给在途项塞一个**本地预览** `blob:` URL(`status` 还是 `ready`)。它不是上传结果、
133
+ 永远不会进 `modelValue` ⇒ 划线 [MUST] 排除它。不排除的话在途项会被当场摘掉,
134
+ 而那一摘撞进 EP 内部 `useVModel(passive)` 的 `isUpdating` 窗口,回灌被吞 ⇒
135
+ **EP 那份列表与本 hook 永久脱钩**(值空了、触发区回来了,卡片却删不掉)。
136
+
137
+ 🔴 **值里允许出现重复 url,复用 [MUST] 按次消费**:`modelValue` 是 `string[]`,
138
+ 同一个 url 出现两次是合法输入(后端去重存储 / 同一文件传两次 / 外部就那么配的)。
139
+ 复用索引若按 url 做「一对一」,重复那几项会拿到**同一个条目对象**放进列表多次 ⇒
140
+ EP 以 `uid` 作 key ⇒ **重复 key**、列表渲染紊乱(实测 DOM 闪出多余卡片约 400ms,
141
+ 终态才收敛 —— 所以**只断终态的测试对它是绿的**)。故索引值是队列:同 url 的第 N 次
142
+ 取第 N 个原条目,取空了才新建水合条目(新 uid)。
143
+
144
+ 🔴 [MUST NOT] 按 `uid` 或数组下标比对 —— 水合项的 uid 是本组件自增的、EP 塞进来的是
145
+ `genFileId()`,两套编号对不上;下标更是一改顺序就错位。
146
+
147
+ ### 两条渲染路径,删除入口是同一个
148
+
149
+ | 路径 | 谁在走 | 删除入口 |
150
+ | --- | --- | --- |
151
+ | EP 默认列表 | 裸 `FormUpload` | EP 的 `on-remove` → `handleRemove` |
152
+ | 自定义列表槽 `#file` | 媒体三件(`FormUploadCard` 卡片) | slot scope 的 `remove` = `() => handleRemove(file)` |
153
+
154
+ 两条路**共用同一份状态**(全仓 `useUploadState` 只有 `FormUpload` 一个使用方)⇒
155
+ 受控行为不会只坏一侧。但**自定义槽的重渲是独立的一条路径**,
156
+ 故它在 `__tests__/FormUploadMedia.test.ts` 有自己的守卫。
157
+
158
+ ### 删除是**自持**的
159
+
160
+ `✕` 直接把该条从 `fileList` 摘掉,再把 url 移出 `modelValue`。
161
+ 🔴 [MUST NOT] 把「删除生效」押在「外部会把值改掉 → 回流 watch 会清列表」这个假设上:
162
+ 外部完全可以不接受这次删除,那时列表该听外部的、而不是**卡在自己的中间态**。
163
+
164
+ ### 为什么这件事值得单独写一段
165
+
166
+ `fileList` 一旦与 `modelValue` 分叉,**五处**一起坏(不是三处):
167
+ 显示 · 删除 · 还能不能再传(触发区按 `fileList.length < maxCount` 显隐)·
168
+ `dc-form-upload_full` 满态样式 · `placeholder` 空态文案(按 `fileList.length === 0` 显隐)。
169
+
94
170
  ## 反模式 / 注意
95
171
 
96
172
  - **值 = URL 不是 File**:`modelValue` 语义是「已上传文件的 URL」——外部塞 File 对象进值是错误用法(表单 stringify 提交的是 URL);上传动作经 `uploadFn` 发生
@@ -98,6 +174,9 @@
98
174
  - **进度不自造**:ElUpload 内置进度条(on-progress → percent → 自动显示)——「把 onprogress 数据传给 getProgress 方法」是自造轮子;只有自定义进度呈现才需要自己拿 `event.percent`
99
175
  - **网络类属性无效**:`action` / `headers` / `http-request` 等传了被忽略(上传走 uploadFn)——别把后端上传配置塞 ElUpload 网络 props
100
176
  - **外部清空联动**:外部把值清空(`undefined` / 空数组)→ 文件列表同步清空——受控语义,别惊讶
177
+ - **给受控组件一个恒定不变的 model**:那等于「外部拒绝一切变更」⇒ 组件会**回到那个恒定值**
178
+ (如恒 `[]` 就是上传完回到空态)。这是受控语义的正确结果,[MUST NOT] 当 bug 报;
179
+ 要「传完就留在界面上」请把 `update:modelValue` 接回去,或用一次性实例(换 `key` 重建)
101
180
  - **多文件去重**:同 URL 重复上传不做客户端去重(服务端职责),`limit` 只限数量
102
181
 
103
182
  ## 关联
@@ -36,6 +36,25 @@
36
36
  - `#file` 插槽(scope 含 `remove`):完全自定义卡片内容(仍建议保留预览与删除能力)。
37
37
  - **完整能力演示**:范式页——单文件必传 / 多文件非必传组合见 FormMain 与 FormSubmitPanel reference。
38
38
 
39
+ ## 受控语义:与裸 `FormUpload` **同一份状态**
40
+
41
+ 值同步、无条件对齐、在途项保留、删除自持生效等全部受控行为,都由基座
42
+ `FormUpload` 的同一个 `useUploadState` 实例提供(薄封装只透传 `v-model`)——
43
+ 细节见 [README-FormUpload](./README-FormUpload.md) 的「受控语义」一节,
44
+ 这里 [MUST NOT] 另立一套说法。
45
+
46
+ 🔴 **薄封装与裸组件的唯一差别是那条渲染路径**:它走**自定义列表槽**
47
+ (`#file` → `FormUploadCard` + `:remove`),而裸组件走 EP 默认列表。
48
+ 所以:
49
+
50
+ - 卡片上的 `✕` 与裸组件的删除是**同一个** `handleRemove`(经 slot scope 的 `remove`
51
+ 下传),[MUST NOT] 在卡片里自行改 `fileList`;
52
+ - 对齐后卡片能否正确重渲,是**独立于 `useUploadState` 单测**的一条路径 ⇒
53
+ 它有自己的守卫(`__tests__/FormUploadMedia.test.ts` 的「默认卡片 ✕」两条),
54
+ [MUST NOT] 把基座那 8 条受控守卫在这里再抄一遍(同一份状态,抄了也是同一条路)。
55
+
56
+ 演示:`/form/upload/showcase` 的 **ⓖ 节**(受控拒绝 / 原地换值,图片与视频各一组)。
57
+
39
58
  ## API
40
59
  > ⚠️ API 以 types.ts 与组件源码为真相源;与文档冲突时以源码为准。
41
60
 
@@ -35,6 +35,25 @@
35
35
  - `#file` 插槽(scope 含 `remove`):完全自定义列表项(建议保留预览与删除)。
36
36
  - **完整能力演示**:范式页——单文件必传 / 多文件非必传组合见 FormMain 与 FormSubmitPanel reference。
37
37
 
38
+ ## 受控语义:与裸 `FormUpload` **同一份状态**
39
+
40
+ 值同步、无条件对齐、在途项保留、删除自持生效等全部受控行为,都由基座
41
+ `FormUpload` 的同一个 `useUploadState` 实例提供(薄封装只透传 `v-model`)——
42
+ 细节见 [README-FormUpload](./README-FormUpload.md) 的「受控语义」一节,
43
+ 这里 [MUST NOT] 另立一套说法。
44
+
45
+ 🔴 **薄封装与裸组件的唯一差别是那条渲染路径**:它走**自定义列表槽**
46
+ (`#file` → `FormUploadCard` + `:remove`),而裸组件走 EP 默认列表。
47
+ 所以:
48
+
49
+ - 卡片上的 `✕` 与裸组件的删除是**同一个** `handleRemove`(经 slot scope 的 `remove`
50
+ 下传),[MUST NOT] 在卡片里自行改 `fileList`;
51
+ - 对齐后卡片能否正确重渲,是**独立于 `useUploadState` 单测**的一条路径 ⇒
52
+ 它有自己的守卫(`__tests__/FormUploadMedia.test.ts` 的「默认卡片 ✕」两条),
53
+ [MUST NOT] 把基座那 8 条受控守卫在这里再抄一遍(同一份状态,抄了也是同一条路)。
54
+
55
+ 演示:`/form/upload/showcase` 的 **ⓖ 节**(受控拒绝 / 原地换值,图片与视频各一组)。
56
+
38
57
  ## API
39
58
  > ⚠️ API 以 types.ts 与组件源码为真相源;与文档冲突时以源码为准。
40
59
 
@@ -24,6 +24,21 @@
24
24
 
25
25
  两个形态件与 `use-list-detail.ts` [MUST NOT] 出桶——换承载体不算 breaking。
26
26
 
27
+ ## 详情区底边(`#detailBottom`)与页面底边(`#bottom`)
28
+
29
+ 门面上**两个都有、同名不同物**,[MUST] 分清:
30
+
31
+ - `#bottom` → `AppPage` 的**页面级四向悬浮槽**,覆盖整页底边(列表 + 详情都在它上方);
32
+ - `#detailBottom` → 逐层透到内置 `ViewLayout` 的 `#bottom`,只是**详情区内部**的底边
33
+ (split 档钉在详情区底 / sheet 档钉在抽屉底),scope 与 `#detail` 完全一致。
34
+
35
+ 判据:**对「当前这一条」的操作放 `#detailBottom`,对整页的放 `#bottom`。**
36
+ 用了 `#detailBottom` 即自动开 `ViewLayout.fillHeight`(内容盒定高 ⇒ 底边贴容器底);
37
+ 没用则一个字都不传,其余 refine 页面零波及。
38
+ 透传链四层:`AppPageListDetailLayout` → `Split` / `Sheet` → `AppPageDetail` → `ViewLayout#bottom`,
39
+ 每层 `v-if="$slots.detailBottom"` 守着 —— 不传即零渲染,[MUST NOT] 无条件透一个空槽下去
40
+ (精细流下空盒会被量高计入 reserve)。
41
+
27
42
  ## 标准的唯一例外
28
43
 
29
44
  `AppPageDetail` 的 **`embedded=true`** 是**显式 opt-out**:宿主(Split / Sheet)已经有一层
@@ -53,6 +53,7 @@
53
53
  | 槽 | scope | 语义 |
54
54
  | --------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
55
55
  | `#detail` | `{ id, record, index, total, hasPrev, hasNext, goPrev, goNext, refreshList, data, loading, fetchSeq }` | 详情内容。`data` / `loading` / `fetchSeq` 仅在传了 `detailApi` 时有值(`fetchSeq` = 成功拉取次数,失败不递增,未传 `detailApi` 恒 0);独立使用时序列字段为零值。⚠️ `resolvedMode` 只有门面下发,独立成页时无此键(没有形态概念) |
56
+ | `#detailBottom` | 同 `#detail` | **详情区内部底边**(透到内置 `ViewLayout` 的 `#bottom`):`refine` 开时固定在底、不随内容滚,且**自动开 `ViewLayout.fillHeight`** 让它贴住容器底(可经 `viewLayoutProps.fillHeight` 关)。**前提是传了 `detailApi`** —— 没有它就没有内置 `ViewLayout`、本槽无落点(同 `viewLayoutProps` 那条边界)。不传本槽即零渲染 |
56
57
  | `#empty` | `{ listEmpty }` | 无当前项时的占位 |
57
58
 
58
59
  ## 反模式 / 注意
@@ -61,6 +62,8 @@
61
62
  - **业务外层再套 `AppPage`** —— 默认态它自己就是 `AppPage`,套了成两层
62
63
  - **`embedded=true` 还指望 `dc-app-page-detail` 根类 / `$attrs` 落地** —— 透传态无宿主元素
63
64
  - **照抄 `SlotLayoutTemplate :disabled` 的写法去做需要具名槽的组件** —— 透传态只渲染默认槽,具名槽会被整个丢掉
65
+ - **没传 `detailApi` 却指望 `#detailBottom` 渲染** —— 它透到的是内置 `ViewLayout` 的 `#bottom`,容器都不存在时无落点;此形态下底部条自己写在 `#detail` 末尾
66
+ - **把随内容滚的尾部内容放 `#detailBottom`** —— 它是固定区,高度从详情内容可用高里扣;要跟着滚就写在 `#detail` 里
64
67
 
65
68
  ## 关联
66
69
 
@@ -122,6 +122,38 @@ watch(isOutOfSync, (v) => v && refreshList());
122
122
  | `#detail` | `{ id, record, index, total, hasPrev, hasNext, goPrev, goNext, refreshList, data, loading, fetchSeq, resolvedMode }` | 详情。`data` / `loading` / `fetchSeq` 仅在传了 `detailApi` 时有值 |
123
123
  | `#empty` | `{ listEmpty, resolvedMode }` | 未选中占位。`listEmpty=true` 列表本身无数据 / `false` 有数据但未选中,**两种文案 [MUST] 分流** |
124
124
  | `#top` / `#bottom` / `#right` | 无 | 直通 `AppPage` 同名悬浮槽(**两形态都支持,`auto` 自动挡切档后依旧在**) |
125
+ | `#detailBottom` | 同 `#detail` | **详情区内部底边**(透到 `ViewLayout` 的 `#bottom`):split 档固定在详情区底、sheet 档固定在抽屉底,内容再长也不滚走。⚠️ 与上面那个 `#bottom` **同名不同物**,见下 |
126
+
127
+ ### 🔴 `#bottom` vs `#detailBottom`(同名不同物,[MUST] 分清)
128
+
129
+ | 槽 | 挂在哪 | 覆盖范围 | 典型内容 |
130
+ | --- | --- | --- | --- |
131
+ | `#bottom` | `AppPage` 的**页面级四向悬浮槽** | 整个页面的底边(列表 + 详情都在它上方) | 页面级状态条 / 全局提示 |
132
+ | `#detailBottom` | `ViewLayout` 的 `#bottom`,在**详情区内部** | 只是详情那一块的底边 | **提交 / 审核 / 保存这一条**的固定操作条、详情摘要 |
133
+
134
+ 选择判据一句话:**这条按钮是对「当前这一条」的操作 → `#detailBottom`;是对整页的 → `#bottom`。**
135
+
136
+ `#detailBottom` 的 scope 与 `#detail` **完全一致**(含 `id` / `record` / 导航句柄 / `data` /
137
+ `loading` / `fetchSeq` / `resolvedMode`)—— 底部操作条要提交「这一条」,没有 `id` / `record`
138
+ 就无处写回。两槽并存时它们拿到的是同一份数据,[MUST NOT] 在 `#detail` 里自己缓存一份再传给底边。
139
+
140
+ ```vue
141
+ <template #detailBottom="{ id, record, loading, refreshList, hasNext, goNext }">
142
+ <ActionBtn type="primary" :disabled="loading" :onClick="() => submit(id)">提交</ActionBtn>
143
+ <ActionBtn v-if="hasNext" @click="goNext">保存并下一条</ActionBtn>
144
+ </template>
145
+ ```
146
+
147
+ - **sheet 档是本槽最大的受益场景**:抽屉里内容一长,写在 `#detail` 末尾的提交按钮会跟着滚走;
148
+ 放 `#detailBottom` 则钉在抽屉底边。
149
+ - **用了本槽即自动开 `ViewLayout.fillHeight`**(内容盒补 `min-height` 定高)——
150
+ 否则详情内容稀疏时底边只会跟在内容后面浮在半空、贴不到容器底。
151
+ 不想要这层定高(接受底边跟着内容走)时传 `viewLayoutProps: { fillHeight: false }` 关掉。
152
+ 🔴 没用本槽时**一个字都不传** ⇒ 其余 refine 页面零波及(这正是它做成 opt-in 的理由)。
153
+ - **前提:传了 `detailApi`**(本槽透到内置 `ViewLayout`,没有 `detailApi` 就没有那个容器 ⇒
154
+ 本槽无落点,与 `viewLayoutProps` 同一条边界)。详情数据业务自持时,底部条自己写在 `#detail` 里。
155
+ - **不传即零渲染**(逐层 `v-if` 守着),[MUST NOT] 传个空 `<template>` 占位 —— 精细流下它会被
156
+ 量高计入 reserve,详情内容区凭空少一截。
125
157
 
126
158
  ### `resolvedMode` —— 实际档位(三槽都有)
127
159
 
@@ -190,6 +222,7 @@ watch(isOutOfSync, (v) => v && refreshList());
190
222
  - **业务外层再套 `AppPage`** —— 页面根二选一,套了就成两层
191
223
  - **`#list` 里只 `v-bind` 一部分 `listProps`** —— 少接 `onDataChange` 则序列 / 自动激活失效、少接 `ref` 则 `refreshList` 失效,且**都不报错**
192
224
  - **自己给列表加 `:ref` 覆盖掉 `listProps.ref`** —— 需要自己也拿实例时 [MUST] 链式调用:`:ref="(el) => { myRef = el; listProps.ref(el) }"`
225
+ - **把整页级的东西塞进 `#detailBottom`(或反过来)** —— 两者同名不同物,见上表的选择判据
193
226
  - **运行时增删 `#top` / `#bottom` / `#right` / `#list`** —— `AppPage` 的槽集判定不响应运行时增删(既有限制),插槽集 [MUST] 当静态结构
194
227
  - **不传 `detailApi` 时期待内置上/下一条** —— 导航住在 `ViewLayout` 内;不内置 `ViewLayout` 就没有内置导航,用 `#detail` scope 的 `goPrev` / `goNext` 自绘
195
228
  - **删除 / 筛掉当前项后期待自动跳到下一条** —— 按不变式两值清空落空态(不自动重激活,避免自动触发详情副作用)
@@ -156,6 +156,23 @@ const toolbarConfig: TableToolbarConfig<Row> = {
156
156
  - **[MUST NOT] 从外面给 `.dc-table-main` 刷 padding / 背景**:面归组件自持,外面刷会让内呼吸游离在高度账之外(这正是被修掉的老毛病)。要关面用 `:surface="false"`
157
157
  - **TableSkeleton / TableToolbar / ToolbarButtons 是内部件**:index.ts 不导出,[MUST NOT] 外部直接消费;对外只认 `TableMain` + types/constants
158
158
 
159
+ ## 面与面不叠(`itemCard`)
160
+
161
+ 项自带卡片时,容器**只撤面色、不撤呼吸与圆角**:
162
+
163
+ ```
164
+ 容器面(--el-bg-color) + 卡片面(--el-bg-color) = 同一个色
165
+ ⇒ gap 漏出的就是与卡片同色的容器面,视觉上「一整片白 + 若干 1px 细框」
166
+ ```
167
+
168
+ - 判据 `itemCard`:`TableMain` 缺省跟随 **`customView`**(该态下内层 `DataListView` 的 `card`
169
+ 默认开)、`DataGridView` 缺省跟随 **`card`**、`InfiniteListView` 缺省 **`false`**
170
+ (它没有内置卡壳、卡由消费方自画,core 无从判断 ⇒ 自画卡时 [MUST] 显式传 `itemCard`)。
171
+ - 🔴 判据 [MUST NOT] 收窄成「`card === true`」—— 那会整条漏掉 `InfiniteListView`。
172
+ - 🔴 **撤色不撤呼吸**:呼吸计入高度账(`surfacePad` 与可用高扣减同源),
173
+ 一并撤会连带改 refine 的 reserve、整页溢出。
174
+ - 阴性:`card: false` / `itemCard: false` 时容器**照常出面**(别把非卡片态的面一起干掉)。
175
+
159
176
  ## 关联
160
177
 
161
178
  - 组合页:`ListLayout`(list-layout 族,内部 FormSearch + TableMain,常用配置提升为直属 prop,TableMain 恒收 `refine=false`)