wwt-certificate-tool 1.2.31 → 1.2.33

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.31</title>
6
+ <title>WWT 金蝶凭证工具 · 纯前端版 P1.2.33</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}
@@ -73,7 +73,8 @@ main{padding:14px 18px 60px}
73
73
 
74
74
  /* 卡片/栅格 */
75
75
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px;margin-top:6px}
76
- .card{background:#fff;border:1px solid #dde2e8;border-radius:7px;padding:9px 11px;display:flex;justify-content:space-between;align-items:center}
76
+ .card{background:#fff;border:1px solid #dde2e8;border-radius:7px;padding:9px 11px;display:flex;justify-content:space-between;align-items:center;cursor:pointer;transition:all .15s ease}
77
+ .card:hover{border-color:#1677ff;box-shadow:0 2px 6px rgba(22,119,255,.15);transform:translateY(-1px)}
77
78
  .card.ok{border-left:3px solid #1a7f37}
78
79
  .card.empty{border-left:3px solid #d0d7de}
79
80
  .cname{font-size:12.5px;font-weight:500}
@@ -253,21 +254,22 @@ tr.bad:hover td{background:#fbe9e9}
253
254
  * 📋 数据浏览/编辑抽屉(P1.2.27 · 对齐后端 v2.17)
254
255
  * ============================================================ */
255
256
  .dv-mask{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:300;display:flex;align-items:stretch;justify-content:flex-start}
256
- .dv-panel{position:absolute;left:0;top:0;bottom:0;width:min(96vw,1100px);background:#fff;box-shadow:4px 0 16px rgba(0,0,0,.18);display:flex;flex-direction:column}
257
- .dv-header{padding:12px 18px;border-bottom:1px solid #eee;background:#fafafa;display:flex;align-items:center;gap:10px;font-size:14px}
257
+ .dv-panel{position:absolute;left:0;top:0;bottom:0;right:0;background:#fff;box-shadow:4px 0 16px rgba(0,0,0,.18);display:flex;flex-direction:column;width:auto}
258
+ .dv-header{padding:10px 18px;border-bottom:1px solid #eee;background:#fafafa;display:flex;align-items:center;gap:10px;font-size:14px;flex-shrink:0}
258
259
  .dv-hint{font-size:12px;color:#888}
259
260
  .dv-body{display:flex;flex:1;min-height:0}
260
- .dv-left{width:240px;border-right:1px solid #eee;overflow:auto;padding:6px;background:#fcfcfc}
261
+ .dv-left{width:260px;flex-shrink:0;border-right:1px solid #eee;overflow:auto;padding:6px;background:#fcfcfc}
261
262
  .dv-grp{padding:4px 10px;color:#888;font-size:11px;border-bottom:1px solid #eee;margin-top:4px}
262
- .dv-key-item{padding:6px 10px;cursor:pointer;border-radius:4px;font-size:12px;color:#333;display:flex;justify-content:space-between;align-items:center}
263
+ .dv-key-item{padding:7px 10px;cursor:pointer;border-radius:4px;font-size:12px;color:#333;display:flex;justify-content:space-between;align-items:center;gap:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
263
264
  .dv-key-item:hover{background:#f0f4f8}
264
265
  .dv-key-item.sel{background:#e6f4ff;color:#1677ff;font-weight:bold}
265
266
  .dv-key-item.empty{color:#aaa}
266
- .dv-key-meta{font-size:10px;color:#bbb;font-weight:normal}
267
+ .dv-key-meta{font-size:10px;color:#bbb;font-weight:normal;flex-shrink:0}
267
268
  .dv-key-item.sel .dv-key-meta{color:#1677ff}
269
+ .dv-key-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
268
270
  .dv-right{flex:1;display:flex;flex-direction:column;min-width:0}
269
- .dv-toolbar{padding:8px 14px;background:#fff;border-bottom:1px solid #f0f0f0;display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:13px}
270
- .dv-filter{flex:1;min-width:160px;padding:4px 8px;border:1px solid #ccc;border-radius:4px;font-size:12px}
271
+ .dv-toolbar{padding:8px 14px;background:#fff;border-bottom:1px solid #f0f0f0;display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:13px;flex-shrink:0}
272
+ .dv-filter{flex:1;min-width:140px;padding:4px 8px;border:1px solid #ccc;border-radius:4px;font-size:12px}
271
273
  .dv-btn{padding:4px 10px;border:0;border-radius:4px;font-size:12px;cursor:pointer}
272
274
  .dv-btn.ghost{background:#f4f6f8;color:#333}
273
275
  .dv-btn.ghost:hover{background:#e8eaed}
@@ -275,13 +277,23 @@ tr.bad:hover td{background:#fbe9e9}
275
277
  .dv-btn.act:hover{background:#0e6ad6}
276
278
  .dv-btn.warn{background:#fa8c16;color:#fff}
277
279
  .dv-btn.warn:hover{background:#d97a14}
278
- .dv-grid{flex:1;overflow:auto;padding:6px 14px}
279
- .dv-table{width:100%;border-collapse:collapse;font-size:12px;background:#fff}
280
- .dv-table th{background:#fafafa;font-weight:600;padding:6px 8px;border-bottom:1px solid #e3e6ea;border-right:1px solid #f0f0f0;text-align:left;position:sticky;top:0;z-index:2;white-space:nowrap}
281
- .dv-table td{padding:0;border-bottom:1px solid #f0f0f0;border-right:1px solid #f7f7f7}
280
+ .dv-grid{flex:1;overflow:auto;padding:6px 14px;min-width:0}
281
+ .dv-grid-inner{display:inline-block;min-width:100%}
282
+ .dv-table{border-collapse:collapse;font-size:12px;background:#fff;table-layout:auto;width:max-content;min-width:100%}
283
+ .dv-table th{background:#fafafa;font-weight:600;padding:6px 10px;border-bottom:1px solid #e3e6ea;border-right:1px solid #f0f0f0;text-align:left;position:sticky;top:0;z-index:2;white-space:nowrap}
284
+ .dv-table td{padding:0;border-bottom:1px solid #f0f0f0;border-right:1px solid #f7f7f7;white-space:nowrap;max-width:360px;overflow:hidden;text-overflow:ellipsis}
282
285
  .dv-table input{width:100%;padding:4px 6px;border:0;background:transparent;font-size:12px;box-sizing:border-box}
283
286
  .dv-table input:focus{outline:2px solid #1677ff;background:#fff}
284
287
  .dv-table tr.sel{background:#f0f7ff}
288
+
289
+ /* 📤 上传 Excel 弹窗(P1.2.32) */
290
+ .uploadpanel{max-width:560px}
291
+ .up-row{display:flex;align-items:center;gap:12px;margin-bottom:14px}
292
+ .up-row label{width:80px;color:#555;font-size:13px;flex-shrink:0}
293
+ .up-row select,.up-row input[type=file]{flex:1;padding:6px 10px;border:1px solid #ccc;border-radius:4px;font-size:13px;min-width:0}
294
+ .up-row.up-actions{justify-content:flex-end;margin-bottom:0;gap:8px}
295
+ .up-tip{font-size:12px;color:#666;background:#f6f8fa;padding:10px 12px;border-radius:4px;margin-bottom:14px;line-height:1.7}
296
+ .up-tip b{color:#1677ff}
285
297
  .dv-cb{width:30px;text-align:center}
286
298
  .dv-cb input{cursor:pointer}
287
299
  .dv-num{width:40px;text-align:right;color:#888}
@@ -293,7 +305,7 @@ tr.bad:hover td{background:#fbe9e9}
293
305
  <div class="brand">
294
306
  <span class="logo">WWT</span>
295
307
  <div>
296
- <h1>金蝶凭证工具 <span class="ver">P1.2.31</span></h1>
308
+ <h1>金蝶凭证工具 <span class="ver">P1.2.33</span></h1>
297
309
  <div class="sub">纯前端离线版 · 双击即用 · 数据只存在本机,不上传任何服务器</div>
298
310
  </div>
299
311
  </div>
@@ -330,9 +342,8 @@ tr.bad:hover td{background:#fbe9e9}
330
342
  <br><b>业务数据表必须是 .xls 原文件,不要改名、不要另存为 .xlsx。</b>
331
343
  </div>
332
344
  <div class="rowacts">
333
- <button class="btn primary big" onclick="UI.openDataTree()">🌲 选择数据表 / 字段(树形导航)</button>
334
- <button class="btn ghost big" onclick="UI.openDataViewer()">📋 数据浏览/编辑(抽屉)</button>
335
- <span class="tip">也可以把 Excel 直接拖到下面的框里自动归位;点「🌲」可逐级展开查看每张表、每个字段;点「📋」打开抽屉,按左侧 key 列表切换表,右侧直接编辑单元格、改完点「保存到 xlsx」回写</span>
345
+ <button class="btn primary big" onclick="UI.openUploadDialog()">📤 上传 Excel 数据</button>
346
+ <span class="tip">点上面的按钮弹上传对话框(17 张表下拉选);或直接把 Excel <b>拖到下方对应区域</b>自动归位。点下方<b>已上传的卡片</b>打开「📋 数据浏览/编辑」抽屉,在那里改单元格、复制行、删行,最后点「💾 保存到 xlsx」回写到本地数据。</span>
336
347
  </div>
337
348
  <h3>基础数据(9 个 · 一次上传长期有效)<span class="tip" style="margin-left:8px">支持 <b>点击按钮</b> 或 <b>把 Excel 文件拖到这里</b></span></h3>
338
349
  <div class="grid dropzone" id="grpBase" data-kind="base"></div>
@@ -2252,10 +2263,15 @@ window.WWT_ENGINE = (function () {
2252
2263
 
2253
2264
  /* ---------------- 金额表达式 ---------------- */
2254
2265
  function isAutoDiff(line) { return clean(line && line.amountField) === "__AUTO_DIFF__"; }
2266
+
2267
+ // 兼容旧接口:返回 { parts: [{fld, sign}], auto? }。新逻辑统一走 evalAmtExpr,
2268
+ // 这里只为了不让外部旧调用点(如果有)报错。
2255
2269
  function parseAmtExpr(expr) {
2256
2270
  const e = clean(expr);
2257
2271
  if (!e || e === "-") return null;
2258
2272
  if (e === "__AUTO_DIFF__") return { auto: true };
2273
+ // 含 * / ( ) 时视为新语法(按 evalAmtExpr 处理),parts 留空
2274
+ if (/[*\/()]/.test(e)) return { parts: [], v2: true };
2259
2275
  const parts = [];
2260
2276
  for (let p of e.split("+")) {
2261
2277
  p = p.trim();
@@ -2266,12 +2282,103 @@ window.WWT_ENGINE = (function () {
2266
2282
  }
2267
2283
  return parts.length ? { parts } : null;
2268
2284
  }
2285
+
2286
+ // ============== 表达式解析器(支持 + - * / 括号 与 字段名/数字常量) ==============
2287
+ // 语法:
2288
+ // expr := term ( ('+'|'-') term )*
2289
+ // term := atom ( ('*'|'/') atom )*
2290
+ // atom := '-' atom | number | identifier | '(' expr ')'
2291
+ // identifier := 除空白与运算符之外的连续字符(即字段名)
2292
+ //
2293
+ // 设计目标:会计场景常见公式——`实收金额+折扣费` / `-折扣费` / `实收金额*0.13` / `金额/数量` / `(金额+税额)*0.13`
2294
+ // tokenize 时跳过空白;遇到运算符边界时,前一段连续字符要么是字段名,要么是数字。
2295
+ function tokenizeExpr(s) {
2296
+ const toks = [];
2297
+ let i = 0;
2298
+ while (i < s.length) {
2299
+ const c = s.charAt(i);
2300
+ if (c === ' ' || c === '\t') { i++; continue; }
2301
+ if (c === '+' || c === '*' || c === '/' || c === '(' || c === ')') { toks.push({ t: c }); i++; continue; }
2302
+ if (c === '-') {
2303
+ // 单目 vs 二元判断:若前一个 token 是操作符/左括号/开头,则是单目
2304
+ const prev = toks[toks.length - 1];
2305
+ const isUnary = !prev || (prev.t !== ')' && !/^\d/.test(prev.v || '') && prev.t !== 'num' && prev.t !== 'id');
2306
+ toks.push({ t: isUnary ? 'unary-' : '-' });
2307
+ i++; continue;
2308
+ }
2309
+ // 读一段连续字符
2310
+ let j = i;
2311
+ while (j < s.length) {
2312
+ const cj = s.charAt(j);
2313
+ if (cj === ' ' || cj === '\t') break;
2314
+ if ('+*-/()'.indexOf(cj) >= 0) break;
2315
+ j++;
2316
+ }
2317
+ const seg = s.slice(i, j);
2318
+ i = j;
2319
+ if (seg) {
2320
+ // 判断是数字(允许可选前导 0/小数点)
2321
+ if (/^-?\d+(\.\d+)?$/.test(seg) || /^\.\d+$/.test(seg)) toks.push({ t: 'num', v: parseFloat(seg) });
2322
+ else toks.push({ t: 'id', v: seg });
2323
+ }
2324
+ }
2325
+ return toks;
2326
+ }
2327
+
2328
+ function evalAmtExpr(row, expr) {
2329
+ const e = clean(expr);
2330
+ if (!e || e === "-") return 0;
2331
+ const toks = tokenizeExpr(e);
2332
+ let pos = 0;
2333
+ function peek() { return toks[pos]; }
2334
+ function eat(t) { if (toks[pos] && toks[pos].t === t) { pos++; return true; } return false; }
2335
+ function err(m) { throw new Error('金额表达式错误: ' + m + ' (pos=' + pos + ', expr=' + e + ')'); }
2336
+
2337
+ function parseExpr() {
2338
+ let v = parseTerm();
2339
+ while (true) {
2340
+ const p = peek();
2341
+ if (!p) break;
2342
+ if (p.t === '+') { pos++; v = v + parseTerm(); }
2343
+ else if (p.t === '-') { pos++; v = v - parseTerm(); }
2344
+ else break;
2345
+ }
2346
+ return v;
2347
+ }
2348
+ function parseTerm() {
2349
+ let v = parseAtom();
2350
+ while (true) {
2351
+ const p = peek();
2352
+ if (!p) break;
2353
+ if (p.t === '*') { pos++; v = v * parseAtom(); }
2354
+ else if (p.t === '/') {
2355
+ pos++;
2356
+ const d = parseAtom();
2357
+ if (d === 0) throw new Error('金额表达式除零 (expr=' + e + ')');
2358
+ v = v / d;
2359
+ }
2360
+ else break;
2361
+ }
2362
+ return v;
2363
+ }
2364
+ function parseAtom() {
2365
+ const p = peek();
2366
+ if (!p) err('缺少操作数');
2367
+ if (p.t === 'unary-') { pos++; return -parseAtom(); }
2368
+ if (p.t === '(') { pos++; const v = parseExpr(); if (!eat(')')) err('缺少右括号'); return v; }
2369
+ if (p.t === 'num') { pos++; return p.v; }
2370
+ if (p.t === 'id') { pos++; return num(row[p.v]); }
2371
+ err('非法 token: ' + JSON.stringify(p));
2372
+ }
2373
+ const v = parseExpr();
2374
+ if (pos !== toks.length) err('未消费完: ' + toks.slice(pos).map(t => t.v || t.t).join(' '));
2375
+ return v;
2376
+ }
2377
+
2269
2378
  function sumAmtExpr(row, expr) {
2270
- const pe = parseAmtExpr(expr);
2271
- if (!pe || pe.auto) return 0;
2272
- let s = 0;
2273
- for (const it of pe.parts) s += num(row[it.fld]) * it.sign;
2274
- return s;
2379
+ if (isAutoDiff({ amountField: expr })) return 0;
2380
+ try { return round2(evalAmtExpr(row, expr)); }
2381
+ catch (e) { console.warn(e.message); return 0; }
2275
2382
  }
2276
2383
 
2277
2384
  /* ---------------- 映射查表 ---------------- */
@@ -3285,7 +3392,7 @@ window.WWT_ENGINE = (function () {
3285
3392
  return {
3286
3393
  clean, num, round2, normPay, pad3,
3287
3394
  buildIndex, getField, compare, evalConditions, applyTransform,
3288
- parseAmtExpr, sumAmtExpr, isAutoDiff, splitFieldsOf,
3395
+ parseAmtExpr, sumAmtExpr, evalAmtExpr, isAutoDiff, splitFieldsOf,
3289
3396
  calcDimValues, leafSubject, resolveEffectiveLines, lookupTypeDim,
3290
3397
  generate, balanceCheck, buildKingdee, detect, auditRule,
3291
3398
  lookupMap, lookupTypeMap,
@@ -3311,7 +3418,7 @@ const S = window.__STATE__ = {
3311
3418
  };
3312
3419
 
3313
3420
  /* ---------------- 版本 & 框架自动更新(只更新程序本身,本地数据/规则不动) ---------------- */
3314
- const WWT_VER=(function(){const raw='P1.2.31';return raw.indexOf('__WVER__')>=0?'P1.2.10':raw;})();
3421
+ const WWT_VER=(function(){const raw='P1.2.33';return raw.indexOf('__WVER__')>=0?'P1.2.10':raw;})();
3315
3422
  const UPD_URL_KEY='wwt_fe_updurl';
3316
3423
  // 默认更新源:Gitee 上本项目的 version.json(API 接口,浏览器可直接跨域读)
3317
3424
  const DEFAULT_UPD_URL='https://gitee.com/api/v5/repos/wwtmrhu/certificate-generation/contents/dist/version.json';
@@ -3732,20 +3839,33 @@ const BASE_KEYWORD={'会计科目表':['科目'],'供应商表':['供应商'],'
3732
3839
 
3733
3840
  function renderUpload(){
3734
3841
  const gb=$('grpBase'),gbi=$('grpBiz');
3735
- // 卡片改为「只读展示」:所有数据选择(上传/预览/删除)统一走顶部「🌲 选择数据表」树形面板,
3736
- // 不再在每张卡片旁放独立按钮(避免按钮过多且不统一)。拖拽上传保留。
3842
+ // 卡片改为「可点击进入抽屉」:已上传表点一下打开抽屉浏览/编辑,未上传表点一下弹「上传 Excel」对话框
3737
3843
  gb.innerHTML=C.BASE_SLOTS.map(s=>{
3738
3844
  const rows=(S.base[s.key]||[]).length;
3739
- return '<div class="card '+(rows?'ok':'empty')+'" data-slot="'+esc(s.key)+'" data-kind="base">'+
3740
- '<div><div class="cname">'+esc(s.label)+'</div><div class="cmeta">'+(rows?'✅ 已上传 · '+rows+' 行':'⚪ 未上传')+'</div></div></div>';
3845
+ const cls='card '+(rows?'ok':'empty');
3846
+ return '<div class="'+cls+'" data-slot="'+esc(s.key)+'" data-kind="base" data-loaded="'+(rows?1:0)+'">'+
3847
+ '<div><div class="cname">'+esc(s.label)+'</div><div class="cmeta">'+(rows?'✅ 已上传 · '+rows+' 行':'⚪ 未上传 · 点此上传')+'</div></div></div>';
3741
3848
  }).join('');
3742
3849
  gbi.innerHTML=C.BIZ_SLOTS.map(s=>{
3743
3850
  const rows=(S.biz[s.key]||[]).length;
3744
3851
  const f=C.SOURCE_META[s.key]?C.SOURCE_META[s.key].file:'';
3745
- return '<div class="card '+(rows?'ok':'empty')+'" data-slot="'+esc(s.key)+'" data-kind="biz">'+
3746
- '<div><div class="cname">'+esc(s.label)+'</div><div class="cmeta">'+(rows?'✅ 已上传 · '+rows+'':'⚪ 未上传')+
3852
+ const cls='card '+(rows?'ok':'empty');
3853
+ return '<div class="'+cls+'" data-slot="'+esc(s.key)+'" data-kind="biz" data-loaded="'+(rows?1:0)+'">'+
3854
+ '<div><div class="cname">'+esc(s.label)+'</div><div class="cmeta">'+(rows?'✅ 已上传 · '+rows+' 行':'⚪ 未上传 · 点此上传')+
3747
3855
  (f?' · '+esc(f):'')+'</div></div></div>';
3748
3856
  }).join('');
3857
+ // 卡片点击:已上传 → 打开抽屉浏览/编辑;未上传 → 弹上传对话框
3858
+ // 用事件委托,一次挂到 grpBase / grpBiz 即可(renderUpload 时不重复挂)
3859
+ [['grpBase','base'],['grpBiz','biz']].forEach(([id,kind])=>{
3860
+ const box=$(id); if(!box || box._clickWired) return; box._clickWired=true;
3861
+ box.addEventListener('click', e=>{
3862
+ const c=e.target.closest&&e.target.closest('.card');
3863
+ if(!c || !box.contains(c)) return;
3864
+ const key=c.dataset.slot, loaded=c.dataset.loaded==='1';
3865
+ if(loaded) openDataViewer(kind, key);
3866
+ else openUploadDialog();
3867
+ });
3868
+ });
3749
3869
  // 拖拽上传:整个区域都能接文件,多文件按 autoAutoFill 逻辑自动归位(监听只挂一次)
3750
3870
  [['grpBase','base'],['grpBiz','biz']].forEach(([id,kind])=>{
3751
3871
  const box=$(id); if(!box || box._dragWired===kind) return; box._dragWired=kind;
@@ -3897,6 +4017,49 @@ function _treeOnClick(e){
3897
4017
  if(act==='refresh'){ refreshUpload(); _treeRefreshIfOpen(); return; }
3898
4018
  }
3899
4019
 
4020
+ // 📤 上传 Excel 数据弹窗(P1.2.32 · 替代旧的树形入口)
4021
+ // 17 张表分组下拉 + 文件 input + 名称自动校验 + 实时上传
4022
+ function openUploadDialog(){
4023
+ const m=$('modal');
4024
+ const optBiz = C.BIZ_SLOTS.map(s=>'<option value="'+esc(s.key)+'" data-kind="biz">'+esc(s.label)+'(业务)</option>').join('');
4025
+ const optBase = C.BASE_SLOTS.map(s=>'<option value="'+esc(s.key)+'" data-kind="base">'+esc(s.label)+'(基础)</option>').join('');
4026
+ m.innerHTML=
4027
+ '<div class="modalbox uploadpanel">'+
4028
+ '<div class="modalhd"><h3>📤 上传 Excel 数据</h3><button class="mini" data-act="close">✕ 关闭</button></div>'+
4029
+ '<div class="modalbd">'+
4030
+ '<div class="up-row"><label>目标表</label>'+
4031
+ '<select id="upSlot"><optgroup label="业务数据(每月重传)">'+optBiz+'</optgroup>'+
4032
+ '<optgroup label="基础数据(一次上传长期有效)">'+optBase+'</optgroup></select>'+
4033
+ '</div>'+
4034
+ '<div class="up-row"><label>选择文件</label><input type="file" id="upFile" accept=".xls,.xlsx"></div>'+
4035
+ '<div class="up-tip">业务数据请用本月原 <b>.xls</b> 文件,<b>不要改名、不要另存为 .xlsx</b>。文件名含表关键字可自动归位(如「入库已确认.xls」「客户表.xls」)。</div>'+
4036
+ '<div class="up-row up-actions">'+
4037
+ '<button class="btn primary" data-act="upload">📤 上传</button>'+
4038
+ '<button class="btn ghost" data-act="close">取消</button>'+
4039
+ '</div>'+
4040
+ '</div>'+
4041
+ '</div>';
4042
+ m.style.display='flex';
4043
+ const box=m.querySelector('.uploadpanel');
4044
+ const close=()=>{ m.style.display='none'; m.innerHTML=''; };
4045
+ box.addEventListener('click', async e=>{
4046
+ if(e.target.dataset && e.target.dataset.act==='close'){ close(); return; }
4047
+ if(e.target.dataset && e.target.dataset.act==='upload'){
4048
+ const sel=$('upSlot'), fileEl=$('upFile');
4049
+ const file=fileEl.files && fileEl.files[0];
4050
+ const opt=sel.options[sel.selectedIndex];
4051
+ const key=sel.value, kind=opt.dataset.kind;
4052
+ if(!file){ toast('请先选文件'); return; }
4053
+ // 业务表按 SOURCE_META.file 关键字校验;基础表按 key 关键字
4054
+ const stem = kind==='base' ? key : ((C.SOURCE_META[key]&&C.SOURCE_META[key].file)||'').replace(/\.xls$/i,'');
4055
+ if(stem && file.name.indexOf(stem)<0 && !confirm('文件名不含「'+stem+'」,可能不是该表对应文件,仍要上传吗?')) return;
4056
+ close();
4057
+ await handleOne(kind, key, file);
4058
+ renderUpload();
4059
+ }
4060
+ });
4061
+ }
4062
+
3900
4063
  function openDataTree(){
3901
4064
  const m=$('modal');
3902
4065
  m.innerHTML=
@@ -4846,7 +5009,7 @@ function lineHTML(bi,li,l){
4846
5009
  return '<div class="sec linerow" data-bi="'+bi+'" data-li="'+li+'" style="background:#fff;margin:5px 0">'+
4847
5010
  '<div class="row"><select class="w60" data-lf="side">'+sideOpts+'</select>'+
4848
5011
  '<span class="w60">科目</span><input class="w140" data-lf="subject" value="'+esc(l.subject||'')+'" placeholder="如 1122 / 6002" list="dlSubjects" title="可下拉选会计科目表里的编码;也可手动输入">'+
4849
- '<span class="w60">金额</span><input class="w180" data-lf="amountField" value="'+esc(l.amountField||'')+'" placeholder="实收金额+折扣费 / -折扣费 / __AUTO_DIFF__" list="dlBizFields" title="多列用 + 连接;减用 - 前缀;__AUTO_DIFF__ 自动补差额">'+
5012
+ '<span class="w60">金额</span><input class="w180" data-lf="amountField" value="'+esc(l.amountField||'')+'" placeholder="实收金额+折扣费 / -折扣费 / 金额*0.13 / 金额/数量" list="dlBizFields" title="支持四则运算与括号:字段名/数字常量;例:实收金额+折扣费、-折扣费、金额*0.13、金额/数量、(金额+税额)*0.13。特殊值:__AUTO_DIFF__ 自动补差额">'+
4850
5013
  '<button class="mini del" onclick="UI.delRow(this,\'linerow\')">删除分录</button></div>'+
4851
5014
  '<div class="row"><span class="w60">摘要</span><input class="grow" data-lf="summary" value="'+esc(l.summary||'')+'" placeholder="{单位名称}{收入板块}对帐S{原单号}"></div>'+
4852
5015
  '<div style="font-size:11.5px;color:#57606a;margin:3px 0">辅助核算维度:</div>'+
@@ -6233,12 +6396,12 @@ function _dvRenderLeft(){
6233
6396
  for(const s of C.BIZ_SLOTS){
6234
6397
  const loaded = (S.biz[s.key]||[]).length;
6235
6398
  const file = (C.SOURCE_META[s.key]&&C.SOURCE_META[s.key].file)||'';
6236
- html += `<div class="dv-key-item${loaded?'':' empty'}" data-key="${esc(s.key)}" data-kind="biz" onclick="UI._dvPickKey('${esc(s.key)}','biz')" title="${esc(file)} · ${loaded} 行">${esc(s.label)}<span class="dv-key-meta">${loaded?('✅ '+loaded):'未上传'}</span></div>`;
6399
+ html += `<div class="dv-key-item${loaded?'':' empty'}" data-key="${esc(s.key)}" data-kind="biz" onclick="UI._dvPickKey('${esc(s.key)}','biz')" title="${esc(file)} · ${loaded} 行"><span class="dv-key-name">${esc(s.label)}</span><span class="dv-key-meta">${loaded?('✅ '+loaded):'未上传'}</span></div>`;
6237
6400
  }
6238
6401
  html += '<div class="dv-grp" style="margin-top:8px">基础数据</div>';
6239
6402
  for(const s of C.BASE_SLOTS){
6240
6403
  const loaded = (S.base[s.key]||[]).length;
6241
- html += `<div class="dv-key-item${loaded?'':' empty'}" data-key="${esc(s.key)}" data-kind="base" onclick="UI._dvPickKey('${esc(s.key)}','base')" title="${esc(s.key)}.xls · ${loaded} 行">${esc(s.label)}<span class="dv-key-meta">${loaded?('✅ '+loaded):'未上传'}</span></div>`;
6404
+ html += `<div class="dv-key-item${loaded?'':' empty'}" data-key="${esc(s.key)}" data-kind="base" onclick="UI._dvPickKey('${esc(s.key)}','base')" title="${esc(s.key)}.xls · ${loaded} 行"><span class="dv-key-name">${esc(s.label)}</span><span class="dv-key-meta">${loaded?('✅ '+loaded):'未上传'}</span></div>`;
6242
6405
  }
6243
6406
  left.innerHTML = html;
6244
6407
  }
@@ -6296,7 +6459,7 @@ function _dvRenderBody(){
6296
6459
  const v = r[c]; return v!==null && v!==undefined && String(v).toLowerCase().indexOf(ft) >= 0;
6297
6460
  }));
6298
6461
  }
6299
- let html = '<table class="dv-table"><thead><tr>';
6462
+ let html = '<div class="dv-grid-inner"><table class="dv-table"><thead><tr>';
6300
6463
  const allPicked = displayRows.length > 0 && _dvState.picked.size === displayRows.length;
6301
6464
  html += `<th class="dv-cb"><input type="checkbox" onchange="UI._dvToggleAll(this.checked)" ${allPicked?'checked':''}></th>`;
6302
6465
  html += '<th class="dv-num">#</th>';
@@ -6311,14 +6474,14 @@ function _dvRenderBody(){
6311
6474
  const v = r[c];
6312
6475
  const isDate = _dvIsDateCol(c, v);
6313
6476
  const display = (v===null||v===undefined)?'':String(v);
6314
- html += `<td><input type="${isDate?'date':'text'}" data-r="${realIdx}" data-c="${esc(c)}" value="${esc(display)}" onblur="UI._dvCellBlur(this)"></td>`;
6477
+ html += `<td title="${esc(display)}"><input type="${isDate?'date':'text'}" data-r="${realIdx}" data-c="${esc(c)}" value="${esc(display)}" onblur="UI._dvCellBlur(this)"></td>`;
6315
6478
  }
6316
6479
  html += '</tr>';
6317
6480
  }
6318
6481
  if(!displayRows.length){
6319
6482
  html += `<tr><td colspan="${cols.length+2}" style="padding:20px;color:#999;text-align:center;">${ft?'没有匹配的':''}空表</td></tr>`;
6320
6483
  }
6321
- html += '</tbody></table>';
6484
+ html += '</tbody></table></div>';
6322
6485
  $('dvBody').innerHTML = html;
6323
6486
  }
6324
6487
  function _dvIsDateCol(head, v){
@@ -6455,6 +6618,7 @@ window.UI={
6455
6618
  DB:DB,
6456
6619
  // ①
6457
6620
  pickOne:pickOne, refreshUpload:refreshUpload, clearBiz:clearBiz,
6621
+ openUploadDialog:openUploadDialog,
6458
6622
  openDataTree:openDataTree, _treeRefreshIfOpen:_treeRefreshIfOpen,
6459
6623
  autoFillBase:()=>autoFill('base'), autoFillBiz:()=>autoFill('biz'),
6460
6624
  delSlot:async(kind,key)=>{
package/dist/version.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
- "ver": "P1.2.31",
3
- "npm": "1.2.31",
4
- "url": "https://cdn.jsdelivr.net/npm/wwt-certificate-tool@1.2.31/dist/app.html",
5
- "url_unpkg": "https://unpkg.com/wwt-certificate-tool@1.2.31/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.31.html",
7
- "note": "修复「下载更新后打不开」:openApp 原先把入参无脑当 base64 解码,而 jsDelivr/unpkg 返回的是明文 HTML InvalidCharacterError(表现:下载成功却提示失败)。现自动识别(以 < 开头即明文)。另含 P1.2.30 的更新源改造:改走 npm wwt-certificate-tool + jsDelivr CDN(npmmirror /files/ 需 cnpm 白名单,新包不可用,仅用其 /latest 查版本号)。"
2
+ "ver": "P1.2.33",
3
+ "npm": "1.2.33",
4
+ "url": "https://cdn.jsdelivr.net/npm/wwt-certificate-tool@1.2.33/dist/app.html",
5
+ "url_unpkg": "https://unpkg.com/wwt-certificate-tool@1.2.33/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.33.html",
7
+ "note": "默认分录金额表达式支持四则运算与括号:① 字段名/数字常量,③ 运算符 + - * /,④ 括号 () 优先级,⑤ 单目负号(-字段 仍可用)。例:实收金额+折扣费、-折扣费、金额*0.13(税额)、金额/数量(单价)、(金额+税额)*0.13 嵌套。placeholder 文案与 tooltip 已同步更新。"
8
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wwt-certificate-tool",
3
- "version": "1.2.31",
3
+ "version": "1.2.33",
4
4
  "description": "WWT 金蝶凭证工具 · 纯前端版(离线单文件 HTML,双击即用)",
5
5
  "keywords": [
6
6
  "WWT",