This commit is contained in:
zlwu committed 2026-08-12 10:02:31 +08:00
1 parent 8aab018558
commit 1b2896c877
70 files changed
+7753 -3306

No files matched your search

+551
View File
@@ -0,0 +1,551 @@
<!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;
--ink: #1f2a26;
--muted: #6b7b74;
--border: #dcebe4;
}
* { 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: 1100px; margin: 0 auto; padding: 24px 16px 96px; }
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; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.card {
background: #fff;
border: 1px solid #e2e8e5;
border-radius: 16px;
padding: 14px;
transition: border-color .15s, box-shadow .15s;
}
.card.is-selected {
border-color: var(--brand);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 28%, transparent);
}
.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: 8px;
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;
}
.tag {
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
background: #f1f5f3;
color: var(--muted);
}
.tag.warn { background: #fff7ed; color: #c2410c; }
.tag.rec { background: var(--brand-soft); color: var(--brand-dark); font-weight: 650; }
.card h2 {
margin: 0 0 4px;
font-size: 15px;
font-weight: 650;
}
.card p {
margin: 0 0 12px;
font-size: 12.5px;
color: var(--muted);
}
.phone {
border: 1px solid var(--border);
border-radius: 14px;
background: var(--bg);
overflow: hidden;
}
.phone-body {
height: 88px;
padding: 12px;
font-size: 12px;
color: var(--muted);
}
.ft {
display: flex;
gap: 10px;
align-items: center;
padding: 10px 12px;
background: #fff;
border-top: 1px solid var(--border);
box-shadow: 0 -4px 16px rgba(31, 42, 38, 0.04);
}
.btn {
margin: 0;
height: 40px;
border-radius: 8px;
font-size: 14px;
font-family: inherit;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 0 12px;
transition: opacity .15s, filter .15s;
position: relative;
overflow: hidden;
-webkit-tap-highlight-color: transparent;
}
.btn:disabled { cursor: not-allowed; }
.btn-cancel {
flex: 0 0 64px;
border: none;
background: transparent;
color: var(--muted);
padding: 0;
}
.btn-draft {
flex: 1;
border: 1.5px solid var(--brand);
background: #fff;
color: var(--brand-dark);
font-weight: 600;
}
.btn-submit {
flex: 1.35;
border: none;
background: linear-gradient(135deg, var(--brand), var(--brand-dark));
color: #fff;
font-weight: 700;
box-shadow: 0 6px 14px rgba(22, 163, 74, 0.28);
}
.btn.is-loading { pointer-events: none; }
/* spinner */
.spin {
width: 16px; height: 16px;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin .7s linear infinite;
flex-shrink: 0;
}
.spin--lg { width: 18px; height: 18px; }
@keyframes spin { to { transform: rotate(360deg); } }
/* A: 现状 — 文字消失,只剩转圈 */
.a .btn.is-loading .label { display: none; }
.a .btn.is-loading .spin-only { display: block; }
.a .spin-only { display: none; }
/* B: 左侧小转圈 + 文案保留/改为「…中」 */
/* default markup handles it */
/* C: 仅改文案,无转圈 */
/* handled in JS text swap */
/* D: 文案 + 跳动三点 */
.dots span {
display: inline-block;
animation: bounce 1.1s infinite ease-in-out;
opacity: .35;
}
.dots span:nth-child(2) { animation-delay: .15s; }
.dots span:nth-child(3) { animation-delay: .3s; }
@keyframes bounce {
0%, 80%, 100% { opacity: .25; transform: translateY(0); }
40% { opacity: 1; transform: translateY(-2px); }
}
/* E: 保留文案 + 按钮内浅色 shimmer */
.e .btn.is-loading::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
105deg,
transparent 35%,
rgba(255,255,255,.35) 50%,
transparent 65%
);
background-size: 200% 100%;
animation: shimmer 1.1s ease-in-out infinite;
pointer-events: none;
}
.e .btn-draft.is-loading::after {
background: linear-gradient(
105deg,
transparent 35%,
rgba(22,163,74,.12) 50%,
transparent 65%
);
background-size: 200% 100%;
}
@keyframes shimmer {
0% { background-position: 100% 0; }
100% { background-position: -100% 0; }
}
/* F: 保留文案 + 整体脉冲透明度 */
.f .btn.is-loading {
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: .55; }
}
/* G: 转圈叠在文字上(半透明底)— 接近现状但更差,作对比 */
.g .btn.is-loading .label { opacity: .25; }
.g .btn.is-loading .overlay-spin {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: rgba(0,0,0,.06);
}
.g .overlay-spin { display: none; }
.g .btn.is-loading .overlay-spin { display: grid; }
.actions {
margin-top: 10px;
display: flex;
gap: 8px;
}
.pick {
flex: 1;
height: 34px;
border-radius: 8px;
border: 1px solid var(--border);
background: #fff;
color: var(--ink);
font-size: 13px;
font-family: inherit;
cursor: pointer;
}
.pick:hover { border-color: var(--brand); color: var(--brand-dark); }
.card.is-selected .pick {
background: var(--brand);
border-color: var(--brand);
color: #fff;
font-weight: 650;
}
.hint {
margin-top: 8px;
font-size: 11px;
color: var(--muted);
text-align: center;
}
.bar {
position: sticky;
bottom: 0;
margin-top: 28px;
padding: 12px 14px;
background: rgba(255,255,255,.92);
backdrop-filter: blur(8px);
border: 1px solid #e2e8e5;
border-radius: 12px;
font-size: 13px;
color: var(--muted);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.bar strong { color: var(--brand-dark); }
</style>
</head>
<body>
<div class="wrap">
<h1>发起审批 · 底栏按钮加载态对比</h1>
<p class="sub">
模拟「暂存草稿 / 提交」点击后的等待反馈。点任意主按钮可预览约 2 秒加载态;选中喜欢的方案后告诉我编号即可落地。
</p>
<div class="note">
<strong>现状问题:</strong>Vant <code>:loading</code> 默认会把按钮文字藏掉,中间只剩一个转圈,看起来像「按钮坏了 / 内容丢失」,而不是「正在处理」。
更好的做法一般是:<strong>保留语义文案</strong>(如「提交中…」),转圈可有可无、且应偏小、靠左。
</div>
<div class="grid">
<!-- A -->
<article class="card a" data-id="A" data-name="现状:只剩转圈">
<div class="badge-row">
<span class="letter">A</span>
<span class="tag warn">现状 / 不推荐</span>
</div>
<h2>文字消失 · 中间转圈</h2>
<p>Vant 默认 loading:文案隐藏,只留 spinner。容易误以为按钮空白或出错。</p>
<div class="phone">
<div class="phone-body">表单区占位…</div>
<div class="ft">
<button type="button" class="btn btn-cancel">取消</button>
<button type="button" class="btn btn-draft" data-role="draft">
<span class="spin spin-only" aria-hidden="true"></span>
<span class="label">暂存草稿</span>
</button>
<button type="button" class="btn btn-submit" data-role="submit">
<span class="spin spin-only spin--lg" aria-hidden="true"></span>
<span class="label">提交</span>
</button>
</div>
</div>
<p class="hint">点「暂存草稿」或「提交」试效果</p>
<div class="actions"><button type="button" class="pick">选这个</button></div>
</article>
<!-- B -->
<article class="card b is-rec" data-id="B" data-name="小转圈 + 文案">
<div class="badge-row">
<span class="letter">B</span>
<span class="tag rec">推荐</span>
</div>
<h2>左侧小转圈 +「…中」文案</h2>
<p>保留可读语义:暂存中… / 提交中…。转圈提示进度,文案说明在做什么。行业常见做法。</p>
<div class="phone">
<div class="phone-body">表单区占位…</div>
<div class="ft">
<button type="button" class="btn btn-cancel">取消</button>
<button type="button" class="btn btn-draft" data-role="draft"
data-idle="暂存草稿" data-busy="暂存中…">
<span class="spin" hidden aria-hidden="true"></span>
<span class="label">暂存草稿</span>
</button>
<button type="button" class="btn btn-submit" data-role="submit"
data-idle="提交" data-busy="提交中…">
<span class="spin" hidden aria-hidden="true"></span>
<span class="label">提交</span>
</button>
</div>
</div>
<p class="hint">点「暂存草稿」或「提交」试效果</p>
<div class="actions"><button type="button" class="pick">选这个</button></div>
</article>
<!-- C -->
<article class="card c" data-id="C" data-name="仅改文案">
<div class="badge-row">
<span class="letter">C</span>
<span class="tag">轻量</span>
</div>
<h2>只改文案 · 无转圈</h2>
<p>「提交」→「提交中…」,按钮禁用。最干净,但进度感稍弱;短请求时够用。</p>
<div class="phone">
<div class="phone-body">表单区占位…</div>
<div class="ft">
<button type="button" class="btn btn-cancel">取消</button>
<button type="button" class="btn btn-draft" data-role="draft"
data-idle="暂存草稿" data-busy="暂存中…">
<span class="label">暂存草稿</span>
</button>
<button type="button" class="btn btn-submit" data-role="submit"
data-idle="提交" data-busy="提交中…">
<span class="label">提交</span>
</button>
</div>
</div>
<p class="hint">点「暂存草稿」或「提交」试效果</p>
<div class="actions"><button type="button" class="pick">选这个</button></div>
</article>
<!-- D -->
<article class="card d" data-id="D" data-name="跳动三点">
<div class="badge-row">
<span class="letter">D</span>
<span class="tag">活泼</span>
</div>
<h2>文案 + 跳动三点</h2>
<p>「提交中···」三点依次闪动。无转圈、有动效,适合品牌感稍强的场景。</p>
<div class="phone">
<div class="phone-body">表单区占位…</div>
<div class="ft">
<button type="button" class="btn btn-cancel">取消</button>
<button type="button" class="btn btn-draft" data-role="draft"
data-idle="暂存草稿" data-busy-html='暂存中<span class="dots"><span>·</span><span>·</span><span>·</span></span>'>
<span class="label">暂存草稿</span>
</button>
<button type="button" class="btn btn-submit" data-role="submit"
data-idle="提交" data-busy-html='提交中<span class="dots"><span>·</span><span>·</span><span>·</span></span>'>
<span class="label">提交</span>
</button>
</div>
</div>
<p class="hint">点「暂存草稿」或「提交」试效果</p>
<div class="actions"><button type="button" class="pick">选这个</button></div>
</article>
<!-- E -->
<article class="card e" data-id="E" data-name="光带扫描">
<div class="badge-row">
<span class="letter">E</span>
<span class="tag">动感</span>
</div>
<h2>保留文案 · 光带扫描</h2>
<p>文字不变(或改为「…中」),按钮表面扫光。进度感强,实现稍重一点。</p>
<div class="phone">
<div class="phone-body">表单区占位…</div>
<div class="ft">
<button type="button" class="btn btn-cancel">取消</button>
<button type="button" class="btn btn-draft" data-role="draft"
data-idle="暂存草稿" data-busy="暂存中…">
<span class="label">暂存草稿</span>
</button>
<button type="button" class="btn btn-submit" data-role="submit"
data-idle="提交" data-busy="提交中…">
<span class="label">提交</span>
</button>
</div>
</div>
<p class="hint">点「暂存草稿」或「提交」试效果</p>
<div class="actions"><button type="button" class="pick">选这个</button></div>
</article>
<!-- F -->
<article class="card f" data-id="F" data-name="透明度脉冲">
<div class="badge-row">
<span class="letter">F</span>
<span class="tag">极简</span>
</div>
<h2>保留文案 · 整体脉冲</h2>
<p>文字改为「…中」,按钮透明度呼吸。实现最简单,但不够「明确在请求」。</p>
<div class="phone">
<div class="phone-body">表单区占位…</div>
<div class="ft">
<button type="button" class="btn btn-cancel">取消</button>
<button type="button" class="btn btn-draft" data-role="draft"
data-idle="暂存草稿" data-busy="暂存中…">
<span class="label">暂存草稿</span>
</button>
<button type="button" class="btn btn-submit" data-role="submit"
data-idle="提交" data-busy="提交中…">
<span class="label">提交</span>
</button>
</div>
</div>
<p class="hint">点「暂存草稿」或「提交」试效果</p>
<div class="actions"><button type="button" class="pick">选这个</button></div>
</article>
<!-- G -->
<article class="card g" data-id="G" data-name="转圈压字">
<div class="badge-row">
<span class="letter">G</span>
<span class="tag warn">对比用</span>
</div>
<h2>转圈压在字上</h2>
<p>文字半透明仍在,转圈居中覆盖。比 A 稍好但仍挤、难读,一般不建议。</p>
<div class="phone">
<div class="phone-body">表单区占位…</div>
<div class="ft">
<button type="button" class="btn btn-cancel">取消</button>
<button type="button" class="btn btn-draft" data-role="draft">
<span class="label">暂存草稿</span>
<span class="overlay-spin"><span class="spin"></span></span>
</button>
<button type="button" class="btn btn-submit" data-role="submit">
<span class="label">提交</span>
<span class="overlay-spin"><span class="spin" style="border-color:#fff;border-right-color:transparent"></span></span>
</button>
</div>
</div>
<p class="hint">点「暂存草稿」或「提交」试效果</p>
<div class="actions"><button type="button" class="pick">选这个</button></div>
</article>
</div>
<div class="bar">
<span>当前选择:<strong id="choice">未选</strong></span>
<span>建议优先试 <strong>B</strong>(小转圈 + 文案),短接口也可考虑 <strong>C</strong>。</span>
</div>
</div>
<script>
const timers = new WeakMap()
function setLoading(card, btn, on) {
const id = card.dataset.id
const label = btn.querySelector('.label')
const spin = btn.querySelector('.spin:not(.spin-only)')
btn.classList.toggle('is-loading', on)
btn.disabled = on
if (id === 'A') {
// CSS handles hide label / show spin-only
return
}
if (id === 'G') {
return
}
if (spin) spin.hidden = !on
if (btn.dataset.busyHtml) {
if (on) label.innerHTML = btn.dataset.busyHtml
else label.textContent = btn.dataset.idle || label.textContent
return
}
if (btn.dataset.busy) {
if (on) label.textContent = btn.dataset.busy
else label.textContent = btn.dataset.idle || label.textContent
}
}
document.querySelectorAll('.card').forEach((card) => {
card.querySelectorAll('[data-role]').forEach((btn) => {
btn.addEventListener('click', () => {
if (btn.classList.contains('is-loading')) return
// clear sibling loading in same footer
card.querySelectorAll('[data-role]').forEach((b) => {
clearTimeout(timers.get(b))
setLoading(card, b, false)
})
setLoading(card, btn, true)
const t = setTimeout(() => setLoading(card, btn, false), 2000)
timers.set(btn, t)
})
})
card.querySelector('.pick').addEventListener('click', () => {
document.querySelectorAll('.card').forEach((c) => c.classList.remove('is-selected'))
card.classList.add('is-selected')
document.getElementById('choice').textContent =
card.dataset.id + ' · ' + card.dataset.name
})
})
</script>
</body>
</html>