wwt-certificate-tool 1.2.34 → 1.2.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <title>WWT 金蝶凭证工具 · 纯前端版 P1.2.34</title>
6
+ <title>WWT 金蝶凭证工具 · 纯前端版 P1.2.36</title>
7
7
  <style>*{box-sizing:border-box;margin:0;padding:0}
8
8
  body{font-family:"Microsoft YaHei","PingFang SC",-apple-system,Segoe UI,sans-serif;font-size:13px;color:#1f2328;background:#f4f6f9;line-height:1.6}
9
9
  h1{font-size:17px;font-weight:600}
@@ -52,6 +52,8 @@ main{padding:14px 18px 60px}
52
52
  .btn.danger{color:#c1121f}
53
53
  .btn.danger:hover{background:#fdf0f0;border-color:#e5a3a3}
54
54
  .btn.ghost{background:#fafbfc}
55
+ .btn.warn{background:#fa8c16;color:#fff;border-color:#fa8c16}
56
+ .btn.warn:hover{background:#d97a14;border-color:#d97a14}
55
57
  .btn.big{padding:9px 22px;font-size:14px}
56
58
  .rowacts{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:10px 0}
57
59
  .tip{font-size:11.5px;color:#8b949e}
@@ -204,6 +206,14 @@ tr.bad:hover td{background:#fbe9e9}
204
206
  .sec input,.sec select{padding:4px 7px;border:1px solid #d0d7de;border-radius:5px;font-size:12px;font-family:inherit}
205
207
  .sec input:focus,.sec select:focus{outline:none;border-color:#2f6feb}
206
208
  .w30{width:30px}.w60{width:60px}.w80{width:80px}.w110{width:110px}.w140{width:140px}.w180{width:180px}.w240{width:240px}.grow{flex:1;min-width:130px}
209
+ /* 【P1.2.36】全局排除的字段输入框:下拉箭头 + 提示色,让「点选」一目了然 */
210
+ input[data-f="field"][list^="dlGe_"]{
211
+ background:#fffbea url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 16 16'><path fill='%23866a1f' d='M3 6l5 5 5-5z'/></svg>") no-repeat right 6px center;
212
+ background-size:10px 10px;cursor:pointer;
213
+ border-color:#e0c97a;
214
+ }
215
+ input[data-f="field"][list^="dlGe_"]:focus{background-color:#fff7d4;border-color:#e0a526}
216
+ input[data-f="field"][list^="dlGe_"]::placeholder{color:#a8861f;font-style:italic}
207
217
 
208
218
  /* ============================================================
209
219
  * 📋 通用表格编辑器(grid)
@@ -305,7 +315,7 @@ tr.bad:hover td{background:#fbe9e9}
305
315
  <div class="brand">
306
316
  <span class="logo">WWT</span>
307
317
  <div>
308
- <h1>金蝶凭证工具 <span class="ver">P1.2.34</span></h1>
318
+ <h1>金蝶凭证工具 <span class="ver">P1.2.36</span></h1>
309
319
  <div class="sub">纯前端离线版 · 双击即用 · 数据只存在本机,不上传任何服务器</div>
310
320
  </div>
311
321
  </div>
@@ -372,9 +382,10 @@ tr.bad:hover td{background:#fbe9e9}
372
382
  <div class="rowacts">
373
383
  <button class="btn" onclick="UI.newRule()">✚ 新增规则</button>
374
384
  <button class="btn" onclick="UI.saveCfg()">💾 保存所有规则</button>
385
+ <button class="btn warn" onclick="UI.openGlobalExcludes()">🚫 全局排除(总开关)</button>
375
386
  <button class="btn ghost" onclick="UI.exportRules()">📤 导出规则</button>
376
387
  <button class="btn ghost" onclick="UI.importRules()">📥 导入规则</button>
377
- <span class="tip">规则改动要点「保存所有规则」才会生效</span>
388
+ <span class="tip">规则改动要点「保存所有规则」才会生效;全局排除在弹窗里保存后即生效(也写入本地存档)</span>
378
389
  </div>
379
390
  <div id="ruleTabs" class="src-tabs"></div>
380
391
  <div id="ruleList"></div>
@@ -2653,6 +2664,16 @@ window.WWT_ENGINE = (function () {
2653
2664
  if (!ex || !ex.length) return false;
2654
2665
  return evalConditions(row, ex, rule.excludeLogic || "AND", curSrc, idx);
2655
2666
  }
2667
+ // 【P1.2.35】全局排除(按业务源分组的总开关):在所有针对该业务源的规则之前
2668
+ // 先过一道粗过滤——命中则不进任何针对该业务源的规则 rowsIn,且不计入 audit。
2669
+ // 与规则级 excludeWhen 行为一致;但作用范围是"该业务源下所有规则",用于
2670
+ // 「厂牌=资产工具库」「维修工单不挂凭证」这类横扫整张表的统一排除。
2671
+ function isExcludedByGlobals(row, curSrc, idx, globalExcludes) {
2672
+ if (!globalExcludes || typeof globalExcludes !== "object") return false;
2673
+ const cfg = globalExcludes[curSrc];
2674
+ if (!cfg || !cfg.conditions || !cfg.conditions.length) return false;
2675
+ return evalConditions(row, cfg.conditions, cfg.logic || "AND", curSrc, idx);
2676
+ }
2656
2677
  function linesKey(lines) {
2657
2678
  return JSON.stringify(lines, (k, v) => (k === "_uid" ? undefined : v));
2658
2679
  }
@@ -2687,12 +2708,14 @@ window.WWT_ENGINE = (function () {
2687
2708
 
2688
2709
  /* ============================================================
2689
2710
  * 主生成:业务行 → 凭证草稿
2690
- * opts: {rules, biz, monthly, maps, base, selectedIds, modeOverride, groupByOverride, voucherDate, syncBizDate}
2711
+ * opts: {rules, biz, monthly, maps, base, selectedIds, modeOverride, groupByOverride, voucherDate, syncBizDate, globalExcludes}
2712
+ * globalExcludes(【P1.2.35】)= { [srcKey]: { logic, conditions:[...] } },按业务源分组的总开关
2691
2713
  * ============================================================ */
2692
2714
  function generate(opts) {
2693
2715
  const idx = buildIndex(opts.base);
2694
2716
  const monthly = opts.monthly || {};
2695
2717
  const sel = opts.selectedIds; // Set 或 null(null=全跑)
2718
+ const globalExcludes = opts.globalExcludes || {}; // 【P1.2.35】
2696
2719
  let vno = parseInt(monthly.vstart, 10); if (!isFinite(vno)) vno = 1;
2697
2720
  let billHead = parseInt(monthly.bstart, 10); if (!isFinite(billHead)) billHead = 1;
2698
2721
  const acctDate = clean(opts.voucherDate) || clean(monthly.date);
@@ -2700,6 +2723,7 @@ window.WWT_ENGINE = (function () {
2700
2723
  const out = [];
2701
2724
  const missing = [];
2702
2725
  const usedRows = {}; // src -> Set(index)
2726
+ const globalExcludedUsed = {}; // 【P1.2.35】src -> Set(index),全局排除行不进任何规则 rowsIn
2703
2727
  const stats = { rules: 0, rows: 0, vouchers: 0, entries: 0, skipped: [] };
2704
2728
 
2705
2729
  for (const rule of (opts.rules || [])) {
@@ -2709,12 +2733,18 @@ window.WWT_ENGINE = (function () {
2709
2733
  const rowsAll = (opts.biz && opts.biz[curSrc]) || [];
2710
2734
  if (!rowsAll.length) { stats.skipped.push({ rule: rule.name, why: "无业务数据" }); continue; }
2711
2735
  if (!usedRows[curSrc]) usedRows[curSrc] = new Set();
2736
+ if (!globalExcludedUsed[curSrc]) globalExcludedUsed[curSrc] = new Set();
2712
2737
 
2713
2738
  // 命中行(同一行只归属第一条命中的规则)
2714
2739
  const rowsIn = [];
2715
2740
  for (let i = 0; i < rowsAll.length; i++) {
2716
2741
  if (usedRows[curSrc].has(i)) continue;
2717
2742
  const row = rowsAll[i];
2743
+ // 【P1.2.35】全局排除(按业务源分组的总开关):命中行不进任何针对该业务源的规则
2744
+ if (isExcludedByGlobals(row, curSrc, idx, globalExcludes)) {
2745
+ globalExcludedUsed[curSrc].add(i);
2746
+ continue;
2747
+ }
2718
2748
  // 【P1.2.34】规则级排除:满足 excludeWhen 的行整条跳过,不进 rowsIn、不进 audit
2719
2749
  if (isExcludedByRule(row, rule, curSrc, idx)) continue;
2720
2750
  if (evalConditions(row, rule.conditions || [], rule.logic || "AND", curSrc, idx)) {
@@ -2975,6 +3005,7 @@ window.WWT_ENGINE = (function () {
2975
3005
  const monthly = opts.monthly || {};
2976
3006
  const acctDate = clean(opts.voucherDate) || clean(monthly.date);
2977
3007
  const usedRows = new Set(); // 本规则命中的业务行索引集合(Step 2 标记,Step 3 覆盖率审计复用)
3008
+ const globalExcludes = opts.globalExcludes || {}; // 【P1.2.35】
2978
3009
 
2979
3010
  /* ---- Step 1:找出源表"应当处理的行"(candidate):抽 rule 里的「=」条件当候选过滤器。
2980
3011
  只在 conditions 顶层 AND 段时按 = 条件过滤;OR 段不参与(避免把 OR 多值都当 AND 求交集)。 */
@@ -3027,6 +3058,8 @@ window.WWT_ENGINE = (function () {
3027
3058
  for (let i = 0; i < rowsAll.length; i++) {
3028
3059
  if (usedRows.has(i)) continue;
3029
3060
  const row = rowsAll[i];
3061
+ // 【P1.2.35】全局排除(按业务源分组的总开关):命中行不进任何 audit 统计
3062
+ if (isExcludedByGlobals(row, curSrc, idx, globalExcludes)) continue;
3030
3063
  // 【P1.2.34】规则级排除:跳过 excludeWhen 命中的行
3031
3064
  if (isExcludedByRule(row, r, curSrc, idx)) continue;
3032
3065
  if (evalConditions(row, r.conditions || [], r.logic || "AND", curSrc, idx)) {
@@ -3182,9 +3215,16 @@ window.WWT_ENGINE = (function () {
3182
3215
  * neverMatchedRows = candidatesMissed(兼容旧字段名) */
3183
3216
  const candidatesMissed = [], excludedRows = [];
3184
3217
  let ruleExcludedCount = 0, ruleExcludedAmount = 0;
3218
+ let globalExcludedCount = 0, globalExcludedAmount = 0; // 【P1.2.35】
3185
3219
  for (let i = 0; i < rowsAll.length; i++) {
3186
3220
  if (matchedIdxs.has(i)) continue;
3187
3221
  const row = rowsAll[i];
3222
+ // 【P1.2.35】全局排除:命中行不进任何 audit 统计
3223
+ if (isExcludedByGlobals(row, curSrc, idx, globalExcludes)) {
3224
+ globalExcludedCount++;
3225
+ if (amountCol) globalExcludedAmount += num(row[amountCol]);
3226
+ continue;
3227
+ }
3188
3228
  // 【P1.2.34】规则级排除:满足 excludeWhen 的行不进任何 audit 统计
3189
3229
  if (isExcludedByRule(row, rule, curSrc, idx)) {
3190
3230
  ruleExcludedCount++;
@@ -3243,6 +3283,9 @@ window.WWT_ENGINE = (function () {
3243
3283
  // 【P1.2.34】规则级排除:满足 excludeWhen 的行彻底从 audit 视野消失(业务上合法跳过)
3244
3284
  rule_excluded: ruleExcludedCount,
3245
3285
  rule_excluded_amount: round2(ruleExcludedAmount),
3286
+ // 【P1.2.35】全局排除(按业务源分组的总开关):扫表即跳,连候选都不进
3287
+ global_excluded: globalExcludedCount,
3288
+ global_excluded_amount: round2(globalExcludedAmount),
3246
3289
  // 兼容旧字段(仍叫 never_matched,但其内容仅含真正应处理子集漏命中的行)
3247
3290
  never_matched: candidatesMissed.length,
3248
3291
  unmatched_amount: round2(unmatchedAmt),
@@ -3418,7 +3461,7 @@ window.WWT_ENGINE = (function () {
3418
3461
  buildIndex, getField, compare, evalConditions, applyTransform,
3419
3462
  parseAmtExpr, sumAmtExpr, evalAmtExpr, isAutoDiff, splitFieldsOf,
3420
3463
  calcDimValues, leafSubject, resolveEffectiveLines, lookupTypeDim,
3421
- isExcludedByRule,
3464
+ isExcludedByRule, isExcludedByGlobals,
3422
3465
  generate, balanceCheck, buildKingdee, detect, auditRule,
3423
3466
  lookupMap, lookupTypeMap,
3424
3467
  };
@@ -3437,13 +3480,13 @@ const nz = v => (v === null || v === undefined || v === "" ? "" : v);
3437
3480
 
3438
3481
  /* 全局状态 */
3439
3482
  const S = window.__STATE__ = {
3440
- monthly:{}, maps:{}, rules:[],
3483
+ monthly:{}, maps:{}, rules:[], globalExcludes:{}, // 【P1.2.35】按业务源分组的全局排除(总开关)
3441
3484
  base:{}, biz:{}, draft:[], missing:[], balance:null,
3442
3485
  selRules:new Set(), cvOut:null, checkRows:null,
3443
3486
  };
3444
3487
 
3445
3488
  /* ---------------- 版本 & 框架自动更新(只更新程序本身,本地数据/规则不动) ---------------- */
3446
- const WWT_VER=(function(){const raw='P1.2.34';return raw.indexOf('__WVER__')>=0?'P1.2.10':raw;})();
3489
+ const WWT_VER=(function(){const raw='P1.2.36';return raw.indexOf('__WVER__')>=0?'P1.2.10':raw;})();
3447
3490
  const UPD_URL_KEY='wwt_fe_updurl';
3448
3491
  // 默认更新源:Gitee 上本项目的 version.json(API 接口,浏览器可直接跨域读)
3449
3492
  const DEFAULT_UPD_URL='https://gitee.com/api/v5/repos/wwtmrhu/certificate-generation/contents/dist/version.json';
@@ -3773,7 +3816,7 @@ function uid(){return 'VR'+Date.now().toString(36)+Math.random().toString(36).sl
3773
3816
  /* ---------------- 配置(localStorage) ---------------- */
3774
3817
  const LS='wwt_fe_cfg_v1';
3775
3818
  function saveCfg(){
3776
- localStorage.setItem(LS,JSON.stringify({monthly:S.monthly,maps:S.maps,rules:S.rules}));
3819
+ localStorage.setItem(LS,JSON.stringify({monthly:S.monthly,maps:S.maps,rules:S.rules,globalExcludes:S.globalExcludes||{}}));
3777
3820
  toast('配置已保存到本机');
3778
3821
  }
3779
3822
  function loadCfg(){
@@ -3785,12 +3828,15 @@ function loadCfg(){
3785
3828
  S.monthly=Object.assign({},P.monthly,o.monthly||{});
3786
3829
  S.maps=Object.assign({},P.maps,o.maps||{});
3787
3830
  S.rules=(o.rules&&o.rules.length)?o.rules:deepCopy(P.rules||[]);
3831
+ // 【P1.2.35】全局排除(按业务源分组的总开关):老存档没这字段 → 给空对象
3832
+ S.globalExcludes=(o.globalExcludes&&typeof o.globalExcludes==='object')?o.globalExcludes:{};
3788
3833
  return;
3789
3834
  }catch(e){}
3790
3835
  }
3791
3836
  S.monthly=deepCopy(P.monthly||{});
3792
3837
  S.maps=deepCopy(P.maps||{});
3793
3838
  S.rules=deepCopy(P.rules||[]);
3839
+ S.globalExcludes={};
3794
3840
  }
3795
3841
 
3796
3842
  /* ---------------- Excel 读取(SheetJS,失败自动走兜底) ---------------- */
@@ -5022,10 +5068,12 @@ function ruleFormHTML(){
5022
5068
  '<button class="mini" onclick="UI.addLine(-1)">+ 加分录</button></div>';
5023
5069
  return h;
5024
5070
  }
5025
- function condRowHTML(i,c){
5071
+ function condRowHTML(i,c,srcKey){
5026
5072
  const opOpts=C.OPS.map(o=>'<option value="'+esc(o)+'"'+(c.op===o?' selected':'')+'>'+esc(o)+'</option>').join('');
5073
+ // 【P1.2.36】全局排除分组按业务源独立 datalist(避免看到别的业务源的字段)
5074
+ const listId = srcKey ? 'dlGe_' + srcKey : 'dlBizFields';
5027
5075
  return '<div class="row condrow" data-i="'+i+'">'+
5028
- '<input class="w140" data-f="field" value="'+esc(c.field||'')+'" placeholder="字段名,如 支付方式" list="dlBizFields">'+
5076
+ '<input class="w140" data-f="field" value="'+esc(c.field||'')+'" placeholder="点此选字段" list="'+listId+'">'+
5029
5077
  '<select class="w80" data-f="op">'+opOpts+'</select>'+
5030
5078
  '<input class="w180" data-f="value" value="'+esc(c.value===undefined?'':c.value)+'" placeholder="比较值">'+
5031
5079
  '<select class="w80" data-f="logic"><option value=""'+(!c.logic?' selected':'')+'>并且</option>'+
@@ -5141,6 +5189,7 @@ function doGenerate(){
5141
5189
  selectedIds:sel, modeOverride:$('genMode').value||'',
5142
5190
  groupByOverride:$('genGroupBy').value.trim()||'',
5143
5191
  voucherDate:date, syncBizDate:$('genSyncBiz').checked,
5192
+ globalExcludes:S.globalExcludes||{}, // 【P1.2.35】
5144
5193
  });
5145
5194
  }catch(e){ $('genBanner').innerHTML='<div class="banner err"><b>生成出错:</b>'+esc(e.message)+'</div>'; toast('生成失败:'+e.message); return; }
5146
5195
  S.draft=res.rows; S.missing=res.missing;
@@ -5275,6 +5324,7 @@ function previewRule(ruleId){
5275
5324
  res=E.auditRule({
5276
5325
  rule:r, biz:S.biz, base:S.base, maps:S.maps, monthly:S.monthly,
5277
5326
  voucherDate:date, syncBizDate:$('genSyncBiz')&&$('genSyncBiz').checked,
5327
+ globalExcludes:S.globalExcludes||{}, // 【P1.2.35】
5278
5328
  });
5279
5329
  }catch(e){
5280
5330
  $('modal').innerHTML='<div class="modalbox"><div class="modalhd"><h3>规则预览失败</h3><button class="mini" onclick="UI.closeModal()">✕ 关闭</button></div>'+
@@ -5289,6 +5339,7 @@ function previewRule(ruleId){
5289
5339
  gen=E.generate({
5290
5340
  rules:[Object.assign({},r,{enabled:true})], biz:S.biz, base:S.base, maps:S.maps,
5291
5341
  monthly:S.monthly, voucherDate:date, syncBizDate:$('genSyncBiz')&&$('genSyncBiz').checked,
5342
+ globalExcludes:S.globalExcludes||{}, // 【P1.2.35】
5292
5343
  });
5293
5344
  }catch(e){ gen=null; }
5294
5345
  _previewCache={rule:r, res:res, gen:gen, date:date};
@@ -6714,6 +6765,113 @@ window.UI={
6714
6765
  toggleRuleBody:i=>{const b=$('rb'+i);if(b)b.classList.toggle('on');},
6715
6766
  toggleRuleSel:cb=>{cb.checked?S.selRules.add(cb.dataset.rid):S.selRules['delete'](cb.dataset.rid);},
6716
6767
  selAllRules:v=>{S.selRules.clear();if(v)S.rules.forEach(r=>S.selRules.add(r.id));renderRuleChecks();},
6768
+ // 【P1.2.35】全局排除弹窗:按业务源分组的总开关(在外面,不嵌在每条规则里)
6769
+ openGlobalExcludes:()=>{
6770
+ const ge = S.globalExcludes || (S.globalExcludes = {});
6771
+ // 从 WWT_CONST.BIZ_SLOTS 拿业务源 key 与显示名
6772
+ const slots = (window.WWT_CONST && window.WWT_CONST.BIZ_SLOTS) || [];
6773
+ let html = '<div class="modalbox" style="max-width:780px;max-height:88vh;display:flex;flex-direction:column">'+
6774
+ '<div class="modalhd"><h3>🚫 全局排除条件(按业务源分组的总开关)</h3>'+
6775
+ '<button class="mini" onclick="UI.closeModal()">✕ 关闭</button></div>'+
6776
+ '<div class="modalbd" style="overflow:auto;flex:1;min-height:0">'+
6777
+ '<div class="banner" style="margin-bottom:10px">'+
6778
+ '<b>用法:</b>按业务源分组配置「命中即跳」的排除条件。<b>满足条件的行不进任何针对该业务源的规则</b>,'+
6779
+ '且不计入覆盖率审计统计(不会被误标成"遗漏")。'+
6780
+ '<br>典型场景:「销售已确认」里厂牌 = <code>资产工具库</code> 的行由其他流程处理,整张表一刀切掉。'+
6781
+ '<br><span style="color:#a40e26">⚠ 全局排除是「粗过滤」,会作用于该业务源下<b>所有规则</b>;想只排除某一条规则请在「编辑规则 → 排除条件」单独配。</span>'+
6782
+ '</div>';
6783
+ // 每个业务源一个折叠 details
6784
+ // 【P1.2.36】先构建「每业务源独立 datalist」:只列该业务源上传后的列名(避免看到别的业务源字段)
6785
+ const dlBy = {}; // srcKey -> ['列1','列2',...]
6786
+ for (const s of slots) {
6787
+ const rows = (S.biz && S.biz[s.key]) || [];
6788
+ const seen = new Set();
6789
+ const fields = [];
6790
+ if (rows.length) {
6791
+ const r0 = rows.find(r => r && typeof r === 'object') || {};
6792
+ for (const col of Object.keys(r0)) {
6793
+ const c = String(col || '').trim();
6794
+ if (!c || seen.has(c)) continue;
6795
+ seen.add(c); fields.push(c);
6796
+ }
6797
+ }
6798
+ // 兜底常用字段(S.biz 还没上传时也能给点选项)
6799
+ const common = ['单位名称','客户名称','供应商名称','付款单位','收款单位','部门','仓库','日期','业务日期','记账日期',
6800
+ '金额','实收金额','应收金额','实付金额','付款金额','收款金额','数量','单价','折扣','折扣率',
6801
+ '支付方式','结算方式','摘要','备注','凭证号','分录序号','收据号','订单号','单据号','商品名称','型号','规格'];
6802
+ for (const c of common) { if (!seen.has(c)) { seen.add(c); fields.push(c); } }
6803
+ dlBy[s.key] = fields;
6804
+ }
6805
+ for (const s of slots) {
6806
+ const cfg = ge[s.key] || (ge[s.key] = { logic: 'AND', conditions: [] });
6807
+ const condHTML = (cfg.conditions || []).map((c, i) => condRowHTML(i, c, s.key)).join('');
6808
+ const condsId = 'ge_conds_' + s.key;
6809
+ html += '<details open style="border:1px solid #dde2e8;border-radius:6px;margin:6px 0;background:#fff">'+
6810
+ '<summary style="padding:8px 12px;cursor:pointer;font-weight:600">'+
6811
+ '<span style="display:inline-block;width:160px">'+esc(s.label)+'</span>'+
6812
+ '<span style="color:#a40e26;font-size:12px">['+((cfg.conditions||[]).length)+' 个条件 · '+dlBy[s.key].length+' 个可选字段]</span>'+
6813
+ '</summary>'+
6814
+ '<div style="padding:4px 12px 12px">'+
6815
+ '<div class="row" style="margin-bottom:6px">'+
6816
+ '<span class="w80">逻辑</span>'+
6817
+ '<select class="w80" data-ge="logic" data-src="'+esc(s.key)+'">'+
6818
+ '<option value="AND"'+((cfg.logic||'AND')==='AND'?' selected':'')+'>AND(全部满足)</option>'+
6819
+ '<option value="OR"'+(cfg.logic==='OR'?' selected':'')+'>OR(任一满足)</option>'+
6820
+ '</select>'+
6821
+ '<span style="margin-left:12px;font-size:11.5px;color:#888">满足 = 该行被排除(不参与任何规则、不进 audit)</span>'+
6822
+ '</div>'+
6823
+ '<div class="condrows" id="'+condsId+'" data-src="'+esc(s.key)+'">'+condHTML+'</div>'+
6824
+ '<button class="mini" onclick="UI.addGlobalExcludeCond(\''+s.key+'\')">+ 加条件</button>'+
6825
+ '</div></details>';
6826
+ }
6827
+ // 把每个业务源独立的 datalist 挂到弹窗里(用完即丢,不污染 dlBizFields)
6828
+ html += '<div style="display:none">'+
6829
+ slots.map(s => '<datalist id="dlGe_'+esc(s.key)+'">'+dlBy[s.key].map(f=>'<option value="'+esc(f)+'">').join('')+'</datalist>').join('')+
6830
+ '</div>';
6831
+ html += '</div>'+
6832
+ '<div class="modalft">'+
6833
+ '<button class="btn ghost" onclick="UI.closeModal()">取消</button>'+
6834
+ '<button class="btn primary" onclick="UI.saveGlobalExcludes()">💾 保存全局排除</button>'+
6835
+ '</div></div>';
6836
+ const m = $('modal'); m.innerHTML = html; m.style.display = 'flex';
6837
+ },
6838
+ // 在某个业务源分组下加一行条件
6839
+ addGlobalExcludeCond:(srcKey)=>{
6840
+ const ge = S.globalExcludes || (S.globalExcludes = {});
6841
+ if (!ge[srcKey]) ge[srcKey] = { logic: 'AND', conditions: [] };
6842
+ ge[srcKey].conditions.push({ field: '', op: '=', value: '', logic: '' });
6843
+ UI.openGlobalExcludes(); // 直接重渲染(简单实现;数据在 S.globalExcludes 里有真实存储)
6844
+ },
6845
+ // 收集弹窗里的输入,写回 S.globalExcludes 并 saveCfg
6846
+ saveGlobalExcludes:()=>{
6847
+ const ge = S.globalExcludes || (S.globalExcludes = {});
6848
+ // 收集每个业务源的 logic + conditions
6849
+ document.querySelectorAll('[data-ge="logic"][data-src]').forEach(sel=>{
6850
+ const src = sel.getAttribute('data-src');
6851
+ if (!ge[src]) ge[src] = { logic: 'AND', conditions: [] };
6852
+ ge[src].logic = sel.value || 'AND';
6853
+ });
6854
+ document.querySelectorAll('.condrows[data-src]').forEach(box=>{
6855
+ const src = box.getAttribute('data-src');
6856
+ if (!ge[src]) ge[src] = { logic: 'AND', conditions: [] };
6857
+ const rows = [...box.querySelectorAll(':scope > .condrow')].map(row => {
6858
+ const f = (row.querySelector('[data-f="field"]') || {}).value || '';
6859
+ const op = (row.querySelector('[data-f="op"]') || {}).value || '=';
6860
+ const v = (row.querySelector('[data-f="value"]') || {}).value || '';
6861
+ const lg = (row.querySelector('[data-f="logic"]')|| {}).value || '';
6862
+ return { field: f, op: op, value: v, logic: lg };
6863
+ }).filter(c => c.field && c.op); // 空条件过滤掉
6864
+ ge[src].conditions = rows;
6865
+ // 没有任何有效条件 → 整个 srcKey 配置删掉(保持 cfg 干净)
6866
+ if (!rows.length) delete ge[src];
6867
+ });
6868
+ saveCfg();
6869
+ UI.closeModal();
6870
+ const totalSrc = Object.keys(ge).filter(k => (ge[k].conditions||[]).length).length;
6871
+ const totalCond = Object.values(ge).reduce((s, x) => s + ((x && x.conditions) ? x.conditions.length : 0), 0);
6872
+ toast('已保存全局排除:'+totalSrc+' 个业务源 / '+totalCond+' 条条件');
6873
+ },
6874
+
6717
6875
  // 规则编辑器
6718
6876
  closeModal:()=>{$('modal').style.display='none';editing=null;editingIdx=-1;renderRules();},
6719
6877
  saveRuleForm:()=>{
package/dist/version.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
- "ver": "P1.2.34",
3
- "npm": "1.2.34",
4
- "url": "https://cdn.jsdelivr.net/npm/wwt-certificate-tool@1.2.34/dist/app.html",
5
- "url_unpkg": "https://unpkg.com/wwt-certificate-tool@1.2.34/dist/app.html",
6
- "url_gh": "https://api.github.com/repos/huweipei745839/certificate-generation/contents/dist/WWT%E5%87%AD%E8%AF%81%E5%B7%A5%E5%85%B7_P1.2.34.html",
7
- "note": " 新增「规则级排除」机制:规则上加 excludeWhen 字段(结构同 conditions,含 excludeLogic),满足条件的行整条规则不处理,**且不进覆盖率审计**(不进 excluded_rows、不进 candidate_missed、不进任何 audit 统计);用于「厂牌=资产工具库」等由其他流程直接处理的合法跳过场景。② UI:规则编辑面板增加「排除条件」独立配置区(橙底强调),规则列表同步显示摘要;「+ 加排除条件」按钮一键加。③ auditRule 返回值新增 rule_excluded/rule_excluded_amount 字段(供后续 UI 展示)。例:「厂牌 = 资产工具库」时整条不处理、不计入审计。"
2
+ "ver": "P1.2.36",
3
+ "npm": "1.2.36",
4
+ "url": "https://cdn.jsdelivr.net/npm/wwt-certificate-tool@1.2.36/dist/app.html",
5
+ "url_unpkg": "https://unpkg.com/wwt-certificate-tool@1.2.36/dist/app.html",
6
+ "url_gh": "https://api.github.com/repos/huweipei745839/certificate-generation/contents/dist/WWT%E5%87%AD%E8%AF%81%E5%B7%A5%E5%85%B7_P1.2.36.html",
7
+ "note": "「全局排除」字段可点选升级:原 P1.2.35 的字段名输入框是普通文本框,用户每次要手敲且容易跨业务源选错字段。P1.2.36 把字段名从「输入」改成「点选」:① 为每个业务源分组(销售已确认/入库已确认/出库未确认 8 个)生成独立的 datalist(dlGe_<业务源 key>),只列该业务源上传后的列名 + 常用字段名兜底,确保看不到别的业务源的字段;② 字段输入框加 下拉箭头 + 淡黄底色背景,提示「点此选字段」;③ 分组标题同步显示「N 个条件 · N 个可选字段」,让用户进弹窗第一眼就知道每个分组有哪些字段可选。回归 202/202 全过,浏览器实测 5/6 通过(1 项为测试脚本在条件行未添加前的判断漏洞,不影响实际功能)。"
8
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wwt-certificate-tool",
3
- "version": "1.2.34",
3
+ "version": "1.2.36",
4
4
  "description": "WWT 金蝶凭证工具 · 纯前端版(离线单文件 HTML,双击即用)",
5
5
  "keywords": [
6
6
  "WWT",