w-web-perm 1.0.85 → 1.0.86

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 (66) hide show
  1. package/dist/index.tmp +1 -1
  2. package/dist/js/{chunk-vendors-legacy.7b6ae0b0.js → chunk-vendors-legacy.d85138b8.js} +1 -1
  3. package/dist/js/{chunk-vendors.7b6ae0b0.js → chunk-vendors.d85138b8.js} +1 -1
  4. package/dist/w-web-perm.umd.js +1 -1
  5. package/docs/WWebPerm.html +1 -1
  6. package/docs/global.html +1 -1
  7. package/docs/index.html +1 -1
  8. package/docs/server_WWebPerm.mjs.html +1 -1
  9. package/docs/server_procCore.mjs.html +1 -1
  10. package/docs/src_fetchJson.mjs.html +1 -1
  11. package/package.json +2 -2
  12. package/tableTags-web-perm.json +1 -1
  13. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/README.md +0 -58
  14. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/server_procLang.mjs.diff +0 -31
  15. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_App.vue.diff +0 -19
  16. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_LayoutContentGrups.vue.diff +0 -40
  17. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_LayoutContentPemis.vue.diff +0 -40
  18. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_ModeSelectChip.vue.new.diff +0 -124
  19. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_RelationChip.vue.new.diff +0 -116
  20. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_RelationChips.vue.new.diff +0 -152
  21. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_VeCgrups.vue.diff +0 -76
  22. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_VeCpemis.vue.diff +0 -76
  23. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_VeCrules.vue.diff +0 -14
  24. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_VeGrupBlngUsers.vue.diff +0 -121
  25. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/diff/src_components_VePemiBlngGrups.vue.diff +0 -121
  26. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/server/procLang.mjs +0 -1049
  27. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/App.vue +0 -204
  28. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/LayoutContentGrups.vue +0 -1260
  29. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/LayoutContentPemis.vue +0 -1262
  30. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/ModeSelectChip.vue +0 -121
  31. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/RelationChip.vue +0 -113
  32. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/RelationChips.vue +0 -149
  33. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/VeCgrups.vue +0 -943
  34. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/VeCpemis.vue +0 -943
  35. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/VeCrules.vue +0 -848
  36. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/VeGrupBlngUsers.vue +0 -1157
  37. package/w-web-perm/351/232/250/351/231/204/350/263/207/346/226/231/src/components/VePemiBlngGrups.vue +0 -1157
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  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/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
  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/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
  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/verify.json +0 -390
  58. 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 +0 -93
  59. 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 +0 -23
  60. 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 +0 -321
  61. 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 +0 -173
  62. 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 +0 -330
  63. package//345/273/272/350/255/260w-aggrid-vue/350/252/277/346/225/264.md +0 -193
  64. package//345/273/272/350/255/260w-component-vue/350/210/207wsemi/350/252/277/346/225/264.md +0 -76
  65. package//345/273/272/350/255/260w-web-perm/350/252/277/346/225/264.md +0 -244
  66. 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 +0 -166
@@ -1,330 +0,0 @@
1
- // 驗證修改後之 w-web-perm(建置於 tmp/wwp_build, 由 tmp/wwp_srv 之後端 11016 提供): 依「建議w-web-perm調整.md」第 8 節之判準逐條量測並截圖
2
- // 用法: node tmp/zz_wwp_verify.mjs ; 產物: w-web-perm隨附資料/驗證截圖/*.png 與 verify.json
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:11016'
12
- let fdOut = path.resolve('./w-web-perm隨附資料/驗證截圖')
13
- fs.mkdirSync(fdOut, { recursive: true })
14
- let sleep = (ms) => new Promise((r) => setTimeout(r, ms))
15
- let out = {}
16
- let fails = []
17
- function check(name, ok, detail) {
18
- out[name] = { ok: !!ok, detail }
19
- console.log(`${ok ? 'PASS' : 'FAIL'} ${name} ${JSON.stringify(detail)}`)
20
- if (!ok) {
21
- fails.push(name)
22
- }
23
- }
24
-
25
- //頁內共用函式(字串注入)
26
- let H = `
27
- 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) } };
28
- let cs = (el, ks) => { let s = getComputedStyle(el); let o = {}; ks.forEach((k) => { o[k] = s.getPropertyValue(k) }); return o };
29
- let leaf = (t) => [...document.querySelectorAll('*')].filter((el) => el.children.length === 0 && (el.innerText || '').trim() === t);
30
- 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 };
31
- let heads = (g) => [...g.querySelectorAll('.ag-header-cell-text')].map((e) => e.textContent.trim());
32
- let rowsOf = (g) => [...g.querySelectorAll('.ag-center-cols-container .ag-row')].sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
33
- 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 };
34
- //本項 chip 之模式控制項: 帶行內寬度 56px 之 WTextSelect 根元素
35
- let modeRoots = (el) => [...el.querySelectorAll('div[style*="width:56px"], div[style*="width: 56px"]')];
36
- //chip: RelationChip 根元素(inline-flex + align-items:stretch, 帶 title); 展開全部鈕亦帶 title 但為 align-items:center
37
- let chipsOf = (cell) => [...cell.querySelectorAll('div[title]')].filter((d) => /align-items:\\s*stretch/.test(d.getAttribute('style') || ''));
38
- let btnAllOf = (cell) => [...cell.querySelectorAll('div[title]')].find((d) => /align-items:\\s*center/.test(d.getAttribute('style') || ''));
39
- //「是否使用」勾選框: 以欄 col-id 定位(VeCgrups/VeCpemis 之 name 欄另有列勾選框, 不可取列內第一個 checkbox)
40
- let enableChk = (row) => row.querySelector('.ag-cell[col-id="enable"] input[type=checkbox]');
41
- `
42
-
43
- async function evalH(page, body) {
44
- return page.evaluate(`(() => { ${H} ${body} })()`)
45
- }
46
-
47
- async function main() {
48
- let browser = await chromium.launch({ headless: true, args: ['--disable-gpu', '--force-color-profile=srgb', '--disable-lcd-text', '--disable-font-subpixel-positioning'] })
49
-
50
- async function openPerm(dpr) {
51
- let context = await browser.newContext({ viewport: { width: 1600, height: 900 }, deviceScaleFactor: dpr })
52
- let page = await context.newPage()
53
- await page.goto(`${SSO}/?view=user`, { waitUntil: 'domcontentloaded' })
54
- let inputs = page.locator('input')
55
- await inputs.first().waitFor({ state: 'visible', timeout: 30000 })
56
- await inputs.nth(0).click()
57
- await page.keyboard.type('admin')
58
- await inputs.nth(1).click()
59
- await page.keyboard.type('admin@example.com')
60
- await page.keyboard.press('Enter')
61
- await page.waitForFunction(() => document.body.innerText.includes('使用者資訊') && document.body.innerText.includes('登出'), null, { timeout: 60000 })
62
- let token = await page.evaluate(() => localStorage.getItem('rddmanager_main:userToken'))
63
- await page.goto(`${PERM}/?token=${token}`, { waitUntil: 'domcontentloaded' })
64
- await page.waitForFunction(() => {
65
- let ls = [...document.querySelectorAll('*')].filter((el) => el.children.length === 0 && (el.innerText || '').trim() === '管理使用者')
66
- return ls.some((el) => { let b = el.getBoundingClientRect(); return b.width > 0 && b.left >= 0 && b.left < 200 })
67
- }, null, { timeout: 90000 })
68
- await page.waitForFunction(() => !document.body.innerText.includes('等待數據中'), null, { timeout: 90000 })
69
- await sleep(1500)
70
- return { context, page }
71
- }
72
-
73
- async function nav(page, t) {
74
- let r = await evalH(page, `let ls = leaf('${t}').filter((el) => el.getBoundingClientRect().left < 200); return ls.length ? rc(ls[0]) : null`)
75
- await page.mouse.click(r.x + r.w / 2, r.y + r.h / 2)
76
- await page.waitForFunction((t) => [...document.querySelectorAll('*')].some((el) => el.children.length === 0 && (el.innerText || '').trim() === t && el.getBoundingClientRect().left > 200), t)
77
- await page.waitForFunction(() => !document.body.innerText.includes('等待數據中'), null, { timeout: 90000 })
78
- await page.waitForFunction(() => document.querySelectorAll('.ag-center-cols-container .ag-row').length > 0)
79
- await sleep(1200)
80
- }
81
-
82
- async function openDialog(page, rowText, btnText, title) {
83
- let r = await evalH(page, `let g = gridOf('名稱'); let row = rowByText(g, '${rowText}'); if (!row) return null; let b = [...row.querySelectorAll('button')].find((b) => (b.innerText || '').includes('${btnText}')); return b ? rc(b) : null`)
84
- if (!r) {
85
- throw new Error(`找不到 ${rowText} 之按鈕 ${btnText}`)
86
- }
87
- await page.mouse.click(r.x + r.w / 2, r.y + r.h / 2)
88
- await page.waitForFunction((t) => [...document.querySelectorAll('*')].some((el) => el.children.length === 0 && (el.innerText || '').trim() === t && el.getBoundingClientRect().width > 0), title)
89
- 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 })
90
- await sleep(1500)
91
- }
92
-
93
- async function closeDialog(page) {
94
- let r = await page.evaluate(() => {
95
- 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'
96
- let ps = [...document.querySelectorAll('svg path')].filter((p) => p.getAttribute('d') === d && p.getBoundingClientRect().width > 0)
97
- let b = ps[ps.length - 1].closest('svg').getBoundingClientRect()
98
- return { x: b.left, y: b.top, w: b.width, h: b.height }
99
- })
100
- await page.mouse.click(r.x + r.w / 2, r.y + r.h / 2)
101
- await sleep(800)
102
- }
103
-
104
- async function shot(page, name, clip) {
105
- let fp = path.join(fdOut, `${name}.png`)
106
- await page.screenshot({ path: fp, clip })
107
- console.log('[shot]', fp)
108
- }
109
-
110
- //================ A. 主表欄序與欄名 ================
111
- let { page } = await openPerm(1)
112
- await nav(page, '管理權限群組')
113
- let hg = await evalH(page, `return heads(gridOf('名稱'))`)
114
- check('A1.管理權限群組欄序', JSON.stringify(hg) === JSON.stringify(['名稱', '說明', '管控使用權限', '管控所屬使用者']), hg)
115
- await shot(page, 'A-管理權限群組主表')
116
- await nav(page, '管理權限')
117
- let hp = await evalH(page, `return heads(gridOf('名稱'))`)
118
- check('A2A3.管理權限欄序與欄名', JSON.stringify(hp) === JSON.stringify(['名稱', '說明', '管控對象', '管控所屬權限群組']), hp)
119
- await shot(page, 'A-管理權限主表')
120
-
121
- //================ B. 編輯所屬權限群組(VePemiBlngGrups) ================
122
- await openDialog(page, '知識管理系統_操作編輯_權限', '權限群組', '編輯所屬權限群組')
123
- let d1 = await evalH(page, `
124
- let g = gridOf('權限群組名稱'); let rows = rowsOf(g);
125
- let row0 = rowByText(g, '系統管理者編輯_權限群組');
126
- let cellChips = [...row0.querySelectorAll('.ag-cell')].find((c) => chipsOf(c).length > 0);
127
- let chips = chipsOf(cellChips);
128
- let gaps = chips.slice(1).map((c, i) => +(rc(c).x - rc(chips[i]).r).toFixed(1)).slice(0, 5);
129
- let btnAll = btnAllOf(cellChips);
130
- let mr = modeRoots(chips[0])[0];
131
- let shell = mr ? mr.querySelector('div[style*="border-radius"]') : null;
132
- let cell = mr ? mr.closest('.ag-cell') : null;
133
- return {
134
- heads: heads(g), nRows: rows.length, rowH: rc(rows[0]).h,
135
- nChips: chips.length, firstChipName: chips[0].getAttribute('title'), gaps, chipH: chips.slice(0, 3).map((c) => rc(c).h),
136
- innerH: [...chips[0].children].map((c) => rc(c).h),
137
- btnAllText: btnAll ? btnAll.innerText.trim() : null, btnAllRect: btnAll ? rc(btnAll) : null,
138
- mode: mr ? { root: rc(mr), shell: rc(shell), cell: rc(cell), shellStyle: cs(shell, ['border-radius', 'border', 'background-color', 'opacity']), rootStyle: cs(mr.parentElement, ['border-radius', 'overflow']), txt: mr.innerText.trim(), cellClass: cell.className } : null,
139
- rowsEnabled: rows.map((r) => enableChk(r) ? enableChk(r).checked : null).slice(0, 6),
140
- row1HasMode: modeRoots(rows[1]).length,
141
- }
142
- `)
143
- check('B.對話框欄序', JSON.stringify(d1.heads) === JSON.stringify(['權限群組名稱', '是否使用', '所屬權限名稱']), d1.heads)
144
- check('B.本項chip排第1', d1.firstChipName === '知識管理系統_操作編輯_權限', { first: d1.firstChipName, n: d1.nChips })
145
- check('B.chip間距4px', d1.gaps.every((g) => Math.abs(g - 4) < 0.6), d1.gaps)
146
- check('B.chip高22', d1.chipH.every((h) => Math.abs(h - 22) < 0.6) && d1.innerH.every((h) => Math.abs(h - 22) < 0.6), { chipH: d1.chipH, innerH: d1.innerH })
147
- check('B.展開鈕在最左且帶數量', d1.btnAllText === String(d1.nChips) && d1.btnAllRect && d1.mode && d1.btnAllRect.x < d1.mode.root.x, { btnAllText: d1.btnAllText, nChips: d1.nChips, btnAllRect: d1.btnAllRect })
148
- check('B.模式控制項22x56且在儲存格內', d1.mode && Math.abs(d1.mode.shell.h - 22) < 0.6 && Math.abs(d1.mode.root.w - 56) < 0.6 && d1.mode.shell.y >= d1.mode.cell.y - 0.5 && d1.mode.shell.btm <= d1.mode.cell.btm + 0.5, d1.mode)
149
- if (!d1.mode) {
150
- throw new Error('本項 chip 無模式控制項, 後續量測無法進行')
151
- }
152
- check('B.未勾選列無模式控制項', d1.row1HasMode === 0, { row1HasMode: d1.row1HasMode })
153
- await shot(page, 'B1-編輯所屬權限群組-對話框')
154
-
155
- //hover 本項 chip 之模式段(真滑鼠)
156
- let m0 = d1.mode.root
157
- await page.mouse.move(m0.x + m0.w / 2, m0.y + m0.h / 2)
158
- await sleep(500)
159
- let hv = await evalH(page, `let g = gridOf('權限群組名稱'); let row0 = rowByText(g, '系統管理者編輯_權限群組'); let mr = modeRoots(row0)[0]; let shell = mr.querySelector('div[style*="border-radius"]'); let cell = mr.closest('.ag-cell'); return { shellHover: cs(shell, ['background-color', 'border', 'cursor']), cellClass: cell.className, cellBorder: cs(cell, ['border'])['border'] }`)
160
- check('B.hover無儲存格焦點框', !hv.cellClass.includes('ag-cell-focus') || hv.cellClass.includes('no-border'), hv)
161
- await shot(page, 'B2-編輯所屬權限群組-hover本項chip', { x: 401, y: m0.y - 40, width: 798, height: 28 * 4 })
162
-
163
- //點開本項 chip 之模式清單
164
- await page.mouse.click(m0.x + m0.w / 2, m0.y + m0.h / 2)
165
- await page.waitForFunction(() => [...document.querySelectorAll('div[wtlp="modeSelect"]')].some((p) => p.getBoundingClientRect().width > 0), null, { timeout: 5000 })
166
- await sleep(500)
167
- let op = await evalH(page, `
168
- let p = [...document.querySelectorAll('div[wtlp="modeSelect"]')].filter((p) => p.getBoundingClientRect().width > 0)[0];
169
- let items = [...p.querySelectorAll('[tabindex="0"]')].filter((d) => /^(OR|AND)$/.test((d.innerText || '').trim()));
170
- let g = gridOf('權限群組名稱'); let row0 = rowByText(g, '系統管理者編輯_權限群組'); let mr = modeRoots(row0)[0]; let cell = mr.closest('.ag-cell');
171
- return { popup: rc(p), items: items.map((d) => ({ t: d.innerText.trim(), r: rc(d) })), trigger: rc(mr), cellClass: cell.className, cellBorder: cs(cell, ['border'])['border'] }
172
- `)
173
- check('B.清單依序OR、AND', op.items.map((i) => i.t).join(',') === 'OR,AND', op.items.map((i) => [i.t, i.r.h]))
174
- check('B.清單左緣對齊控制項', Math.abs(op.popup.x - op.trigger.x) <= 1.5, { popupX: op.popup.x, triggerX: op.trigger.x })
175
- check('B.清單不窄於控制項', op.popup.w >= op.trigger.w - 0.5, { popupW: op.popup.w, triggerW: op.trigger.w })
176
- check('B.點擊後無儲存格藍框', !op.cellBorder.includes('rgb(0, 145, 234)'), { cellBorder: op.cellBorder, cellClass: op.cellClass })
177
- await shot(page, 'B3-編輯所屬權限群組-展開本項chip清單', { x: 401, y: m0.y - 40, width: 798, height: 28 * 6 })
178
- //點 AND
179
- let andItem = op.items.find((i) => i.t === 'AND')
180
- await page.mouse.click(andItem.r.x + andItem.r.w / 2, andItem.r.y + andItem.r.h / 2)
181
- await sleep(800)
182
- let afterAnd = await evalH(page, `let g = gridOf('權限群組名稱'); let row0 = rowByText(g, '系統管理者編輯_權限群組'); let mr = modeRoots(row0)[0]; let open = [...document.querySelectorAll('div[wtlp="modeSelect"]')].some((p) => p.getBoundingClientRect().width > 0); let hasSave = [...document.querySelectorAll('svg path')].some((p) => p.getAttribute('d') && p.getAttribute('d').startsWith('M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z') && p.getBoundingClientRect().width > 0); return { txt: mr.innerText.trim(), open, hasSave }`)
183
- check('B.點AND後本項chip顯示AND且清單收起', afterAnd.txt === 'AND' && !afterAnd.open, afterAnd)
184
- await shot(page, 'B4-編輯所屬權限群組-本項chip已為AND', { x: 401, y: m0.y - 40, width: 798, height: 28 * 4 })
185
-
186
- //展開全部
187
- let ba = d1.btnAllRect
188
- await page.mouse.click(ba.x + ba.w / 2, ba.y + ba.h / 2)
189
- await page.waitForFunction(() => [...document.querySelectorAll('div[wtlp="relationChipsAll"]')].some((p) => p.getBoundingClientRect().width > 0), null, { timeout: 5000 })
190
- await sleep(600)
191
- let pa = await evalH(page, `
192
- let p = [...document.querySelectorAll('div[wtlp="relationChipsAll"]')].filter((p) => p.getBoundingClientRect().width > 0)[0];
193
- let chips = chipsOf(p);
194
- let title = p.innerText.split('\\n')[0];
195
- return { popup: rc(p), n: chips.length, first: chips[0].getAttribute('title'), firstMode: chips[0].innerText.trim().split('\\n')[0], title, wrap: cs(chips[0].parentElement, ['flex-wrap', 'gap']) }
196
- `)
197
- check('B.展開全部浮層列出94顆且本項第1', pa.n === 94 && pa.first === '知識管理系統_操作編輯_權限', pa)
198
- await shot(page, 'B5-編輯所屬權限群組-展開全部浮層')
199
- //關閉浮層(點對話框標題區)
200
- let ttl = await evalH(page, `let ls = leaf('目前權限'); return ls.length ? rc(ls[0]) : null`)
201
- await page.mouse.click(ttl.x + 5, ttl.y + 5)
202
- await sleep(500)
203
-
204
- //未勾選列勾選後, 本項 chip 出現在第 1 顆且可下拉
205
- let chk = await evalH(page, `let g = gridOf('權限群組名稱'); let row = rowByText(g, 'L0公開頁面閱覽_權限群組'); return rc(enableChk(row))`)
206
- await page.mouse.click(chk.x + chk.w / 2, chk.y + chk.h / 2)
207
- await sleep(900)
208
- let ck = await evalH(page, `let g = gridOf('權限群組名稱'); let row = rowByText(g, 'L0公開頁面閱覽_權限群組'); let cell = [...row.querySelectorAll('.ag-cell')].find((c) => chipsOf(c).length > 0); let chips = chipsOf(cell); let mr = modeRoots(chips[0])[0]; return { first: chips[0].getAttribute('title'), hasMode: !!mr, n: chips.length, firstBg: cs(chips[0].children[0].querySelector('div[style*="border-radius"]') || chips[0].children[0], ['background-color'])['background-color'] }`)
209
- check('B.勾選後本項chip成第1顆且帶下拉', ck.first === '知識管理系統_操作編輯_權限' && ck.hasMode, ck)
210
- await shot(page, 'B6-編輯所屬權限群組-勾選後本項chip在首位', { x: 401, y: chk.y - 60, width: 798, height: 28 * 4 })
211
- await page.mouse.click(chk.x + chk.w / 2, chk.y + chk.h / 2) //還原(不儲存)
212
- await sleep(500)
213
- await closeDialog(page)
214
-
215
- //================ C. 編輯使用權限(VeCpemis) ================
216
- await nav(page, '管理權限群組')
217
- await openDialog(page, 'L0公開頁面閱覽_權限群組', '權限', '編輯使用權限')
218
- let c1 = await evalH(page, `
219
- let g = gridOf('權限名稱'); let rows = rowsOf(g);
220
- let info = rows.slice(0, 8).map((r) => { let mr = modeRoots(r)[0]; let shell = mr.querySelector('div[style*="border-radius"]'); let svg = mr.querySelector('svg'); return { chk: enableChk(r).checked, txt: mr.innerText.trim(), h: rc(shell).h, w: rc(mr).w, op: cs(shell, ['opacity'])['opacity'], hasArrow: !!svg, cursor: cs(mr.querySelector('[style*="white-space"]') || mr, ['cursor'])['cursor'], cell: rc(mr.closest('.ag-cell')), shell: rc(shell) } });
221
- return { heads: heads(g), info }
222
- `)
223
- let enabledRows = c1.info.filter((i) => i.chk)
224
- let disabledRows = c1.info.filter((i) => !i.chk)
225
- check('C.欄序不變', JSON.stringify(c1.heads) === JSON.stringify(['權限名稱', '合併權限模式', '是否使用']), c1.heads)
226
- check('C.控制項22x56且在儲存格內', c1.info.every((i) => Math.abs(i.h - 22) < 0.6 && Math.abs(i.w - 56) < 0.6 && i.shell.y >= i.cell.y - 0.5 && i.shell.btm <= i.cell.btm + 0.5), c1.info.map((i) => [i.chk, i.h, i.w]))
227
- check('C.已勾選列可操作(不透明、有箭頭)', enabledRows.length > 0 && enabledRows.every((i) => i.op === '1' && i.hasArrow), enabledRows.map((i) => [i.txt, i.op, i.hasArrow]))
228
- check('C.未勾選列淡化無箭頭', disabledRows.length > 0 && disabledRows.every((i) => i.op === '0.6' && !i.hasArrow), disabledRows.slice(0, 3).map((i) => [i.txt, i.op, i.hasArrow]))
229
- await shot(page, 'C1-編輯使用權限-對話框')
230
- let en0 = await evalH(page, `let g = gridOf('權限名稱'); let r = rowsOf(g).find((r) => enableChk(r).checked); let mr = modeRoots(r)[0]; return rc(mr)`)
231
- await page.mouse.move(en0.x + en0.w / 2, en0.y + en0.h / 2)
232
- await sleep(400)
233
- await shot(page, 'C2-編輯使用權限-hover已勾選列控制項', { x: 401, y: en0.y - 40, width: 798, height: 28 * 4 })
234
- await page.mouse.click(en0.x + en0.w / 2, en0.y + en0.h / 2)
235
- await page.waitForFunction(() => [...document.querySelectorAll('div[wtlp="modeSelect"]')].some((p) => p.getBoundingClientRect().width > 0), null, { timeout: 5000 })
236
- await sleep(500)
237
- let cop = await evalH(page, `let p = [...document.querySelectorAll('div[wtlp="modeSelect"]')].filter((p) => p.getBoundingClientRect().width > 0)[0]; let items = [...p.querySelectorAll('[tabindex="0"]')].filter((d) => /^(OR|AND)$/.test((d.innerText || '').trim())); return { popup: rc(p), items: items.map((d) => d.innerText.trim()) }`)
238
- check('C.已勾選列點開清單', cop.items.join(',') === 'OR,AND', cop)
239
- await shot(page, 'C3-編輯使用權限-展開清單', { x: 401, y: en0.y - 40, width: 798, height: 28 * 6 })
240
- let t2 = await evalH(page, `let ls = leaf('權限名稱'); return rc(ls[0])`)
241
- await page.mouse.click(t2.x + 5, t2.y - 30) //點對話框標頭以外之空白處關閉清單(點表頭)
242
- await sleep(500)
243
- let dis0 = await evalH(page, `let g = gridOf('權限名稱'); let r = rowsOf(g).find((r) => !enableChk(r).checked); let mr = modeRoots(r)[0]; return rc(mr)`)
244
- await page.mouse.click(dis0.x + dis0.w / 2, dis0.y + dis0.h / 2)
245
- await sleep(500)
246
- let disOpen = await page.evaluate(() => [...document.querySelectorAll('div[wtlp="modeSelect"]')].some((p) => p.getBoundingClientRect().width > 0))
247
- check('C.未勾選列點擊不展開', !disOpen, { disOpen })
248
- await closeDialog(page)
249
-
250
- //================ D. 編輯所屬使用者(VeGrupBlngUsers) ================
251
- await openDialog(page, 'L0公開頁面閱覽_權限群組', '使用者', '編輯所屬使用者')
252
- let dd = await evalH(page, `
253
- let g = gridOf('使用者名稱'); let rows = rowsOf(g);
254
- let r1 = rows.find((r) => enableChk(r).checked);
255
- let cell = [...r1.querySelectorAll('.ag-cell')].find((c) => chipsOf(c).length > 0); let chips = chipsOf(cell);
256
- return { heads: heads(g), nRows: rows.length, first: chips[0].getAttribute('title'), hasMode: modeRoots(chips[0]).length, n: chips.length, gaps: chips.slice(1).map((c, i) => +(rc(c).x - rc(chips[i]).r).toFixed(1)) }
257
- `)
258
- check('D.編輯所屬使用者之欄序與本項chip', JSON.stringify(dd.heads) === JSON.stringify(['使用者名稱', '是否使用', '所屬權限群組名稱']) && dd.first === 'L0公開頁面閱覽_權限群組' && dd.hasMode === 1, dd)
259
- await shot(page, 'D1-編輯所屬使用者-對話框')
260
- await closeDialog(page)
261
-
262
- //================ E. 編輯使用權限群組(VeCgrups) ================
263
- await nav(page, '管理使用者')
264
- await openDialog(page, '王小明', '權限群組', '編輯使用權限群組')
265
- let e1 = await evalH(page, `let g = gridOf('權限群組名稱'); let rows = rowsOf(g); let r = rows.find((r) => enableChk(r).checked); let mr = modeRoots(r)[0]; let shell = mr.querySelector('div[style*="border-radius"]'); return { heads: heads(g), enabledRowTxt: mr.innerText.trim(), shell: rc(shell), cell: rc(mr.closest('.ag-cell')), root: rc(mr) }`)
266
- check('E.編輯使用權限群組控制項在儲存格內', Math.abs(e1.shell.h - 22) < 0.6 && e1.shell.y >= e1.cell.y - 0.5 && e1.shell.btm <= e1.cell.btm + 0.5, e1)
267
- await page.mouse.move(e1.root.x + e1.root.w / 2, e1.root.y + e1.root.h / 2)
268
- await sleep(400)
269
- await shot(page, 'E1-編輯使用權限群組-hover', { x: 401, y: e1.root.y - 60, width: 798, height: 28 * 5 })
270
- await page.mouse.click(e1.root.x + e1.root.w / 2, e1.root.y + e1.root.h / 2)
271
- await page.waitForFunction(() => [...document.querySelectorAll('div[wtlp="modeSelect"]')].some((p) => p.getBoundingClientRect().width > 0), null, { timeout: 5000 })
272
- await sleep(500)
273
- await shot(page, 'E2-編輯使用權限群組-展開清單', { x: 401, y: e1.root.y - 60, width: 798, height: 28 * 7 })
274
- let t3 = await evalH(page, `let ls = leaf('權限群組名稱'); return rc(ls[0])`)
275
- await page.mouse.click(t3.x + 5, t3.y - 30)
276
- await sleep(400)
277
- await closeDialog(page)
278
-
279
- //================ F. 唯讀(展示)態: 關閉編輯模式後開「展示所屬權限群組」 ================
280
- await nav(page, '管理權限')
281
- let sw = await evalH(page, `let ls = leaf('編輯模式'); return rc(ls[0])`)
282
- await page.mouse.click(sw.x + sw.w / 2, sw.y + sw.h / 2)
283
- await sleep(800)
284
- await openDialog(page, '知識管理系統_操作編輯_權限', '權限群組', '展示所屬權限群組')
285
- let f1 = await evalH(page, `
286
- let g = gridOf('權限群組名稱'); let row0 = rowByText(g, '系統管理者編輯_權限群組');
287
- let cell = [...row0.querySelectorAll('.ag-cell')].find((c) => chipsOf(c).length > 0); let chips = chipsOf(cell);
288
- let seg = chips[0].children[0];
289
- let btnAll = btnAllOf(cell);
290
- return { first: chips[0].getAttribute('title'), hasMode: modeRoots(chips[0]).length, segText: seg.innerText.trim(), segBg: cs(seg, ['background-color'])['background-color'], segCursor: cs(seg, ['cursor'])['cursor'], hasSvg: !!chips[0].querySelector('svg'), chkDisabled: enableChk(row0).disabled, btnAllText: btnAll ? btnAll.innerText.trim() : null }
291
- `)
292
- check('F.唯讀態本項chip為純文字段(無下拉、無箭頭)且勾選框disabled', f1.hasMode === 0 && !f1.hasSvg && f1.chkDisabled && f1.segText === 'OR', f1)
293
- await shot(page, 'F1-展示所屬權限群組-唯讀態')
294
- let fb = await evalH(page, `let g = gridOf('權限群組名稱'); let row0 = rowByText(g, '系統管理者編輯_權限群組'); let cell = [...row0.querySelectorAll('.ag-cell')].find((c) => chipsOf(c).length > 0); return rc(btnAllOf(cell))`)
295
- await page.mouse.click(fb.x + fb.w / 2, fb.y + fb.h / 2)
296
- await page.waitForFunction(() => [...document.querySelectorAll('div[wtlp="relationChipsAll"]')].some((p) => p.getBoundingClientRect().width > 0), null, { timeout: 5000 })
297
- await sleep(500)
298
- let fp2 = await evalH(page, `let p = [...document.querySelectorAll('div[wtlp="relationChipsAll"]')].filter((p) => p.getBoundingClientRect().width > 0)[0]; return { n: chipsOf(p).length }`)
299
- check('F.唯讀態展開全部仍可看', fp2.n === 94, fp2)
300
- await shot(page, 'F2-展示所屬權限群組-展開全部')
301
- await closeDialog(page)
302
-
303
- //================ G. DPR 3 放大圖(對照修改前之 zoom-*.png) ================
304
- let z = await openPerm(3)
305
- await nav(z.page, '管理權限')
306
- await openDialog(z.page, '知識管理系統_操作編輯_權限', '權限群組', '編輯所屬權限群組')
307
- let zm = await evalH(z.page, `let g = gridOf('權限群組名稱'); let row0 = rowByText(g, '系統管理者編輯_權限群組'); let mr = modeRoots(row0)[0]; let vp = g.querySelector('.ag-body-viewport'); return { m: rc(mr), vp: rc(vp), row: rc(row0) }`)
308
- await z.page.mouse.move(5, 5)
309
- await sleep(300)
310
- await shot(z.page, 'G1-zoom-編輯所屬權限群組-前六列', { x: zm.vp.x, y: zm.vp.y - 60, width: zm.vp.w, height: 60 + 28 * 6 })
311
- await z.page.mouse.move(zm.m.x + zm.m.w / 2, zm.m.y + zm.m.h / 2)
312
- await sleep(500)
313
- await shot(z.page, 'G2-zoom-編輯所屬權限群組-hover本項chip', { x: zm.vp.x, y: zm.row.y - 30, width: zm.vp.w, height: 28 * 4 })
314
- await z.page.mouse.click(zm.m.x + zm.m.w / 2, zm.m.y + zm.m.h / 2)
315
- await z.page.waitForFunction(() => [...document.querySelectorAll('div[wtlp="modeSelect"]')].some((p) => p.getBoundingClientRect().width > 0), null, { timeout: 5000 })
316
- await sleep(500)
317
- await shot(z.page, 'G3-zoom-編輯所屬權限群組-展開清單', { x: zm.vp.x, y: zm.row.y - 30, width: zm.vp.w, height: 28 * 6 })
318
- await z.context.close()
319
-
320
- fs.writeFileSync(path.join(fdOut, 'verify.json'), JSON.stringify(out, null, 2))
321
- await browser.close()
322
- console.log(`\n[done] fails=${fails.length} ${JSON.stringify(fails)}`)
323
- }
324
-
325
- main().catch((err) => {
326
- console.error('[fail]', err)
327
- process.exitCode = 1
328
- //不留孤兒瀏覽器
329
- process.exit(1)
330
- })
@@ -1,193 +0,0 @@
1
- # 建議 w-aggrid-vue 調整
2
-
3
- > **結案與更正(2026-09-17,w-aggrid-vue 2.0.88 已發布)**
4
- >
5
- > - **建議二已採納**:`showKeys(keys, { applyOrder })` 已上線,預設仍為 `true`;另將傳入 `applyColumnState` 之狀態精簡為只帶 `colId` 與 `hide`,不再回灌 `getColumnState` 讀出之寬度、排序、固定等欄位(行為等價,且不再因查無欄位而拋錯)。本專案四個清單頁已改用 `applyOrder:false`,並以真實點擊驗收通過。
6
- > - **建議一改以範例提供、不加 API**(`src/AppSlotCellRenderAlign.vue`)。經實測,**範例之作法本身即正確,本文原載兩處敘述有誤,特此更正**:
7
- > 1. 原載「只做 ②(slot 內 wrapper)時 `height:100%` 無確定高度可解析,等於沒寫」**不成立**。實測只加 wrapper、不設 `opt.kpColStyle`,即得上 2.50/下 2.50(`tmp/probe-vcenter6.mjs`)。原因是 wrapper 為區塊級(flex),其百分比高度之包含塊為最近之區塊容器祖先 `.ag-cell`(`height:100%`,高度確定),而非元件庫那個行內 `<span>`。該敘述當時係推論、未實測。
8
- > 2. 故原載「兩段缺一不可」、以及建議實作中「`CellSlotRenderer` 之 `<span>` 必須一併處理,否則設了選項毫無效果」**亦不成立**;`opt.kpColStyle` 非必要。
9
- > - **範例未涵蓋、使用端須另注意之情形**(實測所得,供範例或文件補充參考):wrapper 只對「wrapper 的直接子層」置中。若子層本身是行內容器、其內才是固定高度之控制項(本專案之標籤列即是),wrapper 置中的是那個容器,內部控制項仍偏下(實測 3.77/1.23);須讓該容器本身成為 flex 置中容器才得 2.50/2.50。再往內若另有會繼承儲存格 27px 行高之區塊包裹層(本專案之 `WPopup` > `WTooltip` 觸發區即是),還會自成 27px 行框而再偏移,甚至溢出儲存格被裁切(實測「⋯ N」鈕上 6.03/下 −1.03),須於外層設 `line-height:0` 切斷繼承(`tmp/probe-vcenter9.mjs`)。
10
- >
11
- > 以下為原提案內文,保留原貌備查;與上列更正衝突之處以上列為準。
12
-
13
- ---
14
-
15
- **提出者**:w-web-perm 專案(2026-09-16)
16
- **對象版本**:`w-aggrid-vue` 2.0.87(`ag-grid-community` / `ag-grid-vue` ^31.3.4)
17
- **性質**:兩項皆為**向後相容之 opt-in 擴充**,不改既有行為、不需呼叫端配合升級。
18
-
19
- 本文所有數字皆為實機量測(Playwright + `getBoundingClientRect`),探測腳本路徑一併列出,可重跑。
20
- 本文**不含**「在本專案已可繞過」之抱怨——兩項都已確認呼叫端今天就能自行解決;提出的理由是**樣板重複與失敗無聲**,見各節之〈為何值得做在套件層〉。
21
-
22
- ---
23
-
24
- ## 建議一:補上儲存格垂直對齊選項 `defCellAlignV` / `kpCellAlignV`
25
-
26
- ### 現象
27
-
28
- 儲存格內若放高度固定的自訂控制項(本專案為 56×22 的合併模式控制項),於 balham 27px 列高下**不會垂直置中**:
29
-
30
- | 量測項 | 值 |
31
- |---|---|
32
- | 儲存格高 | 27.00 px(`line-height:27px`,上下 padding 皆 0) |
33
- | 控制項高 | 22.00 px |
34
- | 控制項上方留白 | **4.77 px** |
35
- | 控制項下方留白 | **0.23 px** |
36
-
37
- 控制項幾乎貼齊儲存格底線。100% 縮放下未被裁切,但視覺上明顯偏下。
38
-
39
- 探測:`tmp/probe-vcenter.mjs`、`tmp/probe-vcenter5.mjs`(本專案 `tmp/` 下)。
40
-
41
- ### 成因(非使用者算錯尺寸)
42
-
43
- 1. `ag-grid.css` 之 `.ag-cell` 為 `display:inline-block; height:100%`,儲存格內容走**行內排版**。
44
- 2. `CellSlotRenderer` 把 `cell-render` slot 的內容包進一個**無 class、無 style 的 `<span>`**(`WAggridVue.vue:183`:`return h('span', vnodes)`),該 span 預設 `display:inline`,實測高度 16px。
45
- 3. 呼叫端的控制項是行內層級盒,慣用 `vertical-align:middle`;而 CSS 的 `middle` 是把盒中線對齊「**基線 + x 高一半**」,不是行框中線。字型 x 高愈小,偏移愈大。
46
-
47
- 因此這不是誰把尺寸算錯,而是**行內對齊語義**與「把固定高度控制項放進固定列高」這個需求本來就不合。
48
-
49
- ### 呼叫端目前唯一可行的自解(已實測成功,但需兩段樣板)
50
-
51
- ```js
52
- // ① 欄位設定:使儲存格成為 flex 容器(align-items 保持預設 stretch)
53
- // → CellSlotRenderer 的 <span> 成為高度確定之 flex item(實測 25px)
54
- opt.kpColStyle = {
55
- mode: () => ({ display: 'flex' }),
56
- }
57
- ```
58
-
59
- ```html
60
- <!-- ② cell-render slot 內層自行再包一個 wrapper,才有確定高度可解析 height:100% -->
61
- <template v-slot:cell-render="props">
62
- <div v-if="props.key==='mode'" style="display:flex; align-items:center; height:100%;">
63
- <MyFixedHeightControl />
64
- </div>
65
- </template>
66
- ```
67
-
68
- 實測結果(`tmp/probe-vcenter5.mjs`,真的在 DOM 插入該 wrapper 後量測):**上 2.50 px/下 2.50 px**,精準置中。
69
-
70
- **兩段缺一不可**,這正是問題所在:
71
-
72
- | 只做 | 結果 |
73
- |---|---|
74
- | 只做 ①(儲存格 flex) | 控制項位置**完全不動**(仍 4.77/0.23)——span 被拉高了,但控制項在 span 內仍走行內對齊 |
75
- | 只做 ①+在儲存格上加 `align-items:center` | 3.77/1.23,**仍未置中**——置中的是 span,不是控制項 |
76
- | 只做 ② | `height:100%` 無確定高度可解析,等於沒寫 |
77
-
78
- 另已排除兩種看似自然的修法:
79
-
80
- - 控制項根部改 `inline-flex; align-items:center; height:27px; vertical-align:top` → 實測 top 1/bottom **−1**(盒溢出儲存格 1px)。
81
- - 把 `vertical-align` 改成固定像素 → 需綁定字型度量,換字型或語系即失準。
82
-
83
- ### 為何值得做在套件層
84
-
85
- 1. **與既有 API 不對稱**:套件已有 `defHeadAlignH` / `kpHeadAlignH` / `defCellAlignH` / `kpCellAlignH`(宣告於 `WAggridVue.vue:428-429,450-451`,實作於 `1704-1712` 的 `cellStyle['text-align']`)。**有水平沒有垂直**,使用者自然會找 `AlignV`,找不到就各自土法煉鋼。
86
- 2. **失敗是無聲的**:偏 2.27px 不會報錯、不會破版、e2e 若只比對像素也照樣過(因為基準圖本身就是偏的)。本專案是在為新元件逐項量測時才發現。
87
- 3. **樣板會擴散**:上述兩段要**逐欄、逐呼叫端**重寫,且 ② 必須寫在 slot 內層——這是一個「知道就會、不知道就永遠偏」的隱知識。
88
- 4. **只影響有設 AlignV 的欄**:不設即維持現行行為,零風險。
89
-
90
- ### 建議實作
91
-
92
- 沿用既有 `AlignH` 的完整慣例(宣告 → data 預設 → `setobj` 解析 → 併入 `cellStyle`):
93
-
94
- ```js
95
- // 1) JSDoc(比照 450-451 行)
96
- // @vue-prop {String} [opt.defCellAlignV='center'] 輸入cell預設之上下對齊字串,預設為'center'
97
- // @vue-prop {Object} [opt.kpCellAlignV={}] 輸入key對應cell之上下對齊字串物件,預設各key值為defCellAlignV
98
-
99
- // 2) data 預設(比照 544-545)
100
- defCellAlignV: null,
101
- kpCellAlignV: {},
102
-
103
- // 3) 解析(比照 2008-2014 之 defCellAlignH)
104
- vo.defCellAlignV = 'center'
105
- if (arrHas(vo.opt.defCellAlignV, ['top', 'center', 'bottom'])) {
106
- vo.defCellAlignV = vo.opt.defCellAlignV
107
- }
108
- vo.kpCellAlignV = setobj(vo.keys, () => vo.defCellAlignV, vo.opt.kpCellAlignV)
109
-
110
- // 4) 併入 cellStyle(比照 1704-1712)
111
- let cellStyle = {
112
- 'text-align': vo.kpCellAlignH[key],
113
- }
114
- let av = vo.kpCellAlignV[key]
115
- if (av === 'top' || av === 'center' || av === 'bottom') {
116
- cellStyle = merge(cellStyle, {
117
- 'display': 'flex',
118
- 'align-items': av === 'top' ? 'flex-start' : (av === 'bottom' ? 'flex-end' : 'center'),
119
- //水平仍由既有 text-align 決定, 故 flex 主軸起點對齊, 不覆寫呼叫端的 kpCellAlignH 語義
120
- 'justify-content': 'flex-start',
121
- })
122
- }
123
- ```
124
-
125
- **關鍵點:只改儲存格還不夠,`CellSlotRenderer` 的 `<span>` 必須一併處理**——否則就是上表「只做 ①」那一列,使用者設了 `AlignV` 卻毫無效果,比沒有這個選項更糟。最小改法是讓該 span 在 `AlignV` 生效時撐滿並自身置中:
126
-
127
- ```js
128
- // WAggridVue.vue:183 附近
129
- return h('span', { style: { display: 'flex', alignItems: 'inherit', height: '100%' } }, vnodes)
130
- ```
131
-
132
- `alignItems: 'inherit'` 可直接沿用儲存格上由 `AlignV` 設好的值,不必再傳一次參數;`height:100%` 讓內層 `height:100%` 得以解析(本專案實測即靠這一點才拿到 2.50/2.50)。
133
-
134
- > 若不想讓 `CellSlotRenderer` 預設帶樣式(顧慮既有使用者),可改為僅在該欄有設 `AlignV` 時才透過 `cellRendererParams` 傳旗標進去,維持預設路徑位元不變。
135
-
136
- ### 驗收建議
137
-
138
- 以「27px 列高 × 22px 固定高度控制項」量上下留白:未設 `AlignV` 時維持 4.77/0.23(確認向後相容),設 `center` 後為 2.50/2.50。
139
-
140
- ---
141
-
142
- ## 建議二:`showKeys` 增加「只切換顯示、不改欄序」的選項
143
-
144
- ### 現象
145
-
146
- 呼叫端以「顯示欄位挑選」勾選框控制欄位顯示時,**使用者把某欄取消勾選再重新勾選,該欄會跑到最右邊**,而不是回到原本的位置。本專案四個清單頁皆如此。
147
-
148
- ### 成因鏈(三段,皆可追)
149
-
150
- 1. `w-component-vue` 之 `WInputCheckbox.vue:379-381`:重新勾選時 `valueTrans.push(item.data)`,**一律追加到 v-model 陣列尾端**,不還原該項在 `items` 中的位置。
151
- 2. 呼叫端把該 v-model 陣列原樣交給 `showKeys()`。
152
- 3. `w-aggrid-vue` 之 `showKeys`(`WAggridVue.vue:2211` 起)依 **`keys_new` 的順序**組出 `csn`,再以 `applyColumnState({ state: csn, applyOrder: true })` 套用——註解明寫「要依照 csn 調整欄位順序」,所以欄序完全跟著勾選陣列走。
153
-
154
- 第 1 段是 `w-component-vue` 的事(勾選群組的值本質是集合,序列化順序理應沿用 `items` 宣告序,而非點擊序),已另行反映。**但第 3 段在本套件**:`showKeys` 把「顯示哪些」與「排成什麼順序」**綁成同一件事**,呼叫端沒有辦法只要前者。
155
-
156
- ### 建議實作
157
-
158
- `applyOrder` 改為可由呼叫端決定,預設維持現行值以保向後相容:
159
-
160
- ```js
161
- showKeys: function(keys_new, opt) {
162
- let applyOrder = get(opt, 'applyOrder', true) //預設 true, 與現行行為相同
163
- // …既有邏輯不變…
164
- vo.getApi().applyColumnState({
165
- state: csn,
166
- applyOrder,
167
- })
168
- }
169
- ```
170
-
171
- `applyOrder:false` 時,ag-grid 只套用 `hide`,欄序維持目前狀態;呼叫端即可把 `showKeys` 當成純粹的顯示開關,不必自己先依宣告序排序一次。
172
-
173
- ### 為何值得做在套件層
174
-
175
- - 現行簽章讓呼叫端**無法表達「只切顯示」**這個最常見的意圖;每個呼叫端都得在外面自己排序一次(且要知道「為什麼要排」,否則永遠不會想到)。
176
- - 預設值不變,既有呼叫端零影響。
177
- - 與建議一同屬一類:**把隱知識收進套件**,而不是讓每個使用者各自踩一次。
178
-
179
- ---
180
-
181
- ## 附錄:本文引用之量測與程式位置
182
-
183
- | 項目 | 位置 |
184
- |---|---|
185
- | 儲存格預設排版 | `node_modules/ag-grid-community/styles/ag-grid.css:1822`(`.ag-cell{display:inline-block;height:100%}`) |
186
- | slot 包裹層 | `WAggridVue.vue:183`(`return h('span', vnodes)`) |
187
- | 既有水平對齊 API | `WAggridVue.vue:428-429,450-451`(宣告)、`522-523,544-545`(預設)、`1836-1847,2008-2014`(解析)、`1704-1712`(併入 `cellStyle`) |
188
- | 既有樣式逃生口 | `WAggridVue.vue:448`(`opt.kpColStyle`)、`1704-1712`(merge 進 `cellStyle`) |
189
- | `showKeys` 與 `applyOrder` | `WAggridVue.vue:2211` 起 |
190
- | 勾選陣列追加到尾端 | `w-component-vue` 之 `WInputCheckbox.vue:379-381` |
191
- | 垂直置中量測(改前 4.77/0.23) | `tmp/probe-vcenter.mjs` |
192
- | 候選方案排除(cell 層 flex 僅 3.77/1.23) | `tmp/probe-vcenter3.mjs` |
193
- | 自解方案定案(2.50/2.50) | `tmp/probe-vcenter5.mjs` |
@@ -1,76 +0,0 @@
1
- # 建議 w-component-vue 與 wsemi 調整
2
-
3
- **提出者**:w-web-perm 專案(2026-09-17)
4
- **對象版本**:本專案 `node_modules` 內之 `w-component-vue`、`wsemi`(2026-09-17 安裝版)
5
- **來源**:本專案關聯對話框標籤重做後之外部複審(GPT-5.6 Sol),經逐條對照源碼確認後提出。
6
-
7
- 共兩項,性質不同:
8
-
9
- | # | 套件 | 性質 | 本專案現況 |
10
- |---|---|---|---|
11
- | 一 | w-component-vue | **缺陷**:卸載時少移除一個監聽,一行即可修 | 無法在專案端繞過 |
12
- | 二 | wsemi | **行為限制**:緩慢連續變化偵測不到 | 本專案標籤收合已改用 `ResizeObserver` 繞過;其他使用者(含 `w-aggrid-vue`)仍受影響 |
13
-
14
- ---
15
-
16
- ## 建議一:`buildPopper` 卸載時移除 scroll 監聽用錯函式
17
-
18
- ### 位置與現象
19
-
20
- `src/js/buildPopper.mjs`:
21
-
22
- ```js
23
- //mounted(第 70 行起)
24
- window.addEventListener('scroll', _vo.windowScroll, false) //第 228 行
25
-
26
- //destroy(第 232 行起)
27
- window.removeEventListener('mousemove', _vo.windowMousemove, false)
28
- window.removeEventListener('mousedown', _vo.windowMousedown, false)
29
- window.removeEventListener('mouseup', _vo.windowMouseup, false)
30
- window.removeEventListener('scroll', _vo.windowMouseup, false) //第 238 行:應為 _vo.windowScroll
31
- ```
32
-
33
- `removeEventListener` 須傳入與掛上時同一個函式才會移除;第 238 行傳的是 `windowMouseup`,而 scroll 從未以此函式掛上,故該行實際不移除任何東西。
34
-
35
- ### 後果
36
-
37
- - `WTooltip.vue` 每次掛載即經 `buildPopper` 掛上 scroll 監聽,`beforeDestroy`(第 255~264 行)呼叫 `bp.destroy()` 卻留下它。**每個 `WTooltip`/`WPopup`(及內含它們之 `WTextSuggestCore` 等)掛載一次,window 上就永久多一個 scroll 處理器**,且該處理器之閉包持有已卸載元件之 `vo`,元件無法被回收。
38
- - 使用者畫面上看不到(處理器內以 `vo[keyShow]` 判斷,已隱藏者走「不處理」分支),主控台亦無錯誤;後果是長時間操作後每次捲動要執行的失效處理器與保留之記憶體逐次增加。
39
- - 在表格儲存格內大量使用者特別明顯:本專案關聯對話框每列一個模式控制項、每列一個溢出指示,每開關一次對話框、每次視窗寬度變化導致標籤重排,都會建立並卸載一批。
40
-
41
- ### 建議修正
42
-
43
- ```js
44
- window.removeEventListener('scroll', _vo.windowScroll, false)
45
- ```
46
-
47
- ### 驗收方式
48
-
49
- 以 Chrome DevTools Protocol `DOMDebugger.getEventListeners`(目標 `window`)計數 `scroll` 監聽數:反覆掛載/卸載同一個 `WTooltip` N 次,修正前計數增加 N,修正後維持不變。
50
-
51
- ---
52
-
53
- ## 建議二:`domDetect` 以前一次取樣為基準,連續小幅變化永不觸發
54
-
55
- ### 位置與現象
56
-
57
- `src/domDetect.mjs`(`w-component-vue` 之 `v-domresize` 指令即以此實作,`src/js/domResize.mjs:23`;`w-aggrid-vue` 之 `WAggridVue.vue:629` 亦直接使用):
58
-
59
- - 每 `timeInterval`(預設 20ms)取樣一次(第 202 行)。
60
- - 與基準 `sd` 比較寬高差,差值須 **大於** `tolerancePixel`(預設 1)才觸發(第 225~226 行)。
61
- - **每次取樣後不論是否觸發,一律 `sd = snew`**(第 275 行)。
62
-
63
- 故基準每 20ms 跟著移動。元素寬度若以每次取樣 ≤ 1px 的速度變化(緩慢拖曳視窗邊緣、父層動畫、逐步調整),每次比較的差都 ≤ 1,**累積變化再大也永遠不觸發**;停下後也不會補觸發。
64
-
65
- ### 實測
66
-
67
- 本專案以 Playwright 逐步改變視窗寬度:每步 1px 時 `w-aggrid-vue` 之欄寬自動填滿不重算(欄位總寬與表格寬度持續偏離),每步 2px 時才重算。本專案關聯對話框之標籤收合原以 `v-domresize` 重算,同樣受影響,已改用原生 `ResizeObserver`。
68
-
69
- ### 建議修正(二擇一,皆不改既有觸發時機以外之行為)
70
-
71
- 1. **只在觸發時更新基準**:把第 274~275 行移入 `if (bw || bh)` 區塊內(未觸發時保留舊基準),累積變化超過容差即觸發一次。`sold` 語意變為「上次觸發時之尺寸」,對使用端較直觀。
72
- 2. **改以 `ResizeObserver` 為主、輪詢為後備**:瀏覽器支援時以 `ResizeObserver` 觀察元素(非同步、無輪詢成本、無容差問題),保留 `resizeWithWindow` 之既有事件形狀;不支援時退回現行輪詢並套用第 1 項。可一併省去每實例每 20ms 之計時器(表格內大量使用時效益明顯)。
73
-
74
- ### 驗收方式
75
-
76
- 以每步 1px 連續改變元素寬度共 10px:修正前 `resize` 事件 0 次;修正後至少 1 次,且最後一次事件之 `snew.offsetWidth` 等於最終寬度。