@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,1338 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import Avatar from './Avatar';
|
|
3
|
+
import Button from './Button';
|
|
4
|
+
import { Checkbox } from './Checkbox';
|
|
5
|
+
import Icon from './Icon';
|
|
6
|
+
import Select from './Select';
|
|
7
|
+
import Switch from './Switch';
|
|
8
|
+
import Tag from './Tag';
|
|
9
|
+
import Tooltip from './Tooltip';
|
|
10
|
+
import { getTeamAvatarBySeed, getTeamMemberByIndex } from '../teamMembers';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Table — B端业务列表表格(Tailwind 内联)
|
|
14
|
+
*
|
|
15
|
+
* 对齐 Figma 业务表格样式,覆盖标题、标签、描述、创建人、创建时间、操作区和分页区。
|
|
16
|
+
* 组件默认沉淀业务列表场景,同时保留列级 render 扩展能力,适合后台内容管理、策略管理、知识库列表等场景。
|
|
17
|
+
* avatar / avatarText 单元格未传 avatarSrc 时,会按 name/description 从本地成员头像素材中取默认图片。
|
|
18
|
+
*
|
|
19
|
+
* @prop {Array<object>} [columns=[]] — 列配置,支持 title / key / dataIndex / width / minWidth / align / type / render
|
|
20
|
+
* @prop {Array<object>} [dataSource=[]] — 行数据
|
|
21
|
+
* @prop {'table'|'card-form'} [variant='table'] — 展示类型:table 为标准表格;card-form 为卡片型表单
|
|
22
|
+
* @prop {string|((record: object, index: number) => string|number)} [rowKey='id'] — 行 key
|
|
23
|
+
* @prop {object|null} [pagination=null] — 分页配置:current / pageSize / total / pageSizeOptions / summaryText / displayPages / pageSizeLabel
|
|
24
|
+
* @prop {Array<string|number>} [expandedRowKeys] — 卡片型表单受控展开项
|
|
25
|
+
* @prop {Array<string|number>} [defaultExpandedRowKeys=[]] — 卡片型表单默认展开项
|
|
26
|
+
* @prop {(keys: Array<string|number>) => void} [onExpandedRowKeysChange=null] — 卡片型表单展开变化回调
|
|
27
|
+
* @prop {(record: object, context: object) => void} [onView=null] — 卡片型表单查看操作
|
|
28
|
+
* @prop {(record: object, context: object) => void} [onMore=null] — 卡片型表单更多操作
|
|
29
|
+
* @prop {(page: number) => void} [onPageChange=null] — 页码变更回调
|
|
30
|
+
* @prop {(pageSize: number) => void} [onPageSizeChange=null] — 每页条数变更回调
|
|
31
|
+
* @prop {string} [emptyText='暂无数据'] — 空态文案
|
|
32
|
+
* @prop {string} [className=''] — 附加类名
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/* ── 默认列宽(对齐 Figma) ── */
|
|
36
|
+
const TYPE_ALIAS_MAP = {
|
|
37
|
+
title: 'link',
|
|
38
|
+
tags: 'tag',
|
|
39
|
+
description: 'text',
|
|
40
|
+
creator: 'avatar',
|
|
41
|
+
datetime: 'datetime',
|
|
42
|
+
actions: 'actions',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const DEFAULT_COLUMN_WIDTH = {
|
|
46
|
+
text: 162,
|
|
47
|
+
link: 188,
|
|
48
|
+
iconText: 176,
|
|
49
|
+
textButton: 186,
|
|
50
|
+
status: 130,
|
|
51
|
+
tag: 112,
|
|
52
|
+
switch: 108,
|
|
53
|
+
checkbox: 72,
|
|
54
|
+
avatar: 168,
|
|
55
|
+
avatarText: 220,
|
|
56
|
+
linkDescription: 220,
|
|
57
|
+
image: 132,
|
|
58
|
+
progress: 164,
|
|
59
|
+
datetime: 180,
|
|
60
|
+
actions: 152,
|
|
61
|
+
dragHandle: 68,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const MIN_COLUMN_WIDTH = {
|
|
65
|
+
text: 116,
|
|
66
|
+
link: 132,
|
|
67
|
+
iconText: 144,
|
|
68
|
+
textButton: 148,
|
|
69
|
+
status: 112,
|
|
70
|
+
tag: 96,
|
|
71
|
+
switch: 96,
|
|
72
|
+
checkbox: 56,
|
|
73
|
+
avatar: 132,
|
|
74
|
+
avatarText: 168,
|
|
75
|
+
linkDescription: 168,
|
|
76
|
+
image: 112,
|
|
77
|
+
progress: 132,
|
|
78
|
+
datetime: 156,
|
|
79
|
+
actions: 132,
|
|
80
|
+
dragHandle: 52,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const CELL_SIZE_CLASS = {
|
|
84
|
+
default: 'px-3 py-3',
|
|
85
|
+
middle: 'px-3 py-2',
|
|
86
|
+
small: 'px-2 py-1',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const HEADER_SIZE_CLASS = {
|
|
90
|
+
default: 'px-3 py-3',
|
|
91
|
+
middle: 'px-3 py-2',
|
|
92
|
+
small: 'px-2 py-1',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const HEADER_MIN_HEIGHT = {
|
|
96
|
+
default: 44,
|
|
97
|
+
middle: 36,
|
|
98
|
+
small: 28,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const TYPE_MIN_HEIGHT = {
|
|
102
|
+
text: { default: 44, middle: 36, small: 28 },
|
|
103
|
+
link: { default: 44, middle: 36, small: 28 },
|
|
104
|
+
iconText: { default: 44, middle: 36, small: 28 },
|
|
105
|
+
textButton: { default: 44, middle: 36, small: 28 },
|
|
106
|
+
status: { default: 44, middle: 36, small: 28 },
|
|
107
|
+
tag: { default: 44, middle: 36, small: 28 },
|
|
108
|
+
switch: { default: 48, middle: 40, small: 32 },
|
|
109
|
+
checkbox: { default: 44, middle: 36, small: 28 },
|
|
110
|
+
avatar: { default: 44, middle: 36, small: 28 },
|
|
111
|
+
avatarText: { default: 64, middle: 56, small: 48 },
|
|
112
|
+
linkDescription: { default: 64, middle: 56, small: 48 },
|
|
113
|
+
image: { default: 72, middle: 64, small: 56 },
|
|
114
|
+
progress: { default: 44, middle: 36, small: 28 },
|
|
115
|
+
datetime: { default: 44, middle: 36, small: 28 },
|
|
116
|
+
actions: { default: 48, middle: 40, small: 32 },
|
|
117
|
+
dragHandle: { default: 44, middle: 36, small: 28 },
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/* ── 容器 / 表格 ──
|
|
121
|
+
* Table 自身不带白色背景、不带四周 padding、不带圆角;
|
|
122
|
+
* 由外部容器(业务白卡 / 预览容器)决定外观与留白。
|
|
123
|
+
* 内部行(HEADER_ROW / BODY_ROW)以及 sticky 列仍保留 bg-surface,
|
|
124
|
+
* 用于保证 sticky 表头与固定列在滚动时不透明、表格行有清晰的可读底色。
|
|
125
|
+
*/
|
|
126
|
+
const WRAPPER = [
|
|
127
|
+
'tfds-table',
|
|
128
|
+
'flex h-full min-h-0 w-full flex-col',
|
|
129
|
+
'[font-family:Inter,"PingFang_SC","PingFang_TC","Hiragino_Sans_GB","Microsoft_YaHei",sans-serif]',
|
|
130
|
+
].join(' ');
|
|
131
|
+
|
|
132
|
+
const TABLE_SHELL = [
|
|
133
|
+
'flex h-full min-h-0 w-full flex-1 flex-col',
|
|
134
|
+
'[font-family:Inter,"PingFang_SC","PingFang_TC","Hiragino_Sans_GB","Microsoft_YaHei",sans-serif]',
|
|
135
|
+
].join(' ');
|
|
136
|
+
|
|
137
|
+
const TABLE = 'flex w-full min-w-0 flex-1 min-h-0 flex-col';
|
|
138
|
+
const TABLE_VIEWPORT = 'flex flex-col flex-1 min-h-0 w-full overflow-x-auto overflow-y-hidden';
|
|
139
|
+
const TABLE_CONTENT = 'flex w-full flex-1 min-h-0 flex-col';
|
|
140
|
+
const TABLE_BODY = 'flex-1 min-h-0 w-full overflow-y-auto';
|
|
141
|
+
|
|
142
|
+
/* ── 表头 / 单元格 ──
|
|
143
|
+
* Table 自身不带任何背景色,行/单元格全部透明,由外部容器决定底色。
|
|
144
|
+
* sticky 表头与固定列因此在滚动时会与内容色一致;如业务需要遮挡,
|
|
145
|
+
* 请在外层容器设置背景色(白卡 / 灰底 page 等)。
|
|
146
|
+
*/
|
|
147
|
+
const HEADER_ROW = 'grid w-full min-w-0';
|
|
148
|
+
const BODY_ROW = 'grid w-full min-w-0';
|
|
149
|
+
|
|
150
|
+
const HEADER_CELL = [
|
|
151
|
+
'flex w-full min-w-0 items-center justify-start gap-2 border-b-2 border-border-default text-left',
|
|
152
|
+
'text-left text-sm [font-weight:var(--font-semibold)] leading-5 text-foreground-muted',
|
|
153
|
+
].join(' ');
|
|
154
|
+
|
|
155
|
+
const BODY_CELL = [
|
|
156
|
+
'flex w-full min-w-0 items-center justify-start border-b border-border-default text-left',
|
|
157
|
+
'text-left text-sm font-normal leading-5 text-foreground',
|
|
158
|
+
].join(' ');
|
|
159
|
+
|
|
160
|
+
/* ── 通用文本截断 ── */
|
|
161
|
+
const TRUNCATE_TEXT = 'block w-full min-w-0 truncate text-left';
|
|
162
|
+
const SECONDARY_TEXT = 'block w-full min-w-0 truncate text-left text-xs leading-4 text-foreground-muted';
|
|
163
|
+
const HEADER_TEXT = 'block w-full min-w-0 truncate text-left text-sm [font-weight:var(--font-semibold)] leading-5 text-foreground-muted';
|
|
164
|
+
const PLAIN_TEXT = 'block w-full min-w-0 truncate text-left text-sm font-normal leading-5 text-foreground';
|
|
165
|
+
const INLINE_PLAIN_TEXT = 'block min-w-0 truncate text-left text-sm font-normal leading-5 text-foreground';
|
|
166
|
+
const EMPHASIS_TEXT = 'block w-full min-w-0 truncate text-left [font-weight:var(--font-semibold)]';
|
|
167
|
+
const ACTION_FULL_TEXT = 'block whitespace-nowrap [font-weight:var(--font-semibold)]';
|
|
168
|
+
const ELLIPSIS_TOOLTIP_WRAP = 'block w-full min-w-0';
|
|
169
|
+
/* ── 标题列 ──
|
|
170
|
+
* link 列单独跟随全局 text-link token(brand 色阶)。
|
|
171
|
+
* 用 !text-brand-* 覆盖 Button variant 自带的 text-brand-500 / hover / active。
|
|
172
|
+
*/
|
|
173
|
+
const TEXT_BRAND_BUTTON_CLASS = [
|
|
174
|
+
'!h-auto !min-h-0 !w-full !max-w-full !min-w-0 !justify-start !gap-0 !border-transparent !p-0',
|
|
175
|
+
'!text-left !text-sm !leading-5',
|
|
176
|
+
'!text-brand-700 hover:!text-brand-800 active:!text-brand-900',
|
|
177
|
+
'[&_span]:truncate',
|
|
178
|
+
].join(' ');
|
|
179
|
+
const ACTION_TEXT_BUTTON_CLASS = [
|
|
180
|
+
'!h-auto !min-h-0 !justify-start !gap-0 !border-transparent !p-0',
|
|
181
|
+
'!text-left !text-sm !leading-5',
|
|
182
|
+
].join(' ');
|
|
183
|
+
const ICON_ONLY_BUTTON_RESET = '!gap-0 !p-0';
|
|
184
|
+
|
|
185
|
+
/* ── 创建人列 ── */
|
|
186
|
+
const CREATOR_WRAP = 'flex w-full min-w-0 items-center justify-start gap-2 text-left';
|
|
187
|
+
const CREATOR_NAME = 'flex-1 min-w-0 truncate text-left text-sm font-normal leading-5 text-foreground';
|
|
188
|
+
const AVATAR_TEXT_WRAP = 'flex min-w-0 flex-1 flex-col items-start justify-center gap-1 text-left';
|
|
189
|
+
|
|
190
|
+
/* ── 操作区 ── */
|
|
191
|
+
const ACTION_WRAP = 'flex w-full min-w-0 items-center justify-start gap-2 pr-3 text-left';
|
|
192
|
+
const MORE_ACTION_BUTTON_CLASS = [
|
|
193
|
+
// 44x44 hit target per design directive, keep icon at 16px
|
|
194
|
+
'!size-11 shrink-0 !rounded-md',
|
|
195
|
+
].join(' ');
|
|
196
|
+
const TEXT_BUTTON_ICON_CLASS = [
|
|
197
|
+
'!size-6 shrink-0 !rounded-md !border-transparent !p-0 !text-foreground',
|
|
198
|
+
'hover:!bg-fill hover:!text-foreground active:!bg-fill',
|
|
199
|
+
].join(' ');
|
|
200
|
+
const DRAG_HANDLE_BUTTON_CLASS = [
|
|
201
|
+
'!size-6 shrink-0 !rounded-md !border-transparent !p-0 !text-foreground-muted',
|
|
202
|
+
'hover:!bg-fill hover:!text-foreground active:!bg-fill',
|
|
203
|
+
].join(' ');
|
|
204
|
+
|
|
205
|
+
/* ── 分页 ── */
|
|
206
|
+
const FOOTER = 'flex shrink-0 flex-wrap items-center justify-between gap-4 px-0 pt-3 pb-0';
|
|
207
|
+
const FOOTER_SUMMARY = 'text-sm font-normal leading-5 text-foreground-muted';
|
|
208
|
+
const FOOTER_RIGHT = 'flex items-center gap-4';
|
|
209
|
+
const PAGE_GROUP = 'flex items-center px-1';
|
|
210
|
+
const PAGE_LIST = 'flex items-center gap-1';
|
|
211
|
+
|
|
212
|
+
const PAGE_ARROW_BUTTON_CLASS = [
|
|
213
|
+
'!size-9 !rounded-md !border-transparent !p-0 !text-foreground-muted',
|
|
214
|
+
'hover:!text-foreground disabled:!text-foreground-disabled',
|
|
215
|
+
].join(' ');
|
|
216
|
+
|
|
217
|
+
const PAGE_NUMBER_BUTTON_CLASS = [
|
|
218
|
+
'!size-8 !rounded-[6px] !border-transparent !p-0',
|
|
219
|
+
'!text-sm !font-normal !leading-5 !text-foreground',
|
|
220
|
+
'hover:!bg-blueGrey-50 focus-visible:!outline-none focus-visible:!bg-blueGrey-50',
|
|
221
|
+
].join(' ');
|
|
222
|
+
|
|
223
|
+
const PAGE_BUTTON_ACTIVE = '!bg-brand-50 ![font-weight:var(--font-semibold)] !text-brand-600 hover:!bg-brand-50 focus-visible:!bg-brand-50';
|
|
224
|
+
const PAGE_BUTTON_ACTIVE_STYLE = { color: 'var(--color-brand-600)' };
|
|
225
|
+
const PAGE_ELLIPSIS = 'inline-flex size-8 items-center justify-center text-sm font-normal leading-5 text-foreground';
|
|
226
|
+
|
|
227
|
+
/* ── 空态 ── */
|
|
228
|
+
const EMPTY_CELL = 'px-4 py-10 text-center text-sm font-normal leading-5 text-foreground-muted';
|
|
229
|
+
|
|
230
|
+
/* ── 卡片型表单 ── */
|
|
231
|
+
const CARD_FORM_BODY = 'flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto';
|
|
232
|
+
const CARD_FORM_ITEM = 'flex shrink-0 flex-col overflow-hidden rounded-xl border border-border-default bg-surface transition-shadow duration-150';
|
|
233
|
+
const CARD_FORM_PARENT = [
|
|
234
|
+
'flex items-center px-8 py-4 transition-colors duration-150',
|
|
235
|
+
'hover:bg-blueGrey-50',
|
|
236
|
+
].join(' ');
|
|
237
|
+
const CARD_FORM_PARENT_STATIC = 'flex items-center px-8 py-4';
|
|
238
|
+
const CARD_FORM_TOGGLE_WRAP = 'shrink-0 transition-transform duration-200';
|
|
239
|
+
const CARD_FORM_MAIN = 'ml-4 flex min-w-0 flex-1 flex-col gap-1.5 overflow-hidden';
|
|
240
|
+
const CARD_FORM_TITLE = 'block w-full min-w-0 truncate text-base [font-weight:var(--font-semibold)] leading-6 text-foreground';
|
|
241
|
+
const CARD_FORM_SUBTITLE = 'block w-full min-w-0 truncate text-xs leading-4 text-foreground-muted';
|
|
242
|
+
const CARD_FORM_ASIDE = 'ml-4 flex shrink-0 items-center gap-8';
|
|
243
|
+
const CARD_FORM_CHILDREN = 'flex flex-col gap-2 px-4 pb-4';
|
|
244
|
+
const CARD_FORM_VERSION_ROW = [
|
|
245
|
+
'flex items-center rounded-lg bg-blueGrey-100 p-4 transition-colors duration-150',
|
|
246
|
+
'hover:bg-blueGrey-200',
|
|
247
|
+
].join(' ');
|
|
248
|
+
const CARD_FORM_VERSION_BADGE = 'inline-flex size-6 shrink-0 items-center justify-center rounded-md bg-blueGrey-900 text-[11px] [font-weight:var(--font-bold)] leading-none text-white';
|
|
249
|
+
const CARD_FORM_VERSION_TITLE = 'ml-4 min-w-0 flex-[0_1_180px]';
|
|
250
|
+
const CARD_FORM_VERSION_TITLE_TEXT = 'block w-full min-w-0 truncate text-xs leading-5 text-foreground-secondary';
|
|
251
|
+
const CARD_FORM_VERSION_STATUS = 'ml-3 shrink-0';
|
|
252
|
+
const CARD_FORM_METRICS = 'ml-5 mr-8 min-w-0 flex-1 overflow-hidden';
|
|
253
|
+
const CARD_FORM_METRICS_LINE = 'block min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-right text-xs leading-4';
|
|
254
|
+
const CARD_FORM_RIGHT = 'ml-4 flex shrink-0 items-center gap-8';
|
|
255
|
+
const CARD_FORM_USER_DATE = 'flex items-center gap-1.5';
|
|
256
|
+
const CARD_FORM_DATE = 'block min-w-0 truncate whitespace-nowrap text-xs leading-4 text-foreground-muted';
|
|
257
|
+
const CARD_FORM_ACTIONS = 'flex items-center gap-2';
|
|
258
|
+
|
|
259
|
+
const CARD_FORM_STATUS_VARIANT = {
|
|
260
|
+
线上生效: 'green',
|
|
261
|
+
暂未生效: 'grey',
|
|
262
|
+
草稿中: 'grey',
|
|
263
|
+
实验中: 'blue',
|
|
264
|
+
已发布: 'green',
|
|
265
|
+
已下线: 'grey',
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
function isFirstColumnFixed(mode) {
|
|
269
|
+
return mode === 'first' || mode === 'both';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function isLastColumnFixed(mode) {
|
|
273
|
+
return mode === 'last' || mode === 'both';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function normalizeColumnWidth(column) {
|
|
277
|
+
if (column.width != null) return column.width;
|
|
278
|
+
const type = normalizeColumnType(column.type);
|
|
279
|
+
return DEFAULT_COLUMN_WIDTH[type] ?? undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function normalizeColumnType(type) {
|
|
283
|
+
if (!type) return 'text';
|
|
284
|
+
return TYPE_ALIAS_MAP[type] || type;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function normalizeCellSize(cellSize) {
|
|
288
|
+
if (cellSize === 'middle' || cellSize === 'small') return cellSize;
|
|
289
|
+
return 'default';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function getTypeMinHeight(type, sizeKey) {
|
|
293
|
+
return TYPE_MIN_HEIGHT[type]?.[sizeKey] ?? TYPE_MIN_HEIGHT.text[sizeKey];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function resolveColumnMetrics(column) {
|
|
297
|
+
const type = normalizeColumnType(column.type);
|
|
298
|
+
const baseWidth = Number(column.width ?? normalizeColumnWidth(column) ?? DEFAULT_COLUMN_WIDTH[type] ?? 1);
|
|
299
|
+
const minWidth = Number(column.minWidth ?? MIN_COLUMN_WIDTH[type] ?? 96);
|
|
300
|
+
return {
|
|
301
|
+
baseWidth,
|
|
302
|
+
minWidth: Math.max(minWidth, 0),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function resolveColumnTrack(column) {
|
|
307
|
+
const { baseWidth, minWidth } = resolveColumnMetrics(column);
|
|
308
|
+
return `minmax(${minWidth}px, ${baseWidth}fr)`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function buildColumnGridTemplate(columns) {
|
|
312
|
+
if (!columns.length) return 'minmax(0, 1fr)';
|
|
313
|
+
return columns.map((column) => resolveColumnTrack(column)).join(' ');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function getColumnTemplateMinWidth(columns) {
|
|
317
|
+
if (!columns.length) return 0;
|
|
318
|
+
return columns.reduce((sum, column) => sum + resolveColumnMetrics(column).minWidth, 0);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function buildCellClass(baseClass, sizeKey, extraClass = '') {
|
|
322
|
+
return [
|
|
323
|
+
baseClass,
|
|
324
|
+
CELL_SIZE_CLASS[sizeKey] || CELL_SIZE_CLASS.default,
|
|
325
|
+
extraClass,
|
|
326
|
+
].filter(Boolean).join(' ');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function getPinnedCellStyle({
|
|
330
|
+
columnIndex,
|
|
331
|
+
columnsLength,
|
|
332
|
+
align,
|
|
333
|
+
minHeight,
|
|
334
|
+
fixedColumnsMode,
|
|
335
|
+
isHeader = false,
|
|
336
|
+
}) {
|
|
337
|
+
const style = {
|
|
338
|
+
minHeight: `${minHeight}px`,
|
|
339
|
+
textAlign: align || 'left',
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const fixFirst = isFirstColumnFixed(fixedColumnsMode) && columnIndex === 0;
|
|
343
|
+
const fixLast = isLastColumnFixed(fixedColumnsMode) && columnIndex === columnsLength - 1;
|
|
344
|
+
|
|
345
|
+
if (!fixFirst && !fixLast) {
|
|
346
|
+
return style;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
style.position = 'sticky';
|
|
350
|
+
style.background = 'inherit';
|
|
351
|
+
style.zIndex = isHeader ? 30 : 20;
|
|
352
|
+
|
|
353
|
+
if (fixFirst) {
|
|
354
|
+
style.left = 0;
|
|
355
|
+
style.boxShadow = '2px 0 0 rgba(45, 66, 107, 0.06)';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (fixLast) {
|
|
359
|
+
style.right = 0;
|
|
360
|
+
style.boxShadow = '-2px 0 0 rgba(45, 66, 107, 0.06)';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return style;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function getRowKeyValue(rowKey, record, index) {
|
|
367
|
+
if (typeof rowKey === 'function') {
|
|
368
|
+
return rowKey(record, index);
|
|
369
|
+
}
|
|
370
|
+
if (typeof rowKey === 'string' && record?.[rowKey] != null) {
|
|
371
|
+
return record[rowKey];
|
|
372
|
+
}
|
|
373
|
+
return `${index}`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function resolveTextValue(value) {
|
|
377
|
+
if (value == null) return '';
|
|
378
|
+
if (typeof value === 'object' && value.label != null) return String(value.label);
|
|
379
|
+
return String(value);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function EllipsisTooltipText({
|
|
383
|
+
value,
|
|
384
|
+
className = TRUNCATE_TEXT,
|
|
385
|
+
wrapperClassName = 'w-full min-w-0',
|
|
386
|
+
}) {
|
|
387
|
+
const text = resolveTextValue(value);
|
|
388
|
+
const textRef = useRef(null);
|
|
389
|
+
const [isOverflowed, setIsOverflowed] = useState(false);
|
|
390
|
+
|
|
391
|
+
const checkOverflow = useCallback(() => {
|
|
392
|
+
const node = textRef.current;
|
|
393
|
+
if (!node) return;
|
|
394
|
+
setIsOverflowed(node.scrollWidth > node.clientWidth + 1);
|
|
395
|
+
}, []);
|
|
396
|
+
|
|
397
|
+
useEffect(() => {
|
|
398
|
+
const node = textRef.current;
|
|
399
|
+
if (!node) return undefined;
|
|
400
|
+
|
|
401
|
+
checkOverflow();
|
|
402
|
+
|
|
403
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const observer = new ResizeObserver(() => {
|
|
408
|
+
checkOverflow();
|
|
409
|
+
});
|
|
410
|
+
observer.observe(node);
|
|
411
|
+
|
|
412
|
+
return () => observer.disconnect();
|
|
413
|
+
}, [checkOverflow, text]);
|
|
414
|
+
|
|
415
|
+
const content = (
|
|
416
|
+
<span ref={textRef} className={className}>
|
|
417
|
+
{text}
|
|
418
|
+
</span>
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const wrapperClass = [ELLIPSIS_TOOLTIP_WRAP, wrapperClassName].filter(Boolean).join(' ');
|
|
422
|
+
|
|
423
|
+
if (!isOverflowed || !text) {
|
|
424
|
+
return (
|
|
425
|
+
<span
|
|
426
|
+
className={wrapperClass}
|
|
427
|
+
onMouseEnter={checkOverflow}
|
|
428
|
+
>
|
|
429
|
+
{content}
|
|
430
|
+
</span>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<Tooltip content={text} placement="top" triggerClassName={wrapperClass}>
|
|
436
|
+
{content}
|
|
437
|
+
</Tooltip>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function renderSingleLineText(value, className = TRUNCATE_TEXT, wrapperClassName = 'w-full min-w-0') {
|
|
442
|
+
return (
|
|
443
|
+
<EllipsisTooltipText
|
|
444
|
+
value={value}
|
|
445
|
+
className={className}
|
|
446
|
+
wrapperClassName={wrapperClassName}
|
|
447
|
+
/>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function buildPageItems(current, totalPages) {
|
|
452
|
+
if (totalPages <= 7) {
|
|
453
|
+
return Array.from({ length: totalPages }, (_, index) => index + 1);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (current <= 3) {
|
|
457
|
+
return [1, 2, 3, 'ellipsis', totalPages - 2, totalPages - 1, totalPages];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (current >= totalPages - 2) {
|
|
461
|
+
return [1, 2, 3, 'ellipsis', totalPages - 2, totalPages - 1, totalPages];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return [1, 'ellipsis', current - 1, current, current + 1, 'ellipsis', totalPages];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function defaultSummaryText(current, pageSize, total) {
|
|
468
|
+
if (!total) {
|
|
469
|
+
return '暂无数据';
|
|
470
|
+
}
|
|
471
|
+
const start = (current - 1) * pageSize + 1;
|
|
472
|
+
const end = Math.min(current * pageSize, total);
|
|
473
|
+
return `显示第 ${start} 条-第 ${end} 条,共 ${total} 条`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function shouldSliceDataSource(pagination, dataSourceLength) {
|
|
477
|
+
if (!pagination) return false;
|
|
478
|
+
if (pagination.clientSide === false) return false;
|
|
479
|
+
if (pagination.clientSide === true) return true;
|
|
480
|
+
return pagination.total == null || Number(pagination.total) === dataSourceLength;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function renderTitleValue(value) {
|
|
484
|
+
const label = resolveTextValue(value);
|
|
485
|
+
const href = typeof value === 'object' ? value?.href : undefined;
|
|
486
|
+
const onClick = typeof value === 'object' ? value?.onClick : undefined;
|
|
487
|
+
const labelNode = renderSingleLineText(label, EMPHASIS_TEXT);
|
|
488
|
+
|
|
489
|
+
if (href) {
|
|
490
|
+
return (
|
|
491
|
+
<Button as="a" href={href} variant="text-brand" size="sm" className={TEXT_BRAND_BUTTON_CLASS}>
|
|
492
|
+
{labelNode}
|
|
493
|
+
</Button>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return (
|
|
498
|
+
<Button type="button" variant="text-brand" size="sm" className={TEXT_BRAND_BUTTON_CLASS} onClick={onClick}>
|
|
499
|
+
{labelNode}
|
|
500
|
+
</Button>
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function renderTagsValue(value) {
|
|
505
|
+
const items = Array.isArray(value) ? value : value ? [value] : [];
|
|
506
|
+
|
|
507
|
+
if (items.length === 0) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const item = items[0];
|
|
512
|
+
const label = typeof item === 'object' ? item.label : item;
|
|
513
|
+
const variant = typeof item === 'object' ? item.variant || 'white' : 'white';
|
|
514
|
+
|
|
515
|
+
return (
|
|
516
|
+
<div className="flex w-full min-w-0 items-center justify-start text-left">
|
|
517
|
+
<Tag
|
|
518
|
+
variant={variant}
|
|
519
|
+
size="m"
|
|
520
|
+
radius="md"
|
|
521
|
+
>
|
|
522
|
+
{label}
|
|
523
|
+
</Tag>
|
|
524
|
+
</div>
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function renderAvatarValue(value, cellSize) {
|
|
529
|
+
if (!value) return null;
|
|
530
|
+
|
|
531
|
+
const shouldDefaultToImage = !value.avatarType || value.avatarType === 'image';
|
|
532
|
+
const resolvedAvatarSrc = value.avatarSrc || (shouldDefaultToImage ? getTeamAvatarBySeed(value.name || value.avatarAlt || value.avatarLabel) : null);
|
|
533
|
+
const avatarType = value.avatarType || (resolvedAvatarSrc ? 'image' : 'fallback');
|
|
534
|
+
const avatarLabel = value.avatarLabel || value.name?.slice(0, 2) || 'AI';
|
|
535
|
+
const avatarSize = cellSize === 'small' ? 'xs' : 'xs';
|
|
536
|
+
|
|
537
|
+
return (
|
|
538
|
+
<div className={CREATOR_WRAP}>
|
|
539
|
+
<Avatar
|
|
540
|
+
size={avatarSize}
|
|
541
|
+
type={avatarType}
|
|
542
|
+
src={avatarType === 'image' ? resolvedAvatarSrc : null}
|
|
543
|
+
alt={value.avatarAlt || `${value.name || '创建人'}头像`}
|
|
544
|
+
label={avatarLabel}
|
|
545
|
+
/>
|
|
546
|
+
{renderSingleLineText(value.name, CREATOR_NAME)}
|
|
547
|
+
</div>
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function renderAvatarTextValue(value) {
|
|
552
|
+
if (!value) return null;
|
|
553
|
+
|
|
554
|
+
const shouldDefaultToImage = !value.avatarType || value.avatarType === 'image';
|
|
555
|
+
const resolvedAvatarSrc = value.avatarSrc || (shouldDefaultToImage ? getTeamAvatarBySeed(value.name || value.description || value.avatarAlt || value.avatarLabel) : null);
|
|
556
|
+
const avatarType = value.avatarType || (resolvedAvatarSrc ? 'image' : 'fallback');
|
|
557
|
+
const avatarLabel = value.avatarLabel || value.name?.slice(0, 2) || 'AI';
|
|
558
|
+
|
|
559
|
+
return (
|
|
560
|
+
<div className={CREATOR_WRAP}>
|
|
561
|
+
<Avatar
|
|
562
|
+
size="xs"
|
|
563
|
+
type={avatarType}
|
|
564
|
+
src={avatarType === 'image' ? resolvedAvatarSrc : null}
|
|
565
|
+
alt={value.avatarAlt || `${value.name || '创建人'}头像`}
|
|
566
|
+
label={avatarLabel}
|
|
567
|
+
/>
|
|
568
|
+
<div className={AVATAR_TEXT_WRAP}>
|
|
569
|
+
{renderSingleLineText(value.name, 'block w-full min-w-0 truncate text-left text-sm font-normal leading-5 text-foreground')}
|
|
570
|
+
{renderSingleLineText(value.description, SECONDARY_TEXT)}
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function renderDateTimeValue(value) {
|
|
577
|
+
return renderSingleLineText(
|
|
578
|
+
value,
|
|
579
|
+
'block w-full min-w-0 truncate text-left text-sm font-normal leading-5 text-foreground [font-family:Inter,"PingFang_SC","PingFang_TC","Hiragino_Sans_GB","Microsoft_YaHei",sans-serif]',
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function renderIconTextValue(value) {
|
|
584
|
+
const label = typeof value === 'object' ? value.label : value;
|
|
585
|
+
const iconName = typeof value === 'object' ? value.iconName || 'award-01-stroked' : 'award-01-stroked';
|
|
586
|
+
return (
|
|
587
|
+
<div className="flex w-full min-w-0 items-center justify-start gap-1 text-left">
|
|
588
|
+
<span className="shrink-0 text-foreground-muted">
|
|
589
|
+
<Icon name={iconName} size="sm" />
|
|
590
|
+
</span>
|
|
591
|
+
{renderSingleLineText(label, PLAIN_TEXT)}
|
|
592
|
+
</div>
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function renderTextButtonValue(value) {
|
|
597
|
+
const label = typeof value === 'object' ? value.label : value;
|
|
598
|
+
const iconName = typeof value === 'object' ? value.iconName || 'edit-04-stroked' : 'edit-04-stroked';
|
|
599
|
+
const onClick = typeof value === 'object' ? value.onClick : undefined;
|
|
600
|
+
return (
|
|
601
|
+
<div className="flex w-full min-w-0 items-center justify-start text-left">
|
|
602
|
+
{renderSingleLineText(label, INLINE_PLAIN_TEXT, 'min-w-0 flex-1')}
|
|
603
|
+
<Button
|
|
604
|
+
type="button"
|
|
605
|
+
variant="ghost-black"
|
|
606
|
+
size="sm"
|
|
607
|
+
iconOnly
|
|
608
|
+
icon={<Icon name={iconName} size="sm" />}
|
|
609
|
+
className={[ICON_ONLY_BUTTON_RESET, TEXT_BUTTON_ICON_CLASS, 'ml-[2px]'].join(' ')}
|
|
610
|
+
aria-label="单元格操作"
|
|
611
|
+
onClick={onClick}
|
|
612
|
+
/>
|
|
613
|
+
</div>
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function renderStatusValue(value) {
|
|
618
|
+
const label = typeof value === 'object' ? value.label : resolveTextValue(value);
|
|
619
|
+
const tone = typeof value === 'object' ? value.tone || 'success' : 'success';
|
|
620
|
+
const dotClass = {
|
|
621
|
+
success: 'bg-green-500',
|
|
622
|
+
warning: 'bg-orange-500',
|
|
623
|
+
danger: 'bg-red-500',
|
|
624
|
+
neutral: 'bg-blueGrey-400',
|
|
625
|
+
}[tone] || 'bg-brand-500';
|
|
626
|
+
|
|
627
|
+
return (
|
|
628
|
+
<div className="flex w-full min-w-0 items-center justify-start gap-2 text-left">
|
|
629
|
+
<span className={['size-2 shrink-0 rounded-full', dotClass].join(' ')} />
|
|
630
|
+
{renderSingleLineText(label, PLAIN_TEXT)}
|
|
631
|
+
</div>
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function renderSwitchValue(value) {
|
|
636
|
+
const checked = typeof value === 'object' ? Boolean(value.checked) : Boolean(value);
|
|
637
|
+
const onChange = typeof value === 'object' ? value.onChange : undefined;
|
|
638
|
+
return (
|
|
639
|
+
<div className="flex w-full min-w-0 items-center justify-start text-left">
|
|
640
|
+
<Switch checked={checked} onChange={onChange} />
|
|
641
|
+
</div>
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function renderCheckboxValue(value, cellSize) {
|
|
646
|
+
const checked = typeof value === 'object' ? Boolean(value.checked) : Boolean(value);
|
|
647
|
+
const onChange = typeof value === 'object' ? value.onChange : undefined;
|
|
648
|
+
return (
|
|
649
|
+
<div className="flex w-full min-w-0 items-center justify-start text-left">
|
|
650
|
+
<Checkbox checked={checked} size={cellSize === 'default' ? 'md' : 'sm'} onChange={onChange} />
|
|
651
|
+
</div>
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function renderLinkDescriptionValue(value) {
|
|
656
|
+
if (!value) return null;
|
|
657
|
+
return (
|
|
658
|
+
<div className="flex w-full min-w-0 flex-col items-start justify-center gap-1 text-left">
|
|
659
|
+
{renderTitleValue({ label: value.label, href: value.href, onClick: value.onClick })}
|
|
660
|
+
{renderSingleLineText(value.description, SECONDARY_TEXT)}
|
|
661
|
+
</div>
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function renderImageValue(value, cellSize) {
|
|
666
|
+
const alt = typeof value === 'object' ? value.alt || '表格图片' : '表格图片';
|
|
667
|
+
const avatarSize = {
|
|
668
|
+
default: 'm',
|
|
669
|
+
middle: 'default',
|
|
670
|
+
small: 's',
|
|
671
|
+
}[cellSize];
|
|
672
|
+
|
|
673
|
+
return (
|
|
674
|
+
<div className="flex w-full min-w-0 items-center justify-start text-left">
|
|
675
|
+
<Avatar
|
|
676
|
+
shape="rect"
|
|
677
|
+
type="robot"
|
|
678
|
+
size={avatarSize}
|
|
679
|
+
alt={alt}
|
|
680
|
+
/>
|
|
681
|
+
</div>
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function renderProgressValue(value) {
|
|
686
|
+
const percent = Math.max(0, Math.min(100, typeof value === 'object' ? Number(value.value ?? 0) : Number(value ?? 0)));
|
|
687
|
+
return (
|
|
688
|
+
<div className="flex w-full min-w-0 items-center justify-start text-left">
|
|
689
|
+
<div className="h-[10px] w-[70px] shrink-0 overflow-hidden rounded-full bg-fill">
|
|
690
|
+
<div
|
|
691
|
+
className="h-full rounded-full bg-[var(--color-data-0)]"
|
|
692
|
+
style={{ width: `${percent}%` }}
|
|
693
|
+
/>
|
|
694
|
+
</div>
|
|
695
|
+
<span className="shrink-0 pl-2 text-sm leading-[24px] tracking-[-0.14px] text-foreground">{percent}%</span>
|
|
696
|
+
</div>
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function renderActionsValue(value) {
|
|
701
|
+
const items = Array.isArray(value) ? value : [];
|
|
702
|
+
|
|
703
|
+
if (items.length === 0) {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return (
|
|
708
|
+
<div className={ACTION_WRAP}>
|
|
709
|
+
{items.map((action, index) => {
|
|
710
|
+
const key = action.key || `${action.label || action.iconName || 'action'}-${index}`;
|
|
711
|
+
|
|
712
|
+
if (action.kind === 'more') {
|
|
713
|
+
return (
|
|
714
|
+
<Button
|
|
715
|
+
key={key}
|
|
716
|
+
type="button"
|
|
717
|
+
variant="text-brand"
|
|
718
|
+
size="sm"
|
|
719
|
+
iconOnly
|
|
720
|
+
icon={<Icon name={action.iconName || 'dots-horizontal-stroked'} size="sm" />}
|
|
721
|
+
className={[ICON_ONLY_BUTTON_RESET, MORE_ACTION_BUTTON_CLASS].join(' ')}
|
|
722
|
+
onClick={action.onClick}
|
|
723
|
+
aria-label={action.ariaLabel || action.label || '更多操作'}
|
|
724
|
+
/>
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (action.href) {
|
|
729
|
+
const label = resolveTextValue(action.label);
|
|
730
|
+
return (
|
|
731
|
+
<Button key={key} as="a" href={action.href} variant="text-brand" size="sm" className={ACTION_TEXT_BUTTON_CLASS}>
|
|
732
|
+
<span className={ACTION_FULL_TEXT}>{label}</span>
|
|
733
|
+
</Button>
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const label = resolveTextValue(action.label);
|
|
738
|
+
return (
|
|
739
|
+
<Button key={key} type="button" variant="text-brand" size="sm" className={ACTION_TEXT_BUTTON_CLASS} onClick={action.onClick}>
|
|
740
|
+
<span className={ACTION_FULL_TEXT}>{label}</span>
|
|
741
|
+
</Button>
|
|
742
|
+
);
|
|
743
|
+
})}
|
|
744
|
+
</div>
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function renderDragHandleValue(value) {
|
|
749
|
+
const onClick = typeof value === 'object' ? value?.onClick : undefined;
|
|
750
|
+
return (
|
|
751
|
+
<Button
|
|
752
|
+
type="button"
|
|
753
|
+
variant="ghost-black"
|
|
754
|
+
size="sm"
|
|
755
|
+
iconOnly
|
|
756
|
+
icon={<Icon name="dots-grid-stroked" size="sm" />}
|
|
757
|
+
className={[ICON_ONLY_BUTTON_RESET, DRAG_HANDLE_BUTTON_CLASS].join(' ')}
|
|
758
|
+
aria-label="拖拽排序"
|
|
759
|
+
onClick={onClick}
|
|
760
|
+
/>
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function getCardFormStatusVariant(status) {
|
|
765
|
+
return CARD_FORM_STATUS_VARIANT[status] || 'grey';
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function getCardFormMetrics(version = {}) {
|
|
769
|
+
if (Array.isArray(version.metrics)) return version.metrics;
|
|
770
|
+
return [
|
|
771
|
+
{ label: '完成率', value: version.completion },
|
|
772
|
+
{ label: '执行次数', value: version.execCount },
|
|
773
|
+
{ label: '满意度', value: version.satisfaction },
|
|
774
|
+
{ label: '转人工率', value: version.humanRate },
|
|
775
|
+
].filter((item) => item.value != null);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function getCardFormMetricText(metrics = []) {
|
|
779
|
+
return metrics
|
|
780
|
+
.filter((metric) => metric?.value != null && metric.value !== '')
|
|
781
|
+
.map((metric) => `${metric.label} ${metric.value}`)
|
|
782
|
+
.join(' ');
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function getCardFormSubtitle(record = {}) {
|
|
786
|
+
if (record.subtitle) return record.subtitle;
|
|
787
|
+
if (record.description && record.description !== '-') return record.description;
|
|
788
|
+
if (record.channels) return `${record.channels}渠道任务`;
|
|
789
|
+
return '策略配置摘要';
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function getCardFormCategoryLabel(category) {
|
|
793
|
+
if (!category) return '';
|
|
794
|
+
return String(category).slice(0, 8);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function CardFormMetrics({ metrics = [] }) {
|
|
798
|
+
const filteredMetrics = useMemo(
|
|
799
|
+
() => metrics.filter((metric) => metric?.value != null && metric.value !== ''),
|
|
800
|
+
[metrics],
|
|
801
|
+
);
|
|
802
|
+
const metricText = useMemo(() => getCardFormMetricText(filteredMetrics), [filteredMetrics]);
|
|
803
|
+
const lineRef = useRef(null);
|
|
804
|
+
const [isOverflowed, setIsOverflowed] = useState(false);
|
|
805
|
+
|
|
806
|
+
const checkOverflow = useCallback(() => {
|
|
807
|
+
const node = lineRef.current;
|
|
808
|
+
if (!node) return;
|
|
809
|
+
setIsOverflowed(node.scrollWidth > node.clientWidth + 1);
|
|
810
|
+
}, []);
|
|
811
|
+
|
|
812
|
+
useEffect(() => {
|
|
813
|
+
const node = lineRef.current;
|
|
814
|
+
if (!node) return undefined;
|
|
815
|
+
|
|
816
|
+
checkOverflow();
|
|
817
|
+
|
|
818
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
819
|
+
return undefined;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const observer = new ResizeObserver(() => {
|
|
823
|
+
checkOverflow();
|
|
824
|
+
});
|
|
825
|
+
observer.observe(node);
|
|
826
|
+
|
|
827
|
+
return () => observer.disconnect();
|
|
828
|
+
}, [checkOverflow, metricText]);
|
|
829
|
+
|
|
830
|
+
if (filteredMetrics.length === 0) return <div className={CARD_FORM_METRICS} />;
|
|
831
|
+
|
|
832
|
+
const line = (
|
|
833
|
+
<span
|
|
834
|
+
ref={lineRef}
|
|
835
|
+
className={CARD_FORM_METRICS_LINE}
|
|
836
|
+
onMouseEnter={checkOverflow}
|
|
837
|
+
>
|
|
838
|
+
{filteredMetrics.map((metric, index) => (
|
|
839
|
+
<span key={`${metric.label}-${metric.value}`} className={index > 0 ? 'ml-5' : ''}>
|
|
840
|
+
<span className="text-foreground-muted">{metric.label} </span>
|
|
841
|
+
<span className="[font-weight:var(--font-semibold)] text-foreground">{metric.value}</span>
|
|
842
|
+
</span>
|
|
843
|
+
))}
|
|
844
|
+
</span>
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
return (
|
|
848
|
+
<div className={CARD_FORM_METRICS}>
|
|
849
|
+
{isOverflowed ? (
|
|
850
|
+
<Tooltip content={metricText} placement="top" triggerClassName="block min-w-0 w-full overflow-hidden">
|
|
851
|
+
{line}
|
|
852
|
+
</Tooltip>
|
|
853
|
+
) : line}
|
|
854
|
+
</div>
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function CardFormDateUser({ date, avatar, seed = 0 }) {
|
|
859
|
+
const defaultMember = getTeamMemberByIndex(seed);
|
|
860
|
+
const avatarName = avatar?.name || defaultMember?.name || '创建人';
|
|
861
|
+
const avatarSrc = avatar?.avatarSrc || defaultMember?.avatarSrc;
|
|
862
|
+
return (
|
|
863
|
+
<div className={CARD_FORM_USER_DATE}>
|
|
864
|
+
<Avatar
|
|
865
|
+
size="mini"
|
|
866
|
+
type={avatarSrc ? 'image' : 'fallback'}
|
|
867
|
+
src={avatarSrc}
|
|
868
|
+
alt={avatar?.avatarAlt || `${avatarName}头像`}
|
|
869
|
+
label={avatar?.avatarLabel || avatarName.slice(0, 2)}
|
|
870
|
+
/>
|
|
871
|
+
{renderSingleLineText(date, CARD_FORM_DATE, 'min-w-0')}
|
|
872
|
+
</div>
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function CardFormActions({ record, context, onView, onMore }) {
|
|
877
|
+
return (
|
|
878
|
+
<div className={CARD_FORM_ACTIONS} onClick={(event) => event.stopPropagation()}>
|
|
879
|
+
<Button
|
|
880
|
+
type="button"
|
|
881
|
+
variant="outline-black"
|
|
882
|
+
onClick={() => onView?.(record, context)}
|
|
883
|
+
>
|
|
884
|
+
查看
|
|
885
|
+
</Button>
|
|
886
|
+
<Button
|
|
887
|
+
type="button"
|
|
888
|
+
variant="ghost-black"
|
|
889
|
+
iconOnly
|
|
890
|
+
icon={<Icon name="dots-horizontal-stroked" />}
|
|
891
|
+
aria-label="更多"
|
|
892
|
+
onClick={() => onMore?.(record, context)}
|
|
893
|
+
/>
|
|
894
|
+
</div>
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function CardFormVersionRow({ version, parent, index, parentIndex, onView, onMore }) {
|
|
899
|
+
return (
|
|
900
|
+
<div className={CARD_FORM_VERSION_ROW} data-tfds-component="Table.CardForm.VersionRow">
|
|
901
|
+
<span className={CARD_FORM_VERSION_BADGE}>v{index + 1}</span>
|
|
902
|
+
{renderSingleLineText(version.title, CARD_FORM_VERSION_TITLE_TEXT, CARD_FORM_VERSION_TITLE)}
|
|
903
|
+
<div className={CARD_FORM_VERSION_STATUS}>
|
|
904
|
+
<Tag size="m" radius="md" variant={getCardFormStatusVariant(version.status)}>
|
|
905
|
+
{version.status}
|
|
906
|
+
</Tag>
|
|
907
|
+
</div>
|
|
908
|
+
<CardFormMetrics metrics={getCardFormMetrics(version)} />
|
|
909
|
+
<div className={CARD_FORM_RIGHT}>
|
|
910
|
+
<CardFormDateUser
|
|
911
|
+
date={version.updatedAt || parent.updatedAt}
|
|
912
|
+
avatar={version.avatar}
|
|
913
|
+
seed={parentIndex * 5 + index + 1}
|
|
914
|
+
/>
|
|
915
|
+
<CardFormActions
|
|
916
|
+
record={version}
|
|
917
|
+
context={{ type: 'version', parentRecord: parent, index, parentIndex }}
|
|
918
|
+
onView={onView}
|
|
919
|
+
onMore={onMore}
|
|
920
|
+
/>
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function CardFormItem({
|
|
927
|
+
record,
|
|
928
|
+
index,
|
|
929
|
+
rowKey,
|
|
930
|
+
expanded,
|
|
931
|
+
onToggle,
|
|
932
|
+
onView,
|
|
933
|
+
onMore,
|
|
934
|
+
}) {
|
|
935
|
+
const keyValue = getRowKeyValue(rowKey, record, index);
|
|
936
|
+
const versions = Array.isArray(record.versions) ? record.versions : [];
|
|
937
|
+
const hasChildren = versions.length > 0;
|
|
938
|
+
const defaultMember = getTeamMemberByIndex(index);
|
|
939
|
+
const parentClassName = hasChildren ? CARD_FORM_PARENT : CARD_FORM_PARENT_STATIC;
|
|
940
|
+
const categoryLabel = getCardFormCategoryLabel(record.category);
|
|
941
|
+
|
|
942
|
+
return (
|
|
943
|
+
<div className={CARD_FORM_ITEM} data-tfds-component="Table.CardForm.Item">
|
|
944
|
+
<div className={parentClassName}>
|
|
945
|
+
<div
|
|
946
|
+
className={CARD_FORM_TOGGLE_WRAP}
|
|
947
|
+
style={{
|
|
948
|
+
transform: expanded ? 'rotate(90deg)' : 'none',
|
|
949
|
+
opacity: hasChildren ? 1 : 0,
|
|
950
|
+
pointerEvents: hasChildren ? 'auto' : 'none',
|
|
951
|
+
}}
|
|
952
|
+
>
|
|
953
|
+
<Button
|
|
954
|
+
type="button"
|
|
955
|
+
size="sm"
|
|
956
|
+
variant="ghost-black"
|
|
957
|
+
iconOnly
|
|
958
|
+
icon={<Icon name="chevron-right-stroked" />}
|
|
959
|
+
aria-label={expanded ? '收起子项' : '展开子项'}
|
|
960
|
+
aria-expanded={hasChildren ? expanded : undefined}
|
|
961
|
+
onClick={(event) => {
|
|
962
|
+
event.stopPropagation();
|
|
963
|
+
if (hasChildren) onToggle(keyValue);
|
|
964
|
+
}}
|
|
965
|
+
/>
|
|
966
|
+
</div>
|
|
967
|
+
<div className={CARD_FORM_MAIN}>
|
|
968
|
+
{renderSingleLineText(record.title, CARD_FORM_TITLE)}
|
|
969
|
+
{renderSingleLineText(getCardFormSubtitle(record), CARD_FORM_SUBTITLE)}
|
|
970
|
+
</div>
|
|
971
|
+
<div className={CARD_FORM_ASIDE}>
|
|
972
|
+
{categoryLabel ? (
|
|
973
|
+
<Tag size="m" radius="md" variant="white">
|
|
974
|
+
{categoryLabel}
|
|
975
|
+
</Tag>
|
|
976
|
+
) : null}
|
|
977
|
+
<CardFormDateUser
|
|
978
|
+
date={record.updatedAt}
|
|
979
|
+
avatar={record.avatar || defaultMember}
|
|
980
|
+
seed={index}
|
|
981
|
+
/>
|
|
982
|
+
<CardFormActions
|
|
983
|
+
record={record}
|
|
984
|
+
context={{ type: 'parent', index }}
|
|
985
|
+
onView={onView}
|
|
986
|
+
onMore={onMore}
|
|
987
|
+
/>
|
|
988
|
+
</div>
|
|
989
|
+
</div>
|
|
990
|
+
|
|
991
|
+
{expanded && hasChildren ? (
|
|
992
|
+
<div className={CARD_FORM_CHILDREN}>
|
|
993
|
+
{versions.map((version, versionIndex) => (
|
|
994
|
+
<CardFormVersionRow
|
|
995
|
+
key={version.id || `version-${version.v ?? versionIndex}`}
|
|
996
|
+
version={version}
|
|
997
|
+
parent={record}
|
|
998
|
+
index={versionIndex}
|
|
999
|
+
parentIndex={index}
|
|
1000
|
+
onView={onView}
|
|
1001
|
+
onMore={onMore}
|
|
1002
|
+
/>
|
|
1003
|
+
))}
|
|
1004
|
+
</div>
|
|
1005
|
+
) : null}
|
|
1006
|
+
</div>
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function renderCellContent(column, record, rowIndex) {
|
|
1011
|
+
const type = normalizeColumnType(column.type);
|
|
1012
|
+
const cellSize = normalizeCellSize(column.cellSize);
|
|
1013
|
+
const rawValue = column.dataIndex ? record?.[column.dataIndex] : record?.[column.key];
|
|
1014
|
+
|
|
1015
|
+
if (typeof column.render === 'function') {
|
|
1016
|
+
return column.render(rawValue, record, rowIndex);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
switch (type) {
|
|
1020
|
+
case 'link':
|
|
1021
|
+
return renderTitleValue(rawValue);
|
|
1022
|
+
case 'iconText':
|
|
1023
|
+
return renderIconTextValue(rawValue);
|
|
1024
|
+
case 'textButton':
|
|
1025
|
+
return renderTextButtonValue(rawValue);
|
|
1026
|
+
case 'status':
|
|
1027
|
+
return renderStatusValue(rawValue);
|
|
1028
|
+
case 'tag':
|
|
1029
|
+
return renderTagsValue(rawValue);
|
|
1030
|
+
case 'switch':
|
|
1031
|
+
return renderSwitchValue(rawValue);
|
|
1032
|
+
case 'checkbox':
|
|
1033
|
+
return renderCheckboxValue(rawValue, cellSize);
|
|
1034
|
+
case 'avatar':
|
|
1035
|
+
return renderAvatarValue(rawValue, cellSize);
|
|
1036
|
+
case 'avatarText':
|
|
1037
|
+
return renderAvatarTextValue(rawValue);
|
|
1038
|
+
case 'linkDescription':
|
|
1039
|
+
return renderLinkDescriptionValue(rawValue);
|
|
1040
|
+
case 'image':
|
|
1041
|
+
return renderImageValue(rawValue, cellSize);
|
|
1042
|
+
case 'progress':
|
|
1043
|
+
return renderProgressValue(rawValue);
|
|
1044
|
+
case 'datetime':
|
|
1045
|
+
return renderDateTimeValue(rawValue);
|
|
1046
|
+
case 'actions':
|
|
1047
|
+
return renderActionsValue(rawValue);
|
|
1048
|
+
case 'dragHandle':
|
|
1049
|
+
return renderDragHandleValue(rawValue);
|
|
1050
|
+
case 'text':
|
|
1051
|
+
default:
|
|
1052
|
+
return renderSingleLineText(rawValue);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
export default function Table({
|
|
1057
|
+
columns = [],
|
|
1058
|
+
dataSource = [],
|
|
1059
|
+
variant = 'table',
|
|
1060
|
+
rowKey = 'id',
|
|
1061
|
+
pagination = null,
|
|
1062
|
+
expandedRowKeys = null,
|
|
1063
|
+
defaultExpandedRowKeys = [],
|
|
1064
|
+
onExpandedRowKeysChange = null,
|
|
1065
|
+
onView = null,
|
|
1066
|
+
onMore = null,
|
|
1067
|
+
onPageChange = null,
|
|
1068
|
+
onPageSizeChange = null,
|
|
1069
|
+
emptyText = '暂无数据',
|
|
1070
|
+
className = '',
|
|
1071
|
+
fixedColumnsMode = 'none',
|
|
1072
|
+
}) {
|
|
1073
|
+
const paginationCurrent = pagination?.current ?? 1;
|
|
1074
|
+
const paginationPageSize = pagination?.pageSize ?? 10;
|
|
1075
|
+
const paginationTotal = pagination?.total ?? dataSource.length;
|
|
1076
|
+
|
|
1077
|
+
const [innerCurrent, setInnerCurrent] = useState(paginationCurrent);
|
|
1078
|
+
const [innerPageSize, setInnerPageSize] = useState(paginationPageSize);
|
|
1079
|
+
const [innerExpandedRowKeys, setInnerExpandedRowKeys] = useState(defaultExpandedRowKeys);
|
|
1080
|
+
|
|
1081
|
+
useEffect(() => {
|
|
1082
|
+
setInnerCurrent(paginationCurrent);
|
|
1083
|
+
}, [paginationCurrent]);
|
|
1084
|
+
|
|
1085
|
+
useEffect(() => {
|
|
1086
|
+
setInnerPageSize(paginationPageSize);
|
|
1087
|
+
}, [paginationPageSize]);
|
|
1088
|
+
|
|
1089
|
+
const current = pagination?.current ?? innerCurrent;
|
|
1090
|
+
const pageSize = pagination?.pageSize ?? innerPageSize;
|
|
1091
|
+
const totalPages = Math.max(1, Math.ceil((paginationTotal || 0) / Math.max(pageSize, 1)));
|
|
1092
|
+
const safeCurrent = Math.min(Math.max(Number(current) || 1, 1), totalPages);
|
|
1093
|
+
const pageItems = pagination?.displayPages || buildPageItems(safeCurrent, totalPages);
|
|
1094
|
+
const shouldPaginateDataSource = shouldSliceDataSource(pagination, dataSource.length);
|
|
1095
|
+
const visibleDataSource = useMemo(() => {
|
|
1096
|
+
if (!shouldPaginateDataSource) return dataSource;
|
|
1097
|
+
const start = (safeCurrent - 1) * pageSize;
|
|
1098
|
+
return dataSource.slice(start, start + pageSize);
|
|
1099
|
+
}, [dataSource, pageSize, safeCurrent, shouldPaginateDataSource]);
|
|
1100
|
+
const pageSizeOptions = pagination?.pageSizeOptions?.length
|
|
1101
|
+
? pagination.pageSizeOptions
|
|
1102
|
+
: [pageSize];
|
|
1103
|
+
const columnTemplate = useMemo(() => buildColumnGridTemplate(columns), [columns]);
|
|
1104
|
+
const columnTemplateMinWidth = useMemo(() => getColumnTemplateMinWidth(columns), [columns]);
|
|
1105
|
+
const selectValue = Number.isNaN(Number(pageSize)) ? pageSize : Number(pageSize);
|
|
1106
|
+
const selectOptions = useMemo(() => {
|
|
1107
|
+
const normalized = Array.from(new Set([...pageSizeOptions, pageSize]));
|
|
1108
|
+
return normalized.map((item) => ({
|
|
1109
|
+
value: Number(item),
|
|
1110
|
+
label:
|
|
1111
|
+
pagination?.pageSizeLabel && Number(item) === Number(pageSize)
|
|
1112
|
+
? pagination.pageSizeLabel
|
|
1113
|
+
: `${item} 条/页`,
|
|
1114
|
+
}));
|
|
1115
|
+
}, [pageSize, pageSizeOptions, pagination?.pageSizeLabel]);
|
|
1116
|
+
|
|
1117
|
+
const handlePageChange = (page) => {
|
|
1118
|
+
if (page < 1 || page > totalPages || page === safeCurrent) return;
|
|
1119
|
+
if (pagination?.current == null) {
|
|
1120
|
+
setInnerCurrent(page);
|
|
1121
|
+
}
|
|
1122
|
+
onPageChange?.(page);
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
const handlePageSizeChange = (nextPageSize) => {
|
|
1126
|
+
const parsed = Number(nextPageSize);
|
|
1127
|
+
if (Number.isNaN(parsed) || parsed === pageSize) return;
|
|
1128
|
+
if (pagination?.pageSize == null) {
|
|
1129
|
+
setInnerPageSize(parsed);
|
|
1130
|
+
}
|
|
1131
|
+
if (pagination?.current == null) {
|
|
1132
|
+
setInnerCurrent(1);
|
|
1133
|
+
}
|
|
1134
|
+
onPageSizeChange?.(parsed);
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
const resolvedExpandedRowKeys = Array.isArray(expandedRowKeys) ? expandedRowKeys : innerExpandedRowKeys;
|
|
1138
|
+
const expandedKeySet = useMemo(
|
|
1139
|
+
() => new Set(resolvedExpandedRowKeys.map((item) => String(item))),
|
|
1140
|
+
[resolvedExpandedRowKeys],
|
|
1141
|
+
);
|
|
1142
|
+
|
|
1143
|
+
const handleCardFormToggle = (key) => {
|
|
1144
|
+
const normalizedKey = String(key);
|
|
1145
|
+
const nextKeys = expandedKeySet.has(normalizedKey)
|
|
1146
|
+
? resolvedExpandedRowKeys.filter((item) => String(item) !== normalizedKey)
|
|
1147
|
+
: [...resolvedExpandedRowKeys, key];
|
|
1148
|
+
if (!Array.isArray(expandedRowKeys)) {
|
|
1149
|
+
setInnerExpandedRowKeys(nextKeys);
|
|
1150
|
+
}
|
|
1151
|
+
onExpandedRowKeysChange?.(nextKeys);
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
const paginationFooter = pagination ? (
|
|
1155
|
+
<div className={FOOTER}>
|
|
1156
|
+
<span className={FOOTER_SUMMARY}>
|
|
1157
|
+
{pagination.summaryText || defaultSummaryText(safeCurrent, pageSize, paginationTotal)}
|
|
1158
|
+
</span>
|
|
1159
|
+
|
|
1160
|
+
<div className={FOOTER_RIGHT}>
|
|
1161
|
+
<div className={PAGE_GROUP}>
|
|
1162
|
+
<div className={PAGE_LIST}>
|
|
1163
|
+
<Button
|
|
1164
|
+
type="button"
|
|
1165
|
+
variant="ghost-black"
|
|
1166
|
+
size="sm"
|
|
1167
|
+
iconOnly
|
|
1168
|
+
icon={<Icon name="chevron-left-stroked" size="sm" />}
|
|
1169
|
+
className={[ICON_ONLY_BUTTON_RESET, PAGE_ARROW_BUTTON_CLASS].join(' ')}
|
|
1170
|
+
onClick={() => handlePageChange(safeCurrent - 1)}
|
|
1171
|
+
disabled={safeCurrent <= 1}
|
|
1172
|
+
aria-label="上一页"
|
|
1173
|
+
data-tfds-component="Table.Pagination"
|
|
1174
|
+
/>
|
|
1175
|
+
|
|
1176
|
+
{pageItems.map((item, index) =>
|
|
1177
|
+
item === 'ellipsis' ? (
|
|
1178
|
+
<span key={`ellipsis-${index}`} className={PAGE_ELLIPSIS}>
|
|
1179
|
+
...
|
|
1180
|
+
</span>
|
|
1181
|
+
) : (
|
|
1182
|
+
<Button
|
|
1183
|
+
key={item}
|
|
1184
|
+
type="button"
|
|
1185
|
+
className={[
|
|
1186
|
+
PAGE_NUMBER_BUTTON_CLASS,
|
|
1187
|
+
Number(item) === safeCurrent ? PAGE_BUTTON_ACTIVE : '',
|
|
1188
|
+
].filter(Boolean).join(' ')}
|
|
1189
|
+
variant="ghost-black"
|
|
1190
|
+
size="sm"
|
|
1191
|
+
style={Number(item) === safeCurrent ? PAGE_BUTTON_ACTIVE_STYLE : undefined}
|
|
1192
|
+
onClick={() => handlePageChange(Number(item))}
|
|
1193
|
+
aria-current={Number(item) === safeCurrent ? 'page' : undefined}
|
|
1194
|
+
data-tfds-component="Table.Pagination"
|
|
1195
|
+
>
|
|
1196
|
+
{item}
|
|
1197
|
+
</Button>
|
|
1198
|
+
),
|
|
1199
|
+
)}
|
|
1200
|
+
|
|
1201
|
+
<Button
|
|
1202
|
+
type="button"
|
|
1203
|
+
variant="ghost-black"
|
|
1204
|
+
size="sm"
|
|
1205
|
+
iconOnly
|
|
1206
|
+
icon={<Icon name="chevron-right-stroked" size="sm" />}
|
|
1207
|
+
className={[ICON_ONLY_BUTTON_RESET, PAGE_ARROW_BUTTON_CLASS].join(' ')}
|
|
1208
|
+
onClick={() => handlePageChange(safeCurrent + 1)}
|
|
1209
|
+
disabled={safeCurrent >= totalPages}
|
|
1210
|
+
aria-label="下一页"
|
|
1211
|
+
data-tfds-component="Table.Pagination"
|
|
1212
|
+
/>
|
|
1213
|
+
</div>
|
|
1214
|
+
</div>
|
|
1215
|
+
|
|
1216
|
+
<Select
|
|
1217
|
+
className="!w-[120px] !min-w-[120px] !max-w-[120px] shrink-0"
|
|
1218
|
+
options={selectOptions}
|
|
1219
|
+
value={selectValue}
|
|
1220
|
+
onChange={(nextValue) => handlePageSizeChange(nextValue)}
|
|
1221
|
+
data-tfds-component="Table.PageSize"
|
|
1222
|
+
/>
|
|
1223
|
+
</div>
|
|
1224
|
+
</div>
|
|
1225
|
+
) : null;
|
|
1226
|
+
|
|
1227
|
+
if (variant === 'card-form') {
|
|
1228
|
+
return (
|
|
1229
|
+
<div className={[WRAPPER, className].filter(Boolean).join(' ')} data-tfds-component="Table" data-variant="card-form">
|
|
1230
|
+
<div className={CARD_FORM_BODY}>
|
|
1231
|
+
{visibleDataSource.length === 0 ? (
|
|
1232
|
+
<div className={EMPTY_CELL}>{emptyText}</div>
|
|
1233
|
+
) : (
|
|
1234
|
+
visibleDataSource.map((record, rowIndex) => {
|
|
1235
|
+
const sourceIndex = shouldPaginateDataSource ? (safeCurrent - 1) * pageSize + rowIndex : rowIndex;
|
|
1236
|
+
const keyValue = getRowKeyValue(rowKey, record, sourceIndex);
|
|
1237
|
+
return (
|
|
1238
|
+
<CardFormItem
|
|
1239
|
+
key={keyValue}
|
|
1240
|
+
record={record}
|
|
1241
|
+
index={sourceIndex}
|
|
1242
|
+
rowKey={rowKey}
|
|
1243
|
+
expanded={expandedKeySet.has(String(keyValue))}
|
|
1244
|
+
onToggle={handleCardFormToggle}
|
|
1245
|
+
onView={onView}
|
|
1246
|
+
onMore={onMore}
|
|
1247
|
+
/>
|
|
1248
|
+
);
|
|
1249
|
+
})
|
|
1250
|
+
)}
|
|
1251
|
+
</div>
|
|
1252
|
+
{paginationFooter}
|
|
1253
|
+
</div>
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return (
|
|
1258
|
+
<div className={[WRAPPER, className].filter(Boolean).join(' ')} data-tfds-component="Table">
|
|
1259
|
+
<div className={TABLE_SHELL}>
|
|
1260
|
+
<div className={TABLE_VIEWPORT}>
|
|
1261
|
+
<div className={TABLE_CONTENT} style={{ minWidth: `${columnTemplateMinWidth}px` }}>
|
|
1262
|
+
<div className={TABLE} role="grid" aria-rowcount={visibleDataSource.length + 1} aria-colcount={columns.length}>
|
|
1263
|
+
<div className={HEADER_ROW} role="row" style={{ gridTemplateColumns: columnTemplate }}>
|
|
1264
|
+
{columns.map((column, columnIndex) => (
|
|
1265
|
+
<div
|
|
1266
|
+
key={column.key || column.dataIndex || column.title}
|
|
1267
|
+
className={[
|
|
1268
|
+
HEADER_CELL,
|
|
1269
|
+
HEADER_SIZE_CLASS[normalizeCellSize(column.cellSize)] || HEADER_SIZE_CLASS.default,
|
|
1270
|
+
].join(' ')}
|
|
1271
|
+
role="columnheader"
|
|
1272
|
+
style={getPinnedCellStyle({
|
|
1273
|
+
columnIndex,
|
|
1274
|
+
columnsLength: columns.length,
|
|
1275
|
+
align: column.align,
|
|
1276
|
+
minHeight: HEADER_MIN_HEIGHT[normalizeCellSize(column.cellSize)] || HEADER_MIN_HEIGHT.default,
|
|
1277
|
+
fixedColumnsMode,
|
|
1278
|
+
isHeader: true,
|
|
1279
|
+
})}
|
|
1280
|
+
>
|
|
1281
|
+
<span className={HEADER_TEXT} data-tfds-component="Table.HeaderCell">{column.title}</span>
|
|
1282
|
+
</div>
|
|
1283
|
+
))}
|
|
1284
|
+
</div>
|
|
1285
|
+
|
|
1286
|
+
<div className={TABLE_BODY} role="rowgroup">
|
|
1287
|
+
{visibleDataSource.length === 0 ? (
|
|
1288
|
+
<div className={BODY_ROW} role="row" style={{ gridTemplateColumns: columnTemplate }}>
|
|
1289
|
+
<div className={EMPTY_CELL} role="gridcell" style={{ gridColumn: '1 / -1' }}>
|
|
1290
|
+
{emptyText}
|
|
1291
|
+
</div>
|
|
1292
|
+
</div>
|
|
1293
|
+
) : (
|
|
1294
|
+
visibleDataSource.map((record, rowIndex) => {
|
|
1295
|
+
const sourceIndex = shouldPaginateDataSource ? (safeCurrent - 1) * pageSize + rowIndex : rowIndex;
|
|
1296
|
+
return (
|
|
1297
|
+
<div
|
|
1298
|
+
key={getRowKeyValue(rowKey, record, sourceIndex)}
|
|
1299
|
+
className={BODY_ROW}
|
|
1300
|
+
role="row"
|
|
1301
|
+
style={{ gridTemplateColumns: columnTemplate }}
|
|
1302
|
+
data-tfds-component="Table.Row"
|
|
1303
|
+
>
|
|
1304
|
+
{columns.map((column, columnIndex) => (
|
|
1305
|
+
<div
|
|
1306
|
+
key={column.key || column.dataIndex || column.title}
|
|
1307
|
+
className={buildCellClass(
|
|
1308
|
+
BODY_CELL,
|
|
1309
|
+
normalizeCellSize(column.cellSize),
|
|
1310
|
+
)}
|
|
1311
|
+
role="gridcell"
|
|
1312
|
+
style={getPinnedCellStyle({
|
|
1313
|
+
columnIndex,
|
|
1314
|
+
columnsLength: columns.length,
|
|
1315
|
+
align: column.align,
|
|
1316
|
+
minHeight: getTypeMinHeight(normalizeColumnType(column.type), normalizeCellSize(column.cellSize)),
|
|
1317
|
+
fixedColumnsMode,
|
|
1318
|
+
})}
|
|
1319
|
+
>
|
|
1320
|
+
<div className="w-full min-w-0">
|
|
1321
|
+
{renderCellContent(column, record, sourceIndex)}
|
|
1322
|
+
</div>
|
|
1323
|
+
</div>
|
|
1324
|
+
))}
|
|
1325
|
+
</div>
|
|
1326
|
+
);
|
|
1327
|
+
})
|
|
1328
|
+
)}
|
|
1329
|
+
</div>
|
|
1330
|
+
</div>
|
|
1331
|
+
</div>
|
|
1332
|
+
</div>
|
|
1333
|
+
</div>
|
|
1334
|
+
|
|
1335
|
+
{paginationFooter}
|
|
1336
|
+
</div>
|
|
1337
|
+
);
|
|
1338
|
+
}
|