oa-approval-condition-from-form-demo.html 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1" />
  6. <title>OA 条件分支 · 表单字段驱动 Demo</title>
  7. <style>
  8. :root {
  9. --primary: #10b981;
  10. --primary-dark: #059669;
  11. --primary-bg: #ecfdf5;
  12. --text: #0f172a;
  13. --text-2: #334155;
  14. --muted: #64748b;
  15. --border: #e2e8f0;
  16. --border-strong: #cbd5e1;
  17. --bg: #f1f5f9;
  18. --warn: #d97706;
  19. --radius: 8px;
  20. }
  21. * { box-sizing: border-box; }
  22. body {
  23. margin: 0;
  24. font: 14px/1.5 Inter, system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
  25. color: var(--text);
  26. background: var(--bg);
  27. }
  28. .page { max-width: 1180px; margin: 0 auto; padding: 16px 20px 40px; }
  29. .banner {
  30. display: flex; flex-wrap: wrap; gap: 10px 16px; align-items: flex-start;
  31. justify-content: space-between;
  32. padding: 12px 16px; margin-bottom: 14px;
  33. background: #fff; border: 1px solid var(--border); border-radius: 10px;
  34. }
  35. .banner h1 { margin: 0 0 4px; font-size: 16px; }
  36. .banner p { margin: 0; color: var(--muted); font-size: 13px; max-width: 720px; }
  37. .tag {
  38. display: inline-block; padding: 2px 8px; border-radius: 999px;
  39. background: var(--primary-bg); color: var(--primary-dark);
  40. font-size: 12px; font-weight: 600;
  41. }
  42. .panel {
  43. background: #fff; border: 1px solid var(--border); border-radius: 10px;
  44. overflow: hidden;
  45. }
  46. .toolbar {
  47. display: flex; align-items: center; justify-content: space-between;
  48. padding: 10px 14px; border-bottom: 1px solid var(--border);
  49. }
  50. .btn {
  51. display: inline-flex; align-items: center; gap: 6px;
  52. height: 34px; padding: 0 12px;
  53. border: 1px solid var(--border); border-radius: 8px;
  54. background: #fff; color: var(--text-2); cursor: pointer; font-size: 13px;
  55. }
  56. .btn:hover { border-color: var(--primary); color: var(--primary-dark); }
  57. .btn.primary {
  58. background: var(--primary); border-color: var(--primary); color: #fff;
  59. }
  60. .btn.primary:hover { background: var(--primary-dark); }
  61. .btn.sm { height: 28px; padding: 0 8px; font-size: 12px; }
  62. .tabs {
  63. display: flex; gap: 0; border-bottom: 1px solid var(--border);
  64. padding: 0 12px; background: #fafafa;
  65. }
  66. .tab {
  67. border: 0; background: transparent; padding: 12px 14px;
  68. color: var(--muted); cursor: pointer; font-size: 13px;
  69. border-bottom: 2px solid transparent; margin-bottom: -1px;
  70. }
  71. .tab.active {
  72. color: var(--primary-dark); font-weight: 600;
  73. border-bottom-color: var(--primary);
  74. }
  75. .step-body { display: none; min-height: 520px; }
  76. .step-body.show { display: block; }
  77. /* ===== 表单设计 ===== */
  78. .form-layout {
  79. display: grid;
  80. grid-template-columns: 200px 1fr 280px;
  81. min-height: 520px;
  82. }
  83. .palette, .props {
  84. background: #fafafa; padding: 12px;
  85. border-right: 1px solid var(--border);
  86. }
  87. .props { border-right: 0; border-left: 1px solid var(--border); }
  88. .palette h3, .props h3 {
  89. margin: 0 0 10px; font-size: 12px; color: var(--muted);
  90. font-weight: 600; letter-spacing: .02em;
  91. }
  92. .palette-item {
  93. padding: 8px 10px; margin-bottom: 6px;
  94. background: #fff; border: 1px solid var(--border); border-radius: 6px;
  95. font-size: 13px; color: var(--text-2);
  96. }
  97. .canvas { padding: 20px 24px; }
  98. .canvas-title { font-size: 15px; font-weight: 600; margin-bottom: 16px; }
  99. .field-card {
  100. position: relative;
  101. padding: 12px 14px; margin-bottom: 10px;
  102. border: 1px solid var(--border); border-radius: 8px;
  103. cursor: pointer; background: #fff;
  104. }
  105. .field-card:hover { border-color: #a7f3d0; }
  106. .field-card.selected {
  107. border-color: var(--primary);
  108. box-shadow: 0 0 0 2px rgba(16, 185, 129, .15);
  109. }
  110. .field-card .label { font-size: 12px; color: var(--muted); margin-bottom: 6px; }
  111. .field-card .fake-input {
  112. height: 36px; border: 1px solid var(--border); border-radius: 8px;
  113. background: #f8fafc; display: flex; align-items: center; padding: 0 10px;
  114. color: var(--muted); font-size: 13px;
  115. }
  116. .field-card .badge {
  117. position: absolute; top: 8px; right: 8px;
  118. font-size: 11px; padding: 1px 6px; border-radius: 4px;
  119. background: var(--primary-bg); color: var(--primary-dark);
  120. display: none;
  121. }
  122. .field-card.in-flow .badge { display: inline-block; }
  123. .prop-row { margin-bottom: 14px; }
  124. .prop-row label {
  125. display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px;
  126. }
  127. .prop-row input, .prop-row select {
  128. width: 100%; height: 34px; padding: 0 10px;
  129. border: 1px solid var(--border); border-radius: 8px; font-size: 13px;
  130. }
  131. .switch-row {
  132. display: flex; align-items: flex-start; gap: 10px;
  133. padding: 12px; border: 1px dashed #a7f3d0; border-radius: 8px;
  134. background: var(--primary-bg);
  135. }
  136. .switch-row input { margin-top: 3px; width: 16px; height: 16px; accent-color: var(--primary); }
  137. .switch-row .t { font-size: 13px; font-weight: 600; color: var(--primary-dark); }
  138. .switch-row .d { font-size: 12px; color: var(--muted); margin-top: 2px; }
  139. .hint {
  140. margin-top: 12px; padding: 10px 12px; font-size: 12px; color: var(--muted);
  141. background: #fff; border: 1px solid var(--border); border-radius: 8px;
  142. }
  143. /* ===== 流程设计 ===== */
  144. .flow-layout {
  145. display: grid; grid-template-columns: 1fr 340px; min-height: 520px;
  146. }
  147. .flow-tree {
  148. padding: 28px 24px; display: flex; flex-direction: column; align-items: center;
  149. background: linear-gradient(180deg, #f8fafc, #fff);
  150. }
  151. .node {
  152. width: 220px; border-radius: 10px; background: #fff;
  153. border: 1px solid var(--border); overflow: hidden; cursor: pointer;
  154. box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
  155. }
  156. .node:hover { border-color: #a7f3d0; }
  157. .node.active { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(16,185,129,.2); }
  158. .node .hd {
  159. padding: 6px 10px; font-size: 12px; font-weight: 600; color: #fff;
  160. }
  161. .node.start .hd { background: #64748b; }
  162. .node.approval .hd { background: var(--primary); }
  163. .node.condition .hd { background: #f59e0b; }
  164. .node.end .hd { background: #94a3b8; }
  165. .node .bd { padding: 10px 12px; font-size: 12px; color: var(--muted); }
  166. .vline {
  167. width: 2px; height: 18px; background: var(--border-strong);
  168. }
  169. .plus {
  170. width: 24px; height: 24px; border-radius: 50%;
  171. border: 1px solid var(--border); background: #fff;
  172. display: flex; align-items: center; justify-content: center;
  173. color: var(--primary-dark); font-weight: 700; font-size: 14px;
  174. }
  175. .branches {
  176. display: flex; gap: 24px; align-items: flex-start; margin-top: 8px;
  177. position: relative;
  178. }
  179. .branches::before {
  180. content: ""; position: absolute; top: 0; left: 25%; right: 25%;
  181. height: 2px; background: var(--border-strong);
  182. }
  183. .branch {
  184. display: flex; flex-direction: column; align-items: center; width: 200px;
  185. }
  186. .branch-tag {
  187. margin: 8px 0; padding: 4px 10px; border-radius: 999px;
  188. background: #fffbeb; border: 1px solid #fde68a;
  189. color: var(--warn); font-size: 12px; font-weight: 600;
  190. max-width: 190px; text-align: center; cursor: pointer;
  191. }
  192. .branch-tag.default { background: #f1f5f9; border-color: var(--border); color: var(--muted); }
  193. .branch-tag.active {
  194. outline: 2px solid var(--primary); outline-offset: 2px;
  195. }
  196. .drawer {
  197. border-left: 1px solid var(--border); background: #fff; padding: 14px 16px;
  198. display: flex; flex-direction: column; gap: 12px;
  199. }
  200. .drawer h3 { margin: 0; font-size: 15px; }
  201. .drawer .sub { margin: 0; font-size: 12px; color: var(--muted); }
  202. .legacy {
  203. padding: 10px; border-radius: 8px; background: #fff7ed; border: 1px solid #fed7aa;
  204. font-size: 12px; color: #9a3412;
  205. }
  206. .legacy code {
  207. display: block; margin-top: 6px; padding: 6px 8px;
  208. background: #fff; border-radius: 6px; font-family: ui-monospace, monospace;
  209. color: var(--text);
  210. }
  211. .cond-box {
  212. border: 1px solid var(--border); border-radius: 8px; padding: 12px;
  213. background: #fafafa;
  214. }
  215. .cond-box .title {
  216. display: flex; justify-content: space-between; align-items: center;
  217. margin-bottom: 10px; font-weight: 600; font-size: 13px;
  218. }
  219. .rule-row {
  220. display: grid; grid-template-columns: 1fr auto 1fr;
  221. gap: 6px; margin-bottom: 8px; align-items: center;
  222. }
  223. .rule-row select, .rule-row input {
  224. height: 32px; border: 1px solid var(--border); border-radius: 6px;
  225. padding: 0 8px; font-size: 12px; background: #fff; width: 100%;
  226. }
  227. .rule-row select:disabled, .rule-row input:disabled {
  228. background: #f1f5f9; color: var(--muted);
  229. }
  230. .preview {
  231. margin-top: 8px; padding: 8px 10px; border-radius: 6px;
  232. background: var(--primary-bg); color: var(--primary-dark);
  233. font-size: 12px;
  234. }
  235. .empty-tip {
  236. padding: 10px; font-size: 12px; color: var(--warn);
  237. background: #fffbeb; border-radius: 6px; border: 1px dashed #fde68a;
  238. }
  239. .footer-note {
  240. margin-top: 14px; font-size: 12px; color: var(--muted); text-align: center;
  241. }
  242. </style>
  243. </head>
  244. <body>
  245. <div class="page">
  246. <div class="banner">
  247. <div>
  248. <h1>条件分支:从表单字段点选配置(交互 Demo)</h1>
  249. <p>
  250. 对照现状「手写 <code>{days} &gt; 5</code>」。本 Demo 演示:在<strong>表单设计</strong>勾选参与分支的字段 →
  251. 在<strong>流程设计</strong>用「字段 + 运算符 + 值」配置条件,无需记变量名。
  252. </p>
  253. </div>
  254. <span class="tag">Fresh Mint · 可点击</span>
  255. </div>
  256. <div class="panel">
  257. <div class="toolbar">
  258. <button class="btn" type="button" disabled>← 返回</button>
  259. <div style="display:flex;gap:8px;">
  260. <button class="btn" type="button">保存草稿</button>
  261. <button class="btn primary" type="button">发布</button>
  262. </div>
  263. </div>
  264. <div class="tabs">
  265. <button class="tab" type="button" data-step="basic">1 基础设置</button>
  266. <button class="tab active" type="button" data-step="form">2 表单设计</button>
  267. <button class="tab" type="button" data-step="flow">3 流程设计</button>
  268. <button class="tab" type="button" data-step="advanced">4 高级设置</button>
  269. </div>
  270. <!-- 表单设计 -->
  271. <div id="step-form" class="step-body show">
  272. <div class="form-layout">
  273. <aside class="palette">
  274. <h3>控件库</h3>
  275. <div class="palette-item">单行输入</div>
  276. <div class="palette-item">数字</div>
  277. <div class="palette-item">金额</div>
  278. <div class="palette-item">单选</div>
  279. <div class="palette-item">日期</div>
  280. <div class="palette-item">附件</div>
  281. </aside>
  282. <main class="canvas">
  283. <div class="canvas-title">请假申请 · 表单画布</div>
  284. <div
  285. class="field-card selected in-flow"
  286. data-field="days"
  287. data-title="请假天数"
  288. data-type="number"
  289. onclick="selectField(this)"
  290. >
  291. <span class="badge">用于分支</span>
  292. <div class="label">请假天数 *</div>
  293. <div class="fake-input">请输入数字</div>
  294. </div>
  295. <div
  296. class="field-card"
  297. data-field="reason"
  298. data-title="请假事由"
  299. data-type="text"
  300. onclick="selectField(this)"
  301. >
  302. <span class="badge">用于分支</span>
  303. <div class="label">请假事由 *</div>
  304. <div class="fake-input">请输入</div>
  305. </div>
  306. <div
  307. class="field-card in-flow"
  308. data-field="leaveType"
  309. data-title="请假类型"
  310. data-type="select"
  311. onclick="selectField(this)"
  312. >
  313. <span class="badge">用于分支</span>
  314. <div class="label">请假类型</div>
  315. <div class="fake-input">事假 / 病假 / 年假</div>
  316. </div>
  317. <div
  318. class="field-card"
  319. data-field="attach"
  320. data-title="附件"
  321. data-type="attachment"
  322. onclick="selectField(this)"
  323. >
  324. <span class="badge">用于分支</span>
  325. <div class="label">附件</div>
  326. <div class="fake-input">上传附件</div>
  327. </div>
  328. </main>
  329. <aside class="props">
  330. <h3>字段属性</h3>
  331. <div class="prop-row">
  332. <label>字段标题</label>
  333. <input id="prop-title" value="请假天数" />
  334. </div>
  335. <div class="prop-row">
  336. <label>字段标识(系统用,用户无需关心)</label>
  337. <input id="prop-field" value="days" readonly style="color:var(--muted)" />
  338. </div>
  339. <div class="switch-row">
  340. <input id="prop-in-flow" type="checkbox" checked onchange="toggleInFlow()" />
  341. <div>
  342. <div class="t">参与流程条件分支</div>
  343. <div class="d">勾选后,可在「3 流程设计」的分支条件里选到此字段,无需手写变量名。</div>
  344. </div>
  345. </div>
  346. <div class="hint" id="prop-hint">
  347. 数字 / 金额 / 单选 / 日期等适合做条件;附件类控件一般不可勾选(本 Demo 里附件勾选会提示)。
  348. </div>
  349. </aside>
  350. </div>
  351. </div>
  352. <!-- 流程设计 -->
  353. <div id="step-flow" class="step-body">
  354. <div class="flow-layout">
  355. <div class="flow-tree">
  356. <div class="node start">
  357. <div class="hd">发起人</div>
  358. <div class="bd">所有人可发起</div>
  359. </div>
  360. <div class="vline"></div>
  361. <div class="plus">+</div>
  362. <div class="vline"></div>
  363. <div class="node condition active" onclick="focusCondition()">
  364. <div class="hd">条件分支</div>
  365. <div class="bd" id="cond-summary">2 个分支 · 点击右侧配置</div>
  366. </div>
  367. <div class="branches">
  368. <div class="branch">
  369. <span class="branch-tag active" id="tag-0" onclick="selectBranch(0)">请假天数 &gt; 3</span>
  370. <div class="vline"></div>
  371. <div class="node approval">
  372. <div class="hd">审批人</div>
  373. <div class="bd">总经理</div>
  374. </div>
  375. <div class="vline"></div>
  376. <div class="node end"><div class="hd">结束</div><div class="bd">流程结束</div></div>
  377. </div>
  378. <div class="branch">
  379. <span class="branch-tag default" id="tag-1" onclick="selectBranch(1)">默认 · 其他情况</span>
  380. <div class="vline"></div>
  381. <div class="node approval">
  382. <div class="hd">审批人</div>
  383. <div class="bd">部门负责人</div>
  384. </div>
  385. <div class="vline"></div>
  386. <div class="node end"><div class="hd">结束</div><div class="bd">流程结束</div></div>
  387. </div>
  388. </div>
  389. </div>
  390. <aside class="drawer">
  391. <h3>配置条件分支</h3>
  392. <p class="sub">用表单里勾选了「参与流程条件分支」的字段来搭建规则</p>
  393. <div class="legacy">
  394. <strong>现状(不好用)</strong>
  395. <code>{days} &gt; 3</code>
  396. 普通用户不知道 days 从哪来、运算符怎么写。
  397. </div>
  398. <div class="prop-row">
  399. <label>当前编辑的分支</label>
  400. <select id="branch-pick" onchange="selectBranch(+this.value)">
  401. <option value="0">分支 1 · 大于 3 天</option>
  402. <option value="1">分支 2 · 默认</option>
  403. </select>
  404. </div>
  405. <div class="cond-box" id="rule-editor">
  406. <div class="title">
  407. <span>条件规则</span>
  408. <button class="btn sm" type="button" onclick="addRule()" id="btn-add-rule">+ 且条件</button>
  409. </div>
  410. <div id="rules"></div>
  411. <div class="preview" id="rule-preview">预览:请假天数 &gt; 3</div>
  412. </div>
  413. <div class="prop-row">
  414. <label><input type="checkbox" id="is-default" onchange="toggleDefault()" /> 设为默认分支(其他条件都不满足时走这里)</label>
  415. </div>
  416. <div class="empty-tip" id="no-fields-tip" style="display:none">
  417. 暂无可用字段。请先到「2 表单设计」勾选至少一个「参与流程条件分支」的字段。
  418. </div>
  419. <button class="btn primary" type="button" style="align-self:flex-start">保存节点配置</button>
  420. </aside>
  421. </div>
  422. </div>
  423. <div id="step-basic" class="step-body" style="padding:48px;text-align:center;color:var(--muted)">
  424. (本 Demo 不展开)基础设置:图标、名称、说明…
  425. </div>
  426. <div id="step-advanced" class="step-body" style="padding:48px;text-align:center;color:var(--muted)">
  427. (本 Demo 不展开)高级设置:…</div>
  428. </div>
  429. <p class="footer-note">
  430. 文件位置:<code>docs/mockups/oa-approval-condition-from-form-demo.html</code>
  431. · 试用:切到「2 表单设计」勾选字段 → 再切「3 流程设计」拉下拉里应能看到标题而非 variable 名
  432. </p>
  433. </div>
  434. <script>
  435. /** 画布字段状态 */
  436. const fields = {
  437. days: { title: '请假天数', type: 'number', inFlow: true },
  438. reason: { title: '请假事由', type: 'text', inFlow: false },
  439. leaveType: { title: '请假类型', type: 'select', inFlow: true, options: ['事假', '病假', '年假'] },
  440. attach: { title: '附件', type: 'attachment', inFlow: false }
  441. }
  442. let selectedField = 'days'
  443. let branchIdx = 0
  444. const branches = [
  445. {
  446. name: '大于 3 天',
  447. isDefault: false,
  448. rules: [{ field: 'days', op: '>', value: '3' }]
  449. },
  450. {
  451. name: '默认',
  452. isDefault: true,
  453. rules: []
  454. }
  455. ]
  456. const OPS = {
  457. number: [
  458. { v: '>', t: '大于' },
  459. { v: '>=', t: '大于等于' },
  460. { v: '<', t: '小于' },
  461. { v: '<=', t: '小于等于' },
  462. { v: '==', t: '等于' },
  463. { v: '!=', t: '不等于' }
  464. ],
  465. text: [
  466. { v: '==', t: '等于' },
  467. { v: '!=', t: '不等于' },
  468. { v: 'contains', t: '包含' }
  469. ],
  470. select: [
  471. { v: '==', t: '等于' },
  472. { v: '!=', t: '不等于' }
  473. ]
  474. }
  475. document.querySelectorAll('.tab').forEach((tab) => {
  476. tab.addEventListener('click', () => {
  477. document.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'))
  478. tab.classList.add('active')
  479. const step = tab.dataset.step
  480. document.querySelectorAll('.step-body').forEach((el) => el.classList.remove('show'))
  481. const body = document.getElementById('step-' + step)
  482. if (body) body.classList.add('show')
  483. if (step === 'flow') renderRules()
  484. })
  485. })
  486. function selectField(el) {
  487. document.querySelectorAll('.field-card').forEach((c) => c.classList.remove('selected'))
  488. el.classList.add('selected')
  489. selectedField = el.dataset.field
  490. const f = fields[selectedField]
  491. document.getElementById('prop-title').value = f.title
  492. document.getElementById('prop-field').value = selectedField
  493. const cb = document.getElementById('prop-in-flow')
  494. cb.checked = !!f.inFlow
  495. cb.disabled = f.type === 'attachment'
  496. const hint = document.getElementById('prop-hint')
  497. if (f.type === 'attachment') {
  498. hint.textContent = '附件不适合做条件判断,已禁用「参与流程条件分支」。'
  499. } else {
  500. hint.textContent = '勾选后出现在「3 流程设计」的字段下拉里,展示标题「' + f.title + '」,内部仍用 field=' + selectedField + '。'
  501. }
  502. }
  503. function toggleInFlow() {
  504. const f = fields[selectedField]
  505. if (f.type === 'attachment') {
  506. document.getElementById('prop-in-flow').checked = false
  507. alert('附件类型不能参与条件分支')
  508. return
  509. }
  510. f.inFlow = document.getElementById('prop-in-flow').checked
  511. const card = document.querySelector('.field-card[data-field="' + selectedField + '"]')
  512. card.classList.toggle('in-flow', f.inFlow)
  513. }
  514. document.getElementById('prop-title').addEventListener('input', (e) => {
  515. fields[selectedField].title = e.target.value
  516. const card = document.querySelector('.field-card[data-field="' + selectedField + '"]')
  517. card.querySelector('.label').textContent = e.target.value + (selectedField === 'days' || selectedField === 'reason' ? ' *' : '')
  518. renderRules()
  519. })
  520. function flowFields() {
  521. return Object.keys(fields)
  522. .filter((k) => fields[k].inFlow && fields[k].type !== 'attachment')
  523. .map((k) => ({ key: k, ...fields[k] }))
  524. }
  525. function selectBranch(idx) {
  526. branchIdx = idx
  527. document.getElementById('branch-pick').value = String(idx)
  528. document.getElementById('is-default').checked = !!branches[idx].isDefault
  529. document.querySelectorAll('.branch-tag').forEach((t, i) => {
  530. t.classList.toggle('active', i === idx)
  531. })
  532. renderRules()
  533. }
  534. function focusCondition() {
  535. selectBranch(branchIdx)
  536. }
  537. function toggleDefault() {
  538. const on = document.getElementById('is-default').checked
  539. branches.forEach((b, i) => { b.isDefault = on && i === branchIdx })
  540. if (on) branches[branchIdx].rules = []
  541. updateTags()
  542. renderRules()
  543. }
  544. function opLabel(op) {
  545. const map = { '>': '大于', '>=': '大于等于', '<': '小于', '<=': '小于等于', '==': '等于', '!=': '不等于', contains: '包含' }
  546. return map[op] || op
  547. }
  548. function renderRules() {
  549. const box = document.getElementById('rules')
  550. const tip = document.getElementById('no-fields-tip')
  551. const editor = document.getElementById('rule-editor')
  552. const b = branches[branchIdx]
  553. const available = flowFields()
  554. if (!available.length) {
  555. tip.style.display = 'block'
  556. } else {
  557. tip.style.display = 'none'
  558. }
  559. if (b.isDefault) {
  560. editor.style.opacity = '0.45'
  561. document.getElementById('btn-add-rule').disabled = true
  562. box.innerHTML = '<div style="font-size:12px;color:var(--muted)">默认分支无需配置条件</div>'
  563. document.getElementById('rule-preview').textContent = '预览:其他条件均不满足时走此分支'
  564. updateTags()
  565. return
  566. }
  567. editor.style.opacity = '1'
  568. document.getElementById('btn-add-rule').disabled = false
  569. if (!b.rules.length) {
  570. b.rules.push({ field: available[0] ? available[0].key : '', op: '>', value: '' })
  571. }
  572. box.innerHTML = ''
  573. b.rules.forEach((rule, i) => {
  574. const fMeta = fields[rule.field] || available[0]
  575. const type = fMeta ? fMeta.type : 'number'
  576. const ops = OPS[type] || OPS.number
  577. const row = document.createElement('div')
  578. row.className = 'rule-row'
  579. const selF = document.createElement('select')
  580. if (!available.length) {
  581. selF.innerHTML = '<option value="">无可用字段</option>'
  582. selF.disabled = true
  583. } else {
  584. available.forEach((f) => {
  585. const opt = document.createElement('option')
  586. opt.value = f.key
  587. opt.textContent = f.title
  588. if (f.key === rule.field) opt.selected = true
  589. selF.appendChild(opt)
  590. })
  591. }
  592. selF.onchange = () => {
  593. rule.field = selF.value
  594. const t = fields[rule.field].type
  595. rule.op = (OPS[t] || OPS.number)[0].v
  596. rule.value = ''
  597. renderRules()
  598. }
  599. const selOp = document.createElement('select')
  600. ops.forEach((o) => {
  601. const opt = document.createElement('option')
  602. opt.value = o.v
  603. opt.textContent = o.t
  604. if (o.v === rule.op) opt.selected = true
  605. selOp.appendChild(opt)
  606. })
  607. selOp.onchange = () => { rule.op = selOp.value; updateTags() }
  608. let valCtrl
  609. if (type === 'select' && fields[rule.field].options) {
  610. valCtrl = document.createElement('select')
  611. fields[rule.field].options.forEach((o) => {
  612. const opt = document.createElement('option')
  613. opt.value = o
  614. opt.textContent = o
  615. if (o === rule.value) opt.selected = true
  616. valCtrl.appendChild(opt)
  617. })
  618. if (!rule.value) rule.value = fields[rule.field].options[0]
  619. valCtrl.onchange = () => { rule.value = valCtrl.value; updateTags() }
  620. } else {
  621. valCtrl = document.createElement('input')
  622. valCtrl.placeholder = type === 'number' ? '数值' : '比较值'
  623. valCtrl.value = rule.value
  624. valCtrl.oninput = () => { rule.value = valCtrl.value; updateTags() }
  625. }
  626. row.appendChild(selF)
  627. row.appendChild(selOp)
  628. row.appendChild(valCtrl)
  629. box.appendChild(row)
  630. if (i < b.rules.length - 1) {
  631. const and = document.createElement('div')
  632. and.style.cssText = 'font-size:11px;color:var(--muted);margin:-2px 0 6px;text-align:center'
  633. and.textContent = '且(AND)'
  634. box.appendChild(and)
  635. }
  636. })
  637. updateTags()
  638. }
  639. function addRule() {
  640. const available = flowFields()
  641. if (!available.length || branches[branchIdx].isDefault) return
  642. branches[branchIdx].rules.push({
  643. field: available[0].key,
  644. op: (OPS[available[0].type] || OPS.number)[0].v,
  645. value: ''
  646. })
  647. renderRules()
  648. }
  649. function updateTags() {
  650. const b0 = branches[0]
  651. const parts = (b0.rules || []).map((r) => {
  652. const title = (fields[r.field] && fields[r.field].title) || r.field
  653. return title + ' ' + opLabel(r.op) + ' ' + (r.value === '' ? '?' : r.value)
  654. })
  655. const text0 = parts.length ? parts.join(' 且 ') : '未配置'
  656. document.getElementById('tag-0').textContent = text0
  657. document.getElementById('tag-1').textContent = '默认 · 其他情况'
  658. document.getElementById('cond-summary').textContent = '2 个分支 · ' + text0
  659. if (!branches[branchIdx].isDefault) {
  660. const cur = branches[branchIdx]
  661. const p = (cur.rules || []).map((r) => {
  662. const title = (fields[r.field] && fields[r.field].title) || r.field
  663. return title + ' ' + opLabel(r.op) + ' ' + (r.value === '' ? '?' : r.value)
  664. })
  665. document.getElementById('rule-preview').textContent =
  666. '预览:' + (p.join(' 且 ') || '未配置') +
  667. ' | 引擎侧仍存为 {field, op, value},等价公式 ' +
  668. (cur.rules[0] ? '{' + cur.rules[0].field + '} ' + cur.rules[0].op + ' ' + cur.rules[0].value : '')
  669. }
  670. }
  671. // init
  672. renderRules()
  673. </script>
  674. </body>
  675. </html>