提交
This commit is contained in:
1 parent
8aab018558
commit
1b2896c877
70 files changed
+7753
-3306
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user