/** 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 =
``
// WeChat 对 charset=utf-8 + encodeURIComponent 偶发不兼容,统一 base64
return 'data:image/svg+xml;base64,' + utf8ToBase64(svg)
}