@tfdesign/b-end 1.0.18 → 1.0.20

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tfdesign/b-end",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "TFDS B-end React components + Tailwind v4 theme.css; self-contained npm install (no monorepo clone required).",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "system": "b-end",
3
3
  "skill": "tfds",
4
- "generatedAt": "2026-05-14T12:28:55.593Z",
4
+ "generatedAt": "2026-05-14T13:46:15.358Z",
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 返回单值(清空时返回 null)。defaultValue 同样支持单值/数组两种形态。",
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-14T12:28:55.593Z",
4
+ "generatedAt": "2026-05-14T13:46:15.358Z",
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": 25,
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": 14,
1322
- "exampleCount": 15,
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 已清空)
@@ -37,7 +38,7 @@ const BASE = [
37
38
  'inline-flex h-[var(--size-control-md)] shrink-0 items-center justify-center gap-[var(--spacing-2)]',
38
39
  'rounded-[var(--radius-full)] border border-solid',
39
40
  'px-[var(--spacing-3)]',
40
- 'text-[var(--text-sm)] leading-[var(--leading-5)] tracking-[var(--tracking-normal)]',
41
+ '[font-size:var(--text-sm)] leading-[var(--leading-5)] tracking-[var(--tracking-normal)]',
41
42
  '[font-family:inherit]',
42
43
  'select-none whitespace-nowrap outline-none',
43
44
  'transition-all duration-150',
@@ -90,7 +91,7 @@ const PANEL_CLASS = [
90
91
  ].join(' ');
91
92
  const OPTION_CLASS = [
92
93
  'flex min-h-[var(--size-control-md)] cursor-pointer items-center gap-[var(--spacing-2)] px-[var(--spacing-3)] py-[var(--spacing-2)]',
93
- 'text-[var(--text-sm)] leading-[var(--leading-5)] text-foreground',
94
+ '[font-size:var(--text-sm)] leading-[var(--leading-5)] text-foreground',
94
95
  'transition-colors duration-100',
95
96
  'hover:bg-fill',
96
97
  ].join(' ');
@@ -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
- // eslint-disable-next-line react-hooks/exhaustive-deps
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 (value !== null && value !== undefined && value !== '') return value;
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 isSelected = selected || selectedOptions.length > 0;
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 = (closable || selectedOptions.length > 0) && !disabled && hasValue;
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
- : [...currentValues, option.value];
261
- commitValues(nextValues, event);
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 ? [] : [option.value];
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([], event);
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: 152,
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: 132,
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-full min-w-0 items-center justify-start gap-2 pr-3 text-left';
191
- const MORE_ACTION_BUTTON_CLASS = [
192
- // 44x44 hit target per design directive, keep icon at 16px
193
- '!size-11 shrink-0 !rounded-md',
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 resolveColumnTrack(column) {
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
- states[lastColumnIndex] = {
341
- side: 'right',
342
- offset: 0,
343
- edge: 'left',
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
- {items.map((action, index) => {
742
- const key = action.key || `${action.label || action.iconName || 'action'}-${index}`;
743
-
744
- if (action.kind === 'more') {
745
- return (
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 columnTemplate = useMemo(() => buildColumnGridTemplate(columns), [columns]);
1140
- const columnTemplateMinWidth = useMemo(() => getColumnTemplateMinWidth(columns), [columns]);
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(columns, fixedColumnsMode),
1143
- [columns, fixedColumnsMode],
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={columns.length}>
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
- {columns.map((column, columnIndex) => (
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
- {columns.map((column, columnIndex) => (
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: 152,
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: index % 2 === 0 ? '编辑配置' : '同步门店信息', onClick: () => {} },
295
- { key: 'view', label: index % 2 === 0 ? '查看详情' : '查看完整方案', onClick: () => {} },
296
- { key: 'more', kind: 'more', onClick: () => {} },
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: 'more',
366
- kind: 'more',
367
- onClick: () => setInteractionMessage(buildActionMessage('更多操作', rowIndex, columnTitle)),
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
- return Array.from({ length: headerCount }, (_, index) => {
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 返回单值(清空时返回 null)。defaultValue 同样支持单值/数组两种形态。',
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: '多选下拉(默认)', code: '<Filter label="筛选项" options={[{ label: "选项一", value: "1" }, { label: "选项二", value: "2" }]} />' },
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: [] */