Files
shanxia2.0/docs/superpowers/demos/2026-10-16-web-admin-version-refresh-demo.html
T

562 lines
17 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>web-admin · 发版后提示刷新 Demo</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--primary: #10b981;
--primary-dark: #059669;
--primary-bg: #ecfdf5;
--text: #1e293b;
--text-muted: #64748b;
--border: #d9e8e2;
--bg-page: #fefefe;
--surface: #ffffff;
--table-head: #f7faf8;
--row-alt: #f1f5f9;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
--radius: 10px;
--warn: #d97706;
--warn-bg: #fffbeb;
--mask: rgba(15, 23, 42, 0.35);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: Inter, -apple-system, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text);
background: #0f172a;
min-height: 100vh;
padding: 28px 20px 48px;
}
.page-head {
max-width: 1100px;
margin: 0 auto 20px;
text-align: center;
color: #fff;
}
.page-head h1 { font-size: 20px; font-weight: 600; margin-bottom: 8px; }
.page-head p {
color: #94a3b8;
font-size: 13px;
max-width: 640px;
margin: 0 auto;
}
.controls {
max-width: 1100px;
margin: 0 auto 16px;
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
}
.controls button {
appearance: none;
border: 1px solid #334155;
background: #1e293b;
color: #e2e8f0;
border-radius: 8px;
padding: 8px 14px;
font-size: 13px;
font-family: inherit;
cursor: pointer;
transition: border-color .15s, background .15s;
}
.controls button:hover { border-color: var(--primary); }
.controls button.is-active {
background: var(--primary-bg);
border-color: var(--primary);
color: var(--primary-dark);
font-weight: 600;
}
.stage-wrap {
max-width: 1100px;
margin: 0 auto;
border-radius: 14px;
overflow: hidden;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
border: 1px solid #334155;
background: var(--bg-page);
position: relative;
min-height: 560px;
}
/* —— 仿管理端壳 —— */
.shell { display: flex; min-height: 560px; }
.sidebar {
width: 200px;
flex-shrink: 0;
background: var(--surface);
border-right: 1px solid var(--border);
padding: 20px 14px;
}
.brand {
font-weight: 700;
font-size: 15px;
color: var(--primary-dark);
margin-bottom: 20px;
padding: 0 6px;
}
.nav-item {
padding: 9px 10px;
border-radius: 8px;
color: var(--text-muted);
font-size: 13px;
margin-bottom: 4px;
}
.nav-item.is-active {
background: var(--primary-bg);
color: var(--primary-dark);
font-weight: 600;
}
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.topbar {
height: 52px;
border-bottom: 1px solid var(--border);
background: var(--surface);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
font-size: 13px;
color: var(--text-muted);
}
.topbar .user { color: var(--text); font-weight: 500; }
.content { padding: 20px; flex: 1; }
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.panel-top {
padding: 14px 16px;
border-bottom: 1px solid var(--border);
background: var(--table-head);
}
.panel-top h2 {
font-size: 15px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.panel-top h2::before {
content: "";
width: 3px;
height: 14px;
border-radius: 2px;
background: var(--primary);
}
.toolbar {
padding: 10px 16px;
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid var(--border);
font-size: 13px;
color: var(--text-muted);
}
.btn {
appearance: none;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
border-radius: 8px;
height: 32px;
padding: 0 12px;
font-size: 13px;
font-family: inherit;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn-main {
background: var(--primary);
border-color: var(--primary);
color: #fff;
font-weight: 500;
}
.btn-main:hover { background: var(--primary-dark); }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { padding: 10px 14px; text-align: left; border-bottom: 1px solid var(--border); }
th { background: var(--table-head); color: var(--text-muted); font-weight: 500; }
tr:nth-child(even) td { background: var(--row-alt); }
.tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
background: #059669;
color: #fff;
}
.version-chip {
font-size: 11px;
color: var(--text-muted);
background: var(--row-alt);
border: 1px solid var(--border);
border-radius: 999px;
padding: 2px 8px;
}
/* —— A:强制弹窗 —— */
.overlay {
position: absolute;
inset: 0;
background: var(--mask);
display: none;
align-items: center;
justify-content: center;
z-index: 40;
backdrop-filter: none;
}
.overlay.is-on { display: flex; }
.dialog {
width: 420px;
max-width: calc(100% - 40px);
background: var(--surface);
border-radius: var(--radius);
box-shadow: 0 20px 48px rgba(15, 23, 42, 0.18);
overflow: hidden;
animation: pop .2s ease-out;
}
@keyframes pop {
from { opacity: 0; transform: translateY(8px) scale(0.98); }
to { opacity: 1; transform: none; }
}
.dialog-body { padding: 24px 24px 8px; }
.dialog-icon {
width: 44px;
height: 44px;
border-radius: 12px;
background: var(--primary-bg);
color: var(--primary-dark);
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
margin-bottom: 14px;
}
.dialog-body h3 { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
.dialog-body p { color: var(--text-muted); font-size: 13px; margin-bottom: 6px; }
.dialog-meta {
margin-top: 12px;
padding: 10px 12px;
background: var(--table-head);
border-radius: 8px;
font-size: 12px;
color: var(--text-muted);
display: flex;
justify-content: space-between;
gap: 12px;
}
.dialog-meta strong { color: var(--text); font-weight: 600; }
.dialog-footer {
padding: 16px 24px 20px;
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* —— B:顶部横幅 —— */
.banner {
display: none;
align-items: center;
gap: 12px;
padding: 10px 16px;
background: var(--warn-bg);
border-bottom: 1px solid rgba(217, 119, 6, 0.25);
color: #92400e;
font-size: 13px;
z-index: 30;
}
.banner.is-on { display: flex; }
.banner .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--warn);
flex-shrink: 0;
animation: pulse 1.4s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
.banner .msg { flex: 1; min-width: 0; }
.banner .msg strong { color: #b45309; }
.banner .actions { display: flex; gap: 8px; flex-shrink: 0; }
.btn-warn {
background: var(--warn);
border-color: var(--warn);
color: #fff;
height: 28px;
padding: 0 10px;
font-size: 12px;
}
.btn-ghost {
background: transparent;
border-color: rgba(217, 119, 6, 0.35);
color: #92400e;
height: 28px;
padding: 0 10px;
font-size: 12px;
}
/* —— C:右下角通知 —— */
.toast {
position: absolute;
right: 20px;
bottom: 20px;
width: 360px;
max-width: calc(100% - 40px);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
padding: 14px 16px;
display: none;
z-index: 35;
animation: slideUp .22s ease-out;
}
.toast.is-on { display: block; }
@keyframes slideUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: none; }
}
.toast-top {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
}
.toast-icon {
width: 32px;
height: 32px;
border-radius: 8px;
background: var(--primary-bg);
color: var(--primary-dark);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 16px;
}
.toast h4 { font-size: 14px; font-weight: 600; }
.toast p { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
.toast-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.toast .btn { height: 28px; font-size: 12px; padding: 0 10px; }
.legend {
max-width: 1100px;
margin: 18px auto 0;
color: #94a3b8;
font-size: 12px;
line-height: 1.7;
text-align: center;
}
.legend code {
background: #1e293b;
color: #cbd5e1;
padding: 1px 6px;
border-radius: 4px;
font-size: 11px;
}
@media (max-width: 800px) {
.sidebar { display: none; }
.banner { flex-wrap: wrap; }
}
</style>
</head>
<body>
<div class="page-head">
<h1>web-admin · 发版后「请刷新」交互 Demo</h1>
<p>
模拟客户长时间不关标签页、仍跑旧前端的场景。下面三种提示样式可切换对比;
Token 对齐 Fresh Mint(<code style="color:#a7f3d0">#10b981</code>)。
</p>
</div>
<div class="controls" role="tablist" aria-label="提示样式">
<button type="button" class="is-active" data-mode="force" aria-pressed="true">A · 强制弹窗(推荐)</button>
<button type="button" data-mode="banner" aria-pressed="false">B · 顶部横幅(可稍后)</button>
<button type="button" data-mode="toast" aria-pressed="false">C · 右下角通知</button>
<button type="button" data-mode="none" aria-pressed="false">隐藏提示</button>
</div>
<div class="stage-wrap" id="stage">
<!-- B: banner sits above shell content -->
<div class="banner" id="banner" role="status">
<span class="dot" aria-hidden="true"></span>
<div class="msg">
<strong>系统已更新</strong>
— 检测到新版本 <strong>3.0.3</strong>(当前页面仍为 3.0.2),请刷新以使用最新功能。
</div>
<div class="actions">
<button type="button" class="btn btn-ghost" data-action="later">稍后</button>
<button type="button" class="btn btn-warn" data-action="reload">立即刷新</button>
</div>
</div>
<div class="shell">
<aside class="sidebar">
<div class="brand">山峡化工 · 门户</div>
<div class="nav-item is-active">订单列表</div>
<div class="nav-item">商品管理</div>
<div class="nav-item">单位管理</div>
<div class="nav-item">系统用户</div>
</aside>
<div class="main">
<header class="topbar">
<span>工作台 / 订单列表</span>
<span>
<span class="version-chip" title="页面内嵌版本">页面 v3.0.2</span>
&nbsp;&nbsp;
<span class="user">张三</span>
</span>
</header>
<div class="content">
<div class="panel">
<div class="panel-top"><h2>订单列表</h2></div>
<div class="toolbar">
<span>已选 0 项</span>
<button type="button" class="btn btn-main">新增订单</button>
<button type="button" class="btn">导出</button>
</div>
<table>
<thead>
<tr>
<th>订单号</th>
<th>客户</th>
<th>状态</th>
<th>金额</th>
</tr>
</thead>
<tbody>
<tr>
<td>SO20261016001</td>
<td>华中物流</td>
<td><span class="tag">配送中</span></td>
<td>¥12,800</td>
</tr>
<tr>
<td>SO20261016002</td>
<td>海河化工</td>
<td><span class="tag">已完成</span></td>
<td>¥6,420</td>
</tr>
<tr>
<td>SO20261015088</td>
<td>金桥供应链</td>
<td><span class="tag">待发货</span></td>
<td>¥9,150</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- A: force modal -->
<div class="overlay is-on" id="overlay" role="dialog" aria-modal="true" aria-labelledby="dlg-title">
<div class="dialog">
<div class="dialog-body">
<div class="dialog-icon" aria-hidden="true">↻</div>
<h3 id="dlg-title">发现新版本</h3>
<p>系统已发布更新,当前页面仍在使用旧功能。请刷新后继续操作,避免数据与界面不一致。</p>
<div class="dialog-meta">
<span>当前:<strong>v3.0.2</strong></span>
<span>最新:<strong>v3.0.3</strong></span>
</div>
</div>
<div class="dialog-footer">
<!-- 强制模式:无「取消 / 稍后」 -->
<button type="button" class="btn btn-main" data-action="reload">立即刷新</button>
</div>
</div>
</div>
<!-- C: corner toast -->
<div class="toast" id="toast" role="status">
<div class="toast-top">
<div class="toast-icon" aria-hidden="true">↻</div>
<div>
<h4>有可用更新</h4>
<p>新版本 v3.0.3 已上线,刷新即可使用最新功能。</p>
</div>
</div>
<div class="toast-actions">
<button type="button" class="btn" data-action="later">稍后</button>
<button type="button" class="btn btn-main" data-action="reload">立即刷新</button>
</div>
</div>
</div>
<p class="legend">
<strong style="color:#e2e8f0">对比要点</strong>:
A 强制弹窗不可稍后(与小程序强制更新一致,适合破坏性发版);
B 顶部横幅不打断当前填表,适合非紧急;
C 右下角最轻,容易被忽略。
检测侧通常对比本地 <code>__APP_VERSION__</code> / 构建产物 hash 与服务器上的 <code>version.json</code>。
</p>
<script>
(function () {
var overlay = document.getElementById('overlay')
var banner = document.getElementById('banner')
var toast = document.getElementById('toast')
var buttons = document.querySelectorAll('.controls button[data-mode]')
function setMode(mode) {
overlay.classList.toggle('is-on', mode === 'force')
banner.classList.toggle('is-on', mode === 'banner')
toast.classList.toggle('is-on', mode === 'toast')
buttons.forEach(function (btn) {
var on = btn.getAttribute('data-mode') === mode
btn.classList.toggle('is-active', on)
btn.setAttribute('aria-pressed', on ? 'true' : 'false')
})
}
buttons.forEach(function (btn) {
btn.addEventListener('click', function () {
setMode(btn.getAttribute('data-mode'))
})
})
document.getElementById('stage').addEventListener('click', function (e) {
var t = e.target
if (!(t instanceof HTMLElement)) return
var action = t.getAttribute('data-action')
if (action === 'reload') {
alert('Demo:实际实现会执行 location.reload()(或带 cache-bust 的硬刷新)')
}
if (action === 'later') {
setMode('none')
}
})
})()
</script>
</body>
</html>