| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922 |
- <!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>
- <style>
- :root {
- --primary: #10b981;
- --primary-dark: #059669;
- --primary-bg: #ecfdf5;
- --text: #0f172a;
- --text-2: #334155;
- --muted: #94a3b8;
- --border: #e2e8f0;
- --border-strong: #cbd5e1;
- --bg: #f1f5f9;
- --danger: #ef4444;
- --radius: 8px;
- }
- * { box-sizing: border-box; }
- html, body {
- margin: 0;
- min-height: 100%;
- font: 14px/1.5 Inter, system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
- color: var(--text);
- background: var(--bg);
- }
- .page {
- max-width: 1180px;
- margin: 0 auto;
- padding: 16px 20px 40px;
- }
- .banner {
- display: flex;
- flex-wrap: wrap;
- gap: 8px 16px;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 14px;
- padding: 12px 16px;
- background: #fff;
- border: 1px solid var(--border);
- border-radius: var(--radius);
- }
- .banner b { color: var(--primary-dark); }
- .banner span { color: var(--muted); font-size: 13px; }
- .tag {
- padding: 2px 10px;
- border-radius: 999px;
- background: var(--primary-bg);
- color: var(--primary-dark);
- font-size: 12px;
- font-weight: 600;
- }
- .section-title {
- margin: 22px 0 10px;
- padding-left: 10px;
- border-left: 3px solid var(--primary);
- color: #1e293b;
- font-size: 15px;
- font-weight: 600;
- }
- .section-desc {
- margin: -4px 0 12px;
- color: var(--muted);
- font-size: 13px;
- }
- /* 方案切换 */
- .mode-bar {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin-bottom: 12px;
- }
- .mode-btn {
- height: 34px;
- padding: 0 14px;
- border: 1px solid var(--border);
- border-radius: 8px;
- background: #fff;
- color: var(--text-2);
- font-size: 13px;
- font-weight: 600;
- cursor: pointer;
- }
- .mode-btn:hover { border-color: #a7f3d0; background: var(--primary-bg); }
- .mode-btn.active {
- border-color: var(--primary);
- background: var(--primary);
- color: #fff;
- }
- /* ===== 设计器三栏缩略 ===== */
- .designer {
- display: grid;
- grid-template-columns: 220px 1fr 260px;
- min-height: 420px;
- overflow: hidden;
- background: #fff;
- border: 1px solid var(--border-strong);
- border-radius: 10px;
- box-shadow: 0 2px 8px rgb(15 23 42 / 8%);
- }
- .pane {
- display: flex;
- flex-direction: column;
- min-width: 0;
- min-height: 0;
- }
- .pane + .pane { border-left: 1px solid var(--border); }
- .pane-head {
- display: flex;
- flex-shrink: 0;
- height: 42px;
- border-bottom: 1px solid var(--border);
- }
- .pane-head span {
- flex: 1;
- display: grid;
- place-items: center;
- color: var(--muted);
- font-size: 13px;
- font-weight: 600;
- }
- .pane-head span.active {
- color: var(--text);
- box-shadow: inset 0 -2px 0 var(--primary);
- }
- .pane-body {
- flex: 1;
- padding: 12px;
- overflow: auto;
- }
- .group-title {
- margin: 4px 0 8px;
- color: var(--muted);
- font-size: 12px;
- font-weight: 600;
- }
- .ctrl-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 8px;
- margin-bottom: 14px;
- }
- .ctrl {
- display: flex;
- align-items: center;
- gap: 6px;
- height: 36px;
- padding: 0 10px;
- border: 1px dashed var(--border-strong);
- border-radius: 8px;
- background: #fafafa;
- color: var(--text-2);
- font-size: 12px;
- cursor: grab;
- }
- .ctrl.highlight {
- border-style: solid;
- border-color: var(--primary);
- background: var(--primary-bg);
- color: var(--primary-dark);
- font-weight: 600;
- box-shadow: 0 0 0 2px rgb(16 185 129 / 18%);
- }
- .ctrl-ico {
- width: 18px;
- text-align: center;
- font-size: 13px;
- opacity: 0.85;
- }
- .canvas {
- padding: 16px 20px 24px;
- background: #f8fafc;
- }
- .device-toggle {
- display: inline-flex;
- gap: 4px;
- margin-bottom: 12px;
- padding: 3px;
- border: 1px solid var(--border);
- border-radius: 8px;
- background: #fff;
- }
- .device-toggle button {
- height: 28px;
- padding: 0 10px;
- border: 0;
- border-radius: 6px;
- background: transparent;
- color: var(--muted);
- font-size: 12px;
- cursor: pointer;
- }
- .device-toggle button.active {
- background: var(--primary-bg);
- color: var(--primary-dark);
- font-weight: 600;
- }
- .form-card {
- max-width: 420px;
- margin: 0 auto;
- padding: 16px;
- background: #fff;
- border: 1px solid var(--border);
- border-radius: 10px;
- }
- .field {
- margin-bottom: 14px;
- padding: 10px;
- border: 1px solid transparent;
- border-radius: 8px;
- }
- .field.active {
- border-color: var(--primary);
- background: rgb(236 253 245 / 45%);
- }
- .field label {
- display: block;
- margin-bottom: 6px;
- color: var(--text-2);
- font-size: 13px;
- font-weight: 600;
- }
- .field label .req {
- margin-left: 2px;
- color: var(--danger);
- }
- .fake-input {
- display: flex;
- align-items: center;
- justify-content: space-between;
- height: 36px;
- padding: 0 12px;
- border: 1px solid var(--border-strong);
- border-radius: 8px;
- background: #fff;
- color: var(--muted);
- font-size: 13px;
- cursor: pointer;
- }
- .fake-input.has-value { color: var(--text); }
- .fake-input .chev { color: var(--muted); font-size: 11px; }
- .prop-item { margin-bottom: 14px; }
- .prop-item > label {
- display: block;
- margin-bottom: 6px;
- color: var(--muted);
- font-size: 12px;
- font-weight: 600;
- }
- .prop-item input,
- .prop-item select {
- width: 100%;
- height: 34px;
- padding: 0 10px;
- border: 1px solid var(--border-strong);
- border-radius: 8px;
- font: inherit;
- }
- .switch-row {
- display: flex;
- align-items: center;
- gap: 8px;
- color: var(--text-2);
- font-size: 13px;
- }
- .switch {
- position: relative;
- width: 36px;
- height: 20px;
- border-radius: 999px;
- background: #cbd5e1;
- cursor: pointer;
- }
- .switch.on { background: var(--primary); }
- .switch::after {
- content: "";
- position: absolute;
- top: 2px;
- left: 2px;
- width: 16px;
- height: 16px;
- border-radius: 50%;
- background: #fff;
- transition: left 0.15s;
- }
- .switch.on::after { left: 18px; }
- /* ===== 发起页运行时 ===== */
- .runtime {
- display: grid;
- grid-template-columns: 1fr 320px;
- gap: 14px;
- }
- @media (max-width: 900px) {
- .designer { grid-template-columns: 1fr; }
- .pane + .pane { border-left: 0; border-top: 1px solid var(--border); }
- .runtime { grid-template-columns: 1fr; }
- }
- .panel {
- padding: 16px;
- background: #fff;
- border: 1px solid var(--border);
- border-radius: 10px;
- }
- .panel h3 {
- margin: 0 0 12px;
- padding-left: 10px;
- border-left: 3px solid var(--primary);
- font-size: 14px;
- font-weight: 600;
- }
- .tree-select {
- position: relative;
- }
- .tree-select-trigger {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- align-items: center;
- min-height: 36px;
- padding: 4px 10px;
- border: 1px solid var(--border-strong);
- border-radius: 8px;
- background: #fff;
- cursor: pointer;
- }
- .tree-select-trigger:hover,
- .tree-select.open .tree-select-trigger {
- border-color: var(--primary);
- }
- .tree-select-trigger .placeholder {
- color: var(--muted);
- font-size: 13px;
- }
- .chip {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- max-width: 100%;
- height: 24px;
- padding: 0 8px;
- border-radius: 6px;
- background: var(--primary-bg);
- color: var(--primary-dark);
- font-size: 12px;
- font-weight: 600;
- }
- .chip button {
- border: 0;
- background: none;
- color: var(--primary-dark);
- font-size: 14px;
- line-height: 1;
- cursor: pointer;
- opacity: 0.7;
- }
- .chip button:hover { opacity: 1; }
- .dropdown {
- display: none;
- position: absolute;
- z-index: 20;
- top: calc(100% + 4px);
- left: 0;
- right: 0;
- max-height: 280px;
- overflow: auto;
- padding: 8px;
- background: #fff;
- border: 1px solid var(--border-strong);
- border-radius: 10px;
- box-shadow: 0 8px 24px rgb(15 23 42 / 12%);
- }
- .tree-select.open .dropdown { display: block; }
- .search {
- width: 100%;
- height: 32px;
- margin-bottom: 8px;
- padding: 0 10px;
- border: 1px solid var(--border);
- border-radius: 8px;
- font: inherit;
- }
- .tree-node {
- display: flex;
- align-items: center;
- gap: 6px;
- min-height: 32px;
- padding: 4px 8px;
- border-radius: 6px;
- cursor: pointer;
- user-select: none;
- }
- .tree-node:hover { background: #f1f5f9; }
- .tree-node.selected {
- background: var(--primary-bg);
- color: var(--primary-dark);
- font-weight: 600;
- }
- .tree-node .twisty {
- width: 14px;
- color: var(--muted);
- font-size: 10px;
- text-align: center;
- }
- .tree-node .check {
- width: 14px;
- height: 14px;
- border: 1.5px solid var(--border-strong);
- border-radius: 3px;
- flex-shrink: 0;
- }
- .tree-node.selected .check {
- border-color: var(--primary);
- background: var(--primary);
- box-shadow: inset 0 0 0 2px #fff;
- }
- .tree-children { margin-left: 18px; }
- .tree-children.collapsed { display: none; }
- .kv {
- display: grid;
- gap: 10px;
- font-size: 13px;
- }
- .kv dt { color: var(--muted); font-size: 12px; }
- .kv dd {
- margin: 2px 0 0;
- padding: 8px 10px;
- border-radius: 8px;
- background: #f8fafc;
- color: var(--text);
- word-break: break-all;
- }
- .kv dd code {
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
- font-size: 12px;
- color: var(--primary-dark);
- }
- .note {
- margin-top: 12px;
- padding: 10px 12px;
- border-radius: 8px;
- background: #fffbeb;
- border: 1px solid #fde68a;
- color: #92400e;
- font-size: 12px;
- line-height: 1.55;
- }
- .footer-actions {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- margin-top: 16px;
- }
- .btn {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- height: 34px;
- padding: 0 14px;
- border: 1px solid var(--border);
- border-radius: 8px;
- background: #fff;
- color: var(--text-2);
- font-size: 13px;
- cursor: pointer;
- }
- .btn.primary {
- border-color: var(--primary);
- background: var(--primary);
- color: #fff;
- }
- </style>
- </head>
- <body>
- <div class="page">
- <div class="banner">
- <div>
- <b>OA 表单设计 ·「部门选择」控件 Demo</b>
- <span>数据源:本租户组织树(sys_dept / fetchDeptTree)· 选中展示全路径</span>
- </div>
- <span class="tag">Fresh Mint</span>
- </div>
- <div class="mode-bar" role="tablist" aria-label="选择模式">
- <button type="button" class="mode-btn active" data-mode="single">方案 A · 单选部门</button>
- <button type="button" class="mode-btn" data-mode="multi">方案 B · 可多选部门</button>
- <button type="button" class="mode-btn" data-mode="dialog">方案 C · 弹窗树选择</button>
- </div>
- <h2 class="section-title">① 设计器:「2 表单设计」左侧组件库</h2>
- <p class="section-desc">「部门」出现在增强分组;拖到画布后,右侧可配标题 / 必填 / 单多选等。</p>
- <div class="designer">
- <aside class="pane">
- <div class="pane-head"><span class="active">组件</span><span>大纲</span></div>
- <div class="pane-body">
- <div class="group-title">基础</div>
- <div class="ctrl-grid">
- <div class="ctrl"><span class="ctrl-ico">Aa</span>单行</div>
- <div class="ctrl"><span class="ctrl-ico">¶</span>多行</div>
- <div class="ctrl"><span class="ctrl-ico">123</span>数字</div>
- <div class="ctrl"><span class="ctrl-ico">📅</span>日期</div>
- </div>
- <div class="group-title">增强</div>
- <div class="ctrl-grid">
- <div class="ctrl"><span class="ctrl-ico">📎</span>附件</div>
- <div class="ctrl highlight"><span class="ctrl-ico">🏢</span>部门</div>
- <div class="ctrl"><span class="ctrl-ico">📍</span>省市区</div>
- <div class="ctrl"><span class="ctrl-ico">ƒ</span>公式</div>
- <div class="ctrl"><span class="ctrl-ico">🔗</span>关联审批</div>
- </div>
- </div>
- </aside>
- <main class="pane">
- <div class="pane-head"><span class="active">画布 · PC</span></div>
- <div class="canvas">
- <div class="device-toggle">
- <button type="button" class="active">🖥 PC</button>
- <button type="button">📱 手机</button>
- </div>
- <div class="form-card">
- <div class="field">
- <label>申请事由</label>
- <div class="fake-input">请输入<span class="chev">▾</span></div>
- </div>
- <div class="field active">
- <label>所属部门<span class="req">*</span></label>
- <div class="fake-input has-value" id="canvasPreview">
- 中科兴皖 / 研发中心 / 前端组
- <span class="chev">▾</span>
- </div>
- </div>
- <div class="field">
- <label>说明</label>
- <div class="fake-input">请输入<span class="chev">▾</span></div>
- </div>
- </div>
- </div>
- </main>
- <aside class="pane">
- <div class="pane-head"><span class="active">控件</span><span>表单</span></div>
- <div class="pane-body">
- <div class="prop-item">
- <label>标题</label>
- <input value="所属部门" />
- </div>
- <div class="prop-item">
- <label>字段名</label>
- <input value="deptId" />
- </div>
- <div class="prop-item">
- <label>占位提示</label>
- <input value="请选择部门" />
- </div>
- <div class="prop-item">
- <label>必填</label>
- <div class="switch-row">
- <span class="switch on" id="propRequired"></span>
- <span>是</span>
- </div>
- </div>
- <div class="prop-item">
- <label>选择模式</label>
- <select id="propMode" disabled>
- <option value="single">仅单选</option>
- <option value="multi">允许多选</option>
- </select>
- </div>
- <div class="note">
- 下拉节点只显示本级名称;输入框 / 发起页 / 详情 / 打印显示<strong>全路径</strong>(分隔符 <code> / </code>)。
- </div>
- </div>
- </aside>
- </div>
- <h2 class="section-title">② 发起页:运行时选择(可点选试用)</h2>
- <p class="section-desc">左侧交互;右侧同步展示将写入 <code>form_data</code> 的值形态(建议锁定一种契约)。</p>
- <div class="runtime">
- <div class="panel">
- <h3>填报表单</h3>
- <div class="prop-item">
- <label style="margin-bottom:8px;color:var(--text-2);font-size:13px;font-weight:600">
- 所属部门<span class="req" style="color:var(--danger)">*</span>
- </label>
- <div class="tree-select" id="deptSelect">
- <div class="tree-select-trigger" id="deptTrigger" tabindex="0">
- <span class="placeholder">请选择部门</span>
- </div>
- <div class="dropdown" id="deptDropdown">
- <input class="search" id="deptSearch" type="search" placeholder="搜索部门名称" />
- <div id="deptTree"></div>
- </div>
- </div>
- </div>
- <div class="footer-actions">
- <button type="button" class="btn">取消</button>
- <button type="button" class="btn primary" id="btnSubmit">✓ 提交</button>
- </div>
- </div>
- <div class="panel">
- <h3>form_data 预览</h3>
- <dl class="kv">
- <div>
- <dt>展示文案(全路径)</dt>
- <dd id="outLabel">—</dd>
- </div>
- <div>
- <dt>存库值(推荐)</dt>
- <dd id="outValue"><code>—</code></dd>
- </div>
- <div>
- <dt>当前方案说明</dt>
- <dd id="outNote">单选:值为部门 id 字符串。</dd>
- </div>
- </dl>
- <div class="note">
- 规格草案:部门控件 <code>type = "dept"</code>(后端发布白名单已预留)。
- 勿存组件实例 / DOM;名称可按 id 在详情侧补全路径。
- </div>
- </div>
- </div>
- </div>
- <!-- 弹窗方案 C -->
- <dialog id="deptDialog" style="
- border:0;border-radius:12px;padding:0;width:min(440px,92vw);
- box-shadow:0 16px 48px rgb(15 23 42 / 20%);
- ">
- <form method="dialog" style="padding:16px 18px 18px">
- <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
- <strong style="font-size:15px">选择部门</strong>
- <button value="cancel" class="btn" style="height:28px;padding:0 8px">✕</button>
- </div>
- <input class="search" id="dialogSearch" type="search" placeholder="搜索部门名称" style="margin-bottom:10px" />
- <div id="dialogTree" style="max-height:320px;overflow:auto;border:1px solid var(--border);border-radius:8px;padding:8px"></div>
- <div class="footer-actions">
- <button value="cancel" class="btn">取消</button>
- <button value="ok" class="btn primary" id="dialogOk">确定</button>
- </div>
- </form>
- </dialog>
- <script>
- /** 模拟本租户组织树 */
- const DEPT_TREE = [
- {
- id: 'd1', name: '中科兴皖',
- children: [
- {
- id: 'd2', name: '研发中心',
- children: [
- { id: 'd3', name: '前端组' },
- { id: 'd4', name: '后端组' }
- ]
- },
- {
- id: 'd5', name: '质量中心',
- children: [
- { id: 'd6', name: '实验室' },
- { id: 'd7', name: '体系办' }
- ]
- },
- { id: 'd8', name: '行政人事部' }
- ]
- }
- ]
- const pathMap = {}
- function walk(nodes, ancestors) {
- nodes.forEach((n) => {
- const path = [...ancestors, n.name]
- pathMap[n.id] = path.join(' / ')
- if (n.children) walk(n.children, path)
- })
- }
- walk(DEPT_TREE, [])
- let mode = 'single' // single | multi | dialog
- let selected = new Set(['d3'])
- let dialogPick = new Set()
- const modeBtns = document.querySelectorAll('.mode-btn')
- const propMode = document.getElementById('propMode')
- const canvasPreview = document.getElementById('canvasPreview')
- const deptSelect = document.getElementById('deptSelect')
- const deptTrigger = document.getElementById('deptTrigger')
- const deptTree = document.getElementById('deptTree')
- const deptSearch = document.getElementById('deptSearch')
- const outLabel = document.getElementById('outLabel')
- const outValue = document.getElementById('outValue')
- const outNote = document.getElementById('outNote')
- const deptDialog = document.getElementById('deptDialog')
- const dialogTree = document.getElementById('dialogTree')
- const dialogSearch = document.getElementById('dialogSearch')
- modeBtns.forEach((btn) => {
- btn.addEventListener('click', () => {
- modeBtns.forEach((b) => b.classList.remove('active'))
- btn.classList.add('active')
- mode = btn.dataset.mode
- propMode.value = mode === 'multi' ? 'multi' : 'single'
- selected = mode === 'multi' ? new Set(['d3', 'd6']) : new Set(['d3'])
- deptSelect.classList.remove('open')
- renderAll()
- })
- })
- function filterTree(nodes, q) {
- if (!q) return nodes
- const lower = q.toLowerCase()
- function filt(list) {
- const out = []
- list.forEach((n) => {
- const kids = n.children ? filt(n.children) : []
- if (n.name.toLowerCase().includes(lower) || kids.length) {
- out.push({ ...n, children: kids.length ? kids : n.children })
- }
- })
- return out
- }
- return filt(nodes)
- }
- function renderTree(container, nodes, opts) {
- const { multi, query, onPick } = opts
- container.innerHTML = ''
- const data = filterTree(nodes, query)
- function renderNodes(list, parentEl) {
- list.forEach((n) => {
- const row = document.createElement('div')
- row.className = 'tree-node' + (selected.has(n.id) || (opts.pickSet && opts.pickSet.has(n.id)) ? ' selected' : '')
- row.style.paddingLeft = '8px'
- const hasKids = !!(n.children && n.children.length)
- row.innerHTML =
- `<span class="twisty">${hasKids ? '▾' : ''}</span>` +
- (multi ? '<span class="check"></span>' : '') +
- `<span>${n.name}</span>`
- row.addEventListener('click', (e) => {
- e.stopPropagation()
- onPick(n.id)
- })
- parentEl.appendChild(row)
- if (hasKids) {
- const wrap = document.createElement('div')
- wrap.className = 'tree-children'
- parentEl.appendChild(wrap)
- renderNodes(n.children, wrap)
- }
- })
- }
- if (!data.length) {
- container.innerHTML = '<div style="padding:12px;color:var(--muted);font-size:13px">无匹配部门</div>'
- return
- }
- renderNodes(data, container)
- }
- function syncOutput() {
- const ids = [...selected]
- const labels = ids.map((id) => pathMap[id] || id)
- outLabel.textContent = labels.length ? labels.join(';') : '—'
- canvasPreview.innerHTML =
- (labels[0] || '请选择部门') + '<span class="chev">▾</span>'
- canvasPreview.classList.toggle('has-value', !!ids.length)
- if (mode === 'multi') {
- outValue.innerHTML = `<code>${JSON.stringify(ids)}</code>`
- outNote.textContent = '多选:值为 deptId 字符串数组。详情按 id 补全全路径。'
- } else {
- outValue.innerHTML = `<code>${ids[0] ? JSON.stringify(ids[0]) : '""'}</code>`
- outNote.textContent =
- mode === 'dialog'
- ? '弹窗单选:值为部门 id 字符串;窄屏/手机更友好。'
- : '单选:值为部门 id 字符串(与用户管理「所属组织」一致)。'
- }
- }
- function renderTrigger() {
- const ids = [...selected]
- deptTrigger.innerHTML = ''
- if (!ids.length) {
- const ph = document.createElement('span')
- ph.className = 'placeholder'
- ph.textContent = '请选择部门'
- deptTrigger.appendChild(ph)
- return
- }
- if (mode === 'multi') {
- ids.forEach((id) => {
- const chip = document.createElement('span')
- chip.className = 'chip'
- chip.innerHTML = `${pathMap[id]}<button type="button" aria-label="移除">×</button>`
- chip.querySelector('button').addEventListener('click', (e) => {
- e.stopPropagation()
- selected.delete(id)
- renderAll()
- })
- deptTrigger.appendChild(chip)
- })
- } else {
- const chip = document.createElement('span')
- chip.className = 'chip'
- chip.textContent = pathMap[ids[0]]
- deptTrigger.appendChild(chip)
- }
- }
- function renderAll() {
- const multi = mode === 'multi'
- renderTree(deptTree, DEPT_TREE, {
- multi,
- query: deptSearch.value.trim(),
- onPick: (id) => {
- if (mode === 'dialog') return
- if (multi) {
- if (selected.has(id)) selected.delete(id)
- else selected.add(id)
- } else {
- selected = new Set([id])
- deptSelect.classList.remove('open')
- }
- renderAll()
- }
- })
- renderTrigger()
- syncOutput()
- }
- deptTrigger.addEventListener('click', () => {
- if (mode === 'dialog') {
- dialogPick = new Set(selected)
- dialogSearch.value = ''
- renderDialogTree()
- deptDialog.showModal()
- return
- }
- deptSelect.classList.toggle('open')
- })
- document.addEventListener('click', (e) => {
- if (!deptSelect.contains(e.target)) deptSelect.classList.remove('open')
- })
- deptSearch.addEventListener('input', renderAll)
- function renderDialogTree() {
- renderTree(dialogTree, DEPT_TREE, {
- multi: false,
- query: dialogSearch.value.trim(),
- pickSet: dialogPick,
- onPick: (id) => {
- dialogPick = new Set([id])
- renderDialogTree()
- }
- })
- // highlight based on dialogPick
- dialogTree.querySelectorAll('.tree-node').forEach((el) => {
- /* re-render already sets selected from selected; override */
- })
- // re-render with temporary selected swap
- const bak = selected
- selected = dialogPick
- renderTree(dialogTree, DEPT_TREE, {
- multi: false,
- query: dialogSearch.value.trim(),
- onPick: (id) => {
- dialogPick = new Set([id])
- selected = dialogPick
- renderDialogTree()
- }
- })
- selected = bak
- }
- dialogSearch.addEventListener('input', renderDialogTree)
- document.getElementById('dialogOk').addEventListener('click', (e) => {
- e.preventDefault()
- selected = new Set(dialogPick)
- deptDialog.close()
- renderAll()
- })
- document.getElementById('btnSubmit').addEventListener('click', () => {
- if (!selected.size) {
- alert('请先选择部门')
- return
- }
- alert('Demo:将提交 form_data →\n' + outValue.textContent)
- })
- document.getElementById('propRequired').addEventListener('click', function () {
- this.classList.toggle('on')
- })
- renderAll()
- </script>
- </body>
- </html>
|