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
@@ -5,8 +5,28 @@
5
5
  @domresize="resize"
6
6
  :changeApis="changeApis"
7
7
  >
8
+ <div style="display:flex; height:100%; width:100%;">
9
+
10
+ <!-- 左側垂直主選單(仿 w-web-sso):切換 API 工作區 / 統計資訊 -->
11
+ <div :style="`width:${widthMainMenu}px; flex-shrink:0; height:100%; box-sizing:border-box; background:var(--bg-2); border-right:1px solid var(--border); display:flex; flex-direction:column; padding:8px 0; gap:4px;`">
12
+ <button
13
+ v-for="m in mainMenus"
14
+ :key="m.id"
15
+ class="w-mm-btn"
16
+ :class="{on: section===m.id}"
17
+ @click="onClickMainMenu(m)"
18
+ >
19
+ <svg viewBox="0 0 24 24" width="22" height="22" style="display:block; margin:0 auto;"><path :d="m.icon" fill="currentColor"></path></svg>
20
+ <div class="w-mm-label">{{$t(m.textKey)}}</div>
21
+ </button>
22
+ </div>
23
+
24
+ <!-- 工作區 -->
25
+ <div style="flex:1; min-width:0; height:100%; position:relative;">
26
+
27
+ <template v-if="section==='api'">
8
28
 
9
- <template v-if="syncState">
29
+ <template v-if="syncState">
10
30
 
11
31
  <WDrawer
12
32
  :style="`height:${panelHeight}px; width:100%;`"
@@ -23,25 +43,44 @@
23
43
  >
24
44
 
25
45
  <template v-slot:drawer="props">
26
- <div :style="`height:${props.height}px; background:#fff; position:relative;`">
46
+ <div :style="`height:${props.height}px; background:var(--bg-2); border-right:1px solid var(--border); position:relative;`">
47
+
48
+ <!-- 搜尋 + 新增API -->
49
+ <div :style="`height:${heightDrawerHeader}px; box-sizing:border-box; padding:8px 40px 8px 8px; border-bottom:1px solid var(--border); display:flex; gap:6px; align-items:center;`">
50
+ <input
51
+ v-model="search"
52
+ type="text"
53
+ :placeholder="$t('searchApiPlaceholder')"
54
+ style="flex:1; min-width:0; padding:5px 8px; border:1px solid var(--border); border-radius:var(--radius); font-size:0.85rem; outline:none; box-sizing:border-box; transition:border-color .15s, box-shadow .15s;"
55
+ @focus="$event.target.style.borderColor='var(--accent)'; $event.target.style.boxShadow='var(--focus)'"
56
+ @blur="$event.target.style.borderColor='var(--border)'; $event.target.style.boxShadow='none'"
57
+ />
58
+ <button
59
+ @click="onClickAddApi"
60
+ class="w-btn-primary"
61
+ style="flex-shrink:0; white-space:nowrap;"
62
+ >+ {{$t('addApi')}}</button>
63
+ </div>
27
64
 
28
65
  <template v-if="apisTree.length>0">
29
66
  <WTree
30
- :style="`height:${props.height}px;`"
31
- :viewHeightMax="props.height"
67
+ :style="`height:${props.height-heightDrawerHeader}px;`"
68
+ :viewHeightMax="props.height-heightDrawerHeader"
32
69
  :data="apisTree"
33
70
  :iconHeight="lineHeightTree"
34
71
  :defItemHeight="lineHeightTree"
35
72
  :activable="true"
36
73
  :itemActive="apiActive"
37
74
  :funActive="funActive"
38
- :itemTextColorActive="'#000'"
39
- :itemBackgroundColorActive ="'rgba(100,100,100,0.15)'"
75
+ :itemTextColorActive="'var(--c-1)'"
76
+ :itemBackgroundColorActive ="'linear-gradient(to right, #0099ff 3px, rgba(0,153,255,0.08) 3px)'"
77
+ :indent="0.25"
78
+ :iconSize="12"
40
79
  >
41
80
  <template v-slot:item="props">
42
81
 
43
82
  <div
44
- :style="`display:flex; align-items:center; min-height:${lineHeightTree}px;`"
83
+ :style="`display:flex; align-items:center; min-height:${lineHeightTree}px; color:var(--c-3); font-size:11px; font-weight:600; letter-spacing:.05em; text-transform:uppercase;`"
45
84
  v-if="props.data.type!=='node'"
46
85
  >
47
86
  {{props.data.key}}
@@ -49,12 +88,12 @@
49
88
 
50
89
  <div
51
90
  :style="`display:flex; align-items:center; min-height:${lineHeightTree}px; cursor:pointer;`"
52
- @click="ckItem(getItemByKey(props.data.text))"
91
+ @click="ckItem(getItemByKey(props.data.text), props.data)"
53
92
  v-else
54
93
  >
55
94
 
56
95
  <div style="padding-right:5px;">
57
- <div :style="`padding:0px 6px; font-size:0.7rem; border-radius:10px; border:1px solid #ddd; color:#fff; background:${getMethodColorByKey(props.data.text)};`">
96
+ <div class="w-mb" :class="getMethodByKey(props.data.text).toLowerCase()">
58
97
  {{getMethodByKey(props.data.text)}}
59
98
  </div>
60
99
  </div>
@@ -81,7 +120,7 @@
81
120
  >
82
121
  <WButtonCircle
83
122
  :paddingStyle="{v:3,h:3}"
84
- :icon="'mdi-arrow-left'"
123
+ :icon="mdiArrowLeft"
85
124
  :iconSize="16"
86
125
  :backgroundColor="'#fff'"
87
126
  :backgroundColorHover="'#eee'"
@@ -101,283 +140,100 @@
101
140
  <template v-slot:content="props">
102
141
  <div :style="`height:${props.height}px; width:${props.width}px; position:relative;`">
103
142
 
104
- <!-- 選擇API之顯示區 -->
105
- <template>
143
+ <!-- 模式切換(文件/編輯/測試) + 內容 -->
144
+ <template v-if="apiSelect || newMode">
145
+
146
+ <div :style="`height:${heightModeBar}px; box-sizing:border-box; padding:8px 12px; border-bottom:1px solid var(--border); background:var(--bg-1); display:flex; align-items:center;`">
147
+ <div class="w-seg">
148
+ <button class="w-seg-btn" :class="{on: mode==='docs'}" @click="onChangeMode({id:'docs'})">{{$t('tabDocs')}}</button>
149
+ <button class="w-seg-btn" :class="{on: mode==='edit'}" @click="onChangeMode({id:'edit'})">{{$t('tabEdit')}}</button>
150
+ <button class="w-seg-btn" :class="{on: mode==='test'}" @click="onChangeMode({id:'test'})">{{$t('tabTest')}}</button>
151
+ </div>
152
+ </div>
106
153
 
107
154
  <div
108
- :style="`height:${props.height}px; overflow-y:auto;`"
109
- v-if="apiSelect"
155
+ :style="`height:${props.height-heightModeBar}px; overflow-y:auto; background:var(--bg-1);`"
156
+ v-if="mode==='docs' && apiSelect"
110
157
  >
111
- <div style="padding:20px;">
112
-
113
- <div style="padding:5px;">
114
-
115
- <div class="bk-title" style="">
116
- {{$t('apiUrl')}}
117
- </div>
118
-
119
- <div style="padding:10px; border-radius:5px; border:1px solid #ddd; background:#fff; display:flex; align-items:center;">
120
-
121
- <div style="padding-right:5px;">
122
- <div :style="`padding:0px 6px; font-size:0.7rem; border-radius:10px; border:1px solid #ddd; color:#fff; background:${getMethodColor(apiSelect)};`">
123
- {{getMethod(apiSelect)}}
124
- </div>
125
- </div>
126
-
127
- <div style="font-size:0.8rem;">
128
- {{$ui.gv(apiSelect,'url')}}
129
- </div>
130
-
131
- </div>
132
-
158
+ <div style="padding:30px 36px;">
159
+
160
+ <!-- op header -->
161
+ <div><span class="w-mb" :class="getMethod(apiSelect).toLowerCase()">{{getMethod(apiSelect)}}</span></div>
162
+ <div class="op-title">{{$ui.gv(apiSelect,'name')}}</div>
163
+ <div class="op-path"><span class="w-mb" :class="getMethod(apiSelect).toLowerCase()">{{getMethod(apiSelect)}}</span><span class="w-mono">{{$ui.gv(apiSelect,'url')}}</span></div>
164
+ <div class="op-desc">{{$ui.gv(apiSelect,'description')}}</div>
165
+
166
+ <!-- metadata pills -->
167
+ <div class="pills">
168
+ <span class="pill" v-if="$ui.gv(apiSelect,'version')"><b>{{$ui.gv(apiSelect,'version')}}</b></span>
169
+ <span class="pill" v-if="$ui.gv(apiSelect,'levels')">{{$t('levels')}} <b>{{$ui.gv(apiSelect,'levels')}}</b></span>
170
+ <span class="pill" v-if="keywordsList.length">{{$t('keywords')}}<span class="kw-chip" v-for="kw in keywordsList" :key="'kw-'+kw">{{kw}}</span></span>
171
+ <span class="pill ok" v-if="$ui.gv(apiSelect,'state')">{{$ui.gv(apiSelect,'state')}}</span>
172
+ <span class="pill" v-if="$ui.gv(apiSelect,'creator')">{{$t('creator')}} <b>{{$ui.gv(apiSelect,'creator')}}</b></span>
173
+ <span class="pill" v-if="$ui.gv(apiSelect,'dataSource')">{{$t('dataSource')}} <b>{{$ui.gv(apiSelect,'dataSource')}}</b></span>
133
174
  </div>
134
175
 
135
- <div style="padding:10px;">
136
-
137
- <div style="color:#485ed5;">
138
- {{$ui.gv(apiSelect,'name')}}
139
- </div>
140
-
141
- <div class="bk-title" style="">
142
- {{$ui.gv(apiSelect,'description')}}
143
- </div>
144
-
145
- </div>
146
-
147
- <div style="padding:10px;">
148
-
149
- <div class="bk" style="">
150
-
151
- <div class="bk-title" style="">
152
- {{$t('tokens')}}
153
- </div>
154
-
155
- <div class="bk-item" style="">
156
- {{$ui.gv(apiSelect,'tokens')}}
157
- </div>
158
-
159
- </div>
160
-
161
- <div class="bk" style="">
162
-
163
- <div class="bk-title" style="">
164
- {{$t('version')}}
165
- </div>
166
-
167
- <div class="bk-item" style="">
168
- {{$ui.gv(apiSelect,'version')}}
169
- </div>
170
-
171
- </div>
172
-
173
- <div class="bk" style="">
174
-
175
- <div class="bk-title" style="">
176
- {{$t('levels')}}
177
- </div>
178
-
179
- <div class="bk-item" style="">
180
- {{$ui.gv(apiSelect,'')}}
181
- </div>
182
-
183
- </div>
184
-
185
- <div class="bk" style="">
186
-
187
- <div class="bk-title" style="">
188
- {{$t('keywords')}}
189
- </div>
190
-
191
- <div class="bk-item" style="">
192
- {{$ui.gv(apiSelect,'')}}
193
- </div>
194
-
195
- </div>
196
-
197
- <div class="bk" style="">
198
-
199
- <div class="bk-title" style="">
200
- {{$t('state')}}
201
- </div>
202
-
203
- <div class="bk-item" style="">
204
- {{$ui.gv(apiSelect,'')}}
205
- </div>
206
-
207
- </div>
208
-
209
- <div class="bk" style="">
210
-
211
- <div class="bk-title" style="">
212
- {{$t('timeCreate')}}
213
- </div>
214
-
215
- <div class="bk-item" style="">
216
- {{$ui.gv(apiSelect,'timeCreate',getDay)}}
217
- </div>
218
-
219
- </div>
220
-
221
- <div class="bk" style="">
222
-
223
- <div class="bk-title" style="">
224
- {{$t('timeUpdate')}}
225
- </div>
226
-
227
- <div class="bk-item" style="">
228
- {{$ui.gv(apiSelect,'timeUpdate',getDay)}}
229
- </div>
230
-
231
- </div>
232
-
233
- <div class="bk" style="">
234
-
235
- <div class="bk-title" style="">
236
- {{$t('creator')}}
237
- </div>
238
-
239
- <div class="bk-item" style="">
240
- {{$ui.gv(apiSelect,'creator')}}
241
- </div>
242
-
243
- </div>
244
-
245
- <div class="bk" style="">
246
-
247
- <div class="bk-title" style="">
248
- {{$t('dataSource')}}
249
- </div>
250
-
251
- <div class="bk-item" style="">
252
- {{$ui.gv(apiSelect,'dataSource')}}
253
- </div>
254
-
255
- </div>
256
-
257
- <div class="bk" style="">
258
-
259
- <div class="bk-title" style="">
260
- {{$t('isActive')}}
176
+ <!-- two-column split -->
177
+ <div class="split">
178
+ <!-- 左欄:參數說明 -->
179
+ <div>
180
+ <div class="dsec">
181
+ <div class="dsec-h">{{$t('docInput')}}</div>
182
+ <div class="doc-md"><MdPanel :md="$ui.gv(apiSelect,'mdInputParams')"></MdPanel></div>
261
183
  </div>
262
-
263
- <div class="bk-item" style="">
264
- {{$ui.gv(apiSelect,'isActive')}}
184
+ <div class="dsec">
185
+ <div class="dsec-h">{{$t('docOutputFields')}}</div>
186
+ <div class="doc-md"><MdPanel :md="$ui.gv(apiSelect,'mdOutputParams')"></MdPanel></div>
265
187
  </div>
266
-
267
- </div>
268
-
269
- </div>
270
-
271
- <div style="padding:10px;">
272
-
273
- <div class="bk-title" style="">
274
- {{$t('mdInputParams')}}
275
- </div>
276
-
277
- <div class="bk-item" style="padding:10px;">
278
- <MdPanel
279
- :md="$ui.gv(apiSelect,'mdInputParams')"
280
- ></MdPanel>
281
- </div>
282
-
283
- </div>
284
-
285
- <div style="padding:10px;">
286
-
287
- <div class="bk-title" style="">
288
- {{$t('inputExample')}}
289
- </div>
290
-
291
- <div class="bk-item" style="padding:10px;">
292
- {{$ui.gv(apiSelect,'inputExample')}}
293
- </div>
294
-
295
- </div>
296
-
297
- <div style="padding:10px;">
298
-
299
- <div class="bk-title" style="">
300
- {{$t('mdOutputParams')}}
301
- </div>
302
-
303
- <div class="bk-item" style="padding:10px;">
304
- <MdPanel
305
- :md="$ui.gv(apiSelect,'mdOutputParams')"
306
- ></MdPanel>
307
188
  </div>
308
-
309
- </div>
310
-
311
- <div style="padding:10px;">
312
-
313
- <div class="bk-title" style="">
314
- {{$t('outputExample')}}
315
- </div>
316
-
317
- <div style="padding-top:3px;">
318
-
319
- <div style="padding-left:10px; margin-bottom:-1px;">
320
- <!-- 因上下皆有border, 故需負margin-bottom來吃掉重複border -->
321
- <WGroupChipRadio
322
- :items="optputMenuItems"
323
- :value="optputMenuItemSelect"
324
- @input="changeOptputMenuItemSelect"
325
- :group="true"
326
- :group-border-radius-style="{top:true}"
327
- :group-shift="7"
328
- :border-radius="20"
329
- :border-color="'#ddd'"
330
- :border-color-hover="'#dadada'"
331
- :border-color-active="'orange lighten-2'"
332
- :margin-style="{}"
333
- ></WGroupChipRadio>
189
+ <!-- 右欄:code rail -->
190
+ <div class="rail">
191
+ <div class="card">
192
+ <div class="card-h"><span class="lab">{{$t('docRequest')}}</span><span class="w-muted" style="margin-left:auto; font-size:11.5px;">cURL</span></div>
193
+ <pre class="code">{{curlOf(apiSelect)}}</pre>
334
194
  </div>
335
-
336
- <div style="border:1px solid #ddd; background:#fff">
337
-
338
- <div
339
- style="padding:10px 15px;"
340
- v-if="optputMenuItemSelectId==='tree'"
341
- >
342
- <WJsonView
343
- style="width:100%;"
344
- :viewHeightMax="null"
345
- :data="getOutputJsonObj(apiSelect)"
346
- ></WJsonView>
347
- </div>
348
-
349
- <div
350
- style="padding:15px; color:#555; font-size:0.85rem;"
351
- v-if="optputMenuItemSelectId==='raw'
352
- ">
353
- <pre style="margin:0px; padding:0px;">{{getOutputJson(apiSelect)}}</pre>
354
- </div>
355
-
195
+ <div class="card">
196
+ <div class="card-h"><span class="lab">{{$t('resTitle')}}</span><span class="st">200 OK</span></div>
197
+ <pre class="code" v-html="jsonHighlight($ui.gv(apiSelect,'outputExample'))"></pre>
356
198
  </div>
357
-
358
199
  </div>
359
-
360
200
  </div>
361
201
 
362
202
  </div>
363
203
  </div>
364
204
 
365
- <div
366
- style="padding:20px; font-size:0.9rem;"
367
- v-else
368
- >
369
- {{$t('noSelectApi')}}
370
- </div>
205
+ <LayoutContentEdit
206
+ v-else-if="mode==='edit'"
207
+ :item="editItem"
208
+ :height="props.height-heightModeBar"
209
+ :isNew="newMode"
210
+ @saved="onEditSaved"
211
+ @deleted="onEditDeleted"
212
+ @cancel="onEditCancel"
213
+ ></LayoutContentEdit>
214
+
215
+ <LayoutContentTest
216
+ v-else-if="mode==='test' && apiSelect"
217
+ :item="apiSelect"
218
+ :height="props.height-heightModeBar"
219
+ ></LayoutContentTest>
371
220
 
372
221
  </template>
373
222
 
223
+ <div
224
+ style="padding:20px; font-size:0.9rem;"
225
+ v-else
226
+ >
227
+ {{$t('noSelectApi')}}
228
+ </div>
229
+
374
230
  <div
375
231
  :style="`position:absolute; top:1px; left:4px;`"
376
232
  v-if="!drawer"
377
233
  >
378
234
  <WButtonCircle
379
235
  :paddingStyle="{v:3,h:3}"
380
- :icon="'mdi-arrow-right'"
236
+ :icon="mdiArrowRight"
381
237
  :iconSize="16"
382
238
  :backgroundColor="'#fff'"
383
239
  :backgroundColorHover="'#eee'"
@@ -406,6 +262,16 @@
406
262
  </div>
407
263
  </template>
408
264
 
265
+ </template>
266
+
267
+ <LayoutContentStats
268
+ v-else-if="section==='stats'"
269
+ :height="panelHeight"
270
+ ></LayoutContentStats>
271
+
272
+ </div>
273
+
274
+ </div>
409
275
  </div>
410
276
  </template>
411
277
 
@@ -416,28 +282,30 @@ import each from 'lodash-es/each.js'
416
282
  import size from 'lodash-es/size.js'
417
283
  import trim from 'lodash-es/trim.js'
418
284
  import find from 'lodash-es/find.js'
285
+ import filter from 'lodash-es/filter.js'
419
286
  import cloneDeep from 'lodash-es/cloneDeep.js'
420
287
  import isobj from 'wsemi/src/isobj.mjs'
421
288
  import iseobj from 'wsemi/src/iseobj.mjs'
422
- import j2o from 'wsemi/src/j2o.mjs'
423
- import timemsTZ2day from 'wsemi/src/timemsTZ2day.mjs'
424
289
  import convertToTree from 'wsemi/src/convertToTree.mjs'
290
+ import { mdiArrowLeft, mdiArrowRight, mdiCloudBraces, mdiChartBar } from '@mdi/js'
425
291
  import WButtonCircle from 'w-component-vue/src/components/WButtonCircle.vue'
426
- import WGroupChipRadio from 'w-component-vue/src/components/WGroupChipRadio.vue'
427
292
  import WDrawer from 'w-component-vue/src/components/WDrawer.vue'
428
293
  import WTree from 'w-component-vue/src/components/WTree.vue'
429
- import WJsonView from 'w-component-vue/src/components/WJsonView.vue'
430
294
  import MdPanel from './MdPanel.vue'
295
+ import LayoutContentEdit from './LayoutContentEdit.vue'
296
+ import LayoutContentTest from './LayoutContentTest.vue'
297
+ import LayoutContentStats from './LayoutContentStats.vue'
431
298
 
432
299
 
433
300
  export default {
434
301
  components: {
435
302
  WButtonCircle,
436
- WGroupChipRadio,
437
303
  WDrawer,
438
304
  WTree,
439
- WJsonView,
440
305
  MdPanel,
306
+ LayoutContentEdit,
307
+ LayoutContentTest,
308
+ LayoutContentStats,
441
309
  },
442
310
  props: {
443
311
  },
@@ -447,6 +315,14 @@ export default {
447
315
  panelWidth: 0,
448
316
  panelHeight: 0,
449
317
 
318
+ //左側垂直主選單
319
+ section: 'api', //'api' | 'stats'
320
+ widthMainMenu: 76,
321
+ mainMenus: [
322
+ { id: 'api', textKey: 'mmApi', icon: mdiCloudBraces },
323
+ { id: 'stats', textKey: 'mmStaInfor', icon: mdiChartBar },
324
+ ],
325
+
450
326
  drawer: true,
451
327
  drawerWidth: 320,
452
328
  drawerWidthMin: 200,
@@ -457,16 +333,17 @@ export default {
457
333
  apisTree: [],
458
334
  kpApisTree: {},
459
335
  apiActive: null,
336
+ mdiArrowLeft,
337
+ mdiArrowRight,
460
338
  apiSelect: null,
461
339
 
462
- kpColorMethod: {
463
- GET: '#7c2',
464
- POST: '#68f',
465
- PUT: '#f82',
466
- DEL: '#f26',
467
- },
468
-
469
- optputMenuItemSelectId: 'tree',
340
+ //API 管理:模式切換 / 新增 / 搜尋
341
+ mode: 'docs', //'docs' | 'edit' | 'test'
342
+ newMode: false,
343
+ blankApi: {},
344
+ search: '',
345
+ heightModeBar: 46,
346
+ heightDrawerHeader: 48,
470
347
 
471
348
  }
472
349
  },
@@ -484,32 +361,25 @@ export default {
484
361
  let vo = this
485
362
  //trigger
486
363
  let apis = vo.apis
364
+ let search = vo.search //讓搜尋變更也觸發tree重建
365
+ void search
487
366
  vo.genTree(apis)
488
367
  return ''
489
368
  },
490
369
 
491
- optputMenuItems: function() {
370
+ editItem: function() {
492
371
  let vo = this
493
- let ms = [
494
- {
495
- id: 'tree',
496
- text: vo.$t('outputMenuTree'),
497
- icon: 'mdi-vanity-light',
498
- },
499
- {
500
- id: 'raw',
501
- text: vo.$t('outputMenuRaw'),
502
- icon: 'mdi-lan',
503
- },
504
- ]
505
- return ms
372
+ return vo.newMode ? vo.blankApi : vo.apiSelect
506
373
  },
507
374
 
508
- optputMenuItemSelect: function() {
375
+ //關鍵字以分號切開成陣列(trim + 去空),供 pills 區逐一以 chip 呈現
376
+ keywordsList: function() {
509
377
  let vo = this
510
- let r = find(vo.optputMenuItems, { id: vo.optputMenuItemSelectId })
511
- // console.log(r)
512
- return r
378
+ let s = get(vo.apiSelect, 'keywords', '')
379
+ if (!s) {
380
+ return []
381
+ }
382
+ return String(s).split(';').map((x) => x.trim()).filter((x) => x !== '')
513
383
  },
514
384
 
515
385
  },
@@ -530,14 +400,17 @@ export default {
530
400
  genTree: function () {
531
401
  let vo = this
532
402
 
403
+ //保留目前選取的API id, 重建後盡量維持選取(避免儲存/同步後選取跳回第一筆)
404
+ let prevId = get(vo.apiSelect, 'id', '')
405
+
533
406
  //default
534
407
  vo.apiActive = null
535
- vo.apiSelect = null
536
408
  vo.apisTree = []
537
409
  vo.kpApisTree = {}
538
410
 
539
411
  //check
540
412
  if (size(vo.apis) === 0) {
413
+ vo.apiSelect = null
541
414
  return
542
415
  }
543
416
 
@@ -545,6 +418,15 @@ export default {
545
418
  let apis = cloneDeep(vo.apis)
546
419
  // console.log('apis', cloneDeep(apis))
547
420
 
421
+ //搜尋過濾(name/keywords/url/levels/method)
422
+ let sterm = trim(vo.search).toLowerCase()
423
+ if (sterm !== '') {
424
+ apis = filter(apis, (v) => {
425
+ let hay = `${get(v, 'name', '')} ${get(v, 'keywords', '')} ${get(v, 'url', '')} ${get(v, 'levels', '')} ${get(v, 'method', '')}`.toLowerCase()
426
+ return hay.indexOf(sterm) >= 0
427
+ })
428
+ }
429
+
548
430
  //tr
549
431
  let tr = {}
550
432
  let kpTree = {}
@@ -570,8 +452,14 @@ export default {
570
452
  let tree = convertToTree(tr, { bindRoot: '全部' })
571
453
  // console.log('tree', cloneDeep(tree))
572
454
 
573
- //apiSelect, 預先選定api項目, 非tree選單active物件, 因id由convertToTree轉換提供, 故不能直接算得active選單物件id
574
- let apiSelect = get(apis, 0, null)
455
+ //apiSelect: 優先保留原選取(仍存在時), 否則取第一筆
456
+ let apiSelect = null
457
+ if (prevId !== '') {
458
+ apiSelect = find(apis, { id: prevId }) || null
459
+ }
460
+ if (!iseobj(apiSelect)) {
461
+ apiSelect = get(apis, 0, null)
462
+ }
575
463
  // console.log('apiSelect', cloneDeep(apiSelect))
576
464
 
577
465
  //save
@@ -596,6 +484,16 @@ export default {
596
484
  return method
597
485
  },
598
486
 
487
+ curlOf: function(item) {
488
+ let vo = this
489
+ let method = vo.getMethod(item) || 'GET'
490
+ let url = get(item, 'inputExample', '')
491
+ if (!url) {
492
+ url = get(item, 'url', '')
493
+ }
494
+ return 'curl -X ' + method + ' "' + url + '"'
495
+ },
496
+
599
497
  getMethodByKey: function(key) {
600
498
  let vo = this
601
499
  let item = vo.getItemByKey(key)
@@ -603,62 +501,102 @@ export default {
603
501
  return method
604
502
  },
605
503
 
606
- getMethodColor: function(item) {
607
- let vo = this
608
- let method = vo.getMethod(item)
609
- let color = get(vo.kpColorMethod, method, '#888')
610
- return color
504
+ jsonHighlight: function(str) {
505
+ let s = (str === null || str === undefined) ? '' : String(str)
506
+ // escape HTML
507
+ s = s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
508
+ // key: "xxx":
509
+ s = s.replace(/("(?:[^"\\]|\\.)*")(\s*:)/g, '<span class="k">$1</span>$2')
510
+ // string value
511
+ s = s.replace(/:(\s*)("(?:[^"\\]|\\.)*")/g, ':$1<span class="s">$2</span>')
512
+ // number
513
+ s = s.replace(/([:\[,\s])(-?\d+\.?\d*)/g, '$1<span class="n">$2</span>')
514
+ // bool/null
515
+ s = s.replace(/\b(true|false|null)\b/g, '<span class="b">$1</span>')
516
+ return s
611
517
  },
612
518
 
613
- getMethodColorByKey: function(key) {
519
+ funActive: function(msg) {
614
520
  let vo = this
615
- let method = vo.getMethodByKey(key)
616
- let color = get(vo.kpColorMethod, method, '#888')
617
- return color
521
+ let b = !Array.isArray(msg.item.children) //children非陣列代表沒有所屬節點(葉節點)
522
+ if (b) {
523
+ //渲染時用穩定的 api id(=節點 text) 對應目前選取項, 把 apiActive 刷新成該葉節點「當前」的 id;
524
+ //避免存檔後 tree 重建、convertToTree 重新產生節點 id, 使 apiActive 變舊而高亮(含左側 accent bar)失效
525
+ let itemText = get(msg, 'item.text', '')
526
+ let itemId = get(msg, 'item.id', '')
527
+ if (iseobj(vo.apiSelect) && itemText === vo.apiSelect.id) {
528
+ if (get(vo.apiActive, 'id') !== itemId) {
529
+ vo.apiActive = { id: itemId }
530
+ }
531
+ }
532
+ else if (!iseobj(vo.apiActive)) { //無選取時初始化第一個葉節點為 active
533
+ vo.apiActive = { id: itemId }
534
+ }
535
+ }
536
+ return b
618
537
  },
619
538
 
620
- getDay: function(t) {
621
- return timemsTZ2day(t)
539
+ ckItem: function(item, node) {
540
+ // console.log('ckItem', item)
541
+ let vo = this
542
+ vo.apiSelect = cloneDeep(item)
543
+ if (iseobj(node)) { //點擊即更新 active 節點 id, 使高亮(含左側 bar)立即移到所點項目
544
+ vo.apiActive = { id: node.id }
545
+ }
546
+ vo.newMode = false //點選既有API即離開新增狀態
622
547
  },
623
548
 
624
- getOutputJson: function(item) {
625
- // let vo = this
626
- let j = get(item, 'outputExample', '')
627
- j = trim(j)
628
- return j
549
+ onChangeMode: function(msg) {
550
+ let vo = this
551
+ vo.mode = msg.id
552
+ //切到非編輯模式時取消新增狀態
553
+ if (vo.newMode && msg.id !== 'edit') {
554
+ vo.newMode = false
555
+ }
629
556
  },
630
557
 
631
- getOutputJsonObj: function(item) {
632
- // let vo = this
633
- let j = this.getOutputJson(item)
634
- // console.log(j)
635
- let o = j2o(j)
636
- return o
558
+ onClickMainMenu: function(m) {
559
+ let vo = this
560
+ vo.section = m.id
637
561
  },
638
562
 
639
- funActive: function(msg) {
563
+ onClickAddApi: function() {
640
564
  let vo = this
641
- let b = !Array.isArray(msg.item.children) //children非陣列代表沒有所屬節點
642
- if (b) {
643
- if (!iseobj(vo.apiActive)) { //偵測若沒有apiActive才初始化預先選擇之api項目, 非api完整資訊
644
- let id = msg.item.id
645
- vo.apiActive = { id }
646
- // console.log('!iseobj(vo.apiActive)', vo.apiActive)
647
- }
565
+ //種子空白API(預設值, 送出時後端funNew會配id/時間)
566
+ vo.blankApi = {
567
+ name: '',
568
+ description: '',
569
+ url: '',
570
+ method: 'get',
571
+ version: 'v1',
572
+ group: '',
573
+ levels: '',
574
+ keywords: '',
575
+ state: 'ok',
576
+ authType: 'none',
577
+ contentType: 'application/json',
648
578
  }
649
- return b
579
+ vo.newMode = true
580
+ vo.mode = 'edit'
650
581
  },
651
582
 
652
- changeOptputMenuItemSelect: function(msg) {
653
- // console.log('changeOptputMenuItemSelect', msg)
583
+ onEditSaved: function() {
654
584
  let vo = this
655
- vo.optputMenuItemSelectId = msg.id
585
+ vo.newMode = false
586
+ vo.mode = 'docs'
656
587
  },
657
588
 
658
- ckItem: function(item) {
659
- // console.log('ckItem', item)
589
+ onEditDeleted: function() {
660
590
  let vo = this
661
- vo.apiSelect = cloneDeep(item)
591
+ vo.newMode = false
592
+ vo.apiSelect = null
593
+ vo.mode = 'docs'
594
+ },
595
+
596
+ onEditCancel: function() {
597
+ let vo = this
598
+ vo.newMode = false
599
+ vo.mode = 'docs'
662
600
  },
663
601
 
664
602
  }
@@ -666,23 +604,34 @@ export default {
666
604
  </script>
667
605
 
668
606
  <style scoped>
669
- .bk {
670
- display:inline-block;
671
- width:200px;
672
- padding:0px 10px 10px 0px;
673
- vertical-align:top;
607
+ .w-mm-btn {
608
+ width: 100%;
609
+ box-sizing: border-box;
610
+ padding: 10px 4px;
611
+ border: none;
612
+ border-left: 3px solid transparent;
613
+ background: transparent;
614
+ color: var(--c-2);
615
+ cursor: pointer;
616
+ display: flex;
617
+ flex-direction: column;
618
+ align-items: center;
619
+ gap: 4px;
620
+ transition: background .15s, color .15s, border-color .15s;
621
+ }
622
+ .w-mm-btn:hover {
623
+ background: rgba(0, 153, 255, 0.06);
624
+ color: var(--c-1);
674
625
  }
675
- .bk-title {
676
- padding:3px;
677
- font-size:0.8rem;
678
- color:#888;
626
+ .w-mm-btn.on {
627
+ color: var(--accent);
628
+ border-left-color: var(--accent);
629
+ background: rgba(0, 153, 255, 0.08);
679
630
  }
680
- .bk-item {
681
- padding:3px 5px;
682
- font-size:0.9rem;
683
- line-height:0.9rem;
684
- border-radius:5px;
685
- border:1px solid #ddd;
686
- background:#fff;
631
+ .w-mm-label {
632
+ font-size: 11px;
633
+ line-height: 1.2;
634
+ text-align: center;
635
+ word-break: break-word;
687
636
  }
688
637
  </style>