For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 将 mobile-h5「发起审批」列表改为与 Web 同构的分类标题行 + 2 列卡片网格,并解析 ep:Icon|#bg 使 logo 背景色/图标色与 Web 一致。
Architecture: 从 Web oa-app-icons.ts 拷贝色对与解析规则到 mobile-h5/utils/oa-app-icons.js;用内联 SVG data-URI + <image> 渲染 ep 图标(兼容微信小程序);仅改 pages/oa/start/index.vue 模板与样式,保留现有 displayGroups 与 Tab/搜索逻辑。
Tech Stack: uni-app Vue2/3(现有 Options API)· 纯 JS utils · 无新增 npm 依赖
Spec: docs/superpowers/specs/2026-08-11-mobile-oa-start-card-grid-design.md
Demo: mobile-ui/zkxw-mobile-oa-start-cards-demo.html
| File | Responsibility |
|---|---|
mobile-h5/utils/oa-app-icons.js |
色对、parseEpIconUrl / resolveOaAppIcon、SVG data-URI 生成 |
mobile-h5/scripts/check-oa-app-icons.js |
Node 断言脚本(无 vitest 时的校验) |
mobile-h5/components/OaAppIcon.vue |
展示 ep / 图片图标 |
mobile-h5/pages/oa/start/index.vue |
卡片网格 UI(保留 Tab + 搜索) |
oa-app-icons.js 色对与解析Files:
mobile-h5/utils/oa-app-icons.jsCreate: mobile-h5/scripts/check-oa-app-icons.js
[x] Step 1: Write the check script (failing until util exists)
Create mobile-h5/scripts/check-oa-app-icons.js:
const assert = require('assert')
const path = require('path')
let mod
try {
mod = require(path.join(__dirname, '../utils/oa-app-icons.js'))
} catch (e) {
console.error('FAIL: cannot load oa-app-icons.js', e.message)
process.exit(1)
}
const {
OA_APP_ICON_COLOR_PAIRS,
resolveOaAppIcon,
parseEpIconUrl,
buildEpIconUrl,
isEpIconUrl
} = mod
assert.strictEqual(OA_APP_ICON_COLOR_PAIRS.length, 8)
assert.strictEqual(OA_APP_ICON_COLOR_PAIRS[0].bg, '#d1fae5')
assert.strictEqual(OA_APP_ICON_COLOR_PAIRS[0].fg, '#059669')
assert.strictEqual(OA_APP_ICON_COLOR_PAIRS[2].bg, '#dbeafe')
assert.strictEqual(OA_APP_ICON_COLOR_PAIRS[2].fg, '#2563eb')
assert.strictEqual(isEpIconUrl('ep:Calendar|#dbeafe'), true)
assert.strictEqual(isEpIconUrl('https://x/a.png'), false)
const p = parseEpIconUrl('ep:Calendar|#dbeafe')
assert.strictEqual(p.icon, 'Calendar')
assert.strictEqual(p.bg, '#dbeafe')
assert.strictEqual(p.fg, '#2563eb')
const bad = parseEpIconUrl('ep:NoSuch|#zzzzzz')
assert.strictEqual(bad.icon, 'Document')
assert.strictEqual(bad.bg, '#d1fae5')
assert.strictEqual(bad.fg, '#059669')
const empty = resolveOaAppIcon('')
assert.strictEqual(empty.kind, 'ep')
assert.strictEqual(empty.icon, 'Document')
assert.strictEqual(empty.bg, '#d1fae5')
const img = resolveOaAppIcon('https://cdn.example/a.png')
assert.strictEqual(img.kind, 'image')
assert.strictEqual(img.src, 'https://cdn.example/a.png')
const built = buildEpIconUrl('Wallet', '#fef3c7')
assert.strictEqual(built, 'ep:Wallet|#fef3c7')
const r = resolveOaAppIcon(built)
assert.strictEqual(r.kind, 'ep')
assert.strictEqual(r.fg, '#d97706')
console.log('ok: oa-app-icons checks passed')
Run: node mobile-h5/scripts/check-oa-app-icons.js
Expected: exit 1, cannot load module
mobile-h5/utils/oa-app-icons.jsCopy color pairs and parse/resolve logic from web-admin/src/utils/oa-app-icons.ts into CommonJS-friendly ESM export (uni-app uses ES modules via bundler). Use:
/** OA 审批应用图标:与 web-admin/src/utils/oa-app-icons.ts 色对/解析对齐 */
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
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) }
}
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 }
}
if (/^https?:\/\//i.test(u) || u.startsWith('/')) {
return { kind: 'image', src: u }
}
return { kind: 'image', src: u }
}
/** Stroke path snippets (viewBox 0 0 24 24). Unknown → Document */
const EP_SVG_PATHS = {
Document:
'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6 M8 13h8 M8 17h8',
Calendar:
'M3 4h18v18H3z M16 2v4 M8 2v4 M3 10h18',
Clock: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z M12 7v5l3 2',
Wallet: 'M2 7h20v12H2z M16 12h4 M2 7l2-3h12l2 3',
User: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2 M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
Tickets: 'M3 7h18v10H3z M3 11h18',
Briefcase: 'M2 7h20v14H2z M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2 M2 12h20',
Money: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z M12 7v10',
Van: 'M3 17h1a3 3 0 0 0 6 0h4a3 3 0 0 0 6 0h1 M3 17V8a2 2 0 0 1 2-2h8l6 4v7',
OfficeBuilding: 'M4 21V5a1 1 0 0 1 1-1h9v17 M14 9h6v12 M8 9h2 M8 13h2 M8 17h2',
Message: 'M21 11.5a8.5 8.5 0 0 1-12.4 7.5L3 21l1.9-5.1A8.5 8.5 0 1 1 21 11.5z',
Setting:
'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z 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.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z',
CollectionTag: 'M20.6 13.4l-7.2 7.2a2 2 0 0 1-2.8 0L3 13V3h10l7.6 7.6a2 2 0 0 1 0 2.8z M7.5 7.5h.01',
Checked: 'M22 11.1V12a10 10 0 1 1-5.9-9.1 M22 4L12 14.01l-3-3',
Present: 'M20 12v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8 M12 22V8 M2 8h20v4H2z M12 8H7.5a2.5 2.5 0 0 1 0-5C11 3 12 8 12 8z M12 8h4.5a2.5 2.5 0 0 0 0-5C13 3 12 8 12 8z',
Stamp: 'M5 21h14 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 M6 15h12v3H6z',
CreditCard: 'M2 7h20v12H2z M2 11h20',
Notebook: 'M4 4h12a2 2 0 0 1 2 2v14H6a2 2 0 0 0-2 2V4z M8 8h6 M8 12h6',
Bell: 'M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9 M13.7 21a2 2 0 0 1-3.4 0',
Location: 'M12 21s7-4.5 7-11a7 7 0 1 0-14 0c0 6.5 7 11 7 11z M12 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4z'
}
/**
* Build data-URI SVG for mini-program <image>.
* Paths above are space-separated path `d` hints — implement as multiple <path> or single polyline set.
* Prefer a compact implementation: for each icon, a full SVG string template with stroke=fg.
*/
export function epIconDataUri(icon, fg) {
const name = ICON_SET.has(icon) ? icon : OA_APP_DEFAULT_EP_ICON
const color = encodeURIComponent(fg || OA_APP_DEFAULT_FG)
// Minimal reliable glyphs (full path d attributes)
const dMap = {
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"/><path d="M2 7l2-3h12l2 3"/>',
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"/>',
Tickets:
'<rect x="3" y="7" width="18" height="10" rx="2"/><line x1="3" y1="11" x2="21" y2="11"/>',
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"/>',
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"/>',
OfficeBuilding:
'<path d="M4 21V5a1 1 0 0 1 1-1h9v17"/><path d="M14 9h6v12"/><line x1="8" y1="9" x2="10" y2="9"/><line x1="8" y1="13" x2="10" y2="13"/><line x1="8" y1="17" x2="10" y2="17"/>',
Message:
'<path d="M21 11.5a8.5 8.5 0 0 1-12.4 7.5L3 21l1.9-5.1A8.5 8.5 0 1 1 21 11.5z"/>',
Setting:
'<circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/>',
CollectionTag:
'<path d="M20.6 13.4l-7.2 7.2a2 2 0 0 1-2.8 0L3 13V3h10l7.6 7.6a2 2 0 0 1 0 2.8z"/><circle cx="7.5" cy="7.5" r="1"/>',
Checked:
'<path d="M22 11.1V12a10 10 0 1 1-5.9-9.1"/><polyline points="22 4 12 14.01 9 11"/>',
Present:
'<polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/>',
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"/>',
CreditCard:
'<rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/>',
Notebook:
'<path d="M4 4h12a2 2 0 0 1 2 2v14H6a2 2 0 0 0-2 2V4z"/><line x1="8" y1="8" x2="14" y2="8"/><line x1="8" y1="12" x2="14" y2="12"/>',
Bell:
'<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/>',
Location:
'<path d="M12 21s7-4.5 7-11a7 7 0 1 0-14 0c0 6.5 7 11 7 11z"/><circle cx="12" cy="10" r="2"/>'
}
const inner = dMap[name] || dMap.Document
const svg =
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${fg}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg)
}
// Also export for Node check script (optional CommonJS bridge at bottom only if needed)
Note for implementer: Uni-app may load ESM; the check script uses require. Prefer dual export OR change the check script to dynamic import():
// at top of check-oa-app-icons.js — use async import instead of require
;(async () => {
const mod = await import('../utils/oa-app-icons.js')
// ...asserts...
})().catch((e) => { console.error(e); process.exit(1) })
Also assert epIconDataUri('Calendar', '#2563eb').startsWith('data:image/svg+xml').
Remove unused EP_SVG_PATHS if you only ship dMap — keep one map only (YAGNI).
Run: node mobile-h5/scripts/check-oa-app-icons.js
Expected: ok: oa-app-icons checks passed
git add mobile-h5/utils/oa-app-icons.js mobile-h5/scripts/check-oa-app-icons.js
git commit -m "feat(mobile-h5): add OA app icon parse and ep SVG data-URI helpers"
OaAppIcon.vue 组件Files:
Create: mobile-h5/components/OaAppIcon.vue
[ ] Step 1: Create component
<template>
<view class="oa-app-icon" :class="'oa-app-icon--' + size" :style="boxStyle">
<image
v-if="resolved.kind === 'ep'"
class="oa-app-icon__img"
:src="epSrc"
mode="aspectFit"
/>
<image
v-else
class="oa-app-icon__img"
:src="resolved.src"
mode="aspectFill"
/>
</view>
</template>
<script>
import { resolveOaAppIcon, epIconDataUri } from '../utils/oa-app-icons.js'
export default {
name: 'OaAppIcon',
props: {
iconUrl: { type: String, default: '' },
size: { type: String, default: 'sm' } // sm=32, md=56
},
computed: {
resolved() {
return resolveOaAppIcon(this.iconUrl)
},
boxStyle() {
if (this.resolved.kind === 'ep') {
return { background: this.resolved.bg }
}
return {}
},
epSrc() {
if (this.resolved.kind !== 'ep') return ''
return epIconDataUri(this.resolved.icon, this.resolved.fg)
}
}
}
</script>
<style scoped>
.oa-app-icon {
flex-shrink: 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.oa-app-icon--sm {
width: 32px;
height: 32px;
border-radius: 8px;
}
.oa-app-icon--md {
width: 56px;
height: 56px;
border-radius: 12px;
}
.oa-app-icon__img {
width: 50%;
height: 50%;
}
.oa-app-icon--sm .oa-app-icon__img {
width: 16px;
height: 16px;
}
.oa-app-icon--md .oa-app-icon__img {
width: 26px;
height: 26px;
}
/* image kind fills the box */
.oa-app-icon--image-fill .oa-app-icon__img,
.oa-app-icon:not([style*='background']) .oa-app-icon__img {
/* for image kind use full size — handle via class */
}
</style>
Fix for image kind: when kind === 'image', set img to 100%×100%:
<image
v-if="resolved.kind === 'ep'"
class="oa-app-icon__glyph"
:src="epSrc"
mode="aspectFit"
/>
<image
v-else
class="oa-app-icon__photo"
:src="resolved.src"
mode="aspectFill"
/>
.oa-app-icon__glyph { width: 16px; height: 16px; }
.oa-app-icon--md .oa-app-icon__glyph { width: 26px; height: 26px; }
.oa-app-icon__photo { width: 100%; height: 100%; }
git add mobile-h5/components/OaAppIcon.vue
git commit -m "feat(mobile-h5): add OaAppIcon component for ep and image logos"
pages/oa/start/index.vue 为卡片网格Files:
Modify: mobile-h5/pages/oa/start/index.vue
[ ] Step 1: Replace list-row cards with section-head + 2-col grid
Keep script (displayGroups, Tab, search, load, onPick) unchanged. Replace the v-else class="body" template block and styles to match Demo / Web:
Template body (keep search + tabs + hint above):
<view v-if="loading" class="state">加载中…</view>
<view v-else-if="!displayGroups.length" class="state">
暂无可发起的审批模板
</view>
<view v-else class="body">
<view v-for="sec in displayGroups" :key="sec.id" class="section">
<view class="section-hd">
<text class="section-name">{{ sec.name }}</text>
<view class="section-dot" />
<text class="section-meta">{{ sec.apps.length }} 个表单</text>
</view>
<view class="card-row">
<view
v-for="row in sec.apps"
:key="row.id"
class="oa-app-card"
@click="onPick(row)"
>
<view class="oa-app-card__main">
<OaAppIcon :icon-url="row.iconUrl" size="sm" />
<view class="oa-app-card__body">
<text class="oa-app-card__name">{{ row.appName }}</text>
<text class="oa-app-card__desc">{{
row.description || '点击发起审批'
}}</text>
</view>
</view>
<view class="oa-app-card__ops">
<view class="oa-app-card__btn" @click.stop="onPick(row)">发起</view>
</view>
</view>
</view>
</view>
</view>
Register component in script:
import OaAppIcon from '../../../components/OaAppIcon.vue'
// in export default:
components: { OaAppIcon },
Replace card-related styles with (rpx for spacing; px for 32px icon box already in component):
.section { margin-bottom: 28rpx; }
.section-hd {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 16rpx;
min-height: 40rpx;
}
.section-name {
font-size: 28rpx;
font-weight: 600;
color: #334155;
}
.section-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #f87171;
flex-shrink: 0;
}
.section-meta {
margin-left: auto;
font-size: 22rpx;
color: #94a3b8;
}
.card-row {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.oa-app-card {
width: calc((100% - 16rpx) / 2);
display: flex;
flex-direction: column;
gap: 16rpx;
min-height: 180rpx;
padding: 20rpx 20rpx 14rpx;
border-radius: 20rpx;
background: #fff;
border: 1px solid #d8e3de;
box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.06);
box-sizing: border-box;
}
.oa-app-card__main {
display: flex;
gap: 16rpx;
align-items: flex-start;
min-width: 0;
}
.oa-app-card__body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4rpx;
}
.oa-app-card__name {
font-size: 26rpx;
font-weight: 600;
color: #0f172a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.oa-app-card__desc {
font-size: 22rpx;
color: #94a3b8;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.oa-app-card__ops {
display: flex;
justify-content: flex-end;
padding-top: 12rpx;
border-top: 1px solid #f1f5f9;
}
.oa-app-card__btn {
height: 44rpx;
line-height: 44rpx;
padding: 0 16rpx;
border-radius: 8rpx;
background: #0a9f6d;
color: #fff;
font-size: 22rpx;
font-weight: 600;
}
Remove obsolete .card / .card-main / .card-icon* / .card-btn list-row styles.
#/pages/oa/start/indexiconUrl 的模板:背景色/图标色一致git add mobile-h5/pages/oa/start/index.vue
git commit -m "feat(mobile-h5): show OA start templates as grouped card grid"
Files: none (verify only)
mobile-ui/zkxw-mobile-oa-start-cards-demo.html 与小程序页,确认:搜索 → Tab → hint → section-head → 2 列卡片。node mobile-h5/scripts/check-oa-app-icons.js,确认仍 PASS。data:image/svg+xml;base64, + btoa(unescape(encodeURIComponent(svg)))(小程序对 encodeURIComponent 形式偶发不兼容)。验证通过后单独 commit:git add mobile-h5/utils/oa-app-icons.js
git commit -m "fix(mobile-h5): use base64 SVG data-URI for WeChat image icons"
| Spec requirement | Task |
|---|---|
| 2 列卡片网格 | Task 3 |
| 分类标题独立一行 | Task 3 |
| 保留搜索下 Tab | Task 3(不删 tabs) |
| Logo 色对对齐 Web | Task 1 |
ep: + 图片 URL |
Task 1–2 |
| 三墙主按钮色 | Task 3 oa-app-card__btn |
| 不改 webview/API | 无任务触碰 |
| Demo 对照 | Task 4 |
No TBD placeholders. Types/names: resolveOaAppIcon / epIconDataUri / OaAppIcon consistent across tasks.