w-web-api 1.0.74 → 1.0.76

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.
@@ -1,637 +1,637 @@
1
- <template>
2
- <div
3
- style="height:100%; width:100%;"
4
- v-domresize
5
- @domresize="resize"
6
- :changeApis="changeApis"
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'">
28
-
29
- <template v-if="syncState">
30
-
31
- <WDrawer
32
- :style="`height:${panelHeight}px; width:100%;`"
33
- v-model="drawer"
34
- :drawerWidth.sync="drawerWidth"
35
- :drawerWidthMin="drawerWidthMin"
36
- :drawerWidthMax="drawerWidthMax"
37
- :mode="'from-left'"
38
- :dragDrawerWidth="true"
39
- :autoSwitchToHide="true"
40
- :autoSwitchToShow="true"
41
- :autoSwitchToFloat="true"
42
- :switchWidth="drawerWidth*2.3"
43
- >
44
-
45
- <template v-slot:drawer="props">
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>
64
-
65
- <template v-if="apisTree.length>0">
66
- <WTree
67
- :style="`height:${props.height-heightDrawerHeader}px;`"
68
- :viewHeightMax="props.height-heightDrawerHeader"
69
- :data="apisTree"
70
- :iconHeight="lineHeightTree"
71
- :defItemHeight="lineHeightTree"
72
- :activable="true"
73
- :itemActive="apiActive"
74
- :funActive="funActive"
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"
79
- >
80
- <template v-slot:item="props">
81
-
82
- <div
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;`"
84
- v-if="props.data.type!=='node'"
85
- >
86
- {{props.data.key}}
87
- </div>
88
-
89
- <div
90
- :style="`display:flex; align-items:center; min-height:${lineHeightTree}px; cursor:pointer;`"
91
- @click="ckItem(getItemByKey(props.data.text), props.data)"
92
- v-else
93
- >
94
-
95
- <div style="padding-right:5px;">
96
- <div class="w-mb" :class="getMethodByKey(props.data.text).toLowerCase()">
97
- {{getMethodByKey(props.data.text)}}
98
- </div>
99
- </div>
100
-
101
- <div>
102
- {{$ui.gv(getItemByKey(props.data.text),'name')}}
103
- </div>
104
-
105
- </div>
106
-
107
- </template>
108
- </WTree>
109
- </template>
110
- <div
111
- style="padding:20px; font-size:0.9rem;"
112
- v-else
113
- >
114
- {{$t('waitingData')}}
115
- </div>
116
-
117
- <div
118
- :style="`position:absolute; top:1px; right:12px;`"
119
- v-if="drawer"
120
- >
121
- <WButtonCircle
122
- :paddingStyle="{v:3,h:3}"
123
- :icon="mdiArrowLeft"
124
- :iconSize="16"
125
- :backgroundColor="'#fff'"
126
- :backgroundColorHover="'#eee'"
127
- :backgroundColorFocus="'#fff'"
128
- :borderColor="'transparent'"
129
- :borderColorHover="'#eee'"
130
- :borderColorFocus="'#eee'"
131
- :tooltip="$t('menuTreeHide')"
132
- :shadow="true"
133
- @click="drawer=false"
134
- ></WButtonCircle>
135
- </div>
136
-
137
- </div>
138
- </template>
139
-
140
- <template v-slot:content="props">
141
- <div :style="`height:${props.height}px; width:${props.width}px; position:relative;`">
142
-
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>
153
-
154
- <div
155
- :style="`height:${props.height-heightModeBar}px; overflow-y:auto; background:var(--bg-1);`"
156
- v-if="mode==='docs' && apiSelect"
157
- >
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>
174
- </div>
175
-
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>
183
- </div>
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>
187
- </div>
188
- </div>
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>
194
- </div>
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>
198
- </div>
199
- </div>
200
- </div>
201
-
202
- </div>
203
- </div>
204
-
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>
220
-
221
- </template>
222
-
223
- <div
224
- style="padding:20px; font-size:0.9rem;"
225
- v-else
226
- >
227
- {{$t('noSelectApi')}}
228
- </div>
229
-
230
- <div
231
- :style="`position:absolute; top:1px; left:4px;`"
232
- v-if="!drawer"
233
- >
234
- <WButtonCircle
235
- :paddingStyle="{v:3,h:3}"
236
- :icon="mdiArrowRight"
237
- :iconSize="16"
238
- :backgroundColor="'#fff'"
239
- :backgroundColorHover="'#eee'"
240
- :backgroundColorFocus="'#fff'"
241
- :borderColor="'transparent'"
242
- :borderColorHover="'#eee'"
243
- :borderColorFocus="'#eee'"
244
- :tooltip="$t('menuTreeShow')"
245
- :shadow="true"
246
- @click="drawer=true"
247
- ></WButtonCircle>
248
- </div>
249
-
250
- </div>
251
- </template>
252
-
253
- </WDrawer>
254
-
255
- </template>
256
-
257
- <template v-else>
258
- <div
259
- style="padding:20px; font-size:0.9rem;"
260
- >
261
- {{$t('waitingData')}}
262
- </div>
263
- </template>
264
-
265
- </template>
266
-
267
- <LayoutContentStats
268
- v-else-if="section==='stats'"
269
- :height="panelHeight"
270
- ></LayoutContentStats>
271
-
272
- </div>
273
-
274
- </div>
275
- </div>
276
- </template>
277
-
278
- <script>
279
- import get from 'lodash-es/get.js'
280
- import set from 'lodash-es/set.js'
281
- import each from 'lodash-es/each.js'
282
- import size from 'lodash-es/size.js'
283
- import trim from 'lodash-es/trim.js'
284
- import find from 'lodash-es/find.js'
285
- import filter from 'lodash-es/filter.js'
286
- import cloneDeep from 'lodash-es/cloneDeep.js'
287
- import isobj from 'wsemi/src/isobj.mjs'
288
- import iseobj from 'wsemi/src/iseobj.mjs'
289
- import convertToTree from 'wsemi/src/convertToTree.mjs'
290
- import { mdiArrowLeft, mdiArrowRight, mdiCloudBraces, mdiChartBar } from '@mdi/js'
291
- import WButtonCircle from 'w-component-vue/src/components/WButtonCircle.vue'
292
- import WDrawer from 'w-component-vue/src/components/WDrawer.vue'
293
- import WTree from 'w-component-vue/src/components/WTree.vue'
294
- import MdPanel from './MdPanel.vue'
295
- import LayoutContentEdit from './LayoutContentEdit.vue'
296
- import LayoutContentTest from './LayoutContentTest.vue'
297
- import LayoutContentStats from './LayoutContentStats.vue'
298
-
299
-
300
- export default {
301
- components: {
302
- WButtonCircle,
303
- WDrawer,
304
- WTree,
305
- MdPanel,
306
- LayoutContentEdit,
307
- LayoutContentTest,
308
- LayoutContentStats,
309
- },
310
- props: {
311
- },
312
- data: function() {
313
- return {
314
-
315
- panelWidth: 0,
316
- panelHeight: 0,
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
-
326
- drawer: true,
327
- drawerWidth: 320,
328
- drawerWidthMin: 200,
329
- drawerWidthMax: 700,
330
-
331
- lineHeightTree: 38,
332
-
333
- apisTree: [],
334
- kpApisTree: {},
335
- apiActive: null,
336
- mdiArrowLeft,
337
- mdiArrowRight,
338
- apiSelect: null,
339
-
340
- //API 管理:模式切換 / 新增 / 搜尋
341
- mode: 'docs', //'docs' | 'edit' | 'test'
342
- newMode: false,
343
- blankApi: {},
344
- search: '',
345
- heightModeBar: 46,
346
- heightDrawerHeader: 48,
347
-
348
- }
349
- },
350
- computed: {
351
-
352
- syncState: function() {
353
- return get(this, '$store.state.syncState')
354
- },
355
-
356
- apis: function() {
357
- return get(this, '$store.state.apis')
358
- },
359
-
360
- changeApis: function() {
361
- let vo = this
362
- //trigger
363
- let apis = vo.apis
364
- let search = vo.search //讓搜尋變更也觸發tree重建
365
- void search
366
- vo.genTree(apis)
367
- return ''
368
- },
369
-
370
- editItem: function() {
371
- let vo = this
372
- return vo.newMode ? vo.blankApi : vo.apiSelect
373
- },
374
-
375
- //關鍵字以分號切開成陣列(trim + 去空),供 pills 區逐一以 chip 呈現
376
- keywordsList: function() {
377
- let vo = this
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 !== '')
383
- },
384
-
385
- },
386
- methods: {
387
-
388
- resize: function(msg) {
389
- // console.log('methods resize', msg)
390
-
391
- let vo = this
392
-
393
- //save
394
- vo.panelWidth = msg.snew.clientWidth
395
- vo.panelHeight = msg.snew.clientHeight
396
- // console.log('vo.panelHeight', vo.panelHeight)
397
-
398
- },
399
-
400
- genTree: function () {
401
- let vo = this
402
-
403
- //保留目前選取的API id, 重建後盡量維持選取(避免儲存/同步後選取跳回第一筆)
404
- let prevId = get(vo.apiSelect, 'id', '')
405
-
406
- //default
407
- vo.apiActive = null
408
- vo.apisTree = []
409
- vo.kpApisTree = {}
410
-
411
- //check
412
- if (size(vo.apis) === 0) {
413
- vo.apiSelect = null
414
- return
415
- }
416
-
417
- //cloneDeep
418
- let apis = cloneDeep(vo.apis)
419
- // console.log('apis', cloneDeep(apis))
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
-
430
- //tr
431
- let tr = {}
432
- let kpTree = {}
433
- each(apis, (v) => {
434
-
435
- //id, levels
436
- let id = v.id
437
- let levels = `${v.levels}.${v.name}`
438
-
439
- //save
440
- kpTree[id] = v
441
-
442
- //check
443
- if (!isobj(get(tr, levels))) {
444
- set(tr, levels, id)
445
- }
446
-
447
- })
448
- // console.log('tr', cloneDeep(tr))
449
- // console.log('kpTree', cloneDeep(kpTree))
450
-
451
- //convertToTree, 由預處理tree物件轉成tree物件
452
- let tree = convertToTree(tr, { bindRoot: '全部' })
453
- // console.log('tree', cloneDeep(tree))
454
-
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
- }
463
- // console.log('apiSelect', cloneDeep(apiSelect))
464
-
465
- //save
466
- vo.apiSelect = apiSelect
467
- vo.apisTree = tree
468
- vo.kpApisTree = kpTree
469
-
470
- },
471
-
472
- getItemByKey: function(key) {
473
- let vo = this
474
- // let k = `kpApisTree.${key}`
475
- // console.log('getItemByKey k', k)
476
- let r = vo.kpApisTree[key]
477
- // console.log('getItemByKey r', r, vo.kpApisTree)
478
- return r
479
- },
480
-
481
- getMethod: function(item) {
482
- // let vo = this
483
- let method = get(item, 'method', '').toUpperCase()
484
- return method
485
- },
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
-
497
- getMethodByKey: function(key) {
498
- let vo = this
499
- let item = vo.getItemByKey(key)
500
- let method = get(item, 'method', '').toUpperCase()
501
- return method
502
- },
503
-
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
517
- },
518
-
519
- funActive: function(msg) {
520
- let vo = this
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
537
- },
538
-
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即離開新增狀態
547
- },
548
-
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
- }
556
- },
557
-
558
- onClickMainMenu: function(m) {
559
- let vo = this
560
- vo.section = m.id
561
- },
562
-
563
- onClickAddApi: function() {
564
- let vo = this
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',
578
- }
579
- vo.newMode = true
580
- vo.mode = 'edit'
581
- },
582
-
583
- onEditSaved: function() {
584
- let vo = this
585
- vo.newMode = false
586
- vo.mode = 'docs'
587
- },
588
-
589
- onEditDeleted: function() {
590
- let vo = this
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'
600
- },
601
-
602
- }
603
- }
604
- </script>
605
-
606
- <style scoped>
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);
625
- }
626
- .w-mm-btn.on {
627
- color: var(--accent);
628
- border-left-color: var(--accent);
629
- background: rgba(0, 153, 255, 0.08);
630
- }
631
- .w-mm-label {
632
- font-size: 11px;
633
- line-height: 1.2;
634
- text-align: center;
635
- word-break: break-word;
636
- }
637
- </style>
1
+ <template>
2
+ <div
3
+ style="height:100%; width:100%;"
4
+ v-domresize
5
+ @domresize="resize"
6
+ :changeApis="changeApis"
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'">
28
+
29
+ <template v-if="syncState">
30
+
31
+ <WDrawer
32
+ :style="`height:${panelHeight}px; width:100%;`"
33
+ v-model="drawer"
34
+ :drawerWidth.sync="drawerWidth"
35
+ :drawerWidthMin="drawerWidthMin"
36
+ :drawerWidthMax="drawerWidthMax"
37
+ :mode="'from-left'"
38
+ :dragDrawerWidth="true"
39
+ :autoSwitchToHide="true"
40
+ :autoSwitchToShow="true"
41
+ :autoSwitchToFloat="true"
42
+ :switchWidth="drawerWidth*2.3"
43
+ >
44
+
45
+ <template v-slot:drawer="props">
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>
64
+
65
+ <template v-if="apisTree.length>0">
66
+ <WTree
67
+ :style="`height:${props.height-heightDrawerHeader}px;`"
68
+ :viewHeightMax="props.height-heightDrawerHeader"
69
+ :data="apisTree"
70
+ :iconHeight="lineHeightTree"
71
+ :defItemHeight="lineHeightTree"
72
+ :activable="true"
73
+ :itemActive="apiActive"
74
+ :funActive="funActive"
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"
79
+ >
80
+ <template v-slot:item="props">
81
+
82
+ <div
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;`"
84
+ v-if="props.data.type!=='node'"
85
+ >
86
+ {{props.data.key}}
87
+ </div>
88
+
89
+ <div
90
+ :style="`display:flex; align-items:center; min-height:${lineHeightTree}px; cursor:pointer;`"
91
+ @click="ckItem(getItemByKey(props.data.text), props.data)"
92
+ v-else
93
+ >
94
+
95
+ <div style="padding-right:5px;">
96
+ <div class="w-mb" :class="getMethodByKey(props.data.text).toLowerCase()">
97
+ {{getMethodByKey(props.data.text)}}
98
+ </div>
99
+ </div>
100
+
101
+ <div>
102
+ {{$ui.gv(getItemByKey(props.data.text),'name')}}
103
+ </div>
104
+
105
+ </div>
106
+
107
+ </template>
108
+ </WTree>
109
+ </template>
110
+ <div
111
+ style="padding:20px; font-size:0.9rem;"
112
+ v-else
113
+ >
114
+ {{$t('waitingData')}}
115
+ </div>
116
+
117
+ <div
118
+ :style="`position:absolute; top:1px; right:12px;`"
119
+ v-if="drawer"
120
+ >
121
+ <WButtonCircle
122
+ :paddingStyle="{v:3,h:3}"
123
+ :icon="mdiArrowLeft"
124
+ :iconSize="16"
125
+ :backgroundColor="'#fff'"
126
+ :backgroundColorHover="'#eee'"
127
+ :backgroundColorFocus="'#fff'"
128
+ :borderColor="'transparent'"
129
+ :borderColorHover="'#eee'"
130
+ :borderColorFocus="'#eee'"
131
+ :tooltip="$t('menuTreeHide')"
132
+ :shadow="true"
133
+ @click="drawer=false"
134
+ ></WButtonCircle>
135
+ </div>
136
+
137
+ </div>
138
+ </template>
139
+
140
+ <template v-slot:content="props">
141
+ <div :style="`height:${props.height}px; width:${props.width}px; position:relative;`">
142
+
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>
153
+
154
+ <div
155
+ :style="`height:${props.height-heightModeBar}px; overflow-y:auto; background:var(--bg-1);`"
156
+ v-if="mode==='docs' && apiSelect"
157
+ >
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>
174
+ </div>
175
+
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>
183
+ </div>
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>
187
+ </div>
188
+ </div>
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>
194
+ </div>
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>
198
+ </div>
199
+ </div>
200
+ </div>
201
+
202
+ </div>
203
+ </div>
204
+
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>
220
+
221
+ </template>
222
+
223
+ <div
224
+ style="padding:20px; font-size:0.9rem;"
225
+ v-else
226
+ >
227
+ {{$t('noSelectApi')}}
228
+ </div>
229
+
230
+ <div
231
+ :style="`position:absolute; top:1px; left:4px;`"
232
+ v-if="!drawer"
233
+ >
234
+ <WButtonCircle
235
+ :paddingStyle="{v:3,h:3}"
236
+ :icon="mdiArrowRight"
237
+ :iconSize="16"
238
+ :backgroundColor="'#fff'"
239
+ :backgroundColorHover="'#eee'"
240
+ :backgroundColorFocus="'#fff'"
241
+ :borderColor="'transparent'"
242
+ :borderColorHover="'#eee'"
243
+ :borderColorFocus="'#eee'"
244
+ :tooltip="$t('menuTreeShow')"
245
+ :shadow="true"
246
+ @click="drawer=true"
247
+ ></WButtonCircle>
248
+ </div>
249
+
250
+ </div>
251
+ </template>
252
+
253
+ </WDrawer>
254
+
255
+ </template>
256
+
257
+ <template v-else>
258
+ <div
259
+ style="padding:20px; font-size:0.9rem;"
260
+ >
261
+ {{$t('waitingData')}}
262
+ </div>
263
+ </template>
264
+
265
+ </template>
266
+
267
+ <LayoutContentStats
268
+ v-else-if="section==='stats'"
269
+ :height="panelHeight"
270
+ ></LayoutContentStats>
271
+
272
+ </div>
273
+
274
+ </div>
275
+ </div>
276
+ </template>
277
+
278
+ <script>
279
+ import get from 'lodash-es/get.js'
280
+ import set from 'lodash-es/set.js'
281
+ import each from 'lodash-es/each.js'
282
+ import size from 'lodash-es/size.js'
283
+ import trim from 'lodash-es/trim.js'
284
+ import find from 'lodash-es/find.js'
285
+ import filter from 'lodash-es/filter.js'
286
+ import cloneDeep from 'lodash-es/cloneDeep.js'
287
+ import isobj from 'wsemi/src/isobj.mjs'
288
+ import iseobj from 'wsemi/src/iseobj.mjs'
289
+ import convertToTree from 'wsemi/src/convertToTree.mjs'
290
+ import { mdiArrowLeft, mdiArrowRight, mdiCloudBraces, mdiChartBar } from '@mdi/js'
291
+ import WButtonCircle from 'w-component-vue/src/components/WButtonCircle.vue'
292
+ import WDrawer from 'w-component-vue/src/components/WDrawer.vue'
293
+ import WTree from 'w-component-vue/src/components/WTree.vue'
294
+ import MdPanel from './MdPanel.vue'
295
+ import LayoutContentEdit from './LayoutContentEdit.vue'
296
+ import LayoutContentTest from './LayoutContentTest.vue'
297
+ import LayoutContentStats from './LayoutContentStats.vue'
298
+
299
+
300
+ export default {
301
+ components: {
302
+ WButtonCircle,
303
+ WDrawer,
304
+ WTree,
305
+ MdPanel,
306
+ LayoutContentEdit,
307
+ LayoutContentTest,
308
+ LayoutContentStats,
309
+ },
310
+ props: {
311
+ },
312
+ data: function() {
313
+ return {
314
+
315
+ panelWidth: 0,
316
+ panelHeight: 0,
317
+
318
+ //左側垂直主選單
319
+ section: 'stats', //'api' | 'stats'; 進站預設頁為統計資訊
320
+ widthMainMenu: 76,
321
+ mainMenus: [
322
+ { id: 'stats', textKey: 'mmStaInfor', icon: mdiChartBar },
323
+ { id: 'api', textKey: 'mmApi', icon: mdiCloudBraces },
324
+ ],
325
+
326
+ drawer: true,
327
+ drawerWidth: 320,
328
+ drawerWidthMin: 200,
329
+ drawerWidthMax: 700,
330
+
331
+ lineHeightTree: 38,
332
+
333
+ apisTree: [],
334
+ kpApisTree: {},
335
+ apiActive: null,
336
+ mdiArrowLeft,
337
+ mdiArrowRight,
338
+ apiSelect: null,
339
+
340
+ //API 管理:模式切換 / 新增 / 搜尋
341
+ mode: 'docs', //'docs' | 'edit' | 'test'
342
+ newMode: false,
343
+ blankApi: {},
344
+ search: '',
345
+ heightModeBar: 46,
346
+ heightDrawerHeader: 48,
347
+
348
+ }
349
+ },
350
+ computed: {
351
+
352
+ syncState: function() {
353
+ return get(this, '$store.state.syncState')
354
+ },
355
+
356
+ apis: function() {
357
+ return get(this, '$store.state.apis')
358
+ },
359
+
360
+ changeApis: function() {
361
+ let vo = this
362
+ //trigger
363
+ let apis = vo.apis
364
+ let search = vo.search //讓搜尋變更也觸發tree重建
365
+ void search
366
+ vo.genTree(apis)
367
+ return ''
368
+ },
369
+
370
+ editItem: function() {
371
+ let vo = this
372
+ return vo.newMode ? vo.blankApi : vo.apiSelect
373
+ },
374
+
375
+ //關鍵字以分號切開成陣列(trim + 去空),供 pills 區逐一以 chip 呈現
376
+ keywordsList: function() {
377
+ let vo = this
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 !== '')
383
+ },
384
+
385
+ },
386
+ methods: {
387
+
388
+ resize: function(msg) {
389
+ // console.log('methods resize', msg)
390
+
391
+ let vo = this
392
+
393
+ //save
394
+ vo.panelWidth = msg.snew.clientWidth
395
+ vo.panelHeight = msg.snew.clientHeight
396
+ // console.log('vo.panelHeight', vo.panelHeight)
397
+
398
+ },
399
+
400
+ genTree: function () {
401
+ let vo = this
402
+
403
+ //保留目前選取的API id, 重建後盡量維持選取(避免儲存/同步後選取跳回第一筆)
404
+ let prevId = get(vo.apiSelect, 'id', '')
405
+
406
+ //default
407
+ vo.apiActive = null
408
+ vo.apisTree = []
409
+ vo.kpApisTree = {}
410
+
411
+ //check
412
+ if (size(vo.apis) === 0) {
413
+ vo.apiSelect = null
414
+ return
415
+ }
416
+
417
+ //cloneDeep
418
+ let apis = cloneDeep(vo.apis)
419
+ // console.log('apis', cloneDeep(apis))
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
+
430
+ //tr
431
+ let tr = {}
432
+ let kpTree = {}
433
+ each(apis, (v) => {
434
+
435
+ //id, levels
436
+ let id = v.id
437
+ let levels = `${v.levels}.${v.name}`
438
+
439
+ //save
440
+ kpTree[id] = v
441
+
442
+ //check
443
+ if (!isobj(get(tr, levels))) {
444
+ set(tr, levels, id)
445
+ }
446
+
447
+ })
448
+ // console.log('tr', cloneDeep(tr))
449
+ // console.log('kpTree', cloneDeep(kpTree))
450
+
451
+ //convertToTree, 由預處理tree物件轉成tree物件
452
+ let tree = convertToTree(tr, { bindRoot: '全部' })
453
+ // console.log('tree', cloneDeep(tree))
454
+
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
+ }
463
+ // console.log('apiSelect', cloneDeep(apiSelect))
464
+
465
+ //save
466
+ vo.apiSelect = apiSelect
467
+ vo.apisTree = tree
468
+ vo.kpApisTree = kpTree
469
+
470
+ },
471
+
472
+ getItemByKey: function(key) {
473
+ let vo = this
474
+ // let k = `kpApisTree.${key}`
475
+ // console.log('getItemByKey k', k)
476
+ let r = vo.kpApisTree[key]
477
+ // console.log('getItemByKey r', r, vo.kpApisTree)
478
+ return r
479
+ },
480
+
481
+ getMethod: function(item) {
482
+ // let vo = this
483
+ let method = get(item, 'method', '').toUpperCase()
484
+ return method
485
+ },
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
+
497
+ getMethodByKey: function(key) {
498
+ let vo = this
499
+ let item = vo.getItemByKey(key)
500
+ let method = get(item, 'method', '').toUpperCase()
501
+ return method
502
+ },
503
+
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
517
+ },
518
+
519
+ funActive: function(msg) {
520
+ let vo = this
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
537
+ },
538
+
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即離開新增狀態
547
+ },
548
+
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
+ }
556
+ },
557
+
558
+ onClickMainMenu: function(m) {
559
+ let vo = this
560
+ vo.section = m.id
561
+ },
562
+
563
+ onClickAddApi: function() {
564
+ let vo = this
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',
578
+ }
579
+ vo.newMode = true
580
+ vo.mode = 'edit'
581
+ },
582
+
583
+ onEditSaved: function() {
584
+ let vo = this
585
+ vo.newMode = false
586
+ vo.mode = 'docs'
587
+ },
588
+
589
+ onEditDeleted: function() {
590
+ let vo = this
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'
600
+ },
601
+
602
+ }
603
+ }
604
+ </script>
605
+
606
+ <style scoped>
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);
625
+ }
626
+ .w-mm-btn.on {
627
+ color: var(--accent);
628
+ border-left-color: var(--accent);
629
+ background: rgba(0, 153, 255, 0.08);
630
+ }
631
+ .w-mm-label {
632
+ font-size: 11px;
633
+ line-height: 1.2;
634
+ text-align: center;
635
+ word-break: break-word;
636
+ }
637
+ </style>