@tfdesign/b-end 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/AI_READ_FIRST.md +131 -0
  2. package/LICENSE +21 -0
  3. package/README.md +353 -0
  4. package/package.json +67 -0
  5. package/scripts/check-tfds-contract.mjs +334 -0
  6. package/scripts/check-tfds-integration.mjs +263 -0
  7. package/scripts/postinstall-cursor-skill.mjs +382 -0
  8. package/scripts/setup.mjs +520 -0
  9. package/skills/tfds/CHECKLIST.md +205 -0
  10. package/skills/tfds/COMMON_FAILURES.md +238 -0
  11. package/skills/tfds/DESIGN_PRINCIPLES.md +477 -0
  12. package/skills/tfds/GLOBAL_DESIGN_RULES.md +636 -0
  13. package/skills/tfds/LAYOUT_RECIPES.md +140 -0
  14. package/skills/tfds/LAYOUT_RULES.md +1355 -0
  15. package/skills/tfds/PAGE_ARCHETYPES.md +201 -0
  16. package/skills/tfds/SKILL.md +188 -0
  17. package/skills/tfds/components.index.json +7305 -0
  18. package/skills/tfds/components.summary.json +1809 -0
  19. package/src/_b_end_runtime/components/AiSuggestionShared.jsx +166 -0
  20. package/src/_b_end_runtime/components/Avatar.jsx +325 -0
  21. package/src/_b_end_runtime/components/Avatar.tokens.js +76 -0
  22. package/src/_b_end_runtime/components/AvatarGridPreview.jsx +56 -0
  23. package/src/_b_end_runtime/components/AvatarGroup.jsx +80 -0
  24. package/src/_b_end_runtime/components/AvatarGroup.tokens.js +28 -0
  25. package/src/_b_end_runtime/components/Button.jsx +144 -0
  26. package/src/_b_end_runtime/components/Button.tokens.js +90 -0
  27. package/src/_b_end_runtime/components/Card.jsx +460 -0
  28. package/src/_b_end_runtime/components/Card.tokens.js +124 -0
  29. package/src/_b_end_runtime/components/CardPreview.jsx +51 -0
  30. package/src/_b_end_runtime/components/ChatBubble.jsx +384 -0
  31. package/src/_b_end_runtime/components/ChatBubble.tokens.js +60 -0
  32. package/src/_b_end_runtime/components/ChatBubblePreview.jsx +129 -0
  33. package/src/_b_end_runtime/components/ChatInput.jsx +1399 -0
  34. package/src/_b_end_runtime/components/ChatInput.tokens.js +75 -0
  35. package/src/_b_end_runtime/components/ChatMessage.jsx +2215 -0
  36. package/src/_b_end_runtime/components/ChatMessage.tokens.js +257 -0
  37. package/src/_b_end_runtime/components/ChatMessagePreview.jsx +388 -0
  38. package/src/_b_end_runtime/components/Checkbox.jsx +317 -0
  39. package/src/_b_end_runtime/components/Checkbox.tokens.js +59 -0
  40. package/src/_b_end_runtime/components/ConversationList.jsx +1264 -0
  41. package/src/_b_end_runtime/components/ConversationList.tokens.js +135 -0
  42. package/src/_b_end_runtime/components/ConversationListPreview.jsx +108 -0
  43. package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.jsx +324 -0
  44. package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.tokens.js +69 -0
  45. package/src/_b_end_runtime/components/DatePicker.jsx +739 -0
  46. package/src/_b_end_runtime/components/DatePicker.tokens.js +99 -0
  47. package/src/_b_end_runtime/components/Empty.jsx +141 -0
  48. package/src/_b_end_runtime/components/Empty.tokens.js +40 -0
  49. package/src/_b_end_runtime/components/Form.jsx +609 -0
  50. package/src/_b_end_runtime/components/Form.tokens.js +77 -0
  51. package/src/_b_end_runtime/components/FormFieldStack.jsx +123 -0
  52. package/src/_b_end_runtime/components/FormFieldStack.tokens.js +12 -0
  53. package/src/_b_end_runtime/components/FormTitle.jsx +119 -0
  54. package/src/_b_end_runtime/components/FormTitle.tokens.js +87 -0
  55. package/src/_b_end_runtime/components/FullScreenPage.jsx +97 -0
  56. package/src/_b_end_runtime/components/FullScreenPage.tokens.js +19 -0
  57. package/src/_b_end_runtime/components/Icon.jsx +172 -0
  58. package/src/_b_end_runtime/components/Icon.tokens.js +26 -0
  59. package/src/_b_end_runtime/components/IconGridPreview.jsx +277 -0
  60. package/src/_b_end_runtime/components/InfoDisplayPanel.jsx +620 -0
  61. package/src/_b_end_runtime/components/InfoDisplayPanel.tokens.js +71 -0
  62. package/src/_b_end_runtime/components/InfoDisplayPanelPreview.jsx +133 -0
  63. package/src/_b_end_runtime/components/Input.jsx +258 -0
  64. package/src/_b_end_runtime/components/Input.tokens.js +68 -0
  65. package/src/_b_end_runtime/components/InputNumber.jsx +242 -0
  66. package/src/_b_end_runtime/components/InputNumber.tokens.js +55 -0
  67. package/src/_b_end_runtime/components/Modal.jsx +155 -0
  68. package/src/_b_end_runtime/components/Modal.tokens.js +73 -0
  69. package/src/_b_end_runtime/components/NavBar.jsx +842 -0
  70. package/src/_b_end_runtime/components/NavBar.tokens.js +97 -0
  71. package/src/_b_end_runtime/components/NavBarPreview.jsx +11 -0
  72. package/src/_b_end_runtime/components/Radio.jsx +227 -0
  73. package/src/_b_end_runtime/components/Radio.tokens.js +59 -0
  74. package/src/_b_end_runtime/components/Select.jsx +766 -0
  75. package/src/_b_end_runtime/components/Select.tokens.js +99 -0
  76. package/src/_b_end_runtime/components/Sheet.jsx +132 -0
  77. package/src/_b_end_runtime/components/Sheet.tokens.js +61 -0
  78. package/src/_b_end_runtime/components/Slider.jsx +346 -0
  79. package/src/_b_end_runtime/components/Slider.tokens.js +47 -0
  80. package/src/_b_end_runtime/components/Switch.jsx +124 -0
  81. package/src/_b_end_runtime/components/Switch.tokens.js +38 -0
  82. package/src/_b_end_runtime/components/Table.jsx +1338 -0
  83. package/src/_b_end_runtime/components/Table.tokens.js +147 -0
  84. package/src/_b_end_runtime/components/TablePreview.jsx +599 -0
  85. package/src/_b_end_runtime/components/Tabs.jsx +149 -0
  86. package/src/_b_end_runtime/components/Tabs.tokens.js +102 -0
  87. package/src/_b_end_runtime/components/Tag.jsx +199 -0
  88. package/src/_b_end_runtime/components/Tag.tokens.js +171 -0
  89. package/src/_b_end_runtime/components/TagBar.jsx +1134 -0
  90. package/src/_b_end_runtime/components/TagBar.tokens.js +75 -0
  91. package/src/_b_end_runtime/components/TagGridPreview.jsx +23 -0
  92. package/src/_b_end_runtime/components/TagInput.jsx +382 -0
  93. package/src/_b_end_runtime/components/TagInput.tokens.js +52 -0
  94. package/src/_b_end_runtime/components/TextArea.jsx +363 -0
  95. package/src/_b_end_runtime/components/TextArea.tokens.js +65 -0
  96. package/src/_b_end_runtime/components/TimePicker.jsx +444 -0
  97. package/src/_b_end_runtime/components/TimePicker.tokens.js +77 -0
  98. package/src/_b_end_runtime/components/Toast.jsx +120 -0
  99. package/src/_b_end_runtime/components/Toast.tokens.js +146 -0
  100. package/src/_b_end_runtime/components/Tooltip.jsx +282 -0
  101. package/src/_b_end_runtime/components/Tooltip.tokens.js +48 -0
  102. package/src/_b_end_runtime/components/TooltipPreview.jsx +50 -0
  103. package/src/_b_end_runtime/components/Upload.jsx +455 -0
  104. package/src/_b_end_runtime/components/Upload.tokens.js +47 -0
  105. package/src/_b_end_runtime/components/avatar-assets/avatar-default.png +0 -0
  106. package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-1.png +0 -0
  107. package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-2.png +0 -0
  108. package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-3.png +0 -0
  109. package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-4.png +0 -0
  110. package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-5.png +0 -0
  111. package/src/_b_end_runtime/components/empty-assets/administrator-1.svg +40 -0
  112. package/src/_b_end_runtime/components/empty-assets/administrator-2.svg +33 -0
  113. package/src/_b_end_runtime/components/empty-assets/construction.svg +33 -0
  114. package/src/_b_end_runtime/components/empty-assets/failure.svg +49 -0
  115. package/src/_b_end_runtime/components/empty-assets/idle.svg +34 -0
  116. package/src/_b_end_runtime/components/empty-assets/no-access.svg +36 -0
  117. package/src/_b_end_runtime/components/empty-assets/no-content.svg +77 -0
  118. package/src/_b_end_runtime/components/empty-assets/no-result.svg +61 -0
  119. package/src/_b_end_runtime/components/empty-assets/not-found.svg +46 -0
  120. package/src/_b_end_runtime/components/empty-assets/success.svg +38 -0
  121. package/src/_b_end_runtime/components/file-type-assets/batch-report.png +0 -0
  122. package/src/_b_end_runtime/components/file-type-assets/catcat.svg +21 -0
  123. package/src/_b_end_runtime/components/file-type-assets/code.png +0 -0
  124. package/src/_b_end_runtime/components/file-type-assets/conversation.png +0 -0
  125. package/src/_b_end_runtime/components/file-type-assets/document.png +0 -0
  126. package/src/_b_end_runtime/components/file-type-assets/feishu-card.png +0 -0
  127. package/src/_b_end_runtime/components/file-type-assets/feishu-sheet.png +0 -0
  128. package/src/_b_end_runtime/components/file-type-assets/feishu.png +0 -0
  129. package/src/_b_end_runtime/components/file-type-assets/image.png +0 -0
  130. package/src/_b_end_runtime/components/file-type-assets/index.js +105 -0
  131. package/src/_b_end_runtime/components/file-type-assets/knowledge.png +0 -0
  132. package/src/_b_end_runtime/components/file-type-assets/pdf.png +0 -0
  133. package/src/_b_end_runtime/components/file-type-assets/pe.png +0 -0
  134. package/src/_b_end_runtime/components/file-type-assets/strategy.png +0 -0
  135. package/src/_b_end_runtime/components/file-type-assets/table.png +0 -0
  136. package/src/_b_end_runtime/components/file-type-assets/webpage.png +0 -0
  137. package/src/_b_end_runtime/components/file-type-assets/xmind.png +0 -0
  138. package/src/_b_end_runtime/components/icons/icon-data.js +12496 -0
  139. package/src/_b_end_runtime/components/nav-bar-assets/bytehi-logo-mark.svg +21 -0
  140. package/src/_b_end_runtime/components/table-assets/avatar.png +0 -0
  141. package/src/_b_end_runtime/components/table-assets/button.png +0 -0
  142. package/src/_b_end_runtime/components/table-assets/icon-chevron-down.png +0 -0
  143. package/src/_b_end_runtime/components/table-cell-assets/avatar.png +0 -0
  144. package/src/_b_end_runtime/components/table-cell-assets/button.png +0 -0
  145. package/src/_b_end_runtime/components/table-cell-assets/checkbox.png +0 -0
  146. package/src/_b_end_runtime/components/table-cell-assets/icon-chevron-right.png +0 -0
  147. package/src/_b_end_runtime/components/table-cell-assets/icon.png +0 -0
  148. package/src/_b_end_runtime/components/table-cell-assets/semi-icons-handle.png +0 -0
  149. package/src/_b_end_runtime/components/table-cell-assets/semi-icons-tree-triangle-right.png +0 -0
  150. package/src/_b_end_runtime/components/table-cell-assets/switch.png +0 -0
  151. package/src/_b_end_runtime/components/tagShared.js +3 -0
  152. package/src/_b_end_runtime/components/team-avatar-assets/chengcheng-murphy.png +0 -0
  153. package/src/_b_end_runtime/components/team-avatar-assets/duan-ran.png +0 -0
  154. package/src/_b_end_runtime/components/team-avatar-assets/guo-zhezhi.png +0 -0
  155. package/src/_b_end_runtime/components/team-avatar-assets/li-siru.png +0 -0
  156. package/src/_b_end_runtime/components/team-avatar-assets/liu-delin.png +0 -0
  157. package/src/_b_end_runtime/components.js +3499 -0
  158. package/src/_b_end_runtime/index.js +9 -0
  159. package/src/_b_end_runtime/page-patterns/BasePageFramePattern.jsx +395 -0
  160. package/src/_b_end_runtime/page-patterns/ChatConversationPattern.jsx +989 -0
  161. package/src/_b_end_runtime/page-patterns/ChatHomePagePattern.jsx +281 -0
  162. package/src/_b_end_runtime/page-patterns/CopilotPagePattern.jsx +380 -0
  163. package/src/_b_end_runtime/page-patterns/CustomerServiceWorkspaceFramePattern.jsx +392 -0
  164. package/src/_b_end_runtime/page-patterns/IMConversationPattern.jsx +590 -0
  165. package/src/_b_end_runtime/page-patterns/McpManagementPage.jsx +237 -0
  166. package/src/_b_end_runtime/page-patterns/StrategyListPage.jsx +189 -0
  167. package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +594 -0
  168. package/src/_b_end_runtime/page-patterns/VariableManagementPage.jsx +87 -0
  169. package/src/_b_end_runtime/page-patterns/pageListShared.jsx +177 -0
  170. package/src/_b_end_runtime/patterns.js +428 -0
  171. package/src/_b_end_runtime/preview-registry.jsx +4719 -0
  172. package/src/_b_end_runtime/teamMembers.js +56 -0
  173. package/src/_b_end_runtime/tokens.js +500 -0
  174. package/src/index.d.ts +1073 -0
  175. package/src/index.js +52 -0
  176. package/theme.css +350 -0
@@ -0,0 +1,594 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import Avatar from '../components/Avatar';
3
+ import Button from '../components/Button';
4
+ import Icon from '../components/Icon';
5
+ import Tabs from '../components/Tabs';
6
+ import Tag from '../components/Tag';
7
+ import { getTeamMemberByIndex, getTeamMemberByName } from '../teamMembers';
8
+ import { FilterBar, WHITE_CARD_STYLE } from './pageListShared';
9
+
10
+ /**
11
+ * TabTopBarListPage — B 端"胶囊 Tab 标题栏 + 卡片列表 + 详情面板"模板(列表示例 3)
12
+ *
13
+ * 模板结构(自上而下):
14
+ * · TopBar:场景胶囊(渠道切换) + 32px 竖分隔 + 胶囊 Tab 一级导航 + 操作按钮组
15
+ * · 白卡(自上而下):
16
+ * 筛选栏
17
+ * └ 横向两栏:左卡片流(flex-1 可滚动) + 右详情面板(shrink-0 460px,可关闭)
18
+ *
19
+ * 极简版交互:
20
+ * · 默认选中第 1 张卡片,详情面板渲染该卡片数据
21
+ * · 点击当前已选中卡片 → 详情收起(toggle 语义),列表撑满整个白卡宽度
22
+ * · 点击其他卡片 → 切换选中并保持/重新打开详情
23
+ * · 详情面板右上角"×" → 等同 toggle 收起
24
+ * · 切换顶部 Tab → 数据集换、详情默认重置为该集第 1 条
25
+ * · 详情打开/收起为宽度 + 透明度 280ms 缓动过渡,无突兀闪现
26
+ */
27
+
28
+ const SCENE_LABEL = '渠道切换';
29
+ const FILTERS = ['知识类型', '创建人', '状态'];
30
+ const DETAIL_WIDTH = 'min(460px, 40vw)';
31
+
32
+ /* 状态 → Tag 颜色变体(Tag.variant) */
33
+ const STATUS_TAG_VARIANT = {
34
+ '生效': 'green',
35
+ '已发布': 'green',
36
+ '草稿': 'grey',
37
+ '审核中': 'orange',
38
+ '已停用': 'red',
39
+ '已下架': 'red',
40
+ };
41
+
42
+ function assignTeamCreators(items, offset = 0) {
43
+ return items.map((item, index) => ({
44
+ ...item,
45
+ creator: getTeamMemberByIndex(offset + index)?.name || item.creator,
46
+ }));
47
+ }
48
+
49
+ const RAW_KNOWLEDGE_TABS = [
50
+ {
51
+ key: 'qa',
52
+ label: 'QA对知识',
53
+ createLabel: '创建QA对',
54
+ iconName: 'message-text-square-01-stroked',
55
+ typeLabel: 'QA',
56
+ data: [
57
+ {
58
+ id: 'qa-1',
59
+ title: '退款流程问答',
60
+ summary: '覆盖订单未发货 / 已发货 / 已签收三阶段的退款规则与超时处置。',
61
+ status: '生效',
62
+ creator: '林小北',
63
+ updatedAt: '2026-04-21 10:30',
64
+ tags: ['退款', '售后'],
65
+ question: '抖音生服订单已签收后还能申请退款吗?',
66
+ answer: '已签收订单仍可在 7 天无理由保障期内发起退款,需提供商品照片或服务异常说明,平台介入后通常 24h 内出结果。',
67
+ },
68
+ {
69
+ id: 'qa-2',
70
+ title: '账号注销问答',
71
+ summary: '提供账号注销前的资产清算与最终确认流程,含余额退还规则。',
72
+ status: '生效',
73
+ creator: '陈一诺',
74
+ updatedAt: '2026-04-20 18:45',
75
+ tags: ['账号', '注销'],
76
+ question: '注销账号后未消费的余额怎么办?',
77
+ answer: '注销前需将账户余额提现或转赠他人;若余额低于提现门槛,可联系客服走线下退款流程。',
78
+ },
79
+ {
80
+ id: 'qa-3',
81
+ title: '会员权益问答',
82
+ summary: '常见会员等级、积分、优惠券获取规则的标准答案。',
83
+ status: '审核中',
84
+ creator: '周知远',
85
+ updatedAt: '2026-04-19 14:12',
86
+ tags: ['会员', '权益'],
87
+ question: 'V4 会员能享受哪些独家折扣?',
88
+ answer: 'V4 会员享 8.8 折通用券、生日双倍积分、专属客服通道,及每月 1 次免预约的精选活动名额。',
89
+ },
90
+ {
91
+ id: 'qa-4',
92
+ title: '物流查询问答',
93
+ summary: '抖音生服 / 主端 / 抖音电商三端物流查询统一话术。',
94
+ status: '生效',
95
+ creator: '顾清禾',
96
+ updatedAt: '2026-04-18 09:20',
97
+ tags: ['物流', '查询'],
98
+ question: '订单显示已发货但快递无更新怎么办?',
99
+ answer: '建议先核对快递单号是否正确,若 24h 仍无更新可联系商家或在订单详情发起客服介入。',
100
+ },
101
+ {
102
+ id: 'qa-5',
103
+ title: '发票申请问答',
104
+ summary: '电子发票、增值税发票申请条件与开票流程。',
105
+ status: '草稿',
106
+ creator: '许言川',
107
+ updatedAt: '2026-04-17 16:05',
108
+ tags: ['发票', '开票'],
109
+ question: '什么订单可以开具增值税专用发票?',
110
+ answer: '企业实名认证账户下单的订单可在订单完成后 30 天内申请增值税专票,需提供完整税务信息。',
111
+ },
112
+ ],
113
+ },
114
+ {
115
+ key: 'doc',
116
+ label: '文档知识',
117
+ createLabel: '上传文档',
118
+ iconName: 'file-04-stroked',
119
+ typeLabel: '文档',
120
+ data: [
121
+ {
122
+ id: 'doc-1',
123
+ title: '商家入驻指引-2026版',
124
+ summary: '商家从入驻申请到首单履约的全流程指引,覆盖资质审核、店铺装修、商品上架、营销配置、客服接入五大节点。',
125
+ status: '生效',
126
+ creator: '林小北',
127
+ updatedAt: '2026-04-21 09:10',
128
+ tags: ['商家入驻', '指引'],
129
+ question: '入驻指引适用范围',
130
+ answer: '本指引面向 2026 年新入驻抖音生服平台的商家,已入驻商家可参考增量章节做能力升级。',
131
+ },
132
+ {
133
+ id: 'doc-2',
134
+ title: '客服培训手册',
135
+ summary: '新客服入职 14 天上岗培训内容沉淀,含话术库、SOP、应急处理三大模块。',
136
+ status: '生效',
137
+ creator: '陈一诺',
138
+ updatedAt: '2026-04-20 12:00',
139
+ tags: ['客服培训', '话术'],
140
+ question: '培训手册章节构成',
141
+ answer: '共分基础认知、产品熟悉、话术演练、模拟实战、上岗考核五个阶段,建议按周分段完成。',
142
+ },
143
+ {
144
+ id: 'doc-3',
145
+ title: '风险识别 SOP',
146
+ summary: '高风险订单 / 异常会话的识别与处置标准,含风险评分阈值与转人工触发条件。',
147
+ status: '审核中',
148
+ creator: '周知远',
149
+ updatedAt: '2026-04-19 17:30',
150
+ tags: ['风险', 'SOP'],
151
+ question: '风险订单分级标准',
152
+ answer: 'L1: 风控评分 60-79(系统自动复核);L2: 80-94(人工复核);L3: ≥95(直接拦截 + 介入)。',
153
+ },
154
+ ],
155
+ },
156
+ {
157
+ key: 'case',
158
+ label: '案例库',
159
+ createLabel: '新建案例',
160
+ iconName: 'atom-01-stroked',
161
+ typeLabel: '案例',
162
+ data: [
163
+ {
164
+ id: 'case-1',
165
+ title: '团购退款超时案例',
166
+ summary: '商家承诺 2 小时退款超时未到账的标准处置示例,含赔付金额计算与商家追责流程。',
167
+ status: '生效',
168
+ creator: '苏锦书',
169
+ updatedAt: '2026-04-21 11:08',
170
+ tags: ['团购', '退款'],
171
+ question: '案例适用场景',
172
+ answer: '适用于团购券/套餐/储值卡类目下"承诺退款时效"被打破的客诉场景。',
173
+ },
174
+ {
175
+ id: 'case-2',
176
+ title: '虚假宣传投诉案例',
177
+ summary: '门店实际服务与团购页面承诺不符的处理参考,含证据收集要点与赔付上限。',
178
+ status: '生效',
179
+ creator: '顾清禾',
180
+ updatedAt: '2026-04-20 14:25',
181
+ tags: ['宣传', '投诉'],
182
+ question: '判定虚假宣传的核心证据',
183
+ answer: '需同时具备:① 团购详情页截图;② 用户实际消费凭证;③ 双方对承诺差异的书面/录音确认。',
184
+ },
185
+ {
186
+ id: 'case-3',
187
+ title: '骑手配送纠纷案例',
188
+ summary: '配送途中商品损毁的责任判定与赔付流程示例。',
189
+ status: '已停用',
190
+ creator: '许言川',
191
+ updatedAt: '2026-04-18 22:50',
192
+ tags: ['配送', '纠纷'],
193
+ question: '损毁责任判定原则',
194
+ answer: '商品在骑手取件后、签收前损毁,原则上由骑手所属配送方承担,需 2 小时内完成首次响应。',
195
+ },
196
+ ],
197
+ },
198
+ ];
199
+
200
+ const KNOWLEDGE_TABS = RAW_KNOWLEDGE_TABS.map((tab, tabIndex) => ({
201
+ ...tab,
202
+ data: assignTeamCreators(tab.data, tabIndex * 4),
203
+ }));
204
+
205
+ export default function TabTopBarListPage({ onCreateQA }) {
206
+ const [activeIndex, setActiveIndex] = useState(0);
207
+ const activeTab = KNOWLEDGE_TABS[activeIndex];
208
+
209
+ /* selectedCardId 同时承担"哪张选中"+"详情是否展开"两个语义:
210
+ * · null → 详情收起;列表中没有任何卡片高亮
211
+ * · 'xxx' → 详情展开渲染对应数据;该卡片高亮 */
212
+ const [selectedCardId, setSelectedCardId] = useState(activeTab.data[0].id);
213
+
214
+ /* 切 Tab 时重置选中为新数据集的第一条(详情自然展开) */
215
+ useEffect(() => {
216
+ setSelectedCardId(activeTab.data[0].id);
217
+ }, [activeTab.key, activeTab.data]);
218
+
219
+ /* 详情数据:用 lastCardId 缓存"上一次打开过的卡片",
220
+ * 这样收起动画过程中(selectedCardId 已变 null)右侧仍能渲染原内容直到动画结束,
221
+ * 避免内容突然消失的视觉跳变 */
222
+ const [lastCardId, setLastCardId] = useState(activeTab.data[0].id);
223
+ useEffect(() => {
224
+ if (selectedCardId) setLastCardId(selectedCardId);
225
+ }, [selectedCardId]);
226
+
227
+ const detailCard = useMemo(
228
+ () =>
229
+ activeTab.data.find((item) => item.id === lastCardId) || activeTab.data[0],
230
+ [activeTab.data, lastCardId],
231
+ );
232
+
233
+ const detailOpen = selectedCardId !== null;
234
+
235
+ function handleSelectCard(id) {
236
+ setSelectedCardId((prev) => (prev === id ? null : id));
237
+ }
238
+
239
+ return (
240
+ <>
241
+ {/* 顶部:胶囊 Tab 标题栏 */}
242
+ <div
243
+ className="flex min-w-0 shrink-0 items-center gap-2 overflow-x-auto"
244
+ style={{ padding: '12px 16px 12px 0' }}
245
+ >
246
+ <ScenePill label={SCENE_LABEL} />
247
+
248
+ <div
249
+ className="h-8 w-px shrink-0"
250
+ style={{ background: 'var(--color-border-default, #E4E7EC)' }}
251
+ />
252
+
253
+ <Tabs
254
+ variant="pill"
255
+ size="md"
256
+ activeIndex={activeIndex}
257
+ onChange={(idx) => setActiveIndex(idx)}
258
+ items={KNOWLEDGE_TABS.map((tab) => ({
259
+ label: tab.label,
260
+ icon: <Icon name={tab.iconName} />,
261
+ }))}
262
+ />
263
+
264
+ <div className="flex flex-1 items-center justify-end gap-2 min-w-0">
265
+ <Button
266
+ variant="ghost-black"
267
+ iconOnly
268
+ icon={<Icon name="dots-horizontal-stroked" />}
269
+ aria-label="更多操作"
270
+ />
271
+ <Button variant="outline-black">知识测试</Button>
272
+ <Button variant="outline-black">任务管理</Button>
273
+ <Button
274
+ variant="primary"
275
+ icon={<Icon name="plus-stroked" />}
276
+ onClick={onCreateQA}
277
+ >
278
+ {activeTab.createLabel}
279
+ </Button>
280
+ </div>
281
+ </div>
282
+
283
+ {/* 白卡:筛选栏 + 横向(卡片列表 + 详情面板) */}
284
+ <div
285
+ className="flex flex-1 min-h-0 min-w-0 flex-col overflow-hidden"
286
+ style={{
287
+ margin: '0 16px 16px 0',
288
+ ...WHITE_CARD_STYLE,
289
+ }}
290
+ >
291
+ <div
292
+ className="flex flex-1 min-h-0 min-w-0 flex-col"
293
+ style={{ padding: '24px', gap: '16px' }}
294
+ >
295
+ <FilterBar filters={FILTERS} />
296
+
297
+ {/* 注:这里不设 gap,详情面板自己用 marginLeft 控制 16px 间距,
298
+ 这样收起时间距也能跟着动画收拢,不会留出 16px 空白 */}
299
+ <div className="flex flex-1 min-h-0 min-w-0 overflow-hidden">
300
+ {/* 左:卡片列表(独立滚动) */}
301
+ <div
302
+ className="flex flex-1 min-h-0 min-w-0 flex-col overflow-y-auto"
303
+ style={{ gap: '12px' }}
304
+ >
305
+ {activeTab.data.map((item) => (
306
+ <KnowledgeCard
307
+ key={item.id}
308
+ item={item}
309
+ typeLabel={activeTab.typeLabel}
310
+ selected={item.id === selectedCardId}
311
+ onClick={() => handleSelectCard(item.id)}
312
+ />
313
+ ))}
314
+ </div>
315
+
316
+ {/* 右:详情面板(width / opacity 过渡收起;点已选卡片 toggle 收起) */}
317
+ <DetailPanel
318
+ card={detailCard}
319
+ typeLabel={activeTab.typeLabel}
320
+ open={detailOpen}
321
+ />
322
+ </div>
323
+ </div>
324
+ </div>
325
+ </>
326
+ );
327
+ }
328
+
329
+ /* ── 单张知识卡片 ──
330
+ * 三态背景:默认浅灰填充 / hover 加深 / 选中浅绿;选中态额外用 1px brand-500 内描边强提示。
331
+ * 用 mouseenter/leave 切 inline background,避免 Tailwind hover: 与 inline style 优先级冲突。 */
332
+ function KnowledgeCard({ item, typeLabel, selected, onClick }) {
333
+ const creator = getTeamMemberByName(item.creator) || getTeamMemberByIndex(0);
334
+ const baseBg = selected
335
+ ? 'var(--color-brand-50, #ECFFF9)'
336
+ : 'var(--color-blueGrey-50, #F4F7FB)';
337
+ const hoverBg = selected
338
+ ? 'var(--color-brand-50, #ECFFF9)'
339
+ : 'var(--color-blueGrey-100, #E4EAF2)';
340
+
341
+ return (
342
+ <button
343
+ type="button"
344
+ onClick={onClick}
345
+ onMouseEnter={(e) => { e.currentTarget.style.background = hoverBg; }}
346
+ onMouseLeave={(e) => { e.currentTarget.style.background = baseBg; }}
347
+ className="min-w-0 text-left cursor-pointer focus-visible:outline-none"
348
+ style={{
349
+ border: selected
350
+ ? '1px solid var(--color-brand-500, #00B384)'
351
+ : '1px solid var(--color-border-default, #E4E7EC)',
352
+ borderRadius: '12px',
353
+ padding: '16px',
354
+ display: 'flex',
355
+ flexDirection: 'column',
356
+ gap: '8px',
357
+ background: baseBg,
358
+ transition: 'background 150ms ease, border-color 150ms ease',
359
+ }}
360
+ >
361
+ <div className="flex items-start justify-between gap-2">
362
+ <p
363
+ className="font-semibold text-blueGrey-900 m-0"
364
+ style={{
365
+ fontSize: '16px',
366
+ lineHeight: '24px',
367
+ display: '-webkit-box',
368
+ WebkitLineClamp: 2,
369
+ WebkitBoxOrient: 'vertical',
370
+ overflow: 'hidden',
371
+ }}
372
+ >
373
+ {item.question}
374
+ </p>
375
+ <Tag variant={STATUS_TAG_VARIANT[item.status] || 'grey'} style={{ flexShrink: 0 }}>
376
+ {item.status}
377
+ </Tag>
378
+ </div>
379
+
380
+ <p
381
+ className="text-blueGrey-600 m-0"
382
+ style={{
383
+ fontSize: '14px',
384
+ lineHeight: '22px',
385
+ display: '-webkit-box',
386
+ WebkitLineClamp: 2,
387
+ WebkitBoxOrient: 'vertical',
388
+ overflow: 'hidden',
389
+ }}
390
+ >
391
+ {item.answer}
392
+ </p>
393
+
394
+ <div className="flex items-center text-blueGrey-500" style={{ fontSize: '12px', gap: '12px' }}>
395
+ <span className="inline-flex items-center" style={{ gap: '4px' }}>
396
+ <Icon name="tag-01-stroked" size="xs" />
397
+ {typeLabel}
398
+ </span>
399
+ <span className="inline-flex items-center" style={{ gap: '4px' }}>
400
+ <Icon name="clock-stroked" size="xs" />
401
+ {item.updatedAt}
402
+ </span>
403
+ <span className="inline-flex items-center" style={{ gap: '4px' }}>
404
+ <Avatar size="xxs" type="image" src={creator?.avatarSrc} alt={`${item.creator}头像`} />
405
+ {item.creator}
406
+ </span>
407
+ </div>
408
+ </button>
409
+ );
410
+ }
411
+
412
+ /* ── 右侧详情面板 ──
413
+ * 外层 wrapper 用 width + opacity 一起过渡,实现"从右侧收起、列表平滑撑满"动效;
414
+ * 内层固定 460px,避免内容随 width 一起被压缩出现错位。
415
+ * 关闭:点击已选中卡片 toggle 收起(无独立 × 按钮)。 */
416
+ function DetailPanel({ card, typeLabel, open }) {
417
+ const creator = getTeamMemberByName(card.creator) || getTeamMemberByIndex(0);
418
+ return (
419
+ <div
420
+ className="shrink-0 overflow-hidden"
421
+ style={{
422
+ width: open ? DETAIL_WIDTH : '0px',
423
+ marginLeft: open ? '16px' : '0px',
424
+ opacity: open ? 1 : 0,
425
+ transition:
426
+ 'width 280ms cubic-bezier(0.4, 0, 0.2, 1), margin-left 280ms cubic-bezier(0.4, 0, 0.2, 1), opacity 200ms ease',
427
+ }}
428
+ aria-hidden={!open}
429
+ >
430
+ <aside
431
+ className="flex flex-col h-full overflow-hidden"
432
+ style={{
433
+ width: DETAIL_WIDTH,
434
+ ...WHITE_CARD_STYLE,
435
+ border: '1px solid var(--color-border-default, #E4E7EC)',
436
+ }}
437
+ >
438
+ <div className="flex flex-col flex-1 min-h-0" style={{ padding: '20px', gap: '16px' }}>
439
+
440
+ {/* Header:标题 + 状态 + 操作按钮组 */}
441
+ <div className="flex items-center justify-between gap-2 shrink-0">
442
+ <div className="flex items-center gap-2 min-w-0">
443
+ <h3
444
+ className="font-semibold text-blueGrey-900 m-0 truncate"
445
+ style={{ fontSize: '16px', lineHeight: '24px' }}
446
+ >
447
+ 知识详情
448
+ </h3>
449
+ <Tag variant={STATUS_TAG_VARIANT[card.status] || 'grey'}>
450
+ {card.status}
451
+ </Tag>
452
+ </div>
453
+ <div className="flex items-center gap-1.5 shrink-0">
454
+ <Button
455
+ variant="ghost-black"
456
+ iconOnly
457
+ icon={<Icon name="dots-horizontal-stroked" />}
458
+ aria-label="更多操作"
459
+ />
460
+ <Button
461
+ variant="outline-black"
462
+ icon={<Icon name="edit-03-stroked" />}
463
+ >
464
+ 编辑
465
+ </Button>
466
+ <Button
467
+ variant="outline-black"
468
+ icon={<Icon name="plus-stroked" />}
469
+ >
470
+ 规则答案
471
+ </Button>
472
+ </div>
473
+ </div>
474
+
475
+ {/* 元信息:行式布局(关联标签 / 知识类型 / 最近更新) */}
476
+ <div className="flex flex-col shrink-0" style={{ gap: '10px' }}>
477
+ <DetailMetaRow label="关联标签">
478
+ <div className="flex flex-wrap" style={{ gap: '4px' }}>
479
+ {(card.tags || []).map((t) => (
480
+ <Tag key={t} variant="grey">{t}</Tag>
481
+ ))}
482
+ </div>
483
+ </DetailMetaRow>
484
+ <DetailMetaRow label="知识类型">
485
+ <span className="text-blueGrey-900" style={{ fontSize: '13px' }}>{typeLabel}</span>
486
+ </DetailMetaRow>
487
+ <DetailMetaRow label="最近更新">
488
+ <span className="inline-flex items-center" style={{ gap: '6px', fontSize: '13px', color: 'var(--color-blueGrey-900)' }}>
489
+ <Avatar size="mini" type="image" src={creator?.avatarSrc} alt={`${card.creator}头像`} />
490
+ {card.updatedAt}
491
+ </span>
492
+ </DetailMetaRow>
493
+ </div>
494
+
495
+ <div
496
+ className="shrink-0"
497
+ style={{ height: '1px', background: 'var(--color-border-default, #E4E7EC)' }}
498
+ />
499
+
500
+ {/* 正文区:问题 + 回答 */}
501
+ <div className="flex flex-col flex-1 min-h-0 overflow-y-auto" style={{ gap: '20px' }}>
502
+ <div className="flex flex-col" style={{ gap: '12px' }}>
503
+ <SectionBadge type="question">Questions / 问</SectionBadge>
504
+ <p
505
+ className="font-semibold text-blueGrey-900 m-0"
506
+ style={{ fontSize: '16px', lineHeight: '26px' }}
507
+ >
508
+ {card.question}
509
+ </p>
510
+ </div>
511
+ <div className="flex flex-col" style={{ gap: '12px' }}>
512
+ <SectionBadge type="answer">Answers / 答</SectionBadge>
513
+ <p
514
+ className="text-blueGrey-700 m-0"
515
+ style={{ fontSize: '14px', lineHeight: '24px' }}
516
+ >
517
+ {card.answer}
518
+ </p>
519
+ </div>
520
+ </div>
521
+
522
+ </div>
523
+ </aside>
524
+ </div>
525
+ );
526
+ }
527
+
528
+ /* 元信息行:固定宽度 label + 右侧 value 插槽 */
529
+ function DetailMetaRow({ label, children }) {
530
+ return (
531
+ <div className="flex items-center" style={{ gap: '16px' }}>
532
+ <span
533
+ className="text-blueGrey-500 shrink-0"
534
+ style={{ fontSize: '13px', lineHeight: '20px', minWidth: '56px' }}
535
+ >
536
+ {label}
537
+ </span>
538
+ <div className="flex items-center min-w-0">{children}</div>
539
+ </div>
540
+ );
541
+ }
542
+
543
+ /* 问题/答案区的分区徽章
544
+ * type="question" → 深色实底(blueGrey-900 底 + 白字)
545
+ * type="answer" → 品牌色描边(brand 文字 + brand 描边,白底) */
546
+ function SectionBadge({ type, children }) {
547
+ const isQuestion = type === 'question';
548
+ return (
549
+ <span
550
+ style={{
551
+ display: 'flex',
552
+ padding: '4px 12px',
553
+ flexDirection: 'column',
554
+ justifyContent: 'center',
555
+ alignItems: 'center',
556
+ gap: '10px',
557
+ borderRadius: '4px',
558
+ fontSize: '12px',
559
+ fontWeight: 600,
560
+ lineHeight: '18px',
561
+ alignSelf: 'flex-start',
562
+ ...(isQuestion
563
+ ? {
564
+ color: '#FFFFFF',
565
+ background: 'var(--color-blueGrey-900, #182230)',
566
+ }
567
+ : {
568
+ color: 'var(--color-brand-700, #18B49D)',
569
+ background: 'var(--color-brand-50, #EAFAF6)',
570
+ }),
571
+ }}
572
+ >
573
+ {children}
574
+ </span>
575
+ );
576
+ }
577
+
578
+ /* ── 顶部场景胶囊 ── */
579
+ function ScenePill({ label }) {
580
+ return (
581
+ <div
582
+ className="inline-flex items-center shrink-0 bg-white/50 ring-1 ring-white ring-inset rounded-full"
583
+ style={{ padding: '6px 8px' }}
584
+ >
585
+ <button
586
+ type="button"
587
+ className="inline-flex items-center gap-1 cursor-pointer h-8 px-4 rounded-full bg-transparent border-0 text-sm font-semibold text-blueGrey-900 leading-5"
588
+ >
589
+ <span>{label}</span>
590
+ <Icon name="chevron-down-stroked" />
591
+ </button>
592
+ </div>
593
+ );
594
+ }
@@ -0,0 +1,87 @@
1
+ import { useMemo } from 'react';
2
+ import Button from '../components/Button';
3
+ import Icon from '../components/Icon';
4
+ import Table from '../components/Table';
5
+ import {
6
+ FilterBar,
7
+ LIST_COLUMNS,
8
+ PAGE_CONTEXT_LABEL,
9
+ PageHeader,
10
+ WHITE_CARD_STYLE,
11
+ buildListDataSource,
12
+ } from './pageListShared';
13
+
14
+ /**
15
+ * VariableManagementPage — B 端"变量管理"列表页模板(单白卡版)
16
+ *
17
+ * 模板结构(自上而下):
18
+ * · 标题栏:左侧竖条 + 标题 + 业务上下文胶囊 + 右侧"新建变量"按钮
19
+ * · 筛选栏:搜索框 + 下拉筛选胶囊 + Checkbox"我创建的"
20
+ * · 表格:列定义共享自 pageListShared
21
+ *
22
+ * 业务接入:
23
+ * · 替换 VARIABLE_DATA 为接口数据即可,列结构不变
24
+ * · 改 PAGE_TITLE / VARIABLE_FILTERS 即可调整文案
25
+ */
26
+
27
+ const PAGE_TITLE = '变量管理';
28
+ const VARIABLE_FILTERS = ['变量类型', '创建人', '流程引用'];
29
+
30
+ const VARIABLE_DATA = [
31
+ { name: 'user_id', tag: 'String', desc: '当前对话用户的统一标识,登录态自动注入。' },
32
+ { name: 'merchant_id', tag: 'String', desc: '当前会话所聚焦的商家 ID,用于权限隔离。' },
33
+ { name: 'activity_id', tag: 'String', desc: '关联的营销活动 ID,参与活动数据查询与配置。' },
34
+ { name: 'city_code', tag: 'Number', desc: '城市行政编码,驱动地域化策略与默认筛选。' },
35
+ { name: 'channel', tag: 'Enum', desc: '渠道来源(app / 小程序 / 抖音 / 客服),影响话术分发。' },
36
+ { name: 'order_no', tag: 'String', desc: '当前会话上下文订单号,触发订单类工具调用。' },
37
+ ];
38
+
39
+ export default function VariableManagementPage({ onNewVariable }) {
40
+ const dataSource = useMemo(() => buildListDataSource('variable', VARIABLE_DATA), []);
41
+
42
+ return (
43
+ <div
44
+ className="flex flex-1 min-h-0 min-w-0 flex-col overflow-hidden"
45
+ style={{
46
+ margin: '16px',
47
+ ...WHITE_CARD_STYLE,
48
+ }}
49
+ >
50
+ <div
51
+ className="flex flex-1 min-h-0 min-w-0 flex-col"
52
+ style={{ padding: '24px', gap: '16px' }}
53
+ >
54
+ <PageHeader
55
+ title={PAGE_TITLE}
56
+ contextLabel={PAGE_CONTEXT_LABEL}
57
+ actions={
58
+ <Button
59
+ variant="primary"
60
+ icon={<Icon name="plus-stroked" />}
61
+ onClick={onNewVariable}
62
+ >
63
+ 新建变量
64
+ </Button>
65
+ }
66
+ />
67
+
68
+ <FilterBar filters={VARIABLE_FILTERS} />
69
+
70
+ <div className="flex flex-1 min-h-0">
71
+ <Table
72
+ className="h-full"
73
+ columns={LIST_COLUMNS}
74
+ dataSource={dataSource}
75
+ fixedColumnsMode="last"
76
+ pagination={{
77
+ current: 1,
78
+ pageSize: 10,
79
+ total: dataSource.length,
80
+ pageSizeOptions: [10, 20],
81
+ }}
82
+ />
83
+ </div>
84
+ </div>
85
+ </div>
86
+ );
87
+ }