Files
zkxw/web-admin/demo-protocol-audit-timeline-tabs.html
T
2026-06-15 11:43:16 +08:00

828 lines
23 KiB
HTML
Raw 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.0" />
<title>采集协议单 · 横向时间线 Tab Demo</title>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--primary: #10b981;
--primary-dark: #059669;
--primary-bg: #ecfdf5;
--mint-border: #c5e0d4;
--text: #1e293b;
--muted: #64748b;
--surface: #fff;
--page-bg: #f0f2f5;
--border: #e2e8f0;
}
body {
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'PingFang SC',
'Hiragino Sans GB',
'Microsoft YaHei',
sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text);
background: var(--page-bg);
padding: 32px 24px 48px;
}
.page-head {
max-width: 1120px;
margin: 0 auto 28px;
}
.page-head h1 {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
}
.page-head p {
color: var(--muted);
max-width: 760px;
}
.demo-block {
max-width: 1120px;
margin: 0 auto 28px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 1px 3px rgb(15 23 42 / 6%);
}
.demo-label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 20px;
border-bottom: 1px solid var(--border);
background: #fafbfc;
}
.demo-label strong {
font-size: 13px;
font-weight: 600;
}
.demo-label span {
font-size: 12px;
color: var(--muted);
}
.panel-shell {
padding: 0;
}
.panel-content {
padding: 20px 24px 24px;
border-top: 1px solid var(--border);
min-height: 120px;
}
.panel-content h3 {
font-size: 15px;
font-weight: 600;
margin-bottom: 6px;
}
.panel-content p {
color: var(--muted);
font-size: 13px;
}
/* ============================================================
方案 A:经典步骤条(圆点 + 连线 + 下方标题)
============================================================ */
.timeline-a {
padding: 24px 32px 8px;
}
.timeline-a__track {
display: flex;
align-items: flex-start;
position: relative;
}
.timeline-a__step {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
border: none;
background: transparent;
cursor: pointer;
padding: 0 8px;
min-width: 0;
text-align: center;
}
.timeline-a__node-wrap {
position: relative;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.timeline-a__line {
position: absolute;
top: 50%;
left: 50%;
right: -50%;
height: 2px;
background: var(--border);
transform: translateY(-50%);
z-index: 0;
}
.timeline-a__step:last-child .timeline-a__line {
display: none;
}
.timeline-a__dot {
position: relative;
z-index: 1;
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid var(--border);
background: var(--surface);
transition: all 0.2s ease;
}
.timeline-a__title {
font-size: 13px;
font-weight: 600;
color: var(--muted);
transition: color 0.2s ease;
}
.timeline-a__desc {
font-size: 11px;
color: #94a3b8;
line-height: 1.35;
max-width: 140px;
}
.timeline-a__step.is-active .timeline-a__dot {
width: 18px;
height: 18px;
border-color: var(--primary);
background: var(--primary);
box-shadow: 0 0 0 4px rgb(16 185 129 / 18%);
}
.timeline-a__step.is-active .timeline-a__title {
color: var(--primary-dark);
}
.timeline-a__step.is-done .timeline-a__dot {
border-color: var(--primary);
background: var(--primary-bg);
}
.timeline-a__step.is-done .timeline-a__line {
background: var(--primary);
}
.timeline-a__step.is-done .timeline-a__title {
color: var(--text);
}
.timeline-a__step:hover:not(.is-active) .timeline-a__title {
color: var(--primary-dark);
}
/* ============================================================
方案 B:带序号胶囊 + 图标(偏流程感)
============================================================ */
.timeline-b {
padding: 20px 24px 10px;
}
.timeline-b__track {
display: flex;
align-items: stretch;
gap: 0;
position: relative;
}
.timeline-b__step {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
border: none;
background: transparent;
cursor: pointer;
padding: 0 6px 4px;
min-width: 0;
}
.timeline-b__connector {
position: absolute;
top: 22px;
left: calc(50% + 28px);
width: calc(100% - 56px);
height: 2px;
background: linear-gradient(90deg, var(--border), var(--border));
z-index: 0;
}
.timeline-b__step:last-child .timeline-b__connector {
display: none;
}
.timeline-b__step.is-done .timeline-b__connector {
background: linear-gradient(90deg, var(--primary), var(--primary));
}
.timeline-b__badge {
position: relative;
z-index: 1;
width: 44px;
height: 44px;
border-radius: 50%;
border: 2px solid var(--border);
background: #f8fafc;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
transition: all 0.2s ease;
}
.timeline-b__num {
position: absolute;
top: -4px;
right: -4px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #e2e8f0;
color: #475569;
font-size: 10px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid var(--surface);
}
.timeline-b__title {
font-size: 13px;
font-weight: 600;
color: var(--muted);
}
.timeline-b__desc {
font-size: 11px;
color: #94a3b8;
text-align: center;
line-height: 1.35;
max-width: 132px;
}
.timeline-b__step.is-active .timeline-b__badge {
border-color: var(--primary);
background: var(--primary-bg);
box-shadow: 0 0 0 4px rgb(16 185 129 / 14%);
}
.timeline-b__step.is-active .timeline-b__num {
background: var(--primary);
color: #fff;
}
.timeline-b__step.is-active .timeline-b__title {
color: var(--primary-dark);
}
.timeline-b__step.is-done .timeline-b__badge {
border-color: var(--primary);
background: var(--primary);
color: #fff;
}
.timeline-b__step.is-done .timeline-b__num {
background: var(--primary-dark);
color: #fff;
}
.timeline-b__step.is-done .timeline-b__title {
color: var(--text);
}
/* ============================================================
方案 C:轨道式 Pill Tab(推荐 · 最贴近 B 端列表页)
============================================================ */
.timeline-c {
padding: 16px 20px 0;
}
.timeline-c__rail {
display: flex;
align-items: center;
background: #f8fafc;
border: 1px solid var(--border);
border-radius: 10px;
padding: 6px;
position: relative;
overflow: hidden;
}
.timeline-c__progress {
position: absolute;
top: 6px;
bottom: 6px;
left: 6px;
width: calc(25% - 3px);
background: var(--surface);
border: 1px solid var(--mint-border);
border-radius: 8px;
box-shadow: 0 1px 3px rgb(15 23 42 / 8%);
transition:
transform 0.25s ease,
width 0.25s ease;
z-index: 0;
}
.timeline-c__step {
flex: 1;
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 44px;
padding: 8px 10px;
border: none;
background: transparent;
cursor: pointer;
border-radius: 8px;
transition: color 0.2s ease;
}
.timeline-c__index {
width: 22px;
height: 22px;
border-radius: 50%;
background: #e2e8f0;
color: #475569;
font-size: 11px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s ease;
}
.timeline-c__text {
display: flex;
flex-direction: column;
align-items: flex-start;
min-width: 0;
text-align: left;
}
.timeline-c__title {
font-size: 13px;
font-weight: 600;
color: var(--muted);
white-space: nowrap;
}
.timeline-c__desc {
font-size: 11px;
color: #94a3b8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.timeline-c__chevron {
position: absolute;
right: -1px;
top: 50%;
transform: translateY(-50%);
color: #cbd5e1;
font-size: 12px;
pointer-events: none;
}
.timeline-c__step:last-child .timeline-c__chevron {
display: none;
}
.timeline-c__step.is-active .timeline-c__index {
background: var(--primary);
color: #fff;
}
.timeline-c__step.is-active .timeline-c__title {
color: var(--primary-dark);
}
.timeline-c__step.is-done .timeline-c__index {
background: var(--primary-bg);
color: var(--primary-dark);
border: 1px solid var(--mint-border);
}
.timeline-c__step.is-done .timeline-c__title {
color: var(--text);
}
.timeline-c__meta {
display: flex;
justify-content: space-between;
padding: 10px 4px 0;
font-size: 11px;
color: #94a3b8;
}
/* ============================================================
方案 D:箭头分段条(强调业务先后顺序)
============================================================ */
.timeline-d {
padding: 18px 20px 8px;
}
.timeline-d__track {
display: flex;
filter: drop-shadow(0 1px 2px rgb(15 23 42 / 6%));
}
.timeline-d__step {
flex: 1;
position: relative;
min-height: 56px;
border: none;
cursor: pointer;
padding: 10px 28px 10px 22px;
text-align: left;
background: #f8fafc;
clip-path: polygon(0 0, calc(100% - 14px) 0, 100% 50%, calc(100% - 14px) 100%, 0 100%, 14px 50%);
margin-left: -10px;
transition:
background 0.2s ease,
color 0.2s ease;
}
.timeline-d__step:first-child {
margin-left: 0;
clip-path: polygon(0 0, calc(100% - 14px) 0, 100% 50%, calc(100% - 14px) 100%, 0 100%);
border-radius: 8px 0 0 8px;
padding-left: 18px;
}
.timeline-d__step:last-child {
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 14px 50%);
border-radius: 0 8px 8px 0;
padding-right: 18px;
}
.timeline-d__title {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--muted);
margin-bottom: 2px;
}
.timeline-d__desc {
display: block;
font-size: 11px;
color: #94a3b8;
line-height: 1.3;
}
.timeline-d__step.is-active {
background: var(--primary-bg);
z-index: 2;
}
.timeline-d__step.is-active .timeline-d__title {
color: var(--primary-dark);
}
.timeline-d__step.is-done {
background: #eefbf5;
z-index: 1;
}
.timeline-d__step.is-done .timeline-d__title {
color: #047857;
}
.timeline-d__step:hover:not(.is-active) {
background: #f1f5f9;
}
@media (max-width: 960px) {
.timeline-c__desc,
.timeline-a__desc,
.timeline-b__desc,
.timeline-d__desc {
display: none;
}
.timeline-c__text {
align-items: center;
}
.timeline-b__badge {
width: 38px;
height: 38px;
font-size: 16px;
}
}
@media (max-width: 720px) {
body {
padding: 16px 12px 32px;
}
.timeline-a,
.timeline-b,
.timeline-c,
.timeline-d {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.timeline-a__track,
.timeline-b__track {
min-width: 640px;
}
.timeline-c__rail {
min-width: 680px;
}
.timeline-d__track {
min-width: 720px;
}
}
</style>
</head>
<body>
<header class="page-head">
<h1>采集协议单 · 横向时间线 Tab 样式 Demo</h1>
<p>
将「协议单管理 → 缴费信息 → 储存证管理 → 满月待办管理」四个视图切换,做成带先后顺序感的时间线 Tab。
点击各步骤可切换下方占位内容。色值对齐 Fresh Mint(主色 #10b981)。
</p>
</header>
<!-- 方案 A -->
<section class="demo-block" data-demo="a">
<div class="demo-label">
<strong>方案 A · 经典步骤条</strong>
<span>圆点 + 连线,轻量、常见于表单分步</span>
</div>
<div class="panel-shell">
<nav class="timeline-a" role="tablist" aria-label="方案A">
<div class="timeline-a__track"></div>
</nav>
<div class="panel-content" data-content="a">
<h3>协议单管理</h3>
<p>协议审核与状态维护 — 此处为 Tab 内容占位区(列表 / 筛选 / 表格)。</p>
</div>
</div>
</section>
<!-- 方案 B -->
<section class="demo-block" data-demo="b">
<div class="demo-label">
<strong>方案 B · 序号 + 图标节点</strong>
<span>流程感更强,适合强调四段业务链路</span>
</div>
<div class="panel-shell">
<nav class="timeline-b" role="tablist" aria-label="方案B">
<div class="timeline-b__track"></div>
</nav>
<div class="panel-content" data-content="b">
<h3>协议单管理</h3>
<p>协议审核与状态维护 — 此处为 Tab 内容占位区。</p>
</div>
</div>
</section>
<!-- 方案 C -->
<section class="demo-block" data-demo="c">
<div class="demo-label">
<strong>方案 C · 轨道 Pill Tab(推荐)</strong>
<span>与现有 B 端面板最协调,可保留副标题</span>
</div>
<div class="panel-shell">
<nav class="timeline-c" role="tablist" aria-label="方案C">
<div class="timeline-c__rail">
<div class="timeline-c__progress"></div>
</div>
<div class="timeline-c__meta">
<span>当前环节</span>
<span data-meta="c">步骤 1 / 4</span>
</div>
</nav>
<div class="panel-content" data-content="c">
<h3>协议单管理</h3>
<p>协议审核与状态维护 — 此处为 Tab 内容占位区。</p>
</div>
</div>
</section>
<!-- 方案 D -->
<section class="demo-block" data-demo="d">
<div class="demo-label">
<strong>方案 D · 箭头分段条</strong>
<span>视觉冲击力大,窄屏需横向滚动</span>
</div>
<div class="panel-shell">
<nav class="timeline-d" role="tablist" aria-label="方案D">
<div class="timeline-d__track"></div>
</nav>
<div class="panel-content" data-content="d">
<h3>协议单管理</h3>
<p>协议审核与状态维护 — 此处为 Tab 内容占位区。</p>
</div>
</div>
</section>
<script>
const STEPS = [
{
key: 'protocol',
title: '协议单管理',
desc: '协议审核与状态维护',
icon: '📄'
},
{
key: 'payment-info',
title: '缴费信息',
desc: '缴费计划与到账查询',
icon: '💳'
},
{
key: 'storage-cert',
title: '储存证管理',
desc: '储存证开具与管理',
icon: '🏅'
},
{
key: 'fullmoon-todo',
title: '满月待办管理',
desc: '满月保险待办办理',
icon: '📅'
}
]
function renderTimelineA(container, activeIndex, onSelect) {
const track = container.querySelector('.timeline-a__track')
track.innerHTML = STEPS.map((step, index) => {
const state =
index === activeIndex ? 'is-active' : index < activeIndex ? 'is-done' : ''
return `
<button type="button" class="timeline-a__step ${state}" role="tab" aria-selected="${index === activeIndex}" data-index="${index}">
<div class="timeline-a__node-wrap">
<span class="timeline-a__line"></span>
<span class="timeline-a__dot"></span>
</div>
<span class="timeline-a__title">${step.title}</span>
<span class="timeline-a__desc">${step.desc}</span>
</button>
`
}).join('')
track.querySelectorAll('.timeline-a__step').forEach((btn) => {
btn.addEventListener('click', () => onSelect(Number(btn.dataset.index)))
})
}
function renderTimelineB(container, activeIndex, onSelect) {
const track = container.querySelector('.timeline-b__track')
track.innerHTML = STEPS.map((step, index) => {
const state =
index === activeIndex ? 'is-active' : index < activeIndex ? 'is-done' : ''
return `
<button type="button" class="timeline-b__step ${state}" role="tab" aria-selected="${index === activeIndex}" data-index="${index}">
<span class="timeline-b__connector"></span>
<span class="timeline-b__badge">
${step.icon}
<span class="timeline-b__num">${index + 1}</span>
</span>
<span class="timeline-b__title">${step.title}</span>
<span class="timeline-b__desc">${step.desc}</span>
</button>
`
}).join('')
track.querySelectorAll('.timeline-b__step').forEach((btn) => {
btn.addEventListener('click', () => onSelect(Number(btn.dataset.index)))
})
}
function renderTimelineC(container, activeIndex, onSelect) {
const rail = container.querySelector('.timeline-c__rail')
const progress = rail.querySelector('.timeline-c__progress')
const buttons = STEPS.map((step, index) => {
const state =
index === activeIndex ? 'is-active' : index < activeIndex ? 'is-done' : ''
return `
<button type="button" class="timeline-c__step ${state}" role="tab" aria-selected="${index === activeIndex}" data-index="${index}">
<span class="timeline-c__index">${index + 1}</span>
<span class="timeline-c__text">
<span class="timeline-c__title">${step.title}</span>
<span class="timeline-c__desc">${step.desc}</span>
</span>
<span class="timeline-c__chevron">›</span>
</button>
`
}).join('')
progress.insertAdjacentHTML('afterend', buttons)
progress.style.width = `calc(${100 / STEPS.length}% - 3px)`
progress.style.transform = `translateX(${activeIndex * 100}%)`
rail.querySelectorAll('.timeline-c__step').forEach((btn) => {
btn.addEventListener('click', () => onSelect(Number(btn.dataset.index)))
})
const meta = container.querySelector('[data-meta="c"]')
if (meta) meta.textContent = `步骤 ${activeIndex + 1} / ${STEPS.length}`
}
function renderTimelineD(container, activeIndex, onSelect) {
const track = container.querySelector('.timeline-d__track')
track.innerHTML = STEPS.map((step, index) => {
const state =
index === activeIndex ? 'is-active' : index < activeIndex ? 'is-done' : ''
return `
<button type="button" class="timeline-d__step ${state}" role="tab" aria-selected="${index === activeIndex}" data-index="${index}">
<span class="timeline-d__title">${step.title}</span>
<span class="timeline-d__desc">${step.desc}</span>
</button>
`
}).join('')
track.querySelectorAll('.timeline-d__step').forEach((btn) => {
btn.addEventListener('click', () => onSelect(Number(btn.dataset.index)))
})
}
function updateContent(demoKey, activeIndex) {
const step = STEPS[activeIndex]
const panel = document.querySelector(`[data-content="${demoKey}"]`)
if (!panel) return
panel.innerHTML = `
<h3>${step.title}</h3>
<p>${step.desc} — 此处为 Tab 内容占位区(列表 / 筛选 / 表格)。</p>
`
}
function initDemo(demoKey, renderFn) {
let activeIndex = 0
const section = document.querySelector(`[data-demo="${demoKey}"]`)
const nav = section.querySelector('nav')
function refresh() {
if (demoKey === 'c') {
nav.querySelector('.timeline-c__rail').innerHTML =
'<div class="timeline-c__progress"></div>'
}
renderFn(nav, activeIndex, (index) => {
activeIndex = index
refresh()
})
updateContent(demoKey, activeIndex)
}
refresh()
}
initDemo('a', renderTimelineA)
initDemo('b', renderTimelineB)
initDemo('c', renderTimelineC)
initDemo('d', renderTimelineD)
</script>
</body>
</html>