@tfdesign/b-end 1.0.14 → 1.0.15

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.
@@ -35,6 +35,13 @@ const DEFAULT_ANIMATED_DESCRIPTION = '基于业务目标、历史数据和运营
35
35
  const DEFAULT_ANIMATED_ICON = 'magic-wand-01-stroked';
36
36
  const DEFAULT_ANIMATED_BADGE = 'AI 推荐';
37
37
  const DEFAULT_ANIMATED_ACTION_TEXT = '立即体验';
38
+ const DEFAULT_INFO3_TITLE = '抖音生服订单已签收后还能申请退款吗?';
39
+ const DEFAULT_INFO3_DESCRIPTION = '已签收订单仍可在 7 天无理由保障期内发起退款,需提供商品照片或服务异常说明,平台介入后通常 24h 内出结果。';
40
+ const DEFAULT_INFO3_STATUS = '生效';
41
+ const DEFAULT_INFO3_META_ITEMS = [
42
+ { iconName: 'tag-01-stroked', value: 'QA' },
43
+ { iconName: 'clock-stroked', value: '2026-04-21 10:30' },
44
+ ];
38
45
  const DEFAULT_INFO_STATS = [
39
46
  { iconName: 'users-01-stroked', value: '128.6K', tooltip: '累计启用该能力的创作者数量' },
40
47
  { iconName: 'star-01-stroked', value: '4.9', tooltip: '用户综合评分,满分 5 分' },
@@ -53,6 +60,12 @@ const CARD_SURFACE = [
53
60
  'hover:shadow-card',
54
61
  ].join(' ');
55
62
 
63
+ const CARD_INTERACTIVE = [
64
+ 'cursor-pointer',
65
+ 'focus:outline-none',
66
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blueGrey-300',
67
+ ].join(' ');
68
+
56
69
  const DATA_CARD = 'min-h-[var(--size-card-min-height)] flex-col justify-between gap-7';
57
70
  const PRODUCT_CARD = 'min-h-[96px] flex-row items-center gap-5 [&_.tfds-tag.bg-white]:hidden';
58
71
  const INFO_CARD = 'min-h-[148px] flex-row items-start gap-6 p-6 [&_.tfds-tag.bg-white]:hidden';
@@ -266,6 +279,34 @@ const ANIMATED_TONE_STYLE = {
266
279
  },
267
280
  };
268
281
 
282
+ /* ── 信息卡片3 ── */
283
+ const INFO3_CARD = [
284
+ 'min-h-[148px] flex-col gap-2 rounded-lg p-4',
285
+ 'text-left',
286
+ ].join(' ');
287
+ const INFO3_HEADER = 'flex items-start justify-between gap-2';
288
+ const INFO3_TITLE = [
289
+ 'm-0 min-w-0 flex-1 text-base [font-weight:var(--font-semibold)] leading-6 text-blueGrey-900',
290
+ 'line-clamp-2',
291
+ ].join(' ');
292
+ const INFO3_DESCRIPTION = 'm-0 line-clamp-2 text-sm font-normal leading-[22px] text-blueGrey-600';
293
+ const INFO3_META = 'flex min-w-0 flex-wrap items-center gap-3 text-xs font-normal leading-4 text-blueGrey-500';
294
+ const INFO3_META_ITEM = 'inline-flex min-w-0 items-center gap-1';
295
+ const INFO3_META_AVATAR = [
296
+ '[&>img]:!scale-100',
297
+ '[&>img]:!translate-y-0',
298
+ '[&>img]:!object-center',
299
+ ].join(' ');
300
+
301
+ const INFO3_STATUS_VARIANT = {
302
+ 生效: 'green',
303
+ 已发布: 'green',
304
+ 草稿: 'grey',
305
+ 审核中: 'orange',
306
+ 已停用: 'red',
307
+ 已下架: 'red',
308
+ };
309
+
269
310
  /* ── 底部 ── */
270
311
  const FOOTER = 'flex w-full items-center justify-between gap-4';
271
312
  const ACTION = [
@@ -279,9 +320,30 @@ const ACTION = [
279
320
  'group-hover:border-black group-hover:text-black',
280
321
  ].join(' ');
281
322
 
323
+ function getInteractiveCardProps(onAction, actionAriaLabel) {
324
+ if (typeof onAction !== 'function') return {};
325
+
326
+ return {
327
+ role: 'button',
328
+ tabIndex: 0,
329
+ 'aria-label': actionAriaLabel,
330
+ onClick: onAction,
331
+ onKeyDown: (event) => {
332
+ if (event.key !== 'Enter' && event.key !== ' ') return;
333
+ event.preventDefault();
334
+ onAction(event);
335
+ },
336
+ };
337
+ }
338
+
339
+ function handleNestedActionClick(event, onAction) {
340
+ event.stopPropagation();
341
+ onAction?.(event);
342
+ }
343
+
282
344
  /**
283
345
  * Card — 业务信息摘要卡片
284
- * @prop {'data'|'product'|'info'|'info2'|'animated'} [type='data'] — 卡片类型:data 为数据卡片,product 为商品卡片,info 为信息卡片1,info2 为信息卡片2;animated 为旧版兼容别名
346
+ * @prop {'data'|'product'|'info'|'info2'|'info3'|'animated'} [type='data'] — 卡片类型:data 为数据卡片,product 为商品卡片,info 为信息卡片1,info2 为信息卡片2,info3 为信息卡片3;animated 为旧版兼容别名
285
347
  * @prop {string} [title] — 卡片标题,商品卡片中为商品标题
286
348
  * @prop {string} [description] — 卡片描述,商品卡片中为数量、价格等辅助信息
287
349
  * @prop {'white'|'grey'} [color='white'] — 卡片颜色:white 为白底样式,适用于灰/浅灰/非纯白容器;grey 为 Blue Grey 灰底样式,适用于纯白容器
@@ -302,20 +364,25 @@ const ACTION = [
302
364
  * @prop {string|null} [animatedIconName='magic-wand-01-stroked'] — 信息卡片2左上角图标
303
365
  * @prop {string|null} [animatedBadge='AI 推荐'] — 信息卡片2左下角徽标
304
366
  * @prop {string|null} [animatedActionText='立即体验'] — 信息卡片2底部右侧操作文案
367
+ * @prop {boolean} [selected=false] — 信息卡片3整体选中态,使用浅绿色背景与 brand 描边
368
+ * @prop {string} [status='生效'] — 信息卡片3右上角状态标签文案
369
+ * @prop {'brand'|'red'|'orange'|'yellow'|'green'|'cyan'|'blue'|'purple'|'pink'|'teal'|'grey'|'white'|'ai'|null} [statusVariant=null] — 信息卡片3右上角状态标签颜色;不传时按常见状态自动映射
370
+ * @prop {Array<{iconName?: string, value: string, avatarSrc?: string, avatarAlt?: string}>|null} [metaItems=null] — 信息卡片3底部元信息,适合类型、更新时间、创建人等密集信息
305
371
  * @prop {string} [infoMetaLabel='段然'] — 信息卡片1底部人名昵称
306
372
  * @prop {string} [infoMetaBadge='官方能力'] — 信息卡片1右上角徽标文案
307
373
  * @prop {'brand'|'red'|'orange'|'yellow'|'green'|'cyan'|'blue'|'purple'|'pink'|'teal'|'grey'|'white'|'ai'|null} [infoMetaBadgeVariant=null] — 信息卡片1右上角徽标颜色;黑底白标时可覆盖,彩色浅底时固定 grey
308
374
  * @prop {string} [infoMetaAvatarSrc] — 信息卡片1底部人名昵称头像
309
375
  * @prop {string} [infoMetaAvatarAlt='用户头像'] — 信息卡片1底部人名昵称头像无障碍文案
310
376
  * @prop {Array<{iconName: string, value: string, tooltip?: string}>|null} [infoStats=null] — 信息卡片1底部右侧辅助项数组,最多展示 3 项;tooltip 用于 hover/focus 展示详细说明
311
- * @prop {function|null} [onAction=null] — 数据卡片右侧箭头操作回调
312
- * @prop {string} [actionAriaLabel='查看卡片详情'] — 右侧操作按钮无障碍文案
377
+ * @prop {function|null} [onAction=null] — 详情跳转 / 进入二级页回调;传入后整张卡片默认可点击
378
+ * @prop {string} [actionAriaLabel='查看卡片详情'] — 整卡与右侧操作按钮共用的无障碍文案
313
379
  * @prop {string} [className=''] — 附加类名
314
380
  * @prop {object} [style] — 内联样式
315
381
  *
316
382
  * 通用 tags 仅用于数据卡片左下角;商品和信息卡片1不渲染标题左侧通用标签。
317
383
  * 所有 Card 分类都遵循“背景反衬”规则:父容器是纯白色时必须使用灰底卡;父容器是灰色、浅灰色或其他非纯白底时必须使用白底卡。
318
384
  * 卡片容器默认半透明白底,hover 后补满白底并出现业务卡片专用投影;灰底卡保持灰底与灰描边,并保留相同投影反馈。
385
+ * 所有 Card 分类只要传入 onAction,就默认整张卡片区域可点击进入;右下角箭头仅作为进入提示,不是唯一热区。
319
386
  */
320
387
  export default function Card({
321
388
  type = 'data',
@@ -345,13 +412,17 @@ export default function Card({
345
412
  animatedIconName = DEFAULT_ANIMATED_ICON,
346
413
  animatedBadge = DEFAULT_ANIMATED_BADGE,
347
414
  animatedActionText = DEFAULT_ANIMATED_ACTION_TEXT,
415
+ selected = false,
416
+ status = DEFAULT_INFO3_STATUS,
417
+ statusVariant = null,
418
+ metaItems = null,
348
419
  children,
349
420
  onAction,
350
421
  actionAriaLabel = '查看卡片详情',
351
422
  className = '',
352
423
  style,
353
424
  }) {
354
- const resolvedType = type === 'product' || type === 'info'
425
+ const resolvedType = type === 'product' || type === 'info' || type === 'info3'
355
426
  ? type
356
427
  : (type === 'info2' || type === 'animated' ? 'info2' : 'data');
357
428
  // Card 颜色依赖父级背景做反衬,不按卡片类型区分。
@@ -359,12 +430,12 @@ export default function Card({
359
430
  const resolvedTitle = title || (
360
431
  resolvedType === 'product'
361
432
  ? DEFAULT_PRODUCT_TITLE
362
- : (resolvedType === 'info' ? DEFAULT_INFO_TITLE : (resolvedType === 'info2' ? DEFAULT_ANIMATED_TITLE : DEFAULT_DATA_TITLE))
433
+ : (resolvedType === 'info' ? DEFAULT_INFO_TITLE : (resolvedType === 'info2' ? DEFAULT_ANIMATED_TITLE : (resolvedType === 'info3' ? DEFAULT_INFO3_TITLE : DEFAULT_DATA_TITLE)))
363
434
  );
364
435
  const resolvedDescription = description || (
365
436
  resolvedType === 'product'
366
437
  ? DEFAULT_PRODUCT_DESCRIPTION
367
- : (resolvedType === 'info' ? DEFAULT_INFO_DESCRIPTION : (resolvedType === 'info2' ? DEFAULT_ANIMATED_DESCRIPTION : DEFAULT_DATA_DESCRIPTION))
438
+ : (resolvedType === 'info' ? DEFAULT_INFO_DESCRIPTION : (resolvedType === 'info2' ? DEFAULT_ANIMATED_DESCRIPTION : (resolvedType === 'info3' ? DEFAULT_INFO3_DESCRIPTION : DEFAULT_DATA_DESCRIPTION)))
368
439
  );
369
440
  const resolvedStats = (Array.isArray(stats) && stats.length > 0 ? stats : DEFAULT_STATS).slice(0, 3);
370
441
  const resolvedTags = resolvedType === 'data'
@@ -394,24 +465,34 @@ export default function Card({
394
465
  const resolvedAnimatedTone = Object.prototype.hasOwnProperty.call(ANIMATED_TONE_STYLE, animatedTone)
395
466
  ? animatedTone
396
467
  : 'grey';
468
+ const resolvedInfo3MetaItems = Array.isArray(metaItems) && metaItems.length > 0
469
+ ? metaItems
470
+ : DEFAULT_INFO3_META_ITEMS;
471
+ const resolvedStatusVariant = INFO_BADGE_VARIANTS.has(statusVariant)
472
+ ? statusVariant
473
+ : (INFO3_STATUS_VARIANT[status] || 'grey');
474
+ const isClickable = typeof onAction === 'function';
397
475
  const cardStyle = {
398
476
  ...(CARD_VARIANT_STYLE[resolvedColor] || {}),
399
477
  ...(resolvedType === 'info' && resolvedInfoIconStyle === 'tone' ? INFO_ICON_TONE_HOVER_STYLE[resolvedInfoIconTone] : {}),
400
478
  ...(resolvedType === 'info2' ? ANIMATED_TONE_STYLE[resolvedAnimatedTone] : {}),
401
479
  ...style,
402
480
  };
481
+ const interactiveProps = getInteractiveCardProps(onAction, actionAriaLabel);
403
482
 
404
483
  if (resolvedType === 'product') {
405
484
  return (
406
485
  <article
407
486
  className={[
408
487
  CARD_SURFACE,
488
+ isClickable ? CARD_INTERACTIVE : null,
409
489
  resolvedInfoLayout === 'icon-right' ? PRODUCT_CARD_ICON_RIGHT : PRODUCT_CARD,
410
490
  CARD_VARIANT_CLASS[resolvedColor],
411
491
  className,
412
492
  ].filter(Boolean).join(' ')}
413
493
  style={cardStyle}
414
494
  data-tfds-component="Card"
495
+ {...interactiveProps}
415
496
  >
416
497
  <div className={PRODUCT_IMAGE_FRAME}>
417
498
  <img
@@ -465,9 +546,10 @@ export default function Card({
465
546
 
466
547
  return (
467
548
  <article
468
- className={[CARD_SURFACE, INFO_CARD, CARD_VARIANT_CLASS[resolvedColor], className].filter(Boolean).join(' ')}
549
+ className={[CARD_SURFACE, isClickable ? CARD_INTERACTIVE : null, INFO_CARD, CARD_VARIANT_CLASS[resolvedColor], className].filter(Boolean).join(' ')}
469
550
  style={cardStyle}
470
551
  data-tfds-component="Card"
552
+ {...interactiveProps}
471
553
  >
472
554
  {resolvedInfoLayout === 'icon-right' ? null : iconNode}
473
555
 
@@ -519,9 +601,10 @@ export default function Card({
519
601
  if (resolvedType === 'info2') {
520
602
  return (
521
603
  <article
522
- className={[CARD_SURFACE, ANIMATED_CARD, CARD_VARIANT_CLASS[resolvedColor], className].filter(Boolean).join(' ')}
604
+ className={[CARD_SURFACE, isClickable ? CARD_INTERACTIVE : null, ANIMATED_CARD, CARD_VARIANT_CLASS[resolvedColor], className].filter(Boolean).join(' ')}
523
605
  style={cardStyle}
524
606
  data-tfds-component="Card"
607
+ {...interactiveProps}
525
608
  >
526
609
  <div className={ANIMATED_CONTENT}>
527
610
  <div className={ANIMATED_HEADER}>
@@ -558,7 +641,7 @@ export default function Card({
558
641
  type="button"
559
642
  className={ACTION}
560
643
  aria-label={animatedActionText || actionAriaLabel}
561
- onClick={onAction}
644
+ onClick={(event) => handleNestedActionClick(event, onAction)}
562
645
  disabled={!onAction}
563
646
  >
564
647
  <Icon name="arrow-narrow-right-stroked" size={16} />
@@ -570,11 +653,64 @@ export default function Card({
570
653
  );
571
654
  }
572
655
 
656
+ if (resolvedType === 'info3') {
657
+ return (
658
+ <article
659
+ className={[
660
+ CARD_SURFACE,
661
+ isClickable ? CARD_INTERACTIVE : null,
662
+ INFO3_CARD,
663
+ CARD_VARIANT_CLASS[resolvedColor],
664
+ className,
665
+ ].filter(Boolean).join(' ')}
666
+ style={{
667
+ ...cardStyle,
668
+ ...(selected ? { borderColor: 'var(--color-brand-500, #00B384)' } : {}),
669
+ }}
670
+ data-tfds-component="Card"
671
+ data-tfds-card-type="info3"
672
+ aria-selected={selected || undefined}
673
+ {...interactiveProps}
674
+ >
675
+ <div className={INFO3_HEADER}>
676
+ <h3 className={INFO3_TITLE}>{resolvedTitle}</h3>
677
+ {status ? (
678
+ <Tag variant={resolvedStatusVariant} className="shrink-0">
679
+ {status}
680
+ </Tag>
681
+ ) : null}
682
+ </div>
683
+
684
+ <p className={INFO3_DESCRIPTION}>{resolvedDescription}</p>
685
+
686
+ <div className={INFO3_META}>
687
+ {resolvedInfo3MetaItems.map((item, index) => (
688
+ <span key={`${item.value}-${index}`} className={INFO3_META_ITEM}>
689
+ {item.avatarSrc ? (
690
+ <Avatar
691
+ size="xxs"
692
+ type="image"
693
+ src={item.avatarSrc}
694
+ alt={item.avatarAlt || `${item.value}头像`}
695
+ className={INFO3_META_AVATAR}
696
+ />
697
+ ) : item.iconName ? (
698
+ <Icon name={item.iconName} size="xs" />
699
+ ) : null}
700
+ <span className="truncate">{item.value}</span>
701
+ </span>
702
+ ))}
703
+ </div>
704
+ </article>
705
+ );
706
+ }
707
+
573
708
  return (
574
709
  <article
575
- className={[CARD_SURFACE, DATA_CARD, CARD_VARIANT_CLASS[resolvedColor], className].filter(Boolean).join(' ')}
710
+ className={[CARD_SURFACE, isClickable ? CARD_INTERACTIVE : null, DATA_CARD, CARD_VARIANT_CLASS[resolvedColor], className].filter(Boolean).join(' ')}
576
711
  style={cardStyle}
577
712
  data-tfds-component="Card"
713
+ {...interactiveProps}
578
714
  >
579
715
  {hasDataIcon ? (
580
716
  <div className={DATA_ICON_ROW}>
@@ -624,7 +760,7 @@ export default function Card({
624
760
  type="button"
625
761
  className={ACTION}
626
762
  aria-label={actionAriaLabel}
627
- onClick={onAction}
763
+ onClick={(event) => handleNestedActionClick(event, onAction)}
628
764
  disabled={!onAction}
629
765
  >
630
766
  <Icon name="arrow-narrow-right-stroked" size={16} />
@@ -98,7 +98,7 @@ export const CARD_TOKEN_MAP = {
98
98
  ],
99
99
  信息卡片2: [
100
100
  { label: '适用场景', cssProp: 'usage', value: 'AI 能力推荐 / 重点工具入口 / 配置向导 / 首页推荐能力;单页建议最多 1-2 张' },
101
- { label: '结构', cssProp: 'layout', value: '左上图标 + 标题/描述 + 左下角徽标 + 右下角圆形箭头操作入口' },
101
+ { label: '结构', cssProp: 'layout', value: '左上图标 + 标题/描述 + 左下角徽标 + 右下角圆形箭头提示入口' },
102
102
  { label: '背景与描边', cssProp: 'background/border', value: '完全复用 Card color=white / grey 的背景、描边和 hover 投影规则' },
103
103
  { label: '交互', cssProp: 'motion', value: '无柔光、无流动动画、无特殊 hover 位移;仅保留普通 Card hover 投影' },
104
104
  { label: '图标色系配置', cssProp: 'prop', value: 'animatedTone: brand / blue / purple / green / orange / grey,默认 grey;仅影响左上图标容器;grey 背景使用 fill-default,描边使用 border-default' },
@@ -112,12 +112,31 @@ export const CARD_TOKEN_MAP = {
112
112
  { label: '描述行数', cssProp: 'line-clamp', value: '最多 2 行' },
113
113
  { label: '徽标位置', cssProp: 'placement', value: '左下角,与标题左边界对齐,并与右下角圆形箭头操作入口同一行垂直居中' },
114
114
  { label: '徽标样式', cssProp: 'component-style', value: 'Tag variant="white" / fontWeight="regular" / size=l / radius=md' },
115
- { label: '操作按钮', cssProp: 'component-style', value: '复用数据卡片 ACTION:28px 圆形箭头按钮 / hover 黑色描边与黑色箭头 / active 缩放;无 onAction 时禁用' },
116
- { label: '操作文案', cssProp: 'a11y-label', value: 'animatedActionText 作为圆形箭头按钮语义文案,不直接渲染为文字按钮' },
115
+ { label: '整卡点击', cssProp: 'interaction', value: '传入 onAction 时整张卡片默认可点击进入,支持鼠标点击与 Enter / Space 键盘触发' },
116
+ { label: '操作按钮', cssProp: 'component-style', value: '复用数据卡片 ACTION:28px 圆形箭头按钮 / hover 黑色描边与黑色箭头 / active 缩放;当 onAction 存在时仅作为进入提示热区,点击后与整卡复用同一回调' },
117
+ { label: '操作文案', cssProp: 'a11y-label', value: 'animatedActionText / actionAriaLabel 同时作为整卡与圆形箭头按钮语义文案,不直接渲染为文字按钮' },
118
+ ],
119
+ 信息卡片3: [
120
+ { label: '适用场景', cssProp: 'usage', value: '密集信息列表 / 知识库知识列表 / 规则列表 / 策略列表 / 工单摘要列表;适合在“列表 + 详情面板”联动页面中作为对象条目' },
121
+ { label: '结构', cssProp: 'layout', value: '标题 + 右上角状态标签 + 两行描述 + 底部元信息行' },
122
+ { label: '标题', cssProp: 'component-style', value: '主标题最多 2 行,字号 16px,字重 600,行高 24px,颜色 blueGrey-900' },
123
+ { label: '描述', cssProp: 'component-style', value: '描述最多 2 行,字号 14px,行高 22px,颜色 blueGrey-600' },
124
+ { label: '状态标签', cssProp: 'component', value: 'Tag / 右上角 / 状态映射 variant:生效/已发布=green,草稿=grey,审核中=orange,已停用/已下架=red;也可通过 statusVariant 覆盖' },
125
+ { label: '底部元信息', cssProp: 'component', value: 'metaItems: Array<{ iconName?, value, avatarSrc?, avatarAlt? }>;默认用于类型、更新时间、创建人头像与姓名' },
126
+ { label: '元信息字号', cssProp: 'font-size', token: '--text-xs', value: '12px' },
127
+ { label: '元信息间距', cssProp: 'gap', value: '12px;每个元信息内部 icon/avatar 与文字间距 4px' },
128
+ { label: '默认背景', cssProp: 'background', value: '完全复用 Card color=white / grey 的背景规则,不单独定义信息卡片3专属底色' },
129
+ { label: '默认描边', cssProp: 'border-color', value: '完全复用 Card color=white / grey 的描边规则,不单独定义信息卡片3专属描边' },
130
+ { label: 'Hover 背景', cssProp: 'background', value: '完全复用 Card color=white / grey 的 hover 背景规则' },
131
+ { label: '圆角', cssProp: 'border-radius', token: '--radius-lg', value: '12px,使用 rounded-lg' },
132
+ { label: '选中提示', cssProp: 'border-color', token: '--color-brand-500', value: '通过 selected 将原本同一条描边从灰色切换为绿色,不新增 ring / outline / 外扩描边' },
133
+ { label: '选中交互', cssProp: 'interaction', value: '通过 selected 控制整卡描边颜色;Card 只负责样式,选中/取消选中的业务状态由页面维护' },
134
+ { label: '整卡点击', cssProp: 'interaction', value: '传入 onAction 后整张卡片默认可点击,支持鼠标点击与 Enter / Space 键盘触发;适用于打开或收起右侧详情面板' },
117
135
  ],
118
136
  卡片: [
119
137
  { label: '容器映射规则', cssProp: 'usage', value: '所有卡片分类统一按父容器背景反衬:纯白容器用 grey 灰底卡;灰色 / 浅灰 / 其他非纯白容器用 white 白底卡' },
120
- { label: '规则适用范围', cssProp: 'scope', value: '数据卡片 / 商品卡片 / 信息卡片1 / 信息卡片2 全部生效,不因卡片类型变化' },
138
+ { label: '规则适用范围', cssProp: 'scope', value: '数据卡片 / 商品卡片 / 信息卡片1 / 信息卡片2 / 信息卡片3 全部生效,不因卡片类型变化' },
139
+ { label: '查看交互规则', cssProp: 'interaction', value: '所有 Card 分类只要传入 onAction(详情跳转能力),默认整张卡片区域都支持点击查看;箭头按钮仅作为进入提示,不是唯一热区' },
121
140
  { label: '白底背景', cssProp: 'background', token: '--color-card-secondary', value: 'rgba(255,255,255,0.65)', semanticRef: 'bg-card-secondary', state: 'default' },
122
141
  { label: '白底 Hover 背景', cssProp: 'background', token: '--color-surface', value: '#FFFFFF', semanticRef: 'bg-surface', state: 'hover' },
123
142
  { label: '白底描边', cssProp: 'border-color', token: '--color-white', value: '#FFFFFF' },
@@ -127,6 +146,7 @@ export const CARD_TOKEN_MAP = {
127
146
  { label: '投影', cssProp: 'box-shadow', token: '--shadow-card', value: '0 30px 50px 0 rgba(0, 9, 36, 0.05)', state: 'hover' },
128
147
  ],
129
148
  操作: [
149
+ { label: '触发热区', cssProp: 'interaction', value: '当 onAction 存在时,整卡为主热区;数据卡片 / 信息卡片2右下角圆形箭头为辅助提示热区' },
130
150
  { label: '图标色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary', state: 'default' },
131
151
  { label: '图标色', cssProp: 'color', token: '--color-black', value: '#000000', state: 'hover' },
132
152
  { label: '描边色', cssProp: 'border-color', token: '--color-transparent', value: 'transparent', state: 'default' },
@@ -147,6 +167,8 @@ export const CARD_TOKEN_MAP = {
147
167
  { label: '信息卡片1标题', cssProp: 'component', value: 'FormTitle / variant=form / showDescription' },
148
168
  { label: '信息卡片1徽标', cssProp: 'component', value: 'Tag / infoMetaBadgeVariant / size=l / radius=md' },
149
169
  { label: '信息卡片1昵称头像', cssProp: 'component', value: 'Avatar / type=image / shape=round / size=xxs' },
170
+ { label: '信息卡片3状态标签', cssProp: 'component', value: 'Tag / 右上角状态 / variant 由 statusVariant 或状态映射决定' },
171
+ { label: '信息卡片3元信息头像', cssProp: 'component', value: 'Avatar / type=image / shape=round / size=xxs' },
150
172
  { label: '数据卡片图标', cssProp: 'component', value: 'Icon / users-01-stroked / message-chat-square-stroked / hearts-stroked / arrow-narrow-right-stroked' },
151
173
  { label: '信息卡片1图标', cssProp: 'component', value: 'Icon / magic-wand-01-stroked / users-01-stroked / star-01-stroked / check-circle-stroked' },
152
174
  ],
@@ -16,7 +16,8 @@ export default function CardPreview({
16
16
  const isProduct = type === 'product';
17
17
  const isInfo = type === 'info';
18
18
  const isInfo2 = type === 'info2' || type === 'animated';
19
- const isData = !isProduct && !isInfo && !isInfo2;
19
+ const isInfo3 = type === 'info3';
20
+ const isData = !isProduct && !isInfo && !isInfo2 && !isInfo3;
20
21
  const resolvedInfoIconStyle = isInfo ? (infoIconStyle || 'inverse') : infoIconStyle;
21
22
  const resolvedInfoLayout = isInfo
22
23
  ? (infoLayout || 'icon-right')
@@ -30,8 +31,8 @@ export default function CardPreview({
30
31
  <Card
31
32
  type={type}
32
33
  color={color}
33
- title={isProduct ? '海底捞门店通用双人套餐' : (isInfo2 ? '智能策略生成' : undefined)}
34
- description={isProduct ? '数量 1 · ¥128.00 · 月售 2,361' : (isInfo2 ? '基于业务目标、历史数据和运营规则,自动生成可执行策略建议。' : undefined)}
34
+ title={isProduct ? '海底捞门店通用双人套餐' : (isInfo2 ? '智能策略生成' : (isInfo3 ? '抖音生服订单已签收后还能申请退款吗?' : undefined))}
35
+ description={isProduct ? '数量 1 · ¥128.00 · 月售 2,361' : (isInfo2 ? '基于业务目标、历史数据和运营规则,自动生成可执行策略建议。' : (isInfo3 ? '已签收订单仍可在 7 天无理由保障期内发起退款,需提供商品照片或服务异常说明,平台介入后通常 24h 内出结果。' : undefined))}
35
36
  tags={isProduct || isInfo ? [] : undefined}
36
37
  productStatus={isProduct ? '已使用' : undefined}
37
38
  infoIconTone={isInfo ? infoIconTone : undefined}
@@ -48,6 +49,13 @@ export default function CardPreview({
48
49
  animatedIconName={isInfo2 ? 'magic-wand-01-stroked' : undefined}
49
50
  animatedBadge={isInfo2 ? 'AI 推荐' : undefined}
50
51
  animatedActionText={isInfo2 ? '立即体验' : undefined}
52
+ selected={false}
53
+ status={isInfo3 ? '生效' : undefined}
54
+ metaItems={isInfo3 ? [
55
+ { iconName: 'tag-01-stroked', value: 'QA' },
56
+ { iconName: 'clock-stroked', value: '2026-04-21 10:30' },
57
+ { value: '李思儒' },
58
+ ] : undefined}
51
59
  actionAriaLabel={isProduct ? undefined : '查看卡片详情'}
52
60
  onAction={isProduct ? undefined : () => {}}
53
61
  />
@@ -797,7 +797,9 @@ function normalizeHumanConfirmNode(node, index) {
797
797
  options,
798
798
  defaultSelectedValue,
799
799
  formItems,
800
+ formValues: node?.formValues && typeof node.formValues === 'object' ? node.formValues : null,
800
801
  onOptionChange: typeof node?.onOptionChange === 'function' ? node.onOptionChange : null,
802
+ onFormChange: typeof node?.onFormChange === 'function' ? node.onFormChange : null,
801
803
  onPrimaryAction: typeof node?.onPrimaryAction === 'function' ? node.onPrimaryAction : null,
802
804
  onSecondaryAction: typeof node?.onSecondaryAction === 'function' ? node.onSecondaryAction : null,
803
805
  onToggleCollapsed: typeof node?.onToggleCollapsed === 'function' ? node.onToggleCollapsed : null,
@@ -1643,6 +1645,26 @@ function UserMessageContent({ tokens }) {
1643
1645
  );
1644
1646
  }
1645
1647
 
1648
+ function getInitialFormValues(formItems = []) {
1649
+ return formItems.reduce((values, item) => {
1650
+ const key = item?.id || item?.name;
1651
+ if (!key) return values;
1652
+ if (item.value !== undefined) values[key] = item.value;
1653
+ else if (item.defaultValue !== undefined) values[key] = item.defaultValue;
1654
+ else if (item.defaultChecked !== undefined) values[key] = item.defaultChecked;
1655
+ else values[key] = '';
1656
+ return values;
1657
+ }, {});
1658
+ }
1659
+
1660
+ function getFieldChangeValue(raw) {
1661
+ if (raw && typeof raw === 'object' && raw.target) {
1662
+ const { type, checked, value } = raw.target;
1663
+ return type === 'checkbox' ? checked : value;
1664
+ }
1665
+ return raw;
1666
+ }
1667
+
1646
1668
  /* ── 人工确认节点 ── */
1647
1669
  function HumanConfirmNode({ node, tokenStyles }) {
1648
1670
  /* 内部自管折叠态(兼容外部受控:node.collapsed + node.onToggleCollapsed 都传时走外部) */
@@ -1656,18 +1678,23 @@ function HumanConfirmNode({ node, tokenStyles }) {
1656
1678
  /* 点击主按钮后进入"已确认"禁用态:内容半透明、按钮禁用 */
1657
1679
  const [confirmed, setConfirmed] = useState(node.defaultConfirmed === true);
1658
1680
  const [selectedOptionValue, setSelectedOptionValue] = useState(node.defaultSelectedValue);
1681
+ const [formValues, setFormValues] = useState(() => node.formValues || getInitialFormValues(node.formItems));
1659
1682
  useEffect(() => {
1660
1683
  if (node.defaultConfirmed === true) setConfirmed(true);
1661
1684
  }, [node.defaultConfirmed]);
1662
1685
  useEffect(() => {
1663
1686
  setSelectedOptionValue(node.defaultSelectedValue);
1664
1687
  }, [node.defaultSelectedValue]);
1688
+ useEffect(() => {
1689
+ setFormValues(node.formValues || getInitialFormValues(node.formItems));
1690
+ }, [node.formItems, node.formValues]);
1665
1691
  const handlePrimary = () => {
1666
1692
  const selectedOption = node.options.find((option) => String(option.value) === String(selectedOptionValue)) ?? null;
1667
1693
  if (typeof node.onPrimaryAction === 'function') {
1668
1694
  node.onPrimaryAction({
1669
1695
  value: selectedOptionValue,
1670
1696
  option: selectedOption,
1697
+ formValues,
1671
1698
  });
1672
1699
  }
1673
1700
  setConfirmed(true);
@@ -1682,11 +1709,41 @@ function HumanConfirmNode({ node, tokenStyles }) {
1682
1709
  node.onOptionChange(nextValue, selectedOption);
1683
1710
  }
1684
1711
  };
1712
+ const handleFormValueChange = (item, rawValue) => {
1713
+ const key = item?.id || item?.name;
1714
+ if (!key) return;
1715
+ const nextValue = getFieldChangeValue(rawValue);
1716
+ setFormValues((prev) => {
1717
+ const nextValues = { ...prev, [key]: nextValue };
1718
+ if (typeof node.onFormChange === 'function') {
1719
+ node.onFormChange(nextValues, {
1720
+ fieldId: key,
1721
+ value: nextValue,
1722
+ item,
1723
+ });
1724
+ }
1725
+ return nextValues;
1726
+ });
1727
+ };
1685
1728
 
1686
1729
  const showIntroText = node.mode === 'text-card' && node.introText && !isCollapsed;
1687
1730
  const showCardBody = !isCollapsed;
1688
1731
  const isOptionCard = node.mode === 'option-card';
1689
1732
  const isFormCard = node.mode === 'form-card';
1733
+ const resolvedFormItems = isFormCard
1734
+ ? node.formItems.map((item) => {
1735
+ const key = item?.id || item?.name;
1736
+ if (!key) return item;
1737
+ return {
1738
+ ...item,
1739
+ value: formValues[key],
1740
+ onChange: (nextValue, ...args) => {
1741
+ handleFormValueChange(item, nextValue);
1742
+ if (typeof item.onChange === 'function') item.onChange(nextValue, ...args);
1743
+ },
1744
+ };
1745
+ })
1746
+ : node.formItems;
1690
1747
 
1691
1748
  const headerClassName = isOptionCard ? HUMAN_CONFIRM_OPTION_HEADER : HUMAN_CONFIRM_HEADER;
1692
1749
  const iconWrapClassName = isOptionCard ? HUMAN_CONFIRM_OPTION_ICON_WRAP : HUMAN_CONFIRM_ICON_WRAP;
@@ -1770,11 +1827,12 @@ function HumanConfirmNode({ node, tokenStyles }) {
1770
1827
  </RadioGroup>
1771
1828
  ) : isFormCard ? (
1772
1829
  <Form
1773
- items={node.formItems}
1830
+ items={resolvedFormItems}
1774
1831
  layout="vertical"
1775
1832
  size="md"
1776
1833
  disabled={confirmed}
1777
1834
  className="w-full min-w-0 gap-3 pb-2"
1835
+ onSubmit={(event) => event.preventDefault()}
1778
1836
  data-tfds-component="ChatMessage.FormCardForm"
1779
1837
  />
1780
1838
  ) : node.description ? (
@@ -46,7 +46,7 @@ const DEFAULT_SECTIONS = [
46
46
  time: '13:32',
47
47
  avatarSrc: liSiruAvatar,
48
48
  tags: [
49
- { label: '异常监控提醒', variant: 'grey' },
49
+ { label: '异常提醒', variant: 'grey' },
50
50
  { label: '待干预', variant: 'red' },
51
51
  ],
52
52
  },
@@ -70,7 +70,7 @@ const DEFAULT_SECTIONS = [
70
70
  time: '12:40',
71
71
  avatarSrc: guoZhezhiAvatar,
72
72
  tags: [
73
- { label: '退款执行错误', variant: 'grey' },
73
+ { label: '退款异常', variant: 'grey' },
74
74
  { label: '待干预', variant: 'red' },
75
75
  ],
76
76
  },
@@ -89,7 +89,7 @@ const DEFAULT_SECTIONS = [
89
89
  time: '11:22',
90
90
  avatarSrc: chengchengAvatar,
91
91
  tags: [
92
- { label: '24时30分', variant: 'grey', iconName: 'alarm-clock-stroked' },
92
+ { label: '24时+', variant: 'grey', iconName: 'alarm-clock-stroked' },
93
93
  { label: '托管中', variant: 'green' },
94
94
  ],
95
95
  },
@@ -112,7 +112,7 @@ const DEFAULT_SECTIONS = [
112
112
  time: '09:46',
113
113
  avatarSrc: liuDelinAvatar,
114
114
  tags: [
115
- { label: '协商不成功', variant: 'grey' },
115
+ { label: '协商未成', variant: 'grey' },
116
116
  { label: '托管中', variant: 'green' },
117
117
  ],
118
118
  },
@@ -248,7 +248,7 @@ const TIME = 'ml-auto shrink-0 pl-2 text-foreground-muted';
248
248
  const AVATAR_ONLY_HEADER = 'flex h-8 w-full items-center justify-center';
249
249
  const AVATAR_ONLY_LIST = 'flex min-h-0 flex-1 flex-col self-stretch gap-1 overflow-y-auto';
250
250
  const AVATAR_ONLY_ITEM = [
251
- 'relative inline-flex h-[68px] w-full items-center rounded-xl px-4',
251
+ 'relative inline-flex h-[68px] min-h-[68px] w-full shrink-0 items-center rounded-xl px-4',
252
252
  'transition-colors duration-150 hover:bg-fill active:bg-fill-active',
253
253
  'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-200',
254
254
  ].join(' ');
@@ -276,10 +276,11 @@ const CARD_MESSAGE_REGION_GENERATING = 'h-[196px] gap-3';
276
276
  const CARD_MESSAGE_REGION_EDITABLE = 'h-[155px] gap-3';
277
277
  const CARD_MESSAGE_ROW_BASE = 'flex w-full';
278
278
  const CARD_MESSAGE_ROW_USER = 'justify-start';
279
- const CARD_MESSAGE_ROW_AGENT = 'justify-end';
279
+ const CARD_MESSAGE_ROW_OUTGOING = 'justify-end';
280
280
  const CARD_MESSAGE_BUBBLE_BASE = 'max-w-[calc(100%-24px)] rounded-lg px-2 py-1.5 text-xs leading-4 text-foreground';
281
281
  const CARD_MESSAGE_BUBBLE_USER = 'rounded-tl-none bg-fill';
282
- const CARD_MESSAGE_BUBBLE_AGENT = 'rounded-tr-none bg-[image:var(--gradient-ai-fill-1)]';
282
+ const CARD_MESSAGE_BUBBLE_BOT = 'rounded-tr-none bg-[image:var(--gradient-ai-fill-1)]';
283
+ const CARD_MESSAGE_BUBBLE_AGENT = 'rounded-tr-none bg-chat-outgoing';
283
284
  const CARD_DIVIDER = 'h-px w-full bg-border-default';
284
285
  const CARD_FOOTER_BASE = 'shrink-0';
285
286
  const CARD_FOOTER_REPLIED = 'px-4 py-4 text-xs leading-4 text-[#22272759]';
@@ -407,13 +408,17 @@ function sortCardTags(tags) {
407
408
  }
408
409
 
409
410
  function ConversationCardMessage({ message }) {
410
- const isAgent = message.role === 'agent';
411
+ const isUser = message.role === 'user';
412
+ const isBot = message.role === 'bot';
413
+ const bubbleClassName = isUser
414
+ ? CARD_MESSAGE_BUBBLE_USER
415
+ : (isBot ? CARD_MESSAGE_BUBBLE_BOT : CARD_MESSAGE_BUBBLE_AGENT);
411
416
 
412
417
  return (
413
- <div className={[CARD_MESSAGE_ROW_BASE, isAgent ? CARD_MESSAGE_ROW_AGENT : CARD_MESSAGE_ROW_USER].join(' ')}>
418
+ <div className={[CARD_MESSAGE_ROW_BASE, isUser ? CARD_MESSAGE_ROW_USER : CARD_MESSAGE_ROW_OUTGOING].join(' ')}>
414
419
  <div className={[
415
420
  CARD_MESSAGE_BUBBLE_BASE,
416
- isAgent ? CARD_MESSAGE_BUBBLE_AGENT : CARD_MESSAGE_BUBBLE_USER,
421
+ bubbleClassName,
417
422
  ].join(' ')}>
418
423
  {message.text}
419
424
  </div>
@@ -63,7 +63,7 @@ export const CONVERSATION_LIST_TOKEN_MAP = {
63
63
  ],
64
64
  收起态: [
65
65
  { label: '列宽', cssProp: 'width', value: '88px' },
66
- { label: '行高', cssProp: 'height', value: '68px' },
66
+ { label: '行高', cssProp: 'height/min-height', value: '68px(固定高度,不参与 flex 压缩;运行时需同时具备 h-[68px] min-h-[68px] shrink-0)' },
67
67
  { label: '选中投影', cssProp: 'box-shadow', token: '--shadow-list', value: '0 0 16px 0 rgba(101,115,137,0.06)', state: 'active' },
68
68
  { label: '选中左条', cssProp: 'background', token: '--color-data-0', value: '#24CDA5', state: 'active' },
69
69
  ],
@@ -93,14 +93,16 @@ export const CONVERSATION_LIST_TOKEN_MAP = {
93
93
  { label: '消息区高度', cssProp: 'height', value: '155px(输入文案)', state: 'card-editable' },
94
94
  { label: '消息区横向内边距', cssProp: 'padding-inline', token: '--spacing-4', value: '16px', state: 'card' },
95
95
  { label: '消息区底部内边距', cssProp: 'padding-bottom', token: '--spacing-2', value: '8px', state: 'card' },
96
- { label: '消息区滚动', cssProp: 'overflow-y', value: 'auto', state: 'card' },
96
+ { label: '消息区滚动', cssProp: 'overflow-y', value: 'auto(固定高度内滚动查看与右侧 IM 同源的完整气泡消息流,不允许按条数截断)', state: 'card' },
97
97
  { label: '消息气泡纵向间距', cssProp: 'row-gap', token: '--spacing-3', value: '12px', state: 'card' },
98
98
  ],
99
99
  卡片消息气泡: [
100
100
  { label: '用户气泡背景', cssProp: 'background', token: '--color-fill', value: 'rgba(83, 96, 143, 0.07)', state: 'card-user' },
101
- { label: 'AI 气泡背景', cssProp: 'background-image', token: '--gradient-ai-fill-1', value: 'linear-gradient(90deg, rgba(230, 247, 244, 1) 0%, rgba(239, 246, 255, 1) 55%, rgba(243, 245, 255, 1) 90%, rgba(252, 243, 255, 1) 100%)', state: 'card-agent' },
101
+ { label: 'AI 气泡背景', cssProp: 'background-image', token: '--gradient-ai-fill-1', value: 'linear-gradient(90deg, rgba(230, 247, 244, 1) 0%, rgba(239, 246, 255, 1) 55%, rgba(243, 245, 255, 1) 90%, rgba(252, 243, 255, 1) 100%)', state: 'card-bot' },
102
+ { label: '人工客服气泡背景', cssProp: 'background', token: '--color-chat-outgoing', value: 'var(--bg-chat-outgoing)', state: 'card-agent' },
102
103
  { label: '气泡圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px(对角缺口按消息方向处理)', state: 'card' },
103
104
  { label: '气泡字号', cssProp: 'font-size', token: '--text-xs', value: '12px', state: 'card' },
105
+ { label: '与右侧聊天一致性', cssProp: 'content/style-source', value: '卡片气泡是右侧 IM 对话气泡的缩小版完整消息流;文案、角色、颜色语义必须同源一致。user -> grey,bot -> ai 渐变,agent -> default 浅青;允许降低字号或隐藏辅助元素,但不允许截断消息条数', state: 'card-preview' },
104
106
  ],
105
107
  卡片底部: [
106
108
  { label: '已回复内边距', cssProp: 'padding', token: '--spacing-4', value: '16px', state: 'replied' },