| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975 |
- <!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>
|