976 lines
31 KiB
HTML
976 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>OA 审批 · 审批人设置抽屉 Demo(Fresh Mint)</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: #0f172a;
|
||
--muted: #64748b;
|
||
--border: #e2e8f0;
|
||
--bg: #f1f5f9;
|
||
--card: #fff;
|
||
--radius: 10px;
|
||
--drawer-w: 640px;
|
||
--warn: #d97706;
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
min-height: 100vh;
|
||
font: 14px/1.5 Inter, system-ui, -apple-system, "PingFang SC", sans-serif;
|
||
color: var(--text);
|
||
background: var(--bg);
|
||
}
|
||
|
||
/* —— 页面壳 —— */
|
||
.page {
|
||
max-width: 1180px;
|
||
margin: 0 auto;
|
||
padding: 28px 24px 48px;
|
||
}
|
||
.page-hd {
|
||
margin-bottom: 20px;
|
||
}
|
||
.page-hd h1 {
|
||
margin: 0 0 6px;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
}
|
||
.page-hd p {
|
||
margin: 0;
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
}
|
||
.page-hd code {
|
||
padding: 1px 6px;
|
||
font-size: 12px;
|
||
background: #fff;
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
/* —— 方案切换 —— */
|
||
.scheme-bar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.scheme-btn {
|
||
appearance: none;
|
||
border: 1px solid var(--border);
|
||
background: #fff;
|
||
color: var(--text);
|
||
padding: 8px 14px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-weight: 500;
|
||
transition: .15s ease;
|
||
}
|
||
.scheme-btn:hover { border-color: #a7f3d0; background: var(--primary-bg); }
|
||
.scheme-btn.is-active {
|
||
border-color: var(--primary);
|
||
background: var(--primary-bg);
|
||
color: var(--primary-dark);
|
||
box-shadow: 0 0 0 1px var(--primary);
|
||
}
|
||
.scheme-btn .tag {
|
||
display: inline-block;
|
||
margin-left: 6px;
|
||
padding: 0 6px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
background: var(--primary);
|
||
border-radius: 999px;
|
||
vertical-align: 1px;
|
||
}
|
||
.scheme-note {
|
||
margin: 0 0 20px;
|
||
padding: 12px 14px;
|
||
background: #fff;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
}
|
||
.scheme-note strong { color: var(--text); font-weight: 600; }
|
||
|
||
/* —— 舞台:流程背景 + 抽屉 —— */
|
||
.stage {
|
||
position: relative;
|
||
height: min(780px, calc(100vh - 200px));
|
||
min-height: 620px;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
border: 1px solid var(--border);
|
||
background: #f8fafc;
|
||
box-shadow: 0 8px 28px rgba(15, 23, 42, .06);
|
||
}
|
||
.stage-canvas {
|
||
height: 100%;
|
||
padding: 40px 24px;
|
||
opacity: .55;
|
||
filter: grayscale(.15);
|
||
pointer-events: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
.flow-node {
|
||
width: 200px;
|
||
padding: 12px 14px;
|
||
background: #fff;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
.flow-node.approval {
|
||
border-color: #a7f3d0;
|
||
background: var(--primary-bg);
|
||
color: var(--primary-dark);
|
||
}
|
||
.flow-line {
|
||
width: 2px;
|
||
height: 20px;
|
||
background: #cbd5e1;
|
||
}
|
||
.stage-mask {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: rgba(15, 23, 42, .28);
|
||
}
|
||
|
||
/* —— 抽屉 —— */
|
||
.drawer {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
width: var(--drawer-w);
|
||
max-width: 100%;
|
||
background: #fff;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-shadow: -8px 0 28px rgba(15, 23, 42, .12);
|
||
animation: slideIn .28s ease;
|
||
}
|
||
@keyframes slideIn {
|
||
from { transform: translateX(24px); opacity: .6; }
|
||
to { transform: none; opacity: 1; }
|
||
}
|
||
|
||
/* 标题栏美化 */
|
||
.drawer-hd {
|
||
flex-shrink: 0;
|
||
padding: 18px 28px 14px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: linear-gradient(180deg, #ecfdf5 0%, #fff 72%);
|
||
}
|
||
.drawer-hd-top {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
margin-bottom: 14px;
|
||
}
|
||
.drawer-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 10px;
|
||
background: var(--primary);
|
||
color: #fff;
|
||
display: grid;
|
||
place-items: center;
|
||
flex-shrink: 0;
|
||
box-shadow: 0 4px 12px rgba(16, 185, 129, .28);
|
||
}
|
||
.drawer-icon svg { width: 22px; height: 22px; }
|
||
.drawer-titles { flex: 1; min-width: 0; }
|
||
.drawer-titles h2 {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
letter-spacing: .01em;
|
||
}
|
||
.drawer-titles p {
|
||
margin: 2px 0 0;
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
.drawer-close {
|
||
appearance: none;
|
||
width: 32px;
|
||
height: 32px;
|
||
border: 0;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
color: var(--muted);
|
||
cursor: pointer;
|
||
font-size: 18px;
|
||
line-height: 1;
|
||
}
|
||
.drawer-close:hover { background: #f1f5f9; color: var(--text); }
|
||
|
||
/* 顶部操作按钮 */
|
||
.drawer-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
}
|
||
.btn {
|
||
appearance: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
height: 36px;
|
||
padding: 0 14px;
|
||
border-radius: 8px;
|
||
font: inherit;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
border: 1px solid var(--border);
|
||
background: #fff;
|
||
color: var(--text);
|
||
transition: .15s ease;
|
||
}
|
||
.btn:hover { background: #f8fafc; }
|
||
.btn-primary {
|
||
border-color: var(--primary);
|
||
background: var(--primary);
|
||
color: #fff;
|
||
}
|
||
.btn-primary:hover { background: var(--primary-dark); border-color: var(--primary-dark); }
|
||
.btn svg { width: 14px; height: 14px; }
|
||
|
||
/* 正文 */
|
||
.drawer-body {
|
||
flex: 1;
|
||
overflow: auto;
|
||
padding: 20px 28px 28px;
|
||
}
|
||
.field { margin-bottom: 18px; }
|
||
.field > label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
.field > label .tip {
|
||
margin-left: 6px;
|
||
font-weight: 400;
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
.input, .select {
|
||
width: 100%;
|
||
height: 36px;
|
||
padding: 0 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
font: inherit;
|
||
background: #fff;
|
||
color: var(--text);
|
||
outline: none;
|
||
}
|
||
.input:focus, .select:focus {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 3px rgba(16, 185, 129, .15);
|
||
}
|
||
|
||
/* 选人方式:分段按钮 */
|
||
.seg {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 8px;
|
||
}
|
||
.seg.two { grid-template-columns: repeat(2, 1fr); }
|
||
.seg-item {
|
||
appearance: none;
|
||
border: 1px solid var(--border);
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 10px 8px;
|
||
font: inherit;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
color: var(--muted);
|
||
text-align: center;
|
||
transition: .15s ease;
|
||
}
|
||
.seg-item:hover { border-color: #a7f3d0; color: var(--text); }
|
||
.seg-item.is-on {
|
||
border-color: var(--primary);
|
||
background: var(--primary-bg);
|
||
color: var(--primary-dark);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 或签 / 会签 */
|
||
.radio-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.radio-card {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
padding: 12px 14px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: .15s ease;
|
||
}
|
||
.radio-card:hover { border-color: #a7f3d0; }
|
||
.radio-card.is-on {
|
||
border-color: var(--primary);
|
||
background: var(--primary-bg);
|
||
}
|
||
.radio-card input { margin-top: 3px; accent-color: var(--primary); }
|
||
.radio-card strong { display: block; font-size: 13px; }
|
||
.radio-card span { font-size: 12px; color: var(--muted); }
|
||
|
||
/* —— 方案 A:头像芯片 + 搜索添加(推荐) —— */
|
||
.member-picker-a .search-box {
|
||
position: relative;
|
||
}
|
||
.member-picker-a .search-box input {
|
||
padding-left: 36px;
|
||
}
|
||
.member-picker-a .search-ico {
|
||
position: absolute;
|
||
left: 12px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
color: var(--muted);
|
||
pointer-events: none;
|
||
}
|
||
.dropdown {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
top: calc(100% + 4px);
|
||
z-index: 5;
|
||
max-height: 220px;
|
||
overflow: auto;
|
||
background: #fff;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
box-shadow: 0 8px 24px rgba(15, 23, 42, .1);
|
||
display: none;
|
||
}
|
||
.dropdown.is-open { display: block; }
|
||
.dropdown-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border: 0;
|
||
background: transparent;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
text-align: left;
|
||
}
|
||
.dropdown-item:hover { background: var(--primary-bg); }
|
||
.dropdown-item.is-picked { opacity: .45; pointer-events: none; }
|
||
.dropdown-empty {
|
||
padding: 16px;
|
||
text-align: center;
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.avatar {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
display: grid;
|
||
place-items: center;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
flex-shrink: 0;
|
||
}
|
||
.avatar.sm { width: 28px; height: 28px; font-size: 11px; }
|
||
.u-meta { min-width: 0; }
|
||
.u-meta .name { font-weight: 600; font-size: 13px; }
|
||
.u-meta .sub { font-size: 12px; color: var(--muted); }
|
||
|
||
.chip-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 12px;
|
||
min-height: 40px;
|
||
padding: 12px;
|
||
background: #f8fafc;
|
||
border: 1px dashed #cbd5e1;
|
||
border-radius: 8px;
|
||
}
|
||
.chip-list.is-empty {
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
}
|
||
.chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 4px 8px 4px 4px;
|
||
background: #fff;
|
||
border: 1px solid var(--border);
|
||
border-radius: 999px;
|
||
max-width: 100%;
|
||
}
|
||
.chip .name {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
max-width: 120px;
|
||
}
|
||
.chip .rm {
|
||
appearance: none;
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 0;
|
||
border-radius: 50%;
|
||
background: #f1f5f9;
|
||
color: var(--muted);
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
line-height: 1;
|
||
flex-shrink: 0;
|
||
}
|
||
.chip .rm:hover { background: #fee2e2; color: #dc2626; }
|
||
.picked-count {
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
/* —— 方案 B:双栏勾选 —— */
|
||
.picker-b {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
min-height: 280px;
|
||
}
|
||
.picker-b-col {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
}
|
||
.picker-b-col + .picker-b-col {
|
||
border-left: 1px solid var(--border);
|
||
}
|
||
.picker-b-col-hd {
|
||
flex-shrink: 0;
|
||
padding: 10px 12px;
|
||
background: #f8fafc;
|
||
border-bottom: 1px solid var(--border);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--muted);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.picker-b-col-hd strong { color: var(--primary-dark); }
|
||
.picker-b-search {
|
||
padding: 8px 10px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.picker-b-list {
|
||
flex: 1;
|
||
overflow: auto;
|
||
max-height: 240px;
|
||
}
|
||
.picker-b-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
width: 100%;
|
||
padding: 9px 12px;
|
||
border: 0;
|
||
background: transparent;
|
||
cursor: pointer;
|
||
font: inherit;
|
||
text-align: left;
|
||
}
|
||
.picker-b-item:hover { background: var(--primary-bg); }
|
||
.picker-b-item input { accent-color: var(--primary); }
|
||
|
||
/* —— 方案 C:穿梭框风格 —— */
|
||
.picker-c {
|
||
display: grid;
|
||
grid-template-columns: 1fr 40px 1fr;
|
||
gap: 8px;
|
||
align-items: stretch;
|
||
min-height: 280px;
|
||
}
|
||
.picker-c-panel {
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
.picker-c-panel-hd {
|
||
padding: 10px 12px;
|
||
background: #f8fafc;
|
||
border-bottom: 1px solid var(--border);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
.picker-c-list {
|
||
flex: 1;
|
||
overflow: auto;
|
||
max-height: 260px;
|
||
}
|
||
.picker-c-mid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
.picker-c-mid .btn {
|
||
width: 32px;
|
||
height: 32px;
|
||
padding: 0;
|
||
justify-content: center;
|
||
}
|
||
|
||
.hidden { display: none !important; }
|
||
|
||
.footer-tip {
|
||
margin-top: 16px;
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<header class="page-hd">
|
||
<h1>审批人设置 · 右侧抽屉 Demo</h1>
|
||
<p>
|
||
Fresh Mint · 对照选型页。文件:
|
||
<code>docs/mockups/oa-approval-approver-drawer-demo.html</code>
|
||
· 浏览器直接打开即可(推荐 Chrome)。
|
||
</p>
|
||
</header>
|
||
|
||
<div class="scheme-bar" role="tablist" aria-label="指定成员展示方案">
|
||
<button type="button" class="scheme-btn is-active" data-scheme="a" role="tab" aria-selected="true">
|
||
方案 A · 头像芯片 + 搜索<span class="tag">推荐</span>
|
||
</button>
|
||
<button type="button" class="scheme-btn" data-scheme="b" role="tab" aria-selected="false">
|
||
方案 B · 双栏勾选
|
||
</button>
|
||
<button type="button" class="scheme-btn" data-scheme="c" role="tab" aria-selected="false">
|
||
方案 C · 穿梭框
|
||
</button>
|
||
</div>
|
||
<p class="scheme-note" id="schemeNote">
|
||
<strong>方案 A(推荐):</strong>搜索添加 + 已选人员以头像芯片展示,可单颗移除。接近钉钉/飞书体验,占高小、多选一眼可读。
|
||
</p>
|
||
|
||
<div class="stage">
|
||
<div class="stage-canvas" aria-hidden="true">
|
||
<div class="flow-node">发起人</div>
|
||
<div class="flow-line"></div>
|
||
<div class="flow-node approval">审批人 · 指定成员</div>
|
||
<div class="flow-line"></div>
|
||
<div class="flow-node">抄送人</div>
|
||
<div class="flow-line"></div>
|
||
<div class="flow-node">结束</div>
|
||
</div>
|
||
<div class="stage-mask" aria-hidden="true"></div>
|
||
|
||
<aside class="drawer" aria-label="审批人设置">
|
||
<div class="drawer-hd">
|
||
<div class="drawer-hd-top">
|
||
<div class="drawer-icon" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||
<circle cx="9" cy="7" r="4" />
|
||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||
</svg>
|
||
</div>
|
||
<div class="drawer-titles">
|
||
<h2>审批人设置</h2>
|
||
<p>配置本节点审批人与多人会签规则</p>
|
||
</div>
|
||
<button type="button" class="drawer-close" title="关闭" aria-label="关闭">×</button>
|
||
</div>
|
||
<div class="drawer-actions">
|
||
<button type="button" class="btn">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
||
取消
|
||
</button>
|
||
<button type="button" class="btn btn-primary">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6 9 17l-5-5"/></svg>
|
||
确定
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="drawer-body">
|
||
<div class="field">
|
||
<label for="nodeName">节点名称</label>
|
||
<input id="nodeName" class="input" value="审批人" maxlength="50" />
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label>选人方式</label>
|
||
<div class="seg" id="assigneeSeg">
|
||
<button type="button" class="seg-item is-on" data-type="users">指定成员</button>
|
||
<button type="button" class="seg-item" data-type="oa_roles">工作流角色</button>
|
||
<button type="button" class="seg-item" data-type="dept_leader">部门负责人</button>
|
||
<button type="button" class="seg-item" data-type="direct_supervisor">发起人主管</button>
|
||
<button type="button" class="seg-item" data-type="initiator_select">发起人自选</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 指定成员:三方案切换 -->
|
||
<div class="field" id="usersField">
|
||
<label>指定成员 <span class="tip">可多选,已选将作为本节点审批人</span></label>
|
||
|
||
<!-- A -->
|
||
<div class="member-picker-a" data-panel="a">
|
||
<div class="search-box">
|
||
<svg class="search-ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
|
||
</svg>
|
||
<input class="input" id="searchA" type="search" placeholder="搜索姓名 / 工号,回车或点击添加" autocomplete="off" />
|
||
<div class="dropdown" id="dropA" role="listbox"></div>
|
||
</div>
|
||
<div class="chip-list is-empty" id="chipList">暂未选择成员,请从上方搜索添加</div>
|
||
<div class="picked-count" id="pickedCountA">已选 0 人</div>
|
||
</div>
|
||
|
||
<!-- B -->
|
||
<div class="picker-b hidden" data-panel="b">
|
||
<div class="picker-b-col">
|
||
<div class="picker-b-col-hd">可选成员</div>
|
||
<div class="picker-b-search">
|
||
<input class="input" id="searchB" type="search" placeholder="筛选…" />
|
||
</div>
|
||
<div class="picker-b-list" id="listBLeft"></div>
|
||
</div>
|
||
<div class="picker-b-col">
|
||
<div class="picker-b-col-hd">已选 <strong id="countB">0</strong> 人</div>
|
||
<div class="picker-b-list" id="listBRight"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- C -->
|
||
<div class="picker-c hidden" data-panel="c">
|
||
<div class="picker-c-panel">
|
||
<div class="picker-c-panel-hd">待选</div>
|
||
<div class="picker-c-list" id="listCLeft"></div>
|
||
</div>
|
||
<div class="picker-c-mid">
|
||
<button type="button" class="btn" id="cToRight" title="添加">›</button>
|
||
<button type="button" class="btn" id="cToLeft" title="移除">‹</button>
|
||
</div>
|
||
<div class="picker-c-panel">
|
||
<div class="picker-c-panel-hd">已选</div>
|
||
<div class="picker-c-list" id="listCRight"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label>多人审批方式</label>
|
||
<div class="radio-row" id="signMode">
|
||
<label class="radio-card is-on">
|
||
<input type="radio" name="sign" value="or" checked />
|
||
<div>
|
||
<strong>或签</strong>
|
||
<span>一人通过即可进入下一节点</span>
|
||
</div>
|
||
</label>
|
||
<label class="radio-card">
|
||
<input type="radio" name="sign" value="and" />
|
||
<div>
|
||
<strong>会签</strong>
|
||
<span>所选成员全部通过后进入下一节点</span>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="footer-tip">交互说明:切换顶部方案 Tab 可对比指定成员的三种呈现;选人数据为 Demo 假数据。</p>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const USERS = [
|
||
{ id: '1', nickName: '张伟', userNo: 'U1001', dept: '研发中心', color: '#059669' },
|
||
{ id: '2', nickName: '李娜', userNo: 'U1002', dept: '质量部', color: '#0ea5e9' },
|
||
{ id: '3', nickName: '王芳', userNo: 'U1003', dept: '市场部', color: '#8b5cf6' },
|
||
{ id: '4', nickName: '刘强', userNo: 'U1004', dept: '财务部', color: '#d97706' },
|
||
{ id: '5', nickName: '陈明', userNo: 'U1005', dept: '人事部', color: '#ef4444' },
|
||
{ id: '6', nickName: '赵敏', userNo: 'U1006', dept: '研发中心', color: '#14b8a6' },
|
||
{ id: '7', nickName: '周杰', userNo: 'U1007', dept: '仓储中心', color: '#6366f1' },
|
||
{ id: '8', nickName: '吴倩', userNo: 'U1008', dept: '客服部', color: '#ec4899' },
|
||
]
|
||
|
||
const selected = new Set(['1', '2'])
|
||
const notes = {
|
||
a: '<strong>方案 A(推荐):</strong>搜索添加 + 已选人员以头像芯片展示,可单颗移除。接近钉钉/飞书体验,占高小、多选一眼可读。',
|
||
b: '<strong>方案 B:</strong>左栏可选、右栏已选,勾选即时同步。适合一次挑很多人、需要对照两侧清单的场景;抽屉较宽时更舒服。',
|
||
c: '<strong>方案 C:</strong>经典穿梭框(Transfer)。规则清晰,但控件偏重、在抽屉里略挤,且不如芯片直观展示「是谁」。',
|
||
}
|
||
|
||
let scheme = 'a'
|
||
let cPendingLeft = new Set()
|
||
let cPendingRight = new Set()
|
||
|
||
function initials(name) {
|
||
return (name || '?').slice(0, 1)
|
||
}
|
||
|
||
function userById(id) {
|
||
return USERS.find((u) => u.id === id)
|
||
}
|
||
|
||
/* —— 方案 A —— */
|
||
function renderChips() {
|
||
const box = document.getElementById('chipList')
|
||
const ids = [...selected]
|
||
document.getElementById('pickedCountA').textContent = `已选 ${ids.length} 人`
|
||
if (!ids.length) {
|
||
box.className = 'chip-list is-empty'
|
||
box.textContent = '暂未选择成员,请从上方搜索添加'
|
||
return
|
||
}
|
||
box.className = 'chip-list'
|
||
box.innerHTML = ids.map((id) => {
|
||
const u = userById(id)
|
||
if (!u) return ''
|
||
return `
|
||
<span class="chip" data-id="${u.id}">
|
||
<span class="avatar sm" style="background:${u.color}">${initials(u.nickName)}</span>
|
||
<span class="name" title="${u.nickName}(${u.userNo})">${u.nickName}</span>
|
||
<button type="button" class="rm" aria-label="移除 ${u.nickName}">×</button>
|
||
</span>`
|
||
}).join('')
|
||
box.querySelectorAll('.rm').forEach((btn) => {
|
||
btn.addEventListener('click', () => {
|
||
selected.delete(btn.closest('.chip').dataset.id)
|
||
syncAll()
|
||
})
|
||
})
|
||
}
|
||
|
||
function renderDropA(q) {
|
||
const drop = document.getElementById('dropA')
|
||
const kw = (q || '').trim().toLowerCase()
|
||
const list = USERS.filter((u) => {
|
||
if (!kw) return true
|
||
return (
|
||
u.nickName.toLowerCase().includes(kw) ||
|
||
u.userNo.toLowerCase().includes(kw) ||
|
||
u.dept.toLowerCase().includes(kw)
|
||
)
|
||
})
|
||
if (!list.length) {
|
||
drop.innerHTML = '<div class="dropdown-empty">无匹配成员</div>'
|
||
drop.classList.add('is-open')
|
||
return
|
||
}
|
||
drop.innerHTML = list.map((u) => `
|
||
<button type="button" class="dropdown-item${selected.has(u.id) ? ' is-picked' : ''}" data-id="${u.id}">
|
||
<span class="avatar" style="background:${u.color}">${initials(u.nickName)}</span>
|
||
<span class="u-meta">
|
||
<div class="name">${u.nickName}</div>
|
||
<div class="sub">${u.userNo} · ${u.dept}</div>
|
||
</span>
|
||
</button>`).join('')
|
||
drop.classList.add('is-open')
|
||
drop.querySelectorAll('.dropdown-item:not(.is-picked)').forEach((el) => {
|
||
el.addEventListener('click', () => {
|
||
selected.add(el.dataset.id)
|
||
document.getElementById('searchA').value = ''
|
||
drop.classList.remove('is-open')
|
||
syncAll()
|
||
})
|
||
})
|
||
}
|
||
|
||
/* —— 方案 B —— */
|
||
function renderB() {
|
||
const kw = (document.getElementById('searchB').value || '').trim().toLowerCase()
|
||
const left = document.getElementById('listBLeft')
|
||
const right = document.getElementById('listBRight')
|
||
document.getElementById('countB').textContent = String(selected.size)
|
||
|
||
left.innerHTML = USERS.filter((u) => {
|
||
if (selected.has(u.id)) return false
|
||
if (!kw) return true
|
||
return u.nickName.toLowerCase().includes(kw) || u.userNo.toLowerCase().includes(kw)
|
||
}).map((u) => `
|
||
<button type="button" class="picker-b-item" data-id="${u.id}">
|
||
<input type="checkbox" tabindex="-1" />
|
||
<span class="avatar sm" style="background:${u.color}">${initials(u.nickName)}</span>
|
||
<span class="u-meta">
|
||
<div class="name">${u.nickName}</div>
|
||
<div class="sub">${u.userNo}</div>
|
||
</span>
|
||
</button>`).join('') || '<div class="dropdown-empty">无更多可选</div>'
|
||
|
||
right.innerHTML = [...selected].map((id) => {
|
||
const u = userById(id)
|
||
if (!u) return ''
|
||
return `
|
||
<button type="button" class="picker-b-item" data-id="${u.id}">
|
||
<input type="checkbox" checked tabindex="-1" />
|
||
<span class="avatar sm" style="background:${u.color}">${initials(u.nickName)}</span>
|
||
<span class="u-meta">
|
||
<div class="name">${u.nickName}</div>
|
||
<div class="sub">${u.userNo}</div>
|
||
</span>
|
||
</button>`
|
||
}).join('') || '<div class="dropdown-empty">尚未选择</div>'
|
||
|
||
left.querySelectorAll('.picker-b-item').forEach((el) => {
|
||
el.addEventListener('click', () => { selected.add(el.dataset.id); syncAll() })
|
||
})
|
||
right.querySelectorAll('.picker-b-item').forEach((el) => {
|
||
el.addEventListener('click', () => { selected.delete(el.dataset.id); syncAll() })
|
||
})
|
||
}
|
||
|
||
/* —— 方案 C —— */
|
||
function renderC() {
|
||
const left = document.getElementById('listCLeft')
|
||
const right = document.getElementById('listCRight')
|
||
left.innerHTML = USERS.filter((u) => !selected.has(u.id)).map((u) => `
|
||
<label class="picker-b-item">
|
||
<input type="checkbox" data-side="left" value="${u.id}" ${cPendingLeft.has(u.id) ? 'checked' : ''} />
|
||
<span class="avatar sm" style="background:${u.color}">${initials(u.nickName)}</span>
|
||
<span class="u-meta"><div class="name">${u.nickName}</div><div class="sub">${u.userNo}</div></span>
|
||
</label>`).join('') || '<div class="dropdown-empty">无</div>'
|
||
|
||
right.innerHTML = [...selected].map((id) => {
|
||
const u = userById(id)
|
||
if (!u) return ''
|
||
return `
|
||
<label class="picker-b-item">
|
||
<input type="checkbox" data-side="right" value="${u.id}" ${cPendingRight.has(u.id) ? 'checked' : ''} />
|
||
<span class="avatar sm" style="background:${u.color}">${initials(u.nickName)}</span>
|
||
<span class="u-meta"><div class="name">${u.nickName}</div><div class="sub">${u.userNo}</div></span>
|
||
</label>`
|
||
}).join('') || '<div class="dropdown-empty">无</div>'
|
||
|
||
left.querySelectorAll('input').forEach((inp) => {
|
||
inp.addEventListener('change', () => {
|
||
if (inp.checked) cPendingLeft.add(inp.value)
|
||
else cPendingLeft.delete(inp.value)
|
||
})
|
||
})
|
||
right.querySelectorAll('input').forEach((inp) => {
|
||
inp.addEventListener('change', () => {
|
||
if (inp.checked) cPendingRight.add(inp.value)
|
||
else cPendingRight.delete(inp.value)
|
||
})
|
||
})
|
||
}
|
||
|
||
function syncAll() {
|
||
renderChips()
|
||
renderB()
|
||
renderC()
|
||
}
|
||
|
||
function setScheme(next) {
|
||
scheme = next
|
||
document.querySelectorAll('.scheme-btn').forEach((btn) => {
|
||
const on = btn.dataset.scheme === next
|
||
btn.classList.toggle('is-active', on)
|
||
btn.setAttribute('aria-selected', on ? 'true' : 'false')
|
||
})
|
||
document.getElementById('schemeNote').innerHTML = notes[next]
|
||
document.querySelectorAll('[data-panel]').forEach((el) => {
|
||
el.classList.toggle('hidden', el.dataset.panel !== next)
|
||
})
|
||
syncAll()
|
||
}
|
||
|
||
/* 事件绑定 */
|
||
document.querySelectorAll('.scheme-btn').forEach((btn) => {
|
||
btn.addEventListener('click', () => setScheme(btn.dataset.scheme))
|
||
})
|
||
|
||
const searchA = document.getElementById('searchA')
|
||
const dropA = document.getElementById('dropA')
|
||
searchA.addEventListener('focus', () => renderDropA(searchA.value))
|
||
searchA.addEventListener('input', () => renderDropA(searchA.value))
|
||
searchA.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') dropA.classList.remove('is-open')
|
||
})
|
||
document.addEventListener('click', (e) => {
|
||
if (!e.target.closest('.member-picker-a .search-box')) {
|
||
dropA.classList.remove('is-open')
|
||
}
|
||
})
|
||
|
||
document.getElementById('searchB').addEventListener('input', renderB)
|
||
|
||
document.getElementById('cToRight').addEventListener('click', () => {
|
||
cPendingLeft.forEach((id) => selected.add(id))
|
||
cPendingLeft.clear()
|
||
syncAll()
|
||
})
|
||
document.getElementById('cToLeft').addEventListener('click', () => {
|
||
cPendingRight.forEach((id) => selected.delete(id))
|
||
cPendingRight.clear()
|
||
syncAll()
|
||
})
|
||
|
||
document.getElementById('assigneeSeg').addEventListener('click', (e) => {
|
||
const btn = e.target.closest('.seg-item')
|
||
if (!btn) return
|
||
document.querySelectorAll('#assigneeSeg .seg-item').forEach((b) => b.classList.remove('is-on'))
|
||
btn.classList.add('is-on')
|
||
document.getElementById('usersField').classList.toggle('hidden', btn.dataset.type !== 'users')
|
||
})
|
||
|
||
document.getElementById('signMode').addEventListener('change', (e) => {
|
||
document.querySelectorAll('#signMode .radio-card').forEach((card) => {
|
||
card.classList.toggle('is-on', card.querySelector('input').checked)
|
||
})
|
||
})
|
||
|
||
setScheme('a')
|
||
</script>
|
||
</body>
|
||
</html>
|