w-web-perm 1.1.13 → 1.1.14

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.
Files changed (107) hide show
  1. package/README.md +70 -6
  2. package/dist/css/{app.f09c7433.css → app.f24c5e50.css} +1 -1
  3. package/dist/index.tmp +1 -1
  4. package/dist/js/app-legacy.033deb22.js +1 -0
  5. package/dist/js/app.033deb22.js +1 -0
  6. package/dist/js/{chunk-vendors.ece5a361.js → chunk-vendors-legacy.b0bbb5ac.js} +4 -4
  7. package/dist/js/{chunk-vendors-legacy.ece5a361.js → chunk-vendors.b0bbb5ac.js} +4 -4
  8. package/dist/w-web-perm.umd.js +2 -2
  9. package/dist/w-web-perm.umd.js.map +1 -1
  10. package/docs/WWebPerm.html +11 -6
  11. package/docs/global.html +11316 -5113
  12. package/docs/index.html +2 -2
  13. package/docs/server_WWebPerm.mjs.html +114 -334
  14. package/docs/server_cascadeWrite.mjs.html +5 -26
  15. package/docs/server_dataChannel.mjs.html +3 -3
  16. package/docs/server_keyCheck.mjs.html +150 -23
  17. package/docs/server_lockSave.mjs.html +2 -2
  18. package/docs/server_maskLog.mjs.html +2 -2
  19. package/docs/server_procCore.mjs.html +246 -62
  20. package/docs/server_staLogs_resolveShellWorker.mjs.html +2 -2
  21. package/docs/server_syncCore.mjs.html +533 -0
  22. package/docs/server_syncKeep.mjs.html +27 -20
  23. package/docs/server_tools_authRefsAudit.mjs.html +2 -2
  24. package/docs/server_tools_authRefsClean.mjs.html +2 -2
  25. package/docs/server_tools_fromTools.mjs.html +2 -2
  26. package/docs/server_tools_isMain.mjs.html +2 -2
  27. package/docs/server_tools_nameDupReport.mjs.html +40 -7
  28. package/docs/server_validateRows.mjs.html +2 -2
  29. package/docs/server_wrapInjected.mjs.html +2 -2
  30. package/docs/src_fetchJson.mjs.html +2 -2
  31. package/docs/src_getPermUserInfor.mjs.html +175 -0
  32. package/docs/src_js_applySaved.mjs.html +128 -0
  33. package/docs/src_js_cellEditing.mjs.html +163 -0
  34. package/docs/src_js_cvKpToArr.mjs.html +2 -2
  35. package/docs/src_js_cvKpToObj.mjs.html +2 -2
  36. package/docs/src_js_draftSync.mjs.html +83 -7
  37. package/docs/src_js_eachOwn.mjs.html +2 -2
  38. package/docs/src_js_editorRefRows.mjs.html +2 -2
  39. package/docs/src_js_externalRows.mjs.html +34 -10
  40. package/docs/src_js_externalUi.mjs.html +4 -8
  41. package/docs/src_js_findRefRows.mjs.html +2 -2
  42. package/docs/src_js_findTouchedDup.mjs.html +2 -2
  43. package/docs/src_js_fmtLossyMsg.mjs.html +2 -2
  44. package/docs/src_js_fromAudit.mjs.html +2 -2
  45. package/docs/src_js_getAddImpact.mjs.html +2 -2
  46. package/docs/src_js_getAuthWarn.mjs.html +2 -2
  47. package/docs/src_js_getCascadePreview.mjs.html +2 -2
  48. package/docs/src_js_getDelRefCount.mjs.html +2 -2
  49. package/docs/src_js_getEffMode.mjs.html +2 -2
  50. package/docs/src_js_getKpLossy.mjs.html +2 -2
  51. package/docs/src_js_getNameErrs.mjs.html +2 -2
  52. package/docs/src_js_getOwn.mjs.html +2 -2
  53. package/docs/src_js_getPropKey.mjs.html +2 -2
  54. package/docs/src_js_getRuleEnable.mjs.html +2 -2
  55. package/docs/src_js_isAuthMode.mjs.html +2 -2
  56. package/docs/src_js_joinNames.mjs.html +2 -2
  57. package/docs/src_js_legendFit.mjs.html +2 -2
  58. package/docs/src_js_pairRowsToSnapshot.mjs.html +2 -2
  59. package/docs/src_js_refAudit.mjs.html +2 -2
  60. package/docs/src_js_refCascade.mjs.html +2 -2
  61. package/docs/src_js_refIdentity.mjs.html +500 -0
  62. package/docs/src_js_relDialogSend.mjs.html +2 -2
  63. package/docs/src_js_rowDigest.mjs.html +75 -145
  64. package/docs/src_plugins_dataReloadGuard.mjs.html +2 -2
  65. package/docs/src_plugins_reuseDeletedIds.mjs.html +2 -2
  66. package/docs/src_plugins_submitGuard.mjs.html +2 -2
  67. package/g_provideGrups.mjs +1 -1
  68. package/g_providePemis.mjs +1 -1
  69. package/g_provideTargets.mjs +1 -1
  70. package/g_provideUsers.mjs +1 -1
  71. package/package.json +2 -2
  72. package/server/WWebPerm.mjs +112 -332
  73. package/server/cascadeWrite.mjs +3 -24
  74. package/server/dataChannel.mjs +1 -1
  75. package/server/keyCheck.mjs +148 -21
  76. package/server/procCore.mjs +244 -60
  77. package/server/procLang.mjs +72 -4
  78. package/server/provideTabs.mjs +6 -4
  79. package/server/syncCore.mjs +461 -0
  80. package/server/syncKeep.mjs +25 -18
  81. package/server/tools/nameDupReport.mjs +38 -5
  82. package/src/components/LayoutContentGrups.vue +57 -8
  83. package/src/components/LayoutContentPemis.vue +57 -8
  84. package/src/components/LayoutContentTargets.vue +38 -7
  85. package/src/components/LayoutContentUsers.vue +57 -8
  86. package/src/components/VeGrupBlngUsers.vue +128 -20
  87. package/src/components/VePemiBlngGrups.vue +128 -20
  88. package/src/getPermUserInfor.mjs +10 -0
  89. package/src/js/applySaved.mjs +56 -0
  90. package/src/js/cellEditing.mjs +91 -0
  91. package/src/js/draftSync.mjs +81 -5
  92. package/src/js/externalRows.mjs +32 -8
  93. package/src/js/externalUi.mjs +2 -6
  94. package/src/js/refIdentity.mjs +428 -0
  95. package/src/js/rowDigest.mjs +73 -143
  96. package/src/schema/contracts/grups.mjs +8 -0
  97. package/src/schema/contracts/index.mjs +17 -0
  98. package/src/schema/contracts/pemis.mjs +8 -0
  99. package/src/schema/contracts/targets.mjs +8 -0
  100. package/src/schema/contracts/users.mjs +13 -0
  101. package/src/schema/tables/grups.mjs +4 -0
  102. package/src/schema/tables/pemis.mjs +4 -0
  103. package/src/schema/tables/targets.mjs +4 -0
  104. package/src/schema/tables/users.mjs +4 -0
  105. package/srv.mjs +19 -0
  106. package/dist/js/app-legacy.8af87052.js +0 -1
  107. package/dist/js/app.8af87052.js +0 -1
@@ -307,7 +307,10 @@ import { getSavePreview } from '../js/getCascadePreview.mjs'
307
307
  import { getAddImpact, getAddConfirmText } from '../js/getAddImpact.mjs'
308
308
  import { isExternalRow } from '../js/externalRows.mjs'
309
309
  import { makeBaseline } from '../js/rowDigest.mjs'
310
- import { refsChanged, planDraftSync, isDraftStale } from '../js/draftSync.mjs'
310
+ import { refsChanged, planDraftSync, isDraftStale, hasDraftNow } from '../js/draftSync.mjs'
311
+ import { applySavedRows } from '../js/applySaved.mjs'
312
+ import { snapRefIds, recordPicks, buildRefDecl, parseRefDecl, countRefMismatch } from '../js/refIdentity.mjs'
313
+ import { isCellEditing, bindCellEditingStopRetry } from '../js/cellEditing.mjs'
311
314
  import { fmtVars, getBannerLines, splitCheckedRows, getExternalEdits } from '../js/externalUi.mjs'
312
315
  import WIcon from 'w-component-vue/src/components/WIcon.vue'
313
316
  import WSwitch from 'w-component-vue/src/components/WSwitch.vue'
@@ -410,6 +413,8 @@ export default {
410
413
  this.syncMark = { refs: null, editable: undefined, dirty: false, baseline: [], tick: 0 }
411
414
  //saveMark(非響應式, 同 syncMark 之理由): saving 為「自己之存檔請求進行中」(送出前設真, 回應〔成功或失敗〕一到歸零)——此期間伺服端寫入所致之推播若先於回應抵達, 不是他人更新(planDraftSync 回 'keepQuiet': 保留草稿但不顯示過期橫幅; run12)
412
415
  this.saveMark = { saving: false }
416
+ //refPicks(非響應式): 草稿內「使用者在編輯器選取的啟用引用項(權限名稱) → 選取當時對應之權限 id 陣列」(src/js/refIdentity.mjs; R-REF-IDENTITY-01), 存檔時據以組引用身分宣告 opt.refs; 重建草稿時歸零
417
+ this.refPicks = new Map()
413
418
  },
414
419
  mounted: function() {
415
420
  // console.log('mounted')
@@ -594,14 +599,14 @@ export default {
594
599
  first: vo.syncMark.refs === null || vo.syncMark.tick !== vo.syncTick, //首次載入或使用者按了「重新載入」(syncTick 遞增)
595
600
  editChanged: vo.syncMark.editable !== isEditable,
596
601
  storeChanged: refsChanged(vo.syncMark.refs, [rows, kpUse]),
597
- hasDraft: () => vo.syncMark.dirty === true,
602
+ hasDraft: () => hasDraftNow({ dirty: vo.syncMark.dirty === true, editing: isCellEditing(get(vo, '$refs.rftable', null)) }), //未存修改, 或儲存格編輯器開啟中(已輸入、未按 Enter; 第 115 項, R-DRAFT-KEEP-01 規則 10)
598
603
  saving: () => vo.saveMark.saving === true, //自己之存檔請求進行中所致之推播 → 'keepQuiet'(不改過期提示)
599
604
  })
600
605
  if (act === 'rebuild') {
601
606
  vo.rebuildDraft(rows, kpUse, isEditable)
602
607
  }
603
608
  else if (act === 'keep') {
604
- vo.staleNotice = isDraftStale({ keys: vo.$ds.grups.keys, rows, baseline: vo.syncMark.baseline })
609
+ vo.staleNotice = isDraftStale({ keyTable: 'grups', rows, baseline: vo.syncMark.baseline })
605
610
  }
606
611
 
607
612
  return ''
@@ -779,7 +784,7 @@ export default {
779
784
  let kpUseGrupUser = cloneDeep(kpUse)
780
785
 
781
786
  //baseline, 與 items 同一處由同一份來源列(grups, genItems 之前)計算: 草稿所依據之版本(F-26), 重建 items 時一併更新, 存檔時不重算
782
- let baseline = makeBaseline(grups, vo.$ds.grups.keys)
787
+ let baseline = makeBaseline(grups, 'grups')
783
788
 
784
789
  //items
785
790
  let items = vo.genItems(grups, kpUseGrupUser)
@@ -791,6 +796,7 @@ export default {
791
796
  vo.isModified = false
792
797
  vo.staleNotice = false
793
798
  vo.syncMark = { refs: [rows, kpUse], editable: isEditable, dirty: false, baseline, tick: vo.syncTick }
799
+ vo.refPicks = new Map() //重建即放棄未存編輯, 其選取記錄一併歸零
794
800
 
795
801
  //genOpt
796
802
  vo.genOpt({ isEditable })
@@ -798,6 +804,11 @@ export default {
798
804
  //firstLoading
799
805
  vo.firstLoading = false
800
806
 
807
+ //第 115 項: 重建後元件可能重建, 對新的 ag-grid 重綁「儲存格編輯結束」監聽(同一 api 只綁一次)
808
+ vo.$nextTick(() => {
809
+ vo.bindEditing()
810
+ })
811
+
801
812
  },
802
813
 
803
814
  curRefs: function() {
@@ -806,6 +817,22 @@ export default {
806
817
  return [vo.grups, vo.kpUseGrupUser]
807
818
  },
808
819
 
820
+ bindEditing: function() {
821
+ //第 115 項(R-DRAFT-KEEP-01 規則 10): 儲存格編輯器開啟中視為有草稿(changeParams 的 hasDraft 以 ag-grid 的 getEditingCells 判定); 編輯結束(提交、Esc、失焦)時補做保留期間跳過之重建
822
+ let vo = this
823
+ bindCellEditingStopRetry(() => get(vo, '$refs.rftable', null), () => {
824
+ vo.afterEditing()
825
+ })
826
+ },
827
+
828
+ afterEditing: function() {
829
+ //儲存格編輯結束: 無未存修改(isModified 為假; 提交者此刻已由 rowsChange 設為真)而 store 於編輯期間已變新者, 補上一次重建; 有未存修改者維持保留(isModified 的 watch 於儲存或放棄後補重建)
830
+ let vo = this
831
+ if (vo.isModified !== true && vo.syncMark.refs !== null && refsChanged(vo.syncMark.refs, vo.curRefs())) {
832
+ vo.reloadDraft()
833
+ }
834
+ },
835
+
809
836
  reloadDraft: function() {
810
837
  //「重新載入」鈕(資料已被更新橫幅): 使用者明確放棄未存編輯, 以目前 store 重建草稿與基準. 不在此直接呼叫 rebuildDraft: 遞增 syncTick 使 changeParams 重算並於 computed 內重建,
811
838
  //genOpt 之反應式依賴(語系等)才會重新收集(保留草稿期間 changeParams 未呼叫 genOpt, 依賴已收縮; 於 computed 外重建不會恢復, 重新載入後再切換語系表頭將停留舊值, 外審 sol 第 4 點)
@@ -1106,6 +1133,7 @@ export default {
1106
1133
  let _r = vo.$ds.grups.funNew()
1107
1134
  r.id = _r.id //使用新建方式預先產生id避免重複
1108
1135
  r.from = '' //複製出之列為後台新增列, 不繼承來源(來源非空即外部列, 後台新增列 from 須為空, 否則伺服端 saveFromNotAllowed; 不論是否啟用外部列保護)
1136
+ r.srcKey = '' //來源鍵 srcKey 與 from 同為身分欄(perm-run03 範圍二, R-IMPORT-01): 後台列之 srcKey 恆空, 不繼承外部列之來源鍵(否則伺服端 saveFromNotAllowed)
1109
1137
  r.name = vo.$s.getNameNew(rows, 'name', nameOld, vo.$t('grupCopyNameNew'))
1110
1138
  r.userId = `{${vo.$t('grupAddIdNew')}}`
1111
1139
  r.timeCreate = `{${vo.$t('grupAddIdNew')}}`
@@ -1393,6 +1421,8 @@ export default {
1393
1421
  //編輯器回傳之字串仍是舊版內容, 回填後存檔會以「新基準」證明過期內容新鮮(還原已被撤銷之授權, 或繞過同表別列已被改動之整表比對); 故回填前比對「草稿現所依據之那份表」與開框時之基準, 有異即放棄回填並提示重新載入.
1394
1422
  //父頁已有未存編輯時推播只保留草稿與其基準(baseRows 不變), 此比對不誤擋, 存檔時由伺服端表級比對拒收
1395
1423
  let baseline0 = cloneDeep(vo.baseline)
1424
+ //refSnap: 開啟編輯器當下之「權限名稱 → 權限 id 陣列」快照(R-REF-IDENTITY-01): 編輯器列出的權限即此刻 store 之權限, 使用者選取的是這些實體; 回填時據以記下新增之啟用引用項選取當時對應的 id, 存檔時送伺服端驗證其仍是同一實體(被刪除後同名重建〔id 不同〕即拒收)
1425
+ let refSnap = snapRefIds('grups', vo.pemis)
1396
1426
  //showVeCpemis
1397
1427
  vo.$dg.showVeCpemis({
1398
1428
  isEditable: vo.isEditable,
@@ -1415,11 +1445,14 @@ export default {
1415
1445
  }
1416
1446
 
1417
1447
  //草稿所依據之表於編輯器開啟後已被改動(整表指紋與開框時之基準不同; 僅 order/伺服器管理欄不同不算): 放棄回填並提示重新載入(同伺服端 saveStale 之語意)
1418
- if (isDraftStale({ keys: vo.$ds.grups.keys, rows: vo.baseRows, baseline: baseline0 })) {
1448
+ if (isDraftStale({ keyTable: 'grups', rows: vo.baseRows, baseline: baseline0 })) {
1419
1449
  vo.$alert(`${vo.$t('saveStale')}`, { type: 'warning' })
1420
1450
  return
1421
1451
  }
1422
1452
 
1453
+ //選取記錄: 本次新增之啟用引用項(權限名稱)在編輯器開啟當下對應之權限 id(查無記空陣列)
1454
+ recordPicks({ keyTable: 'grups', strOld: get(vo, `opt.rows[${krNow}].cpemis`, ''), strNew: cpemis, snap: refSnap, picks: vo.refPicks })
1455
+
1423
1456
  //set
1424
1457
  set(vo, `opt.rows[${krNow}].cpemis`, cpemis)
1425
1458
  // console.log('vo.opt.rows[krNow]', cloneDeep(vo.opt.rows[krNow]))
@@ -1476,6 +1509,15 @@ export default {
1476
1509
  return
1477
1510
  }
1478
1511
 
1512
+ //正向引用身分(perm-run02 範圍一; R-REF-IDENTITY-01; 使用者 2026-10-23 裁示前後端都要做): 本次新增之啟用引用項(權限名稱)所指之權限,須仍是使用者選取當時的同一實體(同 id);
1513
+ //被刪除或刪除後以同名重建(id 不同)即不送出(零寫入)並提示重新載入,不得把授權寫到同名新權限. 與伺服端 checkRefIdentity 同一判準(src/js/refIdentity.mjs); 宣告 refs 隨存檔送伺服端於權限表鎖內以新鮮讀取再驗一次
1514
+ let refs = parseRefDecl(buildRefDecl({ keyTable: 'grups', rowsNew: rows, rowsOld: vo.baseRows, picks: vo.refPicks, refRows: vo.pemis }))
1515
+ if (countRefMismatch({ keyTable: 'grups', decl: refs, refRows: vo.pemis }) > 0) {
1516
+ unlockBtn()
1517
+ await vo.$dg.showCheckYes(vo.$t('saveRefChanged'), { type: 'warning' })
1518
+ return
1519
+ }
1520
+
1479
1521
  //新增列之 transient 欄位 _isNew(addItem / copyItem 標記)隨列送後端: 後端據以拒絕「標為新增但 id 已存在」之列
1480
1522
  //(同一包重送時不把剛建立之列當修改覆寫, 回 saveNewRowExists), 檢查後由 ltdtmapping 依 schema 取欄時剝除不入庫 (ADR-025)
1481
1523
 
@@ -1519,15 +1561,17 @@ export default {
1519
1561
 
1520
1562
  //4) updateGrups, 各自 catch + 旗標短路; 第 2 參 { baseline } 為載入基準(伺服端於表鎖內比對本次將寫入之列是否仍與草稿所依據之版本相同, 否則整次拒收 saveStale)
1521
1563
  let okSave = false
1564
+ let savedRes = null //伺服端回傳(成功 msg key 字串, 或 { msg, rows, others }: 寫入後之列, 鎖內新鮮讀取; 規格第 8 節)
1522
1565
  vo.saveMark.saving = true //自己之存檔請求進行中: 此期間之推播不是他人更新(changeParams 的 planDraftSync 回 'keepQuiet'); 回應(成功或失敗)一到即歸零
1523
- await vo.$fapi.updateGrups(rows, { baseline })
1524
- .then(() => {
1566
+ await vo.$fapi.updateGrups(rows, { baseline, returnRows: true, refs })
1567
+ .then((res) => {
1525
1568
  okSave = true
1569
+ savedRes = res
1526
1570
  vo.saveMark.saving = false
1527
1571
  })
1528
1572
  .catch(async (err) => {
1529
1573
  vo.saveMark.saving = false
1530
- vo.staleNotice = isDraftStale({ keys: vo.$ds.grups.keys, rows: vo.grups, baseline: vo.syncMark.baseline }) //失敗(含 saveStale 被拒): 請求進行中被安靜保留之推播, 此時依目前 store 重算過期提示(同 changeParams 之保留分支)
1574
+ vo.staleNotice = isDraftStale({ keyTable: 'grups', rows: vo.grups, baseline: vo.syncMark.baseline }) //失敗(含 saveStale 被拒): 請求進行中被安靜保留之推播, 此時依目前 store 重算過期提示(同 changeParams 之保留分支)
1531
1575
  vo.$ui.updateLoading(false) //showCheckYes 前關 loading(modal 阻斷期間避免 loading 疊在底下)
1532
1576
  await vo.$dg.showCheckYes(`${vo.$t('grupSaveGrupsFail')}: ${vo.$tErr(err)}`)
1533
1577
  })
@@ -1536,6 +1580,11 @@ export default {
1536
1580
  //isModified; postSaving 自此至成功訊息框關閉後 300ms 內排除 rowsChange(見 data 註解)
1537
1581
  vo.postSaving = true
1538
1582
  vo.isModified = false
1583
+ vo.syncMark.dirty = false //watch 為非同步, 此處同步歸零: 下面 commit 引起之 changeParams 重算須視為「無草稿」而重建
1584
+
1585
+ //存檔成功後新基準取自伺服端且須立即取得(規格第 8 節; R-SAVED-ROWS-01): 以回傳之寫入後之列(鎖內新鮮讀取)更新 store, 本頁依既有流程以之重建畫面與基準;
1586
+ //其後抵達之同內容推播摘要相同, 不視為他人更新. 不以自己送出之值當新基準(clearXSS 等會改寫字串). 回傳不含列(舊伺服端、讀回失敗)時退回等推播
1587
+ applySavedRows('grups', savedRes, (t, rs) => vo.$store.commit(vo.$store.types.UpdateTableData, { tableName: t, data: rs, source: 'savedRows' }))
1539
1588
 
1540
1589
  //alert(showCheckYes 前關 loading,避免 modal 等待期間 loading 疊在底下)
1541
1590
  vo.$ui.updateLoading(false)
@@ -308,7 +308,10 @@ import { getSavePreview } from '../js/getCascadePreview.mjs'
308
308
  import { getAddImpact, getAddConfirmText } from '../js/getAddImpact.mjs'
309
309
  import { isExternalRow } from '../js/externalRows.mjs'
310
310
  import { makeBaseline } from '../js/rowDigest.mjs'
311
- import { refsChanged, planDraftSync, isDraftStale } from '../js/draftSync.mjs'
311
+ import { refsChanged, planDraftSync, isDraftStale, hasDraftNow } from '../js/draftSync.mjs'
312
+ import { applySavedRows } from '../js/applySaved.mjs'
313
+ import { snapRefIds, recordPicks, buildRefDecl, parseRefDecl, countRefMismatch } from '../js/refIdentity.mjs'
314
+ import { isCellEditing, bindCellEditingStopRetry } from '../js/cellEditing.mjs'
312
315
  import { fmtVars, getBannerLines, splitCheckedRows, getExternalEdits } from '../js/externalUi.mjs'
313
316
  import WIcon from 'w-component-vue/src/components/WIcon.vue'
314
317
  import WSwitch from 'w-component-vue/src/components/WSwitch.vue'
@@ -411,6 +414,8 @@ export default {
411
414
  this.syncMark = { refs: null, editable: undefined, dirty: false, baseline: [], tick: 0 }
412
415
  //saveMark(非響應式, 同 syncMark 之理由): saving 為「自己之存檔請求進行中」(送出前設真, 回應〔成功或失敗〕一到歸零)——此期間伺服端寫入所致之推播若先於回應抵達, 不是他人更新(planDraftSync 回 'keepQuiet': 保留草稿但不顯示過期橫幅; run12)
413
416
  this.saveMark = { saving: false }
417
+ //refPicks(非響應式): 草稿內「使用者在編輯器選取的啟用引用項(標的 id) → 選取當時對應之標的 id 陣列」(src/js/refIdentity.mjs; R-REF-IDENTITY-01), 存檔時據以組引用身分宣告 opt.refs; 重建草稿時歸零
418
+ this.refPicks = new Map()
414
419
  },
415
420
  mounted: function() {
416
421
  // console.log('mounted')
@@ -595,14 +600,14 @@ export default {
595
600
  first: vo.syncMark.refs === null || vo.syncMark.tick !== vo.syncTick, //首次載入或使用者按了「重新載入」(syncTick 遞增)
596
601
  editChanged: vo.syncMark.editable !== isEditable,
597
602
  storeChanged: refsChanged(vo.syncMark.refs, [rows, kpUse]),
598
- hasDraft: () => vo.syncMark.dirty === true,
603
+ hasDraft: () => hasDraftNow({ dirty: vo.syncMark.dirty === true, editing: isCellEditing(get(vo, '$refs.rftable', null)) }), //未存修改, 或儲存格編輯器開啟中(已輸入、未按 Enter; 第 115 項, R-DRAFT-KEEP-01 規則 10)
599
604
  saving: () => vo.saveMark.saving === true, //自己之存檔請求進行中所致之推播 → 'keepQuiet'(不改過期提示)
600
605
  })
601
606
  if (act === 'rebuild') {
602
607
  vo.rebuildDraft(rows, kpUse, isEditable)
603
608
  }
604
609
  else if (act === 'keep') {
605
- vo.staleNotice = isDraftStale({ keys: vo.$ds.pemis.keys, rows, baseline: vo.syncMark.baseline })
610
+ vo.staleNotice = isDraftStale({ keyTable: 'pemis', rows, baseline: vo.syncMark.baseline })
606
611
  }
607
612
 
608
613
  return ''
@@ -779,7 +784,7 @@ export default {
779
784
  let kpUsePemiGrup = cloneDeep(kpUse)
780
785
 
781
786
  //baseline, 與 items 同一處由同一份來源列(pemis, genItems 之前)計算: 草稿所依據之版本(F-26), 重建 items 時一併更新, 存檔時不重算
782
- let baseline = makeBaseline(pemis, vo.$ds.pemis.keys)
787
+ let baseline = makeBaseline(pemis, 'pemis')
783
788
 
784
789
  //items
785
790
  let items = vo.genItems(pemis, kpUsePemiGrup)
@@ -791,6 +796,7 @@ export default {
791
796
  vo.isModified = false
792
797
  vo.staleNotice = false
793
798
  vo.syncMark = { refs: [rows, kpUse], editable: isEditable, dirty: false, baseline, tick: vo.syncTick }
799
+ vo.refPicks = new Map() //重建即放棄未存編輯, 其選取記錄一併歸零
794
800
 
795
801
  //genOpt
796
802
  vo.genOpt({ isEditable })
@@ -798,6 +804,11 @@ export default {
798
804
  //firstLoading
799
805
  vo.firstLoading = false
800
806
 
807
+ //第 115 項: 重建後元件可能重建, 對新的 ag-grid 重綁「儲存格編輯結束」監聽(同一 api 只綁一次)
808
+ vo.$nextTick(() => {
809
+ vo.bindEditing()
810
+ })
811
+
801
812
  },
802
813
 
803
814
  curRefs: function() {
@@ -806,6 +817,22 @@ export default {
806
817
  return [vo.pemis, vo.kpUsePemiGrup]
807
818
  },
808
819
 
820
+ bindEditing: function() {
821
+ //第 115 項(R-DRAFT-KEEP-01 規則 10): 儲存格編輯器開啟中視為有草稿(changeParams 的 hasDraft 以 ag-grid 的 getEditingCells 判定); 編輯結束(提交、Esc、失焦)時補做保留期間跳過之重建
822
+ let vo = this
823
+ bindCellEditingStopRetry(() => get(vo, '$refs.rftable', null), () => {
824
+ vo.afterEditing()
825
+ })
826
+ },
827
+
828
+ afterEditing: function() {
829
+ //儲存格編輯結束: 無未存修改(isModified 為假; 提交者此刻已由 rowsChange 設為真)而 store 於編輯期間已變新者, 補上一次重建; 有未存修改者維持保留(isModified 的 watch 於儲存或放棄後補重建)
830
+ let vo = this
831
+ if (vo.isModified !== true && vo.syncMark.refs !== null && refsChanged(vo.syncMark.refs, vo.curRefs())) {
832
+ vo.reloadDraft()
833
+ }
834
+ },
835
+
809
836
  reloadDraft: function() {
810
837
  //「重新載入」鈕(資料已被更新橫幅): 使用者明確放棄未存編輯, 以目前 store 重建草稿與基準. 不在此直接呼叫 rebuildDraft: 遞增 syncTick 使 changeParams 重算並於 computed 內重建,
811
838
  //genOpt 之反應式依賴(語系等)才會重新收集(保留草稿期間 changeParams 未呼叫 genOpt, 依賴已收縮; 於 computed 外重建不會恢復, 重新載入後再切換語系表頭將停留舊值, 外審 sol 第 4 點)
@@ -1107,6 +1134,7 @@ export default {
1107
1134
  let _r = vo.$ds.pemis.funNew()
1108
1135
  r.id = _r.id //使用新建方式預先產生id避免重複
1109
1136
  r.from = '' //複製出之列為後台新增列, 不繼承來源(來源非空即外部列, 後台新增列 from 須為空, 否則伺服端 saveFromNotAllowed; 不論是否啟用外部列保護)
1137
+ r.srcKey = '' //來源鍵 srcKey 與 from 同為身分欄(perm-run03 範圍二, R-IMPORT-01): 後台列之 srcKey 恆空, 不繼承外部列之來源鍵(否則伺服端 saveFromNotAllowed)
1110
1138
  r.name = vo.$s.getNameNew(rows, 'name', nameOld, vo.$t('pemiCopyNameNew'))
1111
1139
  r.userId = `{${vo.$t('pemiAddIdNew')}}`
1112
1140
  r.timeCreate = `{${vo.$t('pemiAddIdNew')}}`
@@ -1395,6 +1423,8 @@ export default {
1395
1423
  //編輯器回傳之字串仍是舊版內容, 回填後存檔會以「新基準」證明過期內容新鮮(還原已被撤銷之授權, 或繞過同表別列已被改動之整表比對); 故回填前比對「草稿現所依據之那份表」與開框時之基準, 有異即放棄回填並提示重新載入.
1396
1424
  //父頁已有未存編輯時推播只保留草稿與其基準(baseRows 不變), 此比對不誤擋, 存檔時由伺服端表級比對拒收
1397
1425
  let baseline0 = cloneDeep(vo.baseline)
1426
+ //refSnap: 開啟編輯器當下之「標的名稱 → 標的 id 陣列」快照(R-REF-IDENTITY-01): 編輯器列出的標的即此刻 store 之標的, 使用者選取的是這些實體; 回填時據以記下新增之啟用引用項選取當時對應的 id, 存檔時送伺服端驗證其仍存在(標的 id 即名稱, 被刪除即拒收)
1427
+ let refSnap = snapRefIds('pemis', vo.targets)
1398
1428
  //showVeCrules
1399
1429
  vo.$dg.showVeCrules({
1400
1430
  isEditable: vo.isEditable,
@@ -1417,11 +1447,14 @@ export default {
1417
1447
  }
1418
1448
 
1419
1449
  //草稿所依據之表於編輯器開啟後已被改動(整表指紋與開框時之基準不同; 僅 order/伺服器管理欄不同不算): 放棄回填並提示重新載入(同伺服端 saveStale 之語意)
1420
- if (isDraftStale({ keys: vo.$ds.pemis.keys, rows: vo.baseRows, baseline: baseline0 })) {
1450
+ if (isDraftStale({ keyTable: 'pemis', rows: vo.baseRows, baseline: baseline0 })) {
1421
1451
  vo.$alert(`${vo.$t('saveStale')}`, { type: 'warning' })
1422
1452
  return
1423
1453
  }
1424
1454
 
1455
+ //選取記錄: 本次新增之啟用引用項(標的 id)在編輯器開啟當下對應之標的 id(查無記空陣列)
1456
+ recordPicks({ keyTable: 'pemis', strOld: get(vo, `opt.rows[${krNow}].crules`, ''), strNew: crules, snap: refSnap, picks: vo.refPicks })
1457
+
1425
1458
  //set
1426
1459
  set(vo, `opt.rows[${krNow}].crules`, crules)
1427
1460
  // console.log('vo.opt.rows[krNow]', cloneDeep(vo.opt.rows[krNow]))
@@ -1478,6 +1511,15 @@ export default {
1478
1511
  return
1479
1512
  }
1480
1513
 
1514
+ //正向引用身分(perm-run02 範圍一; R-REF-IDENTITY-01; 使用者 2026-10-23 裁示前後端都要做): 本次新增之啟用引用項(標的 id)所指之標的,須仍存在(標的 id 即名稱,身分定義即 id,見 R-REF-IDENTITY-01 規則 5);
1515
+ //已被刪除即不送出(零寫入)並提示重新載入. 與伺服端 checkRefIdentity 同一判準(src/js/refIdentity.mjs); 宣告 refs 隨存檔送伺服端於標的表鎖內以新鮮讀取再驗一次
1516
+ let refs = parseRefDecl(buildRefDecl({ keyTable: 'pemis', rowsNew: rows, rowsOld: vo.baseRows, picks: vo.refPicks, refRows: vo.targets }))
1517
+ if (countRefMismatch({ keyTable: 'pemis', decl: refs, refRows: vo.targets }) > 0) {
1518
+ unlockBtn()
1519
+ await vo.$dg.showCheckYes(vo.$t('saveRefChanged'), { type: 'warning' })
1520
+ return
1521
+ }
1522
+
1481
1523
  //新增列之 transient 欄位 _isNew(addItem / copyItem 標記)隨列送後端: 後端據以拒絕「標為新增但 id 已存在」之列
1482
1524
  //(同一包重送時不把剛建立之列當修改覆寫, 回 saveNewRowExists), 檢查後由 ltdtmapping 依 schema 取欄時剝除不入庫 (ADR-025)
1483
1525
 
@@ -1521,15 +1563,17 @@ export default {
1521
1563
 
1522
1564
  //4) updatePemis, 各自 catch + 旗標短路; 第 2 參 { baseline } 為載入基準(伺服端於表鎖內比對本次將寫入之列是否仍與草稿所依據之版本相同, 否則整次拒收 saveStale)
1523
1565
  let okSave = false
1566
+ let savedRes = null //伺服端回傳(成功 msg key 字串, 或 { msg, rows, others }: 寫入後之列, 鎖內新鮮讀取; 規格第 8 節)
1524
1567
  vo.saveMark.saving = true //自己之存檔請求進行中: 此期間之推播不是他人更新(changeParams 的 planDraftSync 回 'keepQuiet'); 回應(成功或失敗)一到即歸零
1525
- await vo.$fapi.updatePemis(rows, { baseline })
1526
- .then(() => {
1568
+ await vo.$fapi.updatePemis(rows, { baseline, returnRows: true, refs })
1569
+ .then((res) => {
1527
1570
  okSave = true
1571
+ savedRes = res
1528
1572
  vo.saveMark.saving = false
1529
1573
  })
1530
1574
  .catch(async (err) => {
1531
1575
  vo.saveMark.saving = false
1532
- vo.staleNotice = isDraftStale({ keys: vo.$ds.pemis.keys, rows: vo.pemis, baseline: vo.syncMark.baseline }) //失敗(含 saveStale 被拒): 請求進行中被安靜保留之推播, 此時依目前 store 重算過期提示(同 changeParams 之保留分支)
1576
+ vo.staleNotice = isDraftStale({ keyTable: 'pemis', rows: vo.pemis, baseline: vo.syncMark.baseline }) //失敗(含 saveStale 被拒): 請求進行中被安靜保留之推播, 此時依目前 store 重算過期提示(同 changeParams 之保留分支)
1533
1577
  vo.$ui.updateLoading(false) //showCheckYes 前關 loading(modal 阻斷期間避免 loading 疊在底下)
1534
1578
  await vo.$dg.showCheckYes(`${vo.$t('pemiSavePemisFail')}: ${vo.$tErr(err)}`)
1535
1579
  })
@@ -1538,6 +1582,11 @@ export default {
1538
1582
  //isModified; postSaving 自此至成功訊息框關閉後 300ms 內排除 rowsChange(見 data 註解)
1539
1583
  vo.postSaving = true
1540
1584
  vo.isModified = false
1585
+ vo.syncMark.dirty = false //watch 為非同步, 此處同步歸零: 下面 commit 引起之 changeParams 重算須視為「無草稿」而重建
1586
+
1587
+ //存檔成功後新基準取自伺服端且須立即取得(規格第 8 節; R-SAVED-ROWS-01): 以回傳之寫入後之列(鎖內新鮮讀取)更新 store, 本頁依既有流程以之重建畫面與基準;
1588
+ //其後抵達之同內容推播摘要相同, 不視為他人更新. 不以自己送出之值當新基準(clearXSS 等會改寫字串). 回傳不含列(舊伺服端、讀回失敗)時退回等推播
1589
+ applySavedRows('pemis', savedRes, (t, rs) => vo.$store.commit(vo.$store.types.UpdateTableData, { tableName: t, data: rs, source: 'savedRows' }))
1541
1590
 
1542
1591
  //alert(showCheckYes 前關 loading,避免 modal 等待期間 loading 疊在底下)
1543
1592
  vo.$ui.updateLoading(false)
@@ -280,7 +280,9 @@ import { getSavePreview } from '../js/getCascadePreview.mjs'
280
280
  import { getAddImpact, getAddConfirmText } from '../js/getAddImpact.mjs'
281
281
  import { isExternalRow } from '../js/externalRows.mjs'
282
282
  import { makeBaseline } from '../js/rowDigest.mjs'
283
- import { refsChanged, planDraftSync, isDraftStale } from '../js/draftSync.mjs'
283
+ import { refsChanged, planDraftSync, isDraftStale, hasDraftNow } from '../js/draftSync.mjs'
284
+ import { applySavedRows } from '../js/applySaved.mjs'
285
+ import { isCellEditing, bindCellEditingStopRetry } from '../js/cellEditing.mjs'
284
286
  import { fmtVars, getBannerLines, splitCheckedRows, getExternalEdits } from '../js/externalUi.mjs'
285
287
  import WIcon from 'w-component-vue/src/components/WIcon.vue'
286
288
  import WSwitch from 'w-component-vue/src/components/WSwitch.vue'
@@ -480,14 +482,14 @@ export default {
480
482
  first: vo.syncMark.refs === null || vo.syncMark.tick !== vo.syncTick, //首次載入或使用者按了「重新載入」(syncTick 遞增)
481
483
  editChanged: vo.syncMark.editable !== isEditable,
482
484
  storeChanged: refsChanged(vo.syncMark.refs, [rows]),
483
- hasDraft: () => vo.syncMark.dirty === true,
485
+ hasDraft: () => hasDraftNow({ dirty: vo.syncMark.dirty === true, editing: isCellEditing(get(vo, '$refs.rftable', null)) }), //未存修改, 或儲存格編輯器開啟中(已輸入、未按 Enter; 第 115 項, R-DRAFT-KEEP-01 規則 10)
484
486
  saving: () => vo.saveMark.saving === true, //自己之存檔請求進行中所致之推播 → 'keepQuiet'(不改過期提示)
485
487
  })
486
488
  if (act === 'rebuild') {
487
489
  vo.rebuildDraft(rows, isEditable)
488
490
  }
489
491
  else if (act === 'keep') {
490
- vo.staleNotice = isDraftStale({ keys: vo.$ds.targets.keys, rows, baseline: vo.syncMark.baseline })
492
+ vo.staleNotice = isDraftStale({ keyTable: 'targets', rows, baseline: vo.syncMark.baseline })
491
493
  }
492
494
 
493
495
  return ''
@@ -660,7 +662,7 @@ export default {
660
662
  let items = cloneDeep(rows)
661
663
 
662
664
  //baseline, 與 items 同一處由同一份來源列(items)計算: 草稿所依據之版本(F-26), 重建 items 時一併更新, 存檔時不重算
663
- let baseline = makeBaseline(items, vo.$ds.targets.keys)
665
+ let baseline = makeBaseline(items, 'targets')
664
666
 
665
667
  //save; 重建即放棄未存編輯(isModified 歸零)與過期提示, syncMark 記下本次所依據之 store 參照與編輯模式
666
668
  vo.items = items
@@ -676,6 +678,11 @@ export default {
676
678
  //firstLoading
677
679
  vo.firstLoading = false
678
680
 
681
+ //第 115 項: 重建後元件可能重建, 對新的 ag-grid 重綁「儲存格編輯結束」監聽(同一 api 只綁一次)
682
+ vo.$nextTick(() => {
683
+ vo.bindEditing()
684
+ })
685
+
679
686
  },
680
687
 
681
688
  curRefs: function() {
@@ -684,6 +691,22 @@ export default {
684
691
  return [vo.targets]
685
692
  },
686
693
 
694
+ bindEditing: function() {
695
+ //第 115 項(R-DRAFT-KEEP-01 規則 10): 儲存格編輯器開啟中視為有草稿(changeParams 的 hasDraft 以 ag-grid 的 getEditingCells 判定); 編輯結束(提交、Esc、失焦)時補做保留期間跳過之重建
696
+ let vo = this
697
+ bindCellEditingStopRetry(() => get(vo, '$refs.rftable', null), () => {
698
+ vo.afterEditing()
699
+ })
700
+ },
701
+
702
+ afterEditing: function() {
703
+ //儲存格編輯結束: 無未存修改(isModified 為假; 提交者此刻已由 rowsChange 設為真)而 store 於編輯期間已變新者, 補上一次重建; 有未存修改者維持保留(isModified 的 watch 於儲存或放棄後補重建)
704
+ let vo = this
705
+ if (vo.isModified !== true && vo.syncMark.refs !== null && refsChanged(vo.syncMark.refs, vo.curRefs())) {
706
+ vo.reloadDraft()
707
+ }
708
+ },
709
+
687
710
  reloadDraft: function() {
688
711
  //「重新載入」鈕(資料已被更新橫幅): 使用者明確放棄未存編輯, 以目前 store 重建草稿與基準. 不在此直接呼叫 rebuildDraft: 遞增 syncTick 使 changeParams 重算並於 computed 內重建,
689
712
  //genOpt 之反應式依賴(語系等)才會重新收集(保留草稿期間 changeParams 未呼叫 genOpt, 依賴已收縮; 於 computed 外重建不會恢復, 重新載入後再切換語系表頭將停留舊值, 外審 sol 第 4 點)
@@ -916,6 +939,7 @@ export default {
916
939
  r.id = _r.id //使用新建方式預先產生id避免重複
917
940
  r.id = vo.$s.getNameNew(rows, 'id', nameOld, vo.$t('targetCopyNameNew'))
918
941
  r.from = '' //複製出之列為後台新增列, 不繼承來源(來源非空即外部列, 後台新增列 from 須為空, 否則伺服端 saveFromNotAllowed; 不論是否啟用外部列保護)
942
+ r.srcKey = '' //來源鍵 srcKey 與 from 同為身分欄(perm-run03 範圍二, R-IMPORT-01): 後台列之 srcKey 恆空, 不繼承外部列之來源鍵(否則伺服端 saveFromNotAllowed)
919
943
  r.userId = `{${vo.$t('targetAddIdNew')}}`
920
944
  r.timeCreate = `{${vo.$t('targetAddIdNew')}}`
921
945
  r.userIdUpdate = `{${vo.$t('targetAddIdNew')}}`
@@ -1090,15 +1114,17 @@ export default {
1090
1114
 
1091
1115
  //4) updateTargets, 各自 catch + 旗標短路; 第 2 參 { baseline } 為載入基準(伺服端於表鎖內比對本次將寫入之列是否仍與草稿所依據之版本相同, 否則整次拒收 saveStale)
1092
1116
  let okSave = false
1117
+ let savedRes = null //伺服端回傳(成功 msg key 字串, 或 { msg, rows, others }: 寫入後之列, 鎖內新鮮讀取; 規格第 8 節)
1093
1118
  vo.saveMark.saving = true //自己之存檔請求進行中: 此期間之推播不是他人更新(changeParams 的 planDraftSync 回 'keepQuiet'); 回應(成功或失敗)一到即歸零
1094
- await vo.$fapi.updateTargets(rowsSend, { baseline })
1095
- .then(() => {
1119
+ await vo.$fapi.updateTargets(rowsSend, { baseline, returnRows: true })
1120
+ .then((res) => {
1096
1121
  okSave = true
1122
+ savedRes = res
1097
1123
  vo.saveMark.saving = false
1098
1124
  })
1099
1125
  .catch(async (err) => {
1100
1126
  vo.saveMark.saving = false
1101
- vo.staleNotice = isDraftStale({ keys: vo.$ds.targets.keys, rows: vo.targets, baseline: vo.syncMark.baseline }) //失敗(含 saveStale 被拒): 請求進行中被安靜保留之推播, 此時依目前 store 重算過期提示(同 changeParams 之保留分支)
1127
+ vo.staleNotice = isDraftStale({ keyTable: 'targets', rows: vo.targets, baseline: vo.syncMark.baseline }) //失敗(含 saveStale 被拒): 請求進行中被安靜保留之推播, 此時依目前 store 重算過期提示(同 changeParams 之保留分支)
1102
1128
  vo.$ui.updateLoading(false) //showCheckYes 前關 loading(modal 阻斷期間避免 loading 疊在底下)
1103
1129
  await vo.$dg.showCheckYes(`${vo.$t('targetSaveTargetsFail')}: ${vo.$tErr(err)}`)
1104
1130
  })
@@ -1106,6 +1132,11 @@ export default {
1106
1132
 
1107
1133
  //isModified
1108
1134
  vo.isModified = false
1135
+ vo.syncMark.dirty = false //watch 為非同步, 此處同步歸零: 下面 commit 引起之 changeParams 重算須視為「無草稿」而重建
1136
+
1137
+ //存檔成功後新基準取自伺服端且須立即取得(規格第 8 節; R-SAVED-ROWS-01): 以回傳之寫入後之列(鎖內新鮮讀取)更新 store, 本頁依既有流程以之重建畫面與基準;
1138
+ //其後抵達之同內容推播摘要相同, 不視為他人更新. 不以自己送出之值當新基準(clearXSS 等會改寫字串). 回傳不含列(舊伺服端、讀回失敗)時退回等推播
1139
+ applySavedRows('targets', savedRes, (t, rs) => vo.$store.commit(vo.$store.types.UpdateTableData, { tableName: t, data: rs, source: 'savedRows' }))
1109
1140
 
1110
1141
  //alert(showCheckYes 前關 loading,避免 modal 等待期間 loading 疊在底下)
1111
1142
  vo.$ui.updateLoading(false)
@@ -311,7 +311,10 @@ import { getRefWarns, getRefWarnText, fmtWarn } from '../js/getAuthWarn.mjs'
311
311
  import { getNameErrs, mapNameKp } from '../js/getNameErrs.mjs'
312
312
  import { isExternalRow } from '../js/externalRows.mjs'
313
313
  import { makeBaseline } from '../js/rowDigest.mjs'
314
- import { refsChanged, planDraftSync, isDraftStale } from '../js/draftSync.mjs'
314
+ import { refsChanged, planDraftSync, isDraftStale, hasDraftNow } from '../js/draftSync.mjs'
315
+ import { applySavedRows } from '../js/applySaved.mjs'
316
+ import { snapRefIds, recordPicks, buildRefDecl, parseRefDecl, countRefMismatch } from '../js/refIdentity.mjs'
317
+ import { isCellEditing, bindCellEditingStopRetry } from '../js/cellEditing.mjs'
315
318
  import { fmtVars, getBannerLines, splitCheckedRows, getExternalEdits } from '../js/externalUi.mjs'
316
319
  import WIcon from 'w-component-vue/src/components/WIcon.vue'
317
320
  import WSwitch from 'w-component-vue/src/components/WSwitch.vue'
@@ -421,6 +424,8 @@ export default {
421
424
  this.syncMark = { refs: null, editable: undefined, dirty: false, baseline: [], tick: 0 }
422
425
  //saveMark(非響應式, 同 syncMark 之理由): saving 為「自己之存檔請求進行中」(送出前設真, 回應〔成功或失敗〕一到歸零)——此期間伺服端寫入所致之推播若先於回應抵達, 不是他人更新(planDraftSync 回 'keepQuiet': 保留草稿但不顯示過期橫幅; run12)
423
426
  this.saveMark = { saving: false }
427
+ //refPicks(非響應式): 草稿內「使用者在編輯器選取的啟用引用項(群組名稱) → 選取當時對應之群組 id 陣列」(src/js/refIdentity.mjs; R-REF-IDENTITY-01), 存檔時據以組引用身分宣告 opt.refs; 重建草稿時歸零
428
+ this.refPicks = new Map()
424
429
  },
425
430
  mounted: function() {
426
431
  // console.log('mounted')
@@ -539,14 +544,14 @@ export default {
539
544
  first: vo.syncMark.refs === null || vo.syncMark.tick !== vo.syncTick, //首次載入或使用者按了「重新載入」(syncTick 遞增)
540
545
  editChanged: vo.syncMark.editable !== isEditable,
541
546
  storeChanged: refsChanged(vo.syncMark.refs, [rows]),
542
- hasDraft: () => vo.syncMark.dirty === true,
547
+ hasDraft: () => hasDraftNow({ dirty: vo.syncMark.dirty === true, editing: isCellEditing(get(vo, '$refs.rftable', null)) }), //未存修改, 或儲存格編輯器開啟中(已輸入、未按 Enter; 第 115 項, R-DRAFT-KEEP-01 規則 10)
543
548
  saving: () => vo.saveMark.saving === true, //自己之存檔請求進行中所致之推播 → 'keepQuiet'(不改過期提示)
544
549
  })
545
550
  if (act === 'rebuild') {
546
551
  vo.rebuildDraft(rows, isEditable)
547
552
  }
548
553
  else if (act === 'keep') {
549
- vo.staleNotice = isDraftStale({ keys: vo.$ds.users.keys, rows, baseline: vo.syncMark.baseline })
554
+ vo.staleNotice = isDraftStale({ keyTable: 'users', rows, baseline: vo.syncMark.baseline })
550
555
  }
551
556
 
552
557
  return ''
@@ -770,7 +775,7 @@ export default {
770
775
  let items = cloneDeep(rows)
771
776
 
772
777
  //baseline, 與 items 同一處由同一份來源列(items)計算: 草稿所依據之版本(F-26), 重建 items 時一併更新, 存檔時不重算
773
- let baseline = makeBaseline(items, vo.$ds.users.keys)
778
+ let baseline = makeBaseline(items, 'users')
774
779
 
775
780
  //save; 重建即放棄未存編輯(isModified 歸零)與過期提示, syncMark 記下本次所依據之 store 參照與編輯模式
776
781
  vo.items = items
@@ -779,6 +784,7 @@ export default {
779
784
  vo.isModified = false
780
785
  vo.staleNotice = false
781
786
  vo.syncMark = { refs: [rows], editable: isEditable, dirty: false, baseline, tick: vo.syncTick }
787
+ vo.refPicks = new Map() //重建即放棄未存編輯, 其選取記錄一併歸零
782
788
 
783
789
  //genOpt
784
790
  vo.genOpt({ isEditable })
@@ -786,6 +792,11 @@ export default {
786
792
  //firstLoading
787
793
  vo.firstLoading = false
788
794
 
795
+ //第 115 項: 重建後元件可能重建, 對新的 ag-grid 重綁「儲存格編輯結束」監聽(同一 api 只綁一次)
796
+ vo.$nextTick(() => {
797
+ vo.bindEditing()
798
+ })
799
+
789
800
  },
790
801
 
791
802
  curRefs: function() {
@@ -794,6 +805,22 @@ export default {
794
805
  return [vo.users]
795
806
  },
796
807
 
808
+ bindEditing: function() {
809
+ //第 115 項(R-DRAFT-KEEP-01 規則 10): 儲存格編輯器開啟中視為有草稿(changeParams 的 hasDraft 以 ag-grid 的 getEditingCells 判定); 編輯結束(提交、Esc、失焦)時補做保留期間跳過之重建
810
+ let vo = this
811
+ bindCellEditingStopRetry(() => get(vo, '$refs.rftable', null), () => {
812
+ vo.afterEditing()
813
+ })
814
+ },
815
+
816
+ afterEditing: function() {
817
+ //儲存格編輯結束: 無未存修改(isModified 為假; 提交者此刻已由 rowsChange 設為真)而 store 於編輯期間已變新者, 補上一次重建; 有未存修改者維持保留(isModified 的 watch 於儲存或放棄後補重建)
818
+ let vo = this
819
+ if (vo.isModified !== true && vo.syncMark.refs !== null && refsChanged(vo.syncMark.refs, vo.curRefs())) {
820
+ vo.reloadDraft()
821
+ }
822
+ },
823
+
797
824
  reloadDraft: function() {
798
825
  //「重新載入」鈕(資料已被更新橫幅): 使用者明確放棄未存編輯, 以目前 store 重建草稿與基準. 不在此直接呼叫 rebuildDraft: 遞增 syncTick 使 changeParams 重算並於 computed 內重建,
799
826
  //genOpt 之反應式依賴(語系等)才會重新收集(保留草稿期間 changeParams 未呼叫 genOpt, 依賴已收縮; 於 computed 外重建不會恢復, 重新載入後再切換語系表頭將停留舊值, 外審 sol 第 4 點)
@@ -1161,6 +1188,7 @@ export default {
1161
1188
  let _r = vo.$ds.users.funNew()
1162
1189
  r.id = _r.id //使用新建方式預先產生id避免重複
1163
1190
  r.from = '' //複製出之列為後台新增列, 不繼承來源(來源非空即外部列, 後台新增列 from 須為空, 否則伺服端 saveFromNotAllowed; 不論是否啟用外部列保護)
1191
+ r.srcKey = '' //來源鍵 srcKey 與 from 同為身分欄(perm-run03 範圍二, R-IMPORT-01): 後台列之 srcKey 恆空, 不繼承外部列之來源鍵(否則伺服端 saveFromNotAllowed)
1164
1192
  r.name = vo.$s.getNameNew(rows, 'name', nameOld, vo.$t('userCopyNameNew'))
1165
1193
  r.userId = `{${vo.$t('userAddIdNew')}}`
1166
1194
  r.timeCreate = `{${vo.$t('userAddIdNew')}}`
@@ -1334,6 +1362,8 @@ export default {
1334
1362
  //編輯器回傳之字串仍是舊版內容, 回填後存檔會以「新基準」證明過期內容新鮮(還原已被撤銷之授權, 或繞過同表別列已被改動之整表比對); 故回填前比對「草稿現所依據之那份表」與開框時之基準, 有異即放棄回填並提示重新載入.
1335
1363
  //父頁已有未存編輯時推播只保留草稿與其基準(baseRows 不變), 此比對不誤擋, 存檔時由伺服端表級比對拒收
1336
1364
  let baseline0 = cloneDeep(vo.baseline)
1365
+ //refSnap: 開啟編輯器當下之「群組名稱 → 群組 id 陣列」快照(R-REF-IDENTITY-01): 編輯器列出的群組即此刻 store 之群組, 使用者選取的是這些實體; 回填時據以記下新增之啟用引用項選取當時對應的 id, 存檔時送伺服端驗證其仍是同一實體(被刪除後同名重建〔id 不同〕即拒收)
1366
+ let refSnap = snapRefIds('users', vo.grups)
1337
1367
  //showVeCgrups
1338
1368
  vo.$dg.showVeCgrups({
1339
1369
  isEditable: vo.isEditable,
@@ -1356,11 +1386,14 @@ export default {
1356
1386
  }
1357
1387
 
1358
1388
  //草稿所依據之表於編輯器開啟後已被改動(整表指紋與開框時之基準不同; 僅 order/伺服器管理欄不同不算): 放棄回填並提示重新載入(同伺服端 saveStale 之語意)
1359
- if (isDraftStale({ keys: vo.$ds.users.keys, rows: vo.baseRows, baseline: baseline0 })) {
1389
+ if (isDraftStale({ keyTable: 'users', rows: vo.baseRows, baseline: baseline0 })) {
1360
1390
  vo.$alert(`${vo.$t('saveStale')}`, { type: 'warning' })
1361
1391
  return
1362
1392
  }
1363
1393
 
1394
+ //選取記錄: 本次新增之啟用引用項(群組名稱)在編輯器開啟當下對應之群組 id(查無記空陣列)
1395
+ recordPicks({ keyTable: 'users', strOld: get(vo, `opt.rows[${krNow}].cgrups`, ''), strNew: cgrups, snap: refSnap, picks: vo.refPicks })
1396
+
1364
1397
  //set
1365
1398
  set(vo, `opt.rows[${krNow}].cgrups`, cgrups)
1366
1399
  // console.log('vo.opt.rows[krNow]', cloneDeep(vo.opt.rows[krNow]))
@@ -1417,6 +1450,15 @@ export default {
1417
1450
  return
1418
1451
  }
1419
1452
 
1453
+ //正向引用身分(perm-run02 範圍一; R-REF-IDENTITY-01; 使用者 2026-10-23 裁示前後端都要做): 本次新增之啟用引用項(群組名稱)所指之群組,須仍是使用者選取當時的同一實體(同 id);
1454
+ //被刪除或刪除後以同名重建(id 不同)即不送出(零寫入)並提示重新載入,不得把授權寫到同名新群組. 與伺服端 checkRefIdentity 同一判準(src/js/refIdentity.mjs); 宣告 refs 隨存檔送伺服端於群組表鎖內以新鮮讀取再驗一次
1455
+ let refs = parseRefDecl(buildRefDecl({ keyTable: 'users', rowsNew: rows, rowsOld: vo.baseRows, picks: vo.refPicks, refRows: vo.grups }))
1456
+ if (countRefMismatch({ keyTable: 'users', decl: refs, refRows: vo.grups }) > 0) {
1457
+ unlockBtn()
1458
+ await vo.$dg.showCheckYes(vo.$t('saveRefChanged'), { type: 'warning' })
1459
+ return
1460
+ }
1461
+
1420
1462
  //新增列之 transient 欄位 _isNew(addItem / copyItem 標記)隨列送後端: 後端據以拒絕「標為新增但 id 已存在」之列
1421
1463
  //(同一包重送時不把剛建立之列當修改覆寫, 回 saveNewRowExists), 檢查後由 ltdtmapping 依 schema 取欄時剝除不入庫 (ADR-025);
1422
1464
  //modeEditUsers='for:grups' 時後端不採納新增列(mergePickKeysOnly), 亦不檢查
@@ -1426,15 +1468,17 @@ export default {
1426
1468
 
1427
1469
  //3) updateUsers, 各自 catch + 旗標短路; 第 2 參 { baseline } 為載入基準(伺服端於表鎖內比對本次將寫入之列是否仍與草稿所依據之版本相同, 否則整次拒收 saveStale)
1428
1470
  let okSave = false
1471
+ let savedRes = null //伺服端回傳(成功 msg key 字串, 或 { msg, rows, others }: 寫入後之列, 鎖內新鮮讀取; 規格第 8 節)
1429
1472
  vo.saveMark.saving = true //自己之存檔請求進行中: 此期間之推播不是他人更新(changeParams 的 planDraftSync 回 'keepQuiet'); 回應(成功或失敗)一到即歸零
1430
- await vo.$fapi.updateUsers(rows, { baseline })
1431
- .then(() => {
1473
+ await vo.$fapi.updateUsers(rows, { baseline, returnRows: true, refs })
1474
+ .then((res) => {
1432
1475
  okSave = true
1476
+ savedRes = res
1433
1477
  vo.saveMark.saving = false
1434
1478
  })
1435
1479
  .catch(async (err) => {
1436
1480
  vo.saveMark.saving = false
1437
- vo.staleNotice = isDraftStale({ keys: vo.$ds.users.keys, rows: vo.users, baseline: vo.syncMark.baseline }) //失敗(含 saveStale 被拒): 請求進行中被安靜保留之推播, 此時依目前 store 重算過期提示(同 changeParams 之保留分支)
1481
+ vo.staleNotice = isDraftStale({ keyTable: 'users', rows: vo.users, baseline: vo.syncMark.baseline }) //失敗(含 saveStale 被拒): 請求進行中被安靜保留之推播, 此時依目前 store 重算過期提示(同 changeParams 之保留分支)
1438
1482
  vo.$ui.updateLoading(false) //showCheckYes 前關 loading(modal 阻斷期間避免 loading 疊在底下)
1439
1483
  await vo.$dg.showCheckYes(`${vo.$t('userSaveUsersFail')}: ${vo.$tErr(err)}`)
1440
1484
  })
@@ -1442,6 +1486,11 @@ export default {
1442
1486
 
1443
1487
  //isModified
1444
1488
  vo.isModified = false
1489
+ vo.syncMark.dirty = false //watch 為非同步, 此處同步歸零: 下面 commit 引起之 changeParams 重算須視為「無草稿」而重建
1490
+
1491
+ //存檔成功後新基準取自伺服端且須立即取得(規格第 8 節; R-SAVED-ROWS-01): 以回傳之寫入後之列(鎖內新鮮讀取)更新 store, 本頁依既有流程以之重建畫面與基準;
1492
+ //其後抵達之同內容推播摘要相同, 不視為他人更新. 不以自己送出之值當新基準(clearXSS 等會改寫字串). 回傳不含列(舊伺服端、讀回失敗)時退回等推播
1493
+ applySavedRows('users', savedRes, (t, rs) => vo.$store.commit(vo.$store.types.UpdateTableData, { tableName: t, data: rs, source: 'savedRows' }))
1445
1494
 
1446
1495
  //alert(showCheckYes 前關 loading,避免 modal 等待期間 loading 疊在底下)
1447
1496
  vo.$ui.updateLoading(false)