Files
zkxw/docs/oa-start-loading-demo.html
2026-08-12 10:02:31 +08:00

515 lines
16 KiB
HTML
Raw Permalink 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" />
<title>发起审批 · 加载态 Demo</title>
<style>
:root {
--brand: #16a34a;
--brand-dark: #15803d;
--brand-soft: #f0fdf4;
--bg: #f5f8f7;
--surface: #fff;
--ink: #1f2a26;
--muted: #6b7b74;
--border: #dcebe4;
--slate: #334155;
--slate-soft: #94a3b8;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", system-ui, sans-serif;
color: var(--ink);
background:
radial-gradient(900px 480px at 0% -10%, #dcfce7 0%, transparent 50%),
radial-gradient(700px 400px at 100% 0%, #e8f5ee 0%, transparent 45%),
var(--bg);
line-height: 1.45;
}
.wrap { max-width: 1200px; margin: 0 auto; padding: 24px 18px 80px; }
h1 { margin: 0 0 8px; font-size: 22px; letter-spacing: -0.02em; }
.sub { margin: 0; color: var(--muted); font-size: 14px; max-width: 720px; }
.note {
margin: 14px 0 22px;
padding: 10px 12px;
border-radius: 10px;
background: #fff;
border: 1px solid #e2e8e5;
font-size: 13px;
color: var(--muted);
}
.note strong { color: var(--brand-dark); font-weight: 650; }
.phones {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 18px;
}
.phone-card {
background: #fff;
border: 1px solid #e2e8e5;
border-radius: 16px;
padding: 14px;
cursor: pointer;
transition: border-color .15s, box-shadow .15s, transform .15s;
}
.phone-card:hover {
transform: translateY(-2px);
box-shadow: 0 12px 28px rgba(20, 40, 30, .08);
}
.phone-card.is-selected {
border-color: var(--brand);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 28%, transparent);
}
.phone-card.is-rec {
outline: 2px dashed color-mix(in srgb, var(--brand) 45%, transparent);
outline-offset: 2px;
}
.badge-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.letter {
width: 28px; height: 28px;
border-radius: 8px;
background: var(--brand-soft);
color: var(--brand-dark);
display: grid; place-items: center;
font-weight: 700; font-size: 13px;
}
.badge {
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
background: #ecfdf5;
color: #15803d;
border: 1px solid #a7f3d0;
font-weight: 600;
}
.badge.muted {
background: #f1f5f9;
color: #64748b;
border-color: #e2e8f0;
}
.title { margin: 0 0 4px; font-size: 15px; font-weight: 700; }
.desc { margin: 0 0 12px; font-size: 12px; color: var(--muted); min-height: 36px; }
/* phone chrome */
.phone {
width: 100%;
max-width: 320px;
margin: 0 auto;
border-radius: 28px;
border: 1px solid #cfd8d3;
background: #111;
padding: 10px;
box-shadow: 0 16px 40px rgba(15, 23, 42, .12);
}
.screen {
border-radius: 20px;
overflow: hidden;
background: var(--bg);
min-height: 520px;
display: flex;
flex-direction: column;
}
.nav {
height: 44px;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #eef2f0;
font-size: 15px;
font-weight: 650;
color: var(--ink);
position: relative;
}
.nav::before {
content: "‹";
position: absolute;
left: 14px;
font-size: 22px;
color: var(--ink);
line-height: 1;
}
.search-wrap {
padding: 10px 12px 4px;
background: #fff;
}
.search {
height: 36px;
border-radius: 8px;
background: var(--bg);
color: #94a3b8;
font-size: 13px;
display: flex;
align-items: center;
padding: 0 12px;
}
.tabs {
display: flex;
gap: 8px;
padding: 8px 12px;
background: #fff;
overflow: hidden;
}
.tab {
flex-shrink: 0;
padding: 6px 12px;
border-radius: 999px;
background: var(--bg);
color: var(--muted);
font-size: 12px;
}
.tab.on {
background: var(--brand);
color: #fff;
}
.hint {
padding: 8px 12px;
color: var(--muted);
font-size: 11px;
background: var(--bg);
}
.body-area {
flex: 1;
background: var(--bg);
padding: 8px 12px 16px;
position: relative;
}
/* A: current plain text */
.state-plain {
padding: 48px 12px;
text-align: center;
color: var(--muted);
font-size: 14px;
}
/* B: brand spinner */
.state-spin {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
padding: 56px 12px 40px;
}
.spin-ring {
width: 40px;
height: 40px;
border-radius: 50%;
border: 3px solid #dcfce7;
border-top-color: var(--brand);
animation: spin 0.85s linear infinite;
}
.spin-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 28px 32px;
border-radius: 16px;
background: #fff;
border: 1px solid var(--border);
box-shadow: 0 8px 24px rgba(21, 128, 61, 0.08);
}
.spin-text {
font-size: 13px;
color: var(--muted);
font-weight: 500;
}
.spin-sub {
font-size: 11px;
color: var(--slate-soft);
margin-top: -6px;
}
/* C: skeleton cards */
.sec-hd {
display: flex;
align-items: center;
gap: 6px;
margin: 4px 0 10px;
}
.sk {
background: linear-gradient(90deg, #e8eeeB 25%, #f4f7f5 50%, #e8eeeB 75%);
background-size: 200% 100%;
animation: shimmer 1.2s ease-in-out infinite;
border-radius: 6px;
}
.sk-title { width: 72px; height: 14px; }
.sk-meta { width: 48px; height: 12px; margin-left: auto; }
.sk-dot {
width: 6px; height: 6px; border-radius: 50%;
background: #fca5a5;
flex-shrink: 0;
opacity: .7;
}
.card-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 16px;
}
.sk-card {
min-height: 96px;
padding: 10px;
border-radius: 12px;
background: #fff;
border: 1px solid #d8e3de;
display: flex;
flex-direction: column;
gap: 10px;
}
.sk-main { display: flex; gap: 10px; }
.sk-icon {
width: 36px; height: 36px;
border-radius: 10px;
flex-shrink: 0;
}
.sk-lines { flex: 1; display: flex; flex-direction: column; gap: 6px; padding-top: 2px; }
.sk-name { width: 78%; height: 12px; }
.sk-desc { width: 55%; height: 10px; }
.sk-ops {
border-top: 1px solid #f1f5f9;
padding-top: 8px;
display: flex;
justify-content: flex-end;
}
.sk-btn { width: 40px; height: 22px; border-radius: 6px; }
/* D: soft pulse dots */
.state-pulse {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
padding: 64px 12px;
}
.pulse-dots {
display: flex;
gap: 8px;
align-items: center;
}
.pulse-dots span {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--brand);
animation: bounce 1.2s ease-in-out infinite;
}
.pulse-dots span:nth-child(2) { animation-delay: .15s; opacity: .75; }
.pulse-dots span:nth-child(3) { animation-delay: .3s; opacity: .55; }
.pulse-label {
font-size: 13px;
color: var(--muted);
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
@keyframes bounce {
0%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-6px); }
}
.pick-hint {
margin-top: 22px;
font-size: 13px;
color: var(--muted);
text-align: center;
}
</style>
</head>
<body>
<div class="wrap">
<h1>发起审批 · 加载态方案</h1>
<p class="sub">
当前页打开时只有一行「加载中…」,观感偏空。下面四种方案都保留搜索栏与分组 Tab(已渲染的壳),仅替换列表区域的加载表现。色值对齐 mobile-h5 Token(品牌绿 <code>#16a34a</code>)。
</p>
<p class="note">
点击卡片可选中;推荐 <strong>C · 骨架屏</strong>(布局不跳动、等待时也有结构感)。选好后在对话里告诉我字母即可。
</p>
<div class="phones">
<!-- 0 current -->
<div class="phone-card" data-choice="0" onclick="pick(this)">
<div class="badge-row">
<div class="letter">0</div>
<span class="badge muted">现状</span>
</div>
<h3 class="title">纯文字</h3>
<p class="desc">仅居中「加载中…」,无动效、无结构提示。</p>
<div class="phone"><div class="screen">
<div class="nav">发起审批</div>
<div class="search-wrap"><div class="search">请输入审批单名称</div></div>
<div class="tabs">
<div class="tab on">全部</div>
<div class="tab">人事</div>
<div class="tab">行政</div>
</div>
<div class="hint">选择一个审批单即可发起</div>
<div class="body-area">
<div class="state-plain">加载中…</div>
</div>
</div></div>
</div>
<!-- A spinner -->
<div class="phone-card" data-choice="a" onclick="pick(this)">
<div class="badge-row">
<div class="letter">A</div>
<span class="badge muted">轻量</span>
</div>
<h3 class="title">品牌旋转环</h3>
<p class="desc">白卡片 + 绿色 spinner + 文案,改动最小,接近 Vant Loading。</p>
<div class="phone"><div class="screen">
<div class="nav">发起审批</div>
<div class="search-wrap"><div class="search">请输入审批单名称</div></div>
<div class="tabs">
<div class="tab on">全部</div>
<div class="tab">人事</div>
<div class="tab">行政</div>
</div>
<div class="hint">选择一个审批单即可发起</div>
<div class="body-area">
<div class="state-spin">
<div class="spin-card">
<div class="spin-ring"></div>
<div class="spin-text">正在加载审批模板</div>
<div class="spin-sub">请稍候</div>
</div>
</div>
</div>
</div></div>
</div>
<!-- B pulse -->
<div class="phone-card" data-choice="b" onclick="pick(this)">
<div class="badge-row">
<div class="letter">B</div>
<span class="badge muted">克制</span>
</div>
<h3 class="title">三点弹跳</h3>
<p class="desc">无卡片、更轻;适合短等待,但信息感偏弱。</p>
<div class="phone"><div class="screen">
<div class="nav">发起审批</div>
<div class="search-wrap"><div class="search">请输入审批单名称</div></div>
<div class="tabs">
<div class="tab on">全部</div>
<div class="tab">人事</div>
<div class="tab">行政</div>
</div>
<div class="hint">选择一个审批单即可发起</div>
<div class="body-area">
<div class="state-pulse">
<div class="pulse-dots">
<span></span><span></span><span></span>
</div>
<div class="pulse-label">加载审批模板…</div>
</div>
</div>
</div></div>
</div>
<!-- C skeleton -->
<div class="phone-card is-rec" data-choice="c" onclick="pick(this)">
<div class="badge-row">
<div class="letter">C</div>
<span class="badge">推荐</span>
</div>
<h3 class="title">双列骨架屏</h3>
<p class="desc">模拟分组标题 + 双列卡片占位,加载完成后几乎无布局跳动。</p>
<div class="phone"><div class="screen">
<div class="nav">发起审批</div>
<div class="search-wrap"><div class="search">请输入审批单名称</div></div>
<div class="tabs">
<div class="tab on">全部</div>
<div class="tab">人事</div>
<div class="tab">行政</div>
</div>
<div class="hint">选择一个审批单即可发起</div>
<div class="body-area">
<div class="sec-hd">
<div class="sk sk-title"></div>
<div class="sk-dot"></div>
<div class="sk sk-meta"></div>
</div>
<div class="card-row">
<div class="sk-card">
<div class="sk-main">
<div class="sk sk-icon"></div>
<div class="sk-lines">
<div class="sk sk-name"></div>
<div class="sk sk-desc"></div>
</div>
</div>
<div class="sk-ops"><div class="sk sk-btn"></div></div>
</div>
<div class="sk-card">
<div class="sk-main">
<div class="sk sk-icon"></div>
<div class="sk-lines">
<div class="sk sk-name"></div>
<div class="sk sk-desc"></div>
</div>
</div>
<div class="sk-ops"><div class="sk sk-btn"></div></div>
</div>
</div>
<div class="sec-hd">
<div class="sk sk-title" style="width:56px"></div>
<div class="sk-dot"></div>
<div class="sk sk-meta" style="width:40px"></div>
</div>
<div class="card-row">
<div class="sk-card">
<div class="sk-main">
<div class="sk sk-icon"></div>
<div class="sk-lines">
<div class="sk sk-name"></div>
<div class="sk sk-desc"></div>
</div>
</div>
<div class="sk-ops"><div class="sk sk-btn"></div></div>
</div>
<div class="sk-card">
<div class="sk-main">
<div class="sk sk-icon"></div>
<div class="sk-lines">
<div class="sk sk-name"></div>
<div class="sk sk-desc"></div>
</div>
</div>
<div class="sk-ops"><div class="sk sk-btn"></div></div>
</div>
</div>
</div>
</div></div>
</div>
</div>
<p class="pick-hint">选中结果会记在下方;也可直接回复 A / B / C。</p>
<p id="picked" class="pick-hint" style="color:var(--brand-dark);font-weight:650;"></p>
</div>
<script>
function pick(el) {
document.querySelectorAll('.phone-card').forEach((c) => c.classList.remove('is-selected'))
el.classList.add('is-selected')
const map = { '0': '现状(纯文字)', a: 'A · 品牌旋转环', b: 'B · 三点弹跳', c: 'C · 双列骨架屏' }
const choice = el.getAttribute('data-choice')
document.getElementById('picked').textContent = '当前选择:' + (map[choice] || choice)
}
</script>
</body>
</html>