gaoluyang
3 小时以前 cb9cd49627b65a4c0e137e08063271a8cefe1826
src/views/im/home/components/group/group-avatar.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,131 @@
<script lang="ts" setup>
import type { GroupMember } from '../../types'
import { computed, ref, watch } from 'vue'
import {
  buildGroupAvatar,
  getCachedGroupAvatar,
  setCachedGroupAvatar
} from '../../../utils/group'
import { getMemberDisplayName } from '../../../utils/user'
import { useFriendStore } from '../../store/friendStore'
import { useGroupStore } from '../../store/groupStore'
import { UserAvatar } from '../user'
defineOptions({ name: 'ImGroupAvatar' })
const props = withDefaults(
  defineProps<{
    clickable?: boolean // æ˜¯å¦å¯ç‚¹å‡»ï¼ˆé»˜è®¤ false,列表里仅展示)
    groupId: number // ç¾¤ç¼–号;用于查 store æ‹¿æˆå‘˜å¤´åƒ
    name?: string // ç¾¤åï¼›è‰²å¡å…œåº•文字
    previewable?: boolean // æ˜¯å¦ç‚¹å¤´åƒç›´æŽ¥æ”¾å¤§é¢„览(群详情大头像位用)
    previewZIndex?: number // é¢„览层 z-index
    radius?: string // åœ†è§’,CSS é•¿åº¦
    size?: number // å°ºå¯¸ï¼ˆpx),正方形
    url?: string // æœåŠ¡ç«¯å·²è®¾ç½®çš„ç¾¤å¤´åƒ URL;非空则直接用,不拼图
  }>(),
  {
    clickable: false,
    name: '',
    previewable: false,
    previewZIndex: 2000,
    radius: '15%',
    size: 42,
    url: ''
  }
)
const friendStore = useFriendStore()
const groupStore = useGroupStore()
const mergedUrl = ref('')
// ç«žæ€ä¿æŠ¤ï¼šä¸¢å¼ƒè¿‡æœŸ await ç»“æžœ
let mergeToken = 0
/** æŒ‰å®¹å™¨ size Ã— DPR ç®— canvas å®žé™…像素,避免 2x / 3x retina å±æ‹¼å›¾ç³Šï¼›DPR å°é¡¶ 3 é˜²æ­¢è¶…高分辨率画布过大 */
function getTargetSize(size: number): number {
  const dpr = Math.min(window.devicePixelRatio || 1, 3)
  return Math.max(Math.round(size * dpr), 64)
}
/** store é‡Œæ•´ç¾¤æˆå‘˜æ˜¯å¦ã€Œå®Œæ•´åŠ è½½ã€è¿‡ï¼›åªåœ¨ä¸º true æ—¶æ‰æ‹¼å›¾ï¼Œé¿å…åˆ—表场景批量发接口 */
const loadedMembers = computed<GroupMember[] | null>(() => {
  const g = groupStore.getGroup(props.groupId)
  if (!g?.membersLoaded || !g.members) {
    return null
  }
  return g.members
})
/** å‰ 9 ä¸ªæˆå‘˜çš„æ‹¼å›¾å…¥å‚ï¼›name èµ° getMemberDisplayName å£å¾„(好友备注 > ç¾¤æ˜µç§° > çœŸå®žæ˜µç§°ï¼‰ */
const memberItems = computed(() => {
  const members = loadedMembers.value
  if (!members) {
    return []
  }
  return members.slice(0, 9).map((m) => ({
    avatar: m.avatar || '',
    name: getMemberDisplayName(m, friendStore.getFriend(m.userId))
  }))
})
/** æˆå‘˜å¿«ç…§ç­¾åï¼šæ‹¼ (avatar, name) å­—段,原地修改任一字段都会让 watch é‡ç®— */
const memberSignature = computed(() =>
  memberItems.value.map((it) => `${it.avatar}#${it.name}`).join('|')
)
/** èµ° buildGroupAvatar æ‹¼å›¾å¹¶å†™å›ž mergedUrl;mergeToken æ ¡éªŒé¿å…è€ await è¦†ç›–新结果 */
async function applyMerge(key: string, targetSize: number): Promise<void> {
  const myToken = ++mergeToken
  const cached = getCachedGroupAvatar(key)
  if (cached) {
    mergedUrl.value = cached
    return
  }
  const dataUrl = await buildGroupAvatar(memberItems.value, { targetSize })
  if (myToken !== mergeToken) {
    return
  }
  if (dataUrl) {
    setCachedGroupAvatar(key, dataUrl)
  }
  mergedUrl.value = dataUrl
}
watch(
  () => [props.url, props.groupId, props.size, memberSignature.value] as const,
  ([url, groupId, size, signature]) => {
    if (url) {
      mergedUrl.value = ''
      return
    }
    if (!signature) {
      mergeToken++
      mergedUrl.value = ''
      groupStore.loadGroupMemberList(groupId)
      return
    }
    const targetSize = getTargetSize(size)
    const key = `${groupId}:${targetSize}:${signature}`
    applyMerge(key, targetSize)
  },
  { immediate: true }
)
/** æœ€ç»ˆå±•示 url:服务端 url ä¼˜å…ˆ â†’ æ‹¼å›¾ â†’ ç©ºå­—符串(让 UserAvatar èµ°è‰²å¡ï¼‰ */
const finalUrl = computed(() => props.url || mergedUrl.value)
</script>
<template>
  <!-- url éžç©ºèµ°åŽŸå›¾ï¼›url ç©ºæ—¶å–前 9 ä¸ªæˆå‘˜å¤´åƒæ‹¼ä¹å®«æ ¼ dataURL,成员未在 store ç¼“存时走色卡兜底 -->
  <UserAvatar
    :url="finalUrl"
    :name="name"
    :size="size"
    :radius="radius"
    :clickable="clickable"
    :previewable="previewable"
    :preview-z-index="previewZIndex"
  />
</template>