1052 lines
31 KiB
HTML
1052 lines
31 KiB
HTML
<!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>
|