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