/** OA 审批应用图标:与 web-admin/src/utils/oa-app-icons.ts 色对/解析对齐 */ import { MINIO_BASE_URL } from './config.js' export const OA_APP_ICON_COLOR_PAIRS = [ { bg: '#d1fae5', fg: '#059669' }, { bg: '#cffafe', fg: '#0891b2' }, { bg: '#dbeafe', fg: '#2563eb' }, { bg: '#e0e7ff', fg: '#4f46e5' }, { bg: '#ede9fe', fg: '#7c3aed' }, { bg: '#fce7f3', fg: '#db2777' }, { bg: '#ffedd5', fg: '#ea580c' }, { bg: '#fef3c7', fg: '#d97706' } ] export const OA_APP_DEFAULT_EP_ICON = 'Document' export const OA_APP_DEFAULT_BG = OA_APP_ICON_COLOR_PAIRS[0].bg export const OA_APP_DEFAULT_FG = OA_APP_ICON_COLOR_PAIRS[0].fg /** 精选 Element Plus 图标名(PascalCase,与 @element-plus/icons-vue 导出一致) */ export const OA_APP_EP_ICON_NAMES = [ 'Document', 'Calendar', 'Clock', 'Wallet', 'User', 'Tickets', 'Briefcase', 'Money', 'Van', 'OfficeBuilding', 'Message', 'Setting', 'CollectionTag', 'Checked', 'Present', 'Stamp', 'CreditCard', 'Notebook', 'Bell', 'Location' ] const BG_SET = new Set(OA_APP_ICON_COLOR_PAIRS.map((p) => p.bg.toLowerCase())) const FG_BY_BG = Object.fromEntries( OA_APP_ICON_COLOR_PAIRS.map((p) => [p.bg.toLowerCase(), p.fg]) ) const ICON_SET = new Set(OA_APP_EP_ICON_NAMES) export function isEpIconUrl(iconUrl) { return String(iconUrl || '').trim().startsWith('ep:') } export function iconFgFromBg(bg) { const key = String(bg || '').trim().toLowerCase() return FG_BY_BG[key] || OA_APP_DEFAULT_FG } export function buildEpIconUrl(icon, bg) { const name = ICON_SET.has(icon) ? icon : OA_APP_DEFAULT_EP_ICON const color = BG_SET.has(String(bg).toLowerCase()) ? String(bg).toLowerCase() : OA_APP_DEFAULT_BG return `ep:${name}|${color}` } export function parseEpIconUrl(iconUrl) { const raw = String(iconUrl || '').trim() if (!raw.startsWith('ep:')) { return { icon: OA_APP_DEFAULT_EP_ICON, bg: OA_APP_DEFAULT_BG, fg: OA_APP_DEFAULT_FG } } const body = raw.slice(3) const pipe = body.indexOf('|') if (pipe < 0) { return { icon: OA_APP_DEFAULT_EP_ICON, bg: OA_APP_DEFAULT_BG, fg: OA_APP_DEFAULT_FG } } let icon = body.slice(0, pipe).trim() let bg = body.slice(pipe + 1).trim().toLowerCase() if (!ICON_SET.has(icon)) icon = OA_APP_DEFAULT_EP_ICON if (!BG_SET.has(bg)) bg = OA_APP_DEFAULT_BG return { icon, bg, fg: iconFgFromBg(bg) } } /** * 旧版静态图标存 `/oa-app-icons/*.svg`(web-admin/public)。 * 小程序/H5 需走 `static/oa-app-icons/`,否则 5173 下 404。 * 上传图存 MinIO 相对 key(如 `202608/11/xxx.png`),需拼 MINIO_BASE_URL。 */ export function resolveOaAppIconImageSrc(iconUrl) { const u = String(iconUrl || '').trim() if (!u || isEpIconUrl(u)) return '' if (/^https?:\/\//i.test(u)) return u if (u.startsWith('/oa-app-icons/')) { return '/static' + u } if (u.startsWith('/static/')) return u const minioBase = String(MINIO_BASE_URL || '').replace(/\/$/, '') // 同域反代路径:本机 H5 无 /minio 代理时,改写为绝对 MinIO 基址 if (u.startsWith('/minio/zkxw/')) { if (minioBase && /^https?:\/\//i.test(minioBase)) { return `${minioBase}/${u.slice('/minio/zkxw/'.length)}` } return u } if (u.startsWith('/')) return u return minioBase ? `${minioBase}/${u.replace(/^\//, '')}` : u } export function resolveOaAppIcon(iconUrl) { const u = String(iconUrl || '').trim() if (!u) { const p = parseEpIconUrl(buildEpIconUrl(OA_APP_DEFAULT_EP_ICON, OA_APP_DEFAULT_BG)) return { kind: 'ep', icon: p.icon, bg: p.bg, fg: p.fg } } if (isEpIconUrl(u)) { const p = parseEpIconUrl(u) return { kind: 'ep', icon: p.icon, bg: p.bg, fg: p.fg } } return { kind: 'image', src: resolveOaAppIconImageSrc(u) } } const EP_ICON_SVG_INNER = { Document: '', Calendar: '', Clock: '', Wallet: '', User: '', Tickets: '', Briefcase: '', Money: '', Van: '', OfficeBuilding: '', Message: '', Setting: '', CollectionTag: '', Checked: '', Present: '', Stamp: '', CreditCard: '', Notebook: '', Bell: '', Location: '' } const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' /** UTF-8 字符串 → base64;不依赖 btoa / Buffer(微信 appservice 两者皆无) */ function utf8ToBase64(str) { const utf8 = unescape(encodeURIComponent(String(str || ''))) if (typeof btoa === 'function') { return btoa(utf8) } let out = '' for (let i = 0; i < utf8.length; i += 3) { const a = utf8.charCodeAt(i) const b = i + 1 < utf8.length ? utf8.charCodeAt(i + 1) : 0 const c = i + 2 < utf8.length ? utf8.charCodeAt(i + 2) : 0 const n = (a << 16) | (b << 8) | c out += BASE64_CHARS[(n >> 18) & 63] out += BASE64_CHARS[(n >> 12) & 63] out += i + 1 < utf8.length ? BASE64_CHARS[(n >> 6) & 63] : '=' out += i + 2 < utf8.length ? BASE64_CHARS[n & 63] : '=' } return out } /** 生成小程序 可用的 SVG data-URI(base64,兼容微信) */ export function epIconDataUri(icon, fg) { const name = ICON_SET.has(icon) ? icon : OA_APP_DEFAULT_EP_ICON const stroke = fg || OA_APP_DEFAULT_FG const inner = EP_ICON_SVG_INNER[name] || EP_ICON_SVG_INNER.Document const svg = `${inner}` // WeChat 对 charset=utf-8 + encodeURIComponent 偶发不兼容,统一 base64 return 'data:image/svg+xml;base64,' + utf8ToBase64(svg) }