Files
zkxw/mobile-ui/zkxw-mobile-oa-manage-capsule-safe-demo.html
T
2026-08-13 16:50:36 +08:00

665 lines
21 KiB
HTML
Raw Permalink 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, maximum-scale=1" />
<title>OA 列表 ·「管理」避开微信胶囊 Demo</title>
<style>
:root {
--brand: #0a9f6d;
--brand-dark: #087f57;
--brand-soft: #e9f9f2;
--warn: #d97706;
--warn-soft: #fff7e8;
--danger: #ef4444;
--nav-bg: #ecfdf5;
--page-bg: #f5f8f7;
--text: #1f2a26;
--text-muted: #6b7b74;
--border: #e8eaed;
--capsule-w: 87px;
--capsule-h: 32px;
--capsule-right: 7px;
--capsule-gap: 8px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
color: var(--text);
background:
radial-gradient(900px 420px at 8% -8%, rgba(10, 159, 109, 0.12), transparent 55%),
linear-gradient(180deg, #f8fffc 0%, #f4f7fb 50%, #f7f8fa 100%);
line-height: 1.45;
}
.stage { max-width: 1240px; margin: 0 auto; padding: 28px 16px 64px; }
.hero h1 { margin: 0 0 8px; font-size: 22px; font-weight: 720; }
.hero p { margin: 0; color: #64748b; font-size: 14px; max-width: 880px; }
.rec {
margin: 16px 0 0;
padding: 14px 16px;
border-radius: 14px;
background: var(--brand-soft);
border: 1px solid #b7ebd4;
font-size: 13px;
color: #166534;
line-height: 1.55;
}
.rec b { color: var(--brand-dark); }
.tabs {
display: flex; flex-wrap: wrap; gap: 10px;
margin: 20px 0 18px;
}
.tab {
appearance: none; border: 1px solid #e2e8f0; background: #fff;
border-radius: 999px; padding: 10px 16px; font-size: 13px;
font-weight: 600; color: #475569; cursor: pointer;
}
.tab.is-active {
color: var(--brand-dark); border-color: var(--brand);
background: var(--brand-soft);
box-shadow: 0 0 0 3px rgba(10, 159, 109, 0.16);
}
.tab .badge {
margin-left: 6px; font-size: 11px; font-weight: 700;
color: var(--brand-dark); background: #d1fae5;
border-radius: 999px; padding: 2px 7px;
}
.tab .badge.warn {
color: #92400e; background: #fef3c7;
}
.layout {
display: grid;
grid-template-columns: minmax(240px, 320px) 1fr;
gap: 20px; align-items: start;
}
@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
.panel {
background: rgba(255,255,255,.94);
border: 1px solid #e2e8f0; border-radius: 16px; padding: 16px;
box-shadow: 0 10px 28px rgba(15,23,42,.05);
}
.panel h2 { margin: 0 0 4px; font-size: 15px; }
.panel .sub { margin: 0 0 10px; font-size: 12px; color: #64748b; }
.panel ul { margin: 0; padding-left: 18px; font-size: 13px; color: #475569; }
.panel li { margin: 6px 0; }
.panel .pick {
margin-top: 12px; padding: 10px 12px; border-radius: 10px;
background: #fff7e8; border: 1px solid #fde68a;
font-size: 12px; color: #92400e;
}
.panel .ok {
margin-top: 10px; padding: 10px 12px; border-radius: 10px;
background: #ecfdf5; border: 1px solid #a7f3d0;
font-size: 12px; color: #047857;
}
.panel .bad {
margin-top: 10px; padding: 10px 12px; border-radius: 10px;
background: #fef2f2; border: 1px solid #fecaca;
font-size: 12px; color: #b91c1c;
}
.phones { display: flex; flex-wrap: wrap; gap: 18px; justify-content: center; }
.phone {
width: 375px; background: #fff; border-radius: 28px; overflow: hidden;
box-shadow: 0 0 0 1px rgb(15 23 42 / 6%), 0 18px 40px rgb(15 23 42 / 12%);
position: relative;
}
.cap {
text-align: center; font-size: 11px; font-weight: 650; color: #64748b;
padding: 8px; background: #f8fafc; border-bottom: 1px solid #eef2f0;
}
/* 模拟系统状态栏 + 自定义导航 */
.sys-bar {
height: 44px;
background: var(--nav-bg);
display: flex;
align-items: flex-end;
justify-content: space-between;
padding: 0 16px 6px;
font-size: 12px;
font-weight: 650;
color: #0f291e;
position: relative;
}
.sys-bar__time { font-variant-numeric: tabular-nums; }
.sys-bar__icons { opacity: 0.75; letter-spacing: 2px; font-size: 11px; }
.nav-wrap {
background: var(--nav-bg);
border-bottom: 1px solid #d8f3e5;
position: relative;
}
.nav {
height: 44px;
display: flex;
align-items: center;
padding: 0 8px 0 4px;
position: relative;
}
.nav.is-capsule-safe {
padding-right: calc(var(--capsule-w) + var(--capsule-right) + var(--capsule-gap));
}
.nav-back {
width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
color: #0f291e; flex-shrink: 0;
}
.nav-back::before {
content: ""; width: 10px; height: 10px;
border-left: 2px solid #0f291e; border-bottom: 2px solid #0f291e;
transform: rotate(45deg); margin-left: 4px;
}
.nav-title {
position: absolute; left: 50%; transform: translateX(-50%);
font-size: 16px; font-weight: 650; color: #0f291e;
white-space: nowrap;
}
.nav-action {
margin-left: auto;
min-width: 48px;
height: 36px;
display: flex; align-items: center; justify-content: center;
padding: 0 8px;
font-size: 14px; font-weight: 650; color: #166534;
cursor: pointer; user-select: none;
position: relative; z-index: 2;
}
.nav-action.is-done { color: #64748b; }
.nav-action.is-hit {
outline: 2px solid var(--danger);
outline-offset: 1px;
border-radius: 6px;
background: rgba(239, 68, 68, 0.08);
}
.nav-action.is-safe {
outline: 2px solid var(--brand);
outline-offset: 1px;
border-radius: 6px;
background: rgba(10, 159, 109, 0.08);
}
/* 微信右上角胶囊(模拟) */
.wx-capsule {
position: absolute;
top: 6px;
right: var(--capsule-right);
width: var(--capsule-w);
height: var(--capsule-h);
border-radius: 16px;
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(0, 0, 0, 0.12);
box-shadow: 0 1px 2px rgba(0,0,0,.04);
display: flex;
align-items: center;
z-index: 50;
pointer-events: none;
overflow: hidden;
}
.wx-capsule__dots,
.wx-capsule__close {
flex: 1;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #111;
font-size: 13px;
font-weight: 700;
}
.wx-capsule__dots { gap: 3px; }
.wx-capsule__dots span {
width: 4px; height: 4px; border-radius: 50%; background: #111;
}
.wx-capsule__divider {
width: 1px; height: 18px; background: rgba(0,0,0,.12);
}
.wx-capsule__close { font-size: 15px; line-height: 1; }
.wx-capsule.is-warn {
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.45);
}
.wx-capsule.is-ok {
box-shadow: 0 0 0 2px rgba(10, 159, 109, 0.35);
}
.hit-label {
position: absolute;
top: 46px;
right: 10px;
z-index: 40;
font-size: 10px;
font-weight: 700;
padding: 3px 8px;
border-radius: 999px;
background: #fee2e2;
color: #b91c1c;
border: 1px solid #fecaca;
}
.safe-label {
position: absolute;
top: 46px;
right: 10px;
z-index: 40;
font-size: 10px;
font-weight: 700;
padding: 3px 8px;
border-radius: 999px;
background: #d1fae5;
color: #047857;
border: 1px solid #a7f3d0;
}
.toolbar {
background: #fff;
padding: 8px 12px;
border-bottom: 1px solid var(--border);
}
.search {
height: 36px;
border-radius: 10px;
border: 1px solid #e8eaed;
background: #fafafa;
display: flex;
align-items: center;
padding: 0 12px;
gap: 8px;
color: #94a3b8;
font-size: 13px;
}
.search::before {
content: "";
width: 14px; height: 14px;
border: 2px solid #94a3b8;
border-radius: 50%;
box-sizing: border-box;
position: relative;
}
.toolbar-row {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-row .search { flex: 1; }
.manage-chip {
flex-shrink: 0;
height: 36px;
padding: 0 12px;
border-radius: 10px;
border: 1px solid #a7f3d0;
background: #ecfdf5;
color: #166534;
font-size: 13px;
font-weight: 650;
display: flex;
align-items: center;
cursor: pointer;
}
.manage-chip.is-done {
border-color: #e2e8f0;
background: #f8fafc;
color: #64748b;
}
.list-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px 6px;
background: var(--page-bg);
}
.list-head__count {
font-size: 12px;
color: var(--text-muted);
}
.list-head__count strong { color: var(--text); font-weight: 700; }
.list-head__link {
font-size: 13px;
font-weight: 650;
color: #166534;
cursor: pointer;
padding: 4px 2px;
}
.list-head__link.is-done { color: #64748b; }
.list-head__link.is-safe {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: 6px;
background: rgba(10, 159, 109, 0.08);
}
.list {
background: var(--page-bg);
padding: 8px 12px 20px;
min-height: 320px;
}
.card {
background: #fff;
border-radius: 14px;
border: 1px solid #eef2f0;
padding: 12px 14px;
margin-bottom: 10px;
box-shadow: 0 4px 12px rgba(15,23,42,.04);
}
.card__title {
font-size: 14px; font-weight: 650; color: #0f291e;
margin-bottom: 6px;
}
.card__meta { font-size: 12px; color: var(--text-muted); }
.card.is-manage {
display: flex; align-items: center; gap: 10px;
}
.check {
width: 20px; height: 20px; border-radius: 50%;
border: 2px solid #cbd5e1; flex-shrink: 0;
}
.check.on {
border-color: var(--brand);
background: var(--brand);
box-shadow: inset 0 0 0 3px #fff;
}
.card__body { flex: 1; min-width: 0; }
.legend {
margin-top: 16px;
display: flex; flex-wrap: wrap; gap: 10px;
font-size: 12px; color: #64748b;
}
.legend span {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 10px; border-radius: 8px; background: #fff;
border: 1px solid #e2e8f0;
}
.dot {
width: 10px; height: 10px; border-radius: 50%;
}
.dot.hit { background: #ef4444; }
.dot.ok { background: #0a9f6d; }
.dot.cap { background: #94a3b8; border: 1px solid #64748b; }
.note {
margin-top: 18px;
font-size: 12px;
color: #64748b;
max-width: 880px;
}
code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
background: #f1f5f9;
padding: 1px 5px;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="stage">
<div class="hero">
<h1>OA 列表 ·「管理」避开微信小程序胶囊</h1>
<p>
影响页面:自定义导航的 <b>草稿箱 / 待办中心 / 抄送我的</b>
(已办中心、我发起的目前用系统导航且无「管理」)。
右上角模拟微信「··· / 关闭」胶囊;红框=被遮挡,绿框=可点区域安全。
</p>
<div class="rec">
<b>建议:</b>优先选 <b>A · 顶栏保留「管理」+ 按胶囊宽度留白</b>
(改动最小,交互习惯不变)。若希望管理与列表语义更近,可选
<b>B · 列表头「共 N 条 | 管理」</b>。
</div>
</div>
<div class="tabs" id="tabs">
<button type="button" class="tab is-active" data-k="now">① 现状·被遮挡<span class="badge warn">问题</span></button>
<button type="button" class="tab" data-k="a">② A · 顶栏留白<span class="badge">推荐</span></button>
<button type="button" class="tab" data-k="b">③ B · 列表头管理</button>
<button type="button" class="tab" data-k="c">④ C · 搜索旁胶囊</button>
</div>
<div class="layout">
<aside class="panel" id="info">
<h2>—</h2>
<p class="sub">—</p>
<ul></ul>
<div class="pick" hidden></div>
<div class="ok" hidden></div>
<div class="bad" hidden></div>
</aside>
<div>
<div class="phones" id="phones"></div>
<div class="legend">
<span><i class="dot cap"></i>微信胶囊(不可覆盖)</span>
<span><i class="dot hit"></i>「管理」与胶囊冲突</span>
<span><i class="dot ok"></i>「管理」可安全点击</span>
</div>
</div>
</div>
<p class="note">
实现提示:方案 A 用 <code>uni.getMenuButtonBoundingClientRect()</code>
(或微信 <code>wx.getMenuButtonBoundingClientRect</code>)取胶囊
<code>width / left / right</code>,给 <code>.nav__inner</code> 增加
<code>padding-right</code>,把「管理」推到胶囊左侧并留约 8px 间隙。
非微信环境可回退为固定约 96px 右内边距。
</p>
</div>
<script>
var SCENES = {
now: {
title: "① 现状 · 顶栏右侧「管理」被胶囊遮住",
sub: "自定义 navigationStyle,仅处理了 statusBar,未避开胶囊。",
items: [
"「管理」贴在导航栏最右侧(margin-left: auto)",
"微信胶囊叠在同一垂直带上,文字被挡住、难以点击",
"草稿箱 / 待办 / 抄送三页结构相同"
],
bad: "不要继续这样落地:真机上关闭区会盖住「管理」。",
phoneCap: "现状 · 冲突",
mode: "now"
},
a: {
title: "② A · 顶栏保留「管理」+ 胶囊安全留白(推荐)",
sub: "仍在标题栏右侧,但右侧 padding 避开胶囊宽度。",
items: [
"交互位置不变:用户仍在顶栏找「管理 / 完成」",
"用 menuButtonBoundingClientRect 动态算右侧安全区",
"改动集中在公共顶栏样式 / 工具函数,三页可复用",
"标题仍居中,不会挤到胶囊上"
],
ok: "推荐:最小改动、符合现有 Demo 约定(顶栏管理)。",
phoneCap: "方案 A · 顶栏留白",
mode: "a"
},
b: {
title: "③ B · 列表头「共 N 条 | 管理」",
sub: "顶栏只留返回+标题;管理下移到搜索下方次级行。",
items: [
"彻底避开胶囊,无需计算胶囊宽度",
"管理与列表批量操作语义更近(对齐早期美化 Demo S1)",
"顶栏更干净,但入口位置相对现状下移",
"三页需改模板结构(多一行 list-head)"
],
pick: "想弱化顶栏操作、强调列表批量时选 B。",
phoneCap: "方案 B · 列表头",
mode: "b"
},
c: {
title: "④ C · 搜索框右侧胶囊按钮",
sub: "顶栏无「管理」;与搜索同排的描边小按钮。",
items: [
"同样避开胶囊,实现简单",
"搜索区会变窄,长 placeholder 更易截断",
"早期 Demo 曾认为「挤在搜索旁偏突兀」"
],
pick: "仅作备选;一般不如 A / B。",
phoneCap: "方案 C · 搜索旁",
mode: "c"
}
}
var cards = [
{ title: "差旅报销申请", meta: "张三 · 技术中心 · 2 小时前" },
{ title: "采购合同审批", meta: "李四 · 采购部 · 昨天" },
{ title: "请假申请", meta: "王五 · 行政 · 周一" }
]
function wxCapsule(extraClass) {
return (
'<div class="wx-capsule ' + (extraClass || "") + '" aria-hidden="true">' +
'<div class="wx-capsule__dots"><span></span><span></span><span></span></div>' +
'<div class="wx-capsule__divider"></div>' +
'<div class="wx-capsule__close">✕</div>' +
'</div>'
)
}
function cardHtml(manage) {
return cards.map(function (c, i) {
if (!manage) {
return (
'<div class="card">' +
'<div class="card__title">' + c.title + '</div>' +
'<div class="card__meta">' + c.meta + '</div>' +
'</div>'
)
}
return (
'<div class="card is-manage">' +
'<div class="check' + (i === 0 ? ' on' : '') + '"></div>' +
'<div class="card__body">' +
'<div class="card__title">' + c.title + '</div>' +
'<div class="card__meta">' + c.meta + '</div>' +
'</div>' +
'</div>'
)
}).join("")
}
function phoneShell(label, bodyInner, flag) {
var labelHtml = ""
if (flag === "hit") labelHtml = '<div class="hit-label">被胶囊遮挡</div>'
if (flag === "safe") labelHtml = '<div class="safe-label">可安全点击</div>'
return (
'<div class="phone">' +
'<div class="cap">' + label + '</div>' +
'<div class="sys-bar">' +
'<span class="sys-bar__time">9:41</span>' +
'<span class="sys-bar__icons">▮▮▮ ░</span>' +
'</div>' +
'<div class="nav-wrap">' +
bodyInner +
(flag === "hit" ? wxCapsule("is-warn") : wxCapsule("is-ok")) +
labelHtml +
'</div>' +
'</div>'
)
}
function setInfo(key) {
var s = SCENES[key]
var info = document.getElementById("info")
info.querySelector("h2").textContent = s.title
info.querySelector(".sub").textContent = s.sub
info.querySelector("ul").innerHTML = s.items
.map(function (t) { return "<li>" + t + "</li>" })
.join("")
;["pick", "ok", "bad"].forEach(function (cls) {
var el = info.querySelector("." + cls)
if (s[cls]) {
el.hidden = false
el.textContent = s[cls]
} else {
el.hidden = true
el.textContent = ""
}
})
}
function buildPhone(mode, manage) {
var actionText = manage ? "完成" : "管理"
var actionCls = manage ? "nav-action is-done" : "nav-action"
var list = '<div class="list">' + cardHtml(manage) + "</div>"
var inner = ""
var flag = "safe"
var label = ""
if (mode === "now") {
flag = "hit"
label = manage ? "现状 · 管理态(冲突)" : "现状 · 普通态(冲突)"
inner =
'<div class="nav">' +
'<div class="nav-back"></div>' +
'<div class="nav-title">待办中心</div>' +
'<div class="' + actionCls + ' is-hit">' + actionText + "</div>" +
"</div>" +
'<div class="toolbar"><div class="search">搜索流程名称、申请人</div></div>' +
list
} else if (mode === "a") {
label = manage ? "A · 管理态" : "A · 普通态"
inner =
'<div class="nav is-capsule-safe">' +
'<div class="nav-back"></div>' +
'<div class="nav-title">待办中心</div>' +
'<div class="' + actionCls + ' is-safe">' + actionText + "</div>" +
"</div>" +
'<div class="toolbar"><div class="search">搜索流程名称、申请人</div></div>' +
list
} else if (mode === "b") {
label = manage ? "B · 管理态" : "B · 普通态"
var linkCls = manage
? "list-head__link is-done is-safe"
: "list-head__link is-safe"
inner =
'<div class="nav">' +
'<div class="nav-back"></div>' +
'<div class="nav-title">待办中心</div>' +
"</div>" +
'<div class="toolbar"><div class="search">搜索流程名称、申请人</div></div>' +
'<div class="list-head">' +
'<div class="list-head__count">共 <strong>3</strong> 条</div>' +
'<div class="' + linkCls + '">' + actionText + "</div>" +
"</div>" +
list
} else {
label = manage ? "C · 管理态" : "C · 普通态"
var chipCls = manage ? "manage-chip is-done" : "manage-chip"
inner =
'<div class="nav">' +
'<div class="nav-back"></div>' +
'<div class="nav-title">待办中心</div>' +
"</div>" +
'<div class="toolbar">' +
'<div class="toolbar-row">' +
'<div class="search">搜索流程名称、申请人</div>' +
'<div class="' + chipCls + '">' + actionText + "</div>" +
"</div>" +
"</div>" +
list
}
return phoneShell(label, inner, flag)
}
function render(key) {
var s = SCENES[key]
document.getElementById("phones").innerHTML =
buildPhone(s.mode, false) + buildPhone(s.mode, true)
setInfo(key)
}
document.getElementById("tabs").addEventListener("click", function (e) {
var btn = e.target.closest(".tab")
if (!btn) return
document.querySelectorAll(".tab").forEach(function (t) {
t.classList.toggle("is-active", t === btn)
})
render(btn.getAttribute("data-k"))
})
render("now")
</script>
</body>
</html>