w-web-api 1.0.52 → 1.0.53

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 (84) hide show
  1. package/_gh-pages-deploy/.nojekyll +0 -0
  2. package/_gh-pages-deploy/WWebApi.html +898 -0
  3. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  4. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  5. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  6. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  7. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  8. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  9. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  10. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  11. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  12. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  13. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  14. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  15. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  16. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  17. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  18. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  19. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  20. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  21. package/_gh-pages-deploy/index.html +84 -0
  22. package/_gh-pages-deploy/scripts/collapse.js +39 -0
  23. package/_gh-pages-deploy/scripts/commonNav.js +28 -0
  24. package/_gh-pages-deploy/scripts/linenumber.js +25 -0
  25. package/_gh-pages-deploy/scripts/nav.js +12 -0
  26. package/_gh-pages-deploy/scripts/polyfill.js +4 -0
  27. package/_gh-pages-deploy/scripts/prettify/Apache-License-2.0.txt +202 -0
  28. package/_gh-pages-deploy/scripts/prettify/lang-css.js +2 -0
  29. package/_gh-pages-deploy/scripts/prettify/prettify.js +28 -0
  30. package/_gh-pages-deploy/scripts/search.js +99 -0
  31. package/{docs/WWebApi.mjs.html → _gh-pages-deploy/server_WWebApi.mjs.html} +96 -39
  32. package/_gh-pages-deploy/src_components_Common_CheckYesNo.vue.html +187 -0
  33. package/_gh-pages-deploy/styles/jsdoc.css +776 -0
  34. package/_gh-pages-deploy/styles/prettify.css +80 -0
  35. package/dist/css/app.fe69497a.css +1 -0
  36. package/dist/index.tmp +4 -2
  37. package/dist/js/app-legacy.15819aa7.js +1 -0
  38. package/dist/js/app.15819aa7.js +1 -0
  39. package/dist/js/chunk-vendors-legacy.c1a32dd1.js +15 -0
  40. package/dist/js/chunk-vendors.c1a32dd1.js +15 -0
  41. package/dist/w-web-api.umd.js +2 -2
  42. package/dist/w-web-api.umd.js.map +1 -1
  43. package/docs/WWebApi.html +2 -2
  44. package/docs/index.html +1 -1
  45. package/docs/server_WWebApi.mjs.html +1012 -0
  46. package/docs/src_components_Common_CheckYesNo.vue.html +187 -0
  47. package/g.getSettings.mjs +3 -0
  48. package/package.json +15 -11
  49. package/public/index.html +9 -2
  50. package/server/WWebApi.mjs +93 -36
  51. package/server/procApis.mjs +62 -0
  52. package/server/procLang.mjs +386 -0
  53. package/server/procProxy.mjs +120 -0
  54. package/server/srLog.mjs +30 -0
  55. package/server/staLogs/staEvent.mjs +129 -0
  56. package/server/staLogs/test_staEvent.mjs +132 -0
  57. package/settings.json +14 -0
  58. package/src/App.vue +11 -7
  59. package/src/assets/scalar-ui.css +113 -0
  60. package/src/components/Common/CheckYes.vue +102 -0
  61. package/src/components/Common/CheckYesNo.vue +115 -0
  62. package/src/components/Common/LoadingWinBar.vue +114 -0
  63. package/src/components/Layout.vue +8 -7
  64. package/src/components/LayoutContent.vue +308 -333
  65. package/src/components/LayoutContentEdit.vue +577 -0
  66. package/src/components/LayoutContentStats.vue +485 -0
  67. package/src/components/LayoutContentTest.vue +799 -0
  68. package/src/components/MdPanel.vue +80 -58
  69. package/src/main.js +8 -0
  70. package/src/plugins/mUI.mjs +19 -4
  71. package/src/schema/tables/apis.mjs +30 -0
  72. package/srv.mjs +47 -19
  73. package/tableTags.json +1 -1
  74. package/.github/workflows/ci-test.yml +0 -24
  75. package/.vscode/launch.json +0 -18
  76. package/db/wapis/apis/data.mdb +0 -0
  77. package/db/wapis/apis/lock.mdb +0 -0
  78. package/dist/css/app.b5cf2ef5.css +0 -1
  79. package/dist/js/app-legacy.927b5679.js +0 -1
  80. package/dist/js/app.927b5679.js +0 -1
  81. package/dist/js/chunk-vendors-legacy.ffb0c6c5.js +0 -13
  82. package/dist/js/chunk-vendors.ffb0c6c5.js +0 -13
  83. package/tableTags-web-api.json +0 -1
  84. package/test/all.test.mjs +0 -10
@@ -0,0 +1,129 @@
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
+
13
+
14
+ //staEvent(通用事件頻率統計,骨架移植自 w-web-sso server/staLogs/staIp.mjs,但「不過濾特定 event」、
15
+ //內層改依 `event` 欄位分組):掃 fdLog 下所有 NDJSON log 檔,依時間 bucket(hr/day)+ 各 event 名聚合次數。
16
+ //輸出供前端統計圖:可呈現「全部(count)」或「各 event 各自」之時段發生頻率。
17
+ //
18
+ //回傳格式(時間遞增、無資料時段補 0):
19
+ // [
20
+ // { time: '2026-06-22T10', data: { count: 6, 'verifyConn': 3, 'kpfun-getApisList': 2, 'kpfun-saveApi': 1 } },
21
+ // { time: '2026-06-22T11', data: { count: 0 } },
22
+ // ...
23
+ // ]
24
+ //
25
+ //參數:
26
+ // timeLength 往回統計天數(預設 7)
27
+ // timeInterval 時間粒度 'hr'(每小時,預設)/ 'day'(每日)
28
+ // opt.fdLog log 資料夾(預設 './_logs',須與 srLog 寫入之 logFd 一致;e2e 可指向合成 log 達 hermetic)
29
+ async function staEvent(timeLength = 7, timeInterval = 'hr', opt = {}) {
30
+
31
+ //fdLog
32
+ let fdLog = get(opt, 'fdLog')
33
+ if (!isestr(fdLog)) {
34
+ fdLog = './_logs'
35
+ }
36
+
37
+ //fmt
38
+ let fmt = timeInterval === 'day' ? 'YYYY-MM-DD' : 'YYYY-MM-DDTHH'
39
+
40
+ //unit
41
+ let unit = timeInterval === 'hr' ? 'hour' : 'day'
42
+
43
+ //kpTime,產生完整的時間區間(無資料時段亦補 { count: 0 },圖表 x 軸不留空洞)
44
+ let now = ot()
45
+ let tStart = now.subtract(timeLength, 'day')
46
+ let tCurr = tStart.startOf(unit)
47
+ let tEnd = now.startOf(unit)
48
+ let kpTime = {}
49
+ while (!tCurr.isAfter(tEnd)) {
50
+ kpTime[tCurr.format(fmt)] = { data: { count: 0 } }
51
+ tCurr = tCurr.add(1, unit)
52
+ }
53
+
54
+ //vpfs
55
+ let vpfs = fsTreeFolder(fdLog)
56
+
57
+ //logs(逐檔逐行 stream parse,挑出 tStart 之後、且帶 event 字串者)
58
+ let logs = []
59
+ for (let vpf of vpfs) {
60
+
61
+ //fp
62
+ let fp = vpf.path
63
+
64
+ //ev
65
+ let ev = fsBuildReadStreamText(fp)
66
+
67
+ //line
68
+ ev.on('line', (line) => {
69
+ let v = null
70
+ try {
71
+ v = JSON.parse(line)
72
+ let t = ot(v.time)
73
+ let timeFmt = t.format(fmt)
74
+ let b1 = t.isAfter(tStart)
75
+ let b2 = isestr(v.event) //通用:任何帶 event 字串之 log 皆納入(非鎖定單一 event)
76
+ let b = b1 && b2
77
+ if (b) {
78
+ logs.push({
79
+ timeFmt,
80
+ ...v,
81
+ })
82
+ }
83
+ }
84
+ catch (err) {} //略過非 JSON / 壞行
85
+ })
86
+
87
+ //await close
88
+ let pm = genPm()
89
+ ev.on('close', () => {
90
+ pm.resolve()
91
+ })
92
+ await pm
93
+
94
+ }
95
+
96
+ //gs,依時間 bucket 分組
97
+ let gs = groupBy(logs, 'timeFmt')
98
+
99
+ //gsLog,各 bucket 內依 event 名計數 + 總數 count
100
+ let gsLog = mapValues(gs, vs => {
101
+ let count = size(vs)
102
+ let gev = groupBy(vs, 'event')
103
+ let kp = {}
104
+ each(gev, (vvs, event) => {
105
+ kp[event] = size(vvs)
106
+ })
107
+ return {
108
+ data: {
109
+ count,
110
+ ...kp,
111
+ }
112
+ }
113
+ })
114
+
115
+ //merge(補零時段 + 實際資料)
116
+ let mr = merge({}, kpTime, gsLog)
117
+
118
+ //rs(時間遞增排序輸出)
119
+ let rs = Object.keys(mr)
120
+ .sort()
121
+ .map(time => ({
122
+ time,
123
+ ...mr[time]
124
+ }))
125
+
126
+ return rs
127
+ }
128
+
129
+ export default staEvent
@@ -0,0 +1,132 @@
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
+
14
+
15
+ //tmp log 資料夾(落在主 cwd ./tmp/,集中可一鍵清;不污染真實 ./_logs)
16
+ let fdLog = './tmp/_logs-test-staEvent'
17
+
18
+ //以「當前小時正中(:30)」為錨點,避免測試剛好跨小時邊界導致 bucket 飄移
19
+ let anchor = ot().startOf('hour')
20
+ let tNow = anchor.add(30, 'minute') //本小時 bucket
21
+ let tPrev = anchor.subtract(30, 'minute') //前一小時 bucket
22
+ let tOld = anchor.subtract(30, 'day') //30 天前(超出 timeLength=7,應被排除)
23
+
24
+ let fmtHr = 'YYYY-MM-DDTHH'
25
+ let keyNow = tNow.format(fmtHr)
26
+ let keyPrev = tPrev.format(fmtHr)
27
+
28
+
29
+ //一筆 log(NDJSON 一行)
30
+ function logLine(t, event, extra = {}) {
31
+ let o = { level: 30, time: t.valueOf(), pid: 1, hostname: 'test', event, ...extra }
32
+ return JSON.stringify(o)
33
+ }
34
+
35
+
36
+ function writeSyntheticLogs() {
37
+ fs.rmSync(fdLog, { recursive: true, force: true })
38
+ fs.mkdirSync(fdLog, { recursive: true })
39
+
40
+ //本小時:verifyConn ×3、kpfun-getApisList ×2、kpfun-saveApi ×1 → count 6
41
+ let lines1 = [
42
+ logLine(tNow, 'verifyConn', { apiType: 'ws' }),
43
+ logLine(tNow, 'verifyConn', { apiType: 'ws' }),
44
+ logLine(tNow, 'verifyConn', { apiType: 'http' }),
45
+ logLine(tNow, 'kpfun-getApisList'),
46
+ logLine(tNow, 'kpfun-getApisList'),
47
+ logLine(tNow, 'kpfun-saveApi'),
48
+ //雜訊:無 event 欄位(應被忽略,不計入 count)
49
+ JSON.stringify({ level: 30, time: tNow.valueOf(), pid: 1, hostname: 'test', msg: 'no-event-here' }),
50
+ //雜訊:壞 JSON 行(應被忽略)
51
+ '{ this is not valid json',
52
+ ]
53
+ //前一小時:verifyConn ×1 → count 1
54
+ let lines2 = [
55
+ logLine(tPrev, 'verifyConn', { apiType: 'ws' }),
56
+ ]
57
+ //30 天前:1 筆(超出 timeLength=7 天,應被排除)
58
+ let lines3 = [
59
+ logLine(tOld, 'verifyConn', { apiType: 'ws' }),
60
+ ]
61
+
62
+ //分檔寫入(模擬輪檔多檔;staEvent 會掃整個資料夾)
63
+ fs.writeFileSync(path.join(fdLog, `${keyNow}.log`), lines1.join('\n') + '\n', 'utf8')
64
+ fs.writeFileSync(path.join(fdLog, `${keyPrev}.log`), lines2.join('\n') + '\n', 'utf8')
65
+ fs.writeFileSync(path.join(fdLog, `old.log`), lines3.join('\n') + '\n', 'utf8')
66
+ }
67
+
68
+
69
+ function findBucket(rs, time) {
70
+ return rs.find((v) => v.time === time)
71
+ }
72
+
73
+
74
+ async function main() {
75
+
76
+ writeSyntheticLogs()
77
+
78
+ //=== 'hr' 粒度 ===
79
+ let rs = await staEvent(7, 'hr', { fdLog })
80
+
81
+ //本小時 bucket:精確比對各 event 與總數
82
+ let bNow = findBucket(rs, keyNow)
83
+ assert.ok(bNow, `應有本小時 bucket ${keyNow}`)
84
+ assert.strictEqual(bNow.data.count, 6, '本小時總次數應為 6')
85
+ assert.strictEqual(bNow.data['verifyConn'], 3, '本小時 verifyConn 應為 3')
86
+ assert.strictEqual(bNow.data['kpfun-getApisList'], 2, '本小時 kpfun-getApisList 應為 2')
87
+ assert.strictEqual(bNow.data['kpfun-saveApi'], 1, '本小時 kpfun-saveApi 應為 1')
88
+
89
+ //前一小時 bucket
90
+ let bPrev = findBucket(rs, keyPrev)
91
+ assert.ok(bPrev, `應有前一小時 bucket ${keyPrev}`)
92
+ assert.strictEqual(bPrev.data.count, 1, '前一小時總次數應為 1')
93
+ assert.strictEqual(bPrev.data['verifyConn'], 1, '前一小時 verifyConn 應為 1')
94
+
95
+ //無資料時段應補 0(kpTime 補零生效):取一個既非 now 亦非 prev 的 bucket
96
+ let bEmpty = rs.find((v) => v.time !== keyNow && v.time !== keyPrev)
97
+ assert.ok(bEmpty, '應有其他時段 bucket')
98
+ assert.strictEqual(bEmpty.data.count, 0, '無資料時段 count 應為 0')
99
+
100
+ //雜訊與過期排除驗證:全部 bucket count 總和 = 7(本小時6 + 前一小時1;無 event/壞行/30天前 皆排除)
101
+ let total = rs.reduce((acc, v) => acc + v.data.count, 0)
102
+ assert.strictEqual(total, 7, '有效事件總和應為 7(無 event/壞行/超期 均不計)')
103
+
104
+ //時間遞增排序
105
+ let times = rs.map((v) => v.time)
106
+ let sorted = [...times].sort()
107
+ assert.deepStrictEqual(times, sorted, '結果應依時間遞增排序')
108
+
109
+ //=== 'day' 粒度 ===(同日事件聚到同一天)
110
+ let rsDay = await staEvent(7, 'day', { fdLog })
111
+ let totalDay = rsDay.reduce((acc, v) => acc + v.data.count, 0)
112
+ assert.strictEqual(totalDay, 7, 'day 粒度有效事件總和亦應為 7')
113
+ let keyDayNow = tNow.format('YYYY-MM-DD')
114
+ let bDay = findBucket(rsDay, keyDayNow)
115
+ assert.ok(bDay, `應有本日 bucket ${keyDayNow}`)
116
+ //本日至少含本小時 6 筆(前一小時若跨日則在另一桶,故用 >=)
117
+ assert.ok(bDay.data['verifyConn'] >= 3, '本日 verifyConn 應 >= 3')
118
+
119
+ //清理
120
+ fs.rmSync(fdLog, { recursive: true, force: true })
121
+
122
+ console.log('=== test_staEvent PASS ===')
123
+ console.log(`hr buckets: ${rs.length}, 本小時=${JSON.stringify(bNow.data)}, 前一小時=${JSON.stringify(bPrev.data)}`)
124
+ }
125
+
126
+
127
+ main().catch((err) => {
128
+ console.error('=== test_staEvent FAIL ===')
129
+ console.error(err)
130
+ try { fs.rmSync(fdLog, { recursive: true, force: true }) } catch (e) {}
131
+ process.exit(1)
132
+ })
package/settings.json ADDED
@@ -0,0 +1,14 @@
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
+ }
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>
@@ -0,0 +1,115 @@
1
+ <template>
2
+ <WConfirm
3
+ :show.sync="bShow"
4
+ :title="$t('systemMessage')"
5
+ :content="content"
6
+ :_contentColor="'white'"
7
+ :_contentIconColor="'white'"
8
+ :_contentBackgroundColor="'orange lighten-5'"
9
+ :_titleColor="'white'"
10
+ :headerBackgroundColor="'grey lighten-3'"
11
+ :noBtnText="$t('no')"
12
+ :yesBtnText="$t('yes')"
13
+ @click-no="clickBtn(false)"
14
+ @click-yes="clickBtn(true)"
15
+ ></WConfirm>
16
+ </template>
17
+
18
+ <script>
19
+ import Vue from 'vue'
20
+ import genPm from 'wsemi/src/genPm.mjs'
21
+ import WConfirm from 'w-component-vue/src/components/WConfirm.vue'
22
+
23
+ export default {
24
+ components: {
25
+ WConfirm,
26
+ },
27
+ props: {
28
+ },
29
+ data: function() {
30
+ return {
31
+
32
+ bShow: false,
33
+ pm: null,
34
+
35
+ content: '',
36
+
37
+ }
38
+ },
39
+ mounted: function() {
40
+ //console.log('mounted')
41
+
42
+ let vo = this
43
+
44
+ //set
45
+ Vue.prototype.$dg.showCheckYesNo = vo.show
46
+
47
+ },
48
+ computed: {
49
+ },
50
+ methods: {
51
+
52
+ /**
53
+ * 顯示 Yes/No 確認對話框並回傳 Promise。
54
+ *
55
+ * **Contract** (caller 須遵守):
56
+ * - 使用者點 **Yes** → Promise `resolve()` (無值, 進 `.then()`)
57
+ * - 使用者點 **No** → Promise `reject('close')` (進 `.catch(err => err === 'close')`)
58
+ *
59
+ * 正確 caller pattern:
60
+ *
61
+ * vo.$dg.showCheckYesNo('確定要 X?')
62
+ * .then(() => {
63
+ * //使用者點 Yes
64
+ * doX()
65
+ * })
66
+ * .catch((err) => {
67
+ * //使用者點 No (err === 'close') 為正常路徑, 靜默處理
68
+ * if (err === 'close') return
69
+ * //非預期錯誤 (CheckYesNo 本身故障等)
70
+ * console.log('showCheckYesNo unexpected error', err)
71
+ * })
72
+ *
73
+ * @param {string} content - 顯示在 modal 中的訊息文字
74
+ * @returns {Promise<void>} Yes → resolve(); No → reject('close')
75
+ */
76
+ show: function(content) {
77
+ //console.log('methods show', content)
78
+
79
+ let vo = this
80
+
81
+ //pm
82
+ vo.pm = genPm()
83
+
84
+ //save
85
+ vo.content = content
86
+
87
+ //show
88
+ vo.bShow = true
89
+
90
+ return vo.pm
91
+ },
92
+
93
+ clickBtn: function(mode) {
94
+ //console.log('methods clickBtn', mode)
95
+
96
+ let vo = this
97
+
98
+ if (mode) {
99
+ vo.pm.resolve()
100
+ }
101
+ else {
102
+ vo.pm.reject('close')
103
+ }
104
+
105
+ //hide
106
+ vo.bShow = false
107
+
108
+ },
109
+
110
+ }
111
+ }
112
+ </script>
113
+
114
+ <style scoped>
115
+ </style>