@lark-apaas/coding-template-nestjs-react-fullstack 0.1.60-alpha.20261105142153 → 0.1.60-beta.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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/template/client/src/components/business-ui/README.md +0 -2
  3. package/template/package-lock.json +30 -546
  4. package/template/package.json +3 -14
  5. package/template/scripts/build.sh +39 -22
  6. package/template/scripts/prune-smart.js +20 -1
  7. package/template/client/src/components/business-ui/comments/README.md +0 -101
  8. package/template/client/src/components/business-ui/comments/comments.tsx +0 -670
  9. package/template/client/src/components/business-ui/comments/contracts.ts +0 -256
  10. package/template/client/src/components/business-ui/comments/index.ts +0 -20
  11. package/template/client/src/components/business-ui/comments/use-comments.ts +0 -492
  12. package/template/client/src/components/business-ui/record-comments/README.md +0 -141
  13. package/template/client/src/components/business-ui/record-comments/app-source.ts +0 -93
  14. package/template/client/src/components/business-ui/record-comments/assets/reaction-sprite.png +0 -0
  15. package/template/client/src/components/business-ui/record-comments/comment-panel.contract.ts +0 -240
  16. package/template/client/src/components/business-ui/record-comments/comment-panel.tsx +0 -231
  17. package/template/client/src/components/business-ui/record-comments/extensions.ts +0 -130
  18. package/template/client/src/components/business-ui/record-comments/index.ts +0 -9
  19. package/template/client/src/components/business-ui/record-comments/internal/composer.tsx +0 -531
  20. package/template/client/src/components/business-ui/record-comments/internal/content.tsx +0 -213
  21. package/template/client/src/components/business-ui/record-comments/internal/discussion.tsx +0 -523
  22. package/template/client/src/components/business-ui/record-comments/internal/editor.tsx +0 -681
  23. package/template/client/src/components/business-ui/record-comments/internal/icons.tsx +0 -148
  24. package/template/client/src/components/business-ui/record-comments/internal/images.tsx +0 -155
  25. package/template/client/src/components/business-ui/record-comments/internal/mentions.tsx +0 -163
  26. package/template/client/src/components/business-ui/record-comments/internal/model.ts +0 -581
  27. package/template/client/src/components/business-ui/record-comments/internal/mutations.ts +0 -198
  28. package/template/client/src/components/business-ui/record-comments/internal/reaction-catalog.ts +0 -196
  29. package/template/client/src/components/business-ui/record-comments/internal/reactions.tsx +0 -683
  30. package/template/client/src/components/business-ui/record-comments/internal/styles.css +0 -1109
  31. package/template/client/src/components/business-ui/record-comments/internal/suggestions.tsx +0 -456
  32. package/template/client/src/components/business-ui/record-comments/internal/view-types.ts +0 -91
  33. package/template/client/src/components/business-ui/record-comments/types.ts +0 -121
@@ -1,683 +0,0 @@
1
- import spriteUrl from '../assets/reaction-sprite.png';
2
- import { DEFAULT_USED_REACTIONS, REACTION_CATALOG, REACTION_SPRITE_SIZE } from './reaction-catalog';
3
- import { safeUrl, COMMENT_THEME_CLASS } from './model';
4
- import { useMentionContext, useMentionEntity } from './mentions';
5
- import { cn } from '@/lib/utils';
6
- import { Image } from '@/components/ui/image';
7
- import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
8
- import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
9
- import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
10
- import { LoaderCircle } from 'lucide-react';
11
- import * as React from 'react';
12
- import { useEffect, useLayoutEffect, useRef, useState } from 'react';
13
-
14
- /** Independent of reply.content; the page owns persistence and toggle direction. */
15
- export interface CommentReaction {
16
- key: string;
17
- count: number;
18
- mine: boolean;
19
- /** Miaoda user IDs. This can be the server's ahead subset, not the full count. */
20
- users: readonly string[];
21
- }
22
-
23
- export interface CommentEmoji {
24
- text?: string;
25
- key: string;
26
- label: string;
27
- group?: string;
28
- /** Authenticated/custom assets are supplied by the host, not fetched via a Lark gateway here. */
29
- url?: string;
30
- }
31
-
32
- const catalog = new Map(REACTION_CATALOG.map((entry) => [entry[0], entry]));
33
- // Keep the server key for explicit add/delete, alias only its visual asset.
34
- catalog.set('MediumThumbsup', catalog.get('THUMBSUP')!);
35
- export const DEFAULT_REACTION_EMOJIS: readonly CommentEmoji[] =
36
- REACTION_CATALOG.map(([key, label]) => ({ key, label }));
37
-
38
- export function CommentReactionEmoji({
39
- code,
40
- size = 24,
41
- emoji,
42
- }: {
43
- code: string;
44
- size?: number;
45
- emoji?: CommentEmoji;
46
- }) {
47
- const entry = catalog.get(code);
48
- const label = emoji?.label || entry?.[1] || code;
49
- if (emoji?.url && safeUrl(emoji.url))
50
- return (
51
- <Image
52
- src={safeUrl(emoji.url)}
53
- alt={label}
54
- draggable={false}
55
- className="replica-comment-reaction-emoji"
56
- style={{ height: size, width: 'auto' }}
57
- />
58
- );
59
- if (emoji?.text || !entry)
60
- return (
61
- <span className="text-xs" role="img" aria-label={label}>
62
- {emoji?.text || `[${label}]`}
63
- </span>
64
- );
65
- const [, , x, y, width, height] = entry;
66
- const ratio = size / height;
67
- return (
68
- <span
69
- role="img"
70
- aria-label={label}
71
- className="replica-comment-reaction-emoji"
72
- style={{
73
- width: width * ratio,
74
- height: size,
75
- backgroundImage: `url(${spriteUrl})`,
76
- backgroundSize: `${REACTION_SPRITE_SIZE[0] * ratio}px ${REACTION_SPRITE_SIZE[1] * ratio}px`,
77
- backgroundPosition: `${-x * ratio}px ${-y * ratio}px`,
78
- }}
79
- />
80
- );
81
- }
82
-
83
- /** Same 14px default/thumbs-up icon as miaoda/replica-comment-reaction-icon.tsx. */
84
- function ReactionIcon() {
85
- return (
86
- <svg
87
- width="14"
88
- height="14"
89
- viewBox="0 0 14 14"
90
- fill="none"
91
- aria-hidden="true"
92
- >
93
- <path
94
- d="M1.75 12.833h-.583a.29.29 0 01-.292-.291V4.958a.292.292 0 01.292-.291h.583a.292.292 0 01.292.291v7.584a.292.292 0 01-.292.291zm7-8.167h2.501c1.74 0 2.14 1.536 1.74 2.688l-1.74 4.582a1.237 1.237 0 01-1.196.902H3.204a.293.293 0 01-.292-.291v-7.59a.292.292 0 01.292-.291h.6a.583.583 0 00.477-.248L6.702.973c.164-.26.607-.46 1.078-.246.708.322 1.554 1.025 1.554 2.19 0 .44-.195 1.023-.585 1.749z"
95
- fill="currentColor"
96
- />
97
- </svg>
98
- );
99
- }
100
-
101
- /** Source hover timings: 200ms open/close, 400ms suppression after selection. */
102
- function useReactionHover() {
103
- const [open, setOpen] = useState(false);
104
- const opening = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
105
- const closing = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
106
- const until = useRef(0);
107
- const cancel = () => {
108
- clearTimeout(opening.current);
109
- clearTimeout(closing.current);
110
- opening.current = undefined;
111
- closing.current = undefined;
112
- };
113
- useEffect(() => cancel, []);
114
- return {
115
- open,
116
- change(next: boolean) {
117
- cancel();
118
- if (!next || Date.now() >= until.current) setOpen(next);
119
- },
120
- enter() {
121
- clearTimeout(closing.current);
122
- if (Date.now() < until.current || open || opening.current) return;
123
- opening.current = setTimeout(() => {
124
- opening.current = undefined;
125
- setOpen(true);
126
- }, 200);
127
- },
128
- leave() {
129
- cancel();
130
- closing.current = setTimeout(() => setOpen(false), 200);
131
- },
132
- close() {
133
- cancel();
134
- until.current = Date.now() + 400;
135
- setOpen(false);
136
- },
137
- };
138
- }
139
-
140
- function ReactionPanel({
141
- emojis,
142
- scopeKey,
143
- onSelect,
144
- }: {
145
- emojis: readonly CommentEmoji[];
146
- scopeKey: string;
147
- onSelect: (key: string) => void;
148
- }) {
149
- const storageKey = `miaoda-comment-replica-reactions:${scopeKey}`;
150
- const [recent, setRecent] = useState<string[]>(() => {
151
- try {
152
- const saved: unknown = JSON.parse(
153
- localStorage.getItem(storageKey) || 'null',
154
- );
155
- if (Array.isArray(saved) && saved.every((key) => typeof key === 'string'))
156
- return saved;
157
- } catch {
158
- /* Storage is optional; preserve the source default order when unavailable. */
159
- }
160
- return DEFAULT_USED_REACTIONS;
161
- });
162
- const byKey = new Map(emojis.map((emoji) => [emoji.key, emoji]));
163
- const groups = new Map<string, CommentEmoji[]>();
164
- for (const emoji of emojis) {
165
- const title = emoji.group || '默认表情';
166
- const group = groups.get(title) || [];
167
- group.push(emoji);
168
- groups.set(title, group);
169
- }
170
- const select = (key: string) => {
171
- const next = [key, ...recent.filter((item) => item !== key)].slice(0, 30);
172
- setRecent(next);
173
- try {
174
- localStorage.setItem(storageKey, JSON.stringify(next));
175
- } catch {
176
- /* Selection works without storage. */
177
- }
178
- onSelect(key);
179
- };
180
- const keydown = (event: React.KeyboardEvent<HTMLDivElement>) => {
181
- if (
182
- ![
183
- 'ArrowLeft',
184
- 'ArrowRight',
185
- 'ArrowDown',
186
- 'ArrowUp',
187
- 'Home',
188
- 'End',
189
- ].includes(event.key)
190
- )
191
- return;
192
- const group = (event.target as HTMLElement).closest('[data-reaction-grid]');
193
- const buttons = Array.from(
194
- group?.querySelectorAll<HTMLButtonElement>('button') || [],
195
- );
196
- if (!buttons.length) return;
197
- const index = buttons.indexOf(event.target as HTMLButtonElement);
198
- const delta =
199
- { ArrowLeft: -1, ArrowRight: 1, ArrowDown: 9, ArrowUp: -9 }[event.key] ||
200
- 0;
201
- const next =
202
- event.key === 'Home'
203
- ? 0
204
- : event.key === 'End'
205
- ? buttons.length - 1
206
- : (index + delta + buttons.length) % buttons.length;
207
- event.preventDefault();
208
- buttons[next]?.focus();
209
- };
210
- const item = (emoji: CommentEmoji, size = 24) => {
211
- const entry = catalog.get(emoji.key);
212
- const width = entry
213
- ? size === 28
214
- ? (entry[4] / entry[5]) * size
215
- : Math.max(36, (entry[4] / entry[5]) * size + 4)
216
- : size;
217
- return (
218
- <button
219
- key={emoji.key}
220
- type="button"
221
- aria-label={emoji.label}
222
- title={emoji.label}
223
- className="replica-comment-reaction-choice"
224
- style={{ minWidth: width }}
225
- onClick={() => select(emoji.key)}
226
- >
227
- <CommentReactionEmoji code={emoji.key} size={size} emoji={emoji} />
228
- </button>
229
- );
230
- };
231
- return (
232
- <div
233
- className="replica-comment-reaction-panel"
234
- onKeyDown={keydown}
235
- onClick={(event) => event.stopPropagation()}
236
- >
237
- <div className="replica-comment-reaction-recent">
238
- <div className="replica-comment-reaction-used-title">最近使用</div>
239
- <div className="replica-comment-reaction-used-grid" data-reaction-grid>
240
- {recent
241
- .filter((key) => byKey.has(key))
242
- .slice(0, 7)
243
- .flatMap((key) => {
244
- const emoji = byKey.get(key);
245
- return emoji ? [item(emoji, 28)] : [];
246
- })}
247
- </div>
248
- </div>
249
- <div className="replica-comment-reaction-scroll">
250
- {[...groups].map(([title, group]) => (
251
- <React.Fragment key={title}>
252
- <div className="replica-comment-reaction-all-title">{title}</div>
253
- <div className="replica-comment-reaction-grid" data-reaction-grid>
254
- {group.map((emoji) => item(emoji))}
255
- </div>
256
- </React.Fragment>
257
- ))}
258
- </div>
259
- </div>
260
- );
261
- }
262
-
263
- export function CommentReactionEntry({
264
- onToggle,
265
- onOpenChange,
266
- disabled,
267
- emojis = DEFAULT_REACTION_EMOJIS,
268
- }: {
269
- onToggle: (key: string) => void;
270
- onOpenChange?: (open: boolean) => void;
271
- disabled?: boolean;
272
- emojis?: readonly CommentEmoji[];
273
- }) {
274
- const { store } = useMentionContext();
275
- const hover = useReactionHover();
276
- const keyboardOpen = useRef(false);
277
- const quick = emojis.find(e=>e.key==='THUMBSUP') ?? emojis[0];
278
- useEffect(() => {
279
- onOpenChange?.(hover.open);
280
- }, [hover.open, onOpenChange]);
281
- useEffect(() => {
282
- if (disabled) hover.change(false);
283
- }, [disabled]);
284
- return (
285
- <Popover
286
- open={!disabled && hover.open}
287
- onOpenChange={hover.change}
288
- modal={false}
289
- >
290
- <PopoverTrigger asChild>
291
- <button
292
- type="button"
293
- aria-label="表情回复"
294
- title="表情回复(点击点赞,悬停选择)"
295
- disabled={disabled || !quick}
296
- className={cn(
297
- COMMENT_THEME_CLASS,
298
- 'inline-flex size-[14px] items-center justify-center text-[var(--subtle-foreground)] hover:text-foreground disabled:text-muted-foreground',
299
- )}
300
- onMouseEnter={() => {
301
- keyboardOpen.current = false;
302
- hover.enter();
303
- }}
304
- onMouseLeave={hover.leave}
305
- onKeyDown={(event) => {
306
- if (event.key === 'ArrowDown') {
307
- event.preventDefault();
308
- keyboardOpen.current = true;
309
- hover.change(true);
310
- }
311
- }}
312
- onClick={(event) => {
313
- event.preventDefault();
314
- event.stopPropagation();
315
- hover.close();
316
- if(quick)onToggle(quick.key);
317
- }}
318
- >
319
- <ReactionIcon />
320
- </button>
321
- </PopoverTrigger>
322
- {hover.open && !disabled ? (
323
- <PopoverContent
324
- align="end"
325
- side="bottom"
326
- sideOffset={6}
327
- data-replica-comment-overlay
328
- data-reaction-picker
329
- className={cn(
330
- COMMENT_THEME_CLASS,
331
- 'w-auto max-w-[calc(100vw-24px)] overflow-hidden rounded-lg border-[0.5px] p-0 data-[state=closed]:animate-none!',
332
- )}
333
- onOpenAutoFocus={(event) => {
334
- event.preventDefault();
335
- if (keyboardOpen.current && event.target instanceof HTMLElement)
336
- event.target
337
- .querySelector<HTMLButtonElement>('.replica-comment-reaction-choice')
338
- ?.focus();
339
- }}
340
- onMouseEnter={hover.enter}
341
- onMouseLeave={hover.leave}
342
- >
343
- <ReactionPanel
344
- key={store.source.scopeKey}
345
- scopeKey={store.source.scopeKey}
346
- emojis={emojis}
347
- onSelect={(key) => {
348
- hover.close();
349
- onToggle(key);
350
- }}
351
- />
352
- </PopoverContent>
353
- ) : null}
354
- </Popover>
355
- );
356
- }
357
-
358
- type UserClick = (userID: string, anchor: HTMLElement) => void;
359
- function ReactionPerson({
360
- userID,
361
- onUserClick,
362
- avatar = false,
363
- comma = false,
364
- }: {
365
- userID: string;
366
- onUserClick?: UserClick;
367
- avatar?: boolean;
368
- comma?: boolean;
369
- }) {
370
- const { resolveUserRef } = useMentionContext();
371
- const entity = useMentionEntity(resolveUserRef?.(userID) || {
372
- kind: 'user', idType: 'apaas-user', id: userID,
373
- });
374
- const name: string =
375
- entity?.status === 'resolved'
376
- ? entity.label
377
- : entity?.status === 'not-found'
378
- ? '用户不存在'
379
- : '用户';
380
- const avatarUrl =
381
- entity?.status === 'resolved' ? safeUrl(entity.avatarUrl) : undefined;
382
- return (
383
- <button
384
- type="button"
385
- className={avatar ? 'replica-comment-reaction-person' : 'replica-comment-reaction-user'}
386
- onClick={(event) => {
387
- event.stopPropagation();
388
- onUserClick?.(userID, event.currentTarget);
389
- }}
390
- >
391
- {avatar ? (
392
- <Avatar className="mr-2.5 size-8 shrink-0">
393
- <AvatarImage src={avatarUrl} alt="" />
394
- <AvatarFallback>{Array.from(name)[0]}</AvatarFallback>
395
- </Avatar>
396
- ) : null}
397
- <span className="truncate" title={name}>
398
- {name}
399
- {comma ? ',' : ''}
400
- </span>
401
- </button>
402
- );
403
- }
404
-
405
- function ReactionBadge({
406
- reaction,
407
- emoji,
408
- onToggle,
409
- onMore,
410
- onUserClick,
411
- }: {
412
- reaction: CommentReaction;
413
- emoji?: CommentEmoji;
414
- onToggle?: (key: string) => void;
415
- onMore: () => void;
416
- onUserClick?: UserClick;
417
- }) {
418
- const badge = useRef<HTMLDivElement>(null);
419
- const [visible, setVisible] = useState(reaction.users.length);
420
- useLayoutEffect(() => {
421
- const element = badge.current;
422
- if (!element?.parentElement) return;
423
- const measure = () => {
424
- const names = Array.from(
425
- element.querySelectorAll<HTMLElement>('[data-measure-user]'),
426
- );
427
- let available = Math.max(0, element.parentElement!.clientWidth - 41);
428
- let count = 0;
429
- const max = names.length > 6 ? 5 : names.length;
430
- const moreWidth = reaction.count > max ? 50 : 0;
431
- for (const name of names.slice(0, max)) {
432
- const width = name.getBoundingClientRect().width + 5;
433
- if (count && width + moreWidth > available) break;
434
- available -= width;
435
- count++;
436
- }
437
- setVisible(count);
438
- };
439
- const observer = new ResizeObserver(measure);
440
- observer.observe(element.parentElement);
441
- observer.observe(element);
442
- const labels = element.querySelector('.replica-comment-reaction-measure');
443
- if (labels) observer.observe(labels);
444
- measure();
445
- return () => observer.disconnect();
446
- }, [reaction.users, reaction.count]);
447
- const count = Math.min(visible, reaction.users.length);
448
- const remaining = Math.max(0, reaction.count - count);
449
- return (
450
- <div
451
- ref={badge}
452
- className="replica-comment-reaction-badge"
453
- data-mine={reaction.mine || undefined}
454
- onClick={(event) => event.stopPropagation()}
455
- >
456
- <button
457
- type="button"
458
- className="shrink-0"
459
- aria-label={`${reaction.mine ? '撤销' : '添加'}${emoji?.label || catalog.get(reaction.key)?.[1] || reaction.key}`}
460
- disabled={!onToggle}
461
- onClick={() => onToggle?.(reaction.key)}
462
- >
463
- <CommentReactionEmoji code={reaction.key} size={18} emoji={emoji} />
464
- </button>
465
- <span className="replica-comment-reaction-separator" />
466
- <div className="replica-comment-reaction-users">
467
- {reaction.users.slice(0, count).map((userID, index) => (
468
- <ReactionPerson
469
- key={userID}
470
- userID={userID}
471
- comma={index < reaction.count - 1}
472
- onUserClick={onUserClick}
473
- />
474
- ))}
475
- </div>
476
- {remaining ? (
477
- <button
478
- type="button"
479
- className="replica-comment-reaction-more"
480
- onClick={onMore}
481
- >
482
- +{remaining} 人
483
- </button>
484
- ) : null}
485
- <div className="replica-comment-reaction-measure" aria-hidden="true">
486
- {reaction.users.map((userID) => (
487
- <span key={userID} data-measure-user>
488
- <ReactionPerson userID={userID} comma />
489
- </span>
490
- ))}
491
- </div>
492
- </div>
493
- );
494
- }
495
-
496
- export function CommentReactionList({
497
- replyId,
498
- reactions,
499
- onToggle,
500
- onUserClick,
501
- loadDetails,
502
- emojis = DEFAULT_REACTION_EMOJIS,
503
- }: {
504
- replyId: string;
505
- reactions: readonly CommentReaction[];
506
- onToggle?: (key: string) => void;
507
- onUserClick?: UserClick;
508
- /** Fetch full data when the list contains only an ahead subset. */
509
- loadDetails?: (signal: AbortSignal) => Promise<readonly CommentReaction[]>;
510
- emojis?: readonly CommentEmoji[];
511
- }) {
512
- const { store } = useMentionContext();
513
- const sessionKey = JSON.stringify([store.source.scopeKey, replyId]);
514
- const [target, setTarget] = useState<{
515
- sessionKey: string;
516
- key: string;
517
- } | null>(null);
518
- const activeKey = target?.sessionKey === sessionKey ? target.key : null;
519
- const setActiveKey = (key: string | null) =>
520
- setTarget(key === null ? null : { sessionKey, key });
521
- const [full, setFull] = useState<{
522
- sessionKey: string;
523
- data: readonly CommentReaction[];
524
- } | null>(null);
525
- const [error, setError] = useState('');
526
- const [retry, setRetry] = useState(0);
527
- const latest = useRef({ reactions, loadDetails });
528
- latest.current = { reactions, loadDetails };
529
- const open =
530
- activeKey !== null && reactions.some((reaction) => reaction.count > 0);
531
- useEffect(() => {
532
- setFull(null);
533
- setError('');
534
- if (!open) return;
535
- const controller = new AbortController();
536
- const load = latest.current.loadDetails;
537
- if (!load) {
538
- setFull({ sessionKey, data: latest.current.reactions });
539
- return;
540
- }
541
- void load(controller.signal)
542
- .then((data) => {
543
- if (!controller.signal.aborted) setFull({ sessionKey, data });
544
- })
545
- .catch((cause: unknown) => {
546
- if (!controller.signal.aborted)
547
- setError(cause instanceof Error ? cause.message : '加载失败,请重试');
548
- });
549
- return () => controller.abort();
550
- }, [open, sessionKey, retry]);
551
- const data = loadDetails
552
- ? full?.sessionKey === sessionKey
553
- ? full.data
554
- : null
555
- : reactions;
556
- const selected =
557
- data?.find((reaction) => reaction.key === activeKey) || data?.[0];
558
- if (!reactions.length) return null;
559
- return (
560
- <>
561
- <div
562
- className={cn(COMMENT_THEME_CLASS, 'mt-1 flex flex-wrap gap-1')}
563
- data-replica-comment-reactions
564
- >
565
- {reactions
566
- .filter((reaction) => reaction.count > 0)
567
- .map((reaction) => (
568
- <ReactionBadge
569
- key={reaction.key}
570
- reaction={reaction}
571
- emoji={emojis.find((emoji) => emoji.key === reaction.key)}
572
- onToggle={onToggle}
573
- onMore={() => setActiveKey(reaction.key)}
574
- onUserClick={onUserClick}
575
- />
576
- ))}
577
- </div>
578
- <Dialog
579
- open={open}
580
- onOpenChange={(next) => {
581
- if (!next) setActiveKey(null);
582
- }}
583
- >
584
- <DialogContent
585
- data-replica-comment-overlay
586
- className={cn(
587
- COMMENT_THEME_CLASS,
588
- 'w-auto max-w-[calc(100vw-24px)] gap-0 p-0 sm:max-w-none',
589
- )}
590
- onClick={(event) => event.stopPropagation()}
591
- onInteractOutside={(event) => {
592
- if (
593
- event.target instanceof Element &&
594
- event.target.closest('[data-replica-comment-overlay]')
595
- )
596
- event.preventDefault();
597
- }}
598
- aria-describedby={undefined}
599
- >
600
- <DialogHeader className="px-6 pb-2 pt-6 text-left">
601
- <DialogTitle className="text-base font-medium leading-6">
602
- 回应详情
603
- </DialogTitle>
604
- </DialogHeader>
605
- <div className="px-6 pb-6">
606
- <div className="replica-comment-reaction-detail">
607
- {error ? (
608
- <div
609
- role="alert"
610
- className="flex h-full items-center justify-center gap-2 text-sm"
611
- >
612
- <span>{error}</span>
613
- <button
614
- type="button"
615
- className="text-primary"
616
- onClick={() => setRetry((value) => value + 1)}
617
- >
618
- 重试
619
- </button>
620
- </div>
621
- ) : !data ? (
622
- <div className="flex h-full items-center justify-center">
623
- <LoaderCircle
624
- className="size-5 animate-spin text-primary"
625
- aria-label="正在加载回应人员"
626
- />
627
- </div>
628
- ) : (
629
- <>
630
- <div
631
- className="replica-comment-reaction-tabs"
632
- role="tablist"
633
- aria-label="回应表情"
634
- >
635
- {data.map((reaction) => (
636
- <button
637
- type="button"
638
- role="tab"
639
- key={reaction.key}
640
- aria-selected={selected?.key === reaction.key}
641
- onClick={() => setActiveKey(reaction.key)}
642
- >
643
- <CommentReactionEmoji
644
- code={reaction.key}
645
- size={20}
646
- emoji={emojis.find(
647
- (emoji) => emoji.key === reaction.key,
648
- )}
649
- />
650
- <span>{reaction.count}</span>
651
- </button>
652
- ))}
653
- </div>
654
- <div
655
- className="replica-comment-reaction-detail-border"
656
- role="tabpanel"
657
- aria-label="回应人员"
658
- >
659
- <div className="replica-comment-reaction-detail-users">
660
- {selected?.users.map((userID) => (
661
- <ReactionPerson
662
- key={userID}
663
- userID={userID}
664
- avatar
665
- onUserClick={onUserClick}
666
- />
667
- ))}
668
- </div>
669
- {selected && selected.count > selected.users.length ? (
670
- <p className="p-3 text-xs text-[var(--subtle-foreground)]">
671
- 当前仅显示部分回应人员。
672
- </p>
673
- ) : null}
674
- </div>
675
- </>
676
- )}
677
- </div>
678
- </div>
679
- </DialogContent>
680
- </Dialog>
681
- </>
682
- );
683
- }