@tfdesign/b-end 1.0.19 → 1.1.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.
- package/package.json +1 -1
- package/skills/tfds/SKILL.md +1 -1
- package/skills/tfds/components.index.json +23 -3
- package/skills/tfds/components.summary.json +4 -4
- package/src/_b_end_runtime/components/Filter.jsx +57 -14
- package/src/_b_end_runtime/components/Table.jsx +289 -60
- package/src/_b_end_runtime/components/TablePreview.jsx +24 -9
- package/src/_b_end_runtime/components.js +11 -2
- package/src/_b_end_runtime/preview-registry.jsx +3 -2
- package/src/index.d.ts +4 -0
package/package.json
CHANGED
package/skills/tfds/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"system": "b-end",
|
|
3
3
|
"skill": "tfds",
|
|
4
|
-
"generatedAt": "2026-05-
|
|
4
|
+
"generatedAt": "2026-05-14T14:00:01.732Z",
|
|
5
5
|
"summary": "B 端 COMPONENTS 37 条 + 列表页模板 4 + PATTERNS 6;import 一律来自 @tfdesign/b-end。",
|
|
6
6
|
"components": [
|
|
7
7
|
{
|
|
@@ -5205,6 +5205,11 @@
|
|
|
5205
5205
|
"【基础组件复用】凡是平台已有基础组件可承载的类型,Table 内必须直接复用:switch→Switch、checkbox→Checkbox、avatar/avatarText→Avatar、tag→Tag、link/textButton/actions/dragHandle/分页按钮→Button、所有图标→Icon、页容量选择器→Select",
|
|
5206
5206
|
"【尺寸】cellSize 支持 default / middle / small,按设计稿映射为 12px / 8px / 4px 内边距与对应单元高度",
|
|
5207
5207
|
"【标题与操作】link、actions 使用 Button 的 text-brand 变体;textButton 使用 Button 的 iconOnly 组合;linkDescription 保留标题+描述双行信息密度",
|
|
5208
|
+
"【标准表格 actions 尾栏定位】只要标准表格使用 type=\"actions\",该列就是行级决策区,视觉位置必须默认在 table 最右侧尾栏。即使 columns 传入顺序不是最后,Table 渲染时也会把 actions 列归位到尾栏;AI 生成 columns 时应主动把 actions 写在最后,避免读代码时产生误判。",
|
|
5209
|
+
"【标准表格 actions 自适应宽度】actions 尾栏宽度必须由当前页真实操作按钮内容统一计算,只包住“编辑 / 查看 / 更多图标”等可见操作及必要单元格内边距;表头与表体必须共用同一列宽,不能让表头按“操作”两个字单独收窄而和表体错位。actions 不参与普通数据列的 fr 比例分配,禁止留大段空白尾距,也不要用固定大宽度制造对齐假象。",
|
|
5210
|
+
"【标准表格 actions 首屏可见】当页面容器宽度不足、表格发生横向滚动时,actions 尾栏必须自动 sticky 吸附在右侧可视边界,保证“编辑 / 查看 / 更多图标”等行操作在第一屏始终可见。不要要求用户先横向滚到最右才能操作;也不要把操作按钮复制到首列或悬浮到表格外。",
|
|
5211
|
+
"【标准表格 actions 固定列关系】actions 自动吸附是组件默认交互,不依赖 fixedColumnsMode。fixedColumnsMode 只用于额外固定首列或非 actions 尾列;当 actions 与 fixedColumnsMode=\"first\"/\"both\" 同时存在时,首列可固定在左侧,actions 仍固定在右侧,形成左右锚点,服务宽表的扫读与快速决策。",
|
|
5212
|
+
"【标准表格 actions 省略】actions 类型必须直接传完整操作数组,Table 默认只露出前 2 个文字 Button;当存在第 3 个及后续操作时,第 3 个位置自动渲染 Button iconOnly + Icon(dots-horizontal-stroked) 作为更多按钮,禁止把“…”当文字文案渲染。hover/focus 更多按钮会展示所有被省略操作(从原第 3 项开始),省略浮层内的每个选项也必须复用基础 Button,禁止用原生 button、span 或 div 手写操作入口。",
|
|
5208
5213
|
"【复合内容】avatar、avatarText、image、progress 等类型优先保证内容完整显示,再参与列宽比例伸缩;avatar/avatarText 单元格未传 avatarSrc 时默认按 name/description seed 取本地成员头像,禁止为表格负责人列生成随机外链头像",
|
|
5209
5214
|
"【分页】pagination 为对象配置,支持 current/pageSize/total/pageSizeOptions/summaryText/displayPages/pageSizeLabel。分页区包含统计文案、页码、翻页箭头和每页条数选择器;页码选中态固定为 bg-brand-50 + text-brand-600 + semibold,数字颜色必须是 Brand 600。",
|
|
5210
5215
|
"【分页真实数量】预览和模板内不得写死虚假 total。若 dataSource 是完整本地数据,pagination.total 必须等于 dataSource.length,Table 会按 current/pageSize 对 dataSource 自动切片,左侧“10条/20条”、实际展示行数、底部统计范围、总条数和页码数量必须保持一致。若业务使用服务端分页且 total 大于当前页 dataSource.length,需显式传入后端 total,并只传当前页数据。"
|
|
@@ -6292,6 +6297,16 @@
|
|
|
6292
6297
|
"type": "array",
|
|
6293
6298
|
"default": []
|
|
6294
6299
|
},
|
|
6300
|
+
{
|
|
6301
|
+
"name": "multiple",
|
|
6302
|
+
"type": "boolean",
|
|
6303
|
+
"default": true
|
|
6304
|
+
},
|
|
6305
|
+
{
|
|
6306
|
+
"name": "allValue",
|
|
6307
|
+
"type": "string|number",
|
|
6308
|
+
"default": "all"
|
|
6309
|
+
},
|
|
6295
6310
|
{
|
|
6296
6311
|
"name": "selectedValues",
|
|
6297
6312
|
"type": "array"
|
|
@@ -6344,8 +6359,9 @@
|
|
|
6344
6359
|
"【选型·vs Button】Filter 只用于筛选条件选择/收起/清除;普通动作(提交、取消、导出、新建)仍使用 Button。",
|
|
6345
6360
|
"【尺寸】固定高度 36px(--size-control-md),左右内距 12px(--spacing-3,对齐 Select md),内容 gap 8px(--spacing-2);不要随意压缩为 24px 或 32px,以保证和 B 端 Input/Select 默认高度对齐。",
|
|
6346
6361
|
"【文字】label 使用 semibold 600,value 使用 normal 400;文案建议 label 2-6 字、value 1-8 字,过长值应在业务层截断或改用 Tooltip。",
|
|
6362
|
+
"【全部基准态】options 中 `value` 等于 `allValue`(默认 \"all\")的选项视为未筛选基准态,兼容旧写法中 label 以“全部”开头的选项;默认推荐文案只写“全部”,触发器呈现为“筛选项 全部”这一类标题+值组合,保持白底常规样式、不显示清除入口;只有选中非“全部”值或显式 selected=true 时才变为品牌绿色筛选态。",
|
|
6347
6363
|
"【下拉单/多选】传入 options 后点击胶囊展开下拉面板;通过 `multiple` 切换:默认 `multiple={true}` 多选(行尾 checkbox 方框,可同时勾多项);`multiple={false}` 单选(行尾 ✓,点击即提交并自动关闭面板,再次点击已选项可反选清空)。两种模式都支持 selectedValues 受控、defaultValue 非受控、onChange 回调;点击外部或 Escape 关闭。",
|
|
6348
|
-
"【受控/回调签名】多选:selectedValues 传数组,onChange 返回数组;单选:selectedValues 可传单值或单元素数组,onChange
|
|
6364
|
+
"【受控/回调签名】多选:selectedValues 传数组,onChange 返回数组;单选:selectedValues 可传单值或单元素数组,onChange 返回单值(无“全部”基准值时清空返回 null)。defaultValue 同样支持单值/数组两种形态。",
|
|
6349
6365
|
"【值展示】未显式传 value 时,单选/多选选中 1 项都展示该项 label,多选选中 ≥2 项展示“已选 N 项”,单选始终最多 1 项 label;显式 value 优先用于自定义展示。",
|
|
6350
6366
|
"【状态】selected=true 使用品牌浅底 + 品牌描边 + 品牌文字;filled=true 使用中性填充底;disabled=true 使用禁用文字与禁用底,且不可点击。",
|
|
6351
6367
|
"【图标】无选中值时显示 12px 下拉箭头;closable=true 或已选中且未禁用时显示 16px 清除按钮(视觉与 Select 清除入口一致),点击清空多选值、关闭下拉并触发 onClear,不触发展开。",
|
|
@@ -6358,9 +6374,13 @@
|
|
|
6358
6374
|
"label": "基础筛选项",
|
|
6359
6375
|
"code": "<Filter label=\"筛选项\" />"
|
|
6360
6376
|
},
|
|
6377
|
+
{
|
|
6378
|
+
"label": "全部默认态",
|
|
6379
|
+
"code": "<Filter label=\"筛选项\" options={[{ label: \"全部\", value: \"all\" }, { label: \"平台申请\", value: \"platform\" }, { label: \"外部导入\", value: \"external\" }]} defaultValue=\"all\" />"
|
|
6380
|
+
},
|
|
6361
6381
|
{
|
|
6362
6382
|
"label": "多选下拉(默认)",
|
|
6363
|
-
"code": "<Filter label=\"筛选项\" options={[{ label: \"选项一\", value: \"1\" }, { label: \"选项二\", value: \"2\" }]} />"
|
|
6383
|
+
"code": "<Filter label=\"筛选项\" options={[{ label: \"全部\", value: \"all\" }, { label: \"选项一\", value: \"1\" }, { label: \"选项二\", value: \"2\" }]} />"
|
|
6364
6384
|
},
|
|
6365
6385
|
{
|
|
6366
6386
|
"label": "单选下拉",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"system": "b-end",
|
|
3
3
|
"skill": "tfds",
|
|
4
|
-
"generatedAt": "2026-05-
|
|
4
|
+
"generatedAt": "2026-05-14T14:00:01.732Z",
|
|
5
5
|
"purpose": "轻量组件与页面模板目录。AI 先读本文件做选型;确定命中组件后,再到 components.index.json 按 id 读取 props / rules / examples。",
|
|
6
6
|
"counts": {
|
|
7
7
|
"total": 47,
|
|
@@ -1052,7 +1052,7 @@
|
|
|
1052
1052
|
"avatar",
|
|
1053
1053
|
"avatarText"
|
|
1054
1054
|
],
|
|
1055
|
-
"ruleCount":
|
|
1055
|
+
"ruleCount": 30,
|
|
1056
1056
|
"exampleCount": 12,
|
|
1057
1057
|
"hasCode": false,
|
|
1058
1058
|
"detailRef": "components.index.json#table"
|
|
@@ -1318,8 +1318,8 @@
|
|
|
1318
1318
|
"multiple={false}",
|
|
1319
1319
|
"selected filter"
|
|
1320
1320
|
],
|
|
1321
|
-
"ruleCount":
|
|
1322
|
-
"exampleCount":
|
|
1321
|
+
"ruleCount": 15,
|
|
1322
|
+
"exampleCount": 16,
|
|
1323
1323
|
"hasCode": false,
|
|
1324
1324
|
"detailRef": "components.index.json#filter"
|
|
1325
1325
|
},
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @prop {string|number|null} [value=null] — 已选值,存在时显示在 label 后
|
|
8
8
|
* @prop {Array<{label:string,value:string|number,disabled?:boolean}>} [options=[]] — 下拉选项
|
|
9
9
|
* @prop {boolean} [multiple=true] — 是否多选;`false` 进入单选模式(点击即提交并关闭面板,行尾用 ✓ 而非 checkbox 方框,触发器仅显示 1 项 label,aria-multiselectable=false)
|
|
10
|
+
* @prop {string|number} [allValue='all'] — “全部”基准选项 value;该选项视为未筛选态,触发器保持白底常规样式
|
|
10
11
|
* @prop {Array<string|number>|string|number} [selectedValues] — 受控值;多选传数组、单选传单值(也兼容单元素数组)
|
|
11
12
|
* @prop {Array<string|number>|string|number} [defaultValue=[]] — 非受控初始值
|
|
12
13
|
* @prop {function|null} [onChange=null] — 变更回调;多选返回数组,单选返回单值(或 null 已清空)
|
|
@@ -131,6 +132,12 @@ function getValueKey(item) {
|
|
|
131
132
|
return String(item);
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
function isAllOption(option, allValue) {
|
|
136
|
+
if (!option) return false;
|
|
137
|
+
if (getValueKey(option.value) === getValueKey(allValue)) return true;
|
|
138
|
+
return String(option.label || '').trim().startsWith('全部');
|
|
139
|
+
}
|
|
140
|
+
|
|
134
141
|
function normalizeOptions(options) {
|
|
135
142
|
const source = Array.isArray(options) && options.length > 0 ? options : [];
|
|
136
143
|
return source
|
|
@@ -177,6 +184,7 @@ export default function Filter({
|
|
|
177
184
|
value = null,
|
|
178
185
|
options = [],
|
|
179
186
|
multiple = true,
|
|
187
|
+
allValue = 'all',
|
|
180
188
|
selectedValues,
|
|
181
189
|
defaultValue,
|
|
182
190
|
onChange = null,
|
|
@@ -195,11 +203,18 @@ export default function Filter({
|
|
|
195
203
|
const panelRef = useRef(null);
|
|
196
204
|
const normalizedOptions = useMemo(() => normalizeOptions(options), [options]);
|
|
197
205
|
const hasDropdown = normalizedOptions.length > 0;
|
|
206
|
+
const allOption = useMemo(
|
|
207
|
+
() => normalizedOptions.find((item) => isAllOption(item, allValue)) || null,
|
|
208
|
+
[allValue, normalizedOptions],
|
|
209
|
+
);
|
|
210
|
+
const fallbackValues = useMemo(
|
|
211
|
+
() => (allOption ? [allOption.value] : []),
|
|
212
|
+
[allOption],
|
|
213
|
+
);
|
|
198
214
|
const isControlled = selectedValues !== undefined;
|
|
199
215
|
const initialValues = useMemo(
|
|
200
|
-
() => normalizeValueList(defaultValue !== undefined ? defaultValue :
|
|
201
|
-
|
|
202
|
-
[],
|
|
216
|
+
() => normalizeValueList(defaultValue !== undefined ? defaultValue : fallbackValues),
|
|
217
|
+
[defaultValue, fallbackValues],
|
|
203
218
|
);
|
|
204
219
|
const [innerValues, setInnerValues] = useState(() => (multiple ? initialValues : initialValues.slice(0, 1)));
|
|
205
220
|
const [open, setOpen] = useState(false);
|
|
@@ -209,6 +224,10 @@ export default function Filter({
|
|
|
209
224
|
width: 180,
|
|
210
225
|
maxHeight: PANEL_MAX_HEIGHT,
|
|
211
226
|
});
|
|
227
|
+
const isFallbackValue = useCallback(
|
|
228
|
+
(item) => fallbackValues.some((fallback) => getValueKey(fallback) === getValueKey(item)),
|
|
229
|
+
[fallbackValues],
|
|
230
|
+
);
|
|
212
231
|
const currentValues = isControlled
|
|
213
232
|
? (multiple ? normalizeValueList(selectedValues) : normalizeValueList(selectedValues).slice(0, 1))
|
|
214
233
|
: innerValues;
|
|
@@ -220,19 +239,25 @@ export default function Filter({
|
|
|
220
239
|
() => normalizedOptions.filter((item) => selectedKeySet.has(getValueKey(item.value))),
|
|
221
240
|
[normalizedOptions, selectedKeySet],
|
|
222
241
|
);
|
|
242
|
+
const effectiveSelectedOptions = useMemo(
|
|
243
|
+
() => selectedOptions.filter((item) => !isAllOption(item, allValue)),
|
|
244
|
+
[allValue, selectedOptions],
|
|
245
|
+
);
|
|
246
|
+
const hasExplicitValue = value !== null && value !== undefined && value !== '';
|
|
223
247
|
const derivedValue = useMemo(() => {
|
|
224
|
-
if (
|
|
248
|
+
if (hasExplicitValue) return value;
|
|
225
249
|
if (selectedOptions.length === 1) return selectedOptions[0].label;
|
|
226
250
|
if (!multiple) return null;
|
|
227
251
|
if (selectedOptions.length > 1) return `已选 ${selectedOptions.length} 项`;
|
|
228
252
|
return null;
|
|
229
|
-
}, [multiple, selectedOptions, value]);
|
|
253
|
+
}, [hasExplicitValue, multiple, selectedOptions, value]);
|
|
230
254
|
const hasValue = derivedValue !== null && derivedValue !== undefined && derivedValue !== '';
|
|
231
|
-
const
|
|
255
|
+
const hasEffectiveFilter = effectiveSelectedOptions.length > 0;
|
|
256
|
+
const isSelected = selected || hasEffectiveFilter;
|
|
232
257
|
const tone = disabled
|
|
233
258
|
? (isSelected ? 'selectedDisabled' : 'disabled')
|
|
234
259
|
: (isSelected ? 'selected' : (filled ? 'fill' : 'outline'));
|
|
235
|
-
const showClear =
|
|
260
|
+
const showClear = !disabled && hasValue && (hasEffectiveFilter || (closable && hasExplicitValue));
|
|
236
261
|
|
|
237
262
|
const updatePosition = useCallback(() => {
|
|
238
263
|
if (!triggerRef.current) return;
|
|
@@ -254,19 +279,30 @@ export default function Filter({
|
|
|
254
279
|
if (option.disabled) return;
|
|
255
280
|
const optionKey = getValueKey(option.value);
|
|
256
281
|
const exists = selectedKeySet.has(optionKey);
|
|
282
|
+
if (isAllOption(option, allValue)) {
|
|
283
|
+
commitValues([option.value], event);
|
|
284
|
+
if (!multiple) {
|
|
285
|
+
setOpen(false);
|
|
286
|
+
triggerRef.current?.focus();
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
257
290
|
if (multiple) {
|
|
258
291
|
const nextValues = exists
|
|
259
|
-
? currentValues.filter((item) => getValueKey(item) !== optionKey)
|
|
260
|
-
: [
|
|
261
|
-
|
|
292
|
+
? currentValues.filter((item) => getValueKey(item) !== optionKey && !isFallbackValue(item))
|
|
293
|
+
: [
|
|
294
|
+
...currentValues.filter((item) => !isFallbackValue(item)),
|
|
295
|
+
option.value,
|
|
296
|
+
];
|
|
297
|
+
commitValues(nextValues.length > 0 ? nextValues : fallbackValues, event);
|
|
262
298
|
return;
|
|
263
299
|
}
|
|
264
|
-
//
|
|
265
|
-
const nextValues = exists ?
|
|
300
|
+
// 单选:再次点击已选项视为反选回“全部”;否则替换为该值并关闭面板
|
|
301
|
+
const nextValues = exists ? fallbackValues : [option.value];
|
|
266
302
|
commitValues(nextValues, event);
|
|
267
303
|
setOpen(false);
|
|
268
304
|
triggerRef.current?.focus();
|
|
269
|
-
}, [commitValues, currentValues, multiple, selectedKeySet]);
|
|
305
|
+
}, [allValue, commitValues, currentValues, fallbackValues, isFallbackValue, multiple, selectedKeySet]);
|
|
270
306
|
|
|
271
307
|
const toggleOpen = useCallback(() => {
|
|
272
308
|
if (disabled || !hasDropdown) return;
|
|
@@ -283,7 +319,7 @@ export default function Filter({
|
|
|
283
319
|
event.stopPropagation();
|
|
284
320
|
if (disabled) return;
|
|
285
321
|
if (hasDropdown) {
|
|
286
|
-
commitValues(
|
|
322
|
+
commitValues(fallbackValues, event);
|
|
287
323
|
setOpen(false);
|
|
288
324
|
}
|
|
289
325
|
onClear?.(event);
|
|
@@ -301,6 +337,13 @@ export default function Filter({
|
|
|
301
337
|
}
|
|
302
338
|
};
|
|
303
339
|
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
if (isControlled || defaultValue !== undefined || innerValues.length > 0 || fallbackValues.length === 0) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
setInnerValues(multiple ? fallbackValues : fallbackValues.slice(0, 1));
|
|
345
|
+
}, [defaultValue, fallbackValues, innerValues.length, isControlled, multiple]);
|
|
346
|
+
|
|
304
347
|
useEffect(() => {
|
|
305
348
|
if (!open) return undefined;
|
|
306
349
|
const handlePointerDown = (event) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
2
3
|
import Avatar from './Avatar';
|
|
3
4
|
import Button from './Button';
|
|
4
5
|
import { Checkbox } from './Checkbox';
|
|
@@ -17,6 +18,7 @@ import { getTeamAvatarBySeed, getTeamMemberByIndex } from '../teamMembers';
|
|
|
17
18
|
* avatar / avatarText 单元格未传 avatarSrc 时,会按 name/description 从本地成员头像素材中取默认图片。
|
|
18
19
|
*
|
|
19
20
|
* @prop {Array<object>} [columns=[]] — 列配置,支持 title / key / dataIndex / width / minWidth / align / type / render
|
|
21
|
+
* type="actions" 会被自动归位到表格尾栏,并在横向空间不足时 sticky 固定在右侧可视区
|
|
20
22
|
* @prop {Array<object>} [dataSource=[]] — 行数据
|
|
21
23
|
* @prop {'table'|'card-form'} [variant='table'] — 展示类型:table 为标准表格;card-form 为卡片型表单
|
|
22
24
|
* @prop {string|((record: object, index: number) => string|number)} [rowKey='id'] — 行 key
|
|
@@ -57,7 +59,7 @@ const DEFAULT_COLUMN_WIDTH = {
|
|
|
57
59
|
image: 132,
|
|
58
60
|
progress: 164,
|
|
59
61
|
datetime: 180,
|
|
60
|
-
actions:
|
|
62
|
+
actions: 104,
|
|
61
63
|
dragHandle: 68,
|
|
62
64
|
};
|
|
63
65
|
|
|
@@ -76,7 +78,7 @@ const MIN_COLUMN_WIDTH = {
|
|
|
76
78
|
image: 112,
|
|
77
79
|
progress: 132,
|
|
78
80
|
datetime: 156,
|
|
79
|
-
actions:
|
|
81
|
+
actions: 96,
|
|
80
82
|
dragHandle: 52,
|
|
81
83
|
};
|
|
82
84
|
|
|
@@ -187,10 +189,18 @@ const CREATOR_NAME = 'flex-1 min-w-0 truncate text-left text-sm font-normal lead
|
|
|
187
189
|
const AVATAR_TEXT_WRAP = 'flex min-w-0 flex-1 flex-col items-start justify-center gap-1 text-left';
|
|
188
190
|
|
|
189
191
|
/* ── 操作区 ── */
|
|
190
|
-
const ACTION_WRAP = 'flex w-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
'!
|
|
192
|
+
const ACTION_WRAP = 'inline-flex w-max min-w-max items-center justify-start gap-2 text-left';
|
|
193
|
+
const ACTION_OVERFLOW_TRIGGER_CLASS = [
|
|
194
|
+
ACTION_TEXT_BUTTON_CLASS,
|
|
195
|
+
'!w-6 !justify-center',
|
|
196
|
+
].join(' ');
|
|
197
|
+
const ACTION_OVERFLOW_PANEL = [
|
|
198
|
+
'tfds-table-action-overflow',
|
|
199
|
+
'flex min-w-[112px] flex-col gap-1 rounded-md border border-border-default bg-surface p-1 shadow-lg',
|
|
200
|
+
].join(' ');
|
|
201
|
+
const ACTION_OVERFLOW_BUTTON_CLASS = [
|
|
202
|
+
'!h-8 !w-full !justify-start !rounded-sm !px-2',
|
|
203
|
+
'!text-left !text-sm !leading-5',
|
|
194
204
|
].join(' ');
|
|
195
205
|
const TEXT_BUTTON_ICON_CLASS = [
|
|
196
206
|
'!size-6 shrink-0 !rounded-md !border-transparent !p-0 !text-foreground',
|
|
@@ -290,6 +300,21 @@ function normalizeCellSize(cellSize) {
|
|
|
290
300
|
return 'default';
|
|
291
301
|
}
|
|
292
302
|
|
|
303
|
+
function isActionsColumn(column) {
|
|
304
|
+
return normalizeColumnType(column?.type) === 'actions';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function normalizeTableColumns(columns) {
|
|
308
|
+
if (!Array.isArray(columns) || columns.length === 0) return [];
|
|
309
|
+
const regularColumns = [];
|
|
310
|
+
const actionColumns = [];
|
|
311
|
+
columns.forEach((column) => {
|
|
312
|
+
if (isActionsColumn(column)) actionColumns.push(column);
|
|
313
|
+
else regularColumns.push(column);
|
|
314
|
+
});
|
|
315
|
+
return [...regularColumns, ...actionColumns];
|
|
316
|
+
}
|
|
317
|
+
|
|
293
318
|
function getTypeMinHeight(type, sizeKey) {
|
|
294
319
|
return TYPE_MIN_HEIGHT[type]?.[sizeKey] ?? TYPE_MIN_HEIGHT.text[sizeKey];
|
|
295
320
|
}
|
|
@@ -304,14 +329,50 @@ function resolveColumnMetrics(column) {
|
|
|
304
329
|
};
|
|
305
330
|
}
|
|
306
331
|
|
|
307
|
-
function
|
|
332
|
+
function getTextVisualWidth(text) {
|
|
333
|
+
return String(text || '').split('').reduce((sum, char) => (
|
|
334
|
+
sum + (char.charCodeAt(0) > 255 ? 14 : 7)
|
|
335
|
+
), 0);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function getActionColumnWidth(column, rows = []) {
|
|
339
|
+
const type = normalizeColumnType(column?.type);
|
|
340
|
+
if (type !== 'actions') return null;
|
|
341
|
+
const configuredWidth = Number(column.width);
|
|
342
|
+
if (Number.isFinite(configuredWidth) && configuredWidth > 0) {
|
|
343
|
+
return configuredWidth;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const cellSize = normalizeCellSize(column.cellSize);
|
|
347
|
+
const horizontalPadding = cellSize === 'small' ? 16 : 24;
|
|
348
|
+
const gap = 8;
|
|
349
|
+
const overflowButtonWidth = 24;
|
|
350
|
+
const labels = rows.flatMap((record) => {
|
|
351
|
+
const rawValue = column.dataIndex ? record?.[column.dataIndex] : record?.[column.key];
|
|
352
|
+
const actions = Array.isArray(rawValue) ? rawValue : [];
|
|
353
|
+
const visibleActions = actions.length > 2 ? actions.slice(0, 2) : actions;
|
|
354
|
+
const textWidth = visibleActions.reduce((sum, action) => sum + getTextVisualWidth(getActionLabel(action)), 0);
|
|
355
|
+
const visibleCount = visibleActions.length + (actions.length > 2 ? 1 : 0);
|
|
356
|
+
const gapWidth = Math.max(0, visibleCount - 1) * gap;
|
|
357
|
+
const overflowWidth = actions.length > 2 ? overflowButtonWidth : 0;
|
|
358
|
+
return [horizontalPadding + textWidth + gapWidth + overflowWidth];
|
|
359
|
+
});
|
|
360
|
+
const contentWidth = Math.max(0, ...labels);
|
|
361
|
+
const minWidth = Number(column.minWidth ?? MIN_COLUMN_WIDTH.actions);
|
|
362
|
+
return Math.ceil(Math.max(minWidth, contentWidth));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function resolveColumnTrack(column, rows) {
|
|
366
|
+
if (isActionsColumn(column)) {
|
|
367
|
+
return `${getActionColumnWidth(column, rows)}px`;
|
|
368
|
+
}
|
|
308
369
|
const { baseWidth, minWidth } = resolveColumnMetrics(column);
|
|
309
370
|
return `minmax(${minWidth}px, ${baseWidth}fr)`;
|
|
310
371
|
}
|
|
311
372
|
|
|
312
|
-
function buildColumnGridTemplate(columns) {
|
|
373
|
+
function buildColumnGridTemplate(columns, rows) {
|
|
313
374
|
if (!columns.length) return 'minmax(0, 1fr)';
|
|
314
|
-
return columns.map((column) => resolveColumnTrack(column)).join(' ');
|
|
375
|
+
return columns.map((column) => resolveColumnTrack(column, rows)).join(' ');
|
|
315
376
|
}
|
|
316
377
|
|
|
317
378
|
function getColumnTemplateMinWidth(columns) {
|
|
@@ -319,10 +380,11 @@ function getColumnTemplateMinWidth(columns) {
|
|
|
319
380
|
return columns.reduce((sum, column) => sum + resolveColumnMetrics(column).minWidth, 0);
|
|
320
381
|
}
|
|
321
382
|
|
|
322
|
-
function getPinnedColumnStates(columns, fixedColumnsMode) {
|
|
383
|
+
function getPinnedColumnStates(columns, fixedColumnsMode, rows = []) {
|
|
323
384
|
const states = columns.map(() => null);
|
|
385
|
+
const hasActionsTail = columns.length > 0 && isActionsColumn(columns[columns.length - 1]);
|
|
324
386
|
|
|
325
|
-
if (!columns.length || fixedColumnsMode === 'none') {
|
|
387
|
+
if (!columns.length || (fixedColumnsMode === 'none' && !hasActionsTail)) {
|
|
326
388
|
return states;
|
|
327
389
|
}
|
|
328
390
|
|
|
@@ -336,12 +398,25 @@ function getPinnedColumnStates(columns, fixedColumnsMode) {
|
|
|
336
398
|
};
|
|
337
399
|
}
|
|
338
400
|
|
|
339
|
-
if (isLastColumnFixed(fixedColumnsMode) && lastColumnIndex > 0) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
401
|
+
if ((isLastColumnFixed(fixedColumnsMode) || hasActionsTail) && lastColumnIndex > 0) {
|
|
402
|
+
let rightPinnedStart = lastColumnIndex;
|
|
403
|
+
if (hasActionsTail) {
|
|
404
|
+
while (rightPinnedStart > 0 && isActionsColumn(columns[rightPinnedStart - 1])) {
|
|
405
|
+
rightPinnedStart -= 1;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let offset = 0;
|
|
410
|
+
for (let index = lastColumnIndex; index >= rightPinnedStart; index -= 1) {
|
|
411
|
+
states[index] = {
|
|
412
|
+
side: 'right',
|
|
413
|
+
offset,
|
|
414
|
+
edge: index === rightPinnedStart ? 'left' : null,
|
|
415
|
+
};
|
|
416
|
+
offset += isActionsColumn(columns[index])
|
|
417
|
+
? getActionColumnWidth(columns[index], rows)
|
|
418
|
+
: resolveColumnMetrics(columns[index]).minWidth;
|
|
419
|
+
}
|
|
345
420
|
}
|
|
346
421
|
|
|
347
422
|
return states;
|
|
@@ -729,6 +804,184 @@ function renderProgressValue(value) {
|
|
|
729
804
|
);
|
|
730
805
|
}
|
|
731
806
|
|
|
807
|
+
function getActionLabel(action) {
|
|
808
|
+
return resolveTextValue(action?.label || action?.ariaLabel || action?.tooltip || '更多操作');
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function measureActionOverflowPosition(trigger) {
|
|
812
|
+
if (typeof window === 'undefined') {
|
|
813
|
+
return { top: 0, left: 0 };
|
|
814
|
+
}
|
|
815
|
+
const rect = trigger.getBoundingClientRect();
|
|
816
|
+
const panelWidth = 112;
|
|
817
|
+
const top = rect.bottom + 6;
|
|
818
|
+
const left = Math.min(
|
|
819
|
+
Math.max(4, rect.left),
|
|
820
|
+
Math.max(4, window.innerWidth - panelWidth - 4),
|
|
821
|
+
);
|
|
822
|
+
return { top, left };
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function renderActionButton(action, key, { className = ACTION_TEXT_BUTTON_CLASS, onDone } = {}) {
|
|
826
|
+
const safeAction = action || {};
|
|
827
|
+
const label = getActionLabel(safeAction);
|
|
828
|
+
const handleClick = (event) => {
|
|
829
|
+
safeAction.onClick?.(event);
|
|
830
|
+
onDone?.();
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
if (safeAction.href) {
|
|
834
|
+
return (
|
|
835
|
+
<Button
|
|
836
|
+
key={key}
|
|
837
|
+
as="a"
|
|
838
|
+
href={safeAction.href}
|
|
839
|
+
variant="text-brand"
|
|
840
|
+
size="sm"
|
|
841
|
+
className={className}
|
|
842
|
+
onClick={safeAction.onClick}
|
|
843
|
+
role={onDone ? 'menuitem' : undefined}
|
|
844
|
+
>
|
|
845
|
+
<span className={ACTION_FULL_TEXT}>{label}</span>
|
|
846
|
+
</Button>
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return (
|
|
851
|
+
<Button
|
|
852
|
+
key={key}
|
|
853
|
+
type="button"
|
|
854
|
+
variant="text-brand"
|
|
855
|
+
size="sm"
|
|
856
|
+
className={className}
|
|
857
|
+
disabled={safeAction.disabled}
|
|
858
|
+
onClick={handleClick}
|
|
859
|
+
role={onDone ? 'menuitem' : undefined}
|
|
860
|
+
>
|
|
861
|
+
<span className={ACTION_FULL_TEXT}>{label}</span>
|
|
862
|
+
</Button>
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function ActionOverflowMenu({ actions = [] }) {
|
|
867
|
+
const triggerRef = useRef(null);
|
|
868
|
+
const panelRef = useRef(null);
|
|
869
|
+
const closeTimer = useRef(null);
|
|
870
|
+
const [open, setOpen] = useState(false);
|
|
871
|
+
const [style, setStyle] = useState({ top: 0, left: 0 });
|
|
872
|
+
|
|
873
|
+
const updatePosition = useCallback(() => {
|
|
874
|
+
if (!triggerRef.current) return;
|
|
875
|
+
setStyle(measureActionOverflowPosition(triggerRef.current));
|
|
876
|
+
}, []);
|
|
877
|
+
|
|
878
|
+
const clearCloseTimer = useCallback(() => {
|
|
879
|
+
if (closeTimer.current) {
|
|
880
|
+
clearTimeout(closeTimer.current);
|
|
881
|
+
closeTimer.current = null;
|
|
882
|
+
}
|
|
883
|
+
}, []);
|
|
884
|
+
|
|
885
|
+
const openPanel = useCallback(() => {
|
|
886
|
+
clearCloseTimer();
|
|
887
|
+
updatePosition();
|
|
888
|
+
setOpen(true);
|
|
889
|
+
requestAnimationFrame(updatePosition);
|
|
890
|
+
}, [clearCloseTimer, updatePosition]);
|
|
891
|
+
|
|
892
|
+
const closePanel = useCallback(() => {
|
|
893
|
+
clearCloseTimer();
|
|
894
|
+
closeTimer.current = setTimeout(() => setOpen(false), 120);
|
|
895
|
+
}, [clearCloseTimer]);
|
|
896
|
+
|
|
897
|
+
const closeImmediately = useCallback(() => {
|
|
898
|
+
clearCloseTimer();
|
|
899
|
+
setOpen(false);
|
|
900
|
+
}, [clearCloseTimer]);
|
|
901
|
+
|
|
902
|
+
useEffect(() => () => clearCloseTimer(), [clearCloseTimer]);
|
|
903
|
+
|
|
904
|
+
useEffect(() => {
|
|
905
|
+
if (!open) return undefined;
|
|
906
|
+
const handleScrollOrResize = () => updatePosition();
|
|
907
|
+
window.addEventListener('scroll', handleScrollOrResize, true);
|
|
908
|
+
window.addEventListener('resize', handleScrollOrResize);
|
|
909
|
+
return () => {
|
|
910
|
+
window.removeEventListener('scroll', handleScrollOrResize, true);
|
|
911
|
+
window.removeEventListener('resize', handleScrollOrResize);
|
|
912
|
+
};
|
|
913
|
+
}, [open, updatePosition]);
|
|
914
|
+
|
|
915
|
+
const handleKeyDown = (event) => {
|
|
916
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
917
|
+
event.preventDefault();
|
|
918
|
+
if (open) closeImmediately();
|
|
919
|
+
else openPanel();
|
|
920
|
+
}
|
|
921
|
+
if (event.key === 'Escape') {
|
|
922
|
+
closeImmediately();
|
|
923
|
+
triggerRef.current?.focus();
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
const panel = open && typeof document !== 'undefined' ? createPortal(
|
|
928
|
+
<div
|
|
929
|
+
ref={panelRef}
|
|
930
|
+
role="menu"
|
|
931
|
+
className={ACTION_OVERFLOW_PANEL}
|
|
932
|
+
data-tfds-component="Table.ActionOverflow"
|
|
933
|
+
style={{
|
|
934
|
+
position: 'fixed',
|
|
935
|
+
top: style.top,
|
|
936
|
+
left: style.left,
|
|
937
|
+
zIndex: 9999,
|
|
938
|
+
}}
|
|
939
|
+
onMouseEnter={openPanel}
|
|
940
|
+
onMouseLeave={closePanel}
|
|
941
|
+
>
|
|
942
|
+
{actions.map((action, index) => renderActionButton(
|
|
943
|
+
action,
|
|
944
|
+
action?.key || `${getActionLabel(action)}-${index}`,
|
|
945
|
+
{ className: ACTION_OVERFLOW_BUTTON_CLASS, onDone: closeImmediately },
|
|
946
|
+
))}
|
|
947
|
+
</div>,
|
|
948
|
+
document.body,
|
|
949
|
+
) : null;
|
|
950
|
+
|
|
951
|
+
return (
|
|
952
|
+
<>
|
|
953
|
+
<span
|
|
954
|
+
ref={triggerRef}
|
|
955
|
+
className="inline-flex"
|
|
956
|
+
onMouseEnter={openPanel}
|
|
957
|
+
onMouseLeave={closePanel}
|
|
958
|
+
onFocus={openPanel}
|
|
959
|
+
onBlur={closePanel}
|
|
960
|
+
>
|
|
961
|
+
<Button
|
|
962
|
+
type="button"
|
|
963
|
+
variant="text-brand"
|
|
964
|
+
size="sm"
|
|
965
|
+
iconOnly
|
|
966
|
+
icon={<Icon name="dots-horizontal-stroked" size="sm" />}
|
|
967
|
+
className={ACTION_OVERFLOW_TRIGGER_CLASS}
|
|
968
|
+
tooltip={open ? null : '更多操作'}
|
|
969
|
+
aria-label="更多操作"
|
|
970
|
+
aria-haspopup="menu"
|
|
971
|
+
aria-expanded={open || undefined}
|
|
972
|
+
onClick={(event) => {
|
|
973
|
+
event.stopPropagation();
|
|
974
|
+
if (open) closeImmediately();
|
|
975
|
+
else openPanel();
|
|
976
|
+
}}
|
|
977
|
+
onKeyDown={handleKeyDown}
|
|
978
|
+
/>
|
|
979
|
+
</span>
|
|
980
|
+
{panel}
|
|
981
|
+
</>
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
|
|
732
985
|
function renderActionsValue(value) {
|
|
733
986
|
const items = Array.isArray(value) ? value : [];
|
|
734
987
|
|
|
@@ -736,44 +989,16 @@ function renderActionsValue(value) {
|
|
|
736
989
|
return null;
|
|
737
990
|
}
|
|
738
991
|
|
|
992
|
+
const visibleItems = items.length > 2 ? items.slice(0, 2) : items;
|
|
993
|
+
const overflowItems = items.length > 2 ? items.slice(2) : [];
|
|
994
|
+
|
|
739
995
|
return (
|
|
740
996
|
<div className={ACTION_WRAP}>
|
|
741
|
-
{
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
<Button
|
|
747
|
-
key={key}
|
|
748
|
-
type="button"
|
|
749
|
-
variant="text-brand"
|
|
750
|
-
size="sm"
|
|
751
|
-
iconOnly
|
|
752
|
-
icon={<Icon name={action.iconName || 'dots-horizontal-stroked'} size="sm" />}
|
|
753
|
-
className={[ICON_ONLY_BUTTON_RESET, MORE_ACTION_BUTTON_CLASS].join(' ')}
|
|
754
|
-
onClick={action.onClick}
|
|
755
|
-
tooltip={action.tooltip || action.ariaLabel || action.label || '更多操作'}
|
|
756
|
-
aria-label={action.ariaLabel || action.label || '更多操作'}
|
|
757
|
-
/>
|
|
758
|
-
);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
if (action.href) {
|
|
762
|
-
const label = resolveTextValue(action.label);
|
|
763
|
-
return (
|
|
764
|
-
<Button key={key} as="a" href={action.href} variant="text-brand" size="sm" className={ACTION_TEXT_BUTTON_CLASS}>
|
|
765
|
-
<span className={ACTION_FULL_TEXT}>{label}</span>
|
|
766
|
-
</Button>
|
|
767
|
-
);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
const label = resolveTextValue(action.label);
|
|
771
|
-
return (
|
|
772
|
-
<Button key={key} type="button" variant="text-brand" size="sm" className={ACTION_TEXT_BUTTON_CLASS} onClick={action.onClick}>
|
|
773
|
-
<span className={ACTION_FULL_TEXT}>{label}</span>
|
|
774
|
-
</Button>
|
|
775
|
-
);
|
|
776
|
-
})}
|
|
997
|
+
{visibleItems.map((action, index) => renderActionButton(
|
|
998
|
+
action,
|
|
999
|
+
action?.key || `${getActionLabel(action)}-${index}`,
|
|
1000
|
+
))}
|
|
1001
|
+
{overflowItems.length > 0 ? <ActionOverflowMenu actions={overflowItems} /> : null}
|
|
777
1002
|
</div>
|
|
778
1003
|
);
|
|
779
1004
|
}
|
|
@@ -1136,11 +1361,15 @@ export default function Table({
|
|
|
1136
1361
|
const pageSizeOptions = pagination?.pageSizeOptions?.length
|
|
1137
1362
|
? pagination.pageSizeOptions
|
|
1138
1363
|
: [pageSize];
|
|
1139
|
-
const
|
|
1140
|
-
const
|
|
1364
|
+
const displayColumns = useMemo(() => normalizeTableColumns(columns), [columns]);
|
|
1365
|
+
const columnTemplate = useMemo(
|
|
1366
|
+
() => buildColumnGridTemplate(displayColumns, visibleDataSource),
|
|
1367
|
+
[displayColumns, visibleDataSource],
|
|
1368
|
+
);
|
|
1369
|
+
const columnTemplateMinWidth = useMemo(() => getColumnTemplateMinWidth(displayColumns), [displayColumns]);
|
|
1141
1370
|
const pinnedColumnStates = useMemo(
|
|
1142
|
-
() => getPinnedColumnStates(
|
|
1143
|
-
[
|
|
1371
|
+
() => getPinnedColumnStates(displayColumns, fixedColumnsMode, visibleDataSource),
|
|
1372
|
+
[displayColumns, fixedColumnsMode, visibleDataSource],
|
|
1144
1373
|
);
|
|
1145
1374
|
const selectValue = Number.isNaN(Number(pageSize)) ? pageSize : Number(pageSize);
|
|
1146
1375
|
const selectOptions = useMemo(() => {
|
|
@@ -1301,9 +1530,9 @@ export default function Table({
|
|
|
1301
1530
|
<div className={TABLE_SHELL}>
|
|
1302
1531
|
<div className={TABLE_VIEWPORT}>
|
|
1303
1532
|
<div className={TABLE_CONTENT} style={{ minWidth: `${columnTemplateMinWidth}px` }}>
|
|
1304
|
-
<div className={TABLE} role="grid" aria-rowcount={visibleDataSource.length + 1} aria-colcount={
|
|
1533
|
+
<div className={TABLE} role="grid" aria-rowcount={visibleDataSource.length + 1} aria-colcount={displayColumns.length}>
|
|
1305
1534
|
<div className={HEADER_ROW} role="row" style={{ gridTemplateColumns: columnTemplate }}>
|
|
1306
|
-
{
|
|
1535
|
+
{displayColumns.map((column, columnIndex) => (
|
|
1307
1536
|
<div
|
|
1308
1537
|
key={column.key || column.dataIndex || column.title}
|
|
1309
1538
|
className={[
|
|
@@ -1341,7 +1570,7 @@ export default function Table({
|
|
|
1341
1570
|
style={{ gridTemplateColumns: columnTemplate }}
|
|
1342
1571
|
data-tfds-component="Table.Row"
|
|
1343
1572
|
>
|
|
1344
|
-
{
|
|
1573
|
+
{displayColumns.map((column, columnIndex) => (
|
|
1345
1574
|
<div
|
|
1346
1575
|
key={column.key || column.dataIndex || column.title}
|
|
1347
1576
|
className={buildCellClass(
|
|
@@ -50,7 +50,7 @@ const TYPE_WIDTH_MAP = {
|
|
|
50
50
|
image: 132,
|
|
51
51
|
progress: 164,
|
|
52
52
|
datetime: 180,
|
|
53
|
-
actions:
|
|
53
|
+
actions: 104,
|
|
54
54
|
dragHandle: 68,
|
|
55
55
|
};
|
|
56
56
|
|
|
@@ -291,9 +291,11 @@ function buildCellValue(type, index) {
|
|
|
291
291
|
return pickMockValue(DATETIME_VALUES, index);
|
|
292
292
|
case 'actions':
|
|
293
293
|
return [
|
|
294
|
-
{ key: 'edit', label:
|
|
295
|
-
{ key: 'view', label:
|
|
296
|
-
{ key: '
|
|
294
|
+
{ key: 'edit', label: '编辑', onClick: () => {} },
|
|
295
|
+
{ key: 'view', label: '查看', onClick: () => {} },
|
|
296
|
+
{ key: 'copy', label: '复制', onClick: () => {} },
|
|
297
|
+
{ key: 'offline', label: '下线', onClick: () => {} },
|
|
298
|
+
{ key: 'delete', label: '删除', onClick: () => {} },
|
|
297
299
|
];
|
|
298
300
|
case 'dragHandle':
|
|
299
301
|
return null;
|
|
@@ -362,9 +364,19 @@ function enhanceCellValue(type, value, { rowIndex, column, setRecords, setIntera
|
|
|
362
364
|
onClick: () => setInteractionMessage(buildActionMessage('查看', rowIndex, columnTitle)),
|
|
363
365
|
},
|
|
364
366
|
{
|
|
365
|
-
key: '
|
|
366
|
-
|
|
367
|
-
onClick: () => setInteractionMessage(buildActionMessage('
|
|
367
|
+
key: 'copy',
|
|
368
|
+
label: '复制',
|
|
369
|
+
onClick: () => setInteractionMessage(buildActionMessage('复制', rowIndex, columnTitle)),
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
key: 'offline',
|
|
373
|
+
label: '下线',
|
|
374
|
+
onClick: () => setInteractionMessage(buildActionMessage('下线', rowIndex, columnTitle)),
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
key: 'delete',
|
|
378
|
+
label: '删除',
|
|
379
|
+
onClick: () => setInteractionMessage(buildActionMessage('删除', rowIndex, columnTitle)),
|
|
368
380
|
},
|
|
369
381
|
];
|
|
370
382
|
case 'dragHandle':
|
|
@@ -379,7 +391,7 @@ function enhanceCellValue(type, value, { rowIndex, column, setRecords, setIntera
|
|
|
379
391
|
function buildColumns(controlValues) {
|
|
380
392
|
const tableSize = controlValues.tableSize || 'default';
|
|
381
393
|
const headerCount = Math.max(2, Math.min(6, Number(controlValues.headerCount || 6)));
|
|
382
|
-
|
|
394
|
+
const columns = Array.from({ length: headerCount }, (_, index) => {
|
|
383
395
|
const type = controlValues[`column${index + 1}Type`] || COLUMN_DEFAULT_TYPES[index] || 'text';
|
|
384
396
|
|
|
385
397
|
return {
|
|
@@ -388,9 +400,12 @@ function buildColumns(controlValues) {
|
|
|
388
400
|
type,
|
|
389
401
|
cellSize: tableSize,
|
|
390
402
|
dataIndex: `column${index + 1}`,
|
|
391
|
-
width: TYPE_WIDTH_MAP[type] || 160,
|
|
403
|
+
width: type === 'actions' ? undefined : TYPE_WIDTH_MAP[type] || 160,
|
|
392
404
|
};
|
|
393
405
|
});
|
|
406
|
+
const regularColumns = columns.filter((column) => column.type !== 'actions');
|
|
407
|
+
const actionColumns = columns.filter((column) => column.type === 'actions');
|
|
408
|
+
return [...regularColumns, ...actionColumns];
|
|
394
409
|
}
|
|
395
410
|
|
|
396
411
|
function buildRow(index, columns) {
|
|
@@ -2812,6 +2812,11 @@ export const COMPONENTS = [
|
|
|
2812
2812
|
'【基础组件复用】凡是平台已有基础组件可承载的类型,Table 内必须直接复用:switch→Switch、checkbox→Checkbox、avatar/avatarText→Avatar、tag→Tag、link/textButton/actions/dragHandle/分页按钮→Button、所有图标→Icon、页容量选择器→Select',
|
|
2813
2813
|
'【尺寸】cellSize 支持 default / middle / small,按设计稿映射为 12px / 8px / 4px 内边距与对应单元高度',
|
|
2814
2814
|
'【标题与操作】link、actions 使用 Button 的 text-brand 变体;textButton 使用 Button 的 iconOnly 组合;linkDescription 保留标题+描述双行信息密度',
|
|
2815
|
+
'【标准表格 actions 尾栏定位】只要标准表格使用 type="actions",该列就是行级决策区,视觉位置必须默认在 table 最右侧尾栏。即使 columns 传入顺序不是最后,Table 渲染时也会把 actions 列归位到尾栏;AI 生成 columns 时应主动把 actions 写在最后,避免读代码时产生误判。',
|
|
2816
|
+
'【标准表格 actions 自适应宽度】actions 尾栏宽度必须由当前页真实操作按钮内容统一计算,只包住“编辑 / 查看 / 更多图标”等可见操作及必要单元格内边距;表头与表体必须共用同一列宽,不能让表头按“操作”两个字单独收窄而和表体错位。actions 不参与普通数据列的 fr 比例分配,禁止留大段空白尾距,也不要用固定大宽度制造对齐假象。',
|
|
2817
|
+
'【标准表格 actions 首屏可见】当页面容器宽度不足、表格发生横向滚动时,actions 尾栏必须自动 sticky 吸附在右侧可视边界,保证“编辑 / 查看 / 更多图标”等行操作在第一屏始终可见。不要要求用户先横向滚到最右才能操作;也不要把操作按钮复制到首列或悬浮到表格外。',
|
|
2818
|
+
'【标准表格 actions 固定列关系】actions 自动吸附是组件默认交互,不依赖 fixedColumnsMode。fixedColumnsMode 只用于额外固定首列或非 actions 尾列;当 actions 与 fixedColumnsMode="first"/"both" 同时存在时,首列可固定在左侧,actions 仍固定在右侧,形成左右锚点,服务宽表的扫读与快速决策。',
|
|
2819
|
+
'【标准表格 actions 省略】actions 类型必须直接传完整操作数组,Table 默认只露出前 2 个文字 Button;当存在第 3 个及后续操作时,第 3 个位置自动渲染 Button iconOnly + Icon(dots-horizontal-stroked) 作为更多按钮,禁止把“…”当文字文案渲染。hover/focus 更多按钮会展示所有被省略操作(从原第 3 项开始),省略浮层内的每个选项也必须复用基础 Button,禁止用原生 button、span 或 div 手写操作入口。',
|
|
2815
2820
|
'【复合内容】avatar、avatarText、image、progress 等类型优先保证内容完整显示,再参与列宽比例伸缩;avatar/avatarText 单元格未传 avatarSrc 时默认按 name/description seed 取本地成员头像,禁止为表格负责人列生成随机外链头像',
|
|
2816
2821
|
'【分页】pagination 为对象配置,支持 current/pageSize/total/pageSizeOptions/summaryText/displayPages/pageSizeLabel。分页区包含统计文案、页码、翻页箭头和每页条数选择器;页码选中态固定为 bg-brand-50 + text-brand-600 + semibold,数字颜色必须是 Brand 600。',
|
|
2817
2822
|
'【分页真实数量】预览和模板内不得写死虚假 total。若 dataSource 是完整本地数据,pagination.total 必须等于 dataSource.length,Table 会按 current/pageSize 对 dataSource 自动切片,左侧“10条/20条”、实际展示行数、底部统计范围、总条数和页码数量必须保持一致。若业务使用服务端分页且 total 大于当前页 dataSource.length,需显式传入后端 total,并只传当前页数据。',
|
|
@@ -3274,6 +3279,8 @@ export const COMPONENTS = [
|
|
|
3274
3279
|
{ name: 'label', type: 'string', default: '筛选项' },
|
|
3275
3280
|
{ name: 'value', type: 'string|number|null', default: null },
|
|
3276
3281
|
{ name: 'options', type: 'array', default: [] },
|
|
3282
|
+
{ name: 'multiple', type: 'boolean', default: true },
|
|
3283
|
+
{ name: 'allValue', type: 'string|number', default: 'all' },
|
|
3277
3284
|
{ name: 'selectedValues', type: 'array', default: undefined },
|
|
3278
3285
|
{ name: 'defaultValue', type: 'array', default: [] },
|
|
3279
3286
|
{ name: 'onChange', type: 'function', default: null },
|
|
@@ -3298,8 +3305,9 @@ export const COMPONENTS = [
|
|
|
3298
3305
|
'【选型·vs Button】Filter 只用于筛选条件选择/收起/清除;普通动作(提交、取消、导出、新建)仍使用 Button。',
|
|
3299
3306
|
'【尺寸】固定高度 36px(--size-control-md),左右内距 12px(--spacing-3,对齐 Select md),内容 gap 8px(--spacing-2);不要随意压缩为 24px 或 32px,以保证和 B 端 Input/Select 默认高度对齐。',
|
|
3300
3307
|
'【文字】label 使用 semibold 600,value 使用 normal 400;文案建议 label 2-6 字、value 1-8 字,过长值应在业务层截断或改用 Tooltip。',
|
|
3308
|
+
'【全部基准态】options 中 `value` 等于 `allValue`(默认 "all")的选项视为未筛选基准态,兼容旧写法中 label 以“全部”开头的选项;默认推荐文案只写“全部”,触发器呈现为“筛选项 全部”这一类标题+值组合,保持白底常规样式、不显示清除入口;只有选中非“全部”值或显式 selected=true 时才变为品牌绿色筛选态。',
|
|
3301
3309
|
'【下拉单/多选】传入 options 后点击胶囊展开下拉面板;通过 `multiple` 切换:默认 `multiple={true}` 多选(行尾 checkbox 方框,可同时勾多项);`multiple={false}` 单选(行尾 ✓,点击即提交并自动关闭面板,再次点击已选项可反选清空)。两种模式都支持 selectedValues 受控、defaultValue 非受控、onChange 回调;点击外部或 Escape 关闭。',
|
|
3302
|
-
'【受控/回调签名】多选:selectedValues 传数组,onChange 返回数组;单选:selectedValues 可传单值或单元素数组,onChange
|
|
3310
|
+
'【受控/回调签名】多选:selectedValues 传数组,onChange 返回数组;单选:selectedValues 可传单值或单元素数组,onChange 返回单值(无“全部”基准值时清空返回 null)。defaultValue 同样支持单值/数组两种形态。',
|
|
3303
3311
|
'【值展示】未显式传 value 时,单选/多选选中 1 项都展示该项 label,多选选中 ≥2 项展示“已选 N 项”,单选始终最多 1 项 label;显式 value 优先用于自定义展示。',
|
|
3304
3312
|
'【状态】selected=true 使用品牌浅底 + 品牌描边 + 品牌文字;filled=true 使用中性填充底;disabled=true 使用禁用文字与禁用底,且不可点击。',
|
|
3305
3313
|
'【图标】无选中值时显示 12px 下拉箭头;closable=true 或已选中且未禁用时显示 16px 清除按钮(视觉与 Select 清除入口一致),点击清空多选值、关闭下拉并触发 onClear,不触发展开。',
|
|
@@ -3309,7 +3317,8 @@ export const COMPONENTS = [
|
|
|
3309
3317
|
],
|
|
3310
3318
|
examples: [
|
|
3311
3319
|
{ label: '基础筛选项', code: '<Filter label="筛选项" />' },
|
|
3312
|
-
{ label: '
|
|
3320
|
+
{ label: '全部默认态', code: '<Filter label="筛选项" options={[{ label: "全部", value: "all" }, { label: "平台申请", value: "platform" }, { label: "外部导入", value: "external" }]} defaultValue="all" />' },
|
|
3321
|
+
{ label: '多选下拉(默认)', code: '<Filter label="筛选项" options={[{ label: "全部", value: "all" }, { label: "选项一", value: "1" }, { label: "选项二", value: "2" }]} />' },
|
|
3313
3322
|
{ label: '单选下拉', code: '<Filter label="状态" multiple={false} options={[{ label: "全部", value: "all" }, { label: "进行中", value: "running" }, { label: "已完成", value: "done" }]} />' },
|
|
3314
3323
|
{ label: '默认已选(多选)', code: '<Filter label="筛选项" options={options} defaultValue={["1"]} />' },
|
|
3315
3324
|
{ label: '默认已选(单选)', code: '<Filter label="状态" multiple={false} options={options} defaultValue="running" />' },
|
|
@@ -140,6 +140,7 @@ function SwitchPreview({ variant = 'brand', defaultChecked, disabled }) {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
const FILTER_SAMPLE_OPTIONS = [
|
|
143
|
+
{ label: '全部', value: 'all' },
|
|
143
144
|
{ label: '选项一', value: 'option-1' },
|
|
144
145
|
{ label: '选项二', value: 'option-2' },
|
|
145
146
|
{ label: '选项三', value: 'option-3' },
|
|
@@ -150,11 +151,11 @@ function FilterPreview({
|
|
|
150
151
|
disabled = false,
|
|
151
152
|
}) {
|
|
152
153
|
const [singleValues, setSingleValues] = useState(
|
|
153
|
-
initial === 'selected' ? ['option-1'] : [],
|
|
154
|
+
initial === 'selected' ? ['option-1'] : ['all'],
|
|
154
155
|
);
|
|
155
156
|
|
|
156
157
|
useEffect(() => {
|
|
157
|
-
setSingleValues(initial === 'selected' ? ['option-1'] : []);
|
|
158
|
+
setSingleValues(initial === 'selected' ? ['option-1'] : ['all']);
|
|
158
159
|
}, [initial]);
|
|
159
160
|
|
|
160
161
|
return (
|
package/src/index.d.ts
CHANGED
|
@@ -961,6 +961,10 @@ export interface FilterProps extends TfdsCommonProps {
|
|
|
961
961
|
value?: unknown;
|
|
962
962
|
/** array, default: [] */
|
|
963
963
|
options?: unknown[];
|
|
964
|
+
/** boolean, default: true */
|
|
965
|
+
multiple?: boolean;
|
|
966
|
+
/** string|number, default: "all" */
|
|
967
|
+
allValue?: unknown;
|
|
964
968
|
/** array, default: undefined */
|
|
965
969
|
selectedValues?: unknown[];
|
|
966
970
|
/** array, default: [] */
|