Spec and mockup for marking form fields for conditions and configuring branches with field/op/value UI instead of raw formulas. Co-authored-by: Cursor <cursoragent@cursor.com>
717 lines
27 KiB
HTML
717 lines
27 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>OA 条件分支 · 表单字段驱动 Demo</title>
|
||
<style>
|
||
:root {
|
||
--primary: #10b981;
|
||
--primary-dark: #059669;
|
||
--primary-bg: #ecfdf5;
|
||
--text: #0f172a;
|
||
--text-2: #334155;
|
||
--muted: #64748b;
|
||
--border: #e2e8f0;
|
||
--border-strong: #cbd5e1;
|
||
--bg: #f1f5f9;
|
||
--warn: #d97706;
|
||
--radius: 8px;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
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: 10px 16px; align-items: flex-start;
|
||
justify-content: space-between;
|
||
padding: 12px 16px; margin-bottom: 14px;
|
||
background: #fff; border: 1px solid var(--border); border-radius: 10px;
|
||
}
|
||
.banner h1 { margin: 0 0 4px; font-size: 16px; }
|
||
.banner p { margin: 0; color: var(--muted); font-size: 13px; max-width: 720px; }
|
||
.tag {
|
||
display: inline-block; padding: 2px 8px; border-radius: 999px;
|
||
background: var(--primary-bg); color: var(--primary-dark);
|
||
font-size: 12px; font-weight: 600;
|
||
}
|
||
.panel {
|
||
background: #fff; border: 1px solid var(--border); border-radius: 10px;
|
||
overflow: hidden;
|
||
}
|
||
.toolbar {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 10px 14px; border-bottom: 1px solid var(--border);
|
||
}
|
||
.btn {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
height: 34px; padding: 0 12px;
|
||
border: 1px solid var(--border); border-radius: 8px;
|
||
background: #fff; color: var(--text-2); cursor: pointer; font-size: 13px;
|
||
}
|
||
.btn:hover { border-color: var(--primary); color: var(--primary-dark); }
|
||
.btn.primary {
|
||
background: var(--primary); border-color: var(--primary); color: #fff;
|
||
}
|
||
.btn.primary:hover { background: var(--primary-dark); }
|
||
.btn.sm { height: 28px; padding: 0 8px; font-size: 12px; }
|
||
.tabs {
|
||
display: flex; gap: 0; border-bottom: 1px solid var(--border);
|
||
padding: 0 12px; background: #fafafa;
|
||
}
|
||
.tab {
|
||
border: 0; background: transparent; padding: 12px 14px;
|
||
color: var(--muted); cursor: pointer; font-size: 13px;
|
||
border-bottom: 2px solid transparent; margin-bottom: -1px;
|
||
}
|
||
.tab.active {
|
||
color: var(--primary-dark); font-weight: 600;
|
||
border-bottom-color: var(--primary);
|
||
}
|
||
.step-body { display: none; min-height: 520px; }
|
||
.step-body.show { display: block; }
|
||
|
||
/* ===== 表单设计 ===== */
|
||
.form-layout {
|
||
display: grid;
|
||
grid-template-columns: 200px 1fr 280px;
|
||
min-height: 520px;
|
||
}
|
||
.palette, .props {
|
||
background: #fafafa; padding: 12px;
|
||
border-right: 1px solid var(--border);
|
||
}
|
||
.props { border-right: 0; border-left: 1px solid var(--border); }
|
||
.palette h3, .props h3 {
|
||
margin: 0 0 10px; font-size: 12px; color: var(--muted);
|
||
font-weight: 600; letter-spacing: .02em;
|
||
}
|
||
.palette-item {
|
||
padding: 8px 10px; margin-bottom: 6px;
|
||
background: #fff; border: 1px solid var(--border); border-radius: 6px;
|
||
font-size: 13px; color: var(--text-2);
|
||
}
|
||
.canvas { padding: 20px 24px; }
|
||
.canvas-title { font-size: 15px; font-weight: 600; margin-bottom: 16px; }
|
||
.field-card {
|
||
position: relative;
|
||
padding: 12px 14px; margin-bottom: 10px;
|
||
border: 1px solid var(--border); border-radius: 8px;
|
||
cursor: pointer; background: #fff;
|
||
}
|
||
.field-card:hover { border-color: #a7f3d0; }
|
||
.field-card.selected {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 2px rgba(16, 185, 129, .15);
|
||
}
|
||
.field-card .label { font-size: 12px; color: var(--muted); margin-bottom: 6px; }
|
||
.field-card .fake-input {
|
||
height: 36px; border: 1px solid var(--border); border-radius: 8px;
|
||
background: #f8fafc; display: flex; align-items: center; padding: 0 10px;
|
||
color: var(--muted); font-size: 13px;
|
||
}
|
||
.field-card .badge {
|
||
position: absolute; top: 8px; right: 8px;
|
||
font-size: 11px; padding: 1px 6px; border-radius: 4px;
|
||
background: var(--primary-bg); color: var(--primary-dark);
|
||
display: none;
|
||
}
|
||
.field-card.in-flow .badge { display: inline-block; }
|
||
.prop-row { margin-bottom: 14px; }
|
||
.prop-row label {
|
||
display: block; font-size: 12px; color: var(--muted); margin-bottom: 6px;
|
||
}
|
||
.prop-row input, .prop-row select {
|
||
width: 100%; height: 34px; padding: 0 10px;
|
||
border: 1px solid var(--border); border-radius: 8px; font-size: 13px;
|
||
}
|
||
.switch-row {
|
||
display: flex; align-items: flex-start; gap: 10px;
|
||
padding: 12px; border: 1px dashed #a7f3d0; border-radius: 8px;
|
||
background: var(--primary-bg);
|
||
}
|
||
.switch-row input { margin-top: 3px; width: 16px; height: 16px; accent-color: var(--primary); }
|
||
.switch-row .t { font-size: 13px; font-weight: 600; color: var(--primary-dark); }
|
||
.switch-row .d { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
||
.hint {
|
||
margin-top: 12px; padding: 10px 12px; font-size: 12px; color: var(--muted);
|
||
background: #fff; border: 1px solid var(--border); border-radius: 8px;
|
||
}
|
||
|
||
/* ===== 流程设计 ===== */
|
||
.flow-layout {
|
||
display: grid; grid-template-columns: 1fr 340px; min-height: 520px;
|
||
}
|
||
.flow-tree {
|
||
padding: 28px 24px; display: flex; flex-direction: column; align-items: center;
|
||
background: linear-gradient(180deg, #f8fafc, #fff);
|
||
}
|
||
.node {
|
||
width: 220px; border-radius: 10px; background: #fff;
|
||
border: 1px solid var(--border); overflow: hidden; cursor: pointer;
|
||
box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
|
||
}
|
||
.node:hover { border-color: #a7f3d0; }
|
||
.node.active { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(16,185,129,.2); }
|
||
.node .hd {
|
||
padding: 6px 10px; font-size: 12px; font-weight: 600; color: #fff;
|
||
}
|
||
.node.start .hd { background: #64748b; }
|
||
.node.approval .hd { background: var(--primary); }
|
||
.node.condition .hd { background: #f59e0b; }
|
||
.node.end .hd { background: #94a3b8; }
|
||
.node .bd { padding: 10px 12px; font-size: 12px; color: var(--muted); }
|
||
.vline {
|
||
width: 2px; height: 18px; background: var(--border-strong);
|
||
}
|
||
.plus {
|
||
width: 24px; height: 24px; border-radius: 50%;
|
||
border: 1px solid var(--border); background: #fff;
|
||
display: flex; align-items: center; justify-content: center;
|
||
color: var(--primary-dark); font-weight: 700; font-size: 14px;
|
||
}
|
||
.branches {
|
||
display: flex; gap: 24px; align-items: flex-start; margin-top: 8px;
|
||
position: relative;
|
||
}
|
||
.branches::before {
|
||
content: ""; position: absolute; top: 0; left: 25%; right: 25%;
|
||
height: 2px; background: var(--border-strong);
|
||
}
|
||
.branch {
|
||
display: flex; flex-direction: column; align-items: center; width: 200px;
|
||
}
|
||
.branch-tag {
|
||
margin: 8px 0; padding: 4px 10px; border-radius: 999px;
|
||
background: #fffbeb; border: 1px solid #fde68a;
|
||
color: var(--warn); font-size: 12px; font-weight: 600;
|
||
max-width: 190px; text-align: center; cursor: pointer;
|
||
}
|
||
.branch-tag.default { background: #f1f5f9; border-color: var(--border); color: var(--muted); }
|
||
.branch-tag.active {
|
||
outline: 2px solid var(--primary); outline-offset: 2px;
|
||
}
|
||
|
||
.drawer {
|
||
border-left: 1px solid var(--border); background: #fff; padding: 14px 16px;
|
||
display: flex; flex-direction: column; gap: 12px;
|
||
}
|
||
.drawer h3 { margin: 0; font-size: 15px; }
|
||
.drawer .sub { margin: 0; font-size: 12px; color: var(--muted); }
|
||
.legacy {
|
||
padding: 10px; border-radius: 8px; background: #fff7ed; border: 1px solid #fed7aa;
|
||
font-size: 12px; color: #9a3412;
|
||
}
|
||
.legacy code {
|
||
display: block; margin-top: 6px; padding: 6px 8px;
|
||
background: #fff; border-radius: 6px; font-family: ui-monospace, monospace;
|
||
color: var(--text);
|
||
}
|
||
.cond-box {
|
||
border: 1px solid var(--border); border-radius: 8px; padding: 12px;
|
||
background: #fafafa;
|
||
}
|
||
.cond-box .title {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
margin-bottom: 10px; font-weight: 600; font-size: 13px;
|
||
}
|
||
.rule-row {
|
||
display: grid; grid-template-columns: 1fr auto 1fr;
|
||
gap: 6px; margin-bottom: 8px; align-items: center;
|
||
}
|
||
.rule-row select, .rule-row input {
|
||
height: 32px; border: 1px solid var(--border); border-radius: 6px;
|
||
padding: 0 8px; font-size: 12px; background: #fff; width: 100%;
|
||
}
|
||
.rule-row select:disabled, .rule-row input:disabled {
|
||
background: #f1f5f9; color: var(--muted);
|
||
}
|
||
.preview {
|
||
margin-top: 8px; padding: 8px 10px; border-radius: 6px;
|
||
background: var(--primary-bg); color: var(--primary-dark);
|
||
font-size: 12px;
|
||
}
|
||
.empty-tip {
|
||
padding: 10px; font-size: 12px; color: var(--warn);
|
||
background: #fffbeb; border-radius: 6px; border: 1px dashed #fde68a;
|
||
}
|
||
.footer-note {
|
||
margin-top: 14px; font-size: 12px; color: var(--muted); text-align: center;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<div class="banner">
|
||
<div>
|
||
<h1>条件分支:从表单字段点选配置(交互 Demo)</h1>
|
||
<p>
|
||
对照现状「手写 <code>{days} > 5</code>」。本 Demo 演示:在<strong>表单设计</strong>勾选参与分支的字段 →
|
||
在<strong>流程设计</strong>用「字段 + 运算符 + 值」配置条件,无需记变量名。
|
||
</p>
|
||
</div>
|
||
<span class="tag">Fresh Mint · 可点击</span>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="toolbar">
|
||
<button class="btn" type="button" disabled>← 返回</button>
|
||
<div style="display:flex;gap:8px;">
|
||
<button class="btn" type="button">保存草稿</button>
|
||
<button class="btn primary" type="button">发布</button>
|
||
</div>
|
||
</div>
|
||
<div class="tabs">
|
||
<button class="tab" type="button" data-step="basic">1 基础设置</button>
|
||
<button class="tab active" type="button" data-step="form">2 表单设计</button>
|
||
<button class="tab" type="button" data-step="flow">3 流程设计</button>
|
||
<button class="tab" type="button" data-step="advanced">4 高级设置</button>
|
||
</div>
|
||
|
||
<!-- 表单设计 -->
|
||
<div id="step-form" class="step-body show">
|
||
<div class="form-layout">
|
||
<aside class="palette">
|
||
<h3>控件库</h3>
|
||
<div class="palette-item">单行输入</div>
|
||
<div class="palette-item">数字</div>
|
||
<div class="palette-item">金额</div>
|
||
<div class="palette-item">单选</div>
|
||
<div class="palette-item">日期</div>
|
||
<div class="palette-item">附件</div>
|
||
</aside>
|
||
<main class="canvas">
|
||
<div class="canvas-title">请假申请 · 表单画布</div>
|
||
<div
|
||
class="field-card selected in-flow"
|
||
data-field="days"
|
||
data-title="请假天数"
|
||
data-type="number"
|
||
onclick="selectField(this)"
|
||
>
|
||
<span class="badge">用于分支</span>
|
||
<div class="label">请假天数 *</div>
|
||
<div class="fake-input">请输入数字</div>
|
||
</div>
|
||
<div
|
||
class="field-card"
|
||
data-field="reason"
|
||
data-title="请假事由"
|
||
data-type="text"
|
||
onclick="selectField(this)"
|
||
>
|
||
<span class="badge">用于分支</span>
|
||
<div class="label">请假事由 *</div>
|
||
<div class="fake-input">请输入</div>
|
||
</div>
|
||
<div
|
||
class="field-card in-flow"
|
||
data-field="leaveType"
|
||
data-title="请假类型"
|
||
data-type="select"
|
||
onclick="selectField(this)"
|
||
>
|
||
<span class="badge">用于分支</span>
|
||
<div class="label">请假类型</div>
|
||
<div class="fake-input">事假 / 病假 / 年假</div>
|
||
</div>
|
||
<div
|
||
class="field-card"
|
||
data-field="attach"
|
||
data-title="附件"
|
||
data-type="attachment"
|
||
onclick="selectField(this)"
|
||
>
|
||
<span class="badge">用于分支</span>
|
||
<div class="label">附件</div>
|
||
<div class="fake-input">上传附件</div>
|
||
</div>
|
||
</main>
|
||
<aside class="props">
|
||
<h3>字段属性</h3>
|
||
<div class="prop-row">
|
||
<label>字段标题</label>
|
||
<input id="prop-title" value="请假天数" />
|
||
</div>
|
||
<div class="prop-row">
|
||
<label>字段标识(系统用,用户无需关心)</label>
|
||
<input id="prop-field" value="days" readonly style="color:var(--muted)" />
|
||
</div>
|
||
<div class="switch-row">
|
||
<input id="prop-in-flow" type="checkbox" checked onchange="toggleInFlow()" />
|
||
<div>
|
||
<div class="t">参与流程条件分支</div>
|
||
<div class="d">勾选后,可在「3 流程设计」的分支条件里选到此字段,无需手写变量名。</div>
|
||
</div>
|
||
</div>
|
||
<div class="hint" id="prop-hint">
|
||
数字 / 金额 / 单选 / 日期等适合做条件;附件类控件一般不可勾选(本 Demo 里附件勾选会提示)。
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 流程设计 -->
|
||
<div id="step-flow" class="step-body">
|
||
<div class="flow-layout">
|
||
<div class="flow-tree">
|
||
<div class="node start">
|
||
<div class="hd">发起人</div>
|
||
<div class="bd">所有人可发起</div>
|
||
</div>
|
||
<div class="vline"></div>
|
||
<div class="plus">+</div>
|
||
<div class="vline"></div>
|
||
<div class="node condition active" onclick="focusCondition()">
|
||
<div class="hd">条件分支</div>
|
||
<div class="bd" id="cond-summary">2 个分支 · 点击右侧配置</div>
|
||
</div>
|
||
<div class="branches">
|
||
<div class="branch">
|
||
<span class="branch-tag active" id="tag-0" onclick="selectBranch(0)">请假天数 > 3</span>
|
||
<div class="vline"></div>
|
||
<div class="node approval">
|
||
<div class="hd">审批人</div>
|
||
<div class="bd">总经理</div>
|
||
</div>
|
||
<div class="vline"></div>
|
||
<div class="node end"><div class="hd">结束</div><div class="bd">流程结束</div></div>
|
||
</div>
|
||
<div class="branch">
|
||
<span class="branch-tag default" id="tag-1" onclick="selectBranch(1)">默认 · 其他情况</span>
|
||
<div class="vline"></div>
|
||
<div class="node approval">
|
||
<div class="hd">审批人</div>
|
||
<div class="bd">部门负责人</div>
|
||
</div>
|
||
<div class="vline"></div>
|
||
<div class="node end"><div class="hd">结束</div><div class="bd">流程结束</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<aside class="drawer">
|
||
<h3>配置条件分支</h3>
|
||
<p class="sub">用表单里勾选了「参与流程条件分支」的字段来搭建规则</p>
|
||
|
||
<div class="legacy">
|
||
<strong>现状(不好用)</strong>
|
||
<code>{days} > 3</code>
|
||
普通用户不知道 days 从哪来、运算符怎么写。
|
||
</div>
|
||
|
||
<div class="prop-row">
|
||
<label>当前编辑的分支</label>
|
||
<select id="branch-pick" onchange="selectBranch(+this.value)">
|
||
<option value="0">分支 1 · 大于 3 天</option>
|
||
<option value="1">分支 2 · 默认</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="cond-box" id="rule-editor">
|
||
<div class="title">
|
||
<span>条件规则</span>
|
||
<button class="btn sm" type="button" onclick="addRule()" id="btn-add-rule">+ 且条件</button>
|
||
</div>
|
||
<div id="rules"></div>
|
||
<div class="preview" id="rule-preview">预览:请假天数 > 3</div>
|
||
</div>
|
||
|
||
<div class="prop-row">
|
||
<label><input type="checkbox" id="is-default" onchange="toggleDefault()" /> 设为默认分支(其他条件都不满足时走这里)</label>
|
||
</div>
|
||
|
||
<div class="empty-tip" id="no-fields-tip" style="display:none">
|
||
暂无可用字段。请先到「2 表单设计」勾选至少一个「参与流程条件分支」的字段。
|
||
</div>
|
||
|
||
<button class="btn primary" type="button" style="align-self:flex-start">保存节点配置</button>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="step-basic" class="step-body" style="padding:48px;text-align:center;color:var(--muted)">
|
||
(本 Demo 不展开)基础设置:图标、名称、说明…
|
||
</div>
|
||
<div id="step-advanced" class="step-body" style="padding:48px;text-align:center;color:var(--muted)">
|
||
(本 Demo 不展开)高级设置:…</div>
|
||
</div>
|
||
|
||
<p class="footer-note">
|
||
文件位置:<code>docs/mockups/oa-approval-condition-from-form-demo.html</code>
|
||
· 试用:切到「2 表单设计」勾选字段 → 再切「3 流程设计」拉下拉里应能看到标题而非 variable 名
|
||
</p>
|
||
</div>
|
||
|
||
<script>
|
||
/** 画布字段状态 */
|
||
const fields = {
|
||
days: { title: '请假天数', type: 'number', inFlow: true },
|
||
reason: { title: '请假事由', type: 'text', inFlow: false },
|
||
leaveType: { title: '请假类型', type: 'select', inFlow: true, options: ['事假', '病假', '年假'] },
|
||
attach: { title: '附件', type: 'attachment', inFlow: false }
|
||
}
|
||
let selectedField = 'days'
|
||
let branchIdx = 0
|
||
const branches = [
|
||
{
|
||
name: '大于 3 天',
|
||
isDefault: false,
|
||
rules: [{ field: 'days', op: '>', value: '3' }]
|
||
},
|
||
{
|
||
name: '默认',
|
||
isDefault: true,
|
||
rules: []
|
||
}
|
||
]
|
||
|
||
const OPS = {
|
||
number: [
|
||
{ v: '>', t: '大于' },
|
||
{ v: '>=', t: '大于等于' },
|
||
{ v: '<', t: '小于' },
|
||
{ v: '<=', t: '小于等于' },
|
||
{ v: '==', t: '等于' },
|
||
{ v: '!=', t: '不等于' }
|
||
],
|
||
text: [
|
||
{ v: '==', t: '等于' },
|
||
{ v: '!=', t: '不等于' },
|
||
{ v: 'contains', t: '包含' }
|
||
],
|
||
select: [
|
||
{ v: '==', t: '等于' },
|
||
{ v: '!=', t: '不等于' }
|
||
]
|
||
}
|
||
|
||
document.querySelectorAll('.tab').forEach((tab) => {
|
||
tab.addEventListener('click', () => {
|
||
document.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'))
|
||
tab.classList.add('active')
|
||
const step = tab.dataset.step
|
||
document.querySelectorAll('.step-body').forEach((el) => el.classList.remove('show'))
|
||
const body = document.getElementById('step-' + step)
|
||
if (body) body.classList.add('show')
|
||
if (step === 'flow') renderRules()
|
||
})
|
||
})
|
||
|
||
function selectField(el) {
|
||
document.querySelectorAll('.field-card').forEach((c) => c.classList.remove('selected'))
|
||
el.classList.add('selected')
|
||
selectedField = el.dataset.field
|
||
const f = fields[selectedField]
|
||
document.getElementById('prop-title').value = f.title
|
||
document.getElementById('prop-field').value = selectedField
|
||
const cb = document.getElementById('prop-in-flow')
|
||
cb.checked = !!f.inFlow
|
||
cb.disabled = f.type === 'attachment'
|
||
const hint = document.getElementById('prop-hint')
|
||
if (f.type === 'attachment') {
|
||
hint.textContent = '附件不适合做条件判断,已禁用「参与流程条件分支」。'
|
||
} else {
|
||
hint.textContent = '勾选后出现在「3 流程设计」的字段下拉里,展示标题「' + f.title + '」,内部仍用 field=' + selectedField + '。'
|
||
}
|
||
}
|
||
|
||
function toggleInFlow() {
|
||
const f = fields[selectedField]
|
||
if (f.type === 'attachment') {
|
||
document.getElementById('prop-in-flow').checked = false
|
||
alert('附件类型不能参与条件分支')
|
||
return
|
||
}
|
||
f.inFlow = document.getElementById('prop-in-flow').checked
|
||
const card = document.querySelector('.field-card[data-field="' + selectedField + '"]')
|
||
card.classList.toggle('in-flow', f.inFlow)
|
||
}
|
||
|
||
document.getElementById('prop-title').addEventListener('input', (e) => {
|
||
fields[selectedField].title = e.target.value
|
||
const card = document.querySelector('.field-card[data-field="' + selectedField + '"]')
|
||
card.querySelector('.label').textContent = e.target.value + (selectedField === 'days' || selectedField === 'reason' ? ' *' : '')
|
||
renderRules()
|
||
})
|
||
|
||
function flowFields() {
|
||
return Object.keys(fields)
|
||
.filter((k) => fields[k].inFlow && fields[k].type !== 'attachment')
|
||
.map((k) => ({ key: k, ...fields[k] }))
|
||
}
|
||
|
||
function selectBranch(idx) {
|
||
branchIdx = idx
|
||
document.getElementById('branch-pick').value = String(idx)
|
||
document.getElementById('is-default').checked = !!branches[idx].isDefault
|
||
document.querySelectorAll('.branch-tag').forEach((t, i) => {
|
||
t.classList.toggle('active', i === idx)
|
||
})
|
||
renderRules()
|
||
}
|
||
|
||
function focusCondition() {
|
||
selectBranch(branchIdx)
|
||
}
|
||
|
||
function toggleDefault() {
|
||
const on = document.getElementById('is-default').checked
|
||
branches.forEach((b, i) => { b.isDefault = on && i === branchIdx })
|
||
if (on) branches[branchIdx].rules = []
|
||
updateTags()
|
||
renderRules()
|
||
}
|
||
|
||
function opLabel(op) {
|
||
const map = { '>': '大于', '>=': '大于等于', '<': '小于', '<=': '小于等于', '==': '等于', '!=': '不等于', contains: '包含' }
|
||
return map[op] || op
|
||
}
|
||
|
||
function renderRules() {
|
||
const box = document.getElementById('rules')
|
||
const tip = document.getElementById('no-fields-tip')
|
||
const editor = document.getElementById('rule-editor')
|
||
const b = branches[branchIdx]
|
||
const available = flowFields()
|
||
|
||
if (!available.length) {
|
||
tip.style.display = 'block'
|
||
} else {
|
||
tip.style.display = 'none'
|
||
}
|
||
|
||
if (b.isDefault) {
|
||
editor.style.opacity = '0.45'
|
||
document.getElementById('btn-add-rule').disabled = true
|
||
box.innerHTML = '<div style="font-size:12px;color:var(--muted)">默认分支无需配置条件</div>'
|
||
document.getElementById('rule-preview').textContent = '预览:其他条件均不满足时走此分支'
|
||
updateTags()
|
||
return
|
||
}
|
||
editor.style.opacity = '1'
|
||
document.getElementById('btn-add-rule').disabled = false
|
||
|
||
if (!b.rules.length) {
|
||
b.rules.push({ field: available[0] ? available[0].key : '', op: '>', value: '' })
|
||
}
|
||
|
||
box.innerHTML = ''
|
||
b.rules.forEach((rule, i) => {
|
||
const fMeta = fields[rule.field] || available[0]
|
||
const type = fMeta ? fMeta.type : 'number'
|
||
const ops = OPS[type] || OPS.number
|
||
const row = document.createElement('div')
|
||
row.className = 'rule-row'
|
||
|
||
const selF = document.createElement('select')
|
||
if (!available.length) {
|
||
selF.innerHTML = '<option value="">无可用字段</option>'
|
||
selF.disabled = true
|
||
} else {
|
||
available.forEach((f) => {
|
||
const opt = document.createElement('option')
|
||
opt.value = f.key
|
||
opt.textContent = f.title
|
||
if (f.key === rule.field) opt.selected = true
|
||
selF.appendChild(opt)
|
||
})
|
||
}
|
||
selF.onchange = () => {
|
||
rule.field = selF.value
|
||
const t = fields[rule.field].type
|
||
rule.op = (OPS[t] || OPS.number)[0].v
|
||
rule.value = ''
|
||
renderRules()
|
||
}
|
||
|
||
const selOp = document.createElement('select')
|
||
ops.forEach((o) => {
|
||
const opt = document.createElement('option')
|
||
opt.value = o.v
|
||
opt.textContent = o.t
|
||
if (o.v === rule.op) opt.selected = true
|
||
selOp.appendChild(opt)
|
||
})
|
||
selOp.onchange = () => { rule.op = selOp.value; updateTags() }
|
||
|
||
let valCtrl
|
||
if (type === 'select' && fields[rule.field].options) {
|
||
valCtrl = document.createElement('select')
|
||
fields[rule.field].options.forEach((o) => {
|
||
const opt = document.createElement('option')
|
||
opt.value = o
|
||
opt.textContent = o
|
||
if (o === rule.value) opt.selected = true
|
||
valCtrl.appendChild(opt)
|
||
})
|
||
if (!rule.value) rule.value = fields[rule.field].options[0]
|
||
valCtrl.onchange = () => { rule.value = valCtrl.value; updateTags() }
|
||
} else {
|
||
valCtrl = document.createElement('input')
|
||
valCtrl.placeholder = type === 'number' ? '数值' : '比较值'
|
||
valCtrl.value = rule.value
|
||
valCtrl.oninput = () => { rule.value = valCtrl.value; updateTags() }
|
||
}
|
||
|
||
row.appendChild(selF)
|
||
row.appendChild(selOp)
|
||
row.appendChild(valCtrl)
|
||
box.appendChild(row)
|
||
|
||
if (i < b.rules.length - 1) {
|
||
const and = document.createElement('div')
|
||
and.style.cssText = 'font-size:11px;color:var(--muted);margin:-2px 0 6px;text-align:center'
|
||
and.textContent = '且(AND)'
|
||
box.appendChild(and)
|
||
}
|
||
})
|
||
|
||
updateTags()
|
||
}
|
||
|
||
function addRule() {
|
||
const available = flowFields()
|
||
if (!available.length || branches[branchIdx].isDefault) return
|
||
branches[branchIdx].rules.push({
|
||
field: available[0].key,
|
||
op: (OPS[available[0].type] || OPS.number)[0].v,
|
||
value: ''
|
||
})
|
||
renderRules()
|
||
}
|
||
|
||
function updateTags() {
|
||
const b0 = branches[0]
|
||
const parts = (b0.rules || []).map((r) => {
|
||
const title = (fields[r.field] && fields[r.field].title) || r.field
|
||
return title + ' ' + opLabel(r.op) + ' ' + (r.value === '' ? '?' : r.value)
|
||
})
|
||
const text0 = parts.length ? parts.join(' 且 ') : '未配置'
|
||
document.getElementById('tag-0').textContent = text0
|
||
document.getElementById('tag-1').textContent = '默认 · 其他情况'
|
||
document.getElementById('cond-summary').textContent = '2 个分支 · ' + text0
|
||
|
||
if (!branches[branchIdx].isDefault) {
|
||
const cur = branches[branchIdx]
|
||
const p = (cur.rules || []).map((r) => {
|
||
const title = (fields[r.field] && fields[r.field].title) || r.field
|
||
return title + ' ' + opLabel(r.op) + ' ' + (r.value === '' ? '?' : r.value)
|
||
})
|
||
document.getElementById('rule-preview').textContent =
|
||
'预览:' + (p.join(' 且 ') || '未配置') +
|
||
' | 引擎侧仍存为 {field, op, value},等价公式 ' +
|
||
(cur.rules[0] ? '{' + cur.rules[0].field + '} ' + cur.rules[0].op + ' ' + cur.rules[0].value : '')
|
||
}
|
||
}
|
||
|
||
// init
|
||
renderRules()
|
||
</script>
|
||
</body>
|
||
</html>
|