|
|
@@ -0,0 +1,922 @@
|
|
|
+<!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>
|