Files
zkxw/docs/demos/oa-app-icon-picker-demo.html
T
zlwuandCursor 0853777fdf docs: add OA app icon pastel + Element Plus design and demo.
Capture the approved encoding (ep:Icon|#bg), 8-color palette, picker UX, and interactive HTML demo for implementation planning.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 12:54:27 +08:00

800 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OA 审批 · 应用图标(淡底色 + EP Icon)Demo</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--primary: #10b981;
--primary-dark: #059669;
--primary-bg: #ecfdf5;
--text: #1e293b;
--text-muted: #64748b;
--text-strong: #0f172a;
--border: #d9e8e2;
--bg-page: #f1f5f9;
--surface: #ffffff;
--shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
--radius: 10px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text);
background: #eef2f0;
padding: 24px;
}
.wrap { max-width: 1120px; margin: 0 auto; }
.page-head h1 {
margin: 0 0 6px;
font-size: 22px;
font-weight: 700;
color: var(--text-strong);
}
.page-head p {
margin: 0 0 20px;
color: var(--text-muted);
max-width: 720px;
}
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-bottom: 16px;
overflow: hidden;
}
.panel-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 18px;
border-bottom: 1px solid #eef2f0;
}
.panel-top h2 {
margin: 0;
font-size: 15px;
font-weight: 650;
display: flex;
align-items: center;
gap: 8px;
}
.panel-top h2::before {
content: '';
width: 3px;
height: 14px;
border-radius: 2px;
background: var(--primary);
}
.panel-body { padding: 18px; }
.hint {
margin: 0 0 14px;
color: var(--text-muted);
font-size: 13px;
}
.compare {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 800px) {
.compare { grid-template-columns: 1fr; }
}
.compare-col {
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 14px;
background: #fafafa;
}
.compare-col h3 {
margin: 0 0 4px;
font-size: 13px;
font-weight: 650;
}
.badge {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
margin-bottom: 12px;
}
.badge.old { background: #fee2e2; color: #b91c1c; }
.badge.new { background: var(--primary-bg); color: var(--primary-dark); }
.icon-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.swatch {
width: 36px;
height: 36px;
border-radius: 9px;
display: grid;
place-items: center;
color: #fff;
box-shadow: 0 1px 2px rgba(15,23,42,.08);
}
.swatch svg { width: 16px; height: 16px; }
/* 图标色由 JS 按底色深色变体写入,勿强制灰/白 */
.card-row {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 12px;
}
.oa-card {
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 14px;
background: #fff;
transition: border-color .15s, box-shadow .15s;
}
.oa-card:hover {
border-color: #a7f3d0;
box-shadow: 0 4px 12px rgba(16,185,129,.08);
}
.oa-card__main {
display: flex;
gap: 12px;
align-items: center;
min-width: 0;
}
.oa-card__icon {
width: 32px;
height: 32px;
border-radius: 8px;
display: grid;
place-items: center;
flex-shrink: 0;
}
.oa-card__icon svg { width: 16px; height: 16px; }
.oa-card__name {
display: block;
font-weight: 600;
color: var(--text-strong);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.oa-card__meta {
display: flex;
gap: 6px;
margin-top: 4px;
flex-wrap: wrap;
}
.tag {
font-size: 11px;
padding: 1px 7px;
border-radius: 4px;
background: var(--primary-bg);
color: var(--primary-dark);
font-weight: 500;
}
.tag.warn { background: #fffbeb; color: #d97706; }
.tag.muted { background: #f1f5f9; color: #64748b; }
.oa-card__ops {
display: flex;
gap: 6px;
justify-content: flex-end;
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid #f1f5f9;
}
.btn {
height: 28px;
padding: 0 10px;
border-radius: 6px;
border: 1px solid #d1d5db;
background: #fff;
color: var(--text);
font-size: 12px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
}
.btn:hover { border-color: #94a3b8; }
.btn.primary {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.btn.ghost {
background: #fff;
border-color: #cbd5e1;
}
.btn:disabled { opacity: .55; cursor: not-allowed; }
/* Dialog */
.dialog-mask {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.35);
display: none;
align-items: center;
justify-content: center;
padding: 16px;
z-index: 40;
}
.dialog-mask.open { display: flex; }
.dialog {
width: min(640px, 100%);
max-height: calc(100vh - 100px);
background: #fff;
border-radius: 12px;
box-shadow: 0 20px 50px rgba(15,23,42,.18);
display: flex;
flex-direction: column;
overflow: hidden;
}
.dialog__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #eef2f0;
font-weight: 650;
}
.dialog__body {
padding: 18px;
overflow: auto;
}
.dialog__foot {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 18px;
border-top: 1px solid #eef2f0;
background: #fafafa;
}
.form-label {
display: block;
font-size: 12px;
color: var(--text-muted);
margin-bottom: 6px;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.span-2 { grid-column: 1 / -1; }
@media (max-width: 560px) {
.form-grid { grid-template-columns: 1fr; }
.span-2 { grid-column: auto; }
}
input[type="text"], textarea, select {
width: 100%;
height: 36px;
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 0 10px;
font: inherit;
color: var(--text);
background: #fff;
}
textarea { height: 72px; padding: 8px 10px; resize: vertical; }
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(16,185,129,.15);
}
.picker {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: flex-start;
}
.preview {
width: 56px;
height: 56px;
border-radius: 12px;
display: grid;
place-items: center;
box-shadow: 0 1px 3px rgba(15,23,42,.08);
flex-shrink: 0;
}
.preview svg { width: 26px; height: 26px; }
.picker-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
flex: 1;
min-width: 200px;
}
.picker-tip {
flex-basis: 100%;
margin: 0;
font-size: 12px;
color: #94a3b8;
}
.icon-panel {
margin-top: 12px;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 12px;
background: #f8fafc;
display: none;
}
.icon-panel.open { display: block; }
.icon-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 10px;
}
.icon-panel__head strong { font-size: 13px; }
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
gap: 8px;
max-height: 220px;
overflow: auto;
}
.icon-cell {
border: 1px solid transparent;
background: #fff;
border-radius: 8px;
height: 56px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
cursor: pointer;
color: #475569;
font-size: 10px;
padding: 4px;
}
.icon-cell:hover { border-color: #a7f3d0; color: var(--primary-dark); }
.icon-cell.active {
border-color: var(--primary);
background: var(--primary-bg);
color: var(--primary-dark);
box-shadow: 0 0 0 2px rgba(16,185,129,.12);
}
.icon-cell svg { width: 20px; height: 20px; }
.color-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.color-dot {
width: 28px;
height: 28px;
border-radius: 8px;
border: 2px solid transparent;
cursor: pointer;
box-shadow: inset 0 0 0 1px rgba(15,23,42,.06);
}
.color-dot.active {
border-color: var(--primary-dark);
box-shadow: 0 0 0 2px rgba(16,185,129,.25);
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.x {
width: 28px;
height: 28px;
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
color: #64748b;
font-size: 18px;
line-height: 1;
}
.x:hover { background: #f1f5f9; color: #0f172a; }
.legend {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 12px;
font-size: 12px;
color: var(--text-muted);
}
.legend span::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
border-radius: 2px;
margin-right: 6px;
vertical-align: middle;
}
.legend .a::before { background: #f97316; }
.legend .b::before { background: #bbf7d0; border: 1px solid #86efac; }
</style>
</head>
<body>
<div class="wrap">
<header class="page-head">
<h1>审批应用图标 · 交互 Demo</h1>
<p>
新增时:<strong>淡色背景随机</strong> + <strong>Element Plus 图标随机</strong>;
图标取底色同色系深色。卡片图标 <strong>32×32</strong>(原 40×40),弹窗预览 <strong>56×56</strong>(原 80×80)。
</p>
</header>
<section class="panel">
<div class="panel-top"><h2>对比:现在 vs 目标</h2></div>
<div class="panel-body">
<div class="compare">
<div class="compare-col">
<span class="badge old">现状</span>
<h3>饱和底色 + 固定三条杠</h3>
<p class="hint">8 张 SVG 只换 fill,中间图形相同</p>
<div class="icon-row" id="oldRow"></div>
</div>
<div class="compare-col">
<span class="badge new">目标</span>
<h3>淡色底 + 随机 / 可选 EP Icon</h3>
<p class="hint">仅这 8 组彩色(禁止灰/slate);图标色 = 同色系深色</p>
<div class="icon-row" id="newRow"></div>
</div>
</div>
<div class="legend">
<span class="a">现状:高饱和实心底</span>
<span class="b">目标:浅色底 + 同色系深色图标</span>
</div>
</div>
</section>
<section class="panel">
<div class="panel-top">
<h2>审批管理 · 流程卡片预览</h2>
<div class="toolbar" style="margin:0">
<button class="btn primary" id="btnAdd" type="button">+ 新增审批</button>
<button class="btn ghost" id="btnRerollAll" type="button">全部重新随机</button>
</div>
</div>
<div class="panel-body">
<p class="hint">卡片图标由「底色色值 + icon 名」组合渲染(非上传图片时)。点「编辑基础」可改图标。</p>
<div class="card-row" id="cardRow"></div>
</div>
</section>
</div>
<div class="dialog-mask" id="dialogMask" role="dialog" aria-modal="true">
<div class="dialog">
<div class="dialog__head">
<span id="dialogTitle">新增审批</span>
<button class="x" type="button" id="btnClose" aria-label="关闭">×</button>
</div>
<div class="dialog__body">
<div class="form-grid">
<div class="span-2">
<label class="form-label">应用图标</label>
<div class="picker">
<div class="preview" id="preview"></div>
<div class="picker-actions">
<button class="btn ghost" type="button" id="btnPickIcon">从图标库选择</button>
<button class="btn ghost" type="button" id="btnRandomColor">换底色</button>
<button class="btn ghost" type="button" id="btnRandomBoth">随机图标+底色</button>
<button class="btn ghost" type="button" disabled title="Demo 未接上传">上传图片</button>
<p class="picker-tip">新增时从上方 8 组彩色中随机底色 + 随机 EP 图标(图标为同色系深色);禁止灰色。可再选图标、换底色,或上传自定义图。</p>
</div>
</div>
<div class="icon-panel" id="iconPanel">
<div class="icon-panel__head">
<strong>Element Plus 图标库(业务精选)</strong>
<button class="btn ghost" type="button" id="btnClosePanel" style="height:24px">收起</button>
</div>
<div class="icon-grid" id="iconGrid"></div>
<div>
<div class="form-label" style="margin-top:10px">底色(仅 8 组彩色,无灰色)</div>
<div class="color-row" id="colorRow"></div>
</div>
</div>
</div>
<div>
<label class="form-label">应用名称</label>
<input type="text" id="appName" placeholder="不超过50字" maxlength="50" />
</div>
<div>
<label class="form-label">所属分组</label>
<select id="group">
<option>人事考勤</option>
<option>财务报销</option>
<option>行政后勤</option>
</select>
</div>
<div class="span-2">
<label class="form-label">说明</label>
<textarea id="desc" placeholder="选填"></textarea>
</div>
</div>
</div>
<div class="dialog__foot">
<button class="btn ghost" type="button" id="btnCancel">取消</button>
<button class="btn primary" type="button" id="btnOk">确定</button>
</div>
</div>
</div>
<script>
/**
* 仅允许 Demo「淡色底 + 随机/可选 EP Icon」区的 8 组彩色
* 禁止灰 / slate 等中性色
*/
const COLOR_PAIRS = [
{ bg: '#d1fae5', fg: '#059669' }, // mint
{ bg: '#cffafe', fg: '#0891b2' }, // cyan
{ bg: '#dbeafe', fg: '#2563eb' }, // blue
{ bg: '#e0e7ff', fg: '#4f46e5' }, // indigo
{ bg: '#ede9fe', fg: '#7c3aed' }, // violet
{ bg: '#fce7f3', fg: '#db2777' }, // pink
{ bg: '#ffedd5', fg: '#ea580c' }, // orange
{ bg: '#fef3c7', fg: '#d97706' } // amber
]
const PASTEL_COLORS = COLOR_PAIRS.map((p) => p.bg)
const FG_BY_BG = Object.fromEntries(COLOR_PAIRS.map((p) => [p.bg.toLowerCase(), p.fg]))
/** 现状高饱和(对比用) */
const OLD_COLORS = ['#f97316', '#10b981', '#3b82f6', '#8b5cf6', '#f43f5e', '#f59e0b', '#06b6d4', '#14b8a6']
/** 精选 EP 风格图标(path 近似 Element Plus outline) */
const ICONS = {
Document: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="16" y2="17"/>',
Calendar: '<rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>',
Clock: '<circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 15 14"/>',
Wallet: '<rect x="2" y="6" width="20" height="14" rx="2"/><path d="M2 10h20"/><circle cx="16" cy="14" r="1.5"/>',
Van: '<path d="M3 17h2a2 2 0 0 0 4 0h6a2 2 0 0 0 4 0h2V9l-4-4H3z"/><circle cx="7" cy="17" r="1.5"/><circle cx="17" cy="17" r="1.5"/>',
User: '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
Stamp: '<path d="M5 21h14"/><path d="M8 17h8l1-4H7z"/><path d="M12 13V8"/><circle cx="12" cy="5" r="2"/>',
Tickets: '<path d="M3 9a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v2a2 2 0 0 0 0 4v2a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2a2 2 0 0 0 0-4z"/><line x1="12" y1="7" x2="12" y2="17"/>',
OfficeBuilding: '<rect x="4" y="3" width="16" height="18" rx="1"/><path d="M9 21V3M15 21V3M4 9h16M4 15h16"/>',
Money: '<circle cx="12" cy="12" r="9"/><path d="M12 7v10M9.5 9.5c.8-1 2.2-1.5 3.5-1 1.6.6 1.8 2.4.3 3.2-1.4.8-2.8 1-3.3 2.3-.5 1.2.4 2.5 2 2.8 1.3.2 2.5-.3 3.2-1.2"/>',
Briefcase: '<rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="3" y1="12" x2="21" y2="12"/>',
Message: '<path d="M21 12a8 8 0 0 1-11.5 7.2L4 20l1-4.2A8 8 0 1 1 21 12z"/>',
Setting: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9c.3.6.9 1 1.6 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/>',
Checked: '<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>',
CollectionTag: '<path d="M20.6 13.4 13.4 20.6a2 2 0 0 1-2.8 0L3 13V4h9l8.6 8.6a2 2 0 0 1 0 2.8z"/><circle cx="7.5" cy="7.5" r="1.5"/>',
Present: '<rect x="3" y="8" width="18" height="13" rx="1"/><path d="M12 8v13M3 12h18"/><path d="M12 8c-2-3-5-3-5-1s3 2 5 1c2 1 5 0 5-1s-3-2-5 1z"/>'
}
const ICON_NAMES = Object.keys(ICONS)
const SAMPLE_APPS = [
{ id: '1', name: '请假申请', group: '人事考勤' },
{ id: '2', name: '差旅报销', group: '财务报销' },
{ id: '3', name: '用车申请', group: '行政后勤' },
{ id: '4', name: '合同用印', group: '行政后勤' },
{ id: '5', name: '加班申请', group: '人事考勤' },
{ id: '6', name: '采购申请', group: '财务报销' }
]
/** 由淡色底得到同色系深色(图标色) */
function iconFgFromBg(bg) {
const key = String(bg || '').toLowerCase()
if (FG_BY_BG[key]) return FG_BY_BG[key]
// 兜底:把浅色压暗为同色系深色
const m = key.match(/^#([0-9a-f]{6})$/i)
if (!m) return '#059669'
const n = parseInt(m[1], 16)
const r = (n >> 16) & 255
const g = (n >> 8) & 255
const b = n & 255
const dark = (c) => Math.max(0, Math.round(c * 0.35))
return '#' + [dark(r), dark(g), dark(b)].map((x) => x.toString(16).padStart(2, '0')).join('')
}
function svgIcon(name, size = 20, stroke = '#059669') {
const body = ICONS[name] || ICONS.Document
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${stroke}" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" style="color:${stroke}">${body}</svg>`
}
function pick(arr) {
return arr[Math.floor(Math.random() * arr.length)]
}
function randomIconState() {
return { icon: pick(ICON_NAMES), color: pick(PASTEL_COLORS) }
}
function renderTile(el, icon, color, soft) {
el.style.background = color
el.classList.toggle('soft', !!soft)
const fg = soft ? iconFgFromBg(color) : '#ffffff'
el.innerHTML = svgIcon(icon, soft ? 16 : 16, fg)
el.style.color = fg
}
// —— 对比区 ——
const oldRow = document.getElementById('oldRow')
const newRow = document.getElementById('newRow')
OLD_COLORS.forEach((c) => {
const d = document.createElement('div')
d.className = 'swatch'
d.style.background = c
// 现状:固定三条杠
d.innerHTML = `<svg viewBox="0 0 24 24" fill="#fff"><rect x="5" y="7" width="14" height="2.2" rx="1"/><rect x="5" y="11" width="14" height="2.2" rx="1"/><rect x="5" y="15" width="14" height="2.2" rx="1"/></svg>`
oldRow.appendChild(d)
})
PASTEL_COLORS.forEach((c, i) => {
const d = document.createElement('div')
d.className = 'swatch soft'
const fg = iconFgFromBg(c)
d.style.background = c
d.style.color = fg
d.innerHTML = svgIcon(ICON_NAMES[i % ICON_NAMES.length], 16, fg)
newRow.appendChild(d)
})
// —— 卡片 ——
let apps = SAMPLE_APPS.map((a) => ({ ...a, ...randomIconState(), status: 'draft' }))
function renderCards() {
const row = document.getElementById('cardRow')
row.innerHTML = ''
apps.forEach((app) => {
const card = document.createElement('div')
card.className = 'oa-card'
card.innerHTML = `
<div class="oa-card__main">
<span class="oa-card__icon" style="background:${app.color};color:${iconFgFromBg(app.color)}">${svgIcon(app.icon, 16, iconFgFromBg(app.color))}</span>
<span>
<span class="oa-card__name">${app.name}</span>
<span class="oa-card__meta">
<span class="tag warn">未发布</span>
<span class="tag">启用</span>
<span class="tag muted">${app.icon}</span>
</span>
</span>
</div>
<div class="oa-card__ops">
<button class="btn primary" type="button" data-act="design">设计</button>
<button class="btn" type="button" data-act="edit">编辑基础</button>
</div>
`
card.querySelector('[data-act="edit"]').onclick = () => openDialog('edit', app)
card.querySelector('[data-act="design"]').onclick = () => alert('Demo:进入设计器(图标会同步展示)')
row.appendChild(card)
})
}
// —— 弹窗状态 ——
let mode = 'add'
let editingId = null
let state = randomIconState()
const mask = document.getElementById('dialogMask')
const preview = document.getElementById('preview')
const iconPanel = document.getElementById('iconPanel')
const iconGrid = document.getElementById('iconGrid')
const colorRow = document.getElementById('colorRow')
function paintPreview() {
const fg = iconFgFromBg(state.color)
preview.style.background = state.color
preview.style.color = fg
preview.innerHTML = svgIcon(state.icon, 26, fg)
// sync active cells
iconGrid.querySelectorAll('.icon-cell').forEach((c) => {
c.classList.toggle('active', c.dataset.icon === state.icon)
})
colorRow.querySelectorAll('.color-dot').forEach((c) => {
c.classList.toggle('active', c.dataset.color === state.color)
})
}
function buildPanels() {
iconGrid.innerHTML = ''
ICON_NAMES.forEach((name) => {
const cell = document.createElement('button')
cell.type = 'button'
cell.className = 'icon-cell'
cell.dataset.icon = name
cell.innerHTML = `${svgIcon(name, 20)}<span>${name}</span>`
cell.onclick = () => {
state.icon = name
paintPreview()
}
iconGrid.appendChild(cell)
})
colorRow.innerHTML = ''
PASTEL_COLORS.forEach((c) => {
const dot = document.createElement('button')
dot.type = 'button'
dot.className = 'color-dot'
dot.dataset.color = c
dot.style.background = c
dot.title = c
dot.onclick = () => {
state.color = c
paintPreview()
}
colorRow.appendChild(dot)
})
}
function openDialog(m, app) {
mode = m
iconPanel.classList.remove('open')
document.getElementById('dialogTitle').textContent = m === 'add' ? '新增审批' : '编辑基础'
if (m === 'add') {
editingId = null
state = randomIconState()
document.getElementById('appName').value = ''
document.getElementById('desc').value = ''
} else {
editingId = app.id
state = { icon: app.icon, color: app.color }
document.getElementById('appName').value = app.name
document.getElementById('desc').value = ''
}
paintPreview()
mask.classList.add('open')
}
function closeDialog() {
mask.classList.remove('open')
}
document.getElementById('btnAdd').onclick = () => openDialog('add')
document.getElementById('btnClose').onclick = closeDialog
document.getElementById('btnCancel').onclick = closeDialog
mask.addEventListener('click', (e) => { if (e.target === mask) closeDialog() })
document.getElementById('btnPickIcon').onclick = () => {
iconPanel.classList.toggle('open')
}
document.getElementById('btnClosePanel').onclick = () => iconPanel.classList.remove('open')
document.getElementById('btnRandomColor').onclick = () => {
state.color = pick(PASTEL_COLORS)
paintPreview()
}
document.getElementById('btnRandomBoth').onclick = () => {
state = randomIconState()
paintPreview()
}
document.getElementById('btnRerollAll').onclick = () => {
apps = apps.map((a) => ({ ...a, ...randomIconState() }))
renderCards()
}
document.getElementById('btnOk').onclick = () => {
const name = document.getElementById('appName').value.trim() || (mode === 'add' ? '未命名审批' : apps.find((a) => a.id === editingId)?.name)
if (mode === 'add') {
apps.unshift({
id: String(Date.now()),
name,
group: document.getElementById('group').value,
...state,
status: 'draft'
})
} else {
const app = apps.find((a) => a.id === editingId)
if (app) {
app.name = name
app.icon = state.icon
app.color = state.color
}
}
renderCards()
closeDialog()
}
buildPanels()
renderCards()
</script>
</body>
</html>