Files
zkxw/docs/mockups/oa-approval-approver-drawer-demo.html
T
2026-07-15 16:20:48 +08:00

976 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>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>