gaoluyang
2026-06-29 27cd042df9aca0383a49f3514bc21958dd890912
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import type { ImFacePackApi } from '#/api/im/face/pack'
import type { ImFaceUserItemApi } from '#/api/im/face/userItem'
 
import { ref } from 'vue'
 
import { acceptHMRUpdate, defineStore } from 'pinia'
 
import { getFacePackList as apiGetFacePackList } from '#/api/im/face/pack'
import { createFaceUserItem as apiCreateFaceUserItem, deleteFaceUserItem as apiDeleteFaceUserItem, getFaceUserItemList as apiGetFaceUserItemList } from '#/api/im/face/userItem'
 
/**
 * IM 表情面板数据 store(系统表情包 + 个人表情)
 *
 * 不持久化:数据小、低频;每次进 IM 第一次打开表情面板时按需拉,关 tab 即丢弃
 * - 系统包:IM 主页 onMounted 后台预拉(不阻塞首屏),消除面板首次展开的白屏
 * - 个人表情:切到「收藏」tab / 长按消息「添加到表情」时按需拉
 */
export const useFaceStore = defineStore('imFace', () => {
 
  /** 系统表情包列表(含每个包的 items);运营管理后台维护 */
  const facePacks = ref<ImFacePackApi.FacePackUser[]>([])
  /** 个人表情包列表(用户长按「添加到表情」/ 上传产生) */
  const faceUserItems = ref<ImFaceUserItemApi.FaceUserItem[]>([])
 
  /** clear() 时递增;旧账号请求返回后不写入新账号内存 */
  let storeEpoch = 0
 
  /**
   * 系统表情包拉取 promise;ensureFacePackList 内 cache:
   * - null = 还没拉过,下次调用真发请求
   * - resolve 后保留对象 = 后续调用 await 立即返回,不再发请求
   * - reject 后置回 null,让调用方下次重试
   */
  let facePacksPromise: null | Promise<void> = null
  /** 按需拉取系统表情包(已拉过则直接复用 cached promise) */
  async function ensureFacePackList(): Promise<void> {
    if (!facePacksPromise) {
      const requestEpoch = storeEpoch
      facePacksPromise = apiGetFacePackList()
        .then((data) => {
          if (requestEpoch !== storeEpoch) {
            return
          }
          facePacks.value = data || []
        })
        .catch((error) => {
          console.warn('[IM] 拉取表情包失败', error)
          if (requestEpoch === storeEpoch) {
            facePacksPromise = null
          }
          throw error
        })
    }
    return facePacksPromise
  }
 
  /** 个人表情拉取 promise;语义同上 */
  let faceUserItemsPromise: null | Promise<void> = null
  /** 按需拉取个人表情(已拉过则直接复用 cached promise) */
  async function ensureFaceUserItemList(): Promise<void> {
    if (!faceUserItemsPromise) {
      const requestEpoch = storeEpoch
      faceUserItemsPromise = apiGetFaceUserItemList()
        .then((data) => {
          if (requestEpoch !== storeEpoch) {
            return
          }
          faceUserItems.value = data || []
        })
        .catch((error) => {
          console.warn('[IM] 拉取个人表情失败', error)
          if (requestEpoch === storeEpoch) {
            faceUserItemsPromise = null
          }
          throw error
        })
    }
    return faceUserItemsPromise
  }
 
  /**
   * 添加个人表情;服务端对同 URL 抛 FACE_USER_ITEM_DUPLICATED 错误
   *
   * 来源:1. 用户在表情面板「+」上传图片  2. 长按消息「添加到表情」
   */
  async function addFaceUserItem(reqVO: ImFaceUserItemApi.FaceUserItemSaveReqVO): Promise<boolean> {
    const requestEpoch = storeEpoch
    const id = await apiCreateFaceUserItem(reqVO)
    if (!id) {
      return false
    }
    // 已切账号时跳过旧请求结果
    if (requestEpoch !== storeEpoch) {
      return false
    }
    // id 不在缓存里才插入;服务端唯一约束兜底了 race,本地理论上不会拿到重复 id
    if (!faceUserItems.value.some((item) => item.id === id)) {
      faceUserItems.value.unshift({
        id,
        url: reqVO.url,
        name: reqVO.name,
        width: reqVO.width,
        height: reqVO.height
      })
    }
    return true
  }
 
  /** 删除个人表情;本地立即移除 */
  async function removeFaceUserItem(id: number): Promise<boolean> {
    const requestEpoch = storeEpoch
    try {
      await apiDeleteFaceUserItem(id)
      // 已切账号时跳过旧请求结果
      if (requestEpoch !== storeEpoch) {
        return false
      }
      faceUserItems.value = faceUserItems.value.filter((item) => item.id !== id)
      return true
    } catch (error) {
      console.warn('[IM] 删除个人表情失败', { id }, error)
      return false
    }
  }
 
  /** 清空表情缓存 */
  function clear(): void {
    facePacks.value = []
    faceUserItems.value = []
    facePacksPromise = null
    faceUserItemsPromise = null
    storeEpoch++
  }
 
  return {
    facePacks,
    faceUserItems,
    ensureFacePackList,
    ensureFaceUserItemList,
    addFaceUserItem,
    removeFaceUserItem,
    clear
  }
})
 
/** 在 setup 外(路由守卫等)取 store 实例的工具方法 */
export const useFaceStoreWithOut = () => useFaceStore()
 
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useFaceStore, import.meta.hot))
}