From b64a0deae5b5d33f9e20671a68936b27f0b9b00b Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 21 七月 2026 18:03:03 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_pro2.0' into dev_pro2.0

---
 src/views/im/utils/group.ts |  289 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 289 insertions(+), 0 deletions(-)

diff --git a/src/views/im/utils/group.ts b/src/views/im/utils/group.ts
new file mode 100644
index 0000000..ddc53fa
--- /dev/null
+++ b/src/views/im/utils/group.ts
@@ -0,0 +1,289 @@
+import type { FriendLite } from '../home/types'
+
+import { loadImage } from './image'
+import { getAvatarBgColor, getAvatarText } from './user'
+
+/** 榛樿缇ゅ悕鐢熸垚锛氭墍閫夊ソ鍙嬪墠 4 涓悕瀛楁嫾鎺ワ紝瓒呰繃琛ャ�岀瓑 N 浜恒�嶏紱涓虹┖鍏滃簳銆岀兢鑱娿�� */
+export function buildDefaultGroupName(members: FriendLite[]): string {
+  if (members.length === 0) {
+    return '缇よ亰'
+  }
+  const names = members.slice(0, 4).map((m) => m.displayName || m.nickname || '')
+  const head = names.filter(Boolean).join('銆�')
+  if (members.length > 4) {
+    // members 鍙惈琚�夊ソ鍙嬶紝+1 鎶婂垱寤鸿�呬篃璁″叆瀹為檯鎴愬憳鏁�
+    return `${head}绛�${members.length + 1}浜篳
+  }
+  return head || '缇よ亰'
+}
+
+/** 缇ゅご鍍忓崟鏍肩殑杈撳叆锛氬ご鍍� URL + 鍚嶅瓧 锛岃嚦灏戠粰涓�涓紝浜岃�呴兘缂烘椂璧扮伆搴曠┖鏍� */
+export interface GroupAvatarMember {
+  /** 澶村儚 URL锛涚己澶辨垨鍔犺浇澶辫触鏃舵寜 name 鐢昏壊鍗� */
+  avatar?: string
+  /** 鏄剧ず鍚嶏紙鏄电О / 澶囨敞锛夛紱鑹插崱鏂囧瓧 + 搴曡壊 hash 鏉ユ簮 */
+  name?: string
+}
+
+/** 缇ゅご鍍忔嫾鎺ョ殑鍙�夊弬鏁� */
+export interface BuildGroupAvatarOptions {
+  /** 杈撳嚭鐢诲竷杈归暱锛堝儚绱狅級锛涢粯璁� 64 */
+  targetSize?: number
+  /** 鍗曟牸涔嬮棿鐨勯棿闅旓紙鍍忕礌锛夛紱榛樿 1 */
+  divider?: number
+  /** 鐢诲竷搴曡壊锛涢粯璁ら�忔槑锛岃涓婁笅鐣欑櫧閫忓嚭瀹夸富瀹瑰櫒搴曡壊 */
+  background?: string
+}
+
+/** 鍗曟牸鍦ㄧ敾甯冧笂鐨勪綅缃� + 灏哄 */
+interface CellRect {
+  x: number
+  y: number
+  w: number
+  h: number
+}
+
+/** 鍗曟牸鐨勬渶缁堢粯鍒跺唴瀹� */
+type Cell =
+  | { bg: string; kind: 'color'; text: string; }
+  | { img: HTMLImageElement; kind: 'image'; }
+
+/**
+ * 鎶婄兢鎴愬憳澶村儚鎷兼垚涓�寮犳柟褰㈢兢澶村儚 dataURL锛屾寜 1 ~ 9 寮犺蛋涔濆鏍煎彉浣撳竷灞�
+ *
+ * - 浠呭彇鍓� 9 涓垚鍛�
+ * - 鍗曟牸 avatar 涓虹┖ / 鍔犺浇澶辫触锛氭寜 name 鍦� canvas 涓婄敾鑹插崱鍏滃簳
+ * - 璺ㄥ煙瑕佹眰鍥剧墖婧愬紑鍚� CORS锛坕mg.crossOrigin = 'anonymous'锛�
+ */
+export async function buildGroupAvatar(
+  members: GroupAvatarMember[],
+  options: BuildGroupAvatarOptions = {}
+): Promise<string> {
+  const targetSize = options.targetSize ?? 64
+  const divider = options.divider ?? 1
+  const background = options.background
+
+  const top = members.slice(0, 9)
+  if (top.length === 0) {
+    return ''
+  }
+
+  const cells = await Promise.all(top.map((m) => resolveCell(m)))
+  const rects = computeCellRects(cells.length, targetSize, divider)
+
+  const canvas = document.createElement('canvas')
+  canvas.width = targetSize
+  canvas.height = targetSize
+  const ctx = canvas.getContext('2d')
+  if (!ctx) {
+    return ''
+  }
+  if (background) {
+    ctx.fillStyle = background
+    ctx.fillRect(0, 0, targetSize, targetSize)
+  }
+  cells.forEach((cell, idx) => {
+    const rect = rects[idx]
+    if (!rect) {
+      return
+    }
+    drawCell(ctx, rect, cell)
+  })
+  return canvas.toDataURL('image/png')
+}
+
+/** 缇ゅご鍍� dataURL 缂撳瓨涓婇檺锛氭瘡鏉$害 5 ~ 30KB锛�200 鏉¤蒋灏侀《绾� 5MB 甯搁┗ */
+const MERGED_AVATAR_CACHE_MAX = 200
+const mergedAvatarCache = new Map<string, string>()
+
+/** 鍙栫兢澶村儚 dataURL 缂撳瓨锛涘懡涓椂鎸� LRU 鎻愬埌鏈熬 */
+export function getCachedGroupAvatar(key: string): string | undefined {
+  const cached = mergedAvatarCache.get(key)
+  if (cached === undefined) {
+    return undefined
+  }
+  mergedAvatarCache.delete(key)
+  mergedAvatarCache.set(key, cached)
+  return cached
+}
+
+/** 鍐欑兢澶村儚 dataURL 缂撳瓨锛涜秴涓婇檺鏃朵涪寮冩渶鏃╀竴鏉★紙Map 杩唬椤哄簭 = 鎻掑叆椤哄簭锛岄厤鍚� get 鐨勬彁鍗囧嵆 LRU锛� */
+export function setCachedGroupAvatar(key: string, value: string): void {
+  if (mergedAvatarCache.has(key)) {
+    mergedAvatarCache.delete(key)
+  } else if (mergedAvatarCache.size >= MERGED_AVATAR_CACHE_MAX) {
+    const oldest = mergedAvatarCache.keys().next().value
+    if (oldest !== undefined) {
+      mergedAvatarCache.delete(oldest)
+    }
+  }
+  mergedAvatarCache.set(key, value)
+}
+
+/** 鍗曟垚鍛� 鈫� Cell锛氭湁 avatar 涓斿姞杞芥垚鍔熻蛋 image锛屽惁鍒欒蛋 color */
+async function resolveCell(member: GroupAvatarMember): Promise<Cell> {
+  if (member.avatar) {
+    const img = await loadImage(member.avatar)
+    if (img) {
+      return { kind: 'image', img }
+    }
+  }
+  return {
+    kind: 'color',
+    text: getAvatarText(member.name),
+    bg: getAvatarBgColor(member.name)
+  }
+}
+
+/** 鎶婂崟涓� Cell 鐢诲埌 ctx 涓婃寚瀹氭牸瀛愰噷锛沬mage 璧� cover 瑁佸壀淇濇瘮渚嬶紝color 璧� fillRect + 灞呬腑鏂囧瓧 */
+function drawCell(
+  ctx: CanvasRenderingContext2D,
+  rect: CellRect,
+  cell: Cell
+): void {
+  const { x, y, w, h } = rect
+  if (cell.kind === 'image') {
+    drawImageCover(ctx, cell.img, x, y, w, h)
+    return
+  }
+  ctx.fillStyle = cell.bg
+  ctx.fillRect(x, y, w, h)
+  if (!cell.text) {
+    return
+  }
+  // 瀛楀彿鎸夌煭杈圭畻锛涘崟瀛� 0.42銆佸弻瀛� 0.34
+  const baseSize = Math.min(w, h)
+  const fontSize = Math.floor(baseSize * (cell.text.length > 1 ? 0.34 : 0.42))
+  ctx.fillStyle = '#FFFFFF'
+  ctx.font = `500 ${fontSize}px -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif`
+  ctx.textAlign = 'center'
+  ctx.textBaseline = 'middle'
+  ctx.fillText(cell.text, x + w / 2, y + h / 2)
+}
+
+/** cover 瑁佸壀锛氫粠婧愬浘涓績瑁佸嚭涓庣洰鏍囩煩褰㈠悓姣斾緥鐨勫瓙鍖哄煙锛岀敾鍒扮洰鏍囩煩褰紙淇濇寔姣斾緥涓嶅彉褰級 */
+function drawImageCover(
+  ctx: CanvasRenderingContext2D,
+  img: HTMLImageElement,
+  dx: number,
+  dy: number,
+  dw: number,
+  dh: number
+): void {
+  const srcAspect = img.width / img.height
+  const dstAspect = dw / dh
+  let sx = 0
+  let sy = 0
+  let sw = img.width
+  let sh = img.height
+  if (srcAspect > dstAspect) {
+    sw = sh * dstAspect
+    sx = (img.width - sw) / 2
+  } else if (srcAspect < dstAspect) {
+    sh = sw / dstAspect
+    sy = (img.height - sh) / 2
+  }
+  ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh)
+}
+
+/**
+ * 鎸夊紶鏁拌绠楁瘡鏍肩殑浣嶇疆 + 灏哄
+ *
+ * - 1 寮狅細鍗犳弧鐢诲竷
+ * - 2~4 寮狅細2 鍒� 2 琛屾鏂瑰舰鍗曟牸
+ * - 5~6 寮狅細3 鍒� 2 琛屾鏂瑰舰鍗曟牸 + 鍨傜洿灞呬腑
+ * - 7~9 寮狅細3 鍒� 3 琛屾鏂瑰舰鍗曟牸
+ */
+function computeCellRects(count: number, target: number, divider: number): CellRect[] {
+  if (count <= 0) {
+    return []
+  }
+  if (count === 1) {
+    return [{ x: 0, y: 0, w: target, h: target }]
+  }
+  if (count <= 4) {
+    return computeCellRectsSmall(count, target, divider)
+  }
+  if (count <= 6) {
+    return computeCellRectsMedium(count, target, divider)
+  }
+  return computeCellRectsLarge(count, target, divider)
+}
+
+/** 2~4 寮狅細2 鍒� 2 琛屾鏂瑰舰 */
+function computeCellRectsSmall(count: number, target: number, divider: number): CellRect[] {
+  const s = (target - 3 * divider) / 2
+  const step = s + divider
+  const rects: CellRect[] = []
+  switch (count) {
+    case 2: {
+      // 灞呬腑 1 琛岋紙鐣欎笂涓嬬┖鐧斤級
+      const y = target / 4
+      rects.push({ x: divider, y, w: s, h: s }, { x: divider + step, y, w: s, h: s })
+      break
+    }
+    case 3: {
+      // 涓� 1 灞呬腑 + 涓� 2
+      rects.push({ x: target / 4, y: divider, w: s, h: s })
+      const y1 = divider + step
+      rects.push({ x: divider, y: y1, w: s, h: s }, { x: divider + step, y: y1, w: s, h: s })
+      break
+    }
+    case 4: {
+      // 2脳2 婊¢摵
+      rects.push({ x: divider, y: divider, w: s, h: s }, { x: divider + step, y: divider, w: s, h: s })
+      const y1 = divider + step
+      rects.push({ x: divider, y: y1, w: s, h: s }, { x: divider + step, y: y1, w: s, h: s })
+      break
+    }
+  }
+  return rects
+}
+
+/** 5~6 寮狅細3 鍒� 2 琛屾鏂瑰舰鍗曟牸 + 鍨傜洿灞呬腑锛涗笂涓嬬暀鐧界敱閫忔槑鑳屾櫙閫忓嚭 GroupAvatar 瀹瑰櫒搴曡壊 */
+function computeCellRectsMedium(count: number, target: number, divider: number): CellRect[] {
+  const s = (target - 4 * divider) / 3
+  const step = s + divider
+  const xs = [divider, divider + step, divider + 2 * step] as const
+  // 2 琛岄珮 + 1 琛岄棿 div
+  const totalH = 2 * s + divider
+  const y0 = (target - totalH) / 2
+  const y1 = y0 + step
+  const rects: CellRect[] = []
+  if (count === 5) {
+    // 涓� 2 灞呬腑锛堝乏鍙冲绉扮暀鐧斤級 + 涓� 3 宸﹀彸璐磋竟
+    const upX0 = (target - 2 * s - divider) / 2
+    rects.push({ x: upX0, y: y0, w: s, h: s }, { x: upX0 + step, y: y0, w: s, h: s }, { x: xs[0], y: y1, w: s, h: s }, { x: xs[1], y: y1, w: s, h: s }, { x: xs[2], y: y1, w: s, h: s })
+    return rects
+  }
+  // count === 6锛氫笂 3 + 涓� 3 婊¢摵
+  rects.push({ x: xs[0], y: y0, w: s, h: s }, { x: xs[1], y: y0, w: s, h: s }, { x: xs[2], y: y0, w: s, h: s }, { x: xs[0], y: y1, w: s, h: s }, { x: xs[1], y: y1, w: s, h: s }, { x: xs[2], y: y1, w: s, h: s })
+  return rects
+}
+
+/** 7~9 寮狅細3 鍒� 3 琛屾鏂瑰舰鍗曟牸 */
+function computeCellRectsLarge(count: number, target: number, divider: number): CellRect[] {
+  const s = (target - 4 * divider) / 3
+  const step = s + divider
+  const xs = [divider, divider + step, divider + 2 * step] as const
+  const ys = [divider, divider + step, divider + 2 * step] as const
+  const rects: CellRect[] = []
+  if (count === 7) {
+    // 涓� 1 灞呬腑 + 涓� 3 + 涓� 3
+    rects.push({ x: xs[1], y: ys[0], w: s, h: s }, { x: xs[0], y: ys[1], w: s, h: s }, { x: xs[1], y: ys[1], w: s, h: s }, { x: xs[2], y: ys[1], w: s, h: s }, { x: xs[0], y: ys[2], w: s, h: s }, { x: xs[1], y: ys[2], w: s, h: s }, { x: xs[2], y: ys[2], w: s, h: s })
+    return rects
+  }
+  if (count === 8) {
+    // 涓� 2 灞呬腑 + 涓� 3 + 涓� 3
+    const upX0 = (target - 2 * s - divider) / 2
+    rects.push({ x: upX0, y: ys[0], w: s, h: s }, { x: upX0 + step, y: ys[0], w: s, h: s }, { x: xs[0], y: ys[1], w: s, h: s }, { x: xs[1], y: ys[1], w: s, h: s }, { x: xs[2], y: ys[1], w: s, h: s }, { x: xs[0], y: ys[2], w: s, h: s }, { x: xs[1], y: ys[2], w: s, h: s }, { x: xs[2], y: ys[2], w: s, h: s })
+    return rects
+  }
+  // count === 9锛�3脳3 婊¢摵
+  for (const y of ys) {
+    for (const x of xs) {
+      rects.push({ x, y, w: s, h: s })
+    }
+  }
+  return rects
+}

--
Gitblit v1.9.3