Files
zkxw/mobile-ui/zkxw-mobile-oa-start-cards-demo.html
T
zlwuandCursor fdaeb6840f docs: add mobile OA start card-grid design and HTML demo
Align the mini-program start list with web card sections and shared ep icon color pairs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 14:20:04 +08:00

533 lines
17 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, maximum-scale=1" />
<title>发起审批 · 移动端卡片 Demo</title>
<style>
:root {
/* 三墙页面骨架 */
--brand: #0a9f6d;
--brand-dark: #087f57;
--brand-bg: #e9f9f2;
--page-bg: #f5f8f7;
--text: #1f2a26;
--text-muted: #6b7b74;
--border: #e2e8e6;
--card-border: #d8e3de;
--danger-dot: #f87171;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
background: linear-gradient(160deg, #e8f5ef 0%, #f0f4f8 48%, #eef2ff 100%);
color: var(--text);
}
.stage {
max-width: 960px;
margin: 0 auto;
padding: 28px 16px 48px;
}
.stage-hd {
margin-bottom: 20px;
}
.stage-hd h1 {
margin: 0 0 6px;
font-size: 22px;
font-weight: 700;
}
.stage-hd p {
margin: 0;
color: #64748b;
font-size: 13px;
line-height: 1.5;
}
.phones {
display: flex;
flex-wrap: wrap;
gap: 28px;
justify-content: center;
}
.phone {
width: 375px;
background: #fff;
border-radius: 28px;
box-shadow:
0 0 0 1px rgb(15 23 42 / 6%),
0 18px 40px rgb(15 23 42 / 12%);
overflow: hidden;
}
.phone-bar {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #0a9f6d, #087f57);
color: #fff;
font-size: 16px;
font-weight: 600;
}
.page {
background: var(--page-bg);
min-height: 640px;
padding-bottom: 24px;
}
.search-wrap {
padding: 12px 14px 8px;
background: #fff;
}
.search {
height: 36px;
padding: 0 14px;
border: 0;
border-radius: 999px;
background: var(--page-bg);
width: 100%;
font-size: 14px;
color: var(--text);
outline: none;
}
.search::placeholder { color: #94a3b8; }
.tabs {
display: flex;
gap: 8px;
padding: 8px 14px 10px;
background: #fff;
overflow-x: auto;
white-space: nowrap;
}
.tab {
flex-shrink: 0;
padding: 6px 14px;
border-radius: 999px;
background: var(--page-bg);
color: var(--text-muted);
font-size: 13px;
border: 0;
cursor: pointer;
}
.tab.active {
color: #fff;
background: var(--brand);
}
.hint {
padding: 10px 14px 4px;
color: var(--text-muted);
font-size: 12px;
}
.body { padding: 8px 12px 0; }
/* 分类标题独立一行(对齐 web section-head) */
.section { margin-bottom: 18px; }
.section-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
min-height: 22px;
}
.section-head h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #334155;
}
.section-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--danger-dot);
flex-shrink: 0;
}
.section-meta {
margin-left: auto;
color: #94a3b8;
font-size: 12px;
}
/* 卡片网格:每行 2 张,对齐 web card-row */
.card-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.oa-app-card {
display: flex;
flex-direction: column;
gap: 10px;
min-height: 108px;
padding: 12px 12px 8px;
background: #fff;
border: 1px solid var(--card-border);
border-radius: 14px;
box-shadow: 0 2px 8px rgb(15 23 42 / 6%);
cursor: pointer;
transition: border-color .15s, box-shadow .15s, transform .15s;
}
.oa-app-card:active {
border-color: #86efac;
box-shadow: 0 4px 12px rgb(10 159 109 / 14%);
transform: translateY(-1px);
}
.oa-app-card__main {
display: flex;
gap: 10px;
align-items: flex-start;
min-width: 0;
}
/* Logo:与 web OaAppIcon sm 一致 32×32 / 圆角 8 */
.oa-app-icon {
width: 32px;
height: 32px;
border-radius: 8px;
display: grid;
place-items: center;
flex-shrink: 0;
overflow: hidden;
}
.oa-app-icon svg {
width: 16px;
height: 16px;
display: block;
}
.oa-app-icon img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.oa-app-card__body {
flex: 1;
min-width: 0;
}
.oa-app-card__name {
display: block;
overflow: hidden;
color: #0f172a;
font-size: 13px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.oa-app-card__desc {
display: block;
margin-top: 3px;
overflow: hidden;
color: #94a3b8;
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.oa-app-card__ops {
display: flex;
justify-content: flex-end;
padding-top: 8px;
border-top: 1px solid #f1f5f9;
}
.oa-app-card__btn {
height: 24px;
padding: 0 10px;
border: 0;
border-radius: 5px;
background: var(--brand);
color: #fff;
font-size: 11px;
font-weight: 600;
cursor: pointer;
}
.note {
margin-top: 24px;
padding: 14px 16px;
background: #fff;
border-radius: 12px;
border: 1px solid #e2e8f0;
font-size: 13px;
color: #475569;
line-height: 1.6;
}
.note code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
background: #f1f5f9;
padding: 1px 6px;
border-radius: 4px;
}
.swatches {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.swatch {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: #64748b;
}
.swatch i {
width: 28px;
height: 28px;
border-radius: 8px;
display: grid;
place-items: center;
}
</style>
</head>
<body>
<div class="stage">
<div class="stage-hd">
<h1>发起审批 · 移动端卡片模式 Demo</h1>
<p>
对齐 web-admin「发起审批」:分类标题独立一行 + 卡片网格(2 列)。
Logo 使用与 web 相同的 <code>ep:Icon|#bg</code> 色对(背景 / 图标色)。
</p>
</div>
<div class="phones">
<div class="phone">
<div class="phone-bar">发起审批</div>
<div class="page">
<div class="search-wrap">
<input class="search" placeholder="请输入审批单名称" />
</div>
<div class="tabs">
<button class="tab active" type="button">全部</button>
<button class="tab" type="button">人事行政</button>
<button class="tab" type="button">财务报销</button>
<button class="tab" type="button">业务审批</button>
</div>
<div class="hint">选择一个审批单即可发起</div>
<div class="body">
<!-- 分类 1:标题独立一行 -->
<section class="section">
<div class="section-head">
<h3>人事行政</h3>
<span class="section-dot" aria-hidden="true"></span>
<span class="section-meta">3 个表单</span>
</div>
<div class="card-row">
<article class="oa-app-card" data-icon="ep:Calendar|#dbeafe">
<div class="oa-app-card__main">
<span class="oa-app-icon" data-ep></span>
<span class="oa-app-card__body">
<span class="oa-app-card__name">请假申请</span>
<span class="oa-app-card__desc">年假 / 事假 / 病假</span>
</span>
</div>
<div class="oa-app-card__ops">
<button class="oa-app-card__btn" type="button">发起</button>
</div>
</article>
<article class="oa-app-card" data-icon="ep:Clock|#cffafe">
<div class="oa-app-card__main">
<span class="oa-app-icon" data-ep></span>
<span class="oa-app-card__body">
<span class="oa-app-card__name">加班申请</span>
<span class="oa-app-card__desc">工作日加班报备</span>
</span>
</div>
<div class="oa-app-card__ops">
<button class="oa-app-card__btn" type="button">发起</button>
</div>
</article>
<article class="oa-app-card" data-icon="ep:Van|#ffedd5">
<div class="oa-app-card__main">
<span class="oa-app-icon" data-ep></span>
<span class="oa-app-card__body">
<span class="oa-app-card__name">出差申请</span>
<span class="oa-app-card__desc">行程与预算预审</span>
</span>
</div>
<div class="oa-app-card__ops">
<button class="oa-app-card__btn" type="button">发起</button>
</div>
</article>
</div>
</section>
<!-- 分类 2 -->
<section class="section">
<div class="section-head">
<h3>财务报销</h3>
<span class="section-dot" aria-hidden="true"></span>
<span class="section-meta">2 个表单</span>
</div>
<div class="card-row">
<article class="oa-app-card" data-icon="ep:Wallet|#d1fae5">
<div class="oa-app-card__main">
<span class="oa-app-icon" data-ep></span>
<span class="oa-app-card__body">
<span class="oa-app-card__name">费用报销</span>
<span class="oa-app-card__desc">发票与明细上传</span>
</span>
</div>
<div class="oa-app-card__ops">
<button class="oa-app-card__btn" type="button">发起</button>
</div>
</article>
<article class="oa-app-card" data-icon="ep:Money|#fef3c7">
<div class="oa-app-card__main">
<span class="oa-app-icon" data-ep></span>
<span class="oa-app-card__body">
<span class="oa-app-card__name">借款申请</span>
<span class="oa-app-card__desc">对公 / 个人借款</span>
</span>
</div>
<div class="oa-app-card__ops">
<button class="oa-app-card__btn" type="button">发起</button>
</div>
</article>
</div>
</section>
<!-- 分类 3 -->
<section class="section">
<div class="section-head">
<h3>业务审批</h3>
<span class="section-dot" aria-hidden="true"></span>
<span class="section-meta">3 个表单</span>
</div>
<div class="card-row">
<article class="oa-app-card" data-icon="ep:Document|#ede9fe">
<div class="oa-app-card__main">
<span class="oa-app-icon" data-ep></span>
<span class="oa-app-card__body">
<span class="oa-app-card__name">合同审批</span>
<span class="oa-app-card__desc">采购与销售合同</span>
</span>
</div>
<div class="oa-app-card__ops">
<button class="oa-app-card__btn" type="button">发起</button>
</div>
</article>
<article class="oa-app-card" data-icon="ep:Stamp|#e0e7ff">
<div class="oa-app-card__main">
<span class="oa-app-icon" data-ep></span>
<span class="oa-app-card__body">
<span class="oa-app-card__name">用印申请</span>
<span class="oa-app-card__desc">公章 / 合同章</span>
</span>
</div>
<div class="oa-app-card__ops">
<button class="oa-app-card__btn" type="button">发起</button>
</div>
</article>
<article class="oa-app-card" data-icon="ep:Briefcase|#fce7f3">
<div class="oa-app-card__main">
<span class="oa-app-icon" data-ep></span>
<span class="oa-app-card__body">
<span class="oa-app-card__name">立项申请</span>
<span class="oa-app-card__desc">项目立项与预算</span>
</span>
</div>
<div class="oa-app-card__ops">
<button class="oa-app-card__btn" type="button">发起</button>
</div>
</article>
</div>
</section>
</div>
</div>
</div>
</div>
<div class="note">
<strong>Logo 色对(与 web <code>OA_APP_ICON_COLOR_PAIRS</code> 一致)</strong>
<div class="swatches" id="swatches"></div>
<p style="margin:12px 0 0">
解析规则:<code>ep:Calendar|#dbeafe</code> → 背景 <code>#dbeafe</code>,图标色由 bg 映射(如 <code>#2563eb</code>)。
自定义上传图则整图铺满 32×32 圆角方块。页面主按钮仍用三墙 <code>#0a9f6d</code>。
</p>
</div>
</div>
<script>
/** 与 web-admin/src/utils/oa-app-icons.ts 锁定一致 */
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' }
]
const FG_BY_BG = Object.fromEntries(
OA_APP_ICON_COLOR_PAIRS.map((p) => [p.bg.toLowerCase(), p.fg])
)
const DEFAULT_BG = OA_APP_ICON_COLOR_PAIRS[0].bg
const DEFAULT_FG = OA_APP_ICON_COLOR_PAIRS[0].fg
/** 简化 SVG,语义对齐 Element Plus 同名图标 */
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: '<path d="M20 7H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/><path d="M16 12h4"/><circle cx="16" cy="12" r="1"/><path d="M2 7l2-3h12l2 3"/>',
Money: '<circle cx="12" cy="12" r="9"/><path d="M12 7v10"/><path d="M15 9.5c0-1.1-1.3-2-3-2s-3 .9-3 2 1.3 2 3 2 3 .9 3 2-1.3 2-3 2-3-.9-3-2"/>',
Van: '<path d="M3 17h1a3 3 0 0 0 6 0h4a3 3 0 0 0 6 0h1"/><path d="M3 17V8a2 2 0 0 1 2-2h8l6 4v7"/><circle cx="7.5" cy="17" r="1.5"/><circle cx="16.5" cy="17" r="1.5"/>',
Stamp: '<path d="M5 21h14"/><path d="M12 3a4 4 0 0 0-4 4c0 2 1.5 3 4 5 2.5-2 4-3 4-5a4 4 0 0 0-4-4z"/><path d="M6 15h12v3H6z"/>',
Briefcase: '<rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/><line x1="2" y1="12" x2="22" y2="12"/>'
}
function parseEp(iconUrl) {
const raw = String(iconUrl || '').trim()
if (!raw.startsWith('ep:')) {
return { icon: 'Document', bg: DEFAULT_BG, fg: DEFAULT_FG }
}
const body = raw.slice(3)
const pipe = body.indexOf('|')
if (pipe < 0) return { icon: 'Document', bg: DEFAULT_BG, fg: DEFAULT_FG }
let icon = body.slice(0, pipe).trim()
let bg = body.slice(pipe + 1).trim().toLowerCase()
if (!ICONS[icon]) icon = 'Document'
if (!FG_BY_BG[bg]) bg = DEFAULT_BG
return { icon, bg, fg: FG_BY_BG[bg] || DEFAULT_FG }
}
function svgFor(name, fg) {
const paths = ICONS[name] || ICONS.Document
return `<svg viewBox="0 0 24 24" fill="none" stroke="${fg}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`
}
document.querySelectorAll('.oa-app-card[data-icon]').forEach((card) => {
const resolved = parseEp(card.getAttribute('data-icon'))
const el = card.querySelector('.oa-app-icon[data-ep]')
if (!el) return
el.style.background = resolved.bg
el.innerHTML = svgFor(resolved.icon, resolved.fg)
})
const sw = document.getElementById('swatches')
OA_APP_ICON_COLOR_PAIRS.forEach((p) => {
const node = document.createElement('span')
node.className = 'swatch'
node.innerHTML = `<i style="background:${p.bg};color:${p.fg}">${svgFor('Document', p.fg)}</i><span>${p.bg} / ${p.fg}</span>`
sw.appendChild(node)
})
</script>
</body>
</html>