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.
- package/README.md +1 -1
- package/dist/css/app.6918ec45.css +1 -0
- package/dist/css/{chunk-vendors.2bcfce08.css → chunk-vendors.80aa3ae8.css} +1 -1
- package/dist/index.tmp +2 -2
- package/dist/js/app-legacy.833f9566.js +1 -0
- package/dist/js/app.833f9566.js +1 -0
- package/dist/js/{chunk-vendors-legacy.fc37f076.js → chunk-vendors-legacy.fbf771bd.js} +16 -16
- package/dist/js/{chunk-vendors.fc37f076.js → chunk-vendors.fbf771bd.js} +16 -16
- package/dist/w-web-perm.umd.js +2 -2
- package/dist/w-web-perm.umd.js.map +1 -1
- package/docs/WWebPerm.html +7 -7
- package/docs/global.html +1545 -60
- package/docs/index.html +2 -2
- package/docs/server_WWebPerm.mjs.html +53 -30
- package/docs/server_maskLog.mjs.html +176 -0
- package/docs/server_procCore.mjs.html +2 -2
- package/docs/server_wrapInjected.mjs.html +163 -0
- package/docs/src_fetchJson.mjs.html +31 -5
- package/docs/src_js_legendFit.mjs.html +225 -0
- package/package.json +5 -5
- package/public/index.html +2 -0
- package/server/WWebPerm.mjs +51 -28
- package/server/maskLog.mjs +104 -0
- package/server/procStaInfor.mjs +32 -14
- package/server/wrapInjected.mjs +91 -0
- package/src/components/LayoutContentStaInfor.vue +74 -5
- package/src/components/RelationChips.vue +11 -31
- package/src/fetchJson.mjs +28 -2
- package/src/getPerm.mjs +9 -6
- package/src/getPermUserInfor.mjs +12 -9
- package/src/js/legendFit.mjs +153 -0
- package/srv.mjs +20 -1
- package/tableTags-web-perm.json +1 -1
- package/dist/css/app.506ce4d8.css +0 -1
- package/dist/js/app-legacy.8e615442.js +0 -1
- 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
|
+
}
|
package/server/procStaInfor.mjs
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
34
|
-
function genMockStaEventTable() {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
// -
|
|
103
|
-
//
|
|
104
|
-
//
|
|
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
|
-
//量不到(元素尚未顯示): 先全部顯示避免儲存格空白, 不在此重試(否則元素持續不可見時會每幀互相呼叫), 待元素顯示使根寬改變、
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
package/src/getPermUserInfor.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
|
|
|
@@ -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
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|