支持审批流程中的分支网关和节点触发条件
This commit is contained in:
@@ -54,5 +54,21 @@ namespace YLErp.DBModels
|
||||
/// </summary>
|
||||
[DisplayName("审批组条件")]
|
||||
public int? approvalCondition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分支网关条件(JSON)。
|
||||
/// <para>用于多分支流程的进入条件判断,结构见 ConditionExpressionConfig。</para>
|
||||
/// <para>为空时回退到旧的 approvalGroupId/approvalCondition 二元判断(双写兼容)。</para>
|
||||
/// </summary>
|
||||
[DisplayName("分支网关条件")]
|
||||
public string conditionConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 节点触发条件(JSON)。
|
||||
/// <para>仅挂在审批节点(node=0)上:推进到该节点时求值,满足才进入该节点审批;不满足则跳过该节点。</para>
|
||||
/// <para>为空视为无条件(默认进入审批)。例:名义本金 A默认审核、B>100W再审核、C>=500W再审核。</para>
|
||||
/// </summary>
|
||||
[DisplayName("节点触发条件")]
|
||||
public string triggerCondition { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Newtonsoft.Json;
|
||||
using YLErp.Helpers;
|
||||
|
||||
namespace YLErp.Helpers.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public class ConditionEvaluatorTests
|
||||
{
|
||||
private static ConditionContext CreateContext(double notional = 0, string tradeType = "", int? userId = null, int? initGroupId = null, string processCategory = "")
|
||||
{
|
||||
return new ConditionContext
|
||||
{
|
||||
Trade = new DBModels.trade
|
||||
{
|
||||
StockEqvNotional = notional,
|
||||
TradeType = tradeType
|
||||
},
|
||||
UserId = userId,
|
||||
InitGroupId = initGroupId,
|
||||
ProcessCategory = processCategory
|
||||
};
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_EmptyOrNullCondition_ReturnsFalse()
|
||||
{
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(null, CreateContext()));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate("", CreateContext()));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(" ", CreateContext()));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_InvalidJson_ReturnsFalse()
|
||||
{
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate("not a json", CreateContext()));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate("{\"tokens\": [", CreateContext()));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_TokenSingleCondition_Works()
|
||||
{
|
||||
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
|
||||
{
|
||||
tokens = new System.Collections.Generic.List<ConditionToken>
|
||||
{
|
||||
new ConditionToken
|
||||
{
|
||||
type = "condition",
|
||||
condition = new ConditionItem { field = "notional", op = ">", value = 100 }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200)));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 100)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_TokenAnd_Works()
|
||||
{
|
||||
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
|
||||
{
|
||||
tokens = new System.Collections.Generic.List<ConditionToken>
|
||||
{
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = ">", value = 100 } },
|
||||
new ConditionToken { type = "operator", connector = "and" },
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = "<=", value = 500 } }
|
||||
}
|
||||
});
|
||||
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200)));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 50)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_TokenOr_Works()
|
||||
{
|
||||
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
|
||||
{
|
||||
tokens = new System.Collections.Generic.List<ConditionToken>
|
||||
{
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = "<", value = 100 } },
|
||||
new ConditionToken { type = "operator", connector = "or" },
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = ">", value = 500 } }
|
||||
}
|
||||
});
|
||||
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 50)));
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 600)));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 300)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_TokenWithParentheses_Works()
|
||||
{
|
||||
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
|
||||
{
|
||||
tokens = new System.Collections.Generic.List<ConditionToken>
|
||||
{
|
||||
new ConditionToken { type = "lparen" },
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = ">", value = 100 } },
|
||||
new ConditionToken { type = "operator", connector = "or" },
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = "<", value = 50 } },
|
||||
new ConditionToken { type = "rparen" },
|
||||
new ConditionToken { type = "operator", connector = "and" },
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "tradeType", op = "==", value = "香草" } }
|
||||
}
|
||||
});
|
||||
|
||||
// (200>100 or 200<50) and tradeType=="香草" => true
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200, tradeType: "香草")));
|
||||
// (30>100 or 30<50) and tradeType=="雪球" => false
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 30, tradeType: "雪球")));
|
||||
// (80>100 or 80<50) and tradeType=="香草" => false
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 80, tradeType: "香草")));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_MixedAndOr_PriorityAndOverOr()
|
||||
{
|
||||
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
|
||||
{
|
||||
tokens = new System.Collections.Generic.List<ConditionToken>
|
||||
{
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = ">", value = 100 } },
|
||||
new ConditionToken { type = "operator", connector = "or" },
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = "<", value = 50 } },
|
||||
new ConditionToken { type = "operator", connector = "and" },
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "tradeType", op = "==", value = "香草" } }
|
||||
}
|
||||
});
|
||||
|
||||
// A or (B and C) — and 优先级高于 or
|
||||
// 200>100 -> true,无需计算右侧
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200, tradeType: "")));
|
||||
// 30>100=false, 30<50=true, 香草==香草=true -> false or (true and true) = true
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 30, tradeType: "香草")));
|
||||
// 80>100=false, 80<50=false, 雪球==香草=false -> false or (false and false) = false
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 80, tradeType: "雪球")));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_InitGroup_Works()
|
||||
{
|
||||
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
|
||||
{
|
||||
tokens = new System.Collections.Generic.List<ConditionToken>
|
||||
{
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "initGroup", op = "==", value = 5 } }
|
||||
}
|
||||
});
|
||||
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, new ConditionContext { InitGroupId = 5 }));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, new ConditionContext { InitGroupId = 3 }));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_ProcessCategory_Works()
|
||||
{
|
||||
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
|
||||
{
|
||||
tokens = new System.Collections.Generic.List<ConditionToken>
|
||||
{
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "processCategory", op = "==", value = "CloseProcess" } }
|
||||
}
|
||||
});
|
||||
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(processCategory: "CloseProcess")));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(processCategory: "TradeProcess")));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_TradeTypeStringComparison_Works()
|
||||
{
|
||||
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
|
||||
{
|
||||
tokens = new System.Collections.Generic.List<ConditionToken>
|
||||
{
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "tradeType", op = "!=", value = "雪球" } }
|
||||
}
|
||||
});
|
||||
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(tradeType: "香草")));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(tradeType: "雪球")));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_IncompleteParentheses_DoesNotThrow()
|
||||
{
|
||||
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
|
||||
{
|
||||
tokens = new System.Collections.Generic.List<ConditionToken>
|
||||
{
|
||||
new ConditionToken { type = "lparen" },
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = ">", value = 100 } }
|
||||
}
|
||||
});
|
||||
|
||||
// Parser tolerates missing closing parenthesis and returns the value inside
|
||||
var result = ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200));
|
||||
Assert.IsTrue(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_NotEquals_Works()
|
||||
{
|
||||
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
|
||||
{
|
||||
tokens = new System.Collections.Generic.List<ConditionToken>
|
||||
{
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = "!=", value = 100 } }
|
||||
}
|
||||
});
|
||||
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200)));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 100)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_GreaterOrEqual_Works()
|
||||
{
|
||||
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
|
||||
{
|
||||
tokens = new System.Collections.Generic.List<ConditionToken>
|
||||
{
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = ">=", value = 100 } }
|
||||
}
|
||||
});
|
||||
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 100)));
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200)));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 99)));
|
||||
}
|
||||
|
||||
// ===== 字母 op 标识符(前端改用 gt/lt/gte/lte/eq/neq 规避 > < 编码问题)=====
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_AlphaOp_GreaterThan_Works()
|
||||
{
|
||||
var condition = BuildTokens(new ConditionItem { field = "notional", op = "gt", value = 100 });
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200)));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 100)));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 50)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_AlphaOp_LessThanOrEqual_Works()
|
||||
{
|
||||
var condition = BuildTokens(new ConditionItem { field = "notional", op = "lte", value = 100 });
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 100)));
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 50)));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 101)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_AlphaOp_EqualAndNotEqual_Works()
|
||||
{
|
||||
var eqCond = BuildTokens(new ConditionItem { field = "tradeType", op = "eq", value = "香草" });
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(eqCond, CreateContext(tradeType: "香草")));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(eqCond, CreateContext(tradeType: "雪球")));
|
||||
|
||||
var neqCond = BuildTokens(new ConditionItem { field = "tradeType", op = "neq", value = "雪球" });
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(neqCond, CreateContext(tradeType: "香草")));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(neqCond, CreateContext(tradeType: "雪球")));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_AlphaOp_CaseInsensitive_Works()
|
||||
{
|
||||
// 大写字母 op 也应识别(归一化为小写)
|
||||
var condition = BuildTokens(new ConditionItem { field = "notional", op = "GTE", value = 100 });
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 100)));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 99)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_MixedAlphaAndSymbolOp_Works()
|
||||
{
|
||||
// 字母 op 与符号 op 混用:notional gt 100 and tradeType == 香草
|
||||
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
|
||||
{
|
||||
tokens = new System.Collections.Generic.List<ConditionToken>
|
||||
{
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = "gt", value = 100 } },
|
||||
new ConditionToken { type = "operator", connector = "and" },
|
||||
new ConditionToken { type = "condition", condition = new ConditionItem { field = "tradeType", op = "==", value = "香草" } }
|
||||
}
|
||||
});
|
||||
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200, tradeType: "香草")));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 50, tradeType: "香草")));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200, tradeType: "雪球")));
|
||||
}
|
||||
|
||||
// ===== 需求①触发条件字段:initialNotional(期初)/ currentNotional(本次)=====
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_InitialNotional_UsesOriginalStockEqvNotional()
|
||||
{
|
||||
// initialNotional 取 trade.OriginalStockEqvNotional(与 StockEqvNotional 是不同字段)
|
||||
var condition = BuildTokens(new ConditionItem { field = "initialNotional", op = "gte", value = 1000000 });
|
||||
var ctx = new ConditionContext
|
||||
{
|
||||
Trade = new DBModels.trade
|
||||
{
|
||||
StockEqvNotional = 500, // 当前份额,不应被 initialNotional 使用
|
||||
OriginalStockEqvNotional = 2000000 // 期初名义本金
|
||||
}
|
||||
};
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, ctx));
|
||||
|
||||
var ctxBelow = new ConditionContext
|
||||
{
|
||||
Trade = new DBModels.trade { OriginalStockEqvNotional = 500000 }
|
||||
};
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, ctxBelow));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_CurrentNotional_UsesContextValue()
|
||||
{
|
||||
// currentNotional 取 ConditionContext.CurrentNotional(了结场景由 trade_cash 取绝对值传入)
|
||||
var condition = BuildTokens(new ConditionItem { field = "currentNotional", op = "gt", value = 1000000 });
|
||||
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, new ConditionContext { CurrentNotional = 5000000 }));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, new ConditionContext { CurrentNotional = 500000 }));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, new ConditionContext { CurrentNotional = null }));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_CurrentNotional_AbsoluteValueSemantics()
|
||||
{
|
||||
// 需求:平仓500万,本次交易名义本金按绝对值判断。
|
||||
// 调用方应传 Math.Abs 后的正值(BuildTriggerContext 已处理),这里验证传入正值即可。
|
||||
var condition = BuildTokens(new ConditionItem { field = "currentNotional", op = "gte", value = 5000000 });
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, new ConditionContext { CurrentNotional = 5000000 }));
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, new ConditionContext { CurrentNotional = 8000000 }));
|
||||
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, new ConditionContext { CurrentNotional = 4999999 }));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Evaluate_InitialNotional_AbsoluteValueStored()
|
||||
{
|
||||
// OriginalStockEqvNotional 的 setter 已做 Math.Abs,负值存入会变正
|
||||
var condition = BuildTokens(new ConditionItem { field = "initialNotional", op = "gt", value = 100 });
|
||||
var ctx = new ConditionContext
|
||||
{
|
||||
Trade = new DBModels.trade { OriginalStockEqvNotional = -500 } // setter 归一化为 500
|
||||
};
|
||||
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, ctx));
|
||||
}
|
||||
|
||||
/// <summary>辅助:单条件 tokens 序列化为 JSON。</summary>
|
||||
private static string BuildTokens(ConditionItem item)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new ConditionExpressionConfig
|
||||
{
|
||||
tokens = new System.Collections.Generic.List<ConditionToken>
|
||||
{
|
||||
new ConditionToken { type = "condition", condition = item }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Newtonsoft.Json;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Helpers;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace YLErp.Helpers.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// 需求①:交易提交进入审批流程时的「起点触发条件跳过」测试。
|
||||
/// <para>场景:交易一进来,若节点1、2配置的触发条件均不满足,应直接从节点3开始审核;
|
||||
/// 若全部节点都不满足,则直接审批通过(无需任何审核)。</para>
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class TriggerNodeSkipTests
|
||||
{
|
||||
/// <summary>构造一个审批节点:order + 可选的触发条件JSON。</summary>
|
||||
private static approvalprocess Node(int order, string triggerCondition = null)
|
||||
{
|
||||
return new approvalprocess
|
||||
{
|
||||
order = order,
|
||||
node = 0,
|
||||
triggerCondition = triggerCondition
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>构造"期初名义本金 > 阈值"的触发条件JSON。</summary>
|
||||
private static string InitialNotionalGt(double threshold)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new ConditionExpressionConfig
|
||||
{
|
||||
tokens = new List<ConditionToken>
|
||||
{
|
||||
new ConditionToken
|
||||
{
|
||||
type = "condition",
|
||||
condition = new ConditionItem { field = "initialNotional", op = "gt", value = threshold }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static ConditionContext Ctx(double initialNotional)
|
||||
{
|
||||
return new ConditionContext
|
||||
{
|
||||
Trade = new trade { OriginalStockEqvNotional = initialNotional }
|
||||
};
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StartNode_NoTrigger_ReturnsStartDirectly()
|
||||
{
|
||||
// 节点1无触发条件 → 直接返回节点1
|
||||
var nodes = new List<approvalprocess> { Node(1), Node(2), Node(3) };
|
||||
var result = ConditionEvaluator.FindFirstTriggeredNode(nodes, nodes[0], Ctx(500));
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(1, result.order);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StartNode_TriggerSatisfied_ReturnsStart()
|
||||
{
|
||||
// 节点1触发条件">100",交易期初200满足 → 返回节点1
|
||||
var nodes = new List<approvalprocess>
|
||||
{
|
||||
Node(1, InitialNotionalGt(100)),
|
||||
Node(2),
|
||||
Node(3)
|
||||
};
|
||||
var result = ConditionEvaluator.FindFirstTriggeredNode(nodes, nodes[0], Ctx(200));
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(1, result.order);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StartNode_TriggerNotSatisfied_SkipsToNextSatisfied()
|
||||
{
|
||||
// 节点1触发">100",节点2触发">500",交易期初200
|
||||
// → 节点1不满足(200>100满足? 满足)... 重新设计:节点1触发">1000",200不满足;节点2无触发 → 返回节点2
|
||||
var nodes = new List<approvalprocess>
|
||||
{
|
||||
Node(1, InitialNotionalGt(1000)), // 200 不满足 >1000
|
||||
Node(2), // 无触发条件
|
||||
Node(3)
|
||||
};
|
||||
var result = ConditionEvaluator.FindFirstTriggeredNode(nodes, nodes[0], Ctx(200));
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(2, result.order);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AllStartNodesNotSatisfied_ReturnsNull_DirectlyApproved()
|
||||
{
|
||||
// 节点1、2、3都有触发条件,交易都不满足 → 返回null(表示直接审批通过)
|
||||
var nodes = new List<approvalprocess>
|
||||
{
|
||||
Node(1, InitialNotionalGt(1000)), // 200 不满足
|
||||
Node(2, InitialNotionalGt(5000)), // 200 不满足
|
||||
Node(3, InitialNotionalGt(10000)) // 200 不满足
|
||||
};
|
||||
var result = ConditionEvaluator.FindFirstTriggeredNode(nodes, nodes[0], Ctx(200));
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SkipMultipleNodes_LandsOnThird()
|
||||
{
|
||||
// 节点1、2都不满足,节点3无触发 → 返回节点3
|
||||
// 模拟"A默认审核、B>100W再审核、C>=500W再审核"中,小额交易跳过B、C直达... 实际应停在满足的节点
|
||||
var nodes = new List<approvalprocess>
|
||||
{
|
||||
Node(1, InitialNotionalGt(1000000)), // 50W 不满足
|
||||
Node(2, InitialNotionalGt(2000000)), // 50W 不满足
|
||||
Node(3) // 无触发条件(兜底审核)
|
||||
};
|
||||
var result = ConditionEvaluator.FindFirstTriggeredNode(nodes, nodes[0], Ctx(500000));
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(3, result.order);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SatisfiedAtSecondNode_StopsThere()
|
||||
{
|
||||
// 节点1触发">1000"不满足,节点2触发">100"满足 → 返回节点2(不会继续到节点3)
|
||||
var nodes = new List<approvalprocess>
|
||||
{
|
||||
Node(1, InitialNotionalGt(1000)), // 200 不满足
|
||||
Node(2, InitialNotionalGt(100)), // 200 满足
|
||||
Node(3, InitialNotionalGt(50)) // 不会走到这
|
||||
};
|
||||
var result = ConditionEvaluator.FindFirstTriggeredNode(nodes, nodes[0], Ctx(200));
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(2, result.order);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StartFromMiddleNode_Works()
|
||||
{
|
||||
// 起点不是节点1(如分支调整后从节点2开始),从节点2起判断
|
||||
var nodes = new List<approvalprocess>
|
||||
{
|
||||
Node(1, InitialNotionalGt(1000)),
|
||||
Node(2, InitialNotionalGt(1000)), // 200 不满足
|
||||
Node(3) // 无触发
|
||||
};
|
||||
var result = ConditionEvaluator.FindFirstTriggeredNode(nodes, nodes[1], Ctx(200));
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(3, result.order);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NullStart_ReturnsNull()
|
||||
{
|
||||
var nodes = new List<approvalprocess> { Node(1) };
|
||||
var result = ConditionEvaluator.FindFirstTriggeredNode(nodes, null, Ctx(500));
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
using BaseOUDAL;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace YLErp.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 审批条件求值器:统一支撑「节点触发条件」与「分支网关条件」。
|
||||
/// <para>需求①(节点触发)与需求③(多分支)共用同一套条件模型,避免两套条件语义。</para>
|
||||
/// <para>支持混合「且/或」与「括号」的布尔表达式,如 A and (B or C)。</para>
|
||||
/// </summary>
|
||||
public static class ConditionEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// 求值条件 JSON。
|
||||
/// </summary>
|
||||
/// <param name="conditionJson">条件 JSON(结构见 ConditionExpressionConfig);为空/null 视为无条件,返回 false(不触发)。</param>
|
||||
/// <param name="context">业务上下文(交易/发起人等)。</param>
|
||||
/// <returns>是否满足条件</returns>
|
||||
public static bool Evaluate(string conditionJson, ConditionContext context)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(conditionJson))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ConditionExpressionConfig config;
|
||||
try
|
||||
{
|
||||
config = JsonConvert.DeserializeObject<ConditionExpressionConfig>(conditionJson);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 容错:非法 JSON 不阻断审批主流程,视为不触发。
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config == null || config.tokens == null || config.tokens.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 递归下降求值(支持括号、且/或混合)。
|
||||
var parser = new ConditionParser(config.tokens, context);
|
||||
return parser.Parse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从起点节点开始,向后查找第一个满足触发条件(或无触发条件)的审批节点(需求①)。
|
||||
/// <para>用于交易提交进入审批流程时确定起始审批节点:若起点节点配置了触发条件且当前业务不满足,
|
||||
/// 则跳过该节点继续向后找,直到找到可进入的节点;若从起点到末尾均不满足则返回 null(表示无需审批,直接通过)。</para>
|
||||
/// </summary>
|
||||
/// <param name="tradeProcess">流程全部节点(已按 order 排序)</param>
|
||||
/// <param name="start">起点节点</param>
|
||||
/// <param name="ctx">条件求值上下文</param>
|
||||
/// <returns>第一个应进入审批的节点;若无需审批则返回 null</returns>
|
||||
public static approvalprocess FindFirstTriggeredNode(
|
||||
List<approvalprocess> tradeProcess,
|
||||
approvalprocess start,
|
||||
ConditionContext ctx)
|
||||
{
|
||||
if (start == null) return null;
|
||||
var current = start;
|
||||
while (current != null)
|
||||
{
|
||||
// 无触发条件,或满足触发条件 → 该节点需审批
|
||||
if (string.IsNullOrWhiteSpace(current.triggerCondition)
|
||||
|| Evaluate(current.triggerCondition, ctx))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
// 不满足 → 向后取下一个主干节点(node=0)
|
||||
current = tradeProcess.FirstOrDefault(x => x.order > current.order && x.node == 0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>求值单个条件。</summary>
|
||||
internal static bool EvaluateSingle(ConditionItem cond, ConditionContext context)
|
||||
{
|
||||
if (cond == null || string.IsNullOrEmpty(cond.field) || string.IsNullOrEmpty(cond.op))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var leftValue = FieldResolver.ResolveValue(cond.field, context);
|
||||
return OperatorCompare(leftValue, cond.op, cond.value);
|
||||
}
|
||||
|
||||
/// <summary>比较:能转数值时按数值比,否则按字符串比。</summary>
|
||||
private static bool OperatorCompare(object left, string op, object right)
|
||||
{
|
||||
if (TryToDouble(left, out var ld) && TryToDouble(right, out var rd))
|
||||
{
|
||||
return CompareNumeric(ld, op, rd);
|
||||
}
|
||||
var ls = left?.ToString() ?? string.Empty;
|
||||
var rs = right?.ToString() ?? string.Empty;
|
||||
return CompareString(ls, op, rs);
|
||||
}
|
||||
|
||||
private static bool CompareNumeric(double left, string op, double right)
|
||||
{
|
||||
return NormalizeOp(op) switch
|
||||
{
|
||||
">" => left > right,
|
||||
"<" => left < right,
|
||||
">=" => left >= right,
|
||||
"<=" => left <= right,
|
||||
"==" => Math.Abs(left - right) < 1e-9,
|
||||
"!=" => Math.Abs(left - right) >= 1e-9,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static bool CompareString(string left, string op, string right)
|
||||
{
|
||||
return NormalizeOp(op) switch
|
||||
{
|
||||
"==" => left == right,
|
||||
"!=" => left != right,
|
||||
">" => string.Compare(left, right, StringComparison.Ordinal) > 0,
|
||||
"<" => string.Compare(left, right, StringComparison.Ordinal) < 0,
|
||||
">=" => string.Compare(left, right, StringComparison.Ordinal) >= 0,
|
||||
"<=" => string.Compare(left, right, StringComparison.Ordinal) <= 0,
|
||||
"in" => right.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Any(r => string.Equals(r.Trim(), left, StringComparison.OrdinalIgnoreCase)),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 归一化操作符:兼容字母标识符(gt/lt/gte/lte/eq/neq)与符号(> < >= <= == != =)。
|
||||
/// </summary>
|
||||
private static string NormalizeOp(string op)
|
||||
{
|
||||
if (string.IsNullOrEmpty(op)) return op;
|
||||
return op.ToLowerInvariant() switch
|
||||
{
|
||||
"gt" or ">" => ">",
|
||||
"lt" or "<" => "<",
|
||||
"gte" or ">=" => ">=",
|
||||
"lte" or "<=" => "<=",
|
||||
"eq" or "==" or "=" => "==",
|
||||
"neq" or "!=" or "<>" => "!=",
|
||||
_ => op
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryToDouble(object value, out double result)
|
||||
{
|
||||
result = 0;
|
||||
if (value == null) return false;
|
||||
return double.TryParse(value.ToString(), out result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归下降解析器:按 token 顺序求值布尔表达式,支持括号与「且/或」优先级。
|
||||
/// <para>文法:Expr := Term (("and"|"or") Term)* ;Term := condition | "(" Expr ")"。</para>
|
||||
/// <para>优先级:and 高于 or(与常规布尔代数一致);同级从左到右。</para>
|
||||
/// </summary>
|
||||
internal class ConditionParser
|
||||
{
|
||||
private readonly List<ConditionToken> _tokens;
|
||||
private readonly ConditionContext _context;
|
||||
private int _pos;
|
||||
|
||||
public ConditionParser(List<ConditionToken> tokens, ConditionContext context)
|
||||
{
|
||||
_tokens = tokens ?? new List<ConditionToken>();
|
||||
_context = context;
|
||||
_pos = 0;
|
||||
}
|
||||
|
||||
public bool Parse()
|
||||
{
|
||||
if (_tokens.Count == 0) return false;
|
||||
return ParseOr();
|
||||
}
|
||||
|
||||
// 低优先级:ParseOr := ParseAnd ( "or" ParseAnd )* 左结合,遇 or 短路(为 true 直接返回后续不再求值)
|
||||
private bool ParseOr()
|
||||
{
|
||||
var left = ParseAnd();
|
||||
while (true)
|
||||
{
|
||||
var op = Peek();
|
||||
if (op == null || op.type != "operator" || !IsOr(op.connector)) break;
|
||||
_pos++; // 消费 or
|
||||
var right = ParseAnd();
|
||||
left = left || right;
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
// 高优先级:ParseAnd := ParseTerm ( "and" ParseTerm )* 左结合,遇 and 短路(为 false 直接返回)
|
||||
private bool ParseAnd()
|
||||
{
|
||||
var left = ParseTerm();
|
||||
while (true)
|
||||
{
|
||||
var op = Peek();
|
||||
if (op == null || op.type != "operator" || IsOr(op.connector)) break;
|
||||
_pos++; // 消费 and
|
||||
var right = ParseTerm();
|
||||
left = left && right;
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
// Term := condition | "(" ParseOr ")"
|
||||
private bool ParseTerm()
|
||||
{
|
||||
var tok = Peek();
|
||||
if (tok == null) return false;
|
||||
|
||||
if (tok.type == "lparen")
|
||||
{
|
||||
_pos++; // 消费 "("
|
||||
var val = ParseOr();
|
||||
var rp = Peek();
|
||||
if (rp != null && rp.type == "rparen") _pos++; // 消费 ")"
|
||||
return val;
|
||||
}
|
||||
|
||||
if (tok.type == "condition")
|
||||
{
|
||||
_pos++;
|
||||
return ConditionEvaluator.EvaluateSingle(tok.condition, _context);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private ConditionToken Peek() => _pos < _tokens.Count ? _tokens[_pos] : null;
|
||||
|
||||
private static bool IsOr(string connector)
|
||||
=> string.Equals(connector, "or", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>条件业务上下文:求值时由调用方构造,封装可参与判断的业务字段。</summary>
|
||||
public class ConditionContext
|
||||
{
|
||||
/// <summary>发起人 userId(用于查发起人审批组)。</summary>
|
||||
public int? UserId { get; set; }
|
||||
|
||||
/// <summary>交易实体(期初名义本金等字段来源)。开户场景可为 null。</summary>
|
||||
public trade Trade { get; set; }
|
||||
|
||||
/// <summary>交易流程类别:开仓(TradeProcess) / 了结(CloseProcess)。需求②用。</summary>
|
||||
public string ProcessCategory { get; set; }
|
||||
|
||||
/// <summary>预解析的发起人审批组(避免重复查库;为空时由 FieldResolver 查)。</summary>
|
||||
public int? InitGroupId { get; set; }
|
||||
|
||||
/// <summary>本次交易名义本金的取值(了结场景由调用方从 trade_cash.UnwindStockEqvNotional 取绝对值传入)。需求①。</summary>
|
||||
public double? CurrentNotional { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 条件表达式配置(对应 conditionConfig / triggerCondition 列的 JSON 结构)。
|
||||
/// <para>tokens:token 序列,支持「且/或」混合与括号,按表达式顺序排列。</para>
|
||||
/// </summary>
|
||||
public class ConditionExpressionConfig
|
||||
{
|
||||
/// <summary>token 序列:条件 / 且或连接符 / 左右括号,按表达式顺序排列。</summary>
|
||||
public List<ConditionToken> tokens { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表达式 token:一个条件、一个连接符、或一个括号。
|
||||
/// </summary>
|
||||
public class ConditionToken
|
||||
{
|
||||
/// <summary>token 类型:condition | operator | lparen | rparen</summary>
|
||||
public string type { get; set; }
|
||||
|
||||
/// <summary>当 type=condition 时的条件体。</summary>
|
||||
public ConditionItem condition { get; set; }
|
||||
|
||||
/// <summary>当 type=operator 时的连接符:and | or</summary>
|
||||
public string connector { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>单个条件:左值字段 + 操作符 + 右值。</summary>
|
||||
public class ConditionItem
|
||||
{
|
||||
/// <summary>左值字段 key,见 FieldResolver(initGroup/notional/tradeType 等)。</summary>
|
||||
public string field { get; set; }
|
||||
|
||||
/// <summary>操作符:> < >= <= == != = in</summary>
|
||||
public string op { get; set; }
|
||||
|
||||
/// <summary>右值</summary>
|
||||
public object value { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 条件左值解析:把 field key 映射到具体业务字段值。
|
||||
/// <para>触发条件字段(需求①):</para>
|
||||
/// <para>- initialNotional:交易的期初名义本金(trade.OriginalStockEqvNotional,已取绝对值)</para>
|
||||
/// <para>- currentNotional :本次交易名义本金(了结场景,由调用方从 trade_cash 取本次影响金额绝对值传入)</para>
|
||||
/// <para>历史兼容:initGroup/tradeType/processCategory/notional 仍可解析。</para>
|
||||
/// </summary>
|
||||
public static class FieldResolver
|
||||
{
|
||||
public static object ResolveValue(string field, ConditionContext context)
|
||||
{
|
||||
if (string.IsNullOrEmpty(field) || context == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (field.ToLowerInvariant())
|
||||
{
|
||||
// 需求①:触发条件字段(仅这两个对外暴露)
|
||||
case "initialnotional":
|
||||
// 交易的期初名义本金
|
||||
return context.Trade?.OriginalStockEqvNotional ?? 0;
|
||||
|
||||
case "currentnotional":
|
||||
// 本次交易名义本金(了结时按本次影响金额绝对值,由调用方传入)
|
||||
return context.CurrentNotional ?? 0;
|
||||
|
||||
// 以下为历史兼容,前端不再暴露
|
||||
case "notional":
|
||||
return context.Trade?.StockEqvNotional ?? 0;
|
||||
|
||||
case "initgroup":
|
||||
if (context.InitGroupId.HasValue)
|
||||
{
|
||||
return context.InitGroupId.Value;
|
||||
}
|
||||
return context.UserId.HasValue
|
||||
? UserBLL.GetApprovalProcessGroup(context.UserId.Value)
|
||||
: 0;
|
||||
|
||||
case "tradetype":
|
||||
return context.Trade?.TradeType ?? string.Empty;
|
||||
|
||||
case "processcategory":
|
||||
return context.ProcessCategory ?? string.Empty;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@ using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using YLErp.BLL.Eod;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Helpers;
|
||||
using YLErp.Model.Enum;
|
||||
using YLErp.Modules.TradeModule;
|
||||
|
||||
namespace YLErp.Modules.SystemModule
|
||||
{
|
||||
@@ -43,7 +45,9 @@ namespace YLErp.Modules.SystemModule
|
||||
approvalGroupId = item.approvalGroupId,
|
||||
node = item.node,
|
||||
parentNode = item.parentNode,
|
||||
approvalCondition = item.approvalCondition
|
||||
approvalCondition = item.approvalCondition,
|
||||
conditionConfig = item.conditionConfig,
|
||||
triggerCondition = item.triggerCondition
|
||||
}).ToList();
|
||||
|
||||
DbContext.approvalprocess.AddRange(list);
|
||||
@@ -81,6 +85,21 @@ namespace YLErp.Modules.SystemModule
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type == ProcessCategoryConst.Close) // 需求②:了结/平仓/行权审批流程
|
||||
{
|
||||
if (data != null && data.Count > 0)
|
||||
{
|
||||
ChangeTradeProcess(data, delList);
|
||||
}
|
||||
else
|
||||
{
|
||||
var trade = DbContext.trade.Where(x => x.ValidState == "Valid" && (x.TradeStatus == ConsTrade.平仓待复核 || x.TradeStatus == ConsTrade.行权待复核 || x.TradeStatus == ConsTrade.互换待复核)).ToList();
|
||||
if (trade != null && trade.Count > 0)
|
||||
{
|
||||
throw new ServiceException("有交易在审批中,不能删除审批流程!");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type == "CreditProcess")
|
||||
{
|
||||
if (data != null && data.Count == 0)
|
||||
@@ -578,7 +597,8 @@ namespace YLErp.Modules.SystemModule
|
||||
{
|
||||
var roles = UserBLL.GetRolesByUserId(userId).Select(o => o.Id);
|
||||
var groupId = UserBLL.GetApprovalProcessGroup(userId);
|
||||
var tradeProcess = TradeProcess();
|
||||
// 需求②:按交易状态推断开仓/了结流程。
|
||||
var tradeProcess = TradeProcessByCategory(trade);
|
||||
bool approvalBranch = tradeProcess.Any(x => x.approvalGroupId != 0);//审批流程有分支情况
|
||||
trade.ProcessOrderBranch = 0;
|
||||
if (trade.ProcessOrderId <= 0)
|
||||
@@ -612,6 +632,16 @@ namespace YLErp.Modules.SystemModule
|
||||
trade.ProcessOrderId = ProcessTradeLog.审批中;
|
||||
}
|
||||
}
|
||||
// 需求①:开仓交易首次进入审批时,应用触发条件——跳过起始就不满足触发条件的节点。
|
||||
// 若所有节点均不满足 → 直接审批通过,无需审核。
|
||||
if (tradeProcess.Count > 0)
|
||||
{
|
||||
ApplyTriggerOnStart(tradeProcess, trade, userId);
|
||||
if (trade.ProcessOrderId == ProcessTradeLog.审批通过)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// 如果是审批组
|
||||
var orderIdCount = tradeProcess.Where(x => x.order == trade.ProcessOrderId);//判断是否有分支
|
||||
int processOrderNode = orderIdCount.Count() > 1 ? trade.ProcessOrderBranch : 0;
|
||||
@@ -658,6 +688,65 @@ namespace YLErp.Modules.SystemModule
|
||||
return -2;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 需求①:开仓交易首次进入审批流程时,从起始节点应用触发条件。
|
||||
/// <para>跳过起始就不满足触发条件的节点;若所有节点均不满足 → 直接审批通过。</para>
|
||||
/// </summary>
|
||||
private void ApplyTriggerOnStart(List<approvalprocess> tradeProcess, trade trade, int userId)
|
||||
{
|
||||
// 取当前起点节点(主干 node=0)
|
||||
var start = tradeProcess.FirstOrDefault(x => x.order == trade.ProcessOrderId && x.node == 0);
|
||||
if (start == null)
|
||||
{
|
||||
start = tradeProcess.FirstOrDefault(x => x.order >= trade.ProcessOrderId && x.node == 0);
|
||||
}
|
||||
if (start == null) return;
|
||||
|
||||
// 构建求值上下文(开仓场景无 trade_cash,CurrentNotional 不设)
|
||||
var ctx = new ConditionContext
|
||||
{
|
||||
UserId = userId,
|
||||
Trade = trade,
|
||||
ProcessCategory = ProcessCategoryConst.Resolve(trade),
|
||||
InitGroupId = UserBLL.GetApprovalProcessGroup(userId)
|
||||
};
|
||||
|
||||
// ===== 临时诊断日志(定位触发条件是否生效,验证后删除)=====
|
||||
try
|
||||
{
|
||||
var tn = trade?.TradeNumber ?? "?";
|
||||
var initNotional = trade?.OriginalStockEqvNotional ?? 0;
|
||||
System.Diagnostics.Debug.WriteLine($"[ApplyTriggerOnStart] trade={tn}, 期初名义本金={initNotional}, 起点order={start.order}, 起点triggerCondition={start.triggerCondition ?? "(空)"}, 节点数={tradeProcess.Count}");
|
||||
foreach (var n in tradeProcess)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($" 节点 order={n.order}, node={n.node}, roleId={n.roleId}, triggerCondition={n.triggerCondition ?? "(空)"}");
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
var target = ConditionEvaluator.FindFirstTriggeredNode(tradeProcess, start, ctx);
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[ApplyTriggerOnStart] FindFirstTriggeredNode 结果 = {(target == null ? "null(全不满足,直接通过)" : "order=" + target.order)}");
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (target == null)
|
||||
{
|
||||
// 所有节点都不满足触发条件 → 直接审批通过
|
||||
trade.ProcessOrderId = ProcessTradeLog.审批通过;
|
||||
trade.CheckTradeUpdate = Convert.ToInt32(TradeCheckEnum.StatusOfNew);
|
||||
trade.ProcessOptDate = DateTime.Now;
|
||||
trade.ProcessStatus = ProcessTradeStatus.通过审批.ToString();
|
||||
return;
|
||||
}
|
||||
if (target.order != trade.ProcessOrderId)
|
||||
{
|
||||
trade.ProcessOrderId = target.order;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有交易审批节点
|
||||
/// </summary>
|
||||
@@ -668,6 +757,21 @@ namespace YLErp.Modules.SystemModule
|
||||
return tradeOrders;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按交易推断流程类别(开仓/了结)获取审批节点(需求②)。
|
||||
/// <para>了结类优先取 CloseProcess;未配置则回退 TradeProcess。</para>
|
||||
/// </summary>
|
||||
public List<approvalprocess> TradeProcessByCategory(trade td)
|
||||
{
|
||||
var category = ProcessCategoryConst.Resolve(td);
|
||||
var orders = DbContext.approvalprocess.Where(t => t.processType == category).OrderBy(o => o.order).ToList();
|
||||
if (category == ProcessCategoryConst.Close && orders.Count == 0)
|
||||
{
|
||||
return TradeProcess();
|
||||
}
|
||||
return orders;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -686,6 +790,12 @@ namespace YLErp.Modules.SystemModule
|
||||
public int node { get; set; }
|
||||
public int parentNode { get; set; }
|
||||
public int approvalCondition { get; set; }
|
||||
|
||||
/// <summary>分支网关条件(JSON),需求①③共用。为空则回退旧 approvalGroupId/approvalCondition 二元判断。</summary>
|
||||
public string conditionConfig { get; set; }
|
||||
|
||||
/// <summary>节点触发条件(JSON),需求①:满足才进入该审批节点,不满足则跳过。</summary>
|
||||
public string triggerCondition { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// 审批流程修改节点
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using BaseOUDAL;
|
||||
using YLErp.BLL;
|
||||
using YLErp.BLL.Eod;
|
||||
using YLErp.Helpers;
|
||||
using YLErp.Model;
|
||||
using YLErp.Model.Enum;
|
||||
using YLErp.Modules.ClientModule;
|
||||
@@ -140,7 +141,8 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
return result;
|
||||
}
|
||||
//TODO 如果是审批组
|
||||
var tradeProessQuery = TradeProcess();
|
||||
// 需求②:按交易状态推断开仓/了结流程。了结类(平仓/行权/互换待复核)走 CloseProcess。
|
||||
var tradeProessQuery = TradeProcessByCategory(td);
|
||||
var count = tradeProessQuery.Count();
|
||||
if (count == 0 || td.ProcessOrderId == ProcessTradeLog.审批通过)//投资规模校验已经将数据设置为已通过
|
||||
{
|
||||
@@ -166,6 +168,8 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
//{
|
||||
// td.ProcessOrderBranch = 0;
|
||||
//}
|
||||
// 节点触发条件(需求①):下一节点配置了 triggerCondition 时,满足才进入审批,不满足则跳过。
|
||||
nextOrder = AdvanceThroughTriggerNodes(tradeProessQuery, nextOrder, BuildTriggerContext(td, UserId));
|
||||
if (nextOrder==null)
|
||||
{
|
||||
|
||||
@@ -225,7 +229,8 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
var childrenTrades = DbContext.trade.Where(x => childrenTradeIds.Contains(x.id)).ToList();
|
||||
var result = new TradeOpenResult(td);
|
||||
// 如果是审批组
|
||||
var tradeProessQuery = TradeProcess();
|
||||
// 需求②:分组了结按交易状态推断开仓/了结流程。
|
||||
var tradeProessQuery = TradeProcessByCategory(td);
|
||||
var count = tradeProessQuery.Count();
|
||||
if (count == 0)
|
||||
{
|
||||
@@ -246,6 +251,8 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
{
|
||||
nextOrder = tradeProessQuery.FirstOrDefault(x => x.order > td.ProcessOrderId && x.node == td.ProcessOrderBranch && x.approvalGroupId == 0);
|
||||
}
|
||||
// 节点触发条件(需求①):分组了结推进同样支持触发条件。
|
||||
nextOrder = AdvanceThroughTriggerNodes(tradeProessQuery, nextOrder, BuildTriggerContext(td, UserId));
|
||||
if (nextOrder == null)
|
||||
{
|
||||
SetParentTradeOpen(req, td, childrenTrades, parentTradeCash);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using YLErp.BLL;
|
||||
using YLErp.DBModels.Consts;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Helpers;
|
||||
|
||||
namespace YLErp.Modules.TradeModule
|
||||
{
|
||||
@@ -46,14 +47,38 @@ namespace YLErp.Modules.TradeModule
|
||||
return _tradeProcessCount.Value;
|
||||
}
|
||||
/// <summary>
|
||||
/// 获取所有交易审批节点
|
||||
/// 获取所有交易审批节点(开仓流程 TradeProcess)。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public List<approvalprocess> TradeProcess()
|
||||
{
|
||||
var tradeOrders = DbContext.approvalprocess.Where(t => t.processType == "TradeProcess").OrderBy(o => o.order).ToList();
|
||||
return tradeOrders;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按交易推断流程类别(开仓/了结)获取审批节点(需求②)。
|
||||
/// <para>了结类(平仓待复核/行权待复核/互换待复核)优先取 CloseProcess;若未配置则回退 TradeProcess,避免卡单。</para>
|
||||
/// </summary>
|
||||
public List<approvalprocess> TradeProcessByCategory(trade td)
|
||||
{
|
||||
var category = ResolveProcessCategory(td);
|
||||
var orders = DbContext.approvalprocess.Where(t => t.processType == category).OrderBy(o => o.order).ToList();
|
||||
// 兼容回退:了结流程未配置时,回退到开仓流程。
|
||||
if (category == ProcessCategoryConst.Close && orders.Count == 0)
|
||||
{
|
||||
return TradeProcess();
|
||||
}
|
||||
return orders;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按类别统计审批节点数(需求②)。
|
||||
/// </summary>
|
||||
public int TradeProcessCountByCategory(trade td)
|
||||
{
|
||||
return TradeProcessByCategory(td).Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化交易 审批点
|
||||
/// </summary>
|
||||
@@ -62,7 +87,7 @@ namespace YLErp.Modules.TradeModule
|
||||
{
|
||||
td.ProcessOrderId = ProcessTradeLog.审批中;
|
||||
td.ProcessStatus = ProcessTradeStatus.审批中.ToString();
|
||||
var tradeProcess = TradeProcess();
|
||||
var tradeProcess = TradeProcessByCategory(td);
|
||||
var groupId = UserBLL.GetApprovalProcessGroup(userId);
|
||||
bool approvalBranch = tradeProcess.Any(x => x.approvalGroupId != 0);//审批流程有分支情况
|
||||
if (td.ProcessOrderId == 1)
|
||||
@@ -88,6 +113,111 @@ namespace YLErp.Modules.TradeModule
|
||||
{
|
||||
td.ProcessOrderId = 2;
|
||||
}
|
||||
|
||||
// 需求①:进入审批流程时即应用触发条件——从起始节点开始,跳过所有不满足触发条件的节点。
|
||||
// 若所有节点均不满足 → 直接审批通过(无需任何人审核)。
|
||||
ApplyTriggerFromStart(tradeProcess, td, userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从当前 ProcessOrderId 起点开始,按触发条件跳过无需审批的节点(需求①)。
|
||||
/// <para>场景:交易提交进入审批流程时,若节点1、2配置的触发条件均不满足,则直接跳到节点3;
|
||||
/// 若全部节点都不满足,则直接审批通过。</para>
|
||||
/// </summary>
|
||||
private void ApplyTriggerFromStart(List<approvalprocess> tradeProcess, trade td, int userId)
|
||||
{
|
||||
if (tradeProcess == null || tradeProcess.Count == 0) return;
|
||||
|
||||
// 取当前起点节点(主干 node=0)
|
||||
var current = tradeProcess.FirstOrDefault(x => x.order == td.ProcessOrderId && x.node == 0);
|
||||
// 若起点不在主干(如分支调整后 ProcessOrderId=2),取该 order 的主干节点
|
||||
if (current == null)
|
||||
{
|
||||
current = tradeProcess.FirstOrDefault(x => x.order >= td.ProcessOrderId && x.node == 0);
|
||||
}
|
||||
if (current == null) return;
|
||||
|
||||
var ctx = BuildTriggerContext(td, userId);
|
||||
var target = ConditionEvaluator.FindFirstTriggeredNode(tradeProcess, current, ctx);
|
||||
if (target == null)
|
||||
{
|
||||
// 所有节点都不满足触发条件 → 直接审批通过,无需审核
|
||||
td.ProcessOrderId = ProcessTradeLog.审批通过;
|
||||
td.ProcessStatus = ProcessTradeStatus.通过审批.ToString();
|
||||
td.ProcessOptDate = DateTime.Now;
|
||||
return;
|
||||
}
|
||||
// target 即为第一个满足触发条件、需实际审批的节点
|
||||
if (target.order != td.ProcessOrderId)
|
||||
{
|
||||
td.ProcessOrderId = target.order;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 节点触发条件(需求①):在已确定下一审批节点 nextOrder 后,若该节点配置了 triggerCondition,
|
||||
/// 则只有「满足触发条件」才进入该节点审批;不满足则跳过该节点,继续向后寻找,直到找到可进入的节点或抵达流程末尾。
|
||||
/// <para>语义:triggerCondition 为空 → 无条件进入审批;非空 → 满足才进入,不满足则跳过。</para>
|
||||
/// <para>非侵入式:原有分支推进逻辑不变,仅在其结果之上叠加触发判断循环。</para>
|
||||
/// </summary>
|
||||
/// <param name="tradeProcess">当前流程的全部节点(已按 order 排序)</param>
|
||||
/// <param name="nextOrder">原逻辑计算出的下一节点(可能为 null)</param>
|
||||
/// <param name="ctx">条件求值业务上下文(已含 trade、本次交易名义本金等)</param>
|
||||
/// <returns>最终应推进到的节点;若应结束流程则返回 null</returns>
|
||||
protected static approvalprocess AdvanceThroughTriggerNodes(
|
||||
List<approvalprocess> tradeProcess,
|
||||
approvalprocess nextOrder,
|
||||
ConditionContext ctx)
|
||||
{
|
||||
// 不满足触发条件的节点需跳过:循环向后找第一个可进入的节点。
|
||||
while (nextOrder != null && !string.IsNullOrWhiteSpace(nextOrder.triggerCondition))
|
||||
{
|
||||
if (ConditionEvaluator.Evaluate(nextOrder.triggerCondition, ctx))
|
||||
{
|
||||
break; // 满足触发条件 → 进入该节点审批,停止跳过。
|
||||
}
|
||||
|
||||
// 不满足触发条件 → 跳过该节点,向后取下一个主干节点(node=0),继续判断。
|
||||
nextOrder = tradeProcess.FirstOrDefault(x => x.order > nextOrder.order && x.node == 0);
|
||||
}
|
||||
return nextOrder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建触发条件求值上下文:从 trade 及其关联的 trade_cash 取本次交易名义本金(了结场景)。
|
||||
/// <para>本次交易名义本金 = 本次了结操作的 trade_cash.UnwindStockEqvNotional 绝对值。</para>
|
||||
/// </summary>
|
||||
protected ConditionContext BuildTriggerContext(trade td, int userId)
|
||||
{
|
||||
var ctx = new ConditionContext
|
||||
{
|
||||
UserId = userId,
|
||||
Trade = td,
|
||||
ProcessCategory = ResolveProcessCategory(td),
|
||||
InitGroupId = UserBLL.GetApprovalProcessGroup(userId)
|
||||
};
|
||||
// 了结场景:取本次影响的名义本金(trade_cash.UnwindStockEqvNotional 绝对值)
|
||||
if (td != null && ctx.ProcessCategory == ProcessCategoryConst.Close)
|
||||
{
|
||||
var tc = DbContext.trade_cash
|
||||
.Where(t => t.TradeId == td.id && t.ValidState == ConsGlobal.InValid && !t.IsDeleted)
|
||||
.OrderByDescending(t => t.id)
|
||||
.FirstOrDefault();
|
||||
if (tc != null && tc.UnwindStockEqvNotional.HasValue)
|
||||
{
|
||||
ctx.CurrentNotional = Math.Abs(tc.UnwindStockEqvNotional.Value);
|
||||
}
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按交易状态推断流程类别:开仓(TradeProcess) / 了结(CloseProcess)。需求②。
|
||||
/// <para>委托给 ProcessCategoryConst.Resolve,供全局复用。</para>
|
||||
/// </summary>
|
||||
protected static string ResolveProcessCategory(trade td)
|
||||
{
|
||||
return ProcessCategoryConst.Resolve(td);
|
||||
}
|
||||
/// <summary>
|
||||
/// 添加交易操作日志
|
||||
@@ -521,4 +651,31 @@ namespace YLErp.Modules.TradeModule
|
||||
return rateCalcModeValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 交易流程类别常量(需求②):对应 approvalprocess.processType 的取值。
|
||||
/// </summary>
|
||||
public static class ProcessCategoryConst
|
||||
{
|
||||
/// <summary>开仓审批流程</summary>
|
||||
public const string Open = "TradeProcess";
|
||||
|
||||
/// <summary>了结/平仓/行权审批流程</summary>
|
||||
public const string Close = "CloseProcess";
|
||||
|
||||
/// <summary>
|
||||
/// 按交易状态推断流程类别:处于平仓/行权/互换待复核的交易视为「了结」类操作。
|
||||
/// </summary>
|
||||
public static string Resolve(trade td)
|
||||
{
|
||||
if (td == null) return Open;
|
||||
if (td.TradeStatus == ConsTrade.平仓待复核
|
||||
|| td.TradeStatus == ConsTrade.行权待复核
|
||||
|| td.TradeStatus == ConsTrade.互换待复核)
|
||||
{
|
||||
return Close;
|
||||
}
|
||||
return Open;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Org.BouncyCastle.Ocsp;
|
||||
using Newtonsoft.Json;
|
||||
using YLErp.Helpers;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Model.Enum;
|
||||
using YLErp.Modules.AppModule;
|
||||
@@ -43,7 +45,7 @@ namespace YLErp.Web.Controllers
|
||||
[HttpPost]
|
||||
public ActionResult AddProcess(string type, List<ApprovalProcessAddRequest> data)
|
||||
{
|
||||
if (type == "TradeProcess" && data != null && data.Count > 0)
|
||||
if ((type == "TradeProcess" || type == "CloseProcess") && data != null && data.Count > 0)
|
||||
{
|
||||
foreach (var item in data)
|
||||
{
|
||||
@@ -54,6 +56,25 @@ namespace YLErp.Web.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
// 校验节点触发条件 JSON 格式,避免非法数据入库
|
||||
if (data != null)
|
||||
{
|
||||
foreach (var item in data)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(item.triggerCondition))
|
||||
{
|
||||
try
|
||||
{
|
||||
JsonConvert.DeserializeObject<ConditionExpressionConfig>(item.triggerCondition);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return JsonError("触发条件格式非法,请检查括号与条件是否完整");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new ApprovalProcessService(CurUser).AddProcess(type, data);
|
||||
return JsonSuccess("设置成功");
|
||||
}
|
||||
@@ -66,10 +87,13 @@ namespace YLErp.Web.Controllers
|
||||
|
||||
var tradeProcess = list.Where(s => s.processType == "TradeProcess").OrderBy(s => s.order).ThenBy(s => s.parentNode).ThenBy(s => s.node).ToList();
|
||||
|
||||
// 需求②:了结/平仓/行权审批流程
|
||||
var closeProcess = list.Where(s => s.processType == "CloseProcess").OrderBy(s => s.order).ThenBy(s => s.parentNode).ThenBy(s => s.node).ToList();
|
||||
|
||||
var creditProcess = list.Where(s => s.processType == "CreditProcess").OrderBy(s => s.order).ToList();
|
||||
var outCashProcess = list.Where(s => s.processType == "OutCashProcess").OrderBy(s => s.order).ToList();
|
||||
var clientProcess = list.Where(s => s.processType == "ClientProcess").OrderBy(s => s.order).ThenBy(s => s.parentNode).ThenBy(s => s.node).ToList();
|
||||
return Json(new { OpenProcess = openProcess, TradeProcess = tradeProcess, CreditProcess = creditProcess, OutCashProcess= outCashProcess,ClientProcess = clientProcess });
|
||||
return Json(new { OpenProcess = openProcess, TradeProcess = tradeProcess, CloseProcess = closeProcess, CreditProcess = creditProcess, OutCashProcess= outCashProcess,ClientProcess = clientProcess });
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-model="openItems">
|
||||
<template>
|
||||
<template v-for="(item,index) in openItems">
|
||||
<!--第一个节点非分支 开始-->
|
||||
<div v-show="item.node==0&&item.parentNode==0">
|
||||
@@ -182,7 +182,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-model="clientItems">
|
||||
<template>
|
||||
<template v-for="(item,index) in clientItems">
|
||||
<!--第一个节点非分支 开始-->
|
||||
<div v-show="item.node==0&&item.parentNode==0">
|
||||
@@ -312,12 +312,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-model="tradeItems">
|
||||
<template>
|
||||
<template v-for="(item,index) in tradeItems">
|
||||
<!--第一个节点非分支 开始-->
|
||||
<div v-show="item.node==0&&item.parentNode==0">
|
||||
<div class="node-wrap">
|
||||
<div class="node-wrap-box start-node " style="width:282px;">
|
||||
<div class="node-wrap-box start-node " style="width:340px;">
|
||||
<div class="title" style="background: rgb(255, 148, 62);">
|
||||
<span class="userEdit">审核节点</span>
|
||||
<i class="glyphicon glyphicon-remove btnRemove" v-on:click="delProcess(item)"></i>
|
||||
@@ -334,6 +334,33 @@
|
||||
<span>审批规则</span>
|
||||
<input :id="'ruleType'+item.Index+item.node" v-model="item.ApprovalRules" type="text" name="selectRule" data-placeholder="" style="width: 200px; height: 20px" multiple="" />
|
||||
</div>
|
||||
<div class="trigger-condition-box">
|
||||
<div class="trigger-condition-title">
|
||||
<span>触发条件</span>
|
||||
<div class="trigger-actions">
|
||||
<button class="trigger-action-btn" v-on:click="addTriggerCondition(item)">+ 条件</button>
|
||||
<button class="trigger-action-btn" v-on:click="addTriggerParen(item,'left')">+ (</button>
|
||||
<button class="trigger-action-btn" v-on:click="addTriggerParen(item,'right')">+ )</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="trigger-token-list">
|
||||
<div v-for="(tok,ti) in item._trigger.tokens" class="trigger-token-row">
|
||||
<span v-if="tok.type=='operator'" class="trigger-connector" v-on:click="toggleTriggerConnector(item,ti)">{{tok.connector=='or'?'或':'且'}}</span>
|
||||
<span v-if="tok.type=='lparen'" class="trigger-paren">( <i class="glyphicon glyphicon-remove trigger-remove" v-on:click="removeTriggerToken(item,ti)"></i></span>
|
||||
<span v-if="tok.type=='rparen'" class="trigger-paren">)<i class="glyphicon glyphicon-remove trigger-remove" v-on:click="removeTriggerToken(item,ti)"></i></span>
|
||||
<span v-if="tok.type=='condition'" class="trigger-condition-row">
|
||||
<select v-model="tok.condition.field">
|
||||
<option v-for="opt in availableConditionFields" v-bind:value="opt.value">{{opt.text}}</option>
|
||||
</select>
|
||||
<select v-model="tok.condition.op">
|
||||
<option v-for="opt in conditionOps" v-bind:value="opt.value">{{opt.text}}</option>
|
||||
</select>
|
||||
<input v-model="tok.condition.value" type="text" placeholder="阈值" />
|
||||
<i class="glyphicon glyphicon-remove trigger-remove" v-on:click="removeTriggerToken(item,ti)"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-add-btn-box">
|
||||
<div class="add-node-btn">
|
||||
@@ -382,7 +409,7 @@
|
||||
<!--第一个节点非分支 开始-->
|
||||
<div v-for="childSecond in filterChild(child.node)">
|
||||
<div class="node-wrap">
|
||||
<div class="node-wrap-box start-node " style="width:282px;">
|
||||
<div class="node-wrap-box start-node " style="width:340px;">
|
||||
<div class="title" style="background: rgb(255, 148, 62);">
|
||||
<span class="userEdit">审核节点</span>
|
||||
<i class="glyphicon glyphicon-remove btnRemove" v-on:click="delProcess(childSecond)"></i>
|
||||
@@ -399,6 +426,33 @@
|
||||
<span>审批规则</span>
|
||||
<input :id="'ruleType'+childSecond.Index+childSecond.node" v-model="childSecond.ApprovalRules" type="text" name="selectRule" data-placeholder="" style="width: 200px; height: 20px" multiple="" />
|
||||
</div>
|
||||
<div class="trigger-condition-box">
|
||||
<div class="trigger-condition-title">
|
||||
<span>触发条件</span>
|
||||
<div class="trigger-actions">
|
||||
<button class="trigger-action-btn" v-on:click="addTriggerCondition(childSecond)">+ 条件</button>
|
||||
<button class="trigger-action-btn" v-on:click="addTriggerParen(childSecond,'left')">+ (</button>
|
||||
<button class="trigger-action-btn" v-on:click="addTriggerParen(childSecond,'right')">+ )</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="trigger-token-list">
|
||||
<div v-for="(tok,ti) in childSecond._trigger.tokens" class="trigger-token-row">
|
||||
<span v-if="tok.type=='operator'" class="trigger-connector" v-on:click="toggleTriggerConnector(childSecond,ti)">{{tok.connector=='or'?'或':'且'}}</span>
|
||||
<span v-if="tok.type=='lparen'" class="trigger-paren">( <i class="glyphicon glyphicon-remove trigger-remove" v-on:click="removeTriggerToken(childSecond,ti)"></i></span>
|
||||
<span v-if="tok.type=='rparen'" class="trigger-paren">)<i class="glyphicon glyphicon-remove trigger-remove" v-on:click="removeTriggerToken(childSecond,ti)"></i></span>
|
||||
<span v-if="tok.type=='condition'" class="trigger-condition-row">
|
||||
<select v-model="tok.condition.field">
|
||||
<option v-for="opt in availableConditionFields" v-bind:value="opt.value">{{opt.text}}</option>
|
||||
</select>
|
||||
<select v-model="tok.condition.op">
|
||||
<option v-for="opt in conditionOps" v-bind:value="opt.value">{{opt.text}}</option>
|
||||
</select>
|
||||
<input v-model="tok.condition.value" type="text" placeholder="阈值" />
|
||||
<i class="glyphicon glyphicon-remove trigger-remove" v-on:click="removeTriggerToken(childSecond,ti)"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-add-btn-box">
|
||||
<div class="add-node-btn">
|
||||
@@ -435,6 +489,199 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 需求②:交易了结流程(结构同交易流程,绑定 closeItems) -->
|
||||
<div v-show="isClose">
|
||||
<div style="margin: 10px auto">交易了结流程(平仓/行权/互换)</div>
|
||||
<div>
|
||||
<div class="node-wrap">
|
||||
<div class="end-node">
|
||||
<div class="end-node-text">
|
||||
申请人
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-add-btn-box">
|
||||
<div class="add-node-btn">
|
||||
<button class="addNodeClick" v-on:click="closeShowProcess(0,false,$event)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template>
|
||||
<template v-for="(item,index) in closeItems">
|
||||
<!--第一个节点非分支 开始-->
|
||||
<div v-show="item.node==0&&item.parentNode==0">
|
||||
<div class="node-wrap">
|
||||
<div class="node-wrap-box start-node " style="width:340px;">
|
||||
<div class="title" style="background: rgb(255, 148, 62);">
|
||||
<span class="userEdit">审核节点</span>
|
||||
<i class="glyphicon glyphicon-remove btnRemove" v-on:click="closeDelProcess(item)"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span>审核角色</span>
|
||||
<select v-model="item.SelectValue" v-on:change="closeSelectChangeType(item.Index-1,item.SelectValue)" style="width:200px;height:20px;">
|
||||
<option v-for="option in roleOptions" v-bind:value="option.Value">
|
||||
{{option.Text}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<span>审批规则</span>
|
||||
<input :id="'closeRuleType'+item.Index+item.node" v-model="item.ApprovalRules" type="text" name="selectRule" data-placeholder="" style="width: 200px; height: 20px" multiple="" />
|
||||
</div>
|
||||
<div class="trigger-condition-box">
|
||||
<div class="trigger-condition-title">
|
||||
<span>触发条件</span>
|
||||
<div class="trigger-actions">
|
||||
<button class="trigger-action-btn" v-on:click="addTriggerCondition(item)">+ 条件</button>
|
||||
<button class="trigger-action-btn" v-on:click="addTriggerParen(item,'left')">+ (</button>
|
||||
<button class="trigger-action-btn" v-on:click="addTriggerParen(item,'right')">+ )</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="trigger-token-list">
|
||||
<div v-for="(tok,ti) in item._trigger.tokens" class="trigger-token-row">
|
||||
<span v-if="tok.type=='operator'" class="trigger-connector" v-on:click="toggleTriggerConnector(item,ti)">{{tok.connector=='or'?'或':'且'}}</span>
|
||||
<span v-if="tok.type=='lparen'" class="trigger-paren">( <i class="glyphicon glyphicon-remove trigger-remove" v-on:click="removeTriggerToken(item,ti)"></i></span>
|
||||
<span v-if="tok.type=='rparen'" class="trigger-paren">)<i class="glyphicon glyphicon-remove trigger-remove" v-on:click="removeTriggerToken(item,ti)"></i></span>
|
||||
<span v-if="tok.type=='condition'" class="trigger-condition-row">
|
||||
<select v-model="tok.condition.field">
|
||||
<option v-for="opt in availableConditionFields" v-bind:value="opt.value">{{opt.text}}</option>
|
||||
</select>
|
||||
<select v-model="tok.condition.op">
|
||||
<option v-for="opt in conditionOps" v-bind:value="opt.value">{{opt.text}}</option>
|
||||
</select>
|
||||
<input v-model="tok.condition.value" type="text" placeholder="阈值" />
|
||||
<i class="glyphicon glyphicon-remove trigger-remove" v-on:click="removeTriggerToken(item,ti)"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-add-btn-box">
|
||||
<div class="add-node-btn">
|
||||
<button class="addNodeClick" v-on:click="closeShowProcess(item.Index,false,$event)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--第一个节点非分支 结束-->
|
||||
<!--第一个节点为分支 分支开始-->
|
||||
<div v-show="item.node==1&&item.approvalCondition!=0">
|
||||
<div class="branch-wrap">
|
||||
<div class="branch-box-wrap">
|
||||
<div class="branch-box">
|
||||
<span class="add-branch" title="添加条件">添加条件</span>
|
||||
<div class="col-box" v-for="child in closeFilterBranch()">
|
||||
<div class="condition-node">
|
||||
<div class="condition-node-box">
|
||||
<div class="node-wrap">
|
||||
<div class="node-wrap-box start-node " style="width: 282px;">
|
||||
<div class="title" style="background: rgb(255, 148, 62);">
|
||||
<span class="userEdit">分支{{child.node}}条件</span>
|
||||
<i class="glyphicon glyphicon-remove btnRemove" v-on:click="closeDelProcess(item)"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span>申请人</span>
|
||||
<select v-model="child.approvalCondition" style="width:75px;height:20px;">
|
||||
<option value="1">属于</option>
|
||||
<option value="2">不属于</option>
|
||||
</select>
|
||||
<select v-model="child.approvalGroupId" style="width:143px;height:20px;">
|
||||
<option v-for="option in grouplist" v-bind:value="option.id">
|
||||
{{option.groupName}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-add-btn-box">
|
||||
<div class="add-node-btn">
|
||||
<button class="addNodeClick" v-on:click="closeAddProcess(item.Index,true,child.node,$event)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--第一个节点非分支 开始-->
|
||||
<div v-for="childSecond in closeFilterChild(child.node)">
|
||||
<div class="node-wrap">
|
||||
<div class="node-wrap-box start-node " style="width:340px;">
|
||||
<div class="title" style="background: rgb(255, 148, 62);">
|
||||
<span class="userEdit">审核节点</span>
|
||||
<i class="glyphicon glyphicon-remove btnRemove" v-on:click="closeDelProcess(childSecond)"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span>审核角色</span>
|
||||
<select v-model="childSecond.SelectValue" style="width:200px;height:20px;">
|
||||
<option v-for="option in roleOptions" v-bind:value="option.Value">
|
||||
{{option.Text}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<span>审批规则</span>
|
||||
<input :id="'closeRuleType'+childSecond.Index+childSecond.node" v-model="childSecond.ApprovalRules" type="text" name="selectRule" data-placeholder="" style="width: 200px; height: 20px" multiple="" />
|
||||
</div>
|
||||
<div class="trigger-condition-box">
|
||||
<div class="trigger-condition-title">
|
||||
<span>触发条件</span>
|
||||
<div class="trigger-actions">
|
||||
<button class="trigger-action-btn" v-on:click="addTriggerCondition(childSecond)">+ 条件</button>
|
||||
<button class="trigger-action-btn" v-on:click="addTriggerParen(childSecond,'left')">+ (</button>
|
||||
<button class="trigger-action-btn" v-on:click="addTriggerParen(childSecond,'right')">+ )</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="trigger-token-list">
|
||||
<div v-for="(tok,ti) in childSecond._trigger.tokens" class="trigger-token-row">
|
||||
<span v-if="tok.type=='operator'" class="trigger-connector" v-on:click="toggleTriggerConnector(childSecond,ti)">{{tok.connector=='or'?'或':'且'}}</span>
|
||||
<span v-if="tok.type=='lparen'" class="trigger-paren">( <i class="glyphicon glyphicon-remove trigger-remove" v-on:click="removeTriggerToken(childSecond,ti)"></i></span>
|
||||
<span v-if="tok.type=='rparen'" class="trigger-paren">)<i class="glyphicon glyphicon-remove trigger-remove" v-on:click="removeTriggerToken(childSecond,ti)"></i></span>
|
||||
<span v-if="tok.type=='condition'" class="trigger-condition-row">
|
||||
<select v-model="tok.condition.field">
|
||||
<option v-for="opt in availableConditionFields" v-bind:value="opt.value">{{opt.text}}</option>
|
||||
</select>
|
||||
<select v-model="tok.condition.op">
|
||||
<option v-for="opt in conditionOps" v-bind:value="opt.value">{{opt.text}}</option>
|
||||
</select>
|
||||
<input v-model="tok.condition.value" type="text" placeholder="阈值" />
|
||||
<i class="glyphicon glyphicon-remove trigger-remove" v-on:click="removeTriggerToken(childSecond,ti)"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-add-btn-box">
|
||||
<div class="add-node-btn">
|
||||
<button class="addNodeClick" v-on:click="closeAddProcess(childSecond.Index,true,childSecond.node,$event)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--第一个节点非分支 结束-->
|
||||
<div class="top-left-cover-line" v-if="child.node==1"></div>
|
||||
<div class="bottom-left-cover-line" v-if="child.node==1"></div>
|
||||
<div class="top-right-cover-line" v-if="child.node!=1"></div>
|
||||
<div class="bottom-right-cover-line" v-if="child.node!=1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-add-btn-box">
|
||||
<div class="add-node-btn">
|
||||
<button class="addNodeClick" v-on:click="closeShowProcess(item.Index,false,$event)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--第一个节点为分支 分支结束-->
|
||||
</template>
|
||||
</template>
|
||||
<!-- 流程结束 -->
|
||||
<div class="end-node">
|
||||
<div class="end-node-circle"></div>
|
||||
<div class="end-node-text">
|
||||
结束流程
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="isCredit">
|
||||
<div style="margin: 10px auto">资信与授信流程</div>
|
||||
<div>
|
||||
|
||||
@@ -10,11 +10,7 @@ function initSelectize(selector, options) {
|
||||
});
|
||||
}
|
||||
};
|
||||
if (options) {
|
||||
_.assign(options, defaultOptions);
|
||||
} else {
|
||||
options = defaultOptions;
|
||||
}
|
||||
options = _.assign({}, defaultOptions, options || {});
|
||||
$(selector).selectize(options);
|
||||
}
|
||||
|
||||
@@ -47,18 +43,21 @@ var app = new Vue({
|
||||
{ text: '客户开户', value: '1' },
|
||||
{ text: '客户信息修改', value: '5' },
|
||||
{ text: '交易', value: '2' },
|
||||
{ text: '交易了结', value: '6' },
|
||||
/* { text: '资信与授信', value: '3' },*/
|
||||
{ text: '出金', value: '4' }
|
||||
|
||||
|
||||
],
|
||||
isOpen: false,
|
||||
isTrade: false,
|
||||
isClose: false,
|
||||
isCredit: false,
|
||||
isOutCash: false,
|
||||
isClient: false,
|
||||
openItems: [],
|
||||
clientItems: [],
|
||||
tradeItems: [],
|
||||
closeItems: [],
|
||||
creditItems: [],
|
||||
outCashItems: [],
|
||||
openCounter: 0,
|
||||
@@ -67,23 +66,32 @@ var app = new Vue({
|
||||
clientCounter: 0,
|
||||
tradeIndex: 0,
|
||||
grouplist: [],
|
||||
node: 0
|
||||
node: 0,
|
||||
// 需求①:节点触发条件字段(期初名义本金 / 本次交易名义本金)
|
||||
// 开仓流程只能选期初名义本金;了结流程两个都可选
|
||||
conditionFields: [
|
||||
{ value: 'initialNotional', text: '交易的期初名义本金' },
|
||||
{ value: 'currentNotional', text: '本次交易名义本金' }
|
||||
],
|
||||
// 开仓流程可选字段(仅期初)
|
||||
conditionFieldsOpen: [
|
||||
{ value: 'initialNotional', text: '交易的期初名义本金' }
|
||||
],
|
||||
conditionOps: [
|
||||
{ value: 'gt', text: '>' },
|
||||
{ value: 'lt', text: '<' },
|
||||
{ value: 'gte', text: '>=' },
|
||||
{ value: 'lte', text: '<=' },
|
||||
{ value: 'eq', text: '=' },
|
||||
{ value: 'neq', text: '≠' }
|
||||
],
|
||||
conditionLogics: [
|
||||
{ value: 'AND', text: '满足全部条件时触发' },
|
||||
{ value: 'OR', text: '满足任一条件时触发' }
|
||||
],
|
||||
roleOptions: []
|
||||
},
|
||||
computed: {
|
||||
roleOptions: function () {
|
||||
var items = [];
|
||||
main.post("/AccountOpeningProcess/GetRolesOption", {}, { async: false }).done(
|
||||
function (res) {
|
||||
for (var i = 0; i < res.length; i++) {
|
||||
items.push({
|
||||
Value: res[i].Value,
|
||||
Text: res[i].Text
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
return items;
|
||||
},
|
||||
filterBranch() {
|
||||
return this.tradeItems.filter(x => x.approvalCondition != 0);
|
||||
},
|
||||
@@ -92,9 +100,28 @@ var app = new Vue({
|
||||
},
|
||||
clientFilterBranch() {
|
||||
return this.clientItems.filter(x => x.approvalCondition != 0);
|
||||
},
|
||||
// 需求①:开仓流程只能选期初名义本金;了结流程两个都可选
|
||||
availableConditionFields() {
|
||||
return this.isClose ? this.conditionFields : this.conditionFieldsOpen;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadRoleOptions: function () {
|
||||
var thisObj = this;
|
||||
main.post("/AccountOpeningProcess/GetRolesOption", {}, { async: false }).done(
|
||||
function (res) {
|
||||
var items = [];
|
||||
for (var i = 0; i < res.length; i++) {
|
||||
items.push({
|
||||
Value: res[i].Value,
|
||||
Text: res[i].Text
|
||||
});
|
||||
}
|
||||
thisObj.roleOptions = items;
|
||||
}
|
||||
);
|
||||
},
|
||||
changeType: function () {
|
||||
var thisObj = this;
|
||||
$("#addtooltip-warpper").hide();
|
||||
@@ -107,6 +134,14 @@ var app = new Vue({
|
||||
} else if (thisObj.selected === '2') {
|
||||
thisObj.isOpen = false;
|
||||
thisObj.isTrade = true;
|
||||
thisObj.isClose = false;
|
||||
thisObj.isCredit = false;
|
||||
thisObj.isOutCash = false;
|
||||
thisObj.isClient = false;
|
||||
} else if (thisObj.selected === '6') { // 需求②:交易了结流程
|
||||
thisObj.isOpen = false;
|
||||
thisObj.isTrade = false;
|
||||
thisObj.isClose = true;
|
||||
thisObj.isCredit = false;
|
||||
thisObj.isOutCash = false;
|
||||
thisObj.isClient = false;
|
||||
@@ -227,6 +262,9 @@ var app = new Vue({
|
||||
if (selectType == "5") {
|
||||
typeName = 'ClientProcess'
|
||||
}
|
||||
if (selectType == "6") {
|
||||
typeName = 'CloseProcess'
|
||||
}
|
||||
var item = {
|
||||
id: 0,
|
||||
Type: typeName,
|
||||
@@ -258,7 +296,10 @@ var app = new Vue({
|
||||
thisObj.clientItems.splice(thisObj.node, 0, item, item2);
|
||||
thisObj.initClientItemIndex();
|
||||
}
|
||||
|
||||
else if (selectType == "6") { //交易了结
|
||||
thisObj.closeItems.splice(thisObj.node, 0, item, item2);
|
||||
thisObj.initCloseItemIndex();
|
||||
}
|
||||
}
|
||||
$("#addtooltip-warpper").hide(100);
|
||||
|
||||
@@ -321,6 +362,10 @@ var app = new Vue({
|
||||
thisObj.addClientProcess(index, child, node);
|
||||
return;
|
||||
}
|
||||
else if (selectType === "6") { //交易了结
|
||||
thisObj.addCloseNode(index, child, node);
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
delProcess: function (openItem) {
|
||||
@@ -471,7 +516,10 @@ var app = new Vue({
|
||||
approvalCondition: 0,
|
||||
node: node,
|
||||
parentNode: child ? index : 0,
|
||||
approvalGroupId: 0
|
||||
approvalGroupId: 0,
|
||||
conditionConfig: '',
|
||||
triggerCondition: '',
|
||||
_trigger: { tokens: [] }
|
||||
}
|
||||
thisObj.getRuleType();
|
||||
var childNodes = thisObj.tradeItems.filter(x => x.Index > index);
|
||||
@@ -537,6 +585,10 @@ var app = new Vue({
|
||||
thisObj.clientOk();
|
||||
return;
|
||||
}
|
||||
else if (selectType === "6") { // 需求②:交易了结
|
||||
thisObj.saveCloseProcess();
|
||||
return;
|
||||
}
|
||||
},
|
||||
openOk() {
|
||||
var thisObj = this;
|
||||
@@ -665,6 +717,12 @@ var app = new Vue({
|
||||
|
||||
thisObj.getRuleType();
|
||||
thisObj.initTradeItemIndex();
|
||||
var triggerValid = thisObj.validateAllTriggers(thisObj.tradeItems);
|
||||
if (!triggerValid.valid) {
|
||||
main.message(triggerValid.message);
|
||||
return;
|
||||
}
|
||||
thisObj.stringifyAllTrigger(thisObj.tradeItems); // 需求①:序列化触发条件
|
||||
var firstChildTradeItem = thisObj.tradeItems.filter(x => x.node == 1);
|
||||
var secondchildTradeItem = thisObj.tradeItems.filter(x => x.node == 2);
|
||||
if (firstChildTradeItem.length == 1 && secondchildTradeItem.length == 1) {
|
||||
@@ -864,6 +922,7 @@ var app = new Vue({
|
||||
var thisObj = this;
|
||||
thisObj.openItems = [];
|
||||
thisObj.tradeItems = [];
|
||||
thisObj.closeItems = [];
|
||||
thisObj.creditItems = [];
|
||||
thisObj.outCashItems = [];
|
||||
thisObj.clientItems = [];
|
||||
@@ -880,7 +939,9 @@ var app = new Vue({
|
||||
node: value.node,
|
||||
parentNode: value.parentNode,
|
||||
approvalGroupId: value.approvalGroupId,
|
||||
approvalCondition: value.approvalCondition
|
||||
approvalCondition: value.approvalCondition,
|
||||
conditionConfig: value.conditionConfig,
|
||||
triggerCondition: value.triggerCondition
|
||||
});
|
||||
});
|
||||
|
||||
@@ -894,9 +955,29 @@ var app = new Vue({
|
||||
node: value.node,
|
||||
parentNode: value.parentNode,
|
||||
approvalGroupId: value.approvalGroupId,
|
||||
approvalCondition: value.approvalCondition
|
||||
approvalCondition: value.approvalCondition,
|
||||
conditionConfig: value.conditionConfig,
|
||||
triggerCondition: value.triggerCondition
|
||||
});
|
||||
});
|
||||
// 需求②:了结/平仓/行权审批流程
|
||||
if (res.CloseProcess) {
|
||||
res.CloseProcess.forEach(function (value, index, array) {
|
||||
thisObj.closeItems.push({
|
||||
id: value.id,
|
||||
Type: value.processType,
|
||||
Index: value.order,
|
||||
SelectValue: value.roleId,
|
||||
ApprovalRules: value.ruleType,
|
||||
node: value.node,
|
||||
parentNode: value.parentNode,
|
||||
approvalGroupId: value.approvalGroupId,
|
||||
approvalCondition: value.approvalCondition,
|
||||
conditionConfig: value.conditionConfig,
|
||||
triggerCondition: value.triggerCondition
|
||||
});
|
||||
});
|
||||
}
|
||||
res.CreditProcess.forEach(function (value, index, array) {
|
||||
thisObj.creditItems.push({
|
||||
Type: value.processType,
|
||||
@@ -920,7 +1001,15 @@ var app = new Vue({
|
||||
node: value.node,
|
||||
parentNode: value.parentNode,
|
||||
approvalGroupId: value.approvalGroupId,
|
||||
approvalCondition: value.approvalCondition
|
||||
approvalCondition: value.approvalCondition,
|
||||
conditionConfig: value.conditionConfig,
|
||||
triggerCondition: value.triggerCondition
|
||||
});
|
||||
});
|
||||
// 需求①:加载后把 triggerCondition(JSON)解析为结构化对象供 UI 编辑
|
||||
['openItems', 'tradeItems', 'closeItems', 'clientItems'].forEach(function (arr) {
|
||||
thisObj[arr].forEach(function (item) {
|
||||
thisObj.parseTriggerCondition(item);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -982,6 +1071,376 @@ var app = new Vue({
|
||||
thisObj.tradeItems = newSortTradeItems.sort((a, b) => a.Index - b.Index);
|
||||
thisObj.initRuleType();
|
||||
},
|
||||
// ===== 需求①:节点触发条件 结构化编辑器(token 数组,支持每行独立且/或 + 括号)=====
|
||||
// _trigger = { tokens: [ {type:'condition',condition:{field,op,value}}, {type:'operator',connector:'and'|'or'}, {type:'lparen'|'rparen'} ] }
|
||||
// op 用字母标识符(gt/lt/gte/lte/eq/neq),规避 > < 在传输中被 HTML 编码导致加载不出来的问题
|
||||
// 把历史符号 op 归一化为字母标识符
|
||||
normalizeOp(op) {
|
||||
var map = { '>': 'gt', '<': 'lt', '>=': 'gte', '<=': 'lte', '==': 'eq', '!=': 'neq', '=': 'eq', '<>': 'neq' };
|
||||
if (!op) return 'gt';
|
||||
var lower = ('' + op).toLowerCase();
|
||||
if (['gt','lt','gte','lte','eq','neq'].indexOf(lower) >= 0) return lower;
|
||||
return map[op] || lower;
|
||||
},
|
||||
// 字段归一化:历史 notional → initialNotional;空默认 initialNotional
|
||||
normalizeField(field) {
|
||||
if (!field) return 'initialNotional';
|
||||
var f = ('' + field);
|
||||
// 历史兼容
|
||||
if (f.toLowerCase() == 'notional') return 'initialNotional';
|
||||
return f;
|
||||
},
|
||||
// 把 item.triggerCondition(JSON 字符串)解析为 item._trigger 结构化对象供 UI 编辑
|
||||
parseTriggerCondition(item) {
|
||||
if (!item) return;
|
||||
if (item._trigger) return; // 已解析则不重复处理
|
||||
var thisObj = this;
|
||||
var parsed = { tokens: [] };
|
||||
if (item.triggerCondition) {
|
||||
try {
|
||||
var obj = JSON.parse(item.triggerCondition);
|
||||
if (obj.tokens && obj.tokens.length) {
|
||||
// 新结构:直接复用 token,规范化字段
|
||||
parsed.tokens = obj.tokens.map(function (t) {
|
||||
if (t.type == 'condition') {
|
||||
var c = t.condition || {};
|
||||
return { type: 'condition', condition: { field: thisObj.normalizeField(c.field), op: thisObj.normalizeOp(c.op), value: c.value != null ? c.value : '' } };
|
||||
}
|
||||
if (t.type == 'operator') return { type: 'operator', connector: (t.connector || 'and').toLowerCase() };
|
||||
if (t.type == 'lparen') return { type: 'lparen' };
|
||||
if (t.type == 'rparen') return { type: 'rparen' };
|
||||
return null;
|
||||
}).filter(function (t) { return t != null; });
|
||||
}
|
||||
} catch (e) {
|
||||
// 非法 JSON,按空处理
|
||||
}
|
||||
}
|
||||
this.$set(item, '_trigger', parsed);
|
||||
},
|
||||
// 新增一条触发条件(自动在前方补连接符,首个不补)
|
||||
addTriggerCondition(item) {
|
||||
this.parseTriggerCondition(item);
|
||||
var toks = item._trigger.tokens;
|
||||
if (toks.length > 0) {
|
||||
var last = toks[toks.length - 1];
|
||||
// 若上一 token 是条件或右括号,则需要连接符
|
||||
if (last.type == 'condition' || last.type == 'rparen') {
|
||||
toks.push({ type: 'operator', connector: 'and' });
|
||||
}
|
||||
}
|
||||
toks.push({ type: 'condition', condition: { field: 'initialNotional', op: 'gt', value: '' } });
|
||||
},
|
||||
// 切换某连接符 and/or
|
||||
toggleTriggerConnector(item, idx) {
|
||||
var t = item._trigger.tokens[idx];
|
||||
if (t && t.type == 'operator') {
|
||||
t.connector = (t.connector == 'and') ? 'or' : 'and';
|
||||
}
|
||||
},
|
||||
// 插入左/右括号
|
||||
addTriggerParen(item, side) {
|
||||
this.parseTriggerCondition(item);
|
||||
var toks = item._trigger.tokens;
|
||||
var type = side == 'left' ? 'lparen' : 'rparen';
|
||||
if (type == 'lparen') {
|
||||
// 左括号前若接条件/右括号,需补连接符
|
||||
if (toks.length > 0) {
|
||||
var last = toks[toks.length - 1];
|
||||
if (last.type == 'condition' || last.type == 'rparen') {
|
||||
toks.push({ type: 'operator', connector: 'and' });
|
||||
}
|
||||
}
|
||||
toks.push({ type: 'lparen' });
|
||||
} else {
|
||||
toks.push({ type: 'rparen' });
|
||||
}
|
||||
},
|
||||
// 删除一个 token,并清理其旁孤立连接符
|
||||
removeTriggerToken(item, idx) {
|
||||
var toks = item._trigger.tokens;
|
||||
if (idx < 0 || idx >= toks.length) return;
|
||||
toks.splice(idx, 1);
|
||||
// 清理首部连接符 / 尾部连接符 / 连续连接符
|
||||
var i = 0;
|
||||
while (i < toks.length) {
|
||||
var t = toks[i];
|
||||
if (t.type == 'operator' && (i == 0 || i == toks.length - 1
|
||||
|| toks[i - 1].type == 'operator')) {
|
||||
toks.splice(i, 1);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
},
|
||||
// 保存前:把 item._trigger 序列化回 item.triggerCondition JSON
|
||||
stringifyTriggerCondition(item) {
|
||||
if (!item || !item._trigger) return;
|
||||
var toks = item._trigger.tokens || [];
|
||||
if (toks.length == 0) {
|
||||
item.triggerCondition = '';
|
||||
return;
|
||||
}
|
||||
// 过滤掉无效条件(字段/值缺失)
|
||||
var valid = toks.filter(function (t) {
|
||||
if (t.type == 'condition') {
|
||||
var c = t.condition || {};
|
||||
return c.field && c.op && c.value !== '';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// 移除首尾连接符
|
||||
while (valid.length > 0 && valid[0].type == 'operator') valid.shift();
|
||||
while (valid.length > 0 && valid[valid.length - 1].type == 'operator') valid.pop();
|
||||
item.triggerCondition = valid.length > 0 ? JSON.stringify({ tokens: valid }) : '';
|
||||
},
|
||||
// 批量把一组 items 的 _trigger 序列化(保存前调用)
|
||||
stringifyAllTrigger(items) {
|
||||
var thisObj = this;
|
||||
if (!items) return;
|
||||
items.forEach(function (item) {
|
||||
if (item.node == 0 || item.approvalCondition == 0) {
|
||||
thisObj.stringifyTriggerCondition(item);
|
||||
}
|
||||
});
|
||||
},
|
||||
// 校验触发条件 token 列表是否合法:括号平衡、无空括号、无首尾连接符等
|
||||
validateTriggerTokens(tokens) {
|
||||
if (!tokens || tokens.length == 0) return { valid: true, message: '' };
|
||||
var paren = 0;
|
||||
for (var i = 0; i < tokens.length; i++) {
|
||||
var t = tokens[i];
|
||||
if (t.type == 'lparen') paren++;
|
||||
if (t.type == 'rparen') paren--;
|
||||
if (paren < 0) return { valid: false, message: '触发条件中右括号过多,请检查括号匹配' };
|
||||
if (t.type == 'operator') {
|
||||
if (i == 0 || i == tokens.length - 1) return { valid: false, message: '触发条件首尾不能为连接符(且/或)' };
|
||||
var prev = tokens[i - 1];
|
||||
if (prev.type == 'operator' || prev.type == 'lparen') return { valid: false, message: '触发条件中连接符(且/或)位置不正确' };
|
||||
}
|
||||
if (t.type == 'condition') {
|
||||
var c = t.condition || {};
|
||||
if (!c.field || !c.op || c.value === '') return { valid: false, message: '触发条件中存在未填写完整的条件' };
|
||||
}
|
||||
}
|
||||
if (paren != 0) return { valid: false, message: '触发条件中括号不匹配,请检查' };
|
||||
return { valid: true, message: '' };
|
||||
},
|
||||
// 校验一组 items 的触发条件,返回第一个错误信息
|
||||
validateAllTriggers(items) {
|
||||
var thisObj = this;
|
||||
if (!items) return { valid: true, message: '' };
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
if ((item.node == 0 || item.approvalCondition == 0) && item._trigger && item._trigger.tokens) {
|
||||
var res = thisObj.validateTriggerTokens(item._trigger.tokens);
|
||||
if (!res.valid) return res;
|
||||
}
|
||||
}
|
||||
return { valid: true, message: '' };
|
||||
},
|
||||
// ===== 需求②:交易了结流程(closeItems)方法集,算法同交易流程 =====
|
||||
closeFilterBranch() {
|
||||
return this.closeItems.filter(x => x.approvalCondition != 0);
|
||||
},
|
||||
closeFilterChild(node) {
|
||||
return this.closeItems.filter(x => x.node == node && x.approvalCondition == 0);
|
||||
},
|
||||
initCloseItemIndex() {
|
||||
var thisObj = this;
|
||||
var newSortItems = [];
|
||||
var mainItems = thisObj.closeItems.filter(x => x.node == 0).sort((a, b) => a.Index - b.Index);
|
||||
var childItem = thisObj.closeItems.filter(x => x.node != 0);
|
||||
var childItemSort = childItem.sort((a, b) => a.Index - b.Index);
|
||||
var firstChildIndex = childItemSort.length > 0 ? childItemSort[0].Index : -1;
|
||||
var j = 1;
|
||||
for (var i = 0; i < mainItems.length; i++) {
|
||||
if (mainItems[i].Index >= firstChildIndex) {
|
||||
if (childItemSort.length > 0) {
|
||||
mainItems[i].Index = childItemSort[childItemSort.length - 1].Index + j;
|
||||
j++;
|
||||
} else {
|
||||
mainItems[i].Index = i + 1;
|
||||
}
|
||||
}
|
||||
newSortItems.push(mainItems[i]);
|
||||
}
|
||||
var firstChildItems = childItem.filter(x => x.node == 1);
|
||||
var secondChildItems = childItem.filter(x => x.node == 2);
|
||||
firstChildItems.forEach((x, index) => {
|
||||
if (x.approvalCondition == 0) {
|
||||
x.Index = firstChildIndex + index;
|
||||
x.parentNode = firstChildIndex + index - 1;
|
||||
}
|
||||
newSortItems.push(x);
|
||||
});
|
||||
secondChildItems.forEach((x, index) => {
|
||||
if (x.approvalCondition == 0) {
|
||||
x.Index = firstChildIndex + index;
|
||||
x.parentNode = firstChildIndex + index - 1;
|
||||
}
|
||||
newSortItems.push(x);
|
||||
});
|
||||
thisObj.closeItems = newSortItems.sort((a, b) => a.Index - b.Index);
|
||||
thisObj.initRuleType();
|
||||
},
|
||||
closeShowProcess(index, child, obj) {
|
||||
var thisObj = this;
|
||||
var closeBranch = thisObj.closeItems.find(x => x.approvalCondition != 0);
|
||||
if (!closeBranch) {
|
||||
let offsetTop = $(obj.target).offset().top;
|
||||
let offsetLeft = $(obj.target).offset().left;
|
||||
$("#addtooltip-warpper").css({ "left": offsetLeft - 180, "top": offsetTop - 110, "margin-left": "30px" });
|
||||
$("#addtooltip-warpper").hide();
|
||||
$("#addtooltip-warpper").show(150);
|
||||
this.tradeIndex = index;
|
||||
thisObj.node = index;
|
||||
} else {
|
||||
thisObj.closeAddProcess(index, child, 0);
|
||||
$("#addtooltip-warpper").hide(100);
|
||||
}
|
||||
},
|
||||
closeAddProcess(index, child, node) {
|
||||
var thisObj = this;
|
||||
thisObj.addCloseNode(index, child, node);
|
||||
$("#addtooltip-warpper").hide(100);
|
||||
},
|
||||
addCloseNode(index, child, node) {
|
||||
var thisObj = this;
|
||||
var item = {
|
||||
id: 0,
|
||||
Type: 'CloseProcess',
|
||||
Index: index + 1,
|
||||
SelectValue: 0,
|
||||
ApprovalRules: '',
|
||||
node: node,
|
||||
parentNode: child ? index : 0,
|
||||
approvalGroupId: 0,
|
||||
approvalCondition: 0,
|
||||
conditionConfig: '',
|
||||
triggerCondition: '',
|
||||
_trigger: { tokens: [] }
|
||||
};
|
||||
thisObj.closeItems.forEach(x => {
|
||||
if (x.Index >= index) {
|
||||
x.Index = x.Index + 1;
|
||||
}
|
||||
});
|
||||
thisObj.closeItems.splice(index, 0, item);
|
||||
thisObj.initCloseItemIndex();
|
||||
},
|
||||
closeDelProcess(closeItem) {
|
||||
var thisObj = this;
|
||||
thisObj.getCloseRuleType();
|
||||
if (closeItem.approvalCondition != 0) {//删除分支下,所有东西
|
||||
var childNodes = thisObj.closeItems.filter(x => x.node != 0);
|
||||
thisObj.closeItems = thisObj.closeItems.concat(childNodes).filter(function (v) {
|
||||
return thisObj.closeItems.indexOf(v) === -1 || childNodes.indexOf(v) === -1
|
||||
});
|
||||
thisObj.initCloseItemIndex();
|
||||
} else if (closeItem.node != 0) {//删除分支下某节点
|
||||
var index = thisObj.closeItems.indexOf(closeItem);
|
||||
thisObj.closeItems.splice(index, 1);
|
||||
var childNodes = thisObj.closeItems.filter(x => x.node == closeItem.node && x.Index >= closeItem.Index);
|
||||
childNodes.forEach(item => {
|
||||
item.parentNode = item.parentNode - 1;
|
||||
item.Index = item.Index - 1;
|
||||
});
|
||||
} else {//删除非分支下某节点
|
||||
var index = thisObj.closeItems.indexOf(closeItem);
|
||||
thisObj.closeItems.splice(index, 1);
|
||||
var nodes = thisObj.closeItems.filter(x => x.Index >= closeItem.Index);
|
||||
nodes.forEach(item => {
|
||||
item.Index = item.Index - 1;
|
||||
});
|
||||
thisObj.initCloseItemIndex();
|
||||
}
|
||||
},
|
||||
closeSelectChangeType(index, value) {
|
||||
var thisObj = this;
|
||||
for (var j = 0; j < thisObj.closeItems.length; j++) {
|
||||
if (j === index) continue;
|
||||
if (thisObj.closeItems[j].SelectValue === parseInt(value) && thisObj.closeItems[j].node == 0) {
|
||||
main.message("不得选择重复角色");
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
saveCloseProcess() {
|
||||
var thisObj = this;
|
||||
// 角色非空校验
|
||||
for (var i = 0; i < thisObj.closeItems.length; i++) {
|
||||
if (thisObj.closeItems[i].SelectValue === "" || thisObj.closeItems[i].SelectValue === 0) {
|
||||
if (thisObj.closeItems[i].approvalCondition == 0) {
|
||||
main.message('流程中断,请重新选择');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var temp1 = 0;
|
||||
var closeLength = thisObj.closeItems.length;
|
||||
for (var w1 = 0; w1 < closeLength - 1; w1++) {
|
||||
for (var n1 = 1; n1 < closeLength; n1++) {
|
||||
if (w1 !== n1) {
|
||||
if (parseInt(thisObj.closeItems[w1].SelectValue) === parseInt(thisObj.closeItems[n1].SelectValue) && parseInt(thisObj.closeItems[n1].SelectValue) != 0) {
|
||||
if (parseInt(thisObj.closeItems[w1].node) == parseInt(thisObj.closeItems[n1].node)) {
|
||||
temp1 = temp1 + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var approvalGroupItems = thisObj.closeItems.filter(x => x.approvalGroupId != 0);
|
||||
var approvalCloseLength = approvalGroupItems.length;
|
||||
var temp2 = 0;
|
||||
for (var w1 = 0; w1 < approvalCloseLength - 1; w1++) {
|
||||
for (var n1 = 1; n1 < approvalCloseLength; n1++) {
|
||||
if (w1 !== n1) {
|
||||
if (parseInt(approvalGroupItems[w1].approvalCondition) === parseInt(approvalGroupItems[n1].approvalCondition) && parseInt(approvalGroupItems[w1].approvalGroupId) === parseInt(approvalGroupItems[n1].approvalGroupId)) {
|
||||
temp2 = temp2 + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (temp1 > 0 || temp2 > 0) {
|
||||
main.message('流程包含重复项,请重新选择');
|
||||
return;
|
||||
}
|
||||
|
||||
thisObj.getCloseRuleType();
|
||||
thisObj.initCloseItemIndex();
|
||||
var firstChildCloseItem = thisObj.closeItems.filter(x => x.node == 1);
|
||||
var secondchildCloseItem = thisObj.closeItems.filter(x => x.node == 2);
|
||||
if (firstChildCloseItem.length == 1 && secondchildCloseItem.length == 1) {
|
||||
main.message('分支节点流程未设置完毕');
|
||||
return;
|
||||
}
|
||||
var triggerValid = thisObj.validateAllTriggers(thisObj.closeItems);
|
||||
if (!triggerValid.valid) {
|
||||
main.message(triggerValid.message);
|
||||
return;
|
||||
}
|
||||
thisObj.stringifyAllTrigger(thisObj.closeItems); // 需求①:序列化触发条件
|
||||
if (thisObj.closeItems != null && thisObj.closeItems.length > 0) {
|
||||
main.confirm("确认修改交易了结审批流程?", function () {
|
||||
main.post("/AccountOpeningProcess/AddProcess",
|
||||
{ type: "CloseProcess", data: thisObj.closeItems },
|
||||
{ async: false }).done(
|
||||
function (res) {
|
||||
thisObj.getProcess();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
main.confirm("删除审批流程后,交易了结审批就会直接审批通过,确认删除?", function () {
|
||||
main.post("/AccountOpeningProcess/AddProcess",
|
||||
{ type: "CloseProcess" },
|
||||
{ async: false }).done(
|
||||
function (res) {
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
initOpenItemIndex() {
|
||||
var thisObj = this;
|
||||
var newSortOpenItems = [];
|
||||
@@ -1058,41 +1517,58 @@ var app = new Vue({
|
||||
});
|
||||
thisObj.clientItems = newSortClientItems.sort((a, b) => a.Index - b.Index);
|
||||
},
|
||||
initRuleType() {
|
||||
initRuleTypeFor(items, prefix) {
|
||||
var thisObj = this;
|
||||
var tradeLength = thisObj.tradeItems.length;
|
||||
for (var i = 0; i < tradeLength; i++) {
|
||||
var ruleId = "#ruleType" + thisObj.tradeItems[i].Index + thisObj.tradeItems[i].node;
|
||||
var length = items.length;
|
||||
for (var i = 0; i < length; i++) {
|
||||
var ruleId = "#" + prefix + items[i].Index + items[i].node;
|
||||
var $selectize = $(ruleId).get(0);
|
||||
if (thisObj.tradeItems[i].ApprovalRules != null && thisObj.tradeItems[i].ApprovalRules.length > 0) {
|
||||
var ruleArr = thisObj.tradeItems[i].ApprovalRules.split(',');
|
||||
if (items[i].ApprovalRules != null && items[i].ApprovalRules.length > 0) {
|
||||
var ruleArr = items[i].ApprovalRules.split(',');
|
||||
$selectize.selectize.setValue(ruleArr);
|
||||
} else if ($selectize) {
|
||||
$selectize.selectize.setValue([]);
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
getRuleType() {
|
||||
initRuleType() {
|
||||
this.initRuleTypeFor(this.tradeItems, 'ruleType');
|
||||
this.initRuleTypeFor(this.closeItems, 'closeRuleType');
|
||||
},
|
||||
getRuleTypeFor(items, prefix) {
|
||||
var thisObj = this;
|
||||
var tradeLength = thisObj.tradeItems.length;
|
||||
for (var i = 0; i < tradeLength; i++) {
|
||||
var ruleId = "#ruleType" + thisObj.tradeItems[i].Index + thisObj.tradeItems[i].node;
|
||||
var length = items.length;
|
||||
for (var i = 0; i < length; i++) {
|
||||
var ruleId = "#" + prefix + items[i].Index + items[i].node;
|
||||
var $selectize = $(ruleId).get(0);
|
||||
if ($selectize) {
|
||||
var ruleArr = $selectize.selectize.items;
|
||||
thisObj.tradeItems[i].ApprovalRules = ruleArr.join(',');
|
||||
items[i].ApprovalRules = ruleArr.join(',');
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
getRuleType() {
|
||||
this.getRuleTypeFor(this.tradeItems, 'ruleType');
|
||||
},
|
||||
getCloseRuleType() {
|
||||
this.getRuleTypeFor(this.closeItems, 'closeRuleType');
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
this.getProcess();
|
||||
this.getApprovalGroup();
|
||||
this.loadRoleOptions();
|
||||
},
|
||||
watch: {
|
||||
tradeItems:
|
||||
{
|
||||
handler(val, oldVal) {
|
||||
this.$nextTick(function () {
|
||||
setRuleDiv();
|
||||
})
|
||||
},
|
||||
},
|
||||
closeItems:
|
||||
{
|
||||
handler(val, oldVal) {
|
||||
this.$nextTick(function () {
|
||||
|
||||
@@ -430,6 +430,141 @@ html {
|
||||
.glyphicon {
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
.trigger-condition-box {
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1);
|
||||
border: 1px solid #F5F5F5;
|
||||
margin: 8px 10px 10px 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.trigger-condition-title {
|
||||
background: rgb(255, 148, 62);
|
||||
color: #FFFFFF;
|
||||
font-size: 12px;
|
||||
padding: 5px 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trigger-condition-title .trigger-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.trigger-action-btn {
|
||||
display: inline-block;
|
||||
border-radius: 15px;
|
||||
color: rgb(50, 150, 250);
|
||||
border: none;
|
||||
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1);
|
||||
background: #FFFFFF;
|
||||
padding: 3px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.trigger-action-btn:hover {
|
||||
background: #f5f7fa;
|
||||
transform: scale(1.05);
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.trigger-token-list {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.trigger-token-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 26px;
|
||||
margin-bottom: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.trigger-token-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.trigger-connector {
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.trigger-paren {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
padding: 4px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.trigger-paren .trigger-remove,
|
||||
.trigger-condition-row .trigger-remove {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.trigger-paren:hover .trigger-remove,
|
||||
.trigger-condition-row:hover .trigger-remove {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.trigger-paren .trigger-remove {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.trigger-condition-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.trigger-condition-row .trigger-remove {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.trigger-condition-row select,
|
||||
.trigger-condition-row input {
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
padding: 0 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.trigger-condition-row select {
|
||||
width: auto;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.trigger-condition-row input {
|
||||
width: 80px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.trigger-remove {
|
||||
color: #ff4d4f !important;
|
||||
cursor: pointer;
|
||||
margin-left: 4px;
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.trigger-remove:hover {
|
||||
color: #ff7875 !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user