w-web-perm 1.1.4 → 1.1.6

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 (56) 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.3c580e72.css} +1 -1
  4. package/dist/index.tmp +2 -2
  5. package/dist/js/app-legacy.5f15a58f.js +1 -0
  6. package/dist/js/app.5f15a58f.js +1 -0
  7. package/dist/js/{chunk-vendors-legacy.fc37f076.js → chunk-vendors-legacy.38f515f9.js} +16 -16
  8. package/dist/js/{chunk-vendors.fc37f076.js → chunk-vendors.38f515f9.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 +2068 -111
  13. package/docs/index.html +2 -2
  14. package/docs/server_WWebPerm.mjs.html +63 -31
  15. package/docs/server_lockSave.mjs.html +115 -0
  16. package/docs/server_maskLog.mjs.html +176 -0
  17. package/docs/server_procCore.mjs.html +31 -5
  18. package/docs/server_wrapInjected.mjs.html +163 -0
  19. package/docs/src_fetchJson.mjs.html +31 -5
  20. package/docs/src_js_legendFit.mjs.html +225 -0
  21. package/docs/src_plugins_reuseDeletedIds.mjs.html +136 -0
  22. package/docs/src_plugins_submitGuard.mjs.html +163 -0
  23. package/package.json +13 -13
  24. package/public/index.html +2 -0
  25. package/server/WWebPerm.mjs +61 -29
  26. package/server/lockSave.mjs +43 -0
  27. package/server/maskLog.mjs +104 -0
  28. package/server/procCore.mjs +29 -3
  29. package/server/procLang.mjs +8 -0
  30. package/server/procStaInfor.mjs +32 -14
  31. package/server/wrapInjected.mjs +91 -0
  32. package/src/components/Common/CheckYes.vue +6 -0
  33. package/src/components/Common/CheckYesNo.vue +6 -0
  34. package/src/components/LayoutContentGrups.vue +30 -21
  35. package/src/components/LayoutContentPemis.vue +30 -21
  36. package/src/components/LayoutContentStaInfor.vue +74 -5
  37. package/src/components/LayoutContentTargets.vue +36 -22
  38. package/src/components/LayoutContentUsers.vue +31 -21
  39. package/src/components/RelationChips.vue +11 -31
  40. package/src/components/VeCgrups.vue +8 -1
  41. package/src/components/VeCpemis.vue +8 -1
  42. package/src/components/VeCrules.vue +8 -1
  43. package/src/components/VeGrupBlngUsers.vue +37 -31
  44. package/src/components/VePemiBlngGrups.vue +37 -31
  45. package/src/fetchJson.mjs +28 -2
  46. package/src/getPerm.mjs +9 -6
  47. package/src/getPermUserInfor.mjs +12 -9
  48. package/src/js/legendFit.mjs +153 -0
  49. package/src/plugins/mUI.mjs +20 -0
  50. package/src/plugins/reuseDeletedIds.mjs +64 -0
  51. package/src/plugins/submitGuard.mjs +91 -0
  52. package/srv.mjs +20 -1
  53. package/dist/css/app.506ce4d8.css +0 -1
  54. package/dist/js/app-legacy.8e615442.js +0 -1
  55. package/dist/js/app.8e615442.js +0 -1
  56. package/tableTags-web-perm.json +0 -1
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -96,7 +96,7 @@ function mergePickKeysOnly(ltdtDb, rowsIn, keyDetect, pickKeys) {
96
96
  }
97
97
 
98
98
 
99
- function proc(woItems, procOrm, { srLog, kmx }) {
99
+ function proc(woItems, procOrm, { srLog, kmx, lockSave }) {
100
100
 
101
101
 
102
102
  //updateTabItems
@@ -106,8 +106,21 @@ function proc(woItems, procOrm, { srLog, kmx }) {
106
106
  //pickKeysOnly, 僅採納既有列之指定欄位變更(見 mergePickKeysOnly), 未給則整表 diff 寫入
107
107
  let pickKeysOnly = get(opt, 'pickKeysOnly', null)
108
108
 
109
+ //idsNew: 前端標為新增(transient 欄位 _isNew === true, 清單頁 addItem / copyItem 標記)之列 id, 供下方比對既有列 (ADR-025);
110
+ //_isNew 不在 schema keys 內, 下方 ltdtmapping 取欄時即剝除不入庫. pickKeysOnly 模式不採納新增列(見 mergePickKeysOnly), 故不收集
111
+ let idsNew = []
112
+ if (!isearr(pickKeysOnly)) {
113
+ each(rows, (r) => {
114
+ if (get(r, '_isNew') === true) {
115
+ idsNew.push(get(r, 'id', ''))
116
+ }
117
+ })
118
+ }
119
+
120
+ //雙擊防護(後端): 同一操作者之同表寫入處理中再送出即拒絕 'saveInProgress'(不排隊, 見 lockSave.mjs, ADR-025);
121
+ //占位包在 kmx 之外層, 不同操作者之並行寫入仍由 kmx 序列化、各自成功(ADR-012)
109
122
  //序列化同表之並行整表批次寫入: 防 lost update (同 keyTable 序列化, 不同表並行)
110
- return await kmx('updateTabItems:' + keyTable, async () => {
123
+ return await lockSave(`updateTabItems:${keyTable}`, userId, () => kmx('updateTabItems:' + keyTable, async () => {
111
124
 
112
125
  //pickKeysOnly, 須於 mutex 內讀取資料庫為基底, 避免與並行寫入 race
113
126
  if (isearr(pickKeysOnly)) {
@@ -184,6 +197,19 @@ function proc(woItems, procOrm, { srLog, kmx }) {
184
197
 
185
198
  //ltdtDiffByKey
186
199
  let ltdtOld = await woItems[keyTable].select()
200
+
201
+ //新增列已存在即拒絕 (ADR-025): 同一包重送且第 2 次於第 1 次完成後才處理時(占位已釋放), 第 1 次已建立之列會被當「修改」(diff),
202
+ //以前端之佔位字覆寫伺服器於新增時填入之建立者 / 建立時間(procOrm save 只覆寫 userIdUpdate / timeUpdate); 故整包拒絕、不寫入
203
+ if (size(idsNew) > 0) {
204
+ let kpIdOld = {}
205
+ each(ltdtOld, (v) => {
206
+ kpIdOld[get(v, 'id', '')] = true
207
+ })
208
+ if (idsNew.some((id) => kpIdOld[id] === true)) {
209
+ return Promise.reject('saveNewRowExists')
210
+ }
211
+ }
212
+
187
213
  let ltdtNew = rows
188
214
  let r = ltdtDiffByKey(ltdtOld, ltdtNew, keyDetect)
189
215
  // console.log('ltdtDiffByKey r', r)
@@ -214,7 +240,7 @@ function proc(woItems, procOrm, { srLog, kmx }) {
214
240
 
215
241
  return ltdtNew
216
242
 
217
- })
243
+ }))
218
244
  }
219
245
 
220
246
 
@@ -298,7 +324,7 @@ export default proc
298
324
  <br class="clear">
299
325
 
300
326
  <footer>
301
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026 13:05:11 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
327
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 02 2026 20:53:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
302
328
  </footer>
303
329
 
304
330
  <script>prettyPrint();</script>
@@ -0,0 +1,163 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>server/wrapInjected.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">server/wrapInjected.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
49
+ import isestr from 'wsemi/src/isestr.mjs'
50
+ import isarr from 'wsemi/src/isarr.mjs'
51
+ import isfun from 'wsemi/src/isfun.mjs'
52
+
53
+
54
+ //wrapInjected: 部署方注入函數(getUserByToken / verifyClientUser / verifyAppUser)之包裝(W 契約, spec/設計要點與取捨.md ADR-023)
55
+ //why: 注入函數拋錯或 reject 時原會被 await 原樣上拋, 其原文經 pm2resolve 回前端、經 verifyConn 之 msg 入 srLog、經 error 事件印出;
56
+ //部署方常以 w-web-sso 對外 helper(≤1.1.4)包裝 getUserByToken, 其 reject 字串含已代入系統介接權杖與使用者權杖之完整網址(修正前探測實證)。
57
+ //包裝後失敗視同否定結果(由呼叫端給 failValue), 走各呼叫點既有之查無 / 無權限分支回自家 key; 上游原文不回前端、不入 log。
58
+
59
+
60
+ //keysUpstream: 上游失敗可照記之 key 白名單 = w-web-sso 對外 helper 之 K 契約 14 個 key(w-web-sso ADR-068)
61
+ //與 sso 之同步點: sso 之 K 契約增減 key 時須同步本表(見 CLAUDE_rulebook.md「權杖不外洩」)
62
+ let keysUpstream = [
63
+ 'invalidUrl',
64
+ 'invalidTokenSelf',
65
+ 'invalidTokenTar',
66
+ 'invalidUserIdTar',
67
+ 'noTokenKeyValueInUrl',
68
+ 'noTokenKeyUserIdInUrl',
69
+ 'noTokenInUrl',
70
+ 'cannotGetUserByUrl',
71
+ 'cannotGetUsersByUrl',
72
+ 'cannotGetUserDataByUrl',
73
+ 'cannotGetUsersDataByUrl',
74
+ 'noUserDataByUrl',
75
+ 'noUsersDataByUrl',
76
+ 'noUserDataAfterConvert',
77
+ ]
78
+
79
+
80
+ //reName: Error.name 之名稱形狀(同 w-web-sso helper 診斷之 errName), 不符者不記(name 亦可被設為任意字串)
81
+ let reName = /^[A-Za-z]{1,40}$/
82
+
83
+
84
+ /**
85
+ * 描述上游失敗, 供寫入 log: 絕不含上游原文
86
+ * - reject 值(字串)或其 message 屬白名單 key → { upstream: key }
87
+ * - 其餘 → { upstreamType, upstreamName }: 型別(null / array / typeof)與名稱形狀之 Error.name(無則 '')
88
+ *
89
+ * @param {*} err 輸入注入函數拋出或 reject 之值
90
+ * @returns {Object} 回傳描述物件
91
+ */
92
+ function describeUpstream(err) {
93
+ let key = isestr(err) ? err : get(err, 'message')
94
+ if (isestr(key) &amp;&amp; keysUpstream.includes(key)) {
95
+ return { upstream: key }
96
+ }
97
+ let upstreamType = (err === null) ? 'null' : (isarr(err) ? 'array' : typeof err)
98
+ let name = get(err, 'name')
99
+ let upstreamName = (isestr(name) &amp;&amp; reName.test(name)) ? name : ''
100
+ return { upstreamType, upstreamName }
101
+ }
102
+
103
+
104
+ /**
105
+ * 包裝注入函數: 回傳(同步或 Promise)原樣回傳; 同步拋錯或 reject 回 failValue, 並以 describeUpstream(err) 呼叫 onFail
106
+ * 包裝後一律回 Promise(呼叫端既有之 ispm 判斷照常)
107
+ *
108
+ * @param {Function} fn 輸入注入函數
109
+ * @param {Object} [opt={}] 輸入設定物件
110
+ * @param {*} [opt.failValue=null] 輸入失敗時之回傳值(getUserByToken 為 null 等同查無, verify* 為 false 等同無權限)
111
+ * @param {Function} [opt.onFail=null] 輸入失敗時之紀錄函數, 只收到 describeUpstream 之結果(收不到上游原文); 其自身拋錯不影響回傳
112
+ * @returns {Function} 回傳包裝後之函數
113
+ */
114
+ function wrapInjected(fn, opt = {}) {
115
+ let failValue = get(opt, 'failValue', null)
116
+ let onFail = get(opt, 'onFail', null)
117
+ return async (...args) => {
118
+ try {
119
+ return await fn(...args) //await 於 try 內: 同步拋錯、reject 與 thenable 之 reject 皆攔截
120
+ }
121
+ catch (err) {
122
+ if (isfun(onFail)) {
123
+ try {
124
+ onFail(describeUpstream(err))
125
+ }
126
+ catch (e) {} //紀錄失敗不改變「失敗=否定結果」之語意
127
+ }
128
+ return failValue
129
+ }
130
+ }
131
+ }
132
+
133
+
134
+ export {
135
+ keysUpstream,
136
+ describeUpstream
137
+ }
138
+ export default wrapInjected
139
+ </code></pre>
140
+ </article>
141
+ </section>
142
+
143
+
144
+
145
+
146
+
147
+
148
+ </div>
149
+
150
+ <br class="clear">
151
+
152
+ <footer>
153
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 02 2026 20:53:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
154
+ </footer>
155
+
156
+ <script>prettyPrint();</script>
157
+ <script src="scripts/polyfill.js"></script>
158
+ <script src="scripts/linenumber.js"></script>
159
+
160
+
161
+
162
+ </body>
163
+ </html>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -45,13 +45,39 @@
45
45
 
46
46
  <section>
47
47
  <article>
48
- <pre class="prettyprint source linenums"><code>/**
48
+ <pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
49
+ import isestr from 'wsemi/src/isestr.mjs'
50
+
51
+
52
+ let reName = /^[A-Za-z]{1,40}$/
53
+ let reCode = /^[A-Z][A-Z0-9_]{1,40}$/
54
+
55
+
56
+ //errDesc: 例外只取名稱(與代碼形狀之 cause.code / code), 不取 message(spec/設計要點與取捨.md ADR-023)
57
+ //why: fetch 例外之 message 可能含完整網址('Failed to parse URL from &lt;url>'、'Request cannot be constructed from a URL that includes credentials: &lt;url>'),
58
+ //而網址常帶權杖(?token=); JSON 解析錯誤之 message 夾帶回應本體片段. reject 物件會經 perm.conn / provideTabs 交給呼叫端與其 log
59
+ function errDesc(err) {
60
+ let name = get(err, 'name')
61
+ let s = (isestr(name) &amp;&amp; reName.test(name)) ? name : 'Error'
62
+ let code = get(err, 'cause.code')
63
+ if (!(isestr(code) &amp;&amp; reCode.test(code))) {
64
+ code = get(err, 'code')
65
+ }
66
+ if (isestr(code) &amp;&amp; reCode.test(code)) {
67
+ s += ` ${code}`
68
+ }
69
+ return s
70
+ }
71
+
72
+
73
+ /**
49
74
  * 以瀏覽器與 node(18+) 內建之 fetch 取得 JSON 回應, 取代 axios(2026-09-07 起本套件不再依賴 axios)
50
75
  *
51
76
  * 與 axios 之語意對齊:
52
77
  * - 網路錯誤或非 2xx: axios 會 reject, fetch 只在網路錯誤 reject, 故此處對 !res.ok 亦 reject, 統一為 { kind: 'request', status, message }
53
78
  * - 回應非 JSON: axios 回原字串(呼叫端判 state 失敗), 此處 res.json() 失敗 reject { kind: 'parse', status, message }
54
79
  * - 成功: resolve 解析後之 JSON(對應 axios 之 res.data)
80
+ * reject 之 message 只含例外名稱與代碼(如 'TypeError ECONNREFUSED'、'SyntaxError')或 'HTTP &lt;status>', 不含網址與回應本體(網址常帶權杖)
55
81
  * 注意: node 端 fetch(undici) 不讀 HTTP_PROXY / HTTPS_PROXY 環境變數(axios 會), 需走 proxy 之部署須自行以 opt.dispatcher 注入 undici Agent
56
82
  *
57
83
  * @param {String} url 輸入請求網址字串
@@ -66,7 +92,7 @@ async function fetchJson(url, opt = {}) {
66
92
  res = await fetch(url, opt)
67
93
  }
68
94
  catch (err) {
69
- return Promise.reject({ kind: 'request', status: 0, message: String((err &amp;&amp; err.message) || err) })
95
+ return Promise.reject({ kind: 'request', status: 0, message: errDesc(err) })
70
96
  }
71
97
 
72
98
  //check status
@@ -79,7 +105,7 @@ async function fetchJson(url, opt = {}) {
79
105
  return await res.json()
80
106
  }
81
107
  catch (err) {
82
- return Promise.reject({ kind: 'parse', status: res.status, message: String((err &amp;&amp; err.message) || err) })
108
+ return Promise.reject({ kind: 'parse', status: res.status, message: errDesc(err) })
83
109
  }
84
110
 
85
111
  }
@@ -100,7 +126,7 @@ export default fetchJson
100
126
  <br class="clear">
101
127
 
102
128
  <footer>
103
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026 13:05:11 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
129
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 02 2026 20:53:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
104
130
  </footer>
105
131
 
106
132
  <script>prettyPrint();</script>
@@ -0,0 +1,225 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>src/js/legendFit.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">src/js/legendFit.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>//圖例排版: 供事件發生頻率圖(LayoutContentStaInfor)使用. 一般式(plain, 可換行)圖例放得下時維持現狀, 放不下(會壓到最上方刻度與繪圖區)時改捲動式(scroll, 單列+翻頁)
49
+ //判定以「離屏量測實例」計算一般式圖例於指定寬度之實際排版, 為寬度與名稱集合之純函數:
50
+ // 不讀可見圖之顯示列表(echarts resize 不 flush zrender, resize 後讀到的是上一個寬度之圖例, echarts.js:964-1012);
51
+ // 不在可見圖上試排(類型改變即重建圖例模型, 捲動式頁次會歸零, 帶回頁次則每次滑動 800ms)
52
+ //本檔無外部相依, 日後可原樣移入 w-echarts-vue(src/js/)
53
+
54
+
55
+ //GRID_TOP: 繪圖區頂(px), 同 echarts 6.1.0 預設(GridModel.js:84 top:65), 於 option 明寫以防預設變動
56
+ let GRID_TOP = 65
57
+
58
+ //LEGEND_MAX_BOTTOM: 一般式圖例底緣上限(px). 最上方 y 軸刻度文字以繪圖區頂為中線(字高 12 → 半高 6), 再留白 2 → 65-6-2=57
59
+ // 實測(echarts 6.1.0, 列距 21): 1 列底 18、2 列底 39(安全)、3 列底 60(擦到刻度)、4 列底 81(壓進繪圖區)
60
+ let LEGEND_MAX_BOTTOM = GRID_TOP - 6 - 2
61
+
62
+
63
+ /**
64
+ * 產生圖例設定(單一來源), genOpt 與執行期切換類型共用
65
+ *
66
+ * 切換類型(plain↔scroll)時 echarts 以新傳入之選項重建圖例模型(Global.js:322-344), 未帶之設定一律回到預設, 故每次都須帶齊
67
+ *
68
+ * @param {String} [type='plain'] 輸入圖例類型字串, 'plain'或'scroll', 預設'plain'
69
+ * @returns {Object} 回傳echarts之legend設定物件
70
+ */
71
+ function genLegend(type = 'plain') {
72
+ let legend = {
73
+ show: true,
74
+ top: 0, //須明給: echarts 6 起 legend 預設由 top:0 改為 bottom, 不給會落到底部壓住 x 軸時間標籤; 切換類型時亦須帶(類型改變即重建圖例模型)
75
+ type,
76
+ }
77
+ if (type === 'scroll') {
78
+ legend.tooltip = { show: true } //捲動式僅出現於窄版, 單一項目可能寬於可視窗而被裁, 滑過顯示全名(靜止畫面不變)
79
+ }
80
+ return legend
81
+ }
82
+
83
+
84
+ /**
85
+ * 依量測結果決定圖例類型
86
+ *
87
+ * @param {Object|null} m 輸入量測結果物件{rows,bottom}, 量測失敗為null
88
+ * @returns {String} 回傳'plain'或'scroll', 量測失敗回'scroll'(單列, 不會重疊)
89
+ */
90
+ function decideLegendType(m) {
91
+ if (!m || typeof m.bottom !== 'number') {
92
+ return 'scroll'
93
+ }
94
+ return m.bottom &lt;= LEGEND_MAX_BOTTOM ? 'plain' : 'scroll'
95
+ }
96
+
97
+
98
+ /**
99
+ * 建立離屏量測器: 以未掛入DOM之echarts實例, 計算一般式圖例於指定寬度之排版
100
+ *
101
+ * 量測實例僅含圖例、隱藏之座標軸與同名空系列, 故顯示列表中「文字等於系列名」之元素即圖例項目(不會與軸刻度文字撞名)
102
+ *
103
+ * @param {Object} echarts 輸入echarts命名空間物件, 須與圖表同一份(例如w-echarts-vue之iniEcharts()), 字型與版面規則方能一致
104
+ * @returns {Object} 回傳物件, 含measure(names,width,height)與dispose(); measure回傳{rows,bottom}(bottom為圖例底緣距圖表頂之px), 量不到回null
105
+ */
106
+ function createLegendMeasurer(echarts) {
107
+ let el = null
108
+ let chart = null
109
+
110
+ let measure = (names, width, height = 300) => {
111
+
112
+ //check
113
+ if (!Array.isArray(names) || names.length === 0) {
114
+ return null
115
+ }
116
+ if (!(width > 0) || !(height > 0)) {
117
+ return null
118
+ }
119
+
120
+ //uniq
121
+ let uniq = []
122
+ for (let name of names) {
123
+ if (uniq.indexOf(name) &lt; 0) {
124
+ uniq.push(name)
125
+ }
126
+ }
127
+
128
+ try {
129
+
130
+ //chart, 首次建立後重用
131
+ if (!chart) {
132
+ el = document.createElement('div')
133
+ chart = echarts.init(el, null, { width, height })
134
+ }
135
+ else {
136
+ chart.resize({ width, height })
137
+ }
138
+
139
+ //setOption, 同步排版(notMerge整體置換)
140
+ chart.setOption({
141
+ animation: false,
142
+ legend: genLegend('plain'),
143
+ xAxis: { type: 'category', show: false, data: [] },
144
+ yAxis: { type: 'value', show: false },
145
+ series: uniq.map((name) => {
146
+ return { name, type: 'line', data: [] }
147
+ }),
148
+ }, { notMerge: true })
149
+
150
+ //圖例項目: 顯示列表中文字等於系列名者, 以其頂緣去重計列數、取最大底緣
151
+ let tops = []
152
+ let bottom = 0
153
+ let n = 0
154
+ for (let e of chart.getZr().storage.getDisplayList(true)) {
155
+ let t = e.style &amp;&amp; e.style.text
156
+ if (!t || uniq.indexOf(t) &lt; 0 || typeof e.transformCoordToGlobal !== 'function') {
157
+ continue
158
+ }
159
+ let r = e.getBoundingRect()
160
+ let top = e.transformCoordToGlobal(r.x, r.y)[1]
161
+ let btm = e.transformCoordToGlobal(r.x, r.y + r.height)[1]
162
+ let tr = Math.round(top)
163
+ if (tops.indexOf(tr) &lt; 0) {
164
+ tops.push(tr)
165
+ }
166
+ bottom = Math.max(bottom, btm)
167
+ n++
168
+ }
169
+
170
+ //check, 找到之圖例文字數須等於系列名數, 否則視為量測失敗
171
+ if (n !== uniq.length) {
172
+ return null
173
+ }
174
+
175
+ return { rows: tops.length, bottom }
176
+ }
177
+ catch (err) {
178
+ return null
179
+ }
180
+ }
181
+
182
+ let dispose = () => {
183
+ if (chart) {
184
+ chart.dispose()
185
+ }
186
+ chart = null
187
+ el = null
188
+ }
189
+
190
+ return { measure, dispose }
191
+ }
192
+
193
+
194
+ export {
195
+ GRID_TOP,
196
+ LEGEND_MAX_BOTTOM,
197
+ genLegend,
198
+ decideLegendType,
199
+ createLegendMeasurer
200
+ }
201
+ </code></pre>
202
+ </article>
203
+ </section>
204
+
205
+
206
+
207
+
208
+
209
+
210
+ </div>
211
+
212
+ <br class="clear">
213
+
214
+ <footer>
215
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 02 2026 20:53:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
216
+ </footer>
217
+
218
+ <script>prettyPrint();</script>
219
+ <script src="scripts/polyfill.js"></script>
220
+ <script src="scripts/linenumber.js"></script>
221
+
222
+
223
+
224
+ </body>
225
+ </html>