@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.
- package/AI_READ_FIRST.md +131 -0
- package/LICENSE +21 -0
- package/README.md +353 -0
- package/package.json +67 -0
- package/scripts/check-tfds-contract.mjs +334 -0
- package/scripts/check-tfds-integration.mjs +263 -0
- package/scripts/postinstall-cursor-skill.mjs +382 -0
- package/scripts/setup.mjs +520 -0
- package/skills/tfds/CHECKLIST.md +205 -0
- package/skills/tfds/COMMON_FAILURES.md +238 -0
- package/skills/tfds/DESIGN_PRINCIPLES.md +477 -0
- package/skills/tfds/GLOBAL_DESIGN_RULES.md +636 -0
- package/skills/tfds/LAYOUT_RECIPES.md +140 -0
- package/skills/tfds/LAYOUT_RULES.md +1355 -0
- package/skills/tfds/PAGE_ARCHETYPES.md +201 -0
- package/skills/tfds/SKILL.md +188 -0
- package/skills/tfds/components.index.json +7305 -0
- package/skills/tfds/components.summary.json +1809 -0
- package/src/_b_end_runtime/components/AiSuggestionShared.jsx +166 -0
- package/src/_b_end_runtime/components/Avatar.jsx +325 -0
- package/src/_b_end_runtime/components/Avatar.tokens.js +76 -0
- package/src/_b_end_runtime/components/AvatarGridPreview.jsx +56 -0
- package/src/_b_end_runtime/components/AvatarGroup.jsx +80 -0
- package/src/_b_end_runtime/components/AvatarGroup.tokens.js +28 -0
- package/src/_b_end_runtime/components/Button.jsx +144 -0
- package/src/_b_end_runtime/components/Button.tokens.js +90 -0
- package/src/_b_end_runtime/components/Card.jsx +460 -0
- package/src/_b_end_runtime/components/Card.tokens.js +124 -0
- package/src/_b_end_runtime/components/CardPreview.jsx +51 -0
- package/src/_b_end_runtime/components/ChatBubble.jsx +384 -0
- package/src/_b_end_runtime/components/ChatBubble.tokens.js +60 -0
- package/src/_b_end_runtime/components/ChatBubblePreview.jsx +129 -0
- package/src/_b_end_runtime/components/ChatInput.jsx +1399 -0
- package/src/_b_end_runtime/components/ChatInput.tokens.js +75 -0
- package/src/_b_end_runtime/components/ChatMessage.jsx +2215 -0
- package/src/_b_end_runtime/components/ChatMessage.tokens.js +257 -0
- package/src/_b_end_runtime/components/ChatMessagePreview.jsx +388 -0
- package/src/_b_end_runtime/components/Checkbox.jsx +317 -0
- package/src/_b_end_runtime/components/Checkbox.tokens.js +59 -0
- package/src/_b_end_runtime/components/ConversationList.jsx +1264 -0
- package/src/_b_end_runtime/components/ConversationList.tokens.js +135 -0
- package/src/_b_end_runtime/components/ConversationListPreview.jsx +108 -0
- package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.jsx +324 -0
- package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.tokens.js +69 -0
- package/src/_b_end_runtime/components/DatePicker.jsx +739 -0
- package/src/_b_end_runtime/components/DatePicker.tokens.js +99 -0
- package/src/_b_end_runtime/components/Empty.jsx +141 -0
- package/src/_b_end_runtime/components/Empty.tokens.js +40 -0
- package/src/_b_end_runtime/components/Form.jsx +609 -0
- package/src/_b_end_runtime/components/Form.tokens.js +77 -0
- package/src/_b_end_runtime/components/FormFieldStack.jsx +123 -0
- package/src/_b_end_runtime/components/FormFieldStack.tokens.js +12 -0
- package/src/_b_end_runtime/components/FormTitle.jsx +119 -0
- package/src/_b_end_runtime/components/FormTitle.tokens.js +87 -0
- package/src/_b_end_runtime/components/FullScreenPage.jsx +97 -0
- package/src/_b_end_runtime/components/FullScreenPage.tokens.js +19 -0
- package/src/_b_end_runtime/components/Icon.jsx +172 -0
- package/src/_b_end_runtime/components/Icon.tokens.js +26 -0
- package/src/_b_end_runtime/components/IconGridPreview.jsx +277 -0
- package/src/_b_end_runtime/components/InfoDisplayPanel.jsx +620 -0
- package/src/_b_end_runtime/components/InfoDisplayPanel.tokens.js +71 -0
- package/src/_b_end_runtime/components/InfoDisplayPanelPreview.jsx +133 -0
- package/src/_b_end_runtime/components/Input.jsx +258 -0
- package/src/_b_end_runtime/components/Input.tokens.js +68 -0
- package/src/_b_end_runtime/components/InputNumber.jsx +242 -0
- package/src/_b_end_runtime/components/InputNumber.tokens.js +55 -0
- package/src/_b_end_runtime/components/Modal.jsx +155 -0
- package/src/_b_end_runtime/components/Modal.tokens.js +73 -0
- package/src/_b_end_runtime/components/NavBar.jsx +842 -0
- package/src/_b_end_runtime/components/NavBar.tokens.js +97 -0
- package/src/_b_end_runtime/components/NavBarPreview.jsx +11 -0
- package/src/_b_end_runtime/components/Radio.jsx +227 -0
- package/src/_b_end_runtime/components/Radio.tokens.js +59 -0
- package/src/_b_end_runtime/components/Select.jsx +766 -0
- package/src/_b_end_runtime/components/Select.tokens.js +99 -0
- package/src/_b_end_runtime/components/Sheet.jsx +132 -0
- package/src/_b_end_runtime/components/Sheet.tokens.js +61 -0
- package/src/_b_end_runtime/components/Slider.jsx +346 -0
- package/src/_b_end_runtime/components/Slider.tokens.js +47 -0
- package/src/_b_end_runtime/components/Switch.jsx +124 -0
- package/src/_b_end_runtime/components/Switch.tokens.js +38 -0
- package/src/_b_end_runtime/components/Table.jsx +1338 -0
- package/src/_b_end_runtime/components/Table.tokens.js +147 -0
- package/src/_b_end_runtime/components/TablePreview.jsx +599 -0
- package/src/_b_end_runtime/components/Tabs.jsx +149 -0
- package/src/_b_end_runtime/components/Tabs.tokens.js +102 -0
- package/src/_b_end_runtime/components/Tag.jsx +199 -0
- package/src/_b_end_runtime/components/Tag.tokens.js +171 -0
- package/src/_b_end_runtime/components/TagBar.jsx +1134 -0
- package/src/_b_end_runtime/components/TagBar.tokens.js +75 -0
- package/src/_b_end_runtime/components/TagGridPreview.jsx +23 -0
- package/src/_b_end_runtime/components/TagInput.jsx +382 -0
- package/src/_b_end_runtime/components/TagInput.tokens.js +52 -0
- package/src/_b_end_runtime/components/TextArea.jsx +363 -0
- package/src/_b_end_runtime/components/TextArea.tokens.js +65 -0
- package/src/_b_end_runtime/components/TimePicker.jsx +444 -0
- package/src/_b_end_runtime/components/TimePicker.tokens.js +77 -0
- package/src/_b_end_runtime/components/Toast.jsx +120 -0
- package/src/_b_end_runtime/components/Toast.tokens.js +146 -0
- package/src/_b_end_runtime/components/Tooltip.jsx +282 -0
- package/src/_b_end_runtime/components/Tooltip.tokens.js +48 -0
- package/src/_b_end_runtime/components/TooltipPreview.jsx +50 -0
- package/src/_b_end_runtime/components/Upload.jsx +455 -0
- package/src/_b_end_runtime/components/Upload.tokens.js +47 -0
- package/src/_b_end_runtime/components/avatar-assets/avatar-default.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-1.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-2.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-3.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-4.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-5.png +0 -0
- package/src/_b_end_runtime/components/empty-assets/administrator-1.svg +40 -0
- package/src/_b_end_runtime/components/empty-assets/administrator-2.svg +33 -0
- package/src/_b_end_runtime/components/empty-assets/construction.svg +33 -0
- package/src/_b_end_runtime/components/empty-assets/failure.svg +49 -0
- package/src/_b_end_runtime/components/empty-assets/idle.svg +34 -0
- package/src/_b_end_runtime/components/empty-assets/no-access.svg +36 -0
- package/src/_b_end_runtime/components/empty-assets/no-content.svg +77 -0
- package/src/_b_end_runtime/components/empty-assets/no-result.svg +61 -0
- package/src/_b_end_runtime/components/empty-assets/not-found.svg +46 -0
- package/src/_b_end_runtime/components/empty-assets/success.svg +38 -0
- package/src/_b_end_runtime/components/file-type-assets/batch-report.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/catcat.svg +21 -0
- package/src/_b_end_runtime/components/file-type-assets/code.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/conversation.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/document.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/feishu-card.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/feishu-sheet.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/feishu.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/image.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/index.js +105 -0
- package/src/_b_end_runtime/components/file-type-assets/knowledge.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/pdf.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/pe.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/strategy.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/table.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/webpage.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/xmind.png +0 -0
- package/src/_b_end_runtime/components/icons/icon-data.js +12496 -0
- package/src/_b_end_runtime/components/nav-bar-assets/bytehi-logo-mark.svg +21 -0
- package/src/_b_end_runtime/components/table-assets/avatar.png +0 -0
- package/src/_b_end_runtime/components/table-assets/button.png +0 -0
- package/src/_b_end_runtime/components/table-assets/icon-chevron-down.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/avatar.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/button.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/checkbox.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/icon-chevron-right.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/icon.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/semi-icons-handle.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/semi-icons-tree-triangle-right.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/switch.png +0 -0
- package/src/_b_end_runtime/components/tagShared.js +3 -0
- package/src/_b_end_runtime/components/team-avatar-assets/chengcheng-murphy.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/duan-ran.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/guo-zhezhi.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/li-siru.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/liu-delin.png +0 -0
- package/src/_b_end_runtime/components.js +3499 -0
- package/src/_b_end_runtime/index.js +9 -0
- package/src/_b_end_runtime/page-patterns/BasePageFramePattern.jsx +395 -0
- package/src/_b_end_runtime/page-patterns/ChatConversationPattern.jsx +989 -0
- package/src/_b_end_runtime/page-patterns/ChatHomePagePattern.jsx +281 -0
- package/src/_b_end_runtime/page-patterns/CopilotPagePattern.jsx +380 -0
- package/src/_b_end_runtime/page-patterns/CustomerServiceWorkspaceFramePattern.jsx +392 -0
- package/src/_b_end_runtime/page-patterns/IMConversationPattern.jsx +590 -0
- package/src/_b_end_runtime/page-patterns/McpManagementPage.jsx +237 -0
- package/src/_b_end_runtime/page-patterns/StrategyListPage.jsx +189 -0
- package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +594 -0
- package/src/_b_end_runtime/page-patterns/VariableManagementPage.jsx +87 -0
- package/src/_b_end_runtime/page-patterns/pageListShared.jsx +177 -0
- package/src/_b_end_runtime/patterns.js +428 -0
- package/src/_b_end_runtime/preview-registry.jsx +4719 -0
- package/src/_b_end_runtime/teamMembers.js +56 -0
- package/src/_b_end_runtime/tokens.js +500 -0
- package/src/index.d.ts +1073 -0
- package/src/index.js +52 -0
- package/theme.css +350 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import Avatar from './Avatar';
|
|
2
|
+
import Icon from './Icon';
|
|
3
|
+
import Tooltip from './Tooltip';
|
|
4
|
+
/* ── 基础容器 ── */
|
|
5
|
+
const BASE = 'tfds-chat-bubble grid w-full items-start gap-x-2';
|
|
6
|
+
const BASE_STYLE = {
|
|
7
|
+
gridTemplateColumns: 'var(--spacing-6) minmax(0, 1fr) var(--spacing-6)',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/* ── 头像槽 ── */
|
|
11
|
+
const AVATAR_SLOT = 'flex h-[var(--spacing-6)] w-[var(--spacing-6)] items-start';
|
|
12
|
+
const EMPTY_SLOT = 'h-[var(--spacing-6)] w-[var(--spacing-6)]';
|
|
13
|
+
|
|
14
|
+
/* ── 角色 → 消息列排列 ── */
|
|
15
|
+
const STACK_CLASS = {
|
|
16
|
+
incoming: 'flex min-w-0 flex-col items-start',
|
|
17
|
+
outgoing: 'flex min-w-0 flex-col items-end',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/* ── 数量 → 气泡组间距 ── */
|
|
21
|
+
const LAYOUT_CLASS = {
|
|
22
|
+
single: 'gap-0',
|
|
23
|
+
multiple: 'gap-1',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/* ── 尺寸 → 气泡内边距 ── */
|
|
27
|
+
const SIZE_CLASS = {
|
|
28
|
+
default: 'px-3 py-2',
|
|
29
|
+
large: 'px-4 py-3',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/* ── 角色 → 单条消息行排列 ── */
|
|
33
|
+
const ROW_CLASS = {
|
|
34
|
+
incoming: 'group relative flex max-w-[var(--size-chat-bubble-max)] items-end justify-start',
|
|
35
|
+
outgoing: 'group relative flex max-w-[var(--size-chat-bubble-max)] items-end gap-1',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/* ── 气泡底色(左右各自四档:绿 / 白 / 灰 / AI 渐变)── */
|
|
39
|
+
const BUBBLE_AI_STYLE = { background: 'var(--bg-chat-outgoing-ai)' };
|
|
40
|
+
|
|
41
|
+
const INCOMING_TONE_CLASS = {
|
|
42
|
+
default: 'rounded-bl-lg rounded-br-lg rounded-tr-lg bg-chat-incoming',
|
|
43
|
+
white: 'rounded-bl-lg rounded-br-lg rounded-tr-lg bg-surface',
|
|
44
|
+
grey: 'rounded-bl-lg rounded-br-lg rounded-tr-lg bg-fill',
|
|
45
|
+
ai: 'rounded-bl-lg rounded-br-lg rounded-tr-lg',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const OUTGOING_TONE_CLASS = {
|
|
49
|
+
default: 'rounded-bl-lg rounded-br-lg rounded-tl-lg bg-chat-outgoing',
|
|
50
|
+
white: 'rounded-bl-lg rounded-br-lg rounded-tl-lg bg-surface',
|
|
51
|
+
grey: 'rounded-bl-lg rounded-br-lg rounded-tl-lg bg-fill',
|
|
52
|
+
ai: 'rounded-bl-lg rounded-br-lg rounded-tl-lg',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/* ── 文字 ── */
|
|
56
|
+
const TEXT = 'm-0 w-full text-sm font-normal leading-5 text-foreground';
|
|
57
|
+
const TIMESTAMP_TOP = 'absolute bottom-full z-10 mb-[var(--spacing-0_5)] flex items-center gap-1 whitespace-nowrap opacity-0 transition-opacity duration-150 group-hover:opacity-100';
|
|
58
|
+
const TIMESTAMP_TOP_ALIGN = {
|
|
59
|
+
incoming: 'left-0 justify-start',
|
|
60
|
+
outgoing: 'right-0 justify-end',
|
|
61
|
+
};
|
|
62
|
+
const TIMESTAMP_SIDE = 'absolute top-1/2 z-10 flex -translate-y-1/2 items-center gap-1 whitespace-nowrap opacity-0 transition-opacity duration-150 group-hover:opacity-100';
|
|
63
|
+
const TIMESTAMP_SIDE_ALIGN = {
|
|
64
|
+
incoming: 'right-full mr-[var(--spacing-2)] justify-end',
|
|
65
|
+
outgoing: 'left-full ml-[var(--spacing-2)] justify-start',
|
|
66
|
+
};
|
|
67
|
+
const TIMESTAMP_TEXT = 'text-xs font-normal leading-4 text-foreground-muted';
|
|
68
|
+
const TIMESTAMP_DATE = 'inline-flex cursor-default items-center';
|
|
69
|
+
|
|
70
|
+
/* ── 已读回执 ── */
|
|
71
|
+
const RECEIPT_CLASS = {
|
|
72
|
+
hidden: 'hidden',
|
|
73
|
+
read: [
|
|
74
|
+
'inline-flex h-[var(--spacing-3)] w-[var(--spacing-3)] shrink-0 items-center justify-center',
|
|
75
|
+
'text-success',
|
|
76
|
+
].join(' '),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/* ── 头像 → 复用 Avatar 组件的类型映射 ── */
|
|
80
|
+
const AVATAR_PROPS = {
|
|
81
|
+
image: { type: 'image' },
|
|
82
|
+
robot: { type: 'robot' },
|
|
83
|
+
ai: { type: 'ai' },
|
|
84
|
+
none: null,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const BUBBLE_TONE_KEYS = new Set(['default', 'white', 'grey', 'ai']);
|
|
88
|
+
|
|
89
|
+
function normalizeBubbleTone(value, fallback) {
|
|
90
|
+
return BUBBLE_TONE_KEYS.has(value) ? value : fallback;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const DEFAULT_MESSAGES = {
|
|
94
|
+
incoming: {
|
|
95
|
+
single: ['为什么我无法设置点赞列表查看权限'],
|
|
96
|
+
multiple: ['为什么我无法设置点赞列表查看权限', '那怎么看自己的直播回放?'],
|
|
97
|
+
},
|
|
98
|
+
outgoing: {
|
|
99
|
+
single: ['您好,因产品功能更新,“作品点赞信息”设置开关正逐步下线中'],
|
|
100
|
+
multiple: [
|
|
101
|
+
'您好,因产品功能更新,“作品点赞信息”设置开关正逐步下线中',
|
|
102
|
+
'目前不支持设置“作品点赞信息”权限。该功能下线后,您发布的作品的点赞列表仅对您自己可见。',
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function pad2(value) {
|
|
108
|
+
return String(value).padStart(2, '0');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createTimestampMeta(rawValue) {
|
|
112
|
+
if (typeof rawValue !== 'string') {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const value = rawValue.trim();
|
|
117
|
+
if (!value) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const fullDatePatterns = [
|
|
122
|
+
/^(\d{4})年(\d{1,2})月(\d{1,2})日\s*(\d{1,2}):(\d{2})(?::(\d{2}))?$/,
|
|
123
|
+
/^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})(?:\s+|T)(\d{1,2}):(\d{2})(?::(\d{2}))?$/,
|
|
124
|
+
];
|
|
125
|
+
const monthDayPattern = /^(\d{1,2})月(\d{1,2})日\s*(\d{1,2}):(\d{2})(?::(\d{2}))?$/;
|
|
126
|
+
const timePattern = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/;
|
|
127
|
+
|
|
128
|
+
for (const pattern of fullDatePatterns) {
|
|
129
|
+
const match = value.match(pattern);
|
|
130
|
+
if (!match) continue;
|
|
131
|
+
|
|
132
|
+
const [, year, month, day, hour, minute, second = '00'] = match;
|
|
133
|
+
const monthLabel = `${Number(month)}月${Number(day)}日`;
|
|
134
|
+
const timeLabel = `${pad2(hour)}:${minute}`;
|
|
135
|
+
return {
|
|
136
|
+
summary: `${monthLabel} ${timeLabel}`,
|
|
137
|
+
dateLabel: monthLabel,
|
|
138
|
+
timeLabel,
|
|
139
|
+
detail: `${year}年${Number(month)}月${Number(day)}日 ${pad2(hour)}:${minute}:${pad2(second)}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const monthDayMatch = value.match(monthDayPattern);
|
|
144
|
+
if (monthDayMatch) {
|
|
145
|
+
const [, month, day, hour, minute, second] = monthDayMatch;
|
|
146
|
+
const monthLabel = `${Number(month)}月${Number(day)}日`;
|
|
147
|
+
const timeLabel = `${pad2(hour)}:${minute}`;
|
|
148
|
+
return {
|
|
149
|
+
summary: `${monthLabel} ${timeLabel}`,
|
|
150
|
+
dateLabel: monthLabel,
|
|
151
|
+
timeLabel,
|
|
152
|
+
detail: second ? `${monthLabel} ${pad2(hour)}:${minute}:${pad2(second)}` : null,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const timeMatch = value.match(timePattern);
|
|
157
|
+
if (timeMatch) {
|
|
158
|
+
const [, hour, minute, second] = timeMatch;
|
|
159
|
+
return {
|
|
160
|
+
summary: `${pad2(hour)}:${minute}`,
|
|
161
|
+
dateLabel: null,
|
|
162
|
+
timeLabel: `${pad2(hour)}:${minute}`,
|
|
163
|
+
detail: second ? `${pad2(hour)}:${minute}:${pad2(second)}` : null,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
summary: value,
|
|
169
|
+
dateLabel: null,
|
|
170
|
+
timeLabel: value,
|
|
171
|
+
detail: null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getTimestampPlacement(layout, index) {
|
|
176
|
+
return layout === 'multiple' && index > 0 ? 'side' : 'top';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderTimestampDetail(text, detail) {
|
|
180
|
+
if (!detail) {
|
|
181
|
+
return <span className={TIMESTAMP_TEXT}>{text}</span>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<Tooltip content={detail} placement="top" triggerClassName={TIMESTAMP_DATE}>
|
|
186
|
+
<span className={TIMESTAMP_TEXT}>{text}</span>
|
|
187
|
+
</Tooltip>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function TimestampLabel({ value, align = 'incoming', placement = 'top', showDate = true }) {
|
|
192
|
+
const meta = createTimestampMeta(value);
|
|
193
|
+
|
|
194
|
+
if (!meta?.summary) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const dateLabel = showDate ? meta.dateLabel : null;
|
|
199
|
+
const showDetailTooltip = Boolean(meta.detail && meta.detail !== meta.summary);
|
|
200
|
+
|
|
201
|
+
const containerClass = placement === 'side'
|
|
202
|
+
? [TIMESTAMP_SIDE, TIMESTAMP_SIDE_ALIGN[align]]
|
|
203
|
+
: [TIMESTAMP_TOP, TIMESTAMP_TOP_ALIGN[align]];
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div className={containerClass.join(' ')}>
|
|
207
|
+
{dateLabel ? (
|
|
208
|
+
renderTimestampDetail(dateLabel, showDetailTooltip ? meta.detail : null)
|
|
209
|
+
) : null}
|
|
210
|
+
|
|
211
|
+
{meta.timeLabel ? (
|
|
212
|
+
showDetailTooltip && !dateLabel ? (
|
|
213
|
+
renderTimestampDetail(meta.timeLabel, meta.detail)
|
|
214
|
+
) : (
|
|
215
|
+
<span className={TIMESTAMP_TEXT}>{meta.timeLabel}</span>
|
|
216
|
+
)
|
|
217
|
+
) : null}
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* ChatBubble — B端业务 IM 聊天气泡
|
|
224
|
+
* @prop {'incoming'|'outgoing'} [variant='incoming'] — 气泡角色:incoming 为用户左侧,outgoing 为客服右侧
|
|
225
|
+
* @prop {'single'|'multiple'} [layout='single'] — 气泡数量偏好;当传入多条消息时,组件会自动按 multiple 连续气泡布局展示
|
|
226
|
+
* @prop {'image'|'robot'|'ai'|'none'} [avatarType='image'] — 头像类型,支持图片 / 机器人 / AI头像 / 不显示头像
|
|
227
|
+
* @prop {'default'|'large'} [size='default'] — 气泡尺寸:default 为 12px/8px 内边距,large 为 16px/12px 内边距
|
|
228
|
+
* @prop {'default'|'white'|'grey'|'ai'} [incomingTone='default'] — 左侧气泡:default 浅灰透明底,white 白底,grey 为 fill-default 填充底,ai 同右列 AI 渐变
|
|
229
|
+
* @prop {'default'|'white'|'grey'|'ai'} [outgoingTone] — 右侧气泡;未传时回退到 `tone`
|
|
230
|
+
* @prop {'default'|'white'|'grey'|'ai'} [tone='default'] — 兼容旧 API,等价于仅设置右侧 `outgoingTone`
|
|
231
|
+
* @prop {'hidden'|'read'} [receipt='hidden'] — 右侧消息回执:hidden 隐藏,read 显示已读徽标
|
|
232
|
+
* @prop {string[]|null} [messages=null] — 消息文案数组;为空时使用内置示例文案
|
|
233
|
+
* @prop {string[]|null} [timestamps=null] — 消息时间数组;单条和连续首条消息 hover 时显示“月日 + 时间”,连续消息后续条目改为在气泡侧边显示
|
|
234
|
+
* @prop {string|null} [avatarSrc=null] — 图片头像地址,仅 avatarType="image" 时生效;不传时由 Avatar 默认随机使用本地成员头像图片素材
|
|
235
|
+
* @prop {string} [avatarAlt='头像'] — 头像替代文本
|
|
236
|
+
* @prop {string} [className=''] — 附加类名
|
|
237
|
+
* @prop {React.CSSProperties|undefined} [style=undefined] — 附加内联样式
|
|
238
|
+
*
|
|
239
|
+
* 组件复用 Avatar 负责头像本体,复用 Icon 负责已读徽标中的勾形图标;
|
|
240
|
+
* 气泡本体覆盖 Figma 中的左侧浅灰气泡、右侧浅青气泡,以及右侧连续消息的回执布局。
|
|
241
|
+
*/
|
|
242
|
+
export default function ChatBubble({
|
|
243
|
+
variant = 'incoming',
|
|
244
|
+
layout = 'single',
|
|
245
|
+
avatarType = 'image',
|
|
246
|
+
size = 'default',
|
|
247
|
+
incomingTone = 'default',
|
|
248
|
+
outgoingTone,
|
|
249
|
+
tone = 'default',
|
|
250
|
+
receipt = 'hidden',
|
|
251
|
+
messages = null,
|
|
252
|
+
timestamps = null,
|
|
253
|
+
avatarSrc = null,
|
|
254
|
+
avatarAlt = '头像',
|
|
255
|
+
className = '',
|
|
256
|
+
style,
|
|
257
|
+
}) {
|
|
258
|
+
const resolvedVariant = variant === 'outgoing' ? 'outgoing' : 'incoming';
|
|
259
|
+
const preferredLayout = Object.prototype.hasOwnProperty.call(LAYOUT_CLASS, layout) ? layout : 'single';
|
|
260
|
+
const resolvedSize = Object.prototype.hasOwnProperty.call(SIZE_CLASS, size) ? size : 'default';
|
|
261
|
+
const resolvedReceipt = Object.prototype.hasOwnProperty.call(RECEIPT_CLASS, receipt) ? receipt : 'hidden';
|
|
262
|
+
const preferredAvatarType = Object.prototype.hasOwnProperty.call(AVATAR_PROPS, avatarType) ? avatarType : 'image';
|
|
263
|
+
const resolvedIncomingTone = normalizeBubbleTone(incomingTone, 'default');
|
|
264
|
+
const resolvedOutgoingTone = normalizeBubbleTone(outgoingTone ?? tone, 'default');
|
|
265
|
+
/* 非 none:显式 AI 头像或 AI 色气泡时,头像与渐变底联动;none 时不升档为 AI 头像 */
|
|
266
|
+
let effIncoming = resolvedIncomingTone;
|
|
267
|
+
let effOutgoing = resolvedOutgoingTone;
|
|
268
|
+
if (resolvedVariant === 'outgoing') {
|
|
269
|
+
effOutgoing = (preferredAvatarType === 'ai' || resolvedOutgoingTone === 'ai') ? 'ai' : resolvedOutgoingTone;
|
|
270
|
+
} else {
|
|
271
|
+
effIncoming = (preferredAvatarType === 'ai' || resolvedIncomingTone === 'ai') ? 'ai' : resolvedIncomingTone;
|
|
272
|
+
}
|
|
273
|
+
/* AI 色气泡可与 AI 头像联动;显式隐藏头像时不展示头像,仅保留渐变底与空槽位对齐 */
|
|
274
|
+
const resolvedAvatarType = preferredAvatarType === 'none'
|
|
275
|
+
? 'none'
|
|
276
|
+
: (effOutgoing === 'ai' && resolvedVariant === 'outgoing')
|
|
277
|
+
|| (effIncoming === 'ai' && resolvedVariant === 'incoming')
|
|
278
|
+
? 'ai'
|
|
279
|
+
: preferredAvatarType === 'ai'
|
|
280
|
+
? 'ai'
|
|
281
|
+
: preferredAvatarType;
|
|
282
|
+
|
|
283
|
+
const hasCustomMessages = Array.isArray(messages) && messages.length > 0;
|
|
284
|
+
const actualLayout = hasCustomMessages && messages.length > 1 ? 'multiple' : preferredLayout;
|
|
285
|
+
const fallbackMessages = DEFAULT_MESSAGES[resolvedVariant][actualLayout];
|
|
286
|
+
const sourceMessages = hasCustomMessages ? messages : fallbackMessages;
|
|
287
|
+
const resolvedMessages = actualLayout === 'single' ? sourceMessages.slice(0, 1) : sourceMessages;
|
|
288
|
+
const resolvedTimestamps = Array.isArray(timestamps) ? timestamps : [];
|
|
289
|
+
const avatarConfig = AVATAR_PROPS[resolvedAvatarType];
|
|
290
|
+
const showReceipt = resolvedVariant === 'outgoing' && resolvedReceipt === 'read';
|
|
291
|
+
const mergedStyle = { ...BASE_STYLE, ...style };
|
|
292
|
+
const avatarNode = avatarConfig ? (
|
|
293
|
+
<Avatar
|
|
294
|
+
size="xs"
|
|
295
|
+
type={avatarConfig.type}
|
|
296
|
+
src={resolvedAvatarType === 'image' ? avatarSrc : null}
|
|
297
|
+
alt={avatarAlt}
|
|
298
|
+
/>
|
|
299
|
+
) : null;
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<div className={[BASE, className].filter(Boolean).join(' ')} style={mergedStyle} data-tfds-component="ChatBubble">
|
|
303
|
+
{resolvedVariant === 'incoming' ? (
|
|
304
|
+
<>
|
|
305
|
+
<div className={AVATAR_SLOT}>
|
|
306
|
+
{avatarNode}
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<div className={[STACK_CLASS.incoming, LAYOUT_CLASS[actualLayout]].join(' ')}>
|
|
310
|
+
{resolvedMessages.map((message, index) => {
|
|
311
|
+
const placement = getTimestampPlacement(actualLayout, index);
|
|
312
|
+
const showDate = index === 0;
|
|
313
|
+
return (
|
|
314
|
+
<div
|
|
315
|
+
key={`${message}-${index}`}
|
|
316
|
+
className={ROW_CLASS.incoming}
|
|
317
|
+
data-tfds-component="ChatBubble.Message"
|
|
318
|
+
>
|
|
319
|
+
<TimestampLabel
|
|
320
|
+
value={resolvedTimestamps[index]}
|
|
321
|
+
align="incoming"
|
|
322
|
+
placement={placement}
|
|
323
|
+
showDate={showDate}
|
|
324
|
+
/>
|
|
325
|
+
<div
|
|
326
|
+
className={[INCOMING_TONE_CLASS[effIncoming], SIZE_CLASS[resolvedSize]].join(' ')}
|
|
327
|
+
style={effIncoming === 'ai' ? BUBBLE_AI_STYLE : undefined}
|
|
328
|
+
data-tfds-component="ChatBubble.Bubble"
|
|
329
|
+
>
|
|
330
|
+
<p className={TEXT}>{message}</p>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
})}
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<div className={EMPTY_SLOT} aria-hidden="true" />
|
|
338
|
+
</>
|
|
339
|
+
) : (
|
|
340
|
+
<>
|
|
341
|
+
<div className={EMPTY_SLOT} aria-hidden="true" />
|
|
342
|
+
|
|
343
|
+
<div className={[STACK_CLASS.outgoing, LAYOUT_CLASS[actualLayout]].join(' ')}>
|
|
344
|
+
{resolvedMessages.map((message, index) => {
|
|
345
|
+
const placement = getTimestampPlacement(actualLayout, index);
|
|
346
|
+
const showDate = index === 0;
|
|
347
|
+
return (
|
|
348
|
+
<div
|
|
349
|
+
key={`${message}-${index}`}
|
|
350
|
+
className={ROW_CLASS.outgoing}
|
|
351
|
+
data-tfds-component="ChatBubble.Message"
|
|
352
|
+
>
|
|
353
|
+
<TimestampLabel
|
|
354
|
+
value={resolvedTimestamps[index]}
|
|
355
|
+
align="outgoing"
|
|
356
|
+
placement={placement}
|
|
357
|
+
showDate={showDate}
|
|
358
|
+
/>
|
|
359
|
+
{showReceipt ? (
|
|
360
|
+
<span className={RECEIPT_CLASS.read} aria-label="已读" data-tfds-component="ChatBubble.Receipt">
|
|
361
|
+
<Icon name="check-circle-stroked" size="xs" />
|
|
362
|
+
</span>
|
|
363
|
+
) : null}
|
|
364
|
+
|
|
365
|
+
<div
|
|
366
|
+
className={[OUTGOING_TONE_CLASS[effOutgoing], SIZE_CLASS[resolvedSize]].join(' ')}
|
|
367
|
+
style={effOutgoing === 'ai' ? BUBBLE_AI_STYLE : undefined}
|
|
368
|
+
data-tfds-component="ChatBubble.Bubble"
|
|
369
|
+
>
|
|
370
|
+
<p className={TEXT}>{message}</p>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
})}
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
<div className={AVATAR_SLOT}>
|
|
378
|
+
{avatarNode}
|
|
379
|
+
</div>
|
|
380
|
+
</>
|
|
381
|
+
)}
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const CHATBUBBLE_TOKEN_MAP = {
|
|
2
|
+
base: [],
|
|
3
|
+
文字: [
|
|
4
|
+
{ label: '颜色', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
|
|
5
|
+
{ label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
6
|
+
{ label: '字重', cssProp: 'font-weight', token: '--font-normal', value: '400' },
|
|
7
|
+
{ label: '行高', cssProp: 'line-height', token: '--leading-5', value: '20px' },
|
|
8
|
+
],
|
|
9
|
+
左气泡: [
|
|
10
|
+
{ label: '默认底(incomingTone=default)', cssProp: 'background', token: '--color-chat-incoming', value: 'rgba(52,59,57,0.05)', semanticRef: 'bg-chat-incoming' },
|
|
11
|
+
{ label: '白底', cssProp: 'background', token: '--color-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
|
|
12
|
+
{ label: '灰底(incomingTone=grey)', cssProp: 'background', token: '--color-fill', value: 'rgba(83, 96, 143, 0.07)', semanticRef: 'fill-default' },
|
|
13
|
+
{ label: 'AI渐变(incomingTone=ai)', cssProp: 'background', token: '--color-chat-outgoing-ai', 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%)', semanticRef: 'bg-chat-outgoing-ai' },
|
|
14
|
+
{ label: '圆角', cssProp: 'border-radius', token: '--radius-lg', value: '12px' },
|
|
15
|
+
],
|
|
16
|
+
右气泡: [
|
|
17
|
+
{ label: '浅青背景(outgoingTone=default)', cssProp: 'background', token: '--color-chat-outgoing', value: '#E5F7F7', semanticRef: 'bg-chat-outgoing' },
|
|
18
|
+
{ label: '白底背景', cssProp: 'background', token: '--color-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
|
|
19
|
+
{ label: '灰底(outgoingTone=grey)', cssProp: 'background', token: '--color-fill', value: 'rgba(83, 96, 143, 0.07)', semanticRef: 'fill-default' },
|
|
20
|
+
{ label: 'AI渐变背景', cssProp: 'background', token: '--color-chat-outgoing-ai', 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%)', semanticRef: 'bg-chat-outgoing-ai' },
|
|
21
|
+
{ label: '圆角', cssProp: 'border-radius', token: '--radius-lg', value: '12px' },
|
|
22
|
+
],
|
|
23
|
+
回执: [
|
|
24
|
+
{ label: '图标色', cssProp: 'color', token: '--color-success', value: '#3EB346', semanticRef: 'status-success' },
|
|
25
|
+
{ label: '边长', cssProp: 'width / height', token: '--spacing-3', value: '12px' },
|
|
26
|
+
],
|
|
27
|
+
时间标签: [
|
|
28
|
+
{ label: '颜色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary' },
|
|
29
|
+
{ label: '字号', cssProp: 'font-size', token: '--text-xs', value: '12px' },
|
|
30
|
+
{ label: '行高', cssProp: 'line-height', token: '--leading-4', value: '16px' },
|
|
31
|
+
{ label: '上方气泡间距', cssProp: 'margin-bottom', token: '--spacing-0_5', value: '2px' },
|
|
32
|
+
{ label: '日期时间间距', cssProp: 'column-gap', token: '--spacing-1', value: '4px' },
|
|
33
|
+
{ label: '侧边气泡间距', cssProp: 'margin-inline', token: '--spacing-2', value: '8px' },
|
|
34
|
+
],
|
|
35
|
+
时间浮层: [
|
|
36
|
+
{ label: '背景色', cssProp: 'background', token: '--color-blueGrey-800', value: '#344054' },
|
|
37
|
+
{ label: '文字色', cssProp: 'color', token: '--color-white', value: '#FFFFFF' },
|
|
38
|
+
{ label: '左右内距', cssProp: 'padding-inline', token: '--spacing-3', value: '12px' },
|
|
39
|
+
{ label: '上下内距', cssProp: 'padding-block', token: '--spacing-2', value: '8px' },
|
|
40
|
+
{ label: '圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px' },
|
|
41
|
+
{ label: '浮层间距', cssProp: 'margin-bottom', token: '--spacing-1', value: '4px' },
|
|
42
|
+
{ label: '投影', cssProp: 'box-shadow', token: '--shadow-lg', value: '0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)' },
|
|
43
|
+
],
|
|
44
|
+
布局: [
|
|
45
|
+
{ label: '头像槽宽', cssProp: 'grid-template-columns', token: '--spacing-6', value: '24px' },
|
|
46
|
+
{ label: '头像间距', cssProp: 'column-gap', token: '--spacing-2', value: '8px' },
|
|
47
|
+
{ label: '组间距', cssProp: 'row-gap', token: '--spacing-1', value: '4px' },
|
|
48
|
+
],
|
|
49
|
+
尺寸: [
|
|
50
|
+
{ label: 'Default 左右内距', cssProp: 'padding-inline', token: '--spacing-3', value: '12px' },
|
|
51
|
+
{ label: 'Default 上下内距', cssProp: 'padding-block', token: '--spacing-2', value: '8px' },
|
|
52
|
+
{ label: 'Large 左右内距', cssProp: 'padding-inline', token: '--spacing-4', value: '16px' },
|
|
53
|
+
{ label: 'Large 上下内距', cssProp: 'padding-block', token: '--spacing-3', value: '12px' },
|
|
54
|
+
{ label: '消息列最大宽度', cssProp: 'max-width', token: '--size-chat-bubble-max', value: '500px' },
|
|
55
|
+
],
|
|
56
|
+
引用组件: [
|
|
57
|
+
{ label: '头像', cssProp: 'component', value: 'Avatar / size=xs / type=image|robot(avatarType=none 时隐藏)' },
|
|
58
|
+
{ label: '回执图标', cssProp: 'component', value: 'Icon / check-circle-stroked / size=xs' },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import ChatBubble from './ChatBubble';
|
|
2
|
+
import { getTeamMemberByName } from '../teamMembers';
|
|
3
|
+
|
|
4
|
+
const CANVAS = 'flex w-full justify-center';
|
|
5
|
+
const THREAD = 'w-full max-w-[456px]';
|
|
6
|
+
|
|
7
|
+
const PREVIEW_MESSAGES = {
|
|
8
|
+
incoming: {
|
|
9
|
+
single: ['为什么我无法设置点赞列表查看权限'],
|
|
10
|
+
trailing: ['那怎么看自己的直播回放?'],
|
|
11
|
+
},
|
|
12
|
+
outgoing: {
|
|
13
|
+
single: ['您好,因产品功能更新,“作品点赞信息”设置开关正逐步下线中'],
|
|
14
|
+
multiple: [
|
|
15
|
+
'您好,因产品功能更新,“作品点赞信息”设置开关正逐步下线中',
|
|
16
|
+
'目前不支持设置“作品点赞信息”权限。该功能下线后,您发布的作品的点赞列表仅对您自己可见。',
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const PREVIEW_TIMESTAMPS = {
|
|
22
|
+
incoming: {
|
|
23
|
+
single: ['2026-02-26 10:24:14'],
|
|
24
|
+
trailing: ['2026-02-26 10:26:08'],
|
|
25
|
+
},
|
|
26
|
+
outgoing: {
|
|
27
|
+
single: ['2026-02-26 10:25:03'],
|
|
28
|
+
multiple: ['2026-02-26 10:25:03', '2026-02-26 10:26:44'],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const RECEIPT_OPTIONS = new Set(['hidden', 'read']);
|
|
33
|
+
const PREVIEW_USER_AVATAR = getTeamMemberByName('程程murphy');
|
|
34
|
+
const PREVIEW_AGENT_AVATAR = getTeamMemberByName('段然');
|
|
35
|
+
|
|
36
|
+
function getPreviewAvatarSrc(variant, avatarType) {
|
|
37
|
+
if (avatarType !== 'image') return null;
|
|
38
|
+
return variant === 'outgoing'
|
|
39
|
+
? PREVIEW_AGENT_AVATAR?.avatarSrc
|
|
40
|
+
: PREVIEW_USER_AVATAR?.avatarSrc;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default function ChatBubblePreview({
|
|
44
|
+
previewVariant = 'incoming',
|
|
45
|
+
layout = 'single',
|
|
46
|
+
avatarType = 'image',
|
|
47
|
+
size = 'default',
|
|
48
|
+
incomingTone = 'default',
|
|
49
|
+
outgoingTone = 'white',
|
|
50
|
+
tone,
|
|
51
|
+
receipt = 'hidden',
|
|
52
|
+
timestamps = null,
|
|
53
|
+
...props
|
|
54
|
+
}) {
|
|
55
|
+
const isBoth = previewVariant === 'both';
|
|
56
|
+
const resolvedReceipt = RECEIPT_OPTIONS.has(receipt) ? receipt : 'hidden';
|
|
57
|
+
const resolvedPreviewTimestamps = Array.isArray(timestamps) ? timestamps : PREVIEW_TIMESTAMPS[previewVariant]?.[layout] || PREVIEW_TIMESTAMPS.incoming.single;
|
|
58
|
+
const mergedOutgoingTone = outgoingTone ?? tone ?? 'white';
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className={CANVAS}>
|
|
62
|
+
<div className={THREAD}>
|
|
63
|
+
{isBoth ? (
|
|
64
|
+
<div className="flex w-full flex-col gap-5">
|
|
65
|
+
<ChatBubble
|
|
66
|
+
{...props}
|
|
67
|
+
variant="incoming"
|
|
68
|
+
layout="single"
|
|
69
|
+
avatarType={avatarType}
|
|
70
|
+
size={size}
|
|
71
|
+
incomingTone={incomingTone}
|
|
72
|
+
outgoingTone={mergedOutgoingTone}
|
|
73
|
+
tone={mergedOutgoingTone}
|
|
74
|
+
avatarSrc={getPreviewAvatarSrc('incoming', avatarType)}
|
|
75
|
+
avatarAlt="用户头像"
|
|
76
|
+
messages={PREVIEW_MESSAGES.incoming.single}
|
|
77
|
+
timestamps={PREVIEW_TIMESTAMPS.incoming.single}
|
|
78
|
+
/>
|
|
79
|
+
<ChatBubble
|
|
80
|
+
{...props}
|
|
81
|
+
variant="outgoing"
|
|
82
|
+
layout={layout}
|
|
83
|
+
avatarType={avatarType}
|
|
84
|
+
size={size}
|
|
85
|
+
incomingTone={incomingTone}
|
|
86
|
+
outgoingTone={mergedOutgoingTone}
|
|
87
|
+
tone={mergedOutgoingTone}
|
|
88
|
+
avatarSrc={getPreviewAvatarSrc('outgoing', avatarType)}
|
|
89
|
+
avatarAlt="客服头像"
|
|
90
|
+
receipt={resolvedReceipt}
|
|
91
|
+
messages={layout === 'multiple' ? PREVIEW_MESSAGES.outgoing.multiple : PREVIEW_MESSAGES.outgoing.single}
|
|
92
|
+
timestamps={layout === 'multiple' ? PREVIEW_TIMESTAMPS.outgoing.multiple : PREVIEW_TIMESTAMPS.outgoing.single}
|
|
93
|
+
/>
|
|
94
|
+
{layout === 'multiple' ? (
|
|
95
|
+
<ChatBubble
|
|
96
|
+
{...props}
|
|
97
|
+
variant="incoming"
|
|
98
|
+
layout="single"
|
|
99
|
+
avatarType={avatarType}
|
|
100
|
+
size={size}
|
|
101
|
+
incomingTone={incomingTone}
|
|
102
|
+
outgoingTone={mergedOutgoingTone}
|
|
103
|
+
tone={mergedOutgoingTone}
|
|
104
|
+
avatarSrc={getPreviewAvatarSrc('incoming', avatarType)}
|
|
105
|
+
avatarAlt="用户头像"
|
|
106
|
+
messages={PREVIEW_MESSAGES.incoming.trailing}
|
|
107
|
+
timestamps={PREVIEW_TIMESTAMPS.incoming.trailing}
|
|
108
|
+
/>
|
|
109
|
+
) : null}
|
|
110
|
+
</div>
|
|
111
|
+
) : (
|
|
112
|
+
<ChatBubble
|
|
113
|
+
{...props}
|
|
114
|
+
variant={previewVariant}
|
|
115
|
+
layout={layout}
|
|
116
|
+
avatarType={avatarType}
|
|
117
|
+
size={size}
|
|
118
|
+
incomingTone={incomingTone}
|
|
119
|
+
outgoingTone={mergedOutgoingTone}
|
|
120
|
+
tone={mergedOutgoingTone}
|
|
121
|
+
avatarSrc={getPreviewAvatarSrc(previewVariant, avatarType)}
|
|
122
|
+
receipt={resolvedReceipt}
|
|
123
|
+
timestamps={resolvedPreviewTimestamps}
|
|
124
|
+
/>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|