w-web-perm 1.1.4 → 1.1.5

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 (36) hide show
  1. package/README.md +1 -1
  2. package/dist/css/app.6918ec45.css +1 -0
  3. package/dist/css/{chunk-vendors.2bcfce08.css → chunk-vendors.80aa3ae8.css} +1 -1
  4. package/dist/index.tmp +2 -2
  5. package/dist/js/app-legacy.833f9566.js +1 -0
  6. package/dist/js/app.833f9566.js +1 -0
  7. package/dist/js/{chunk-vendors-legacy.fc37f076.js → chunk-vendors-legacy.fbf771bd.js} +16 -16
  8. package/dist/js/{chunk-vendors.fc37f076.js → chunk-vendors.fbf771bd.js} +16 -16
  9. package/dist/w-web-perm.umd.js +2 -2
  10. package/dist/w-web-perm.umd.js.map +1 -1
  11. package/docs/WWebPerm.html +7 -7
  12. package/docs/global.html +1545 -60
  13. package/docs/index.html +2 -2
  14. package/docs/server_WWebPerm.mjs.html +53 -30
  15. package/docs/server_maskLog.mjs.html +176 -0
  16. package/docs/server_procCore.mjs.html +2 -2
  17. package/docs/server_wrapInjected.mjs.html +163 -0
  18. package/docs/src_fetchJson.mjs.html +31 -5
  19. package/docs/src_js_legendFit.mjs.html +225 -0
  20. package/package.json +5 -5
  21. package/public/index.html +2 -0
  22. package/server/WWebPerm.mjs +51 -28
  23. package/server/maskLog.mjs +104 -0
  24. package/server/procStaInfor.mjs +32 -14
  25. package/server/wrapInjected.mjs +91 -0
  26. package/src/components/LayoutContentStaInfor.vue +74 -5
  27. package/src/components/RelationChips.vue +11 -31
  28. package/src/fetchJson.mjs +28 -2
  29. package/src/getPerm.mjs +9 -6
  30. package/src/getPermUserInfor.mjs +12 -9
  31. package/src/js/legendFit.mjs +153 -0
  32. package/srv.mjs +20 -1
  33. package/tableTags-web-perm.json +1 -1
  34. package/dist/css/app.506ce4d8.css +0 -1
  35. package/dist/js/app-legacy.8e615442.js +0 -1
  36. package/dist/js/app.8e615442.js +0 -1
@@ -0,0 +1,104 @@
1
+ import isestr from 'wsemi/src/isestr.mjs'
2
+ import isarr from 'wsemi/src/isarr.mjs'
3
+
4
+
5
+ //maskLog: 權杖與網址寫入 console / srLog 前之遮罩(M 契約, spec/設計要點與取捨.md ADR-023)
6
+ //四 repo 一致(w-web-sso server/srLog.mjs 之 maskToken 與 src/maskUrl.mjs、w-web-api / w-web-task 之 maskTok), 以同一組測資守護(test/unit-maskLog.test.mjs)
7
+ //why: 原 console 站點原樣印出 token / req.query / authorization(Hapi 對重複之 query 參數給陣列, 型別檢查失敗之分支正是印出處), verifyConn 之 srLog 原樣記 referer
8
+
9
+
10
+ //safe: 露出字元之控制字元換成 '?', 避免請求端以 CR/LF 偽造 log 行
11
+ function safe(s) {
12
+ return Array.from(s, (c) => {
13
+ let x = c.charCodeAt(0)
14
+ return (x < 32 || x === 127) ? '?' : c
15
+ }).join('')
16
+ }
17
+
18
+
19
+ /**
20
+ * 權杖遮罩
21
+ * - 空值(undefined / null / '')回 ''
22
+ * - 陣列逐元素遮罩
23
+ * - 字串(長度 n)露出前後各 k=min(4, floor(n/8)) 字: k=0 只回 '(len=n)', 否則 '前k...後k(len=n)'; 36 字元之 session token 為前 4 後 4(與 w-web-sso 格式相同)
24
+ * - 其他型別只回 '(typeof)', 絕不輸出原值
25
+ *
26
+ * @param {*} token 輸入權杖
27
+ * @returns {String|Array} 回傳遮罩後字串, 輸入陣列時回傳陣列
28
+ */
29
+ function maskTok(token) {
30
+ if (isarr(token)) {
31
+ return token.map(maskTok)
32
+ }
33
+ if (token === undefined || token === null || token === '') {
34
+ return ''
35
+ }
36
+ if (!isestr(token)) {
37
+ return `(${typeof token})`
38
+ }
39
+ let n = token.length
40
+ let k = Math.min(4, Math.floor(n / 8))
41
+ if (k === 0) {
42
+ return `(len=${n})`
43
+ }
44
+ return `${safe(token.slice(0, k))}...${safe(token.slice(-k))}(len=${n})`
45
+ }
46
+
47
+
48
+ /**
49
+ * 查詢參數遮罩: 淺拷貝, 鍵名小寫為 token 者以 maskTok 處理, 其餘原樣(供印出 req.query)
50
+ * 非物件輸入依 maskTok 處理
51
+ *
52
+ * @param {*} q 輸入查詢參數物件(Hapi 之 req.query 為 null-prototype 物件)
53
+ * @returns {Object|String|Array} 回傳遮罩後之淺拷貝
54
+ */
55
+ function maskQuery(q) {
56
+ if (q === null || typeof q !== 'object' || isarr(q)) {
57
+ return maskTok(q)
58
+ }
59
+ let r = {}
60
+ for (let k of Object.keys(q)) {
61
+ r[k] = (k.toLowerCase() === 'token') ? maskTok(q[k]) : q[k]
62
+ }
63
+ return r
64
+ }
65
+
66
+
67
+ /**
68
+ * 網址遮罩: http(s) 只留 origin + pathname, 捨棄 query / fragment / userinfo(其內常含權杖, 如 ?token=)
69
+ * - 陣列逐元素處理; 空值回 ''; 其他非字串只回 '(typeof)'
70
+ * - 非 http(s) 之網址(如 data:、javascript:)路徑即全部內容, 只回 protocol(同 w-web-sso src/maskUrl.mjs)
71
+ * - 無法解析回 '(invalid-url)'
72
+ *
73
+ * @param {*} u 輸入網址
74
+ * @returns {String|Array} 回傳可寫入 log 之字串, 輸入陣列時回傳陣列
75
+ */
76
+ function maskUrl(u) {
77
+ if (isarr(u)) {
78
+ return u.map(maskUrl)
79
+ }
80
+ if (u === undefined || u === null || u === '') {
81
+ return ''
82
+ }
83
+ if (!isestr(u)) {
84
+ return `(${typeof u})`
85
+ }
86
+ let x = null
87
+ try {
88
+ x = new URL(u)
89
+ }
90
+ catch (err) {
91
+ return '(invalid-url)'
92
+ }
93
+ if (x.protocol !== 'http:' && x.protocol !== 'https:') {
94
+ return x.protocol
95
+ }
96
+ return `${x.origin}${x.pathname}`
97
+ }
98
+
99
+
100
+ export {
101
+ maskTok,
102
+ maskQuery,
103
+ maskUrl
104
+ }
@@ -6,13 +6,23 @@ import staEvent from './staLogs/staEvent.callWorker.mjs'
6
6
  import staEventTable from './staLogs/staEventTable.callWorker.mjs'
7
7
 
8
8
 
9
+ //mock 之事件名單:
10
+ // true(預設 5 種):既有 e2e 標準圖所用;
11
+ // 'many'(13 種):本系統實際會記錄之事件名(取自本機 log),供圖例列數跨門檻之 e2e(1440 寬時一般式 2 列,勾全部加總並切中文為 3 列)
12
+ let MOCK_EVENTS = ['verifyConn', 'updateTargets-success', 'checkUser-error', 'api/getPerm-success', 'getWebInfor-success']
13
+ let MOCK_EVENTS_MANY = ['api/getPermUserInfor-success', 'api/getUserByToken-success', 'getStaEvent-success', 'getStaEventTable-success', 'getTokenUser-error', 'getWebInfor-success', 'updateGrups-success', 'updatePemis-success', 'updateTabItems-pickKeysOnly', 'updateTargets-success', 'updateUsers-success', 'verifyConn', 'verifyConn-error']
14
+ function getMockEvents(mock) {
15
+ return mock === 'many' ? MOCK_EVENTS_MANY : MOCK_EVENTS
16
+ }
17
+
18
+
9
19
  //mock 確定性資料集(供 e2e 統計圖穩定用):固定起點時間 + 固定 sin 計數,不依 now / log → 每次完全相同。
10
- //觸發:opt.mock=true(由 settings.json staEventMock 經 srv.mjs → WWebPerm 傳入)。非 mock 時走真實 staEvent。
11
- function genMockStaEvent(timeInterval = 'hr') {
20
+ //觸發:opt.mock 為 true 或 'many'(由 settings.json staEventMock 經 srv.mjs → WWebPerm 傳入)。非 mock 時走真實 staEvent。
21
+ function genMockStaEvent(timeInterval = 'hr', mock = true) {
12
22
  let fmt = timeInterval === 'day' ? 'YYYY-MM-DD' : 'YYYY-MM-DDTHH'
13
23
  let unit = timeInterval === 'hr' ? 'hour' : 'day'
14
24
  let nBuckets = timeInterval === 'hr' ? 48 : 7
15
- let events = ['verifyConn', 'updateTargets-success', 'checkUser-error', 'api/getPerm-success', 'getWebInfor-success']
25
+ let events = getMockEvents(mock)
16
26
  let base = ot('2025-01-01T00:00:00') //固定起點,不依 now
17
27
  let rs = []
18
28
  for (let i = 0; i < nBuckets; i++) {
@@ -30,15 +40,23 @@ function genMockStaEvent(timeInterval = 'hr') {
30
40
 
31
41
 
32
42
  //mock 確定性資料集(供 e2e 統計表穩定用):固定窗計數,不依 now / log → 每次完全相同。
33
- //5 個 event 與 genMockStaEvent 同名單;各事件滿足 last1Day>last8Hour>last4Hour>last1Hour,且各事件 last1Day 互不相同(排序明確、上多下少)。
34
- function genMockStaEventTable() {
35
- let rs = [
36
- { event: 'verifyConn', last1Day: 240, last8Hour: 90, last4Hour: 50, last1Hour: 15 },
37
- { event: 'updateTargets-success', last1Day: 180, last8Hour: 70, last4Hour: 38, last1Hour: 11 },
38
- { event: 'checkUser-error', last1Day: 120, last8Hour: 45, last4Hour: 24, last1Hour: 7 },
39
- { event: 'api/getPerm-success', last1Day: 90, last8Hour: 33, last4Hour: 18, last1Hour: 5 },
40
- { event: 'getWebInfor-success', last1Day: 60, last8Hour: 22, last4Hour: 12, last1Hour: 3 },
41
- ]
43
+ //事件名單與 genMockStaEvent 相同;各事件滿足 last1Day>last8Hour>last4Hour>last1Hour,且各事件 last1Day 互不相同(排序明確、上多下少)。
44
+ function genMockStaEventTable(mock = true) {
45
+ if (mock !== 'many') {
46
+ let rs = [
47
+ { event: 'verifyConn', last1Day: 240, last8Hour: 90, last4Hour: 50, last1Hour: 15 },
48
+ { event: 'updateTargets-success', last1Day: 180, last8Hour: 70, last4Hour: 38, last1Hour: 11 },
49
+ { event: 'checkUser-error', last1Day: 120, last8Hour: 45, last4Hour: 24, last1Hour: 7 },
50
+ { event: 'api/getPerm-success', last1Day: 90, last8Hour: 33, last4Hour: 18, last1Hour: 5 },
51
+ { event: 'getWebInfor-success', last1Day: 60, last8Hour: 22, last4Hour: 12, last1Hour: 3 },
52
+ ]
53
+ return rs
54
+ }
55
+ //'many': 依名單順序遞減, last1Day 自 400 起每事件少 20(互不相同)
56
+ let rs = MOCK_EVENTS_MANY.map((event, k) => {
57
+ let last1Day = 400 - k * 20
58
+ return { event, last1Day, last8Hour: Math.round(last1Day * 0.4), last4Hour: Math.round(last1Day * 0.2), last1Hour: Math.round(last1Day * 0.05) }
59
+ })
42
60
  return rs
43
61
  }
44
62
 
@@ -66,7 +84,7 @@ function proc(opt = {}) {
66
84
 
67
85
  //mock 模式回固定確定性資料集
68
86
  if (mock) {
69
- return genMockStaEvent(timeInterval)
87
+ return genMockStaEvent(timeInterval, mock)
70
88
  }
71
89
 
72
90
  //staEvent
@@ -98,7 +116,7 @@ function proc(opt = {}) {
98
116
 
99
117
  //mock 模式回固定確定性資料集
100
118
  if (mock) {
101
- return genMockStaEventTable()
119
+ return genMockStaEventTable(mock)
102
120
  }
103
121
 
104
122
  //staEventTable
@@ -0,0 +1,91 @@
1
+ import get from 'lodash-es/get.js'
2
+ import isestr from 'wsemi/src/isestr.mjs'
3
+ import isarr from 'wsemi/src/isarr.mjs'
4
+ import isfun from 'wsemi/src/isfun.mjs'
5
+
6
+
7
+ //wrapInjected: 部署方注入函數(getUserByToken / verifyClientUser / verifyAppUser)之包裝(W 契約, spec/設計要點與取捨.md ADR-023)
8
+ //why: 注入函數拋錯或 reject 時原會被 await 原樣上拋, 其原文經 pm2resolve 回前端、經 verifyConn 之 msg 入 srLog、經 error 事件印出;
9
+ //部署方常以 w-web-sso 對外 helper(≤1.1.4)包裝 getUserByToken, 其 reject 字串含已代入系統介接權杖與使用者權杖之完整網址(修正前探測實證)。
10
+ //包裝後失敗視同否定結果(由呼叫端給 failValue), 走各呼叫點既有之查無 / 無權限分支回自家 key; 上游原文不回前端、不入 log。
11
+
12
+
13
+ //keysUpstream: 上游失敗可照記之 key 白名單 = w-web-sso 對外 helper 之 K 契約 14 個 key(w-web-sso ADR-068)
14
+ //與 sso 之同步點: sso 之 K 契約增減 key 時須同步本表(見 CLAUDE_rulebook.md「權杖不外洩」)
15
+ let keysUpstream = [
16
+ 'invalidUrl',
17
+ 'invalidTokenSelf',
18
+ 'invalidTokenTar',
19
+ 'invalidUserIdTar',
20
+ 'noTokenKeyValueInUrl',
21
+ 'noTokenKeyUserIdInUrl',
22
+ 'noTokenInUrl',
23
+ 'cannotGetUserByUrl',
24
+ 'cannotGetUsersByUrl',
25
+ 'cannotGetUserDataByUrl',
26
+ 'cannotGetUsersDataByUrl',
27
+ 'noUserDataByUrl',
28
+ 'noUsersDataByUrl',
29
+ 'noUserDataAfterConvert',
30
+ ]
31
+
32
+
33
+ //reName: Error.name 之名稱形狀(同 w-web-sso helper 診斷之 errName), 不符者不記(name 亦可被設為任意字串)
34
+ let reName = /^[A-Za-z]{1,40}$/
35
+
36
+
37
+ /**
38
+ * 描述上游失敗, 供寫入 log: 絕不含上游原文
39
+ * - reject 值(字串)或其 message 屬白名單 key → { upstream: key }
40
+ * - 其餘 → { upstreamType, upstreamName }: 型別(null / array / typeof)與名稱形狀之 Error.name(無則 '')
41
+ *
42
+ * @param {*} err 輸入注入函數拋出或 reject 之值
43
+ * @returns {Object} 回傳描述物件
44
+ */
45
+ function describeUpstream(err) {
46
+ let key = isestr(err) ? err : get(err, 'message')
47
+ if (isestr(key) && keysUpstream.includes(key)) {
48
+ return { upstream: key }
49
+ }
50
+ let upstreamType = (err === null) ? 'null' : (isarr(err) ? 'array' : typeof err)
51
+ let name = get(err, 'name')
52
+ let upstreamName = (isestr(name) && reName.test(name)) ? name : ''
53
+ return { upstreamType, upstreamName }
54
+ }
55
+
56
+
57
+ /**
58
+ * 包裝注入函數: 回傳(同步或 Promise)原樣回傳; 同步拋錯或 reject 回 failValue, 並以 describeUpstream(err) 呼叫 onFail
59
+ * 包裝後一律回 Promise(呼叫端既有之 ispm 判斷照常)
60
+ *
61
+ * @param {Function} fn 輸入注入函數
62
+ * @param {Object} [opt={}] 輸入設定物件
63
+ * @param {*} [opt.failValue=null] 輸入失敗時之回傳值(getUserByToken 為 null 等同查無, verify* 為 false 等同無權限)
64
+ * @param {Function} [opt.onFail=null] 輸入失敗時之紀錄函數, 只收到 describeUpstream 之結果(收不到上游原文); 其自身拋錯不影響回傳
65
+ * @returns {Function} 回傳包裝後之函數
66
+ */
67
+ function wrapInjected(fn, opt = {}) {
68
+ let failValue = get(opt, 'failValue', null)
69
+ let onFail = get(opt, 'onFail', null)
70
+ return async (...args) => {
71
+ try {
72
+ return await fn(...args) //await 於 try 內: 同步拋錯、reject 與 thenable 之 reject 皆攔截
73
+ }
74
+ catch (err) {
75
+ if (isfun(onFail)) {
76
+ try {
77
+ onFail(describeUpstream(err))
78
+ }
79
+ catch (e) {} //紀錄失敗不改變「失敗=否定結果」之語意
80
+ }
81
+ return failValue
82
+ }
83
+ }
84
+ }
85
+
86
+
87
+ export {
88
+ keysUpstream,
89
+ describeUpstream
90
+ }
91
+ export default wrapInjected
@@ -80,9 +80,12 @@
80
80
 
81
81
  </div>
82
82
 
83
+ <!-- autoresize: 容器尺寸與圖不一致時 WEchartsVue(w-echarts-vue ≥1.0.15, 含圖剛建立之同一幀)先 resize 並 flush, 再呼叫 onResize(fitLegend) 重判圖例類型 -->
83
84
  <WEchartsVue
85
+ ref="chartEvent"
84
86
  style="width:100%; height:300px;"
85
87
  :options="optEvent"
88
+ :autoresize="autoresizeEvent"
86
89
  v-if="optEvent"
87
90
  ></WEchartsVue>
88
91
 
@@ -186,10 +189,16 @@ import debounce from 'wsemi/src/debounce.mjs'
186
189
  import isearr from 'wsemi/src/isearr.mjs'
187
190
  import isestr from 'wsemi/src/isestr.mjs'
188
191
  import WEchartsVue from 'w-echarts-vue/src/components/WEchartsVue.vue'
192
+ import iniEcharts from 'w-echarts-vue/src/js/iniEcharts.mjs'
189
193
  import { mdiChartBoxOutline, mdiTableLarge } from '@mdi/js/mdi.js'
190
194
  import WIcon from 'w-component-vue/src/components/WIcon.vue'
191
195
  import WIconLoading from 'w-component-vue/src/components/WIconLoading.vue'
192
196
  import WTextSelect from 'w-component-vue/src/components/WTextSelect.vue'
197
+ import { GRID_TOP, genLegend, decideLegendType, createLegendMeasurer } from '../js/legendFit.mjs'
198
+
199
+
200
+ //echarts, 與 WEchartsVue 同一份(iniEcharts 回傳同一命名空間), 供圖例離屏量測使用
201
+ let echarts = iniEcharts()
193
202
 
194
203
 
195
204
  //時間分組下拉之結構寬度(px): 左右內距 8+8、左右邊框 1+1、展開圖標 18、圖標與文字間隙 2 (同 w-web-sso LayoutContentStaInfor / w-web-api LayoutContentStats 之 SEL_STRUCT_WIDTH)
@@ -235,6 +244,12 @@ export default {
235
244
 
236
245
  optEvent: null,
237
246
 
247
+ //事件圖例類型: 一般式放得下(不壓到最上方刻度)維持 plain, 否則 scroll(單列+翻頁); 由 fitLegend 依圖寬決定, genOpt 重建 option 時沿用. 不讀入模板, 改變不觸發重繪
248
+ legendType: 'plain',
249
+
250
+ //WEchartsVue 之 autoresize: 容器尺寸改變後先 chart.resize() 並 flush 再呼叫 onResize(見模板註解)
251
+ autoresizeEvent: { throttle: 100, onResize: this.fitLegend },
252
+
238
253
  eventTable: [],
239
254
 
240
255
  }
@@ -244,6 +259,9 @@ export default {
244
259
 
245
260
  let vo = this
246
261
 
262
+ //legendMeasurer, 圖例離屏量測器(非響應式屬性), 於 beforeDestroy 釋放
263
+ vo.legendMeasurer = createLegendMeasurer(echarts)
264
+
247
265
  //時間分組下拉觸發區字型: 字級 0.875rem(同原 text-sm)× 根字級, 字族/字重承襲本頁; 供 timeIntervalSelWidth 以 canvas 量最長選項文字實寬(同 sso)
248
266
  let cs = getComputedStyle(vo.$el)
249
267
  let rootPx = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16
@@ -259,6 +277,18 @@ export default {
259
277
  vo.errMsg = vo.$t('getDataError')
260
278
  })
261
279
 
280
+ },
281
+ beforeDestroy: function() {
282
+ // console.log('beforeDestroy')
283
+
284
+ let vo = this
285
+
286
+ //dispose, 釋放圖例離屏量測實例
287
+ if (vo.legendMeasurer) {
288
+ vo.legendMeasurer.dispose()
289
+ vo.legendMeasurer = null
290
+ }
291
+
262
292
  },
263
293
  computed: {
264
294
 
@@ -348,7 +378,7 @@ export default {
348
378
  return colors[k % colors.length]
349
379
  }
350
380
 
351
- //getName: count 系列顯示為「全部」, 其餘 event 名照原字串
381
+ //getName: count 系列顯示為「全部加總」(staTotal; eng 為 Total), 其餘 event 名照原字串
352
382
  let getName = (valueKey) => {
353
383
  if (valueKey === 'count') {
354
384
  return vo.$t('staTotal')
@@ -392,13 +422,11 @@ export default {
392
422
  grid: {
393
423
  left: '3%',
394
424
  right: '4%',
425
+ top: GRID_TOP, //明寫(同 echarts 6.1.0 預設 65): 圖例判定之上緣幾何以此為準, 見 legendFit.mjs
395
426
  bottom: '3%',
396
427
  containLabel: true
397
428
  },
398
- legend: {
399
- show: true,
400
- top: 0, //須明給: echarts 6 起 legend 預設由 top:0 改為 bottom, 不給會落到底部壓住 x 軸時間標籤
401
- },
429
+ legend: genLegend(vo.legendType), //一般式或捲動式依圖寬決定(fitLegend); 設定之單一來源見 legendFit.mjs genLegend
402
430
  xAxis: [
403
431
  {
404
432
  type: 'category',
@@ -483,6 +511,47 @@ export default {
483
511
  //optEvent
484
512
  vo.optEvent = vo.genOpt(vo.$t('staEventTitle'), vo.freqEvent, ksEvent)
485
513
 
514
+ //fitLegend: option 更新後判定圖例類型. nextTick 時 WEchartsVue 已於同一輪 flush 內 setOption;
515
+ //圖剛建立與容器改寬落在同一幀者, 由 WEchartsVue 偵測到容器與圖尺寸不一致而 resize 後經 onResize 再判
516
+ vo.$nextTick(() => {
517
+ vo.fitLegend()
518
+ })
519
+
520
+ },
521
+
522
+ fitLegend: function() {
523
+ //依圖寬決定事件圖例類型: 一般式圖例放得下(底緣 ≤ LEGEND_MAX_BOTTOM)維持 plain, 否則 scroll; 規則與量測見 src/js/legendFit.mjs
524
+ //呼叫時機: updateCharts 之 option 更新後, 以及 autoresizeEvent.onResize(視窗縮放、導覽區收合展開、捲軸出現等容器寬度改變後)
525
+
526
+ let vo = this
527
+
528
+ //chart
529
+ let cmp = vo.$refs.chartEvent
530
+ let chart = (cmp && cmp.getChart) ? cmp.getChart() : null
531
+ if (!chart || !vo.optEvent || !vo.legendMeasurer) {
532
+ return
533
+ }
534
+
535
+ //check, 容器隱藏或寬 0 不判
536
+ let width = chart.getWidth()
537
+ if (!(width > 0)) {
538
+ return
539
+ }
540
+
541
+ //type, 以離屏量測計算一般式圖例於此寬之排版(為事件名稱與圖寬之純函數, 不依可見圖之繪製時序)
542
+ let names = map(get(vo, 'optEvent.series', []), 'name')
543
+ let type = decideLegendType(vo.legendMeasurer.measure(names, width, chart.getHeight()))
544
+ vo.legendType = type
545
+
546
+ //類型不變不動圖(捲動式之頁次得以保留)
547
+ let cur = get(chart.getOption(), 'legend.0', {})
548
+ if (cur.type === type) {
549
+ return
550
+ }
551
+
552
+ //切換類型: 帶完整圖例設定(否則重建模型後 top 遺失落到底部), 並帶入目前 selected 保留使用者隱藏之事件
553
+ chart.setOption({ legend: { ...genLegend(type), selected: cur.selected || {} } })
554
+
486
555
  },
487
556
 
488
557
  updateChartsDebounce: function() {
@@ -2,6 +2,8 @@
2
2
  <div
3
3
  ref="root"
4
4
  :style="`position:relative; display:flex; align-items:center; gap:${gap}px; height:100%; min-width:0; overflow:hidden; white-space:nowrap;`"
5
+ v-domresize="{ event: 'resize', tolerancePixel: 0 }"
6
+ @domresize="relayout"
5
7
  >
6
8
 
7
9
  <!-- 儲存格內之標籤: 本項永遠第 1 顆(唯一可編輯者, 永不被收合); 只渲染放得下的前 nShow 顆 -->
@@ -84,6 +86,7 @@ import get from 'lodash-es/get.js'
84
86
  import size from 'lodash-es/size.js'
85
87
  import filter from 'lodash-es/filter.js'
86
88
  import sum from 'lodash-es/sum.js'
89
+ import domResize from 'w-component-vue/src/js/domResize.mjs'
87
90
  import WPopup from 'w-component-vue/src/components/WPopup.vue'
88
91
  import RelationChip from './RelationChip.vue'
89
92
 
@@ -99,11 +102,13 @@ import RelationChip from './RelationChip.vue'
99
102
  // - 浮層內本項標籤與儲存格內同樣可改模式(舊版浮層一律唯讀, 展開後反而不能改); 改動後由呼叫端 revRows 重繪列,
100
103
  // 本元件隨儲存格重建而卸載、浮層隨之關閉——該列唯一可改之項已改完, 關閉符合預期
101
104
  // - 垂直置中: 根元素 flex 置中且 height:100%(其百分比高度之包含塊為 .ag-cell), 標籤與指示於 27px 儲存格內上下各 2.5
102
- // - 尺寸監聽用原生 ResizeObserver, 不用元件庫之 v-domresize(wsemi domDetect): 後者每實例一個 20ms setInterval 輪詢 offsetWidth,
103
- // 且比較基準為「上一次取樣」, 連續 ≤1px 之變化永不觸發(2026-09-17 讀碼定案並實測: 視窗逐 1px 縮窄時表格欄寬始終不重算),
104
- // 每列一實例時輪詢成本隨可見列數累積; ResizeObserver 為事件驅動、任何寬度變化皆觸發。根元素寬由儲存格決定(overflow:hidden),
105
- // 子元素增減不改變根寬, 無 ResizeObserver 迴圈之虞; 回呼以 requestAnimationFrame 合併
105
+ // - 尺寸監聽: 元件庫之 v-domresize(w-component-vue 2.5.23 起轉發設定給 wsemi domDetect; domDetect 自 1.9.4 起以 ResizeObserver 偵測, 比較基準為上次發出時之尺寸,
106
+ // 故逐 1px 之連續變化亦累積發出, 不再是每實例一個輪詢): event 為 'resize' 只聽根元素尺寸(同原生 ResizeObserver 之語意), tolerancePixel 為 0 使 1px 之變化亦發出;
107
+ // 元素隱藏(尺寸 0)時不發出, 恢復顯示時發出, 發出為非同步(與 ResizeObserver 回呼脫勾)。根元素寬由儲存格決定(overflow:hidden), 子元素增減不改變根寬, relayout 不會使根寬再變
106
108
  export default {
109
+ directives: {
110
+ domresize: domResize(),
111
+ },
107
112
  components: {
108
113
  WPopup,
109
114
  RelationChip,
@@ -149,32 +154,7 @@ export default {
149
154
  mounted: function() {
150
155
  let vo = this
151
156
  vo.measure()
152
-
153
- //ResizeObserver: 根元素寬度變化(儲存格欄寬改變、視窗變動致表格重算欄寬)即重算
154
- if (typeof window !== 'undefined' && window.ResizeObserver) {
155
- vo.ro = new window.ResizeObserver(() => {
156
- if (vo.rafId) {
157
- return
158
- }
159
- vo.rafId = window.requestAnimationFrame(() => {
160
- vo.rafId = null
161
- vo.relayout()
162
- })
163
- })
164
- vo.ro.observe(vo.$refs.root)
165
- }
166
-
167
- },
168
- beforeDestroy: function() {
169
- let vo = this
170
- if (vo.ro) {
171
- vo.ro.disconnect()
172
- vo.ro = null
173
- }
174
- if (vo.rafId) {
175
- window.cancelAnimationFrame(vo.rafId)
176
- vo.rafId = null
177
- }
157
+ //根元素寬度變化(儲存格欄寬改變、視窗變動致表格重算欄寬)由 v-domresize 觸發 relayout
178
158
  },
179
159
  watch: {
180
160
  items: {
@@ -305,7 +285,7 @@ export default {
305
285
  //relayout
306
286
  vo.measuring = false
307
287
  if (!vo.measured) {
308
- //量不到(元素尚未顯示): 先全部顯示避免儲存格空白, 不在此重試(否則元素持續不可見時會每幀互相呼叫), 待元素顯示使根寬改變、ResizeObserver 觸發 relayout 時再量
288
+ //量不到(元素尚未顯示): 先全部顯示避免儲存格空白, 不在此重試(否則元素持續不可見時會每幀互相呼叫), 待元素顯示使根寬改變、v-domresize 觸發 relayout 時再量
309
289
  vo.nShow = vo.n
310
290
  return
311
291
  }
package/src/fetchJson.mjs CHANGED
@@ -1,3 +1,28 @@
1
+ import get from 'lodash-es/get.js'
2
+ import isestr from 'wsemi/src/isestr.mjs'
3
+
4
+
5
+ let reName = /^[A-Za-z]{1,40}$/
6
+ let reCode = /^[A-Z][A-Z0-9_]{1,40}$/
7
+
8
+
9
+ //errDesc: 例外只取名稱(與代碼形狀之 cause.code / code), 不取 message(spec/設計要點與取捨.md ADR-023)
10
+ //why: fetch 例外之 message 可能含完整網址('Failed to parse URL from <url>'、'Request cannot be constructed from a URL that includes credentials: <url>'),
11
+ //而網址常帶權杖(?token=); JSON 解析錯誤之 message 夾帶回應本體片段. reject 物件會經 perm.conn / provideTabs 交給呼叫端與其 log
12
+ function errDesc(err) {
13
+ let name = get(err, 'name')
14
+ let s = (isestr(name) && reName.test(name)) ? name : 'Error'
15
+ let code = get(err, 'cause.code')
16
+ if (!(isestr(code) && reCode.test(code))) {
17
+ code = get(err, 'code')
18
+ }
19
+ if (isestr(code) && reCode.test(code)) {
20
+ s += ` ${code}`
21
+ }
22
+ return s
23
+ }
24
+
25
+
1
26
  /**
2
27
  * 以瀏覽器與 node(18+) 內建之 fetch 取得 JSON 回應, 取代 axios(2026-09-07 起本套件不再依賴 axios)
3
28
  *
@@ -5,6 +30,7 @@
5
30
  * - 網路錯誤或非 2xx: axios 會 reject, fetch 只在網路錯誤 reject, 故此處對 !res.ok 亦 reject, 統一為 { kind: 'request', status, message }
6
31
  * - 回應非 JSON: axios 回原字串(呼叫端判 state 失敗), 此處 res.json() 失敗 reject { kind: 'parse', status, message }
7
32
  * - 成功: resolve 解析後之 JSON(對應 axios 之 res.data)
33
+ * reject 之 message 只含例外名稱與代碼(如 'TypeError ECONNREFUSED'、'SyntaxError')或 'HTTP <status>', 不含網址與回應本體(網址常帶權杖)
8
34
  * 注意: node 端 fetch(undici) 不讀 HTTP_PROXY / HTTPS_PROXY 環境變數(axios 會), 需走 proxy 之部署須自行以 opt.dispatcher 注入 undici Agent
9
35
  *
10
36
  * @param {String} url 輸入請求網址字串
@@ -19,7 +45,7 @@ async function fetchJson(url, opt = {}) {
19
45
  res = await fetch(url, opt)
20
46
  }
21
47
  catch (err) {
22
- return Promise.reject({ kind: 'request', status: 0, message: String((err && err.message) || err) })
48
+ return Promise.reject({ kind: 'request', status: 0, message: errDesc(err) })
23
49
  }
24
50
 
25
51
  //check status
@@ -32,7 +58,7 @@ async function fetchJson(url, opt = {}) {
32
58
  return await res.json()
33
59
  }
34
60
  catch (err) {
35
- return Promise.reject({ kind: 'parse', status: res.status, message: String((err && err.message) || err) })
61
+ return Promise.reject({ kind: 'parse', status: res.status, message: errDesc(err) })
36
62
  }
37
63
 
38
64
  }
package/src/getPerm.mjs CHANGED
@@ -2,7 +2,6 @@ import get from 'lodash-es/get.js'
2
2
  import isestr from 'wsemi/src/isestr.mjs'
3
3
  import iseobj from 'wsemi/src/iseobj.mjs'
4
4
  import isfun from 'wsemi/src/isfun.mjs'
5
- import ispm from 'wsemi/src/ispm.mjs'
6
5
  import fetchJson from './fetchJson.mjs'
7
6
 
8
7
 
@@ -25,7 +24,9 @@ async function getPerm(url, tokenTar, opt = {}) {
25
24
  if (url.indexOf('token={token}') < 0) {
26
25
  return Promise.reject('noTokenInUrl')
27
26
  }
28
- url = url.replaceAll('{token}', tokenTar)
27
+ //代入: 值經 encodeURIComponent 並以 split / join 取代(ADR-023); 不用 replaceAll——其取代字串會解讀 $` $& $' 等樣式(可把佔位符前之網址前綴複製進查詢值),
28
+ //不編碼時 + 被伺服端解成空白、& 與 = 形成額外參數. 對 UUID、英數與 -_.~ 為恆等轉換
29
+ url = url.split('{token}').join(encodeURIComponent(tokenTar))
29
30
  // console.log('getPerm: url', url)
30
31
 
31
32
  //get, 內建 fetch(不依賴 axios); 網路錯誤或非 2xx → cannotGetUserByUrl, 回應非 JSON → cannotGetUserDataByUrl(對齊 axios 時期語意)
@@ -65,10 +66,12 @@ async function getPerm(url, tokenTar, opt = {}) {
65
66
  //check
66
67
  if (isfun(funConvertPerm)) {
67
68
 
68
- //funConvertPerm
69
- ur = funConvertPerm(ur)
70
- if (ispm(ur)) {
71
- ur = await ur
69
+ //funConvertPerm, 同步拋錯或 reject 一律 reject 'noUserDataAfterConvert'(部署方之錯誤原文不上拋, ADR-023)
70
+ try {
71
+ ur = await funConvertPerm(ur) //await 於 try 內: 同步拋錯、reject 與 thenable 之 reject 皆攔截; 非 Promise 之回傳原樣取得
72
+ }
73
+ catch (err) {
74
+ return Promise.reject('noUserDataAfterConvert')
72
75
  }
73
76
  // console.log('getPerm ur(funConvertPerm)', ur)
74
77
 
@@ -2,7 +2,6 @@ import get from 'lodash-es/get.js'
2
2
  import isestr from 'wsemi/src/isestr.mjs'
3
3
  import iseobj from 'wsemi/src/iseobj.mjs'
4
4
  import isfun from 'wsemi/src/isfun.mjs'
5
- import ispm from 'wsemi/src/ispm.mjs'
6
5
  import fetchJson from './fetchJson.mjs'
7
6
 
8
7
 
@@ -24,12 +23,14 @@ async function getPermUserInfor(url, tokenSelf, userIdTar, opt = {}) {
24
23
  //funConvertPerm
25
24
  let funConvertPerm = get(opt, 'funConvertPerm')
26
25
 
27
- //url
28
- if (url.indexOf('token={sysToken}') < 0 && url.indexOf('userId={userId}') < 0) {
26
+ //url, 兩個佔位符缺一即拒(原為 &&: 只有兩者皆缺才拒, 缺一時仍送出請求; 對齊 w-web-sso ADR-056 修正紀錄)
27
+ if (url.indexOf('token={sysToken}') < 0 || url.indexOf('userId={userId}') < 0) {
29
28
  return Promise.reject('noTokenUserIdInUrl')
30
29
  }
31
- url = url.replaceAll('{sysToken}', tokenSelf) //系統介接用permToken
32
- url = url.replaceAll('{userId}', userIdTar)
30
+ //代入: 值經 encodeURIComponent 並以 split / join 取代(ADR-023); 不用 replaceAll——其取代字串會解讀 $` $& $' 等樣式(userIdTar 含 $` 時會把已代入之介接權杖複製進 userId),
31
+ //不編碼時 + 被伺服端解成空白、& 與 = 形成額外參數, 值內之 {userId} 字樣亦會被二次代入. 對 UUID、英數與 -_.~ 為恆等轉換
32
+ url = url.split('{sysToken}').join(encodeURIComponent(tokenSelf)) //系統介接用permToken
33
+ url = url.split('{userId}').join(encodeURIComponent(userIdTar))
33
34
  // console.log('getPermUserInfor: url', url)
34
35
 
35
36
  //get, 內建 fetch(不依賴 axios); 網路錯誤或非 2xx → cannotGetUserByUrl, 回應非 JSON → cannotGetUserDataByUrl(對齊 axios 時期語意)
@@ -69,10 +70,12 @@ async function getPermUserInfor(url, tokenSelf, userIdTar, opt = {}) {
69
70
  //check
70
71
  if (isfun(funConvertPerm)) {
71
72
 
72
- //funConvertPerm
73
- ur = funConvertPerm(ur)
74
- if (ispm(ur)) {
75
- ur = await ur
73
+ //funConvertPerm, 同步拋錯或 reject 一律 reject 'noUserDataAfterConvert'(部署方之錯誤原文不上拋, ADR-023)
74
+ try {
75
+ ur = await funConvertPerm(ur) //await 於 try 內: 同步拋錯、reject 與 thenable 之 reject 皆攔截; 非 Promise 之回傳原樣取得
76
+ }
77
+ catch (err) {
78
+ return Promise.reject('noUserDataAfterConvert')
76
79
  }
77
80
  // console.log('getPermUserInfor ur(funConvertPerm)', ur)
78
81