w-web-perm 1.0.83 → 1.0.85

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 (86) hide show
  1. package/dist/css/{chunk-vendors.034f7a87.css → chunk-vendors.18034562.css} +1 -1
  2. package/dist/index.tmp +1 -1
  3. package/dist/js/app-legacy.aaaa3681.js +1 -0
  4. package/dist/js/app.aaaa3681.js +1 -0
  5. package/dist/js/{chunk-vendors-legacy.d0596078.js → chunk-vendors-legacy.7b6ae0b0.js} +3 -3
  6. package/dist/js/{chunk-vendors.d0596078.js → chunk-vendors.7b6ae0b0.js} +3 -3
  7. package/dist/w-web-perm.umd.js +2 -2
  8. package/dist/w-web-perm.umd.js.map +1 -1
  9. package/docs/WWebPerm.html +1 -1
  10. package/docs/global.html +1 -1
  11. package/docs/index.html +1 -1
  12. package/docs/server_WWebPerm.mjs.html +1 -1
  13. package/docs/server_procCore.mjs.html +1 -1
  14. package/docs/src_fetchJson.mjs.html +1 -1
  15. package/package.json +2 -2
  16. package/server/procLang.mjs +12 -4
  17. package/src/components/LayoutContentGrups.vue +6 -6
  18. package/src/components/LayoutContentPemis.vue +6 -6
  19. package/src/components/LayoutContentTargets.vue +2 -2
  20. package/src/components/LayoutContentUsers.vue +5 -3
  21. package/src/components/ModeSelectChip.vue +182 -0
  22. package/src/components/RelationChip.vue +114 -0
  23. package/src/components/RelationChips.vue +375 -0
  24. package/src/components/VeCgrups.vue +23 -20
  25. package/src/components/VeCpemis.vue +21 -20
  26. package/src/components/VeCrules.vue +8 -1
  27. package/src/components/VeGrupBlngUsers.vue +40 -51
  28. package/src/components/VePemiBlngGrups.vue +27 -41
  29. package/tableTags-web-perm.json +1 -1
  30. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/README.md +58 -0
  31. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/server_procLang.mjs.diff +31 -0
  32. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_App.vue.diff +19 -0
  33. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_LayoutContentGrups.vue.diff +40 -0
  34. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_LayoutContentPemis.vue.diff +40 -0
  35. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_ModeSelectChip.vue.new.diff +124 -0
  36. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_RelationChip.vue.new.diff +116 -0
  37. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_RelationChips.vue.new.diff +152 -0
  38. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_VeCgrups.vue.diff +76 -0
  39. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_VeCpemis.vue.diff +76 -0
  40. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_VeCrules.vue.diff +14 -0
  41. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_VeGrupBlngUsers.vue.diff +121 -0
  42. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_VePemiBlngGrups.vue.diff +121 -0
  43. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/server/procLang.mjs +1049 -0
  44. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/App.vue +204 -0
  45. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/LayoutContentGrups.vue +1260 -0
  46. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/LayoutContentPemis.vue +1262 -0
  47. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/ModeSelectChip.vue +121 -0
  48. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/RelationChip.vue +113 -0
  49. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/RelationChips.vue +149 -0
  50. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/VeCgrups.vue +943 -0
  51. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/VeCpemis.vue +943 -0
  52. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/VeCrules.vue +848 -0
  53. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/VeGrupBlngUsers.vue +1157 -0
  54. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/VePemiBlngGrups.vue +1157 -0
  55. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/A-/347/256/241/347/220/206/346/254/212/351/231/220/344/270/273/350/241/250.png +0 -0
  56. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/A-/347/256/241/347/220/206/346/254/212/351/231/220/347/276/244/347/265/204/344/270/273/350/241/250.png +0 -0
  57. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/B1-/347/267/250/350/274/257/346/211/200/345/261/254/346/254/212/351/231/220/347/276/244/347/265/204-/345/260/215/350/251/261/346/241/206.png +0 -0
  58. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/B2-/347/267/250/350/274/257/346/211/200/345/261/254/346/254/212/351/231/220/347/276/244/347/265/204-hover/346/234/254/351/240/205chip.png +0 -0
  59. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/B3-/347/267/250/350/274/257/346/211/200/345/261/254/346/254/212/351/231/220/347/276/244/347/265/204-/345/261/225/351/226/213/346/234/254/351/240/205chip/346/270/205/345/226/256.png +0 -0
  60. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/B4-/347/267/250/350/274/257/346/211/200/345/261/254/346/254/212/351/231/220/347/276/244/347/265/204-/346/234/254/351/240/205chip/345/267/262/347/202/272AND.png +0 -0
  61. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/B5-/347/267/250/350/274/257/346/211/200/345/261/254/346/254/212/351/231/220/347/276/244/347/265/204-/345/261/225/351/226/213/345/205/250/351/203/250/346/265/256/345/261/244.png +0 -0
  62. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/B6-/347/267/250/350/274/257/346/211/200/345/261/254/346/254/212/351/231/220/347/276/244/347/265/204-/345/213/276/351/201/270/345/276/214/346/234/254/351/240/205chip/345/234/250/351/246/226/344/275/215.png +0 -0
  63. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/C1-/347/267/250/350/274/257/344/275/277/347/224/250/346/254/212/351/231/220-/345/260/215/350/251/261/346/241/206.png +0 -0
  64. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/C2-/347/267/250/350/274/257/344/275/277/347/224/250/346/254/212/351/231/220-hover/345/267/262/345/213/276/351/201/270/345/210/227/346/216/247/345/210/266/351/240/205.png +0 -0
  65. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/C3-/347/267/250/350/274/257/344/275/277/347/224/250/346/254/212/351/231/220-/345/261/225/351/226/213/346/270/205/345/226/256.png +0 -0
  66. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/D1-/347/267/250/350/274/257/346/211/200/345/261/254/344/275/277/347/224/250/350/200/205-/345/260/215/350/251/261/346/241/206.png +0 -0
  67. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/E1-/347/267/250/350/274/257/344/275/277/347/224/250/346/254/212/351/231/220/347/276/244/347/265/204-hover.png +0 -0
  68. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/E2-/347/267/250/350/274/257/344/275/277/347/224/250/346/254/212/351/231/220/347/276/244/347/265/204-/345/261/225/351/226/213/346/270/205/345/226/256.png +0 -0
  69. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/F1-/345/261/225/347/244/272/346/211/200/345/261/254/346/254/212/351/231/220/347/276/244/347/265/204-/345/224/257/350/256/200/346/205/213.png +0 -0
  70. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/F2-/345/261/225/347/244/272/346/211/200/345/261/254/346/254/212/351/231/220/347/276/244/347/265/204-/345/261/225/351/226/213/345/205/250/351/203/250.png +0 -0
  71. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/G1-zoom-/347/267/250/350/274/257/346/211/200/345/261/254/346/254/212/351/231/220/347/276/244/347/265/204-/345/211/215/345/205/255/345/210/227.png +0 -0
  72. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/G2-zoom-/347/267/250/350/274/257/346/211/200/345/261/254/346/254/212/351/231/220/347/276/244/347/265/204-hover/346/234/254/351/240/205chip.png +0 -0
  73. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/G3-zoom-/347/267/250/350/274/257/346/211/200/345/261/254/346/254/212/351/231/220/347/276/244/347/265/204-/345/261/225/351/226/213/346/270/205/345/226/256.png +0 -0
  74. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/346/210/252/345/234/226/verify.json +390 -0
  75. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/350/205/263/346/234/254/build.log +93 -0
  76. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/350/205/263/346/234/254/lint.log +23 -0
  77. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/350/205/263/346/234/254/zz_perm_probe.mjs +321 -0
  78. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/350/205/263/346/234/254/zz_perm_zoom.mjs +173 -0
  79. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231//351/251/227/350/255/211/350/205/263/346/234/254/zz_wwp_verify.mjs +330 -0
  80. package//345/273/272/350/255/260w-aggrid-vue/350/252/277/346/225/264.md +193 -0
  81. package//345/273/272/350/255/260w-component-vue/350/210/207wsemi/350/252/277/346/225/264.md +76 -0
  82. package//345/273/272/350/255/260w-web-perm/350/252/277/346/225/264.md +244 -0
  83. package//346/226/275/345/267/245/345/226/256_/345/273/272/350/255/260w-web-perm/350/252/277/346/225/264.md +166 -0
  84. package/dist/index.html +0 -4
  85. package/dist/js/app-legacy.04fa72c9.js +0 -1
  86. package/dist/js/app.04fa72c9.js +0 -1
@@ -0,0 +1,321 @@
1
+ // 探測權限管理系統各關聯彈窗之 chip 間距、合併模式下拉幾何與裁切(經 w-screenctl 7000; 只量測不改資料)
2
+ // 用法: node tmp/zz_perm_probe.mjs
3
+ // 產物: tmp/perm_probe/*.png(截圖) 與 stdout(量測 JSON)
4
+ import fs from 'fs'
5
+ import path from 'path'
6
+
7
+ let HOST = 'http://127.0.0.1:7000'
8
+ let SSO = 'http://localhost:11007'
9
+ let PERM = 'http://localhost:11006'
10
+ let fdOut = path.resolve('./tmp/perm_probe')
11
+ fs.mkdirSync(fdOut, { recursive: true })
12
+ let MDI_CLOSE = 'M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z'
13
+
14
+ let sleep = (ms) => new Promise((r) => setTimeout(r, ms))
15
+ async function api(p, body, method = 'POST') {
16
+ let r = await fetch(HOST + p, { method, headers: { 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined })
17
+ return r.json()
18
+ }
19
+ async function ev(script) {
20
+ let r = await api('/chrome/evaluate', { script })
21
+ if (!r.ok) {
22
+ throw new Error(`evaluate 失敗: ${r.error} :: ${script.slice(0, 160)}`)
23
+ }
24
+ return r.result
25
+ }
26
+ async function waitFor(script, timeout = 60000, poll = 300) {
27
+ let t0 = Date.now()
28
+ while (Date.now() - t0 < timeout) {
29
+ let v = await ev(script)
30
+ if (v) {
31
+ return v
32
+ }
33
+ await sleep(poll)
34
+ }
35
+ throw new Error(`waitFor 逾時: ${script.slice(0, 120)}`)
36
+ }
37
+ async function click(x, y) {
38
+ let r = await api('/chrome/mouse/click', { x: Math.round(x), y: Math.round(y) })
39
+ if (!r.ok) {
40
+ throw new Error(`click 失敗: ${r.error}`)
41
+ }
42
+ await sleep(400)
43
+ }
44
+ async function type(text) {
45
+ let r = await api('/chrome/keyboard/type', { text })
46
+ if (!r.ok) {
47
+ throw new Error(`type 失敗: ${r.error}`)
48
+ }
49
+ }
50
+ async function key(keys) {
51
+ let r = await api('/chrome/keyboard/key', { keys })
52
+ if (!r.ok) {
53
+ throw new Error(`key 失敗: ${r.error}`)
54
+ }
55
+ }
56
+ let nShot = 0
57
+ async function shot(name) {
58
+ let r = await api('/chrome/screenshot', {})
59
+ if (!r.ok) {
60
+ throw new Error(`screenshot 失敗: ${r.error}`)
61
+ }
62
+ nShot += 1
63
+ let fp = path.join(fdOut, `${String(nShot).padStart(2, '0')}-${name}.png`)
64
+ fs.writeFileSync(fp, Buffer.from(r.image, 'base64'))
65
+ console.log(`[shot] ${fp}`)
66
+ }
67
+ let out = {}
68
+ function log(k, v) {
69
+ out[k] = v
70
+ console.log(`\n=== ${k} ===\n${JSON.stringify(v, null, 1)}`)
71
+ }
72
+
73
+ //頁面內共用函式(以字串注入, 每次 evaluate 都重新定義)
74
+ let H = `
75
+ let rc = (el) => { let b = el.getBoundingClientRect(); return { x: +b.left.toFixed(1), y: +b.top.toFixed(1), w: +b.width.toFixed(1), h: +b.height.toFixed(1), r: +b.right.toFixed(1), btm: +b.bottom.toFixed(1) } };
76
+ let leaf = (t, pred) => [...document.querySelectorAll('*')].filter((el) => el.children.length === 0 && (el.innerText || '').trim() === t && (!pred || pred(el)));
77
+ let cs = (el, ks) => { let s = getComputedStyle(el); let o = {}; ks.forEach((k) => { o[k] = s.getPropertyValue(k) }); return o };
78
+ let gridOf = (headText) => { let hs = [...document.querySelectorAll('.ag-header-cell-text')].filter((e) => e.textContent.trim() === headText); return hs.length ? hs[hs.length - 1].closest('.ag-root-wrapper') : null };
79
+ let heads = (g) => [...g.querySelectorAll('.ag-header-cell-text')].map((e) => e.textContent.trim());
80
+ let rowsOf = (g) => [...g.querySelectorAll('.ag-center-cols-container .ag-row')].sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
81
+ let rowByText = (g, t) => { let hit = [...g.querySelectorAll('.ag-row')].find((r) => (r.innerText || '').includes(t)); if (!hit) return null; let idx = hit.getAttribute('row-index'); return g.querySelector('.ag-center-cols-container .ag-row[row-index="' + idx + '"]') || hit };
82
+ `
83
+
84
+ async function login() {
85
+ let r = await api('/chrome/open', { url: `${SSO}/?view=user`, mode: 'replace', viewport: { width: 1600, height: 900 }, opt: { headless: true } })
86
+ console.log('[open]', JSON.stringify(r))
87
+ //共用 user data 內可能已有登入態(直接落在使用者資訊頁), 兩種情形都接受
88
+ let st = await waitFor(`(() => { let ins = [...document.querySelectorAll('input')].filter((i) => i.getBoundingClientRect().width > 0); if (ins.length >= 2) return 'form'; let t = document.body.innerText || ''; if (t.includes('使用者資訊') && t.includes('登出')) return 'user'; return '' })()`)
89
+ console.log('[sso state]', st)
90
+ if (st === 'form') {
91
+ await sleep(600)
92
+ let ins = await ev(`(() => { ${H} return [...document.querySelectorAll('input')].filter((i) => i.getBoundingClientRect().width > 0).slice(0, 2).map(rc) })()`)
93
+ await click(ins[0].x + ins[0].w / 2, ins[0].y + ins[0].h / 2)
94
+ await type('admin')
95
+ await click(ins[1].x + ins[1].w / 2, ins[1].y + ins[1].h / 2)
96
+ await type('admin@example.com')
97
+ await key('Enter')
98
+ await waitFor(`(document.body.innerText.includes('使用者資訊') && document.body.innerText.includes('登出'))`)
99
+ }
100
+ let token = await ev(`localStorage.getItem('rddmanager_main:userToken')`)
101
+ if (!token) {
102
+ throw new Error('取不到主系統權杖')
103
+ }
104
+ let r2 = await api('/chrome/navigate', { url: `${PERM}/?token=${token}` })
105
+ console.log('[navigate perm]', JSON.stringify(r2))
106
+ await waitFor(`(() => { ${H} let ls = leaf('管理使用者'); return ls.some((el) => { let b = el.getBoundingClientRect(); return b.width > 0 && b.left >= 0 && b.left < 200 }) })()`, 90000)
107
+ await waitFor(`(!document.body.innerText.includes('等待數據中'))`, 90000)
108
+ await sleep(1500)
109
+ }
110
+
111
+ async function gotoNav(t) {
112
+ let r = await ev(`(() => { ${H} let ls = leaf('${t}').filter((el) => el.getBoundingClientRect().left < 200); return ls.length ? rc(ls[0]) : null })()`)
113
+ if (!r) {
114
+ throw new Error(`找不到導覽項 ${t}`)
115
+ }
116
+ await click(r.x + r.w / 2, r.y + r.h / 2)
117
+ await waitFor(`(() => { ${H} return leaf('${t}').some((el) => el.getBoundingClientRect().left > 200) })()`)
118
+ await waitFor(`(!document.body.innerText.includes('等待數據中'))`, 90000)
119
+ await waitFor(`(document.querySelectorAll('.ag-center-cols-container .ag-row').length > 0)`)
120
+ await sleep(1200)
121
+ }
122
+
123
+ //主表: 表頭順序與關聯欄按鈕
124
+ async function probeMainTable(tag, nameHead) {
125
+ let r = await ev(`(() => { ${H} let g = gridOf('${nameHead}'); return { heads: heads(g), nRows: rowsOf(g).length, rowH: rowsOf(g)[0] ? rc(rowsOf(g)[0]).h : null } })()`)
126
+ log(`${tag}.main.heads`, r)
127
+ return r
128
+ }
129
+
130
+ //於主表點某列某欄之按鈕(按鈕文字含 btnText)
131
+ async function openDialogFromRow(rowText, btnText, dialogTitle) {
132
+ let r = await ev(`(() => { ${H} let g = gridOf('名稱'); let row = rowByText(g, '${rowText}'); if (!row) return null; let bs = [...row.querySelectorAll('button')].filter((b) => (b.innerText || '').includes('${btnText}')); return bs.length ? { btn: rc(bs[0]), txt: bs[0].innerText } : null })()`)
133
+ if (!r) {
134
+ throw new Error(`找不到列 ${rowText} 之按鈕 ${btnText}`)
135
+ }
136
+ console.log('[open dialog]', rowText, r.txt)
137
+ await click(r.btn.x + r.btn.w / 2, r.btn.y + r.btn.h / 2)
138
+ await waitFor(`(() => { ${H} return leaf('${dialogTitle}').some((el) => el.getBoundingClientRect().width > 0) })()`)
139
+ await waitFor(`(() => { ${H} let hs = [...document.querySelectorAll('.ag-root-wrapper')]; return hs.length >= 2 && hs[hs.length - 1].querySelectorAll('.ag-center-cols-container .ag-row').length > 0 })()`)
140
+ await sleep(1500)
141
+ }
142
+
143
+ //彈窗表格之幾何: 表頭、列高、儲存格 overflow、下拉外框(WShellEllipse)之圓角/邊框/高度、chip 間距
144
+ async function probeDialogGrid(tag, nameHead, chipHead) {
145
+ let r = await ev(`(() => { ${H}
146
+ let g = gridOf('${nameHead}');
147
+ let rows = rowsOf(g);
148
+ let row0 = rows[0];
149
+ let o = { heads: heads(g), nRows: rows.length, rowH: rc(row0).h, rowsBox: rc(g.querySelector('.ag-body-viewport')) };
150
+ //下拉整顆: 儲存格內帶行內寬度之最外層祖先(w-web-perm 之 :style width:72px)
151
+ let sels = rows.slice(0, 3).map((row) => {
152
+ let root = [...row.querySelectorAll('div[style*="width:72px"], div[style*="width: 72px"]')][0];
153
+ if (!root) return null;
154
+ let cell = root.closest('.ag-cell');
155
+ let shell = root.querySelector('div[style*="border-radius"]');
156
+ let txt = [...root.querySelectorAll('div')].find((d) => d.children.length === 0 && /^(OR|AND)$/.test((d.innerText || '').trim()));
157
+ let svg = root.querySelector('svg');
158
+ return {
159
+ root: rc(root), cell: rc(cell), cellOverflow: cs(cell, ['overflow', 'overflow-x', 'overflow-y', 'padding-left', 'padding-right', 'line-height']),
160
+ shell: rc(shell), shellStyle: cs(shell, ['border-radius', 'border', 'background-color', 'padding', 'opacity', 'box-shadow', 'height']),
161
+ txt: txt ? rc(txt) : null, txtStyle: txt ? cs(txt, ['font-size', 'line-height', 'color']) : null,
162
+ svg: svg ? rc(svg) : null,
163
+ rowOfCell: rc(row),
164
+ }
165
+ });
166
+ o.selects = sels;
167
+ //chip: 每列之 chip 外框 span(height:20px)
168
+ if ('${chipHead}') {
169
+ let chipRows = rows.slice(0, 6).map((row) => {
170
+ let cell = [...row.querySelectorAll('.ag-cell')].find((c) => c.querySelector('span[style*="height:20px"], span[style*="height: 20px"]'));
171
+ if (!cell) return null;
172
+ let chips = [...cell.querySelectorAll('span[style*="height:20px"], span[style*="height: 20px"]')].map((s) => {
173
+ let ds = s.querySelectorAll('div');
174
+ return { box: rc(s), modeBox: rc(ds[1]), nameBox: rc(ds[2]), mode: ds[1].innerText.trim(), name: ds[2].innerText.trim(), nameStyle: cs(ds[2], ['font-size', 'padding', 'border-radius', 'background-color', 'color', 'white-space']), modeStyle: cs(ds[1], ['padding', 'border-radius', 'background-color', 'color', 'border-left']) };
175
+ });
176
+ let gaps = chips.slice(1).map((c, i) => +(c.box.x - chips[i].box.r).toFixed(1));
177
+ let cellR = rc(cell);
178
+ let nCut = chips.filter((c) => c.box.r > cellR.r + 0.5).length;
179
+ return { row: (row.querySelector('.ag-cell') || row).innerText.trim().slice(0, 30), cell: cellR, cellStyle: cs(cell, ['overflow', 'white-space', 'padding-left', 'padding-right', 'text-overflow']), nChips: chips.length, gaps, nChipsBeyondCell: nCut, chips: chips.slice(0, 4), lastChipRight: chips.length ? chips[chips.length - 1].box.r : null };
180
+ });
181
+ o.chipRows = chipRows;
182
+ }
183
+ return o;
184
+ })()`)
185
+ log(`${tag}.dialog`, r)
186
+ return r
187
+ }
188
+
189
+ //滑鼠移到列上(真滑鼠, 以 drag 零距離不可行, 改以頁面內派發 mouseenter/mouseover 讓 ag-grid 與 WShellEllipse 進入 hover 態), 量 hover 態樣式並截圖
190
+ async function probeHover(tag, nameHead, rowIdx = 1) {
191
+ let r = await ev(`(() => { ${H}
192
+ let g = gridOf('${nameHead}');
193
+ let row = rowsOf(g)[${rowIdx}];
194
+ let root = [...row.querySelectorAll('div[style*="width:72px"], div[style*="width: 72px"]')][0];
195
+ let shell = root.querySelector('div[style*="border-radius"]');
196
+ //ag-grid 之列 hover 以 mouseenter/mouseover 觸發(ag-row-hover class); WShellEllipse 監聽 mouseenter
197
+ [row, ...row.querySelectorAll('.ag-cell')].forEach((el) => { el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: false })); el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); el.dispatchEvent(new MouseEvent('mousemove', { bubbles: true })); });
198
+ shell.dispatchEvent(new MouseEvent('mouseenter', { bubbles: false }));
199
+ return true
200
+ })()`)
201
+ await sleep(500)
202
+ let r2 = await ev(`(() => { ${H}
203
+ let g = gridOf('${nameHead}');
204
+ let row = rowsOf(g)[${rowIdx}];
205
+ let root = [...row.querySelectorAll('div[style*="width:72px"], div[style*="width: 72px"]')][0];
206
+ let shell = root.querySelector('div[style*="border-radius"]');
207
+ let cell = root.closest('.ag-cell');
208
+ return { rowClass: row.className, rowBg: cs(row, ['background-color']), cellBg: cs(cell, ['background-color']), shellHover: cs(shell, ['border-radius', 'border', 'background-color', 'height']), shell: rc(shell), row: rc(row), cell: rc(cell), clipTop: +(rc(row).y - rc(shell).y).toFixed(1), clipBottom: +(rc(shell).btm - rc(row).btm).toFixed(1) }
209
+ })()`)
210
+ log(`${tag}.hover`, r2)
211
+ await shot(`${tag}-hover`)
212
+ return r2
213
+ }
214
+
215
+ //點開下拉, 量浮層與選項, 截圖, 再關閉
216
+ async function probeOpenSelect(tag, nameHead, rowIdx = 1) {
217
+ let r = await ev(`(() => { ${H}
218
+ let g = gridOf('${nameHead}');
219
+ let row = rowsOf(g)[${rowIdx}];
220
+ let root = [...row.querySelectorAll('div[style*="width:72px"], div[style*="width: 72px"]')][0];
221
+ return rc(root)
222
+ })()`)
223
+ await click(r.x + r.w / 2, r.y + r.h / 2)
224
+ await waitFor(`(() => { let ps = [...document.querySelectorAll('div[wtlp="modeSelect"]')].filter((p) => p.getBoundingClientRect().width > 0); return ps.length === 1 })()`)
225
+ await sleep(500)
226
+ let r2 = await ev(`(() => { ${H}
227
+ let p = [...document.querySelectorAll('div[wtlp="modeSelect"]')].filter((p) => p.getBoundingClientRect().width > 0)[0];
228
+ let items = [...p.querySelectorAll('div')].filter((d) => d.children.length === 1 && /^(OR|AND)$/.test((d.innerText || '').trim()) && d.getAttribute('tabindex') === '0');
229
+ let g = gridOf('${nameHead}');
230
+ let row = rowsOf(g)[${rowIdx}];
231
+ let root = [...row.querySelectorAll('div[style*="width:72px"], div[style*="width: 72px"]')][0];
232
+ let shell = root.querySelector('div[style*="border-radius"]');
233
+ let cell = root.closest('.ag-cell');
234
+ return { popup: rc(p), popupStyle: cs(p, ['border-radius', 'box-shadow', 'padding', 'background-color', 'border']), items: items.map((d) => ({ box: rc(d), t: d.innerText.trim(), style: cs(d, ['padding', 'font-size']) })), trigger: rc(root), shellOpen: cs(shell, ['border-radius', 'border', 'background-color']), cellFocus: cs(cell, ['border', 'border-color', 'outline']), cellClass: cell.className, viewport: rc(g.querySelector('.ag-body-viewport')), dlg: rc(root.closest('[role="dialog"]') || document.body) }
235
+ })()`)
236
+ log(`${tag}.open`, r2)
237
+ await shot(`${tag}-open`)
238
+ await key('Escape')
239
+ await sleep(400)
240
+ let still = await ev(`([...document.querySelectorAll('div[wtlp="modeSelect"]')].filter((p) => p.getBoundingClientRect().width > 0).length)`)
241
+ if (still > 0) {
242
+ //Escape 不關則點對話框標題區
243
+ let t = await ev(`(() => { ${H} let ls = leaf('目前權限群組').concat(leaf('目前權限')); return ls.length ? rc(ls[0]) : null })()`)
244
+ if (t) {
245
+ await click(t.x + 5, t.y + 5)
246
+ }
247
+ }
248
+ await sleep(400)
249
+ log(`${tag}.open.closedByEscape`, still === 0)
250
+ return r2
251
+ }
252
+
253
+ async function closeDialog() {
254
+ let r = await ev(`(() => { ${H} let ps = [...document.querySelectorAll('svg path')].filter((p) => p.getAttribute('d') === '${MDI_CLOSE}' && p.getBoundingClientRect().width > 0); if (!ps.length) return null; let p = ps[ps.length - 1]; return rc(p.closest('svg')) })()`)
255
+ if (!r) {
256
+ throw new Error('找不到關閉圖示')
257
+ }
258
+ await click(r.x + r.w / 2, r.y + r.h / 2)
259
+ await sleep(800)
260
+ }
261
+
262
+ async function main() {
263
+ await login()
264
+ await shot('perm-home')
265
+
266
+ //一、管理權限群組
267
+ await gotoNav('管理權限群組')
268
+ await probeMainTable('grups', '名稱')
269
+ await shot('grups-main')
270
+ //1. 編輯所屬使用者(VeGrupBlngUsers): 列 L1源項…
271
+ await openDialogFromRow('L1源項資料庫系統頁面閱覽_權限群組', '使用者', '編輯所屬使用者')
272
+ await probeDialogGrid('grupBlngUsers', '使用者名稱', '所屬權限群組名稱')
273
+ await shot('grupBlngUsers-dialog')
274
+ await probeHover('grupBlngUsers', '使用者名稱', 1)
275
+ await probeOpenSelect('grupBlngUsers', '使用者名稱', 1)
276
+ await closeDialog()
277
+ //2. 編輯使用權限(VeCpemis): 列 L1源項…
278
+ await openDialogFromRow('L1源項資料庫系統頁面閱覽_權限群組', '權限', '編輯使用權限')
279
+ await probeDialogGrid('cpemis', '權限名稱', '')
280
+ await shot('cpemis-dialog')
281
+ await probeHover('cpemis', '權限名稱', 1)
282
+ await probeOpenSelect('cpemis', '權限名稱', 1)
283
+ await closeDialog()
284
+
285
+ //二、管理權限
286
+ await gotoNav('管理權限')
287
+ await probeMainTable('pemis', '名稱')
288
+ await shot('pemis-main')
289
+ //3. 編輯所屬權限群組(VePemiBlngGrups): 列 知識管理系統_操作編輯_權限(E2E-009 同列)
290
+ await openDialogFromRow('知識管理系統_操作編輯_權限', '權限群組', '編輯所屬權限群組')
291
+ await probeDialogGrid('pemiBlngGrups', '權限群組名稱', '所屬權限名稱')
292
+ await shot('pemiBlngGrups-dialog')
293
+ await probeHover('pemiBlngGrups', '權限群組名稱', 1)
294
+ await probeOpenSelect('pemiBlngGrups', '權限群組名稱', 1)
295
+ await closeDialog()
296
+
297
+ //三、管理使用者
298
+ await gotoNav('管理使用者')
299
+ await probeMainTable('users', '名稱')
300
+ await shot('users-main')
301
+ //4. 編輯使用權限群組(VeCgrups): 列 王小明
302
+ await openDialogFromRow('王小明', '權限群組', '編輯使用權限群組')
303
+ await probeDialogGrid('cgrups', '權限群組名稱', '')
304
+ await shot('cgrups-dialog')
305
+ await probeHover('cgrups', '權限群組名稱', 1)
306
+ await probeOpenSelect('cgrups', '權限群組名稱', 1)
307
+ await closeDialog()
308
+
309
+ //四、管理對象(現有功能清單)之表頭
310
+ await gotoNav('現有功能清單')
311
+ await probeMainTable('targets', '對象名稱').catch((e) => log('targets.main.err', e.message))
312
+ await shot('targets-main')
313
+
314
+ fs.writeFileSync(path.join(fdOut, 'probe.json'), JSON.stringify(out, null, 2))
315
+ console.log('\n[done] 量測 JSON 已寫入', path.join(fdOut, 'probe.json'))
316
+ }
317
+
318
+ main().catch((err) => {
319
+ console.error('[fail]', err)
320
+ process.exitCode = 1
321
+ })
@@ -0,0 +1,173 @@
1
+ // 以 Playwright(真滑鼠 hover)拍權限管理系統關聯彈窗之局部放大圖, 供目視確認 chip 間距與下拉裁切
2
+ // 用法: node tmp/zz_perm_zoom.mjs ; 產物: tmp/perm_probe/zoom-*.png(DPR 3)
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+ import { createRequire } from 'module'
6
+
7
+ let require = createRequire('C:/prrdd/最終處置計畫資料庫系統/rddmanager_2_perm/package.json')
8
+ let { chromium } = require('playwright')
9
+
10
+ let SSO = 'http://localhost:11007'
11
+ let PERM = 'http://localhost:11006'
12
+ let fdOut = path.resolve('./tmp/perm_probe')
13
+ fs.mkdirSync(fdOut, { recursive: true })
14
+ let sleep = (ms) => new Promise((r) => setTimeout(r, ms))
15
+
16
+ async function leafRect(page, t, pred) {
17
+ return page.evaluate(({ t, pred }) => {
18
+ let ls = [...document.querySelectorAll('*')].filter((el) => el.children.length === 0 && (el.innerText || '').trim() === t)
19
+ if (pred === 'nav') {
20
+ ls = ls.filter((el) => el.getBoundingClientRect().left < 200)
21
+ }
22
+ if (!ls.length) {
23
+ return null
24
+ }
25
+ let b = ls[0].getBoundingClientRect()
26
+ return { x: b.left, y: b.top, w: b.width, h: b.height }
27
+ }, { t, pred })
28
+ }
29
+
30
+ async function gridOf(page, headText) {
31
+ return page.evaluateHandle((headText) => {
32
+ let hs = [...document.querySelectorAll('.ag-header-cell-text')].filter((e) => e.textContent.trim() === headText)
33
+ return hs.length ? hs[hs.length - 1].closest('.ag-root-wrapper') : null
34
+ }, headText)
35
+ }
36
+
37
+ async function main() {
38
+ let browser = await chromium.launch({ headless: true, args: ['--disable-gpu', '--force-color-profile=srgb', '--disable-lcd-text', '--disable-font-subpixel-positioning'] })
39
+ let context = await browser.newContext({ viewport: { width: 1600, height: 900 }, deviceScaleFactor: 3 })
40
+ let page = await context.newPage()
41
+
42
+ //login via sso ui
43
+ await page.goto(`${SSO}/?view=user`, { waitUntil: 'domcontentloaded' })
44
+ let inputs = page.locator('input')
45
+ await inputs.first().waitFor({ state: 'visible', timeout: 30000 })
46
+ await inputs.nth(0).click()
47
+ await page.keyboard.type('admin')
48
+ await inputs.nth(1).click()
49
+ await page.keyboard.type('admin@example.com')
50
+ await page.keyboard.press('Enter')
51
+ await page.waitForFunction(() => document.body.innerText.includes('使用者資訊') && document.body.innerText.includes('登出'), null, { timeout: 60000 })
52
+ let token = await page.evaluate(() => localStorage.getItem('rddmanager_main:userToken'))
53
+ await page.goto(`${PERM}/?token=${token}`, { waitUntil: 'domcontentloaded' })
54
+ await page.waitForFunction(() => {
55
+ let ls = [...document.querySelectorAll('*')].filter((el) => el.children.length === 0 && (el.innerText || '').trim() === '管理使用者')
56
+ return ls.some((el) => { let b = el.getBoundingClientRect(); return b.width > 0 && b.left >= 0 && b.left < 200 })
57
+ }, null, { timeout: 90000 })
58
+ await page.waitForFunction(() => !document.body.innerText.includes('等待數據中'), null, { timeout: 90000 })
59
+ await sleep(1500)
60
+
61
+ async function nav(t) {
62
+ let r = await leafRect(page, t, 'nav')
63
+ await page.mouse.click(r.x + r.w / 2, r.y + r.h / 2)
64
+ await page.waitForFunction((t) => [...document.querySelectorAll('*')].some((el) => el.children.length === 0 && (el.innerText || '').trim() === t && el.getBoundingClientRect().left > 200), t)
65
+ await page.waitForFunction(() => !document.body.innerText.includes('等待數據中'), null, { timeout: 90000 })
66
+ await page.waitForFunction(() => document.querySelectorAll('.ag-center-cols-container .ag-row').length > 0)
67
+ await sleep(1200)
68
+ }
69
+
70
+ async function openDialog(rowText, btnText, title) {
71
+ let r = await page.evaluate(({ rowText, btnText }) => {
72
+ let hs = [...document.querySelectorAll('.ag-header-cell-text')].filter((e) => e.textContent.trim() === '名稱')
73
+ let g = hs[hs.length - 1].closest('.ag-root-wrapper')
74
+ let hit = [...g.querySelectorAll('.ag-row')].find((r) => (r.innerText || '').includes(rowText))
75
+ let idx = hit.getAttribute('row-index')
76
+ let row = g.querySelector('.ag-center-cols-container .ag-row[row-index="' + idx + '"]')
77
+ let b = [...row.querySelectorAll('button')].find((b) => (b.innerText || '').includes(btnText))
78
+ let bb = b.getBoundingClientRect()
79
+ return { x: bb.left, y: bb.top, w: bb.width, h: bb.height }
80
+ }, { rowText, btnText })
81
+ await page.mouse.click(r.x + r.w / 2, r.y + r.h / 2)
82
+ await page.waitForFunction((t) => [...document.querySelectorAll('*')].some((el) => el.children.length === 0 && (el.innerText || '').trim() === t && el.getBoundingClientRect().width > 0), title)
83
+ await page.waitForFunction(() => { let hs = [...document.querySelectorAll('.ag-root-wrapper')]; return hs.length >= 2 && hs[hs.length - 1].querySelectorAll('.ag-center-cols-container .ag-row').length > 0 })
84
+ await sleep(1500)
85
+ }
86
+
87
+ async function selectRect(nameHead, rowIdx) {
88
+ return page.evaluate(({ nameHead, rowIdx }) => {
89
+ let hs = [...document.querySelectorAll('.ag-header-cell-text')].filter((e) => e.textContent.trim() === nameHead)
90
+ let g = hs[hs.length - 1].closest('.ag-root-wrapper')
91
+ let rows = [...g.querySelectorAll('.ag-center-cols-container .ag-row')].sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top)
92
+ let row = rows[rowIdx]
93
+ let root = [...row.querySelectorAll('div[style*="width:72px"], div[style*="width: 72px"]')][0]
94
+ let b = root.getBoundingClientRect()
95
+ let rb = row.getBoundingClientRect()
96
+ let vb = g.querySelector('.ag-body-viewport').getBoundingClientRect()
97
+ return { x: b.left, y: b.top, w: b.width, h: b.height, row: { x: rb.left, y: rb.top, w: rb.width, h: rb.height }, vp: { x: vb.left, y: vb.top, w: vb.width, h: vb.height } }
98
+ }, { nameHead, rowIdx })
99
+ }
100
+
101
+ async function closeDialog() {
102
+ let r = await page.evaluate(() => {
103
+ let d = 'M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z'
104
+ let ps = [...document.querySelectorAll('svg path')].filter((p) => p.getAttribute('d') === d && p.getBoundingClientRect().width > 0)
105
+ let b = ps[ps.length - 1].closest('svg').getBoundingClientRect()
106
+ return { x: b.left, y: b.top, w: b.width, h: b.height }
107
+ })
108
+ await page.mouse.click(r.x + r.w / 2, r.y + r.h / 2)
109
+ await sleep(800)
110
+ }
111
+
112
+ //一、管理權限: 編輯所屬權限群組(多 chip)
113
+ await nav('管理權限')
114
+ await openDialog('知識管理系統_操作編輯_權限', '權限群組', '編輯所屬權限群組')
115
+ let s = await selectRect('權限群組名稱', 1)
116
+ //(a) 靜態: 前六列
117
+ await page.mouse.move(5, 5)
118
+ await sleep(300)
119
+ await page.screenshot({ path: path.join(fdOut, 'zoom-pemiBlngGrups-rows.png'), clip: { x: s.vp.x, y: s.vp.y - 60, width: s.vp.w, height: 60 + 28 * 6 } })
120
+ //(b) 真滑鼠 hover 在合併模式儲存格上
121
+ await page.mouse.move(s.x + s.w / 2, s.y + s.h / 2)
122
+ await sleep(600)
123
+ await page.screenshot({ path: path.join(fdOut, 'zoom-pemiBlngGrups-hover.png'), clip: { x: s.vp.x, y: s.row.y - 30, width: s.vp.w, height: 28 * 4 } })
124
+ //(c) 點開下拉
125
+ await page.mouse.click(s.x + s.w / 2, s.y + s.h / 2)
126
+ await page.waitForFunction(() => [...document.querySelectorAll('div[wtlp="modeSelect"]')].some((p) => p.getBoundingClientRect().width > 0))
127
+ await sleep(600)
128
+ await page.screenshot({ path: path.join(fdOut, 'zoom-pemiBlngGrups-open.png'), clip: { x: s.vp.x, y: s.row.y - 30, width: s.vp.w, height: 28 * 6 } })
129
+ //(d) 選 AND 後, chip 之當前項會變紅並顯示 AND: 先關清單, 勾選該列「是否使用」再看 chip
130
+ await page.mouse.click(s.vp.x + 20, s.vp.y - 70) //點對話框標題區關閉清單
131
+ await sleep(500)
132
+ let chk = await page.evaluate(({ rowIdx }) => {
133
+ let hs = [...document.querySelectorAll('.ag-header-cell-text')].filter((e) => e.textContent.trim() === '權限群組名稱')
134
+ let g = hs[hs.length - 1].closest('.ag-root-wrapper')
135
+ let rows = [...g.querySelectorAll('.ag-center-cols-container .ag-row')].sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top)
136
+ let ib = rows[rowIdx].querySelector('input[type=checkbox]').getBoundingClientRect()
137
+ return { x: ib.left, y: ib.top, w: ib.width, h: ib.height }
138
+ }, { rowIdx: 1 })
139
+ await page.mouse.click(chk.x + chk.w / 2, chk.y + chk.h / 2)
140
+ await sleep(800)
141
+ await page.mouse.move(5, 5)
142
+ await sleep(300)
143
+ await page.screenshot({ path: path.join(fdOut, 'zoom-pemiBlngGrups-checked.png'), clip: { x: s.vp.x, y: s.row.y - 30, width: s.vp.w, height: 28 * 4 } })
144
+ //取消勾選還原(不儲存, 關閉即棄)
145
+ await page.mouse.click(chk.x + chk.w / 2, chk.y + chk.h / 2)
146
+ await sleep(500)
147
+ await closeDialog()
148
+ console.log('[zoom] pemiBlngGrups done')
149
+
150
+ //二、管理使用者: 編輯使用權限群組(使用者截圖之彈窗)
151
+ await nav('管理使用者')
152
+ await openDialog('王小明', '權限群組', '編輯使用權限群組')
153
+ let s2 = await selectRect('權限群組名稱', 0)
154
+ await page.mouse.move(s2.x + s2.w / 2, s2.y + s2.h / 2)
155
+ await sleep(600)
156
+ await page.screenshot({ path: path.join(fdOut, 'zoom-cgrups-hover.png'), clip: { x: s2.vp.x, y: s2.row.y - 60, width: s2.vp.w, height: 28 * 5 } })
157
+ await page.mouse.click(s2.x + s2.w / 2, s2.y + s2.h / 2)
158
+ await page.waitForFunction(() => [...document.querySelectorAll('div[wtlp="modeSelect"]')].some((p) => p.getBoundingClientRect().width > 0))
159
+ await sleep(600)
160
+ await page.screenshot({ path: path.join(fdOut, 'zoom-cgrups-open.png'), clip: { x: s2.vp.x, y: s2.row.y - 60, width: s2.vp.w, height: 28 * 6 } })
161
+ await page.mouse.click(s2.vp.x + 20, s2.vp.y - 30)
162
+ await sleep(500)
163
+ await closeDialog()
164
+ console.log('[zoom] cgrups done')
165
+
166
+ await browser.close()
167
+ console.log('[done]')
168
+ }
169
+
170
+ main().catch((err) => {
171
+ console.error('[fail]', err)
172
+ process.exitCode = 1
173
+ })