Files
zkxw/docs/oa-start-picker-list-style-demo.html
T
2026-08-12 15:48:15 +08:00

1052 lines
31 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" />
<title>发起审批 · 下拉选择弹窗列表样式 Demo</title>
<style>
:root {
--ink: #0f172a;
--muted: #64748b;
--line: #e2e8f0;
--brand: #22c55e;
--brand-dark: #16a34a;
--brand-deep: #15803d;
--brand-soft: #ecfdf5;
--text: #0f291e;
--text-muted: #5f7a6d;
--border-mint: #d8f3e5;
}
* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; }
body {
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, system-ui, sans-serif;
color: var(--ink);
background:
radial-gradient(900px 420px at 8% -8%, rgba(52, 211, 153, 0.16), transparent 55%),
radial-gradient(700px 380px at 92% 0%, rgba(148, 163, 184, 0.14), transparent 50%),
linear-gradient(180deg, #f8fffc 0%, #f4f7fb 50%, #f7f8fa 100%);
line-height: 1.45;
}
.wrap {
max-width: 1220px;
margin: 0 auto;
padding: 28px 18px 96px;
}
h1 {
margin: 0 0 8px;
font-size: 24px;
letter-spacing: -0.02em;
font-weight: 720;
}
.sub {
margin: 0;
max-width: 760px;
color: var(--muted);
font-size: 14px;
}
.note {
margin: 14px 0 22px;
padding: 10px 12px;
border-radius: 10px;
background: #fff;
border: 1px solid var(--line);
font-size: 13px;
color: var(--muted);
}
.note strong { color: var(--brand-deep); }
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.chip {
appearance: none;
border: 1px solid var(--line);
background: #fff;
border-radius: 999px;
padding: 8px 14px;
font-size: 13px;
font-weight: 650;
color: #475569;
cursor: pointer;
font-family: inherit;
}
.chip.is-on {
border-color: var(--brand);
background: var(--brand-soft);
color: var(--brand-deep);
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.12);
}
.layout {
display: grid;
grid-template-columns: 1fr minmax(320px, 380px);
gap: 22px;
align-items: start;
}
@media (max-width: 960px) {
.layout { grid-template-columns: 1fr; }
}
.schemes { display: grid; gap: 10px; }
.scheme {
appearance: none;
width: 100%;
text-align: left;
border: 1px solid var(--line);
background: #fff;
border-radius: 16px;
padding: 14px 16px;
cursor: pointer;
transition: border-color .15s, box-shadow .15s, background .15s;
font-family: inherit;
color: inherit;
}
.scheme:hover {
border-color: #cbd5e1;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
}
.scheme.is-on {
border-color: var(--brand);
background: linear-gradient(180deg, #f0fdf4, #fff);
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
}
.scheme-top {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 4px;
}
.letter {
width: 28px; height: 28px;
border-radius: 8px;
display: grid; place-items: center;
font-size: 13px; font-weight: 750;
background: #f1f5f9;
color: #334155;
flex-shrink: 0;
}
.scheme.is-on .letter {
background: var(--brand-soft);
color: var(--brand-deep);
}
.scheme h2 {
margin: 0;
font-size: 15px;
font-weight: 680;
}
.scheme p {
margin: 0;
font-size: 12.5px;
color: var(--muted);
padding-left: 38px;
}
.tag {
margin-left: auto;
font-size: 10px;
font-weight: 700;
padding: 2px 8px;
border-radius: 999px;
background: #f1f5f9;
color: #64748b;
white-space: nowrap;
}
.tag.rec {
background: var(--brand-soft);
color: var(--brand-deep);
}
.tag.base {
background: #fef3c7;
color: #92400e;
}
/* —— 右侧手机 —— */
.stage { position: sticky; top: 16px; }
.phone {
background: #111827;
border-radius: 36px;
padding: 10px;
box-shadow: 0 24px 50px rgba(15, 23, 42, 0.18);
}
.phone-screen {
border-radius: 28px;
overflow: hidden;
background: #f1f5f9;
height: 680px;
display: flex;
flex-direction: column;
position: relative;
}
.phone-nav {
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-bottom: 1px solid #f1f5f9;
font-size: 15px;
font-weight: 650;
color: var(--text);
position: relative;
flex-shrink: 0;
}
.phone-nav .back {
position: absolute;
left: 14px;
color: var(--text-muted);
font-size: 13px;
font-weight: 500;
}
.form-mock {
flex: 1;
padding: 12px;
display: flex;
flex-direction: column;
gap: 10px;
overflow: auto;
}
.field {
background: #fff;
border-radius: 12px;
border: 1px solid #eef2f6;
padding: 12px 14px;
cursor: pointer;
}
.field:active { opacity: 0.92; }
.field .lab {
font-size: 12px;
color: #94a3b8;
margin-bottom: 6px;
}
.field .val {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
color: #334155;
}
.field .val.ph { color: #cbd5e1; }
.field .chev { color: #cbd5e1; font-size: 12px; }
/* 遮罩 + 底部弹窗 */
.mask {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.42);
display: flex;
align-items: flex-end;
z-index: 5;
}
.sheet {
width: 100%;
height: 72%;
background: #fff;
border-radius: 18px 18px 0 0;
display: flex;
flex-direction: column;
overflow: hidden;
animation: sheetUp .22s ease-out;
}
@keyframes sheetUp {
from { transform: translateY(18%); opacity: .6; }
to { transform: translateY(0); opacity: 1; }
}
.sheet-hd {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid var(--border-mint);
flex-shrink: 0;
}
.sheet-hd .cancel {
min-width: 48px;
color: var(--text-muted);
font-size: 14px;
cursor: pointer;
}
.sheet-hd strong {
font-size: 16px;
font-weight: 700;
color: var(--text);
}
.sheet-hd .ok {
min-width: 48px;
color: var(--brand);
font-size: 14px;
font-weight: 600;
text-align: right;
cursor: pointer;
}
.search {
padding: 10px 12px;
background: #fff;
flex-shrink: 0;
}
.search-inner {
display: flex;
align-items: center;
gap: 8px;
height: 36px;
padding: 0 12px;
border-radius: 999px;
background: #f3f4f6;
color: #94a3b8;
font-size: 13px;
}
.search-inner svg { width: 14px; height: 14px; flex-shrink: 0; }
.pick-list {
flex: 1;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.pick-item {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
.pick-item:active { opacity: 0.88; }
.pick-main { flex: 1; min-width: 0; }
.pick-title {
font-size: 15px;
font-weight: 600;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pick-sub {
margin-top: 2px;
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.check {
width: 20px;
height: 20px;
border-radius: 50%;
border: 1.5px solid #cbd5e1;
flex-shrink: 0;
display: grid;
place-items: center;
background: #fff;
transition: all .15s;
}
.check.sq { border-radius: 5px; }
.pick-item.is-on .check {
border-color: var(--brand);
background: var(--brand);
color: #fff;
}
.check svg { width: 12px; height: 12px; opacity: 0; }
.pick-item.is-on .check svg { opacity: 1; }
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 14px;
font-weight: 700;
color: var(--brand-deep);
background: var(--brand-soft);
flex-shrink: 0;
}
.badge {
font-size: 10px;
font-weight: 700;
padding: 2px 7px;
border-radius: 999px;
background: #f1f5f9;
color: #64748b;
flex-shrink: 0;
}
.badge.ok {
background: var(--brand-soft);
color: var(--brand-deep);
}
/* ========== 方案 A:现状基线 ========== */
.s-a .pick-list {
padding: 12px;
background: transparent;
}
.s-a .pick-item {
margin-bottom: 10px;
padding: 12px 14px;
border-radius: 10px;
background: transparent;
}
.s-a .pick-item:last-child { margin-bottom: 0; }
/* ========== 方案 B:柔和卡片 ========== */
.s-b .pick-list {
padding: 12px;
background: #f8fafc;
}
.s-b .pick-item {
margin-bottom: 8px;
padding: 12px 14px;
border-radius: 12px;
background: #fff;
border: 1px solid #eef2f6;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
}
.s-b .pick-item:last-child { margin-bottom: 0; }
.s-b .pick-item.is-on {
border-color: #86efac;
background: linear-gradient(180deg, #f0fdf4, #fff);
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.12);
}
/* ========== 方案 C:分割线行 ========== */
.s-c .pick-list {
padding: 0;
background: #fff;
}
.s-c .pick-item {
padding: 14px 16px;
border-bottom: 1px solid #f1f5f9;
background: #fff;
}
.s-c .pick-item:last-child { border-bottom: none; }
.s-c .pick-item.is-on {
background: var(--brand-soft);
}
.s-c .pick-item.is-on .pick-title { color: var(--brand-deep); }
/* ========== 方案 D:头像 + 右侧勾选 ========== */
.s-d .pick-list {
padding: 8px 12px 16px;
background: #f8fafc;
}
.s-d .pick-item {
margin-bottom: 8px;
padding: 12px 14px;
border-radius: 14px;
background: #fff;
border: 1px solid #eef2f6;
flex-direction: row;
}
.s-d .pick-item .check { order: 3; margin-left: 4px; }
.s-d .pick-item.is-on {
border-color: #bbf7d0;
background: #fff;
}
.s-d .pick-item.is-on .avatar {
background: var(--brand);
color: #fff;
}
/* ========== 方案 E:左侧色条 ========== */
.s-e .pick-list {
padding: 12px;
background: #fff;
}
.s-e .pick-item {
position: relative;
margin-bottom: 8px;
padding: 13px 14px 13px 16px;
border-radius: 10px;
background: #f8fafc;
overflow: hidden;
}
.s-e .pick-item::before {
content: "";
position: absolute;
left: 0; top: 10px; bottom: 10px;
width: 3px;
border-radius: 0 3px 3px 0;
background: transparent;
transition: background .15s;
}
.s-e .pick-item.is-on {
background: var(--brand-soft);
}
.s-e .pick-item.is-on::before {
background: var(--brand);
}
/* ========== 方案 F:紧凑密排 ========== */
.s-f .pick-list {
padding: 4px 0;
background: #fff;
}
.s-f .pick-item {
padding: 10px 16px;
gap: 8px;
border-bottom: 1px solid #f8fafc;
}
.s-f .pick-title { font-size: 14px; font-weight: 550; }
.s-f .pick-sub { font-size: 11px; }
.s-f .check { width: 18px; height: 18px; }
.s-f .pick-item.is-on { background: #f0fdf4; }
/* ========== 方案 G:整行高亮 + 右勾 ========== */
.s-g .pick-list {
padding: 12px;
background: #f1f5f9;
}
.s-g .pick-item {
margin-bottom: 8px;
padding: 14px 14px;
border-radius: 12px;
background: #fff;
border: 1.5px solid transparent;
}
.s-g .pick-item .check {
order: 3;
margin-left: auto;
border: none;
background: #e2e8f0;
color: transparent;
}
.s-g .pick-item.is-on {
border-color: var(--brand);
background: #fff;
box-shadow: 0 4px 14px rgba(22, 163, 74, 0.12);
}
.s-g .pick-item.is-on .check {
background: var(--brand);
color: #fff;
}
.s-g .pick-item.is-on .check svg { opacity: 1; }
.s-g .pick-item:not(.is-on) .check svg { opacity: 0; }
/* ========== 方案 H:双行元信息卡 ========== */
.s-h .pick-list {
padding: 12px;
background: linear-gradient(180deg, #f0fdf4 0%, #f8fafc 40%);
}
.s-h .pick-item {
margin-bottom: 10px;
padding: 14px;
border-radius: 14px;
background: rgba(255,255,255,0.95);
border: 1px solid #e2e8f0;
align-items: flex-start;
}
.s-h .pick-item.is-on {
border-color: #86efac;
box-shadow: 0 8px 20px rgba(22, 163, 74, 0.1);
}
.s-h .meta-row {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
flex-wrap: wrap;
}
.s-h .meta-pill {
font-size: 10px;
font-weight: 650;
padding: 2px 8px;
border-radius: 999px;
background: #f1f5f9;
color: #64748b;
}
.s-h .pick-item.is-on .meta-pill.pri {
background: var(--brand-soft);
color: var(--brand-deep);
}
/* ========== 方案 I:单选点选(无复选框感) ========== */
.s-i .pick-list {
padding: 8px 12px 16px;
background: #fff;
}
.s-i .pick-item {
margin-bottom: 6px;
padding: 13px 14px;
border-radius: 12px;
background: #f8fafc;
border: 1px solid transparent;
}
.s-i .pick-item .check {
width: 18px;
height: 18px;
border-width: 2px;
}
.s-i .pick-item.is-on {
background: var(--brand-soft);
border-color: #86efac;
}
.s-i .pick-item.is-on .pick-title { color: var(--brand-deep); }
.s-i .pick-item.is-on .check {
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18);
}
/* ========== 方案 J:分组标题 ========== */
.s-j .pick-list {
padding: 0 0 12px;
background: #f8fafc;
}
.s-j .group-title {
padding: 10px 16px 6px;
font-size: 12px;
font-weight: 650;
color: #94a3b8;
letter-spacing: 0.02em;
}
.s-j .group-block {
margin: 0 12px 10px;
background: #fff;
border-radius: 12px;
border: 1px solid #eef2f6;
overflow: hidden;
}
.s-j .pick-item {
padding: 12px 14px;
border-bottom: 1px solid #f8fafc;
}
.s-j .pick-item:last-child { border-bottom: none; }
.s-j .pick-item.is-on { background: #f0fdf4; }
.stage-meta {
margin-top: 14px;
padding: 12px 14px;
background: rgba(255,255,255,.92);
border: 1px solid var(--line);
border-radius: 14px;
font-size: 13px;
color: var(--muted);
}
.stage-meta strong { color: var(--brand-deep); }
.stage-meta .why {
margin-top: 6px;
font-size: 12.5px;
line-height: 1.55;
}
.hint {
margin-top: 8px;
font-size: 12px;
color: #94a3b8;
}
</style>
</head>
<body>
<div class="wrap">
<h1>发起审批 · 下拉选择弹窗列表样式</h1>
<p class="sub">
针对 OA 发起页中人员 / 部门 / 岗位 / 收款账户 / 关联审批等底部弹窗列表,提供多套视觉方案。
左侧切换方案,右侧手机预览可点选体验;上方可切换「单选 / 多选」与「数据场景」。
</p>
<div class="note">
Token 对齐 mobile-h5「薄荷微风」:主色 <strong>#22c55e</strong>、软底 <strong>#ecfdf5</strong>、正文 <strong>#0f291e</strong>。
当前线上基线为方案 A(透明 cell + 间距,选中态弱)。
</div>
<div class="toolbar">
<button type="button" class="chip is-on" data-mode="multi">多选</button>
<button type="button" class="chip" data-mode="single">单选</button>
<span style="width:8px"></span>
<button type="button" class="chip is-on" data-scene="person">人员</button>
<button type="button" class="chip" data-scene="dept">部门</button>
<button type="button" class="chip" data-scene="option">简单选项</button>
<button type="button" class="chip" data-scene="related">关联审批</button>
</div>
<div class="layout">
<div class="schemes" id="schemes"></div>
<div class="stage">
<div class="phone">
<div class="phone-screen" id="phone">
<div class="phone-nav">
<span class="back">‹</span>
<span>发起审批</span>
</div>
<div class="form-mock">
<div class="field" id="openField">
<div class="lab" id="fieldLab">审批人</div>
<div class="val ph" id="fieldVal">
<span>请选择</span>
<span class="chev">›</span>
</div>
</div>
<div class="field">
<div class="lab">事由</div>
<div class="val ph"><span>请输入</span></div>
</div>
<p class="hint">点击上方字段打开选择弹窗。切换方案时弹窗保持打开,便于对比。</p>
</div>
<div class="mask" id="mask" hidden>
<div class="sheet" id="sheet">
<div class="sheet-hd">
<span class="cancel" id="btnCancel">取消</span>
<strong id="sheetTitle">选择人员</strong>
<span class="ok" id="btnOk">确定</span>
</div>
<div class="search">
<div class="search-inner">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="7"/><path d="M20 20l-3-3"/>
</svg>
<span id="searchPh">搜索姓名或工号</span>
</div>
</div>
<div class="pick-list" id="pickList"></div>
</div>
</div>
</div>
</div>
<div class="stage-meta">
当前方案:<strong id="metaName">A · 现状基线</strong>
<div class="why" id="metaWhy"></div>
</div>
</div>
</div>
</div>
<script>
const SCHEMES = [
{
id: 'a',
letter: 'A',
name: '现状基线',
tag: '当前',
tagClass: 'base',
desc: '透明 cell + 10px 间距,与现网 oa-pick__list 接近;选中态几乎不可见。',
why: '层级弱、选中反馈不足,长列表时条目边界不清晰。适合作为改造前对照。'
},
{
id: 'b',
letter: 'B',
name: '柔和白卡片',
tag: '',
tagClass: '',
desc: '灰底上白卡片 + 浅描边;选中后薄荷绿描边与外环光。',
why: '卡片边界清晰、选中反馈强,适配人员/部门/岗位等通用列表,落地成本低。'
},
{
id: 'c',
letter: 'C',
name: '分割线行表',
tag: '已采用',
tagClass: 'rec',
desc: '白底连续行 + 发丝分割线;选中整行浅绿底。',
why: '信息密度高、偏系统原生感;适合简单选项、枚举类单选。当前已落地到 OA 选择弹窗。'
},
{
id: 'd',
letter: 'D',
name: '头像 + 右勾选',
tag: '人员',
tagClass: 'rec',
desc: '左侧头像字、中部姓名/副标题、右侧圆形勾选;选中头像变主色。',
why: '专为人员/抄送场景;身份识别强。部门/简单选项场景可隐藏头像。'
},
{
id: 'e',
letter: 'E',
name: '左侧色条',
tag: '',
tagClass: '',
desc: '灰底圆角行,选中出现左侧品牌色竖条 + 软绿底。',
why: '选中态含蓄但不丢识别度,适合希望「少边框、少阴影」的清爽风。'
},
{
id: 'f',
letter: 'F',
name: '紧凑密排',
tag: '长列表',
tagClass: '',
desc: '更小行高与字号,弱分割线;一屏展示更多选项。',
why: '人员很多、或枚举项较长时优先;牺牲一点呼吸感换效率。'
},
{
id: 'g',
letter: 'G',
name: '描边高亮 + 右勾',
tag: '',
tagClass: '',
desc: '灰底面板内白卡片;选中品牌描边 + 阴影,勾选移到右侧。',
why: '选中态最醒目,适合「必须看清已选」的多选场景(审批人、抄送人)。'
},
{
id: 'h',
letter: 'H',
name: '双行元信息卡',
tag: '关联单',
tagClass: 'rec',
desc: '标题 + 副标题 + 底部 pill 元数据(单号/状态/时间)。',
why: '专为关联审批、收款账户等「需要多字段」的选项;普通短列表略重。'
},
{
id: 'i',
letter: 'I',
name: '轻量点选',
tag: '单选友好',
tagClass: '',
desc: '浅灰圆角块,选中软绿底 + 描边;勾选圈带外环光。',
why: '适合部门、岗位、简单下拉等单选为主的字段;多选也可用。'
},
{
id: 'j',
letter: 'J',
name: '分组块列表',
tag: '',
tagClass: '',
desc: '按字母/部门分组,组内白卡片连续行。',
why: '选项可按组织/首字母归类时体验最好;需后端或前端分组能力。'
}
]
const SCENES = {
person: {
title: '选择人员',
lab: '审批人',
search: '搜索姓名或工号',
groups: [
{
title: '研发中心',
items: [
{ id: '1', title: '张晓明', sub: '工号 · 10086', avatar: '张', meta: ['研发', '在职'] },
{ id: '2', title: '李思远', sub: '工号 · 10087', avatar: '李', meta: ['研发', '在职'] },
{ id: '3', title: '王芳', sub: '工号 · 10091', avatar: '王', meta: ['产品', '在职'] }
]
},
{
title: '职能支持',
items: [
{ id: '4', title: '陈静', sub: '工号 · 20012', avatar: '陈', meta: ['人事', '在职'] },
{ id: '5', title: '赵磊', sub: '工号 · 20018', avatar: '赵', meta: ['财务', '在职'] }
]
}
]
},
dept: {
title: '选择部门',
lab: '所属部门',
search: '搜索部门名称',
groups: [
{
title: '组织架构',
items: [
{ id: 'd1', title: '中科兴皖 / 研发中心', sub: '一级部门', avatar: '研', meta: ['12 人'] },
{ id: 'd2', title: '中科兴皖 / 产品中心', sub: '一级部门', avatar: '产', meta: ['8 人'] },
{ id: 'd3', title: '中科兴皖 / 运营中心', sub: '一级部门', avatar: '运', meta: ['15 人'] },
{ id: 'd4', title: '中科兴皖 / 财务部', sub: '一级部门', avatar: '财', meta: ['5 人'] }
]
}
]
},
option: {
title: '选择类型',
lab: '出差类型',
search: '搜索选项',
groups: [
{
title: '常用',
items: [
{ id: 'o1', title: '市内出差', sub: '当日往返', avatar: '市', meta: [] },
{ id: 'o2', title: '省内出差', sub: '需填写行程', avatar: '省', meta: [] },
{ id: 'o3', title: '省外出差', sub: '需关联预算', avatar: '外', meta: [] },
{ id: 'o4', title: '海外出差', sub: '需提前审批', avatar: '海', meta: [] }
]
}
]
},
related: {
title: '关联审批',
lab: '关联单据',
search: '搜索单号或标题',
groups: [
{
title: '最近',
items: [
{ id: 'r1', title: '出差申请 · 上海客户拜访', sub: 'OA-20260320-018', avatar: '差', meta: ['已通过', '3/18'] },
{ id: 'r2', title: '费用报销 · 差旅费', sub: 'OA-20260318-042', avatar: '报', meta: ['审批中', '3/18'] },
{ id: 'r3', title: '用章申请 · 合同用印', sub: 'OA-20260315-009', avatar: '章', meta: ['已通过', '3/15'] }
]
}
]
}
}
const state = {
scheme: 'c',
mode: 'multi',
scene: 'person',
selected: new Set(['1', '2']),
open: true
}
const checkSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M5 12l5 5L20 7"/></svg>`
function flatItems() {
return SCENES[state.scene].groups.flatMap((g) => g.items)
}
function renderSchemes() {
const box = document.getElementById('schemes')
box.innerHTML = SCHEMES.map((s) => `
<button type="button" class="scheme ${state.scheme === s.id ? 'is-on' : ''}" data-id="${s.id}">
<div class="scheme-top">
<span class="letter">${s.letter}</span>
<h2>${s.name}</h2>
${s.tag ? `<span class="tag ${s.tagClass}">${s.tag}</span>` : ''}
</div>
<p>${s.desc}</p>
</button>
`).join('')
box.querySelectorAll('.scheme').forEach((el) => {
el.addEventListener('click', () => {
state.scheme = el.dataset.id
apply()
})
})
}
function renderList() {
const list = document.getElementById('pickList')
const scene = SCENES[state.scene]
const sq = state.mode === 'multi' ? 'sq' : ''
const showAvatar = state.scheme === 'd' || state.scene === 'person' && state.scheme === 'h'
const useGroups = state.scheme === 'j'
if (useGroups) {
list.innerHTML = scene.groups.map((g) => `
<div class="group-title">${g.title}</div>
<div class="group-block">
${g.items.map((it) => itemHtml(it, sq, true)).join('')}
</div>
`).join('')
} else if (state.scheme === 'h') {
list.innerHTML = flatItems().map((it) => `
<div class="pick-item ${state.selected.has(it.id) ? 'is-on' : ''}" data-id="${it.id}">
<span class="check ${sq}">${checkSvg}</span>
<div class="pick-main">
<div class="pick-title">${it.title}</div>
<div class="pick-sub">${it.sub}</div>
${it.meta?.length ? `<div class="meta-row">${it.meta.map((m, i) => `<span class="meta-pill ${i === 0 ? 'pri' : ''}">${m}</span>`).join('')}</div>` : ''}
</div>
</div>
`).join('')
} else {
list.innerHTML = flatItems().map((it) => itemHtml(it, sq, state.scheme === 'd')).join('')
}
list.querySelectorAll('.pick-item').forEach((el) => {
el.addEventListener('click', () => toggle(el.dataset.id))
})
}
function itemHtml(it, sq, withAvatar) {
return `
<div class="pick-item ${state.selected.has(it.id) ? 'is-on' : ''}" data-id="${it.id}">
<span class="check ${sq}">${checkSvg}</span>
${withAvatar ? `<span class="avatar">${it.avatar || it.title.slice(0, 1)}</span>` : ''}
<div class="pick-main">
<div class="pick-title">${it.title}</div>
${it.sub ? `<div class="pick-sub">${it.sub}</div>` : ''}
</div>
</div>
`
}
function toggle(id) {
if (state.mode === 'single') {
state.selected = new Set([id])
} else {
if (state.selected.has(id)) state.selected.delete(id)
else state.selected.add(id)
}
updateFieldVal()
renderList()
}
function updateFieldVal() {
const el = document.getElementById('fieldVal')
const map = new Map(flatItems().map((x) => [x.id, x.title]))
const names = [...state.selected].map((id) => map.get(id)).filter(Boolean)
if (!names.length) {
el.className = 'val ph'
el.innerHTML = `<span>请选择</span><span class="chev">›</span>`
} else {
el.className = 'val'
el.innerHTML = `<span>${names.join('、')}</span><span class="chev">›</span>`
}
}
function apply() {
const phone = document.getElementById('phone')
const sheet = document.getElementById('sheet')
const scene = SCENES[state.scene]
const scheme = SCHEMES.find((s) => s.id === state.scheme)
phone.className = 'phone-screen s-' + state.scheme
sheet.className = 'sheet s-' + state.scheme
document.getElementById('sheetTitle').textContent = scene.title
document.getElementById('fieldLab').textContent = scene.lab
document.getElementById('searchPh').textContent = scene.search
document.getElementById('metaName').textContent = `${scheme.letter} · ${scheme.name}`
document.getElementById('metaWhy').textContent = scheme.why
document.getElementById('mask').hidden = !state.open
document.getElementById('btnOk').style.visibility = state.mode === 'single' ? 'hidden' : 'visible'
renderSchemes()
renderList()
updateFieldVal()
}
document.getElementById('openField').addEventListener('click', () => {
state.open = true
apply()
})
document.getElementById('btnCancel').addEventListener('click', () => {
state.open = false
apply()
})
document.getElementById('btnOk').addEventListener('click', () => {
state.open = false
apply()
})
document.getElementById('mask').addEventListener('click', (e) => {
if (e.target.id === 'mask') {
state.open = false
apply()
}
})
document.querySelectorAll('.chip[data-mode]').forEach((el) => {
el.addEventListener('click', () => {
document.querySelectorAll('.chip[data-mode]').forEach((c) => c.classList.remove('is-on'))
el.classList.add('is-on')
state.mode = el.dataset.mode
if (state.mode === 'single' && state.selected.size > 1) {
const first = [...state.selected][0]
state.selected = new Set(first ? [first] : [])
}
apply()
})
})
document.querySelectorAll('.chip[data-scene]').forEach((el) => {
el.addEventListener('click', () => {
document.querySelectorAll('.chip[data-scene]').forEach((c) => c.classList.remove('is-on'))
el.classList.add('is-on')
state.scene = el.dataset.scene
const ids = flatItems().map((x) => x.id)
state.selected = new Set(ids.slice(0, state.mode === 'multi' ? 2 : 1))
state.open = true
apply()
})
})
apply()
</script>
</body>
</html>