im-ui-mobile 0.1.0 → 0.1.2

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 (85) hide show
  1. package/components/im-avatar/im-avatar.vue +7 -7
  2. package/components/im-badge/im-badge.vue +326 -0
  3. package/components/im-button/im-button.vue +71 -34
  4. package/components/im-card/im-card.vue +563 -0
  5. package/components/im-chat-item/im-chat-item.vue +5 -4
  6. package/components/im-col/im-col.vue +191 -0
  7. package/components/im-dialog/im-dialog.vue +543 -0
  8. package/components/im-double-tap-view/im-double-tap-view.vue +93 -0
  9. package/components/im-emoji-picker/im-emoji-picker.vue +1143 -0
  10. package/components/im-friend-item/im-friend-item.vue +1 -1
  11. package/components/im-group-item/im-group-item.vue +1 -1
  12. package/components/im-group-member-selector/im-group-member-selector.vue +5 -5
  13. package/components/im-group-rtc-join/im-group-rtc-join.vue +8 -8
  14. package/components/im-icon/im-icon.vue +593 -0
  15. package/components/im-image-upload/im-image-upload.vue +0 -2
  16. package/components/im-link/im-link.vue +628 -0
  17. package/components/im-loading/im-loading.vue +13 -4
  18. package/components/im-mention-picker/im-mention-picker.vue +8 -7
  19. package/components/im-message-action/im-message-action.vue +678 -0
  20. package/components/im-message-item/im-message-item.vue +28 -26
  21. package/components/im-message-list/im-message-list.vue +1108 -0
  22. package/components/im-modal/im-modal.vue +373 -0
  23. package/components/im-nav-bar/im-nav-bar.vue +689 -75
  24. package/components/im-parse/im-parse.vue +1054 -0
  25. package/components/im-popup/im-popup.vue +467 -0
  26. package/components/im-read-receipt/im-read-receipt.vue +10 -10
  27. package/components/im-row/im-row.vue +189 -0
  28. package/components/im-search/im-search.vue +762 -0
  29. package/components/im-sku/im-sku.vue +720 -0
  30. package/components/im-sku/utils/helper.ts +182 -0
  31. package/components/im-stepper/im-stepper.vue +585 -0
  32. package/components/im-stepper/utils/helper.ts +167 -0
  33. package/components/im-tabs/im-tabs.vue +1022 -0
  34. package/components/im-tabs/tabs-navigation.vue +489 -0
  35. package/components/im-tabs/utils/helper.ts +181 -0
  36. package/components/im-tabs-tab-pane/im-tabs-tab-pane.vue +145 -0
  37. package/components/im-upload/im-upload.vue +1236 -0
  38. package/components/im-voice-input/im-voice-input.vue +1 -1
  39. package/index.js +3 -5
  40. package/index.scss +19 -0
  41. package/libs/emoji-data.ts +229 -0
  42. package/libs/index.ts +16 -16
  43. package/package.json +1 -2
  44. package/styles/button.scss +33 -33
  45. package/theme.scss +2 -2
  46. package/types/components/badge.d.ts +42 -0
  47. package/types/components/button.d.ts +2 -1
  48. package/types/components/card.d.ts +122 -0
  49. package/types/components/col.d.ts +37 -0
  50. package/types/components/dialog.d.ts +125 -0
  51. package/types/components/double-tap-view.d.ts +31 -0
  52. package/types/components/emoji-picker.d.ts +121 -0
  53. package/types/components/group-rtc-join.d.ts +1 -1
  54. package/types/components/icon.d.ts +77 -0
  55. package/types/components/link.d.ts +55 -0
  56. package/types/components/loading.d.ts +1 -0
  57. package/types/components/message-action.d.ts +96 -0
  58. package/types/components/message-item.d.ts +2 -2
  59. package/types/components/message-list.d.ts +136 -0
  60. package/types/components/modal.d.ts +106 -0
  61. package/types/components/nav-bar.d.ts +125 -0
  62. package/types/components/parse.d.ts +90 -0
  63. package/types/components/popup.d.ts +58 -0
  64. package/types/components/row.d.ts +31 -0
  65. package/types/components/search.d.ts +54 -0
  66. package/types/components/sku.d.ts +195 -0
  67. package/types/components/stepper.d.ts +99 -0
  68. package/types/components/tabs-tab-pane.d.ts +27 -0
  69. package/types/components/tabs.d.ts +117 -0
  70. package/types/components/upload.d.ts +137 -0
  71. package/types/components.d.ts +19 -1
  72. package/types/index.d.ts +38 -1
  73. package/types/libs/index.d.ts +10 -10
  74. package/types/utils/base64.d.ts +5 -0
  75. package/types/utils/dom.d.ts +3 -0
  76. package/types/utils/enums.d.ts +4 -5
  77. package/types/utils/validator.d.ts +74 -0
  78. package/utils/base64.js +18 -0
  79. package/utils/dom.js +353 -1
  80. package/utils/enums.js +4 -5
  81. package/utils/validator.js +230 -0
  82. package/components/im-file-upload/im-file-upload.vue +0 -309
  83. package/plugins/uview-plus.js +0 -29
  84. package/types/components/arrow-bar.d.ts +0 -14
  85. package/types/components/file-upload.d.ts +0 -58
@@ -0,0 +1,1143 @@
1
+ <template>
2
+ <view class="im-emoji-picker" :class="[
3
+ `im-emoji-picker--${theme}`,
4
+ `im-emoji-picker--${size}`,
5
+ {
6
+ 'im-emoji-picker--visible': visible,
7
+ 'im-emoji-picker--recent': showRecent,
8
+ 'im-emoji-picker--search': enableSearch
9
+ }
10
+ ]" :style="pickerStyle">
11
+ <!-- 搜索框 -->
12
+ <view v-if="enableSearch" class="im-emoji-picker__search">
13
+ <view class="im-emoji-picker__search-box">
14
+ <im-icon name="search" :size="searchIconSize" :color="searchIconColor" />
15
+ <input class="im-emoji-picker__search-input" v-model="searchKeyword" :placeholder="searchPlaceholder"
16
+ :placeholder-style="placeholderStyle" :focus="autoFocus" @input="handleSearch"
17
+ @confirm="handleSearchConfirm" />
18
+ <view v-if="searchKeyword" class="im-emoji-picker__search-clear" @click="clearSearch">
19
+ <im-icon name="close" :size="16" :color="searchIconColor" />
20
+ </view>
21
+ </view>
22
+ </view>
23
+
24
+ <!-- 分类标签 -->
25
+ <view v-if="showTabs && !isSearching" class="im-emoji-picker__tabs">
26
+ <scroll-view class="im-emoji-picker__tabs-scroll" scroll-x :show-scrollbar="false">
27
+ <view class="im-emoji-picker__tabs-list">
28
+ <!-- 最近使用 -->
29
+ <view v-if="showRecent" class="im-emoji-picker__tab"
30
+ :class="{ 'im-emoji-picker__tab--active': activeTab === 'recent' }"
31
+ @click="switchTab('recent')">
32
+ <im-icon name="time" :size="tabIconSize" />
33
+ <text v-if="showTabText" class="im-emoji-picker__tab-text">最近</text>
34
+ </view>
35
+
36
+ <!-- 表情分类 -->
37
+ <view v-for="category in filteredCategories" :key="category.id" class="im-emoji-picker__tab"
38
+ :class="{ 'im-emoji-picker__tab--active': activeTab === category.id }"
39
+ @click="switchTab(category.id)">
40
+ <text class="im-emoji-picker__tab-emoji">{{ category.icon }}</text>
41
+ <text v-if="showTabText" class="im-emoji-picker__tab-text">{{ category.name }}</text>
42
+ </view>
43
+ </view>
44
+ </scroll-view>
45
+ </view>
46
+
47
+ <!-- 表情列表 -->
48
+ <scroll-view class="im-emoji-picker__content" scroll-y :scroll-into-view="scrollToId"
49
+ :scroll-with-animation="true" :show-scrollbar="showScrollbar" @scroll="handleScroll">
50
+ <!-- 搜索结果显示 -->
51
+ <template v-if="isSearching">
52
+ <view v-if="searchResults.length > 0" class="im-emoji-picker__search-results">
53
+ <view class="im-emoji-picker__category">
54
+ <text class="im-emoji-picker__category-title">搜索结果 ({{ searchResults.length }})</text>
55
+ </view>
56
+ <view class="im-emoji-picker__emoji-grid">
57
+ <view v-for="emoji in searchResults" :key="emoji.key" class="im-emoji-picker__emoji-item"
58
+ :class="{ 'im-emoji-picker__emoji-item--selected': isSelected(emoji) }"
59
+ @click="handleEmojiClick(emoji)" @longpress="handleEmojiLongPress(emoji)">
60
+ <image v-if="emoji.type === 'image'" class="im-emoji-picker__emoji-image" :src="emoji.url"
61
+ mode="aspectFit" lazy-load />
62
+ <text v-else class="im-emoji-picker__emoji-char">{{ emoji.char }}</text>
63
+
64
+ <!-- 表情名称提示 -->
65
+ <view v-if="showEmojiName" class="im-emoji-picker__emoji-name">
66
+ <text>{{ emoji.name || emoji.key }}</text>
67
+ </view>
68
+ </view>
69
+ </view>
70
+ </view>
71
+
72
+ <!-- 空状态 -->
73
+ <view v-else class="im-emoji-picker__empty">
74
+ <im-icon name="emoji" :size="emptyIconSize" :color="emptyIconColor" />
75
+ <text class="im-emoji-picker__empty-text">未找到相关表情</text>
76
+ </view>
77
+ </template>
78
+
79
+ <!-- 正常分类显示 -->
80
+ <template v-else>
81
+ <!-- 最近使用 -->
82
+ <view v-if="showRecent && recentEmojis.length > 0 && activeTab === 'recent'"
83
+ class="im-emoji-picker__category">
84
+ <view class="im-emoji-picker__category-header">
85
+ <text class="im-emoji-picker__category-title">最近使用</text>
86
+ <view v-if="showRecentClear" class="im-emoji-picker__category-action"
87
+ @click="clearRecentEmojis">
88
+ <text class="im-emoji-picker__category-action-text">清空</text>
89
+ </view>
90
+ </view>
91
+ <view class="im-emoji-picker__emoji-grid">
92
+ <view v-for="emoji in recentEmojis" :key="emoji.key" class="im-emoji-picker__emoji-item"
93
+ :class="{ 'im-emoji-picker__emoji-item--selected': isSelected(emoji) }"
94
+ @click="handleEmojiClick(emoji)" @longpress="handleEmojiLongPress(emoji)">
95
+ <image v-if="emoji.type === 'image'" class="im-emoji-picker__emoji-image" :src="emoji.url"
96
+ mode="aspectFit" lazy-load />
97
+ <text v-else class="im-emoji-picker__emoji-char">{{ emoji.char }}</text>
98
+ </view>
99
+ </view>
100
+ </view>
101
+
102
+ <!-- 分类表情 -->
103
+ <view v-for="category in filteredCategories" :key="category.id" class="im-emoji-picker__category"
104
+ :id="`category-${category.id}`" v-show="activeTab === 'all' || activeTab === category.id">
105
+ <view class="im-emoji-picker__category-header">
106
+ <text class="im-emoji-picker__category-title">{{ category.name }}</text>
107
+ <text v-if="showCategoryCount" class="im-emoji-picker__category-count">
108
+ {{ getCategoryEmojis(category.id).length }}
109
+ </text>
110
+ </view>
111
+ <view class="im-emoji-picker__emoji-grid">
112
+ <view v-for="emoji in getCategoryEmojis(category.id)" :key="emoji.key"
113
+ class="im-emoji-picker__emoji-item"
114
+ :class="{ 'im-emoji-picker__emoji-item--selected': isSelected(emoji) }"
115
+ @click="handleEmojiClick(emoji)" @longpress="handleEmojiLongPress(emoji)">
116
+ <image v-if="emoji.type === 'image'" class="im-emoji-picker__emoji-image" :src="emoji.url"
117
+ mode="aspectFit" lazy-load />
118
+ <text v-else class="im-emoji-picker__emoji-char">{{ emoji.char }}</text>
119
+
120
+ <!-- 表情名称提示 -->
121
+ <view v-if="showEmojiName" class="im-emoji-picker__emoji-name">
122
+ <text>{{ emoji.name || emoji.key }}</text>
123
+ </view>
124
+ </view>
125
+ </view>
126
+ </view>
127
+ </template>
128
+
129
+ <!-- 底部填充 -->
130
+ <view class="im-emoji-picker__footer-spacer"></view>
131
+ </scroll-view>
132
+
133
+ <!-- 底部操作栏 -->
134
+ <view v-if="showFooter" class="im-emoji-picker__footer">
135
+ <!-- 自定义插槽 -->
136
+ <slot name="footer">
137
+ <view class="im-emoji-picker__footer-default">
138
+ <view class="im-emoji-picker__preview" v-if="selectedEmoji && showPreview">
139
+ <view class="im-emoji-picker__preview-emoji">
140
+ <text v-if="selectedEmoji.type === 'unicode'">{{ selectedEmoji.char }}</text>
141
+ <image v-else :src="selectedEmoji.url" mode="aspectFit" />
142
+ </view>
143
+ <text class="im-emoji-picker__preview-name">{{ selectedEmoji.name || selectedEmoji.key }}</text>
144
+ </view>
145
+
146
+ <view class="im-emoji-picker__actions">
147
+ <im-button v-if="showDeleteButton" size="small" type="default" :disabled="!selectedEmoji"
148
+ @click="handleDelete">
149
+ <im-icon name="delete" size="16" />
150
+ </im-button>
151
+
152
+ <im-button v-if="showSendButton" size="small" type="primary" :disabled="!selectedEmoji"
153
+ @click="handleSend">
154
+ 发送
155
+ </im-button>
156
+ </view>
157
+ </view>
158
+ </slot>
159
+ </view>
160
+
161
+ <!-- 大表情预览 -->
162
+ <view v-if="showPreviewPopup && previewEmoji" class="im-emoji-picker__preview-popup" @click="closePreview">
163
+ <view class="im-emoji-picker__preview-content" @click.stop>
164
+ <view class="im-emoji-picker__preview-large">
165
+ <text v-if="previewEmoji.type === 'unicode'">{{ previewEmoji.char }}</text>
166
+ <image v-else :src="previewEmoji.url" mode="aspectFit" />
167
+ </view>
168
+ <text class="im-emoji-picker__preview-desc">{{ previewEmoji.name || previewEmoji.key }}</text>
169
+ <text class="im-emoji-picker__preview-code">{{ previewEmoji.code || previewEmoji.key }}</text>
170
+
171
+ <view class="im-emoji-picker__preview-actions">
172
+ <im-button size="mini" @click="handlePreviewSelect(previewEmoji)">选择</im-button>
173
+ <im-button size="mini" type="default" @click="closePreview">关闭</im-button>
174
+ </view>
175
+ </view>
176
+ </view>
177
+ </view>
178
+ </template>
179
+
180
+ <script setup lang="ts">
181
+ import { ref, computed, watch, onMounted } from 'vue'
182
+
183
+ // 类型定义
184
+ interface Emoji {
185
+ key: string
186
+ char: string
187
+ name: string
188
+ category: string
189
+ url?: string
190
+ type: 'unicode' | 'image'
191
+ code?: string
192
+ keywords?: string[]
193
+ }
194
+
195
+ interface EmojiCategory {
196
+ id: string
197
+ name: string
198
+ icon: string
199
+ order: number
200
+ }
201
+
202
+ interface Props {
203
+ // 显示控制
204
+ visible?: boolean
205
+ theme?: 'light' | 'dark' | 'card'
206
+ size?: 'small' | 'medium' | 'large'
207
+
208
+ // 数据源
209
+ emojis?: Emoji[]
210
+ categories?: EmojiCategory[]
211
+ recentEmojis?: Emoji[]
212
+
213
+ // 配置选项
214
+ showRecent?: boolean
215
+ showTabs?: boolean
216
+ showTabText?: boolean
217
+ showFooter?: boolean
218
+ showPreview?: boolean
219
+ showEmojiName?: boolean
220
+ showCategoryCount?: boolean
221
+ showRecentClear?: boolean
222
+ showDeleteButton?: boolean
223
+ showSendButton?: boolean
224
+ showScrollbar?: boolean
225
+
226
+ // 搜索功能
227
+ enableSearch?: boolean
228
+ searchPlaceholder?: string
229
+ searchIconSize?: number
230
+ searchIconColor?: string
231
+ autoFocus?: boolean
232
+
233
+ // 布局配置
234
+ columns?: number
235
+ emojiSize?: string | number
236
+ spacing?: string | number
237
+ borderRadius?: string | number
238
+
239
+ // 样式控制
240
+ height?: string | number
241
+ backgroundColor?: string
242
+ categoryColor?: string
243
+
244
+ // 交互配置
245
+ maxRecentCount?: number
246
+ allowMultiple?: boolean
247
+ selectedEmojis?: Emoji[]
248
+
249
+ // 空状态
250
+ emptyIconSize?: number
251
+ emptyIconColor?: string
252
+ emptyText?: string
253
+ }
254
+
255
+ const props = withDefaults(defineProps<Props>(), {
256
+ visible: false,
257
+ theme: 'light',
258
+ size: 'medium',
259
+
260
+ // emojis: () => getDefaultEmojis(),
261
+ emojis: (): Emoji[] => {
262
+ return [
263
+ // 笑脸和情感
264
+ { key: 'grinning', char: '😀', name: '大笑', category: 'smileys', type: 'unicode' },
265
+ { key: 'smiley', char: '😃', name: '微笑', category: 'smileys', type: 'unicode' },
266
+ { key: 'smile', char: '😄', name: '开心', category: 'smileys', type: 'unicode' },
267
+ { key: 'grin', char: '😁', name: '咧嘴笑', category: 'smileys', type: 'unicode' },
268
+ { key: 'laughing', char: '😆', name: '大笑', category: 'smileys', type: 'unicode' },
269
+
270
+ // 手势
271
+ { key: 'thumbsup', char: '👍', name: '点赞', category: 'gestures', type: 'unicode' },
272
+ { key: 'thumbsdown', char: '👎', name: '点踩', category: 'gestures', type: 'unicode' },
273
+ { key: 'clap', char: '👏', name: '鼓掌', category: 'gestures', type: 'unicode' },
274
+ { key: 'pray', char: '🙏', name: '祈祷', category: 'gestures', type: 'unicode' },
275
+
276
+ // 物品
277
+ { key: 'heart', char: '❤️', name: '红心', category: 'objects', type: 'unicode' },
278
+ { key: 'star', char: '⭐', name: '星星', category: 'objects', type: 'unicode' },
279
+ { key: 'fire', char: '🔥', name: '火焰', category: 'objects', type: 'unicode' },
280
+ { key: 'rocket', char: '🚀', name: '火箭', category: 'objects', type: 'unicode' },
281
+
282
+ // 自然
283
+ { key: 'sun', char: '☀️', name: '太阳', category: 'nature', type: 'unicode' },
284
+ { key: 'moon', char: '🌙', name: '月亮', category: 'nature', type: 'unicode' },
285
+ { key: 'star2', char: '🌟', name: '闪星', category: 'nature', type: 'unicode' },
286
+ { key: 'cloud', char: '☁️', name: '云', category: 'nature', type: 'unicode' },
287
+
288
+ // 食物
289
+ { key: 'pizza', char: '🍕', name: '披萨', category: 'food', type: 'unicode' },
290
+ { key: 'hamburger', char: '🍔', name: '汉堡', category: 'food', type: 'unicode' },
291
+ { key: 'coffee', char: '☕', name: '咖啡', category: 'food', type: 'unicode' },
292
+ { key: 'beer', char: '🍺', name: '啤酒', category: 'food', type: 'unicode' },
293
+
294
+ // 活动
295
+ { key: 'soccer', char: '⚽', name: '足球', category: 'activities', type: 'unicode' },
296
+ { key: 'basketball', char: '🏀', name: '篮球', category: 'activities', type: 'unicode' },
297
+ { key: 'tennis', char: '🎾', name: '网球', category: 'activities', type: 'unicode' },
298
+ { key: 'music', char: '🎵', name: '音乐', category: 'activities', type: 'unicode' },
299
+ ]
300
+ },
301
+ // categories: () => getDefaultCategories(),
302
+ categories: (): EmojiCategory[] => {
303
+ return [
304
+ { id: 'recent', name: '最近', icon: '🕒', order: 0 },
305
+ { id: 'smileys', name: '表情', icon: '😀', order: 1 },
306
+ { id: 'gestures', name: '手势', icon: '👋', order: 2 },
307
+ { id: 'objects', name: '物品', icon: '💡', order: 3 },
308
+ { id: 'nature', name: '自然', icon: '🌳', order: 4 },
309
+ { id: 'food', name: '食物', icon: '🍎', order: 5 },
310
+ { id: 'activities', name: '活动', icon: '⚽', order: 6 },
311
+ { id: 'flags', name: '旗帜', icon: '🇨🇳', order: 7 },
312
+ ]
313
+ },
314
+ recentEmojis: () => [],
315
+
316
+ showRecent: true,
317
+ showTabs: true,
318
+ showTabText: true,
319
+ showFooter: true,
320
+ showPreview: true,
321
+ showEmojiName: false,
322
+ showCategoryCount: false,
323
+ showRecentClear: true,
324
+ showDeleteButton: false,
325
+ showSendButton: false,
326
+ showScrollbar: false,
327
+
328
+ enableSearch: true,
329
+ searchPlaceholder: '搜索表情',
330
+ searchIconSize: 20,
331
+ searchIconColor: '#909399',
332
+ autoFocus: false,
333
+
334
+ columns: 8,
335
+ emojiSize: '48rpx',
336
+ spacing: '8rpx',
337
+ borderRadius: '8rpx',
338
+
339
+ height: '600rpx',
340
+ backgroundColor: '',
341
+ categoryColor: '',
342
+
343
+ maxRecentCount: 36,
344
+ allowMultiple: false,
345
+ selectedEmojis: () => [],
346
+
347
+ emptyIconSize: 48,
348
+ emptyIconColor: '#C0C4CC',
349
+ emptyText: '未找到相关表情'
350
+ })
351
+
352
+ // Emits
353
+ const emit = defineEmits<{
354
+ // 基础事件
355
+ 'update:visible': [visible: boolean]
356
+ 'open': []
357
+ 'close': []
358
+
359
+ // 表情事件
360
+ 'select': [emoji: Emoji]
361
+ 'delete': [emoji: Emoji]
362
+ 'send': [emoji: Emoji]
363
+ 'longpress': [emoji: Emoji]
364
+
365
+ // 搜索事件
366
+ 'search': [keyword: string]
367
+ 'search-clear': []
368
+
369
+ // 最近使用
370
+ 'recent-clear': []
371
+
372
+ // 分类切换
373
+ 'tab-change': [tabId: string]
374
+ }>()
375
+
376
+ // 响应式数据
377
+ const activeTab = ref('recent')
378
+ const searchKeyword = ref('')
379
+ const scrollToId = ref('')
380
+ const selectedEmoji = ref<Emoji | null>(null)
381
+ const previewEmoji = ref<Emoji | null>(null)
382
+ const showPreviewPopup = ref(false)
383
+ const recentEmojisLocal = ref<Emoji[]>([])
384
+ const currentScrollTop = ref(0)
385
+
386
+ // 计算属性
387
+ const isSearching = computed(() => searchKeyword.value.trim().length > 0)
388
+
389
+ const filteredCategories = computed(() => {
390
+ return props.categories.filter(category => {
391
+ const emojis = getCategoryEmojis(category.id)
392
+ return emojis.length > 0
393
+ }).sort((a, b) => a.order - b.order)
394
+ })
395
+
396
+ const searchResults = computed(() => {
397
+ if (!searchKeyword.value.trim()) return []
398
+
399
+ const keyword = searchKeyword.value.toLowerCase().trim()
400
+ return props.emojis.filter(emoji => {
401
+ // 搜索名称
402
+ if (emoji.name?.toLowerCase().includes(keyword)) return true
403
+
404
+ // 搜索关键词
405
+ if (emoji.keywords?.some(kw => kw.toLowerCase().includes(keyword))) return true
406
+
407
+ // 搜索 key
408
+ if (emoji.key?.toLowerCase().includes(keyword)) return true
409
+
410
+ // 搜索 code
411
+ if (emoji.code?.toLowerCase().includes(keyword)) return true
412
+
413
+ return false
414
+ })
415
+ })
416
+
417
+ const pickerStyle = computed(() => {
418
+ const style: Record<string, string> = {
419
+ '--emoji-size': typeof props.emojiSize === 'number' ? `${props.emojiSize}px` : props.emojiSize,
420
+ '--spacing': typeof props.spacing === 'number' ? `${props.spacing}px` : props.spacing,
421
+ '--border-radius': typeof props.borderRadius === 'number' ? `${props.borderRadius}px` : props.borderRadius,
422
+ '--columns': props.columns.toString()
423
+ }
424
+
425
+ if (props.height) {
426
+ style.height = typeof props.height === 'number' ? `${props.height}px` : props.height
427
+ }
428
+
429
+ if (props.backgroundColor) {
430
+ style.backgroundColor = props.backgroundColor
431
+ }
432
+
433
+ return style
434
+ })
435
+
436
+ const placeholderStyle = computed(() => {
437
+ return `color: ${props.theme === 'dark' ? '#888' : '#999'}; font-size: 28rpx;`
438
+ })
439
+
440
+ const tabIconSize = computed(() => {
441
+ switch (props.size) {
442
+ case 'small': return 16
443
+ case 'large': return 24
444
+ default: return 20
445
+ }
446
+ })
447
+
448
+ // 方法
449
+ const getDefaultEmojis = (): Emoji[] => {
450
+ // 这里可以返回默认的表情数据
451
+ // 实际项目中可以从外部导入
452
+ return [
453
+ // 笑脸和情感
454
+ { key: 'grinning', char: '😀', name: '大笑', category: 'smileys', type: 'unicode' },
455
+ { key: 'smiley', char: '😃', name: '微笑', category: 'smileys', type: 'unicode' },
456
+ { key: 'smile', char: '😄', name: '开心', category: 'smileys', type: 'unicode' },
457
+ { key: 'grin', char: '😁', name: '咧嘴笑', category: 'smileys', type: 'unicode' },
458
+ { key: 'laughing', char: '😆', name: '大笑', category: 'smileys', type: 'unicode' },
459
+
460
+ // 手势
461
+ { key: 'thumbsup', char: '👍', name: '点赞', category: 'gestures', type: 'unicode' },
462
+ { key: 'thumbsdown', char: '👎', name: '点踩', category: 'gestures', type: 'unicode' },
463
+ { key: 'clap', char: '👏', name: '鼓掌', category: 'gestures', type: 'unicode' },
464
+ { key: 'pray', char: '🙏', name: '祈祷', category: 'gestures', type: 'unicode' },
465
+
466
+ // 物品
467
+ { key: 'heart', char: '❤️', name: '红心', category: 'objects', type: 'unicode' },
468
+ { key: 'star', char: '⭐', name: '星星', category: 'objects', type: 'unicode' },
469
+ { key: 'fire', char: '🔥', name: '火焰', category: 'objects', type: 'unicode' },
470
+ { key: 'rocket', char: '🚀', name: '火箭', category: 'objects', type: 'unicode' },
471
+
472
+ // 自然
473
+ { key: 'sun', char: '☀️', name: '太阳', category: 'nature', type: 'unicode' },
474
+ { key: 'moon', char: '🌙', name: '月亮', category: 'nature', type: 'unicode' },
475
+ { key: 'star2', char: '🌟', name: '闪星', category: 'nature', type: 'unicode' },
476
+ { key: 'cloud', char: '☁️', name: '云', category: 'nature', type: 'unicode' },
477
+
478
+ // 食物
479
+ { key: 'pizza', char: '🍕', name: '披萨', category: 'food', type: 'unicode' },
480
+ { key: 'hamburger', char: '🍔', name: '汉堡', category: 'food', type: 'unicode' },
481
+ { key: 'coffee', char: '☕', name: '咖啡', category: 'food', type: 'unicode' },
482
+ { key: 'beer', char: '🍺', name: '啤酒', category: 'food', type: 'unicode' },
483
+
484
+ // 活动
485
+ { key: 'soccer', char: '⚽', name: '足球', category: 'activities', type: 'unicode' },
486
+ { key: 'basketball', char: '🏀', name: '篮球', category: 'activities', type: 'unicode' },
487
+ { key: 'tennis', char: '🎾', name: '网球', category: 'activities', type: 'unicode' },
488
+ { key: 'music', char: '🎵', name: '音乐', category: 'activities', type: 'unicode' },
489
+ ]
490
+ }
491
+
492
+ const getDefaultCategories = (): EmojiCategory[] => {
493
+ return [
494
+ { id: 'recent', name: '最近', icon: '🕒', order: 0 },
495
+ { id: 'smileys', name: '表情', icon: '😀', order: 1 },
496
+ { id: 'gestures', name: '手势', icon: '👋', order: 2 },
497
+ { id: 'objects', name: '物品', icon: '💡', order: 3 },
498
+ { id: 'nature', name: '自然', icon: '🌳', order: 4 },
499
+ { id: 'food', name: '食物', icon: '🍎', order: 5 },
500
+ { id: 'activities', name: '活动', icon: '⚽', order: 6 },
501
+ { id: 'flags', name: '旗帜', icon: '🇨🇳', order: 7 },
502
+ ]
503
+ }
504
+
505
+ const getCategoryEmojis = (categoryId: string): Emoji[] => {
506
+ return props.emojis.filter(emoji => emoji.category === categoryId)
507
+ }
508
+
509
+ const switchTab = (tabId: string) => {
510
+ activeTab.value = tabId
511
+
512
+ if (tabId !== 'recent') {
513
+ scrollToId.value = `category-${tabId}`
514
+ }
515
+
516
+ emit('tab-change', tabId)
517
+ }
518
+
519
+ const handleEmojiClick = (emoji: Emoji) => {
520
+ selectedEmoji.value = emoji
521
+
522
+ // 添加到最近使用
523
+ addToRecent(emoji)
524
+
525
+ // 触发选择事件
526
+ emit('select', emoji)
527
+ }
528
+
529
+ const handleEmojiLongPress = (emoji: Emoji) => {
530
+ previewEmoji.value = emoji
531
+ showPreviewPopup.value = true
532
+ emit('longpress', emoji)
533
+ }
534
+
535
+ const handlePreviewSelect = (emoji: Emoji) => {
536
+ handleEmojiClick(emoji)
537
+ closePreview()
538
+ }
539
+
540
+ const closePreview = () => {
541
+ showPreviewPopup.value = false
542
+ previewEmoji.value = null
543
+ }
544
+
545
+ const handleSearch = () => {
546
+ emit('search', searchKeyword.value)
547
+ }
548
+
549
+ const handleSearchConfirm = () => {
550
+ if (searchResults.value.length > 0) {
551
+ handleEmojiClick(searchResults.value[0])
552
+ }
553
+ }
554
+
555
+ const clearSearch = () => {
556
+ searchKeyword.value = ''
557
+ emit('search-clear')
558
+ }
559
+
560
+ const handleDelete = () => {
561
+ if (selectedEmoji.value) {
562
+ emit('delete', selectedEmoji.value)
563
+ selectedEmoji.value = null
564
+ }
565
+ }
566
+
567
+ const handleSend = () => {
568
+ if (selectedEmoji.value) {
569
+ emit('send', selectedEmoji.value)
570
+ }
571
+ }
572
+
573
+ const addToRecent = (emoji: Emoji) => {
574
+ // 移除重复项
575
+ const index = recentEmojisLocal.value.findIndex(item => item.key === emoji.key)
576
+ if (index > -1) {
577
+ recentEmojisLocal.value.splice(index, 1)
578
+ }
579
+
580
+ // 添加到开头
581
+ recentEmojisLocal.value.unshift({ ...emoji })
582
+
583
+ // 限制数量
584
+ if (recentEmojisLocal.value.length > props.maxRecentCount) {
585
+ recentEmojisLocal.value = recentEmojisLocal.value.slice(0, props.maxRecentCount)
586
+ }
587
+
588
+ // 保存到本地存储(如果需要)
589
+ saveRecentEmojis()
590
+ }
591
+
592
+ const clearRecentEmojis = () => {
593
+ recentEmojisLocal.value = []
594
+ emit('recent-clear')
595
+ // 清除本地存储
596
+ uni.removeStorageSync('im_emoji_recent')
597
+ }
598
+
599
+ const saveRecentEmojis = () => {
600
+ try {
601
+ const data = JSON.stringify(recentEmojisLocal.value.slice(0, props.maxRecentCount))
602
+ uni.setStorageSync('im_emoji_recent', data)
603
+ } catch (error) {
604
+ console.error('保存最近表情失败:', error)
605
+ }
606
+ }
607
+
608
+ const loadRecentEmojis = () => {
609
+ try {
610
+ const data = uni.getStorageSync('im_emoji_recent')
611
+ if (data) {
612
+ recentEmojisLocal.value = JSON.parse(data)
613
+ }
614
+ } catch (error) {
615
+ console.error('加载最近表情失败:', error)
616
+ recentEmojisLocal.value = []
617
+ }
618
+ }
619
+
620
+ const isSelected = (emoji: Emoji): boolean => {
621
+ if (props.allowMultiple) {
622
+ return props.selectedEmojis.some(item => item.key === emoji.key)
623
+ }
624
+ return selectedEmoji.value?.key === emoji.key
625
+ }
626
+
627
+ const handleScroll = (event: any) => {
628
+ currentScrollTop.value = event.detail.scrollTop
629
+
630
+ // 自动切换分类标签(如果需要)
631
+ if (!isSearching.value && activeTab.value !== 'all') {
632
+ // 可以在这里实现滚动时自动切换分类
633
+ }
634
+ }
635
+
636
+ const open = () => {
637
+ if (!props.visible) {
638
+ emit('update:visible', true)
639
+ emit('open')
640
+ }
641
+ }
642
+
643
+ const close = () => {
644
+ if (props.visible) {
645
+ emit('update:visible', false)
646
+ emit('close')
647
+ }
648
+ }
649
+
650
+ const insertEmoji = (emoji: Emoji) => {
651
+ handleEmojiClick(emoji)
652
+ }
653
+
654
+ // 初始化
655
+ onMounted(() => {
656
+ loadRecentEmojis()
657
+ })
658
+
659
+ // 监听可见性变化
660
+ watch(() => props.visible, (newVal) => {
661
+ if (newVal) {
662
+ // 打开时重置搜索
663
+ searchKeyword.value = ''
664
+ }
665
+ })
666
+
667
+ // 监听最近表情变化
668
+ watch(() => props.recentEmojis, (newVal) => {
669
+ if (newVal && newVal.length > 0) {
670
+ recentEmojisLocal.value = [...newVal]
671
+ }
672
+ }, { deep: true })
673
+
674
+ // 暴露给父组件的方法
675
+ defineExpose({
676
+ open,
677
+ close,
678
+ insertEmoji,
679
+ clearRecentEmojis,
680
+ getRecentEmojis: () => recentEmojisLocal.value,
681
+ searchEmoji: (keyword: string) => {
682
+ searchKeyword.value = keyword
683
+ return searchResults.value
684
+ }
685
+ })
686
+ </script>
687
+
688
+ <style scoped lang="scss">
689
+ .im-emoji-picker {
690
+ position: relative;
691
+ width: 100%;
692
+ background-color: #ffffff;
693
+ border-radius: 16rpx;
694
+ overflow: hidden;
695
+ box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.1);
696
+ transition: transform 0.3s ease, opacity 0.3s ease;
697
+ transform: translateY(100%);
698
+ opacity: 0;
699
+
700
+ // 主题变体
701
+ &--light {
702
+ background-color: #ffffff;
703
+ color: #333333;
704
+ }
705
+
706
+ &--dark {
707
+ background-color: #1a1a1a;
708
+ color: #ffffff;
709
+ }
710
+
711
+ &--card {
712
+ background-color: #ffffff;
713
+ border: 1rpx solid #e4e7ed;
714
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
715
+ }
716
+
717
+ // 尺寸变体
718
+ &--small {
719
+ .im-emoji-picker__emoji-item {
720
+ width: calc((100% - var(--spacing) * (var(--columns) + 1)) / var(--columns));
721
+ height: calc((100% - var(--spacing) * (var(--columns) + 1)) / var(--columns));
722
+ }
723
+ }
724
+
725
+ &--medium {
726
+ .im-emoji-picker__emoji-item {
727
+ width: calc((100% - var(--spacing) * (var(--columns) + 1)) / var(--columns));
728
+ height: calc((100% - var(--spacing) * (var(--columns) + 1)) / var(--columns));
729
+ }
730
+ }
731
+
732
+ &--large {
733
+ .im-emoji-picker__emoji-item {
734
+ width: calc((100% - var(--spacing) * (var(--columns) + 1)) / var(--columns));
735
+ height: calc((100% - var(--spacing) * (var(--columns) + 1)) / var(--columns));
736
+ }
737
+ }
738
+
739
+ // 可见状态
740
+ &--visible {
741
+ transform: translateY(0);
742
+ opacity: 1;
743
+ }
744
+ }
745
+
746
+ // 搜索框
747
+ .im-emoji-picker__search {
748
+ padding: 20rpx 24rpx;
749
+ border-bottom: 1rpx solid #f0f0f0;
750
+
751
+ .im-emoji-picker--dark & {
752
+ border-bottom-color: rgba(255, 255, 255, 0.1);
753
+ }
754
+ }
755
+
756
+ .im-emoji-picker__search-box {
757
+ display: flex;
758
+ align-items: center;
759
+ background-color: #f5f7fa;
760
+ border-radius: 20rpx;
761
+ padding: 0 24rpx;
762
+ height: 64rpx;
763
+
764
+ .im-emoji-picker--dark & {
765
+ background-color: rgba(255, 255, 255, 0.1);
766
+ }
767
+ }
768
+
769
+ .im-emoji-picker__search-input {
770
+ flex: 1;
771
+ height: 100%;
772
+ padding: 0 16rpx;
773
+ font-size: 28rpx;
774
+ color: inherit;
775
+ background-color: transparent;
776
+
777
+ &::placeholder {
778
+ color: #999999;
779
+
780
+ .im-emoji-picker--dark & {
781
+ color: #888888;
782
+ }
783
+ }
784
+ }
785
+
786
+ .im-emoji-picker__search-clear {
787
+ padding: 4rpx;
788
+ border-radius: 50%;
789
+ background-color: rgba(0, 0, 0, 0.1);
790
+ display: flex;
791
+ align-items: center;
792
+ justify-content: center;
793
+ cursor: pointer;
794
+
795
+ .im-emoji-picker--dark & {
796
+ background-color: rgba(255, 255, 255, 0.2);
797
+ }
798
+ }
799
+
800
+ // 分类标签
801
+ .im-emoji-picker__tabs {
802
+ border-bottom: 1rpx solid #f0f0f0;
803
+
804
+ .im-emoji-picker--dark & {
805
+ border-bottom-color: rgba(255, 255, 255, 0.1);
806
+ }
807
+ }
808
+
809
+ .im-emoji-picker__tabs-scroll {
810
+ white-space: nowrap;
811
+ }
812
+
813
+ .im-emoji-picker__tabs-list {
814
+ display: inline-flex;
815
+ padding: 0 24rpx;
816
+ }
817
+
818
+ .im-emoji-picker__tab {
819
+ display: flex;
820
+ flex-direction: column;
821
+ align-items: center;
822
+ padding: 16rpx 12rpx;
823
+ min-width: 80rpx;
824
+ cursor: pointer;
825
+ transition: all 0.2s ease;
826
+ position: relative;
827
+
828
+ &:active {
829
+ opacity: 0.7;
830
+ }
831
+
832
+ &--active {
833
+ color: #409eff;
834
+
835
+ &::after {
836
+ content: '';
837
+ position: absolute;
838
+ bottom: 0;
839
+ left: 50%;
840
+ transform: translateX(-50%);
841
+ width: 40rpx;
842
+ height: 4rpx;
843
+ background-color: #409eff;
844
+ border-radius: 2rpx;
845
+ }
846
+ }
847
+ }
848
+
849
+ .im-emoji-picker__tab-emoji {
850
+ font-size: 32rpx;
851
+ line-height: 1;
852
+ margin-bottom: 4rpx;
853
+ }
854
+
855
+ .im-emoji-picker__tab-text {
856
+ font-size: 22rpx;
857
+ white-space: nowrap;
858
+ }
859
+
860
+ // 内容区域
861
+ .im-emoji-picker__content {
862
+ height: calc(100% - 120rpx);
863
+ box-sizing: border-box;
864
+ }
865
+
866
+ // 分类
867
+ .im-emoji-picker__category {
868
+ padding: 24rpx 24rpx 0;
869
+ }
870
+
871
+ .im-emoji-picker__category-header {
872
+ display: flex;
873
+ align-items: center;
874
+ justify-content: space-between;
875
+ margin-bottom: 20rpx;
876
+ }
877
+
878
+ .im-emoji-picker__category-title {
879
+ font-size: 28rpx;
880
+ font-weight: 600;
881
+ color: inherit;
882
+ }
883
+
884
+ .im-emoji-picker__category-count {
885
+ font-size: 24rpx;
886
+ color: #909399;
887
+
888
+ .im-emoji-picker--dark & {
889
+ color: #888888;
890
+ }
891
+ }
892
+
893
+ .im-emoji-picker__category-action {
894
+ cursor: pointer;
895
+
896
+ &:active {
897
+ opacity: 0.7;
898
+ }
899
+ }
900
+
901
+ .im-emoji-picker__category-action-text {
902
+ font-size: 24rpx;
903
+ color: #909399;
904
+
905
+ .im-emoji-picker--dark & {
906
+ color: #888888;
907
+ }
908
+ }
909
+
910
+ // 表情网格
911
+ .im-emoji-picker__emoji-grid {
912
+ display: grid;
913
+ grid-template-columns: repeat(var(--columns), 1fr);
914
+ gap: var(--spacing);
915
+ justify-items: center;
916
+ }
917
+
918
+ .im-emoji-picker__emoji-item {
919
+ display: flex;
920
+ align-items: center;
921
+ justify-content: center;
922
+ width: calc((100% - var(--spacing) * (var(--columns) - 1)) / var(--columns));
923
+ height: 0;
924
+ padding-bottom: calc((100% - var(--spacing) * (var(--columns) - 1)) / var(--columns));
925
+ position: relative;
926
+ cursor: pointer;
927
+ border-radius: var(--border-radius);
928
+ transition: all 0.2s ease;
929
+
930
+ &:active {
931
+ background-color: rgba(0, 0, 0, 0.05);
932
+ transform: scale(0.95);
933
+
934
+ .im-emoji-picker--dark & {
935
+ background-color: rgba(255, 255, 255, 0.1);
936
+ }
937
+ }
938
+
939
+ &--selected {
940
+ background-color: rgba(64, 158, 255, 0.1);
941
+ border: 2rpx solid #409eff;
942
+ }
943
+ }
944
+
945
+ .im-emoji-picker__emoji-char {
946
+ position: absolute;
947
+ top: 50%;
948
+ left: 50%;
949
+ transform: translate(-50%, -50%);
950
+ font-size: var(--emoji-size);
951
+ line-height: 1;
952
+ user-select: none;
953
+ }
954
+
955
+ .im-emoji-picker__emoji-image {
956
+ position: absolute;
957
+ top: 50%;
958
+ left: 50%;
959
+ transform: translate(-50%, -50%);
960
+ width: calc(var(--emoji-size) * 1.2);
961
+ height: calc(var(--emoji-size) * 1.2);
962
+ object-fit: contain;
963
+ }
964
+
965
+ .im-emoji-picker__emoji-name {
966
+ position: absolute;
967
+ bottom: -30rpx;
968
+ left: 50%;
969
+ transform: translateX(-50%);
970
+ font-size: 20rpx;
971
+ color: #909399;
972
+ white-space: nowrap;
973
+ max-width: 120%;
974
+ overflow: hidden;
975
+ text-overflow: ellipsis;
976
+
977
+ .im-emoji-picker--dark & {
978
+ color: #888888;
979
+ }
980
+ }
981
+
982
+ // 搜索结果
983
+ .im-emoji-picker__search-results {
984
+ padding: 24rpx;
985
+ }
986
+
987
+ // 空状态
988
+ .im-emoji-picker__empty {
989
+ display: flex;
990
+ flex-direction: column;
991
+ align-items: center;
992
+ justify-content: center;
993
+ padding: 80rpx 0;
994
+ color: #c0c4cc;
995
+
996
+ .im-emoji-picker--dark & {
997
+ color: #666666;
998
+ }
999
+ }
1000
+
1001
+ .im-emoji-picker__empty-text {
1002
+ margin-top: 20rpx;
1003
+ font-size: 28rpx;
1004
+ }
1005
+
1006
+ // 底部填充
1007
+ .im-emoji-picker__footer-spacer {
1008
+ height: 40rpx;
1009
+ }
1010
+
1011
+ // 底部操作栏
1012
+ .im-emoji-picker__footer {
1013
+ border-top: 1rpx solid #f0f0f0;
1014
+ padding: 20rpx 24rpx;
1015
+
1016
+ .im-emoji-picker--dark & {
1017
+ border-top-color: rgba(255, 255, 255, 0.1);
1018
+ }
1019
+ }
1020
+
1021
+ .im-emoji-picker__footer-default {
1022
+ display: flex;
1023
+ align-items: center;
1024
+ justify-content: space-between;
1025
+ }
1026
+
1027
+ .im-emoji-picker__preview {
1028
+ display: flex;
1029
+ align-items: center;
1030
+ gap: 16rpx;
1031
+ }
1032
+
1033
+ .im-emoji-picker__preview-emoji {
1034
+ font-size: 40rpx;
1035
+ line-height: 1;
1036
+
1037
+ image {
1038
+ width: 40rpx;
1039
+ height: 40rpx;
1040
+ }
1041
+ }
1042
+
1043
+ .im-emoji-picker__preview-name {
1044
+ font-size: 28rpx;
1045
+ color: #606266;
1046
+
1047
+ .im-emoji-picker--dark & {
1048
+ color: #cccccc;
1049
+ }
1050
+ }
1051
+
1052
+ .im-emoji-picker__actions {
1053
+ display: flex;
1054
+ gap: 16rpx;
1055
+ }
1056
+
1057
+ // 大表情预览
1058
+ .im-emoji-picker__preview-popup {
1059
+ position: fixed;
1060
+ top: 0;
1061
+ left: 0;
1062
+ right: 0;
1063
+ bottom: 0;
1064
+ background-color: rgba(0, 0, 0, 0.5);
1065
+ display: flex;
1066
+ align-items: center;
1067
+ justify-content: center;
1068
+ z-index: 10000;
1069
+ }
1070
+
1071
+ .im-emoji-picker__preview-content {
1072
+ background-color: #ffffff;
1073
+ border-radius: 24rpx;
1074
+ padding: 48rpx;
1075
+ text-align: center;
1076
+ min-width: 300rpx;
1077
+ max-width: 80vw;
1078
+
1079
+ .im-emoji-picker--dark & {
1080
+ background-color: #2a2a2a;
1081
+ }
1082
+ }
1083
+
1084
+ .im-emoji-picker__preview-large {
1085
+ font-size: 80rpx;
1086
+ line-height: 1;
1087
+ margin-bottom: 32rpx;
1088
+
1089
+ image {
1090
+ width: 80rpx;
1091
+ height: 80rpx;
1092
+ }
1093
+ }
1094
+
1095
+ .im-emoji-picker__preview-desc {
1096
+ display: block;
1097
+ font-size: 32rpx;
1098
+ font-weight: 600;
1099
+ margin-bottom: 8rpx;
1100
+ color: inherit;
1101
+ }
1102
+
1103
+ .im-emoji-picker__preview-code {
1104
+ display: block;
1105
+ font-size: 24rpx;
1106
+ color: #909399;
1107
+ margin-bottom: 32rpx;
1108
+
1109
+ .im-emoji-picker--dark & {
1110
+ color: #888888;
1111
+ }
1112
+ }
1113
+
1114
+ .im-emoji-picker__preview-actions {
1115
+ display: flex;
1116
+ gap: 24rpx;
1117
+ justify-content: center;
1118
+ }
1119
+
1120
+ // 响应式调整
1121
+ @media (max-width: 768px) {
1122
+ .im-emoji-picker {
1123
+ --columns: 7;
1124
+ }
1125
+
1126
+ .im-emoji-picker--small {
1127
+ --columns: 6;
1128
+ }
1129
+
1130
+ .im-emoji-picker--large {
1131
+ --columns: 5;
1132
+ }
1133
+ }
1134
+
1135
+ // 暗色模式适配
1136
+ @media (prefers-color-scheme: dark) {
1137
+ .im-emoji-picker--light {
1138
+ background-color: #2a2a2a;
1139
+ color: #ffffff;
1140
+ box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.3);
1141
+ }
1142
+ }
1143
+ </style>