552 lines
19 KiB
HTML
552 lines
19 KiB
HTML
<!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>
|