| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
- <title>值班抢班 · 公开 H5 Demo</title>
- <style>
- :root {
- --brand: #0a9f6d;
- --brand-dark: #087f57;
- --brand-soft: #e9f9f2;
- --barley: #d6a343;
- --barley-soft: #fff7e8;
- --bg: #f5f8f7;
- --surface: #ffffff;
- --text: #1f2a26;
- --muted: #6b7b74;
- --border: #dcebe4;
- --danger: #d14343;
- --warning: #cf8a19;
- --success: #189b65;
- --am: #cf8a19;
- --am-bg: #fff7e8;
- --pm: #3b82f6;
- --pm-bg: #eff6ff;
- --duty: #d14343;
- --duty-bg: #fef2f2;
- }
- * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
- body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
- background: #dfe8e4;
- color: var(--text);
- min-height: 100vh;
- }
- .chrome {
- position: sticky; top: 0; z-index: 40;
- background: #12241c; color: #d7e8df;
- padding: 10px 16px;
- display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
- }
- .chrome strong { color: #fff; margin-right: 4px; }
- .chrome button {
- border: 1px solid #2a4638; background: #1a3228; color: #d7e8df;
- border-radius: 8px; padding: 6px 12px; font-size: 12px; cursor: pointer;
- }
- .chrome button.active {
- background: var(--brand); border-color: var(--brand); color: #fff; font-weight: 600;
- }
- .chrome .hint { width: 100%; font-size: 11px; color: #8eaa9a; margin-top: 2px; }
- .stage {
- display: flex; justify-content: center; gap: 28px; padding: 28px 16px 48px;
- flex-wrap: wrap; align-items: flex-start;
- }
- .stage-label {
- text-align: center; font-size: 12px; color: #4a6358; margin-bottom: 10px; font-weight: 600;
- }
- .phone {
- width: 375px;
- background: var(--bg);
- border-radius: 32px;
- border: 10px solid #1a2420;
- box-shadow: 0 24px 60px rgba(18, 36, 28, 0.28);
- overflow: hidden;
- min-height: 760px;
- position: relative;
- display: flex;
- flex-direction: column;
- }
- .notch {
- position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
- width: 110px; height: 24px; background: #1a2420; border-radius: 0 0 14px 14px; z-index: 5;
- }
- .page { display: none; flex: 1; flex-direction: column; min-height: 740px; }
- .page.active { display: flex; }
- .header {
- background: linear-gradient(160deg, #0a9f6d 0%, #087f57 100%);
- color: #fff;
- padding: 44px 18px 22px;
- border-radius: 0 0 22px 22px;
- position: relative;
- }
- .header .eyebrow {
- font-size: 12px; opacity: 0.88; margin-bottom: 6px;
- }
- .header h1 {
- margin: 0; font-size: 22px; font-weight: 700; letter-spacing: 0.02em;
- }
- .header .sub {
- margin: 8px 0 0; font-size: 13px; line-height: 1.45; opacity: 0.92;
- }
- .header .meta {
- margin-top: 14px;
- display: flex; gap: 8px; flex-wrap: wrap;
- }
- .pill {
- display: inline-flex; align-items: center; gap: 4px;
- background: rgba(255,255,255,0.18);
- border: 1px solid rgba(255,255,255,0.22);
- border-radius: 999px; padding: 4px 10px; font-size: 11px;
- }
- .body { padding: 14px 14px 28px; flex: 1; }
- .tip {
- background: var(--barley-soft);
- border: 1px solid #f0dfb5;
- color: #8a6418;
- border-radius: 12px;
- padding: 10px 12px;
- font-size: 12px;
- line-height: 1.5;
- margin-bottom: 12px;
- }
- .slot {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: 16px;
- padding: 14px;
- margin-bottom: 12px;
- box-shadow: 0 1px 2px rgba(31, 42, 38, 0.04);
- }
- .slot.is-full { opacity: 0.92; }
- .slot.is-closed { opacity: 0.85; }
- .slot-hd {
- display: flex; align-items: flex-start; justify-content: space-between; gap: 10px;
- }
- .slot-title {
- display: flex; align-items: center; gap: 8px;
- font-size: 16px; font-weight: 700;
- }
- .dot {
- width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0;
- }
- .dot.am { background: var(--am); }
- .dot.pm { background: var(--pm); }
- .dot.duty { background: var(--duty); }
- .badge {
- flex-shrink: 0;
- font-size: 11px; font-weight: 600;
- padding: 3px 8px; border-radius: 999px;
- }
- .badge.open { background: var(--brand-soft); color: var(--brand-dark); }
- .badge.full { background: #eef2f0; color: var(--muted); }
- .badge.closed { background: #fde8e8; color: var(--danger); }
- .cap-row {
- margin-top: 10px;
- display: flex; justify-content: space-between; align-items: baseline;
- font-size: 12px; color: var(--muted);
- }
- .cap-row strong { color: var(--text); font-size: 15px; }
- .bar {
- margin-top: 8px; height: 7px; background: #e8f0ec; border-radius: 999px; overflow: hidden;
- }
- .bar > i {
- display: block; height: 100%; border-radius: 999px; background: var(--brand);
- }
- .bar.full > i { background: #9aabb0; }
- .bar.am > i { background: var(--am); }
- .bar.pm > i { background: var(--pm); }
- .bar.duty > i { background: var(--duty); }
- .people {
- margin-top: 10px;
- display: flex; flex-wrap: wrap; gap: 6px;
- }
- .person {
- font-size: 12px;
- padding: 4px 9px;
- border-radius: 999px;
- background: #f3f7f5;
- color: var(--muted);
- border: 1px solid var(--border);
- }
- .person.empty {
- background: transparent; border-style: dashed; color: #8a9a93;
- }
- .field { margin-top: 12px; }
- .field label {
- display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px;
- }
- .field input {
- width: 100%; height: 44px;
- border: 1px solid var(--border);
- border-radius: 12px;
- padding: 0 12px;
- font-size: 15px;
- background: #fff;
- outline: none;
- }
- .field input:focus { border-color: var(--brand); box-shadow: 0 0 0 3px rgba(10,159,109,0.12); }
- .btn-grab {
- margin-top: 10px;
- width: 100%; height: 44px;
- border: none; border-radius: 12px;
- background: var(--brand); color: #fff;
- font-size: 15px; font-weight: 700;
- cursor: pointer;
- }
- .btn-grab:active { background: var(--brand-dark); }
- .btn-grab:disabled {
- background: #b7c5bf; color: #fff; cursor: not-allowed;
- }
- .btn-grab.secondary {
- background: #fff; color: var(--brand-dark);
- border: 1px solid var(--brand);
- }
- .footer-note {
- text-align: center; font-size: 11px; color: #8a9a93;
- padding: 4px 16px 18px; line-height: 1.5;
- }
- /* Success / closed full-page states */
- .state-panel {
- flex: 1; display: flex; flex-direction: column;
- align-items: center; justify-content: center;
- padding: 40px 28px; text-align: center;
- background: var(--bg);
- }
- .state-icon {
- width: 72px; height: 72px; border-radius: 50%;
- display: flex; align-items: center; justify-content: center;
- font-size: 34px; margin-bottom: 16px;
- }
- .state-icon.ok { background: var(--brand-soft); color: var(--brand); }
- .state-icon.off { background: #eef2f0; color: var(--muted); }
- .state-panel h2 { margin: 0; font-size: 20px; }
- .state-panel p { margin: 10px 0 0; color: var(--muted); font-size: 14px; line-height: 1.55; }
- .state-card {
- width: 100%; margin-top: 22px; text-align: left;
- background: #fff; border: 1px solid var(--border);
- border-radius: 14px; padding: 14px;
- }
- .kv { display: flex; justify-content: space-between; padding: 6px 0; font-size: 13px; }
- .kv span:first-child { color: var(--muted); }
- .kv span:last-child { font-weight: 600; }
- .toast {
- position: absolute; left: 50%; bottom: 28px; transform: translateX(-50%);
- background: rgba(31,42,38,0.92); color: #fff;
- padding: 10px 14px; border-radius: 10px; font-size: 13px;
- white-space: nowrap; display: none; z-index: 20;
- max-width: 90%;
- }
- .toast.show { display: block; animation: fadeUp .2s ease; }
- @keyframes fadeUp {
- from { opacity: 0; transform: translate(-50%, 8px); }
- to { opacity: 1; transform: translate(-50%, 0); }
- }
- @media (max-width: 420px) {
- .stage { padding: 12px 0 24px; }
- .phone {
- width: 100%; border-radius: 0; border: none; min-height: 100vh;
- box-shadow: none;
- }
- .notch { display: none; }
- .chrome { border-radius: 0; }
- }
- </style>
- </head>
- <body>
- <div class="chrome">
- <strong>公开 H5 · 抢班页</strong>
- <button type="button" class="active" data-page="grab" onclick="showPage('grab', this)">可抢班(主流程)</button>
- <button type="button" data-page="success" onclick="showPage('success', this)">提交成功</button>
- <button type="button" data-page="closed" onclick="showPage('closed', this)">链接已关闭</button>
- <button type="button" data-page="invalid" onclick="showPage('invalid', this)">链接无效</button>
- <div class="hint">一天一个链接 · 班次独立罗列 · 免登录只填姓名 · 满员不可抢 · 对齐三墙 Token</div>
- </div>
- <div class="stage">
- <div>
- <div class="stage-label">375px 手机预览 · 点击上方切换状态</div>
- <div class="phone" id="phone">
- <div class="notch"></div>
- <!-- 主流程:可抢 -->
- <div class="page active" id="page-grab">
- <div class="header">
- <div class="eyebrow">山峡化工 · 值班抢班</div>
- <h1>8月20日 值班</h1>
- <p class="sub">无需登录,填写姓名后选择班次提交。名额满了就不能再抢。</p>
- <div class="meta">
- <span class="pill">周三</span>
- <span class="pill">链接有效</span>
- <span class="pill">剩余 2 个名额</span>
- </div>
- </div>
- <div class="body">
- <div class="tip">提示:同名可能重复,请填写真实姓名;提交后如需改动请联系管理员。</div>
- <!-- 早班 已满 -->
- <section class="slot is-full">
- <div class="slot-hd">
- <div class="slot-title"><span class="dot am"></span>早班</div>
- <span class="badge full">已满</span>
- </div>
- <div class="cap-row">
- <span>名额</span>
- <strong>2 / 2</strong>
- </div>
- <div class="bar full am"><i style="width:100%"></i></div>
- <div class="people">
- <span class="person">张伟</span>
- <span class="person">陈静</span>
- </div>
- <button class="btn-grab" type="button" disabled>名额已满</button>
- </section>
- <!-- 晚班 可抢 -->
- <section class="slot" id="slot-pm">
- <div class="slot-hd">
- <div class="slot-title"><span class="dot pm"></span>晚班</div>
- <span class="badge open">可抢</span>
- </div>
- <div class="cap-row">
- <span>名额</span>
- <strong id="pm-count">1 / 2</strong>
- </div>
- <div class="bar pm"><i id="pm-bar" style="width:50%"></i></div>
- <div class="people" id="pm-people">
- <span class="person">李娜</span>
- <span class="person empty">空缺 1</span>
- </div>
- <div class="field">
- <label for="name-pm">您的姓名</label>
- <input id="name-pm" type="text" maxlength="20" placeholder="请输入真实姓名" autocomplete="name" />
- </div>
- <button class="btn-grab" type="button" onclick="submitGrab('pm')">提交抢班</button>
- </section>
- <!-- 值班 空 -->
- <section class="slot" id="slot-duty">
- <div class="slot-hd">
- <div class="slot-title"><span class="dot duty"></span>值班</div>
- <span class="badge open">可抢</span>
- </div>
- <div class="cap-row">
- <span>名额</span>
- <strong id="duty-count">0 / 1</strong>
- </div>
- <div class="bar duty"><i id="duty-bar" style="width:0%"></i></div>
- <div class="people" id="duty-people">
- <span class="person empty">暂无人报名</span>
- </div>
- <div class="field">
- <label for="name-duty">您的姓名</label>
- <input id="name-duty" type="text" maxlength="20" placeholder="请输入真实姓名" autocomplete="name" />
- </div>
- <button class="btn-grab" type="button" onclick="submitGrab('duty')">提交抢班</button>
- </section>
- </div>
- <div class="footer-note">提交即表示确认当日可到岗值班<br />链接由管理员分享 · 请勿转发无关人员</div>
- </div>
- <!-- 成功页 -->
- <div class="page" id="page-success">
- <div class="header" style="padding-bottom:28px">
- <div class="eyebrow">山峡化工 · 值班抢班</div>
- <h1>报名成功</h1>
- <p class="sub">请按时到岗。如需调整请联系管理员。</p>
- </div>
- <div class="state-panel" style="justify-content:flex-start;padding-top:28px">
- <div class="state-icon ok">✓</div>
- <h2>已抢到晚班</h2>
- <p>2026年8月20日(周三)</p>
- <div class="state-card">
- <div class="kv"><span>日期</span><span>2026-08-20</span></div>
- <div class="kv"><span>班次</span><span>晚班</span></div>
- <div class="kv"><span>姓名</span><span>王小明</span></div>
- <div class="kv"><span>状态</span><span style="color:var(--success)">已报名</span></div>
- </div>
- <button class="btn-grab secondary" style="margin-top:18px;max-width:280px" type="button" onclick="showPage('grab')">返回查看当日班次</button>
- </div>
- </div>
- <!-- 链接关闭 -->
- <div class="page" id="page-closed">
- <div class="state-panel">
- <div class="state-icon off">⊘</div>
- <h2>报名已关闭</h2>
- <p>管理员已关闭本链接,暂时无法继续抢班。<br />如需值班请联系管理员另行安排。</p>
- <div class="state-card">
- <div class="kv"><span>日期</span><span>2026-08-20</span></div>
- <div class="kv"><span>状态</span><span style="color:var(--danger)">已关闭</span></div>
- </div>
- </div>
- </div>
- <!-- 链接无效 -->
- <div class="page" id="page-invalid">
- <div class="state-panel">
- <div class="state-icon off">?</div>
- <h2>链接无效或已失效</h2>
- <p>请确认是否复制完整,或向管理员重新索取当日值班链接。</p>
- </div>
- </div>
- <div class="toast" id="toast"></div>
- </div>
- </div>
- </div>
- <script>
- const slots = {
- pm: { taken: 1, cap: 2, names: ['李娜'] },
- duty: { taken: 0, cap: 1, names: [] }
- };
- function showPage(id, btn) {
- document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
- document.getElementById('page-' + id).classList.add('active');
- document.querySelectorAll('.chrome button').forEach(b => b.classList.remove('active'));
- if (btn) btn.classList.add('active');
- else {
- const b = document.querySelector('.chrome button[data-page="' + id + '"]');
- if (b) b.classList.add('active');
- }
- }
- function toast(msg) {
- const t = document.getElementById('toast');
- t.textContent = msg;
- t.classList.add('show');
- clearTimeout(toast._timer);
- toast._timer = setTimeout(() => t.classList.remove('show'), 1800);
- }
- function renderSlot(key) {
- const s = slots[key];
- const countEl = document.getElementById(key + '-count');
- const barEl = document.getElementById(key + '-bar');
- const peopleEl = document.getElementById(key + '-people');
- const section = document.getElementById('slot-' + key);
- if (!countEl) return;
- countEl.textContent = s.taken + ' / ' + s.cap;
- barEl.style.width = Math.round((s.taken / s.cap) * 100) + '%';
- peopleEl.innerHTML = '';
- s.names.forEach(n => {
- const span = document.createElement('span');
- span.className = 'person';
- span.textContent = n;
- peopleEl.appendChild(span);
- });
- const left = s.cap - s.taken;
- if (left > 0) {
- const empty = document.createElement('span');
- empty.className = 'person empty';
- empty.textContent = s.taken === 0 ? '暂无人报名' : ('空缺 ' + left);
- peopleEl.appendChild(empty);
- }
- const badge = section.querySelector('.badge');
- const btn = section.querySelector('.btn-grab');
- const field = section.querySelector('.field');
- const bar = section.querySelector('.bar');
- if (s.taken >= s.cap) {
- badge.className = 'badge full';
- badge.textContent = '已满';
- btn.disabled = true;
- btn.textContent = '名额已满';
- if (field) field.style.display = 'none';
- section.classList.add('is-full');
- bar.classList.add('full');
- } else {
- badge.className = 'badge open';
- badge.textContent = '可抢';
- btn.disabled = false;
- btn.textContent = '提交抢班';
- if (field) field.style.display = '';
- section.classList.remove('is-full');
- bar.classList.remove('full');
- }
- }
- function submitGrab(key) {
- const input = document.getElementById('name-' + key);
- const name = (input.value || '').trim();
- if (!name) {
- toast('请先填写姓名');
- input.focus();
- return;
- }
- const s = slots[key];
- if (s.taken >= s.cap) {
- toast('该班次名额已满');
- renderSlot(key);
- return;
- }
- s.names.push(name);
- s.taken += 1;
- input.value = '';
- renderSlot(key);
- toast(name + ' 已提交成功');
- setTimeout(() => showPage('success'), 600);
- }
- renderSlot('pm');
- renderSlot('duty');
- </script>
- </body>
- </html>
|