@tbox.cn/app-template-agent 0.1.0
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/.gitignore +13 -0
- package/.nvmrc +1 -0
- package/.tbox/app.json +4 -0
- package/AGENTS.md +101 -0
- package/ENV.md +77 -0
- package/LEGAL.md +7 -0
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/apps/client/STYLE_GUIDE.md +81 -0
- package/apps/client/VISUAL_RESTORE.md +90 -0
- package/apps/client/assets/copy@2x.png +0 -0
- package/apps/client/assets/debug.png +0 -0
- package/apps/client/assets/dislike@2x.png +0 -0
- package/apps/client/assets/keyboard-circle.svg +12 -0
- package/apps/client/assets/like@2x.png +0 -0
- package/apps/client/assets/more-circle.svg +3 -0
- package/apps/client/assets/new-chat.svg +3 -0
- package/apps/client/assets/quick-command-star.svg +3 -0
- package/apps/client/assets/send-gradient.svg +14 -0
- package/apps/client/assets/speaker@2x.png +0 -0
- package/apps/client/assets/stop@2x.png +0 -0
- package/apps/client/assets/voice-circle.svg +3 -0
- package/apps/client/babel-plugins/code-inspect.cjs +110 -0
- package/apps/client/index.html +36 -0
- package/apps/client/package.json +34 -0
- package/apps/client/src/app.tsx +5 -0
- package/apps/client/src/business/App.tsx +11 -0
- package/apps/client/src/business/README.md +82 -0
- package/apps/client/src/cards/CardRenderer.tsx +10 -0
- package/apps/client/src/cards/registry.ts +22 -0
- package/apps/client/src/components/ActionBar/DebugButton.tsx +49 -0
- package/apps/client/src/components/ActionBar/index.less +120 -0
- package/apps/client/src/components/ActionBar/index.tsx +157 -0
- package/apps/client/src/components/ChatHeader/index.less +104 -0
- package/apps/client/src/components/ChatHeader/index.tsx +63 -0
- package/apps/client/src/components/ChatInput/index.less +332 -0
- package/apps/client/src/components/ChatInput/index.tsx +251 -0
- package/apps/client/src/components/ChatInput/voice/VoicePressField.tsx +34 -0
- package/apps/client/src/components/ChatInput/voice/VoiceRecordingOverlay.tsx +51 -0
- package/apps/client/src/components/ChatInput/voice/machine.ts +27 -0
- package/apps/client/src/components/ChatInput/voice/useVoicePress.ts +188 -0
- package/apps/client/src/components/ErrorBoundary.tsx +2 -0
- package/apps/client/src/components/MediaList/index.less +241 -0
- package/apps/client/src/components/MediaList/index.tsx +256 -0
- package/apps/client/src/components/MessageBubble/index.less +309 -0
- package/apps/client/src/components/MessageBubble/index.tsx +163 -0
- package/apps/client/src/components/PullDownIndicator/index.less +32 -0
- package/apps/client/src/components/PullDownIndicator/index.tsx +33 -0
- package/apps/client/src/components/QuickCommands/index.less +48 -0
- package/apps/client/src/components/QuickCommands/index.tsx +22 -0
- package/apps/client/src/components/ReasoningBlock/index.less +90 -0
- package/apps/client/src/components/ReasoningBlock/index.tsx +56 -0
- package/apps/client/src/components/ScrollToBottomButton/index.less +32 -0
- package/apps/client/src/components/ScrollToBottomButton/index.tsx +10 -0
- package/apps/client/src/components/ToolCallBlock/index.less +91 -0
- package/apps/client/src/components/ToolCallBlock/index.tsx +63 -0
- package/apps/client/src/examples/tourism/TourismLanding.tsx +96 -0
- package/apps/client/src/examples/tourism/index.less +192 -0
- package/apps/client/src/hooks/useMediaUpload.ts +237 -0
- package/apps/client/src/hooks/usePullDownHistory.ts +97 -0
- package/apps/client/src/hooks/useTTSAutoPlay.ts +80 -0
- package/apps/client/src/main.tsx +17 -0
- package/apps/client/src/modules.ts +30 -0
- package/apps/client/src/pages/index/index.less +43 -0
- package/apps/client/src/pages/index/index.tsx +145 -0
- package/apps/client/src/platform/AgentChatApp.tsx +39 -0
- package/apps/client/src/platform/agent-runtime.ts +15 -0
- package/apps/client/src/platform/state/createPersistedStore.ts +19 -0
- package/apps/client/src/shims/node-stub.ts +14 -0
- package/apps/client/src/store/chat-config.ts +29 -0
- package/apps/client/src/store/chat.ts +201 -0
- package/apps/client/src/styles/global.less +60 -0
- package/apps/client/src/styles/tokens.less +60 -0
- package/apps/client/src/types/chat.ts +29 -0
- package/apps/client/src/utils/copyText.ts +11 -0
- package/apps/client/src/utils/imagePreview.ts +10 -0
- package/apps/client/src/utils/session-error.ts +5 -0
- package/apps/client/src/utils/toast.ts +15 -0
- package/apps/client/src/vite-env.d.ts +3 -0
- package/apps/client/tsconfig.json +11 -0
- package/apps/client/vite.config.ts +40 -0
- package/apps/server/package.json +40 -0
- package/apps/server/src/cli/collect-registry.ts +26 -0
- package/apps/server/src/cli/plugin-doctor.ts +89 -0
- package/apps/server/src/cli/plugin-inspect.ts +49 -0
- package/apps/server/src/index.ts +230 -0
- package/apps/server/src/intent-routes.ts +23 -0
- package/apps/server/src/modules.ts +31 -0
- package/apps/server/src/tools/translate.ts +41 -0
- package/apps/server/tests/cards-registry.test.ts +55 -0
- package/apps/server/tests/tool-runtime.test.ts +49 -0
- package/apps/server/tsconfig.json +5 -0
- package/apps/server/tsup.config.ts +25 -0
- package/apps/server/vitest.config.ts +8 -0
- package/config/chat.json +8 -0
- package/config/llm_provider.json +3 -0
- package/config/plugins.json +3 -0
- package/eslint.config.mjs +41 -0
- package/package.json +51 -0
- package/packages/module-weather/package.json +35 -0
- package/packages/module-weather/src/client/cards/weather/index.less +22 -0
- package/packages/module-weather/src/client/cards/weather/index.tsx +14 -0
- package/packages/module-weather/src/client/index.ts +19 -0
- package/packages/module-weather/src/index.ts +2 -0
- package/packages/module-weather/src/server/cards/weather/meta.ts +28 -0
- package/packages/module-weather/src/server/index.ts +19 -0
- package/packages/module-weather/src/server/tool.ts +29 -0
- package/packages/module-weather/tbox.component.json +27 -0
- package/packages/module-weather/tsconfig.json +14 -0
- package/pnpm-workspace.template.yaml +60 -0
- package/scripts/build.sh +39 -0
- package/scripts/check-inspector.sh +91 -0
- package/scripts/check-workspace.mjs +164 -0
- package/scripts/dev.sh +15 -0
- package/scripts/prepare.sh +11 -0
- package/scripts/start.sh +11 -0
- package/scripts/stop.sh +26 -0
- package/scripts/verify-generated.mjs +142 -0
- package/tsconfig.base.json +20 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
.tbox-media-list {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: flex-end;
|
|
4
|
+
gap: 16px;
|
|
5
|
+
padding: 0 0 16px;
|
|
6
|
+
overflow: visible;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.tbox-media-list__scroll {
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-wrap: nowrap;
|
|
12
|
+
overflow-x: auto;
|
|
13
|
+
gap: 16px;
|
|
14
|
+
-webkit-overflow-scrolling: touch;
|
|
15
|
+
min-width: 0;
|
|
16
|
+
|
|
17
|
+
&::-webkit-scrollbar {
|
|
18
|
+
display: none;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.tbox-media-item {
|
|
23
|
+
width: var(--tbox-media-item-size);
|
|
24
|
+
min-width: var(--tbox-media-item-size);
|
|
25
|
+
height: var(--tbox-media-item-size);
|
|
26
|
+
border-radius: var(--tbox-media-item-radius);
|
|
27
|
+
overflow: visible;
|
|
28
|
+
position: relative;
|
|
29
|
+
background: #f5f5f5;
|
|
30
|
+
|
|
31
|
+
&--image {
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&__thumb {
|
|
36
|
+
width: 100%;
|
|
37
|
+
height: 100%;
|
|
38
|
+
display: block;
|
|
39
|
+
object-fit: cover;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&__overlay {
|
|
43
|
+
position: absolute;
|
|
44
|
+
inset: 0;
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
background: rgba(0, 0, 0, 0.5);
|
|
50
|
+
border-radius: var(--tbox-media-item-radius);
|
|
51
|
+
gap: 8px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&__progress-ring {
|
|
55
|
+
width: 60px;
|
|
56
|
+
height: 60px;
|
|
57
|
+
border-radius: 50%;
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
transition: background 0.3s ease;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&__progress-inner {
|
|
65
|
+
width: 48px;
|
|
66
|
+
height: 48px;
|
|
67
|
+
border-radius: 50%;
|
|
68
|
+
background: rgba(0, 0, 0, 0.7);
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
justify-content: center;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
&__progress-text {
|
|
75
|
+
color: #fff;
|
|
76
|
+
font-size: 20px;
|
|
77
|
+
font-weight: bold;
|
|
78
|
+
text-align: center;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
&__status-icon {
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
justify-content: center;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
&__status-text {
|
|
88
|
+
color: rgba(255, 255, 255, 0.85);
|
|
89
|
+
font-size: 20px;
|
|
90
|
+
font-weight: 500;
|
|
91
|
+
text-align: center;
|
|
92
|
+
white-space: nowrap;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
&__remove {
|
|
96
|
+
position: absolute;
|
|
97
|
+
top: 12px;
|
|
98
|
+
right: 12px;
|
|
99
|
+
width: 36px;
|
|
100
|
+
height: 36px;
|
|
101
|
+
border-radius: 18px;
|
|
102
|
+
background: rgba(0, 0, 0, 0.55);
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
justify-content: center;
|
|
106
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
|
107
|
+
z-index: 2;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* 非图片(视频/文件)卡片 */
|
|
112
|
+
.tbox-file-item {
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-direction: row;
|
|
115
|
+
align-items: center;
|
|
116
|
+
gap: 12px;
|
|
117
|
+
min-width: 280px;
|
|
118
|
+
height: var(--tbox-media-item-size);
|
|
119
|
+
border-radius: var(--tbox-media-item-radius);
|
|
120
|
+
background: #fcfcfc;
|
|
121
|
+
padding: 16px;
|
|
122
|
+
box-sizing: border-box;
|
|
123
|
+
position: relative;
|
|
124
|
+
overflow: visible;
|
|
125
|
+
|
|
126
|
+
&__icon {
|
|
127
|
+
flex-shrink: 0;
|
|
128
|
+
width: 48px;
|
|
129
|
+
height: 48px;
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
justify-content: center;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
&__info {
|
|
136
|
+
display: flex;
|
|
137
|
+
flex-direction: column;
|
|
138
|
+
justify-content: center;
|
|
139
|
+
gap: 4px;
|
|
140
|
+
flex: 1;
|
|
141
|
+
min-width: 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
&__name {
|
|
145
|
+
font-size: 24px;
|
|
146
|
+
color: #333;
|
|
147
|
+
font-weight: 500;
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
text-overflow: ellipsis;
|
|
150
|
+
white-space: nowrap;
|
|
151
|
+
max-width: 100%;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
&__meta {
|
|
155
|
+
font-size: 20px;
|
|
156
|
+
color: rgba(0, 0, 0, 0.25);
|
|
157
|
+
white-space: nowrap;
|
|
158
|
+
|
|
159
|
+
&--failed {
|
|
160
|
+
color: #ff4d4f;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.tbox-media-item__remove {
|
|
165
|
+
position: absolute;
|
|
166
|
+
top: 12px;
|
|
167
|
+
right: 12px;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* 末尾“加更多”按钮 */
|
|
172
|
+
.tbox-media-add {
|
|
173
|
+
width: var(--tbox-media-item-size);
|
|
174
|
+
min-width: var(--tbox-media-item-size);
|
|
175
|
+
height: var(--tbox-media-item-size);
|
|
176
|
+
border-radius: var(--tbox-media-item-radius);
|
|
177
|
+
background: rgba(255, 255, 255, 0.6);
|
|
178
|
+
display: flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
justify-content: center;
|
|
181
|
+
flex-shrink: 0;
|
|
182
|
+
position: relative;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* 加更多的二级 popover */
|
|
186
|
+
.tbox-media-popover {
|
|
187
|
+
position: absolute;
|
|
188
|
+
bottom: calc(var(--tbox-media-item-size) + 8px);
|
|
189
|
+
right: 0;
|
|
190
|
+
width: var(--tbox-media-item-size);
|
|
191
|
+
background: #fff;
|
|
192
|
+
border-radius: 20px;
|
|
193
|
+
padding: 16px 0;
|
|
194
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
|
195
|
+
z-index: 10;
|
|
196
|
+
opacity: 0;
|
|
197
|
+
transform: translateY(8px);
|
|
198
|
+
transition: opacity 0.2s ease-out, transform 0.2s ease-out;
|
|
199
|
+
pointer-events: none;
|
|
200
|
+
|
|
201
|
+
&--visible {
|
|
202
|
+
opacity: 1;
|
|
203
|
+
transform: translateY(0);
|
|
204
|
+
pointer-events: auto;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
&__item {
|
|
208
|
+
display: flex;
|
|
209
|
+
flex-direction: row;
|
|
210
|
+
align-items: center;
|
|
211
|
+
height: 60px;
|
|
212
|
+
gap: 8px;
|
|
213
|
+
padding: 0 16px;
|
|
214
|
+
|
|
215
|
+
& + & {
|
|
216
|
+
margin-top: 8px;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
&__icon-wrap {
|
|
221
|
+
width: 40px;
|
|
222
|
+
height: 40px;
|
|
223
|
+
border-radius: 8px;
|
|
224
|
+
display: flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
justify-content: center;
|
|
227
|
+
flex-shrink: 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
&__icon {
|
|
231
|
+
width: 32px;
|
|
232
|
+
height: 32px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
&__label {
|
|
236
|
+
font-size: 28px;
|
|
237
|
+
color: rgba(31, 59, 99, 0.88);
|
|
238
|
+
font-weight: 400;
|
|
239
|
+
line-height: 1.4;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { X, Warning, Prohibit, Plus, VideoCamera } from '@phosphor-icons/react';
|
|
3
|
+
import { previewImages } from '@/utils/imagePreview';
|
|
4
|
+
import { toast } from '@/utils/toast';
|
|
5
|
+
import type { MediaItem } from '@/types/chat';
|
|
6
|
+
import './index.less';
|
|
7
|
+
|
|
8
|
+
/** 媒体选项图标(远程素材,对齐原模板)。 */
|
|
9
|
+
const POPOVER_ICONS: Record<string, { icon: string; label: string; bg: string }> = {
|
|
10
|
+
image: {
|
|
11
|
+
icon: 'https://mdn.alipayobjects.com/huamei_nzq4hr/afts/img/A*DfDhSYAMzoQAAAAAHiAAAAgAel-hAQ/original',
|
|
12
|
+
label: '相册',
|
|
13
|
+
bg: '#0000001a',
|
|
14
|
+
},
|
|
15
|
+
camera: {
|
|
16
|
+
icon: 'https://mdn.alipayobjects.com/huamei_nzq4hr/afts/img/A*xdKLSabxqmQAAAAAOnAAAAgAel-hAQ/original',
|
|
17
|
+
label: '拍摄',
|
|
18
|
+
bg: '#4f58ec26',
|
|
19
|
+
},
|
|
20
|
+
video: {
|
|
21
|
+
icon: 'https://mdn.alipayobjects.com/huamei_nzq4hr/afts/img/A*xdKLSabxqmQAAAAAOnAAAAgAel-hAQ/original',
|
|
22
|
+
label: '视频',
|
|
23
|
+
bg: '#4f58ec26',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function formatSize(bytes?: number): string {
|
|
28
|
+
if (!bytes) return '';
|
|
29
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
30
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
31
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ImageItemCard({
|
|
35
|
+
item,
|
|
36
|
+
allImages,
|
|
37
|
+
onRemove,
|
|
38
|
+
onRetry,
|
|
39
|
+
}: {
|
|
40
|
+
item: MediaItem;
|
|
41
|
+
allImages: string[];
|
|
42
|
+
onRemove: (id: string) => void;
|
|
43
|
+
onRetry?: (id: string) => void;
|
|
44
|
+
}) {
|
|
45
|
+
const isUploading = !!item.isUploading;
|
|
46
|
+
const isFailed = !!item.uploadFailed;
|
|
47
|
+
const isIllegal = !!item.illegal;
|
|
48
|
+
const progress = item.uploadProgress ?? 0;
|
|
49
|
+
|
|
50
|
+
const onClick = () => {
|
|
51
|
+
if (isUploading) return;
|
|
52
|
+
if (isFailed) return onRetry?.(item.id);
|
|
53
|
+
if (isIllegal) return void toast('内容未过审核');
|
|
54
|
+
previewImages(allImages, allImages.indexOf(item.src));
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="tbox-media-item tbox-media-item--image" onClick={onClick}>
|
|
59
|
+
<img className="tbox-media-item__thumb" src={item.src} alt="" />
|
|
60
|
+
|
|
61
|
+
{isUploading && (
|
|
62
|
+
<div className="tbox-media-item__overlay">
|
|
63
|
+
<div
|
|
64
|
+
className="tbox-media-item__progress-ring"
|
|
65
|
+
style={{ background: `conic-gradient(#fff ${progress}%, rgba(255,255,255,0.25) ${progress}%)` }}
|
|
66
|
+
>
|
|
67
|
+
<div className="tbox-media-item__progress-inner">
|
|
68
|
+
<span className="tbox-media-item__progress-text">{progress}%</span>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{isFailed && (
|
|
75
|
+
<div className="tbox-media-item__overlay">
|
|
76
|
+
<div className="tbox-media-item__status-icon">
|
|
77
|
+
<Warning size={20} color="#fff" />
|
|
78
|
+
</div>
|
|
79
|
+
<span className="tbox-media-item__status-text">点击重试</span>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{isIllegal && (
|
|
84
|
+
<div className="tbox-media-item__overlay">
|
|
85
|
+
<div className="tbox-media-item__status-icon">
|
|
86
|
+
<Prohibit size={20} color="#fff" />
|
|
87
|
+
</div>
|
|
88
|
+
<span className="tbox-media-item__status-text">未过审核</span>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{!isUploading && (
|
|
93
|
+
<div className="tbox-media-item__remove" onClick={(e) => { e.stopPropagation(); onRemove(item.id); }}>
|
|
94
|
+
<X size={14} color="#fff" />
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function FileItemCard({
|
|
102
|
+
item,
|
|
103
|
+
onRemove,
|
|
104
|
+
onRetry,
|
|
105
|
+
}: {
|
|
106
|
+
item: MediaItem;
|
|
107
|
+
onRemove: (id: string) => void;
|
|
108
|
+
onRetry?: (id: string) => void;
|
|
109
|
+
}) {
|
|
110
|
+
const isUploading = !!item.isUploading;
|
|
111
|
+
const isFailed = !!item.uploadFailed;
|
|
112
|
+
|
|
113
|
+
const meta = isUploading
|
|
114
|
+
? `上传中...${item.uploadProgress ?? 0}%`
|
|
115
|
+
: isFailed
|
|
116
|
+
? '上传失败'
|
|
117
|
+
: formatSize(item.size);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="tbox-file-item" onClick={() => isFailed && onRetry?.(item.id)}>
|
|
121
|
+
<div className="tbox-file-item__icon">
|
|
122
|
+
<VideoCamera size={28} color="#506fff" />
|
|
123
|
+
</div>
|
|
124
|
+
<div className="tbox-file-item__info">
|
|
125
|
+
<span className="tbox-file-item__name">{item.name ?? '未知文件'}</span>
|
|
126
|
+
<span className={`tbox-file-item__meta${isFailed ? ' tbox-file-item__meta--failed' : ''}`}>{meta}</span>
|
|
127
|
+
</div>
|
|
128
|
+
{!isUploading && (
|
|
129
|
+
<div className="tbox-media-item__remove" onClick={(e) => { e.stopPropagation(); onRemove(item.id); }}>
|
|
130
|
+
<X size={14} color="#fff" />
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function MediaPopover({
|
|
138
|
+
visible,
|
|
139
|
+
options,
|
|
140
|
+
onSelect,
|
|
141
|
+
}: {
|
|
142
|
+
visible: boolean;
|
|
143
|
+
options: { type: string; icon: string; label: string; bg: string }[];
|
|
144
|
+
onSelect: (type: string) => void;
|
|
145
|
+
}) {
|
|
146
|
+
return (
|
|
147
|
+
<div className={`tbox-media-popover${visible ? ' tbox-media-popover--visible' : ''}`}>
|
|
148
|
+
{options.map((opt) => (
|
|
149
|
+
<div
|
|
150
|
+
key={opt.type}
|
|
151
|
+
className="tbox-media-popover__item"
|
|
152
|
+
onClick={(e) => { e.stopPropagation(); onSelect(opt.type); }}
|
|
153
|
+
>
|
|
154
|
+
<div className="tbox-media-popover__icon-wrap" style={{ background: opt.bg }}>
|
|
155
|
+
<img className="tbox-media-popover__icon" src={opt.icon} alt="" />
|
|
156
|
+
</div>
|
|
157
|
+
<span className="tbox-media-popover__label">{opt.label}</span>
|
|
158
|
+
</div>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface MediaListProps {
|
|
165
|
+
items: MediaItem[];
|
|
166
|
+
onRemove: (id: string) => void;
|
|
167
|
+
onRetry?: (id: string) => void;
|
|
168
|
+
onPickImage?: () => void;
|
|
169
|
+
onPickCamera?: () => void;
|
|
170
|
+
onPickVideo?: () => void;
|
|
171
|
+
enableImage?: boolean;
|
|
172
|
+
enableVideo?: boolean;
|
|
173
|
+
isWebPc?: boolean;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 已选媒体预览列表(对齐原模板:进度环 / 失败-未过审 overlay / 删除 / 末尾加号 popover)。 */
|
|
177
|
+
export function MediaList({
|
|
178
|
+
items,
|
|
179
|
+
onRemove,
|
|
180
|
+
onRetry,
|
|
181
|
+
onPickImage,
|
|
182
|
+
onPickCamera,
|
|
183
|
+
onPickVideo,
|
|
184
|
+
enableImage,
|
|
185
|
+
enableVideo,
|
|
186
|
+
isWebPc,
|
|
187
|
+
}: MediaListProps) {
|
|
188
|
+
const [popoverVisible, setPopoverVisible] = useState(false);
|
|
189
|
+
const hasCamera = !!enableImage && !isWebPc;
|
|
190
|
+
|
|
191
|
+
const options = [
|
|
192
|
+
enableImage && { type: 'image', ...POPOVER_ICONS.image },
|
|
193
|
+
hasCamera && { type: 'camera', ...POPOVER_ICONS.camera },
|
|
194
|
+
enableVideo && { type: 'video', ...POPOVER_ICONS.video },
|
|
195
|
+
].filter(Boolean) as { type: string; icon: string; label: string; bg: string }[];
|
|
196
|
+
|
|
197
|
+
const allImages = items
|
|
198
|
+
.filter((i) => i.type === 'image' && !i.isUploading && !i.uploadFailed && !i.illegal)
|
|
199
|
+
.map((i) => i.src);
|
|
200
|
+
|
|
201
|
+
const handleAddClick = useCallback(() => {
|
|
202
|
+
if (options.length <= 1) {
|
|
203
|
+
if (enableImage) onPickImage?.();
|
|
204
|
+
else if (enableVideo) onPickVideo?.();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
setPopoverVisible((v) => !v);
|
|
208
|
+
}, [options.length, enableImage, enableVideo, onPickImage, onPickVideo]);
|
|
209
|
+
|
|
210
|
+
const handleSelect = useCallback(
|
|
211
|
+
(type: string) => {
|
|
212
|
+
setPopoverVisible(false);
|
|
213
|
+
if (type === 'image') onPickImage?.();
|
|
214
|
+
else if (type === 'camera') onPickCamera?.();
|
|
215
|
+
else if (type === 'video') onPickVideo?.();
|
|
216
|
+
},
|
|
217
|
+
[onPickImage, onPickCamera, onPickVideo],
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (!popoverVisible) return;
|
|
222
|
+
const onDown = (e: MouseEvent | TouchEvent) => {
|
|
223
|
+
const t = e.target as HTMLElement;
|
|
224
|
+
if (t && !t.closest('.tbox-media-add') && !t.closest('.tbox-media-popover')) setPopoverVisible(false);
|
|
225
|
+
};
|
|
226
|
+
document.addEventListener('mousedown', onDown);
|
|
227
|
+
document.addEventListener('touchstart', onDown, { passive: true });
|
|
228
|
+
return () => {
|
|
229
|
+
document.removeEventListener('mousedown', onDown);
|
|
230
|
+
document.removeEventListener('touchstart', onDown);
|
|
231
|
+
};
|
|
232
|
+
}, [popoverVisible]);
|
|
233
|
+
|
|
234
|
+
if (items.length === 0) return null;
|
|
235
|
+
const showAddButton = options.length > 0;
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className="tbox-media-list">
|
|
239
|
+
<div className="tbox-media-list__scroll">
|
|
240
|
+
{items.map((item) =>
|
|
241
|
+
item.type === 'image' ? (
|
|
242
|
+
<ImageItemCard key={item.id} item={item} allImages={allImages} onRemove={onRemove} onRetry={onRetry} />
|
|
243
|
+
) : (
|
|
244
|
+
<FileItemCard key={item.id} item={item} onRemove={onRemove} onRetry={onRetry} />
|
|
245
|
+
),
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
{showAddButton && (
|
|
249
|
+
<div className="tbox-media-add" onClick={handleAddClick}>
|
|
250
|
+
<Plus size={22} color="rgba(0,0,0,0.65)" />
|
|
251
|
+
<MediaPopover visible={popoverVisible} options={options} onSelect={handleSelect} />
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|