@lark-apaas/coding-template-nestjs-react-fullstack 0.1.56 → 0.1.57-alpha.20261103143319

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 (26) hide show
  1. package/package.json +1 -1
  2. package/template/client/src/components/business-ui/README.md +2 -0
  3. package/template/client/src/components/business-ui/record-comments/README.md +141 -0
  4. package/template/client/src/components/business-ui/record-comments/app-source.ts +93 -0
  5. package/template/client/src/components/business-ui/record-comments/assets/reaction-sprite.png +0 -0
  6. package/template/client/src/components/business-ui/record-comments/comment-panel.contract.ts +240 -0
  7. package/template/client/src/components/business-ui/record-comments/comment-panel.tsx +231 -0
  8. package/template/client/src/components/business-ui/record-comments/extensions.ts +129 -0
  9. package/template/client/src/components/business-ui/record-comments/index.ts +9 -0
  10. package/template/client/src/components/business-ui/record-comments/internal/composer.tsx +529 -0
  11. package/template/client/src/components/business-ui/record-comments/internal/content.tsx +213 -0
  12. package/template/client/src/components/business-ui/record-comments/internal/discussion.tsx +517 -0
  13. package/template/client/src/components/business-ui/record-comments/internal/editor.tsx +675 -0
  14. package/template/client/src/components/business-ui/record-comments/internal/icons.tsx +148 -0
  15. package/template/client/src/components/business-ui/record-comments/internal/images.tsx +155 -0
  16. package/template/client/src/components/business-ui/record-comments/internal/mentions.tsx +163 -0
  17. package/template/client/src/components/business-ui/record-comments/internal/model.ts +581 -0
  18. package/template/client/src/components/business-ui/record-comments/internal/mutations.ts +170 -0
  19. package/template/client/src/components/business-ui/record-comments/internal/reaction-catalog.ts +196 -0
  20. package/template/client/src/components/business-ui/record-comments/internal/reactions.tsx +681 -0
  21. package/template/client/src/components/business-ui/record-comments/internal/styles.css +1109 -0
  22. package/template/client/src/components/business-ui/record-comments/internal/suggestions.tsx +456 -0
  23. package/template/client/src/components/business-ui/record-comments/internal/view-types.ts +84 -0
  24. package/template/client/src/components/business-ui/record-comments/types.ts +118 -0
  25. package/template/package-lock.json +529 -13
  26. package/template/package.json +15 -4
@@ -0,0 +1,170 @@
1
+ import type { MentionRef } from '../types';
2
+ import type { DiscussionDraft, DiscussionImage, DiscussionReply, DiscussionThread } from './view-types';
3
+ import type { CommentIDs, DeleteCommentResult, CommentMutationFailure } from '../comment-panel.contract';
4
+ import { commentFailureMessage, normalizeValue, readApiContent } from './model';
5
+
6
+ export interface LocalCommentMutation {
7
+ id: string;
8
+ kind: 'add' | 'edit' | 'delete';
9
+ phase: 'sending' | 'failed' | 'uncertain' | 'confirmed';
10
+ message?: string;
11
+ }
12
+ interface Operation extends LocalCommentMutation {
13
+ thread: DiscussionThread;
14
+ commentID: string;
15
+ replyID: string;
16
+ draft?: DiscussionDraft;
17
+ author?: MentionRef;
18
+ createTime: number;
19
+ wholeThread?: boolean;
20
+ execute: () => Promise<CommentIDs | DeleteCommentResult>;
21
+ }
22
+ const contentKey = (reply: Pick<DiscussionReply, 'value'>) => JSON.stringify(readApiContent(reply.value));
23
+ const imageKey = (images: readonly DiscussionImage[]) => JSON.stringify(images.map(({ id, url }) => [id, url]));
24
+ const uncertain = (message: string) => Object.assign(new Error(message), { outcome: 'unknown' as const });
25
+
26
+ /** Component-local overlay. Never writes the host's data or performs a request itself. */
27
+ export class CommentMutations {
28
+ private operations: Operation[] = [];
29
+ private listeners = new Set<() => void>();
30
+ private version = 0;
31
+ private active = true;
32
+ subscribe = (listener: () => void) => { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; };
33
+ getVersion = () => this.version;
34
+ private changed() { if (this.active) { this.version++; this.listeners.forEach((listener) => listener()); } }
35
+ activate() { this.active = true; }
36
+ private release(operation: Operation) {
37
+ for (const image of operation.draft?.images ?? [])
38
+ if (image.file && image.url.startsWith('blob:')) URL.revokeObjectURL(image.url);
39
+ }
40
+ dispose() {
41
+ this.active = false;
42
+ this.operations.forEach((operation) => this.release(operation));
43
+ this.operations = [];
44
+ }
45
+ replyID(id: string) { return this.operations.find((operation) => operation.id === id)?.replyID ?? id; }
46
+ private assertAvailable(commentID: string, replyID?: string) {
47
+ if (!this.active) throw new Error('评论会话已关闭');
48
+ if (this.operations.some((operation) => operation.commentID === commentID &&
49
+ (operation.wholeThread || (operation.kind === 'add' && !operation.draft?.commentID) || operation.replyID === replyID)))
50
+ throw new Error('该评论仍在处理中,请先完成同步或处理失败提示');
51
+ }
52
+ private start(operation: Operation) {
53
+ this.operations.push(operation); this.changed();
54
+ void this.run(operation);
55
+ return operation.id;
56
+ }
57
+ add(thread: DiscussionThread | undefined, draft: DiscussionDraft, author: MentionRef | undefined, execute: () => Promise<CommentIDs>) {
58
+ if (thread) this.assertAvailable(thread.commentID);
59
+ else if (!this.active) throw new Error('评论会话已关闭');
60
+ const id = `local:${crypto.randomUUID()}`;
61
+ const createTime = Math.floor(Date.now() / 1000);
62
+ const commentID = thread?.commentID ?? `local:thread:${crypto.randomUUID()}`;
63
+ return this.start({ id, kind: 'add', phase: 'sending', commentID, replyID: id,
64
+ thread: thread ?? { commentID, creator: author ?? null, createTime, isSolved: false, replies: [] },
65
+ draft: { ...draft, commentID: thread?.commentID, value: normalizeValue(draft.value), images: [...draft.images] }, author, createTime, execute });
66
+ }
67
+ edit(thread: DiscussionThread, replyID: string, draft: DiscussionDraft, execute: () => Promise<CommentIDs>) {
68
+ this.assertAvailable(thread.commentID, replyID);
69
+ return this.start({ id: `local:${crypto.randomUUID()}`, kind: 'edit', phase: 'sending',
70
+ thread, commentID: thread.commentID, replyID, createTime: Math.floor(Date.now() / 1000),
71
+ draft: { ...draft, value: normalizeValue(draft.value), images: [...draft.images] }, execute });
72
+ }
73
+ delete(thread: DiscussionThread, replyID: string, execute: () => Promise<DeleteCommentResult>) {
74
+ // The UI always deletes one selected message, including the current body.
75
+ const wholeThread = false;
76
+ this.assertAvailable(thread.commentID, replyID);
77
+ return this.start({ id: `local:${crypto.randomUUID()}`, kind: 'delete', phase: 'sending',
78
+ thread, commentID: thread.commentID, replyID, wholeThread, createTime: Math.floor(Date.now() / 1000), execute });
79
+ }
80
+ private async run(operation: Operation) {
81
+ operation.phase = 'sending'; operation.message = undefined; this.changed();
82
+ try {
83
+ const result = await operation.execute();
84
+ if (!this.active) return;
85
+ if (!/^\d+$/.test(result.commentID)) throw uncertain('提交结果缺少有效评论 ID');
86
+ if (operation.kind !== 'add' || operation.draft?.commentID !== undefined) {
87
+ if (result.commentID !== operation.commentID) throw uncertain('提交结果与目标讨论不一致');
88
+ }
89
+ if (operation.kind === 'delete') {
90
+ if (!('commentDeleted' in result) || typeof result.commentDeleted !== 'boolean') throw uncertain('删除结果未确认');
91
+ operation.wholeThread = result.commentDeleted;
92
+ } else {
93
+ if (!('replyID' in result) || !/^\d+$/.test(result.replyID)) throw uncertain('提交结果缺少有效回复 ID');
94
+ if (operation.kind === 'edit' && result.replyID !== operation.replyID) throw uncertain('编辑结果与目标回复不一致');
95
+ operation.replyID = result.replyID;
96
+ }
97
+ operation.commentID = result.commentID;
98
+ operation.phase = 'confirmed';
99
+ } catch (cause) {
100
+ if (!this.active) return;
101
+ operation.phase = (cause as CommentMutationFailure | null)?.outcome === 'unknown' ? 'uncertain' : 'failed';
102
+ operation.message = commentFailureMessage(cause, '操作失败,请重试');
103
+ }
104
+ this.changed();
105
+ }
106
+ retry(id: string) {
107
+ const operation = this.operations.find((item) => item.id === id);
108
+ if (!operation || operation.phase !== 'failed' || !this.active) return;
109
+ void this.run(operation);
110
+ }
111
+ dismiss(id: string) {
112
+ const operation = this.operations.find((item) => item.id === id);
113
+ if (!operation || !['failed', 'uncertain'].includes(operation.phase)) return;
114
+ this.operations = this.operations.filter((item) => item !== operation);
115
+ this.release(operation); this.changed();
116
+ }
117
+ private acknowledged(operation: Operation, base: readonly DiscussionThread[]) {
118
+ const thread = base.find((item) => item.commentID === operation.commentID);
119
+ const reply = thread?.replies.find((item) => item.replyID === operation.replyID);
120
+ if (operation.kind === 'delete') return operation.wholeThread ? !thread : !reply;
121
+ if (!reply) return false;
122
+ if (operation.kind === 'add') return true;
123
+ const before = operation.thread.replies.find((item) => item.replyID === operation.replyID)!;
124
+ // A later authoritative edit wins even when another editor changed the text.
125
+ // Compare server revisions, never the optimistic client wall clock.
126
+ if (operation.phase === 'confirmed' && reply.updateTime !== undefined &&
127
+ reply.updateTime > (before.updateTime ?? before.createTime)) return true;
128
+ if (contentKey(reply) !== contentKey({ value: operation.draft!.value })) return false;
129
+ const images = operation.draft!.images;
130
+ if (imageKey(images) === imageKey(before.images)) return true;
131
+ return images.some((image) => image.file)
132
+ ? reply.images.length === images.length && imageKey(reply.images) !== imageKey(before.images)
133
+ : imageKey(reply.images) === imageKey(images);
134
+ }
135
+ reconcile(base: readonly DiscussionThread[]) {
136
+ const finished = this.operations.filter((operation) => (operation.phase === 'confirmed' ||
137
+ (operation.phase === 'uncertain' && operation.kind !== 'add')) && this.acknowledged(operation, base));
138
+ if (!finished.length) return;
139
+ this.operations = this.operations.filter((operation) => !finished.includes(operation));
140
+ finished.forEach((operation) => this.release(operation)); this.changed();
141
+ }
142
+ view(base: readonly DiscussionThread[]): DiscussionThread[] {
143
+ let threads = base.map((thread) => ({ ...thread, replies: [...thread.replies] }));
144
+ for (const operation of this.operations) {
145
+ let thread = threads.find((item) => item.commentID === operation.commentID);
146
+ if (operation.kind === 'delete' && operation.phase === 'confirmed') {
147
+ if (operation.wholeThread) threads = threads.filter((item) => item !== thread);
148
+ else if (thread) thread.replies = thread.replies.filter((reply) => reply.replyID !== operation.replyID);
149
+ continue;
150
+ }
151
+ if (!thread) {
152
+ thread = { ...operation.thread, commentID: operation.commentID, replies: [...operation.thread.replies] };
153
+ threads.push(thread);
154
+ }
155
+ const index = thread.replies.findIndex((item) => item.replyID === operation.replyID);
156
+ const before = thread.replies[index] ?? operation.thread.replies.find((item) => item.replyID === operation.replyID);
157
+ const mutation: LocalCommentMutation = { id: operation.id, kind: operation.kind, phase: operation.phase, message: operation.message };
158
+ const reply: DiscussionReply = operation.kind === 'add'
159
+ ? { replyID: operation.replyID, author: operation.author ?? null, createTime: operation.createTime,
160
+ value: operation.draft!.value, images: operation.draft!.images, reactions: [], mutation }
161
+ : { ...before!, ...(operation.draft ? { value: operation.draft.value, images: operation.draft.images,
162
+ edited: before?.edited || operation.phase === 'confirmed',
163
+ updateTime: operation.phase === 'confirmed' ? operation.createTime : before?.updateTime } : {}), mutation };
164
+ if (index < 0) thread.replies.push(reply); else thread.replies[index] = reply;
165
+ if ((operation.kind === 'delete' && operation.wholeThread) || (operation.kind === 'add' && !operation.draft?.commentID))
166
+ thread.replies = thread.replies.map((item) => ({ ...item, mutationBlocked: true }));
167
+ }
168
+ return threads.filter((thread) => thread.replies.length > 0);
169
+ }
170
+ }
@@ -0,0 +1,196 @@
1
+
2
+
3
+ /** UD biz-react 6.11.4 Emoji: original zh-CN labels, order and sprite coordinates. */
4
+ export const REACTION_SPRITE_SIZE = [1741, 1632] as const;
5
+ export const DEFAULT_USED_REACTIONS = [
6
+ 'MUSCLE',
7
+ 'APPLAUSE',
8
+ 'OK',
9
+ 'THUMBSUP',
10
+ 'HEART',
11
+ 'JIAYI',
12
+ 'DONE',
13
+ 'BLUSH',
14
+ 'FACEPALM',
15
+ ];
16
+ export const REACTION_CATALOG: ReadonlyArray<
17
+ readonly [
18
+ key: string,
19
+ label: string,
20
+ x: number,
21
+ y: number,
22
+ width: number,
23
+ height: number,
24
+ ]
25
+ > = [
26
+ ['OK', 'OK', 208, 576, 104, 96],
27
+ ['THUMBSUP', '赞', 96, 1536, 96, 96],
28
+ ['THANKS', '双手合十', 1549, 1248, 96, 96],
29
+ ['MUSCLE', '加油', 288, 1056, 96, 96],
30
+ ['FINGERHEART', '比心', 384, 1056, 96, 96],
31
+ ['APPLAUSE', '鼓掌', 579, 768, 96, 96],
32
+ ['FISTBUMP', '碰拳', 768, 1056, 96, 96],
33
+ ['JIAYI', '+1', 1453, 672, 96, 96],
34
+ ['DONE', '完成', 1069, 96, 96, 96],
35
+ ['SMILE', '微笑', 1248, 1440, 96, 96],
36
+ ['Delighted', '愉快', 864, 960, 96, 96],
37
+ ['BeamingFace', '笑容满面', 877, 192, 96, 96],
38
+ ['BLUSH', '呲牙', 0, 1152, 96, 96],
39
+ ['LAUGH', '大笑', 1056, 1152, 96, 96],
40
+ ['SMIRK', '坏笑', 194, 768, 97, 96],
41
+ ['LOL', '笑哭', 0, 480, 108, 96],
42
+ ['FACEPALM', '捂脸', 192, 1056, 96, 96],
43
+ ['LOVE', '送心', 192, 1248, 96, 96],
44
+ ['WINK', '调皮', 1440, 1536, 96, 96],
45
+ ['PROUD', '得意', 1453, 864, 96, 96],
46
+ ['WITTY', '灵光一闪', 512, 672, 102, 96],
47
+ ['SMART', '机智', 1152, 1440, 96, 96],
48
+ ['SCOWL', '惊呆', 0, 1440, 96, 96],
49
+ ['THINKING', '思考', 1549, 1440, 96, 96],
50
+ ['SOB', '流泪', 1549, 0, 96, 96],
51
+ ['CRY', '泣不成声', 973, 480, 96, 96],
52
+ ['ERROR', '黑线', 0, 1056, 96, 96],
53
+ ['NOSEPICK', '抠鼻', 1453, 96, 96, 96],
54
+ ['HAUGHTY', '烦躁', 768, 1152, 96, 96],
55
+ ['SLAP', '打脸', 864, 1440, 96, 96],
56
+ ['SPITBLOOD', '吐血', 410, 672, 102, 96],
57
+ ['TOASTED', '衰', 192, 1536, 96, 96],
58
+ ['ColdSweat', '冷汗', 973, 96, 96, 96],
59
+ ['BLACKFACE', '新月脸', 877, 576, 96, 96],
60
+ ['FullMoonFace', '满月脸', 1056, 1056, 96, 96],
61
+ ['GLANCE', '看', 1165, 864, 96, 96],
62
+ ['DULL', '呆无辜', 1069, 480, 96, 96],
63
+ ['ROSE', '玫瑰', 1453, 1248, 96, 96],
64
+ ['HEART', '爱心', 192, 1152, 96, 96],
65
+ ['PARTY', '撒花', 1453, 288, 96, 96],
66
+ ['INNOCENTSMILE', '无辜笑', 672, 1152, 96, 96],
67
+ ['SHY', '害羞', 384, 1440, 96, 96],
68
+ ['CHUCKLE', '偷笑', 672, 864, 96, 96],
69
+ ['JOYFUL', '笑', 864, 1152, 96, 96],
70
+ ['WOW', '惊喜', 1536, 1536, 96, 96],
71
+ ['TRICK', '嬉笑', 384, 1536, 96, 96],
72
+ ['YEAH', '耶', 614, 672, 102, 96],
73
+ ['ENOUGH', '我想静静', 557, 384, 108, 96],
74
+ ['TEARS', '泪奔', 1549, 960, 96, 96],
75
+ ['EMBARRASSED', '尬笑', 1069, 960, 96, 96],
76
+ ['KISS', '亲亲', 960, 1152, 96, 96],
77
+ ['SMOOCH', '飞吻', 777, 192, 100, 96],
78
+ ['DROOL', '爱慕', 1069, 192, 96, 96],
79
+ ['OBSESSED', '心心脸', 384, 1152, 96, 96],
80
+ ['MONEY', '财迷', 1248, 1344, 96, 96],
81
+ ['TEASE', '做鬼脸', 1549, 1056, 96, 96],
82
+ ['SHOWOFF', '酷', 288, 1440, 96, 96],
83
+ ['COMFORT', '摸头', 973, 192, 96, 96],
84
+ ['CLAP', '欢呼', 206, 672, 102, 96],
85
+ ['PRAISE', '强', 0, 672, 103, 96],
86
+ ['STRIVE', '奋斗', 1549, 864, 96, 96],
87
+ ['XBLUSH', '脸红', 877, 768, 96, 96],
88
+ ['SILENT', '闭嘴', 672, 1440, 96, 96],
89
+ ['WAVE', '再见', 777, 384, 99, 96],
90
+ ['WHAT', '什么?', 520, 576, 104, 96],
91
+ ['FROWN', '皱眉', 960, 1056, 96, 96],
92
+ ['DIZZY', '晕', 1069, 0, 96, 96],
93
+ ['LOOKDOWN', '鄙视', 0, 1248, 96, 96],
94
+ ['WAIL', '大哭', 447, 384, 110, 96],
95
+ ['CRAZY', '抓狂', 973, 288, 96, 96],
96
+ ['WHIMPER', '可怜', 1344, 1536, 96, 96],
97
+ ['HUG', '求抱抱', 338, 288, 132, 96],
98
+ ['BLUBBER', '快哭了', 877, 672, 96, 96],
99
+ ['WRONGED', '委屈', 1645, 0, 96, 96],
100
+ ['HUSKY', '翻白眼', 576, 1152, 96, 96],
101
+ ['SHHH', '嘘', 192, 1440, 96, 96],
102
+ ['SMUG', '撇嘴', 1344, 1440, 96, 96],
103
+ ['ANGRY', '发怒', 483, 768, 96, 96],
104
+ ['HAMMER', '敲打', 96, 1152, 96, 96],
105
+ ['SHOCKED', '震惊', 324, 480, 107, 96],
106
+ ['TERROR', '惊恐', 1549, 1152, 96, 96],
107
+ ['PETRIFIED', '石化', 1453, 480, 96, 96],
108
+ ['SKULL', '骷髅', 768, 1440, 96, 96],
109
+ ['SWEAT', '汗', 777, 288, 100, 96],
110
+ ['SPEECHLESS', '无语', 103, 672, 103, 96],
111
+ ['SLEEP', '鼾睡', 960, 1440, 96, 96],
112
+ ['DROWSY', '困', 1069, 288, 96, 96],
113
+ ['YAWN', '哈欠', 1645, 288, 96, 96],
114
+ ['SICK', '戴口罩', 480, 1440, 96, 96],
115
+ ['PUKE', '吐', 1453, 960, 96, 96],
116
+ ['BETRAYED', '背叛', 877, 384, 96, 96],
117
+ ['HEADSET', '听歌', 431, 480, 106, 96],
118
+ ['EatingFood', '吃饭', 1069, 768, 96, 96],
119
+ ['Typing', '敲键盘', 864, 1536, 96, 96],
120
+ ['Lemon', '柠檬', 1152, 1152, 96, 96],
121
+ ['Get', '了解', 1165, 672, 96, 96],
122
+ ['LGTM', '我看行', 0, 768, 97, 96],
123
+ ['OnIt', '在做了', 337, 384, 110, 96],
124
+ ['OneSecond', '稍等', 216, 480, 108, 96],
125
+ ['Sigh', '叹气', 576, 1440, 96, 96],
126
+ ['MeMeMe', '举手', 108, 480, 108, 96],
127
+ ['VRHeadset', 'VR', 1056, 1536, 96, 96],
128
+ ['GoGoGo', '冲!', 671, 96, 105, 96],
129
+ ['Partying', '派对', 416, 576, 104, 96],
130
+ ['ThanksFace', '感谢', 1549, 1344, 96, 96],
131
+ ['SaluteFace', '致敬', 671, 192, 105, 96],
132
+ ['Shrug', '耸肩摊手', 777, 96, 100, 96],
133
+ ['YouAreTheBest', '送你小红花', 1645, 384, 96, 96],
134
+ ['SALUTE', '抱拳', 1453, 1344, 96, 96],
135
+ ['SHAKE', '握手', 96, 1440, 96, 96],
136
+ ['HIGHFIVE', '击掌', 480, 1152, 96, 96],
137
+ ['WavingHand', '挥手', 1152, 1536, 96, 96],
138
+ ['UPPERLEFT', '点击', 864, 864, 96, 96],
139
+ ['ThumbsDown', '踩', 0, 1536, 96, 96],
140
+ ['ClownFace', '小丑脸', 973, 0, 96, 96],
141
+ ['SLIGHT', '白眼', 1056, 1440, 96, 96],
142
+ ['TONGUE', '吐舌', 288, 1536, 96, 96],
143
+ ['EYESCLOSED', '不看', 96, 1056, 96, 96],
144
+ ['BEAR', '啊?', 117, 384, 110, 96],
145
+ ['BULL', '公牛', 192, 864, 96, 96],
146
+ ['CALF', '小牛', 384, 864, 96, 96],
147
+ ['LIPS', '嘴唇', 1261, 1152, 96, 96],
148
+ ['BEER', '啤酒', 877, 288, 96, 96],
149
+ ['CAKE', '蛋糕', 288, 864, 96, 96],
150
+ ['GIFT', '礼物', 1165, 768, 96, 96],
151
+ ['CUCUMBER', '胡瓜', 973, 576, 96, 96],
152
+ ['Drumstick', '鸡腿', 1069, 384, 96, 96],
153
+ ['Pepper', '辣椒', 1453, 384, 96, 96],
154
+ ['CANDIEDHAWS', '冰糖葫芦', 480, 864, 96, 96],
155
+ ['BubbleTea', '奶茶', 96, 864, 96, 96],
156
+ ['Coffee', '咖啡', 671, 384, 104, 96],
157
+ ['Yes', 'Yes', 624, 576, 104, 96],
158
+ ['No', 'No', 104, 576, 104, 96],
159
+ ['OKR', 'OKR', 312, 576, 104, 96],
160
+ ['CheckMark', '勾号', 576, 864, 96, 96],
161
+ ['CrossMark', '叉号', 973, 384, 96, 96],
162
+ ['MinusOne', '-1', 1152, 1344, 96, 96],
163
+ ['Hundred', '100分', 0, 576, 104, 96],
164
+ ['AWESOMEN', '牛', 771, 768, 96, 96],
165
+ ['Pin', '图钉', 1453, 576, 96, 96],
166
+ ['Alarm', '闹钟', 387, 768, 96, 96],
167
+ ['Loudspeaker', '喇叭', 96, 1248, 96, 96],
168
+ ['Trophy', '奖杯', 480, 1536, 96, 96],
169
+ ['Fire', '火', 480, 1056, 96, 96],
170
+ ['RAINBOWPUKE', '彩虹', 1453, 1056, 96, 96],
171
+ ['Music', '音乐', 1453, 0, 96, 96],
172
+ ['TV', '电视', 576, 1536, 96, 96],
173
+ ['Movie', '电影', 1344, 1344, 96, 96],
174
+ ['Pumpkin', '南瓜灯', 777, 0, 100, 96],
175
+ ['XmasTree', '圣诞树', 1645, 192, 96, 96],
176
+ ['Snowman', '雪人', 1440, 1440, 96, 96],
177
+ ['XmasHat', '圣诞帽', 1645, 96, 96, 96],
178
+ ['FIREWORKS', '烟花', 672, 1056, 96, 96],
179
+ ['2023', '2023', 291, 768, 96, 96],
180
+ ['JubilantRabbit', '欢兔喝彩', 227, 384, 110, 96],
181
+ ['REDPACKET', '红包', 1453, 1152, 96, 96],
182
+ ['FORTUNE', '发', 864, 1056, 96, 96],
183
+ ['LUCK', '福', 288, 1248, 96, 96],
184
+ ['FIRECRACKER', '爆竹', 576, 1056, 96, 96],
185
+ ['StickyRiceBalls', '元宵', 671, 0, 106, 96],
186
+ ['Mooncake', '月饼', 97, 768, 97, 96],
187
+ ['MoonRabbit', '玉兔', 537, 480, 106, 96],
188
+ ['HEARTBROKEN', '心碎', 288, 1152, 96, 96],
189
+ ['BOMB', '炸弹', 0, 864, 96, 96],
190
+ ['POOP', '便便', 1453, 768, 96, 96],
191
+ ['18X', '18禁', 1069, 864, 96, 96],
192
+ ['CLEAVER', '刀', 768, 864, 96, 96],
193
+ ['Basketball', '篮球', 877, 96, 96, 96],
194
+ ['Soccer', '足球', 1549, 96, 96, 96],
195
+ ['GeneralDoNotDisturb', '请勿打扰', 1165, 192, 96, 96],
196
+ ];