w-web-api 1.0.52 → 1.0.55

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 (78) hide show
  1. package/README.md +1 -1
  2. package/dist/css/app.cad4a1ca.css +1 -0
  3. package/dist/index.tmp +4 -2
  4. package/dist/js/app-legacy.7687b23b.js +1 -0
  5. package/dist/js/app.7687b23b.js +1 -0
  6. package/dist/js/chunk-vendors-legacy.4c27cf51.js +15 -0
  7. package/dist/js/chunk-vendors.4c27cf51.js +15 -0
  8. package/dist/w-web-api.umd.js +2 -2
  9. package/dist/w-web-api.umd.js.map +1 -1
  10. package/docs/WWebApi.html +2 -2
  11. package/docs/index.html +1 -1
  12. package/docs/{WWebApi.mjs.html → server_WWebApi.mjs.html} +113 -66
  13. package/docs/src_components_Common_CheckYesNo.vue.html +187 -0
  14. package/g.getSettings.mjs +3 -0
  15. package/logs/2026-06-19T16.log +6 -0
  16. package/logs/2026-06-20T11.log +4 -0
  17. package/logs/2026-06-20T14.log +2 -0
  18. package/logs/2026-06-21T20.log +1 -0
  19. package/logs/2026-06-21T21.log +4 -0
  20. package/logs/2026-06-22T07.log +12 -0
  21. package/logs/2026-06-22T08.log +808 -0
  22. package/logs/2026-06-22T09.log +104 -0
  23. package/logs/2026-06-22T10.log +26 -0
  24. package/logs/2026-06-22T11.log +860 -0
  25. package/logs/2026-06-22T12.log +430 -0
  26. package/logs/2026-06-22T23.log +1547 -0
  27. package/logs/2026-06-23T00.log +473 -0
  28. package/logs/2026-06-23T08.log +231 -0
  29. package/logs/2026-06-23T09.log +179 -0
  30. package/logs/2026-07-01T11.log +488 -0
  31. package/logs/2026-07-09T22.log +370 -0
  32. package/logs/2026-07-09T23.log +370 -0
  33. package/logs/2026-07-10T12.log +284 -0
  34. package/logs/2026-07-10T13.log +777 -0
  35. package/logs/2026-07-11T17.log +2 -0
  36. package/package.json +20 -15
  37. package/public/index.html +9 -2
  38. package/server/WWebApi.mjs +110 -63
  39. package/server/procApis.mjs +62 -0
  40. package/server/procLang.mjs +372 -66
  41. package/server/procProxy.mjs +120 -0
  42. package/server/srLog.mjs +30 -0
  43. package/server/staLogs/filterVpfsByWindow.mjs +27 -0
  44. package/server/staLogs/staEvent.mjs +131 -0
  45. package/server/staLogs/test_staEvent.mjs +198 -0
  46. package/settings.json +18 -0
  47. package/src/App.vue +11 -7
  48. package/src/assets/scalar-ui.css +113 -0
  49. package/src/components/Common/CheckYes.vue +102 -0
  50. package/src/components/Common/CheckYesNo.vue +115 -0
  51. package/src/components/Common/LoadingWinBar.vue +114 -0
  52. package/src/components/Layout.vue +8 -26
  53. package/src/components/LayoutContent.vue +299 -350
  54. package/src/components/LayoutContentEdit.vue +577 -0
  55. package/src/components/LayoutContentStats.vue +485 -0
  56. package/src/components/LayoutContentTest.vue +781 -0
  57. package/src/components/MdPanel.vue +80 -58
  58. package/src/main.js +11 -8
  59. package/src/plugins/mUI.mjs +19 -39
  60. package/src/schema/tables/apis.mjs +30 -0
  61. package/src/store/mutations.mjs +0 -14
  62. package/src/store/types.mjs +0 -3
  63. package/srv.mjs +50 -20
  64. package/tableTags.json +1 -1
  65. package/z/344/277/256/346/255/243/350/246/217/345/212/203/346/233/270.md +246 -0
  66. package/.github/workflows/ci-test.yml +0 -24
  67. package/.vscode/launch.json +0 -18
  68. package/db/wapis/apis/data.mdb +0 -0
  69. package/db/wapis/apis/lock.mdb +0 -0
  70. package/dist/css/app.b5cf2ef5.css +0 -1
  71. package/dist/js/app-legacy.927b5679.js +0 -1
  72. package/dist/js/app.927b5679.js +0 -1
  73. package/dist/js/chunk-vendors-legacy.ffb0c6c5.js +0 -13
  74. package/dist/js/chunk-vendors.ffb0c6c5.js +0 -13
  75. package/src/plugins/mDataSelectorSchema.mjs +0 -30
  76. package/src/plugins/mShare.mjs +0 -44
  77. package/tableTags-web-api.json +0 -1
  78. package/test/all.test.mjs +0 -10
@@ -0,0 +1,131 @@
1
+ import ot from 'dayjs'
2
+ import get from 'lodash-es/get.js'
3
+ import size from 'lodash-es/size.js'
4
+ import groupBy from 'lodash-es/groupBy.js'
5
+ import each from 'lodash-es/each.js'
6
+ import mapValues from 'lodash-es/mapValues.js'
7
+ import merge from 'lodash-es/merge.js'
8
+ import genPm from 'wsemi/src/genPm.mjs'
9
+ import isestr from 'wsemi/src/isestr.mjs'
10
+ import fsTreeFolder from 'wsemi/src/fsTreeFolder.mjs'
11
+ import fsBuildReadStreamText from 'wsemi/src/fsBuildReadStreamText.mjs'
12
+ import filterVpfsByWindow from './filterVpfsByWindow.mjs'
13
+
14
+
15
+ //staEvent(通用事件頻率統計,骨架移植自 w-web-sso server/staLogs/staIp.mjs,但「不過濾特定 event」、
16
+ //內層改依 `event` 欄位分組):掃 fdLog 下所有 NDJSON log 檔,依時間 bucket(hr/day)+ 各 event 名聚合次數。
17
+ //輸出供前端統計圖:可呈現「全部(count)」或「各 event 各自」之時段發生頻率。
18
+ //
19
+ //回傳格式(時間遞增、無資料時段補 0):
20
+ // [
21
+ // { time: '2026-06-22T10', data: { count: 6, 'verifyConn': 3, 'kpfun-getApisList': 2, 'kpfun-saveApi': 1 } },
22
+ // { time: '2026-06-22T11', data: { count: 0 } },
23
+ // ...
24
+ // ]
25
+ //
26
+ //參數:
27
+ // timeLength 往回統計天數(預設 7)
28
+ // timeInterval 時間粒度 'hr'(每小時,預設)/ 'day'(每日)
29
+ // opt.fdLog log 資料夾(預設 './logs',須與 srLog 寫入之 logFd 一致;e2e 可指向合成 log 達 hermetic)
30
+ async function staEvent(timeLength = 7, timeInterval = 'hr', opt = {}) {
31
+
32
+ //fdLog
33
+ let fdLog = get(opt, 'fdLog')
34
+ if (!isestr(fdLog)) {
35
+ fdLog = './logs'
36
+ }
37
+
38
+ //fmt
39
+ let fmt = timeInterval === 'day' ? 'YYYY-MM-DD' : 'YYYY-MM-DDTHH'
40
+
41
+ //unit
42
+ let unit = timeInterval === 'hr' ? 'hour' : 'day'
43
+
44
+ //kpTime,產生完整的時間區間(無資料時段亦補 { count: 0 },圖表 x 軸不留空洞)
45
+ let now = ot()
46
+ let tStart = now.subtract(timeLength, 'day')
47
+ let tCurr = tStart.startOf(unit)
48
+ let tEnd = now.startOf(unit)
49
+ let kpTime = {}
50
+ while (!tCurr.isAfter(tEnd)) {
51
+ kpTime[tCurr.format(fmt)] = { data: { count: 0 } }
52
+ tCurr = tCurr.add(1, unit)
53
+ }
54
+
55
+ //vpfs
56
+ let vpfs = fsTreeFolder(fdLog)
57
+ vpfs = filterVpfsByWindow(vpfs, tStart, fmt) //開檔前剔除窗外檔, 見該模組註解
58
+
59
+ //logs(逐檔逐行 stream parse,挑出 tStart 之後、且帶 event 字串者)
60
+ let logs = []
61
+ for (let vpf of vpfs) {
62
+
63
+ //fp
64
+ let fp = vpf.path
65
+
66
+ //ev
67
+ let ev = fsBuildReadStreamText(fp)
68
+
69
+ //line
70
+ ev.on('line', (line) => {
71
+ let v = null
72
+ try {
73
+ v = JSON.parse(line)
74
+ let t = ot(v.time)
75
+ let timeFmt = t.format(fmt)
76
+ let b1 = t.isAfter(tStart)
77
+ let b2 = isestr(v.event) //通用:任何帶 event 字串之 log 皆納入(非鎖定單一 event)
78
+ let b = b1 && b2
79
+ if (b) {
80
+ logs.push({
81
+ timeFmt,
82
+ ...v,
83
+ })
84
+ }
85
+ }
86
+ catch (err) {} //略過非 JSON / 壞行
87
+ })
88
+
89
+ //await close
90
+ let pm = genPm()
91
+ ev.on('close', () => {
92
+ pm.resolve()
93
+ })
94
+ await pm
95
+
96
+ }
97
+
98
+ //gs,依時間 bucket 分組
99
+ let gs = groupBy(logs, 'timeFmt')
100
+
101
+ //gsLog,各 bucket 內依 event 名計數 + 總數 count
102
+ let gsLog = mapValues(gs, vs => {
103
+ let count = size(vs)
104
+ let gev = groupBy(vs, 'event')
105
+ let kp = {}
106
+ each(gev, (vvs, event) => {
107
+ kp[event] = size(vvs)
108
+ })
109
+ return {
110
+ data: {
111
+ count,
112
+ ...kp,
113
+ }
114
+ }
115
+ })
116
+
117
+ //merge(補零時段 + 實際資料)
118
+ let mr = merge({}, kpTime, gsLog)
119
+
120
+ //rs(時間遞增排序輸出)
121
+ let rs = Object.keys(mr)
122
+ .sort()
123
+ .map(time => ({
124
+ time,
125
+ ...mr[time]
126
+ }))
127
+
128
+ return rs
129
+ }
130
+
131
+ export default staEvent
@@ -0,0 +1,198 @@
1
+ //test_staEvent:staEvent 通用事件頻率統計之單元測試。
2
+ //
3
+ //與 w-web-sso 之 test_staIp.mjs(純 console.log 人工核對)不同,本檔走「合成 log + 真 assert」(對齊全域 §6.2:
4
+ //測試是規格的翻譯,非現狀指紋):自建確定性 NDJSON log → 呼叫 staEvent → 斷言聚合結果 → 清理。
5
+ //
6
+ //執行:node server/staLogs/test_staEvent.mjs(全綠印 PASS、process exit 0;任一斷言失敗 throw、exit 1)。
7
+ //
8
+ import assert from 'assert'
9
+ import fs from 'fs'
10
+ import path from 'path'
11
+ import ot from 'dayjs'
12
+ import staEvent from './staEvent.mjs'
13
+ import filterVpfsByWindow from './filterVpfsByWindow.mjs'
14
+
15
+
16
+ //tmp log 資料夾(落在主 cwd ./tmp/,集中可一鍵清;不污染真實 ./logs)
17
+ let fdLog = './tmp/_logs-test-staEvent'
18
+
19
+ //以「當前小時正中(:30)」為錨點,避免測試剛好跨小時邊界導致 bucket 飄移
20
+ let anchor = ot().startOf('hour')
21
+ let tNow = anchor.add(30, 'minute') //本小時 bucket
22
+ let tPrev = anchor.subtract(30, 'minute') //前一小時 bucket
23
+ let tOld = anchor.subtract(30, 'day') //30 天前(超出 timeLength=7,應被排除)
24
+
25
+ let fmtHr = 'YYYY-MM-DDTHH'
26
+ let keyNow = tNow.format(fmtHr)
27
+ let keyPrev = tPrev.format(fmtHr)
28
+
29
+
30
+ //一筆 log(NDJSON 一行)
31
+ function logLine(t, event, extra = {}) {
32
+ let o = { level: 30, time: t.valueOf(), pid: 1, hostname: 'test', event, ...extra }
33
+ return JSON.stringify(o)
34
+ }
35
+
36
+
37
+ function writeSyntheticLogs() {
38
+ fs.rmSync(fdLog, { recursive: true, force: true })
39
+ fs.mkdirSync(fdLog, { recursive: true })
40
+
41
+ //本小時:verifyConn ×3、kpfun-getApisList ×2、kpfun-saveApi ×1 → count 6
42
+ let lines1 = [
43
+ logLine(tNow, 'verifyConn', { apiType: 'ws' }),
44
+ logLine(tNow, 'verifyConn', { apiType: 'ws' }),
45
+ logLine(tNow, 'verifyConn', { apiType: 'http' }),
46
+ logLine(tNow, 'kpfun-getApisList'),
47
+ logLine(tNow, 'kpfun-getApisList'),
48
+ logLine(tNow, 'kpfun-saveApi'),
49
+ //雜訊:無 event 欄位(應被忽略,不計入 count)
50
+ JSON.stringify({ level: 30, time: tNow.valueOf(), pid: 1, hostname: 'test', msg: 'no-event-here' }),
51
+ //雜訊:壞 JSON 行(應被忽略)
52
+ '{ this is not valid json',
53
+ ]
54
+ //前一小時:verifyConn ×1 → count 1
55
+ let lines2 = [
56
+ logLine(tPrev, 'verifyConn', { apiType: 'ws' }),
57
+ ]
58
+ //30 天前:1 筆(超出 timeLength=7 天,應被排除)
59
+ let lines3 = [
60
+ logLine(tOld, 'verifyConn', { apiType: 'ws' }),
61
+ ]
62
+
63
+ //分檔寫入(模擬輪檔多檔;staEvent 會掃整個資料夾)
64
+ fs.writeFileSync(path.join(fdLog, `${keyNow}.log`), lines1.join('\n') + '\n', 'utf8')
65
+ fs.writeFileSync(path.join(fdLog, `${keyPrev}.log`), lines2.join('\n') + '\n', 'utf8')
66
+ fs.writeFileSync(path.join(fdLog, `old.log`), lines3.join('\n') + '\n', 'utf8')
67
+ }
68
+
69
+
70
+ function findBucket(rs, time) {
71
+ return rs.find((v) => v.time === time)
72
+ }
73
+
74
+
75
+ //testFilterVpfsByWindowDirect:直測 filterVpfsByWindow「粒度自適應」防線(filterVpfsByWindow.mjs:22),
76
+ //以自造 vpfs 物件陣列覆蓋 P-2 要求之三個分支,不經 staEvent/fsTreeFolder。
77
+ function testFilterVpfsByWindowDirect() {
78
+
79
+ //tStart 固定為 2026-07-08 21:00,keyStart(fmt='YYYY-MM-DDTHH')= '2026-07-08T21'
80
+ let tStart = ot('2026-07-08T21:00:00')
81
+ let fmtHr2 = 'YYYY-MM-DDTHH'
82
+
83
+ //①day 粒度檔名(10 字元)× fmt='YYYY-MM-DDTHH',且 tStart 落在該日內 → 應保留。
84
+ //對應規格:filterVpfsByWindow.mjs:11-14 註解——不 slice 則 '2026-07-08' < '2026-07-08T21'(同日
85
+ //字典序比較,短字串為前綴恆較小)誤判窗外漏讀整天;filterVpfsByWindow.mjs:22 之
86
+ //`bn >= keyStart.slice(0, bn.length)` 為此案例之防線(slice 後同為 '2026-07-08',相等即保留)。
87
+ let vpfDaySameDate = { path: '/x/2026-07-08.log', name: '2026-07-08.log', isFolder: false, level: 1 }
88
+ let rs1 = filterVpfsByWindow([vpfDaySameDate], tStart, fmtHr2)
89
+ assert.strictEqual(rs1.length, 1, 'day 粒度同日檔名(10 字元)於 hr fmt(13 字元)下應被保留')
90
+
91
+ //②明確窗外之 hr 檔名(早於 tStart 所在小時)→ 應剔除。
92
+ //對應規格:filterVpfsByWindow.mjs:22 主邏輯 `bn >= keyStart.slice(0, bn.length)`——
93
+ //bn 與 keyStart 同為 hr 粒度(13 字元)時 slice 為 no-op,純字串比較即可判窗外。
94
+ let vpfHrBefore = { path: '/x/2026-07-08T10.log', name: '2026-07-08T10.log', isFolder: false, level: 1 }
95
+ let rs2 = filterVpfsByWindow([vpfHrBefore], tStart, fmtHr2)
96
+ assert.strictEqual(rs2.length, 0, '明確窗外(早於 tStart 小時)之 hr 檔名應被剔除')
97
+
98
+ //③非 ISO 前綴檔名 → fail-open 保留。
99
+ //對應規格:filterVpfsByWindow.mjs:9,19-20 註解——「fail-open: 檔名不符 ISO 前綴者一律保留
100
+ //(寧可多讀, 不可漏讀)」。
101
+ let vpfNonIso = { path: '/x/current.log', name: 'current.log', isFolder: false, level: 1 }
102
+ let rs3 = filterVpfsByWindow([vpfNonIso], tStart, fmtHr2)
103
+ assert.strictEqual(rs3.length, 1, '非 ISO 前綴檔名應 fail-open 保留')
104
+
105
+ console.log('=== testFilterVpfsByWindowDirect PASS ===')
106
+ }
107
+
108
+
109
+ //testGranularityAdaptiveIntegration:整合路徑驗證——srLog 之 logInterval(決定檔名粒度)與 staLogs 之
110
+ //timeInterval(決定 fmt)不同調時(day 粒度檔名 × hr 粒度 fmt),窗內事件仍須被 staEvent 讀到。
111
+ //對應規格:filterVpfsByWindow.mjs:11-14 註解之情境重現於整合路徑(經 fsTreeFolder + fsBuildReadStreamText
112
+ //實際開檔讀行),證明 filterVpfsByWindow.mjs:22 之 slice 防線在 staEvent.mjs:57 呼叫處確實生效,
113
+ //而非僅單元測試層級成立。
114
+ async function testGranularityAdaptiveIntegration() {
115
+ let fdLog2 = './tmp/_logs-test-staEvent-granularity'
116
+ fs.rmSync(fdLog2, { recursive: true, force: true })
117
+ fs.mkdirSync(fdLog2, { recursive: true })
118
+
119
+ //tRef 近似 staEvent 內部之 tStart(timeLength=0 → tStart ≈ now);day 粒度檔名(10 字元)= tRef 當日
120
+ let tRef = ot()
121
+ let dayFileName = `${tRef.format('YYYY-MM-DD')}.log`
122
+ let tEvent = tRef.add(5, 'second') //晚於 tStart,使 staEvent.mjs:76 之 per-line `t.isAfter(tStart)` 成立
123
+ let lines = [logLine(tEvent, 'granularityProbe')]
124
+ fs.writeFileSync(path.join(fdLog2, dayFileName), lines.join('\n') + '\n', 'utf8')
125
+
126
+ let rsG = await staEvent(0, 'hr', { fdLog: fdLog2 })
127
+ let totalG = rsG.reduce((acc, v) => acc + v.data.count, 0)
128
+ assert.strictEqual(totalG, 1, 'day 粒度檔名於 hr timeInterval 下,窗內事件應被 staEvent 計入(不被誤判窗外漏讀)')
129
+
130
+ fs.rmSync(fdLog2, { recursive: true, force: true })
131
+
132
+ console.log('=== testGranularityAdaptiveIntegration PASS ===')
133
+ }
134
+
135
+
136
+ async function main() {
137
+
138
+ writeSyntheticLogs()
139
+
140
+ //=== 'hr' 粒度 ===
141
+ let rs = await staEvent(7, 'hr', { fdLog })
142
+
143
+ //本小時 bucket:精確比對各 event 與總數
144
+ let bNow = findBucket(rs, keyNow)
145
+ assert.ok(bNow, `應有本小時 bucket ${keyNow}`)
146
+ assert.strictEqual(bNow.data.count, 6, '本小時總次數應為 6')
147
+ assert.strictEqual(bNow.data['verifyConn'], 3, '本小時 verifyConn 應為 3')
148
+ assert.strictEqual(bNow.data['kpfun-getApisList'], 2, '本小時 kpfun-getApisList 應為 2')
149
+ assert.strictEqual(bNow.data['kpfun-saveApi'], 1, '本小時 kpfun-saveApi 應為 1')
150
+
151
+ //前一小時 bucket
152
+ let bPrev = findBucket(rs, keyPrev)
153
+ assert.ok(bPrev, `應有前一小時 bucket ${keyPrev}`)
154
+ assert.strictEqual(bPrev.data.count, 1, '前一小時總次數應為 1')
155
+ assert.strictEqual(bPrev.data['verifyConn'], 1, '前一小時 verifyConn 應為 1')
156
+
157
+ //無資料時段應補 0(kpTime 補零生效):取一個既非 now 亦非 prev 的 bucket
158
+ let bEmpty = rs.find((v) => v.time !== keyNow && v.time !== keyPrev)
159
+ assert.ok(bEmpty, '應有其他時段 bucket')
160
+ assert.strictEqual(bEmpty.data.count, 0, '無資料時段 count 應為 0')
161
+
162
+ //雜訊與過期排除驗證:全部 bucket count 總和 = 7(本小時6 + 前一小時1;無 event/壞行/30天前 皆排除)
163
+ let total = rs.reduce((acc, v) => acc + v.data.count, 0)
164
+ assert.strictEqual(total, 7, '有效事件總和應為 7(無 event/壞行/超期 均不計)')
165
+
166
+ //時間遞增排序
167
+ let times = rs.map((v) => v.time)
168
+ let sorted = [...times].sort()
169
+ assert.deepStrictEqual(times, sorted, '結果應依時間遞增排序')
170
+
171
+ //=== 'day' 粒度 ===(同日事件聚到同一天)
172
+ let rsDay = await staEvent(7, 'day', { fdLog })
173
+ let totalDay = rsDay.reduce((acc, v) => acc + v.data.count, 0)
174
+ assert.strictEqual(totalDay, 7, 'day 粒度有效事件總和亦應為 7')
175
+ let keyDayNow = tNow.format('YYYY-MM-DD')
176
+ let bDay = findBucket(rsDay, keyDayNow)
177
+ assert.ok(bDay, `應有本日 bucket ${keyDayNow}`)
178
+ //本日至少含本小時 6 筆(前一小時若跨日則在另一桶,故用 >=)
179
+ assert.ok(bDay.data['verifyConn'] >= 3, '本日 verifyConn 應 >= 3')
180
+
181
+ //清理
182
+ fs.rmSync(fdLog, { recursive: true, force: true })
183
+
184
+ //=== 粒度自適應防線(P-2)===
185
+ testFilterVpfsByWindowDirect()
186
+ await testGranularityAdaptiveIntegration()
187
+
188
+ console.log('=== test_staEvent PASS ===')
189
+ console.log(`hr buckets: ${rs.length}, 本小時=${JSON.stringify(bNow.data)}, 前一小時=${JSON.stringify(bPrev.data)}`)
190
+ }
191
+
192
+
193
+ main().catch((err) => {
194
+ console.error('=== test_staEvent FAIL ===')
195
+ console.error(err)
196
+ try { fs.rmSync(fdLog, { recursive: true, force: true }) } catch (e) {}
197
+ process.exit(1)
198
+ })
package/settings.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ // 應用層設定(JSON5 格式:可加註解)。
3
+ // db 連線等系統參數仍在 g.getSettings.mjs(供 g.mOrm 共用);本檔放可調整之 app 設定。
4
+ // srv.mjs 以 `node srv.mjs <pathSettings>` 載入本檔(預設 ./settings.json),供日後更細緻功能擴充。
5
+
6
+ // 是否顯示右上語系切換選單:'y' 顯示 / 'n' 隱藏
7
+ showLanguage: 'y',
8
+
9
+ // 初始語系(server 注入 index.html → 首次載入即此語系):'eng' / 'cht'
10
+ language: 'eng',
11
+
12
+ // 伺服器通訊 port
13
+ serverPort: 11005,
14
+
15
+ logFd: './logs',
16
+ logInterval: 'hr',
17
+
18
+ }
package/src/App.vue CHANGED
@@ -10,10 +10,9 @@
10
10
  <Layout v-if="ready"></Layout>
11
11
  </transition>
12
12
 
13
- <!--
14
13
  <LoadingWinBar></LoadingWinBar>
15
14
  <CheckYesNo></CheckYesNo>
16
- <CheckYes></CheckYes> -->
15
+ <CheckYes></CheckYes>
17
16
 
18
17
  </div>
19
18
  </template>
@@ -27,12 +26,18 @@ import isDev from 'wsemi/src/isDev.mjs'
27
26
  import wui from 'w-ui-loginout/src/WUiLoginout.mjs'
28
27
  import Layout from './components/Layout.vue'
29
28
  import LayoutState from './components/LayoutState.vue'
29
+ import LoadingWinBar from './components/Common/LoadingWinBar.vue'
30
+ import CheckYesNo from './components/Common/CheckYesNo.vue'
31
+ import CheckYes from './components/Common/CheckYes.vue'
30
32
 
31
33
 
32
34
  export default {
33
35
  components: {
34
36
  Layout,
35
37
  LayoutState,
38
+ LoadingWinBar,
39
+ CheckYesNo,
40
+ CheckYes,
36
41
  },
37
42
  data: function() {
38
43
  return {
@@ -47,10 +52,9 @@ export default {
47
52
  //setVo, 更換ui內vo, 才能使用廣播技術, 更換語系才能用廣播通知全部組件forceUpdate
48
53
  vo.$ui.setVo(vo)
49
54
 
50
- //setLang
51
- let lang = get(window, '___pmwperm___.language', '')
52
- vo.$ui.setLang(lang, 'app init') //初始化先讀取html內語系設定進行變更
53
- // console.log('lang', lang)
55
+ //setLang:交由 getLang 解析(優先序 URL ?lang= > window ___pmwperm___.language > store),
56
+ //支援「以 ?lang= 指定語系載入初始畫面」(對齊 w-web-sso),無 URL 時行為同舊(讀 html 注入語系)。
57
+ vo.$ui.setLang(null, 'app init')
54
58
 
55
59
  function loginSuccess(data) {
56
60
  console.log('login success', cloneDeep(data.user))
@@ -131,7 +135,7 @@ export default {
131
135
  <style>
132
136
  html,
133
137
  body {
134
- font-family: '微軟正黑體', 'Microsoft JhengHei', 'MicrosoftJhengHeiRegular', 'Avenir', Helvetica, Arial, sans-serif;
138
+ font-family: var(--font);
135
139
  overflow-y: hidden;
136
140
  }
137
141
 
@@ -0,0 +1,113 @@
1
+ /* ============================================================
2
+ Scalar 風格 UI 設計系統 v2(light + Scalar 配色,結構重設計)
3
+ 由 src/main.js import;全域非 scoped。對照 zMockup-scalar-v2.html。
4
+ ============================================================ */
5
+ :root{
6
+ --bg-1:#ffffff; --bg-2:#f7f8fa; --bg-3:#eceef1; --bg-accent:rgba(0,153,255,0.08);
7
+ --code-bg:#0d1117; --code-fg:#c9d1d9;
8
+ --c-1:#1b1b1b; --c-2:#6b7280; --c-3:#9aa1ac;
9
+ --accent:#0099ff; --accent-hover:#0077cc;
10
+ --border:#e6e8ec; --border-soft:#eef0f3;
11
+ /* method(Scalar 配色:GET藍/POST綠/PUT橙/DEL紅/PATCH黃) */
12
+ --m-get:#0082d0; --m-get-bg:rgba(0,130,208,0.10);
13
+ --m-post:#069061; --m-post-bg:rgba(6,144,97,0.11);
14
+ --m-put:#c2670a; --m-put-bg:rgba(255,88,0,0.10);
15
+ --m-del:#e01b54; --m-del-bg:rgba(239,0,6,0.09);
16
+ --m-patch:#b7860b;--m-patch-bg:rgba(237,190,32,0.16);
17
+ --m-other:#6b7280;--m-other-bg:rgba(107,114,128,0.10);
18
+ /* status */
19
+ --s-2xx:#069061; --s-2xx-bg:rgba(6,144,97,0.12);
20
+ --s-3xx:#0082d0; --s-3xx-bg:rgba(0,130,208,0.10);
21
+ --s-4xx:#c2670a; --s-4xx-bg:rgba(255,88,0,0.11);
22
+ --s-5xx:#e01b54; --s-5xx-bg:rgba(239,0,6,0.10);
23
+ --danger:#e01b54; --danger-bg:#fff5f7; --success:#069061;
24
+ --ok-fg:#069061; --ok-bg:rgba(6,144,97,0.12);
25
+ --font:"Microsoft JhengHei","微軟正黑體",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
26
+ --font-code:ui-monospace,"SFMono-Regular",Menlo,Consolas,"Courier New",monospace;
27
+ --fs-h1:27px; --fs-h2:18px; --fs-h3:15px; --fs-body:14px; --fs-sm:13px; --fs-xs:11.5px;
28
+ --fw-medium:500; --fw-bold:600;
29
+ --radius:8px; --radius-sm:5px; --radius-lg:11px;
30
+ --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-6:24px; --sp-8:32px;
31
+ --focus:0 0 0 3px rgba(0,153,255,0.18);
32
+ --focus-danger:0 0 0 3px rgba(239,0,6,0.15);
33
+ }
34
+
35
+ /* ---------- method badge ---------- */
36
+ .w-mb{font-family:var(--font-code);font-weight:var(--fw-bold);text-transform:uppercase;font-size:10px;
37
+ letter-spacing:.03em;padding:2px 6px;min-width:40px;text-align:center;border-radius:var(--radius-sm);
38
+ border:none;display:inline-block;flex:0 0 auto;line-height:1.5}
39
+ .w-mb.get{color:var(--m-get);background:var(--m-get-bg)}
40
+ .w-mb.post{color:var(--m-post);background:var(--m-post-bg)}
41
+ .w-mb.put{color:var(--m-put);background:var(--m-put-bg)}
42
+ .w-mb.del{color:var(--m-del);background:var(--m-del-bg)}
43
+ .w-mb.patch{color:var(--m-patch);background:var(--m-patch-bg)}
44
+ .w-mb.other{color:var(--m-other);background:var(--m-other-bg)}
45
+
46
+ /* ---------- 共用輸入 / 按鈕(edit/test 用) ---------- */
47
+ .w-input,.w-textarea,.w-select{width:100%;box-sizing:border-box;padding:8px 12px;font-size:var(--fs-body);
48
+ color:var(--c-1);background:var(--bg-1);border:1px solid var(--border);border-radius:var(--radius);
49
+ outline:none;transition:border-color .15s,box-shadow .15s;font-family:var(--font)}
50
+ .w-textarea{font-family:var(--font-code);font-size:var(--fs-sm);resize:vertical;min-height:72px;line-height:1.55}
51
+ .w-input:focus,.w-textarea:focus,.w-select:focus{border-color:var(--accent);box-shadow:var(--focus)}
52
+ .w-input.is-error,.w-textarea.is-error{border-color:var(--danger);box-shadow:var(--focus-danger)}
53
+ .w-btn-primary{padding:8px 17px;font-size:var(--fs-body);font-weight:var(--fw-medium);border-radius:var(--radius);
54
+ border:1px solid var(--accent);background:var(--accent);color:#fff;cursor:pointer;transition:.15s}
55
+ .w-btn-primary:hover{background:var(--accent-hover);border-color:var(--accent-hover)}
56
+ .w-btn-danger{padding:8px 17px;font-size:var(--fs-body);border-radius:var(--radius);border:1px solid var(--danger);
57
+ background:transparent;color:var(--danger);cursor:pointer;transition:.15s}
58
+ .w-btn-danger:hover{background:var(--danger-bg)}
59
+ .w-btn-ghost{padding:7px 14px;font-size:var(--fs-sm);border:1px solid var(--border);background:transparent;
60
+ color:var(--c-2);border-radius:var(--radius);cursor:pointer;transition:.15s}
61
+ .w-btn-ghost:hover{background:var(--bg-3)}
62
+
63
+ /* ---------- 雜項 ---------- */
64
+ .w-mono{font-family:var(--font-code)}
65
+ .w-muted{color:var(--c-2)}
66
+ .w-tnum{font-variant-numeric:tabular-nums}
67
+
68
+ /* ---------- segmented 分頁(Docs/Edit/Test) ---------- */
69
+ .w-seg{display:inline-flex;gap:2px;background:var(--bg-2);border:1px solid var(--border);border-radius:9px;padding:3px}
70
+ .w-seg-btn{border:none;background:transparent;color:var(--c-2);font-size:13px;padding:5px 17px;border-radius:6px;
71
+ cursor:pointer;font-weight:var(--fw-medium);transition:.15s}
72
+ .w-seg-btn:hover{color:var(--c-1)}
73
+ .w-seg-btn.on{background:var(--bg-1);color:var(--accent);box-shadow:0 1px 2px rgba(0,0,0,.07)}
74
+
75
+ /* ---------- DOCS:operation header + pills ---------- */
76
+ .op-title{font-size:var(--fs-h1);font-weight:var(--fw-bold);letter-spacing:-.01em;margin:6px 0 10px;color:var(--c-1)}
77
+ .op-path{display:inline-flex;align-items:center;gap:10px;padding:8px 13px;background:var(--bg-2);
78
+ border:1px solid var(--border);border-radius:var(--radius);font-family:var(--font-code);font-size:13px;color:var(--c-1);
79
+ max-width:100%;overflow:auto}
80
+ .op-desc{color:var(--c-2);margin:14px 0 0;max-width:720px;font-size:14.5px;line-height:1.7}
81
+ .pills{display:flex;flex-wrap:wrap;gap:7px;margin:16px 0 4px}
82
+ .pill{font-size:12px;color:var(--c-2);background:var(--bg-2);border:1px solid var(--border);border-radius:20px;padding:3px 11px}
83
+ .pill b{color:var(--c-1);font-weight:var(--fw-bold)}
84
+ .pill.ok{color:var(--ok-fg);background:var(--ok-bg);border-color:transparent;font-weight:var(--fw-bold)}
85
+ .pill .kw-chip{display:inline-block;vertical-align:middle;font-size:11px;font-weight:var(--fw-bold);color:#0082d0;background:rgba(0,130,208,0.12);border-radius:9px;padding:1px 7px;margin-left:5px}
86
+
87
+ /* ---------- DOCS:兩欄 + 區塊 ---------- */
88
+ .split{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(0,1fr);gap:34px;margin-top:28px;align-items:start}
89
+ @media (max-width:1100px){ .split{grid-template-columns:1fr} }
90
+ .dsec{margin-bottom:28px}
91
+ .dsec-h{font-size:13px;font-weight:var(--fw-bold);letter-spacing:.05em;text-transform:uppercase;color:var(--c-3);
92
+ padding-bottom:9px;border-bottom:1px solid var(--border-soft);margin-bottom:12px}
93
+
94
+ /* 左欄 Markdown 由 MdPanel.vue 自身 scoped 樣式處理(Scalar 乾淨表格/標題) */
95
+
96
+ /* ---------- 右欄 code rail(Scalar 招牌) ---------- */
97
+ .rail{position:sticky;top:14px;display:flex;flex-direction:column;gap:16px}
98
+ .card{border-radius:var(--radius-lg);overflow:hidden;border:1px solid var(--border)}
99
+ .card-h{display:flex;align-items:center;gap:9px;padding:9px 13px;background:var(--bg-2);border-bottom:1px solid var(--border);font-size:12px;color:var(--c-2)}
100
+ .card-h .lab{font-weight:var(--fw-bold);color:var(--c-1)}
101
+ .card-h .st{margin-left:auto;font-family:var(--font-code);font-size:11.5px;font-weight:var(--fw-bold);
102
+ color:var(--ok-fg);background:var(--ok-bg);padding:1px 8px;border-radius:var(--radius-sm)}
103
+ .code{background:var(--code-bg);color:var(--code-fg);font-family:var(--font-code);font-size:12.5px;line-height:1.7;
104
+ padding:14px 16px;white-space:pre;overflow:auto;margin:0}
105
+ .code .k{color:#79c0ff}.code .s{color:#a5d6ff}.code .n{color:#f0883e}.code .pn{color:#8b949e}.code .b{color:#ff7b72}
106
+
107
+ /* ---------- TEST:回應狀態徽章 ---------- */
108
+ .w-status{font-family:var(--font-code);font-weight:var(--fw-bold);font-size:13px;padding:2px 11px;
109
+ border-radius:var(--radius-sm);display:inline-block}
110
+ .w-status.s2{color:var(--s-2xx);background:var(--s-2xx-bg)}
111
+ .w-status.s3{color:var(--s-3xx);background:var(--s-3xx-bg)}
112
+ .w-status.s4{color:var(--s-4xx);background:var(--s-4xx-bg)}
113
+ .w-status.s5{color:var(--s-5xx);background:var(--s-5xx-bg)}
@@ -0,0 +1,102 @@
1
+ <template>
2
+ <WConfirm
3
+ :show.sync="bShow"
4
+ :title="$t('systemMessage')"
5
+ :content="content"
6
+ :contentIcon="contentIcon"
7
+ :contentIconColor="contentIconColor"
8
+ :_contentColor="'white'"
9
+ :_contentIconColor="'white'"
10
+ :_contentBackgroundColor="'orange lighten-5'"
11
+ :headerBackgroundColor="'grey lighten-3'"
12
+ :hasNoBtn="false"
13
+ :yesBtnText="$t('ok')"
14
+ @click-yes="clickUnit()"
15
+ ></WConfirm>
16
+ </template>
17
+
18
+ <script>
19
+ import Vue from 'vue'
20
+ import { mdiAlert, mdiCheckCircle } from '@mdi/js'
21
+ import genPm from 'wsemi/src/genPm.mjs'
22
+ import WConfirm from 'w-component-vue/src/components/WConfirm.vue'
23
+
24
+ export default {
25
+ components: {
26
+ WConfirm,
27
+ },
28
+ props: {
29
+ },
30
+ data: function() {
31
+ return {
32
+
33
+ bShow: false,
34
+ pm: null,
35
+
36
+ content: '',
37
+
38
+ //content icon(依 type 切換:success=綠勾,其餘=驚嘆號)
39
+ contentIcon: mdiAlert,
40
+ contentIconColor: '#D81B60',
41
+
42
+ }
43
+ },
44
+ mounted: function() {
45
+ //console.log('mounted')
46
+
47
+ let vo = this
48
+
49
+ //set
50
+ Vue.prototype.$dg.showCheckYes = vo.show
51
+
52
+ },
53
+ computed: {
54
+ },
55
+ methods: {
56
+
57
+ show: function (content, opt = {}) {
58
+ //console.log('methods show', content)
59
+
60
+ let vo = this
61
+
62
+ //pm
63
+ vo.pm = genPm()
64
+
65
+ //save
66
+ vo.content = content
67
+
68
+ //content icon:type='success' 顯示綠勾,其餘維持驚嘆號
69
+ if (opt && opt.type === 'success') {
70
+ vo.contentIcon = mdiCheckCircle
71
+ vo.contentIconColor = '#2E7D32'
72
+ }
73
+ else {
74
+ vo.contentIcon = mdiAlert
75
+ vo.contentIconColor = '#D81B60'
76
+ }
77
+
78
+ //show
79
+ vo.bShow = true
80
+
81
+ return vo.pm
82
+ },
83
+
84
+ clickUnit: function() {
85
+ //console.log('methods clickUnit')
86
+
87
+ let vo = this
88
+
89
+ //resolve
90
+ vo.pm.resolve()
91
+
92
+ //hide
93
+ vo.bShow = false
94
+
95
+ },
96
+
97
+ }
98
+ }
99
+ </script>
100
+
101
+ <style scoped>
102
+ </style>