Files
zkxw/docs/demos/2026-10-12-market-protocol-source-tabs-demo.html
T
2026-10-13 15:18:01 +08:00

767 lines
21 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>
:root {
--brand: #0a9f6d;
--brand-dark: #087f57;
--brand-soft: #e9f9f2;
--canvas: #f4f8f6;
--surface: #fff;
--text: #18332a;
--muted: #6b7f77;
--line: #dce9e4;
--danger: #d14343;
--warning: #d98516;
--shadow: 0 10px 30px rgba(25, 65, 49, 0.12);
}
* {
box-sizing: border-box;
}
html,
body {
min-height: 100%;
margin: 0;
color: var(--text);
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', sans-serif;
background: #eaf1ed;
}
button,
input {
font: inherit;
}
button {
cursor: pointer;
}
.demo-stage {
min-height: 100vh;
display: grid;
grid-template-columns: minmax(270px, 330px) 390px;
align-items: center;
justify-content: center;
gap: 36px;
padding: 30px;
}
.demo-controls {
padding: 26px;
border: 1px solid rgba(10, 159, 109, 0.12);
border-radius: 24px;
background: rgba(255, 255, 255, 0.94);
box-shadow: var(--shadow);
}
.eyebrow {
margin: 0 0 8px;
color: var(--brand);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.demo-controls h2 {
margin: 0;
font-size: 22px;
line-height: 1.3;
}
.intro {
margin: 10px 0 20px;
color: var(--muted);
font-size: 14px;
line-height: 1.75;
}
.control-label {
display: block;
margin: 16px 0 8px;
color: #506259;
font-size: 12px;
font-weight: 750;
}
.segmented {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
padding: 4px;
border-radius: 14px;
background: #edf3f0;
}
.segmented button {
min-height: 44px;
border: 0;
border-radius: 11px;
color: #52635b;
background: transparent;
font-size: 14px;
font-weight: 700;
}
.segmented button.is-active {
color: #fff;
background: var(--brand);
box-shadow: 0 5px 14px rgba(10, 159, 109, 0.2);
}
.bullet-list {
margin: 16px 0 0;
padding: 14px 14px 14px 32px;
border-left: 3px solid var(--brand);
border-radius: 10px;
background: var(--brand-soft);
color: #4c6258;
font-size: 13px;
line-height: 1.7;
}
.demo-note {
margin: 12px 0 0;
color: #8a9891;
font-size: 12px;
line-height: 1.6;
}
.phone {
position: relative;
width: 390px;
height: min(844px, calc(100vh - 36px));
min-height: 650px;
overflow: hidden;
border: 8px solid #203129;
border-radius: 34px;
background: var(--canvas);
box-shadow: 0 28px 70px rgba(19, 47, 36, 0.3);
}
.phone::before {
content: '';
position: absolute;
z-index: 40;
top: 8px;
left: 50%;
width: 104px;
height: 23px;
border-radius: 0 0 14px 14px;
background: #203129;
transform: translateX(-50%);
}
.phone-scroll {
height: 100%;
overflow: auto;
padding-bottom: 88px;
}
.sticky-top {
position: sticky;
z-index: 20;
top: 0;
background: #fff;
border-bottom: 1px solid var(--line);
}
.mobile-nav {
display: grid;
grid-template-columns: 52px 1fr 52px;
align-items: end;
height: 66px;
padding: 0 8px 10px;
}
.mobile-nav h1 {
margin: 0;
font-size: 17px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nav-back {
display: grid;
place-items: center;
width: 44px;
height: 44px;
margin: 0 auto;
border: 0;
border-radius: 50%;
background: transparent;
color: var(--text);
font-size: 28px;
line-height: 1;
}
/* Web panel-pill-tabs 的移动端缩比:轨道胶囊 + 激活实心绿 */
.source-tabs {
padding: 8px 12px 0;
}
.source-tabs__track {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
padding: 4px;
border-radius: 999px;
background: #edf3f0;
}
.source-tabs__btn {
min-height: 40px;
border: 0;
border-radius: 999px;
color: #52635b;
background: transparent;
font-size: 14px;
font-weight: 700;
transition:
background 0.18s ease,
color 0.18s ease,
box-shadow 0.18s ease;
}
.source-tabs__btn.is-active {
color: #fff;
background: var(--brand);
box-shadow: 0 4px 12px rgba(10, 159, 109, 0.22);
}
.source-tabs__btn small {
display: inline-block;
margin-left: 4px;
min-width: 18px;
padding: 0 6px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.22);
color: inherit;
font-size: 11px;
font-weight: 700;
line-height: 18px;
text-align: center;
}
.source-tabs__btn:not(.is-active) small {
background: rgba(10, 159, 109, 0.12);
color: var(--brand-dark);
}
.search-wrap {
padding: 8px 12px 10px;
}
.search-box {
display: flex;
align-items: center;
gap: 8px;
min-height: 40px;
padding: 0 14px;
border-radius: 999px;
background: #f3f7f5;
color: var(--muted);
font-size: 13px;
}
.search-box input {
width: 100%;
border: 0;
outline: 0;
background: transparent;
color: var(--text);
}
.list {
display: grid;
gap: 12px;
padding: 12px;
}
.biz-card {
padding: 14px;
border: 1px solid var(--line);
border-radius: 16px;
background: var(--surface);
box-shadow: 0 6px 20px rgb(8 127 87 / 7%);
}
.card-head {
display: flex;
align-items: center;
gap: 10px;
padding-bottom: 12px;
border-bottom: 1px solid #eef4f1;
}
.index-dot {
display: grid;
place-items: center;
width: 30px;
height: 30px;
flex: 0 0 30px;
border-radius: 50%;
background: var(--brand-soft);
color: var(--brand-dark);
font-size: 13px;
font-weight: 700;
}
.card-title {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
}
.card-title strong {
overflow: hidden;
font-size: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-title small {
overflow: hidden;
color: var(--muted);
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.tag {
display: inline-flex;
align-items: center;
max-width: 112px;
padding: 2px 8px;
border-radius: 999px;
background: var(--brand-soft);
color: var(--brand-dark);
font-size: 12px;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tag.is-warning {
background: #fff6e8;
color: var(--warning);
}
.tag.is-info {
background: #eef2f0;
color: #4d6a5d;
}
.field-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 16px;
padding: 14px 0;
}
.field {
display: flex;
min-width: 0;
flex-direction: column;
gap: 4px;
color: var(--muted);
font-size: 12px;
}
.field.is-wide {
grid-column: 1 / -1;
}
.field b {
color: var(--text);
font-weight: 500;
overflow-wrap: anywhere;
}
.card-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
}
.card-actions button {
min-height: 32px;
padding: 0 12px;
border: 1px solid var(--line);
border-radius: 999px;
background: #fff;
color: var(--text);
font-size: 12px;
font-weight: 650;
}
.card-actions button.is-primary {
border-color: var(--brand);
background: var(--brand);
color: #fff;
}
.card-actions button.is-danger {
border-color: #f3c1c1;
color: var(--danger);
}
.empty {
padding: 48px 20px;
color: var(--muted);
text-align: center;
font-size: 14px;
}
.fab {
position: absolute;
z-index: 30;
right: 18px;
bottom: 22px;
display: grid;
place-items: center;
width: 52px;
height: 52px;
border: 0;
border-radius: 50%;
background: var(--brand);
color: #fff;
font-size: 28px;
box-shadow: 0 10px 24px rgba(10, 159, 109, 0.35);
}
.toast {
position: absolute;
z-index: 50;
left: 50%;
bottom: 88px;
transform: translateX(-50%);
padding: 10px 14px;
border-radius: 999px;
background: rgba(24, 51, 42, 0.92);
color: #fff;
font-size: 13px;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
white-space: nowrap;
}
.toast.is-show {
opacity: 1;
}
@media (max-width: 860px) {
.demo-stage {
grid-template-columns: 1fr;
justify-items: center;
padding: 18px;
}
.demo-controls {
width: min(390px, 100%);
}
}
</style>
</head>
<body>
<div class="demo-stage">
<aside class="demo-controls">
<p class="eyebrow">Mobile Preview</p>
<h2>协议单来源双 Tab</h2>
<p class="intro">
对齐 Web「采集协议单」:顶部固定两个胶囊 Tab,切换后仅展示对应来源单据。细胞存储副标题展示样本编码;科研捐赠副标题展示捐赠编号。
</p>
<label class="control-label">角色</label>
<div class="segmented" id="roleSwitch">
<button type="button" data-role="specialist" class="is-active">市场专员</button>
<button type="button" data-role="manager">地区经理</button>
</div>
<ul class="bullet-list">
<li>两个 Tab:细胞存储 / 科研捐赠(无「全部」)</li>
<li>切换 Tab 时列表与搜索词独立重置(示意)</li>
<li>卡片字段沿用现有市场端列表密度</li>
</ul>
<p class="demo-note">静态 HTML Demo,用于确认交互与视觉后再落代码。</p>
</aside>
<div class="phone" id="phone">
<div class="phone-scroll">
<div class="sticky-top">
<div class="mobile-nav">
<button type="button" class="nav-back" aria-label="返回">‹</button>
<h1 id="pageTitle">市场专员 · 协议单</h1>
<span></span>
</div>
<div class="source-tabs">
<div class="source-tabs__track" role="tablist" aria-label="协议来源">
<button
type="button"
class="source-tabs__btn is-active"
role="tab"
aria-selected="true"
data-source="cell_storage"
>
细胞存储 <small id="countCell">0</small>
</button>
<button
type="button"
class="source-tabs__btn"
role="tab"
aria-selected="false"
data-source="research_donation"
>
科研捐赠 <small id="countDonation">0</small>
</button>
</div>
</div>
<div class="search-wrap">
<label class="search-box">
<span>⌕</span>
<input id="keyword" type="search" placeholder="搜索母亲姓名 / 样本编码" />
</label>
</div>
</div>
<div class="list" id="list"></div>
</div>
<button type="button" class="fab" id="fab" aria-label="新增协议单">+</button>
<div class="toast" id="toast"></div>
</div>
</div>
<script>
const DEMO = {
cell_storage: [
{
id: 'c1',
motherName: '李安然',
sampleCode: 'ZK260411018',
status: '生效中',
registerDate: '2026-10-11',
motherPhone: '138****8018',
sampleTypeName: '脐带血',
hospitalName: '安徽省妇幼保健院',
salesUserName: '王敏',
sampleCollectTime: '2026-10-11 09:20'
},
{
id: 'c2',
motherName: '周雨桐',
sampleCode: 'ZK260411022',
status: '待生效',
registerDate: '2026-10-10',
motherPhone: '139****6622',
sampleTypeName: '脐带血 + 脐带',
hospitalName: '合肥市妇幼保健院',
salesUserName: '王敏',
sampleCollectTime: '2026-10-10 14:05'
},
{
id: 'c3',
motherName: '陈思琪',
sampleCode: 'ZK260409007',
status: '生效中',
registerDate: '2026-10-09',
motherPhone: '136****9007',
sampleTypeName: '胎盘',
hospitalName: '安医大一附院',
salesUserName: '刘畅',
sampleCollectTime: '2026-10-09 11:40'
}
],
research_donation: [
{
id: 'd1',
motherName: '韩晓雯',
donationNo: 'JZ20261011003',
status: '生效中',
donationDate: '2026-10-11',
motherPhone: '137****3303',
sampleTypeName: '脐带血',
hospitalName: '安徽省立医院',
salesUserName: '王敏',
sampleCollectTime: '2026-10-11 16:10'
},
{
id: 'd2',
motherName: '许若涵',
donationNo: 'JZ20261008011',
status: '已作废',
donationDate: '2026-10-08',
motherPhone: '158****8811',
sampleTypeName: '脐带',
hospitalName: '合肥市妇幼保健院',
salesUserName: '刘畅',
sampleCollectTime: '2026-10-08 08:50'
}
]
}
const state = {
role: 'specialist',
source: 'cell_storage',
keyword: ''
}
const roleTitles = { specialist: '市场专员', manager: '地区经理' }
const listEl = document.getElementById('list')
const toastEl = document.getElementById('toast')
const keywordEl = document.getElementById('keyword')
let toastTimer = null
function esc(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
function showToast(message) {
toastEl.textContent = message
toastEl.classList.add('is-show')
window.clearTimeout(toastTimer)
toastTimer = window.setTimeout(() => toastEl.classList.remove('is-show'), 1600)
}
function statusTone(status) {
if (/作废|拒绝/.test(status)) return 'is-warning'
if (/待/.test(status)) return 'is-info'
return ''
}
function filteredRows() {
const rows = DEMO[state.source] || []
const keyword = state.keyword.trim().toLowerCase()
if (!keyword) return rows
return rows.filter((row) => JSON.stringify(row).toLowerCase().includes(keyword))
}
function field(label, value, wide) {
return `<div class="field${wide ? ' is-wide' : ''}"><span>${esc(label)}</span><b>${value}</b></div>`
}
function renderCard(row, index) {
const isCell = state.source === 'cell_storage'
const subtitle = isCell
? `细胞存储 · ${row.sampleCode || '—'}`
: `科研捐赠 · ${row.donationNo || '—'}`
const dateLabel = isCell ? '登记日期' : '捐赠日期'
const dateValue = isCell ? row.registerDate : row.donationDate
const actions = `
<button type="button" class="is-primary" data-action="view">查看</button>
<button type="button" data-action="edit">编辑</button>
${state.role === 'manager' ? '<button type="button" data-action="audit">审核</button>' : ''}
<button type="button" class="is-danger" data-action="delete">删除</button>
`
return `
<article class="biz-card" data-id="${esc(row.id)}">
<div class="card-head">
<span class="index-dot">${index + 1}</span>
<div class="card-title">
<strong>${esc(row.motherName)}</strong>
<small>${esc(subtitle)}</small>
</div>
<span class="tag ${statusTone(row.status)}">${esc(row.status)}</span>
</div>
<div class="field-grid">
${field('状态', `<span class="tag ${statusTone(row.status)}">${esc(row.status)}</span>`)}
${field(dateLabel, esc(dateValue))}
${field('母亲姓名', esc(row.motherName))}
${field('母亲电话', esc(row.motherPhone))}
${field(isCell ? '样本编码' : '捐赠编号', esc(isCell ? row.sampleCode : row.donationNo), true)}
${field('样本类型', esc(row.sampleTypeName), true)}
${field('采集医院', esc(row.hospitalName), true)}
${field('市场专员', esc(row.salesUserName))}
${field('样本采集时间', esc(row.sampleCollectTime), true)}
</div>
<div class="card-actions">${actions}</div>
</article>
`
}
function updatePlaceholders() {
keywordEl.placeholder =
state.source === 'cell_storage'
? '搜索母亲姓名 / 样本编码'
: '搜索母亲姓名 / 捐赠编号 / 样本编码'
}
function render() {
document.getElementById('pageTitle').textContent = `${roleTitles[state.role]} · 协议单`
document.getElementById('countCell').textContent = String(DEMO.cell_storage.length)
document.getElementById('countDonation').textContent = String(DEMO.research_donation.length)
document.querySelectorAll('.source-tabs__btn').forEach((btn) => {
const active = btn.dataset.source === state.source
btn.classList.toggle('is-active', active)
btn.setAttribute('aria-selected', active ? 'true' : 'false')
})
updatePlaceholders()
const rows = filteredRows()
listEl.innerHTML = rows.length
? rows.map((row, index) => renderCard(row, index)).join('')
: '<div class="empty">该来源下暂无协议单</div>'
}
document.getElementById('roleSwitch').addEventListener('click', (event) => {
const btn = event.target.closest('button[data-role]')
if (!btn) return
state.role = btn.dataset.role
document.querySelectorAll('#roleSwitch button').forEach((item) => {
item.classList.toggle('is-active', item === btn)
})
render()
})
document.querySelector('.source-tabs__track').addEventListener('click', (event) => {
const btn = event.target.closest('button[data-source]')
if (!btn || btn.dataset.source === state.source) return
state.source = btn.dataset.source
state.keyword = ''
keywordEl.value = ''
render()
showToast(state.source === 'cell_storage' ? '已切换到细胞存储' : '已切换到科研捐赠')
})
keywordEl.addEventListener('input', () => {
state.keyword = keywordEl.value
render()
})
listEl.addEventListener('click', (event) => {
const btn = event.target.closest('button[data-action]')
if (!btn) return
const card = btn.closest('.biz-card')
showToast(`${btn.textContent} · ${card?.querySelector('strong')?.textContent || ''}`)
})
document.getElementById('fab').addEventListener('click', () => {
showToast(
state.source === 'cell_storage' ? '新增细胞存储协议单' : '新增科研捐赠协议单'
)
})
render()
</script>
</body>
</html>