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,577 @@
1
+ <template>
2
+ <div :style="`height:${height}px; box-sizing:border-box; overflow-y:auto; padding:20px;`">
3
+
4
+ <!-- 區塊一:基本資訊 -->
5
+ <div style="margin-bottom:20px;">
6
+
7
+ <div class="bk-title-sec">
8
+ {{$t('secBasic')}}
9
+ </div>
10
+
11
+ <div class="bk-row">
12
+ <div class="bk-col-label bk-title">{{$t('name')}}<span style="color:var(--danger); margin-left:2px;">*</span></div>
13
+ <div class="bk-col-field">
14
+ <input class="bk-input" v-model="form.name" type="text" />
15
+ <div class="bk-err" v-if="errName">{{errName}}</div>
16
+ </div>
17
+ </div>
18
+
19
+ <div class="bk-row">
20
+ <div class="bk-col-label bk-title">{{$t('description')}}</div>
21
+ <div class="bk-col-field">
22
+ <textarea class="bk-textarea" v-model="form.description" rows="3"></textarea>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="bk-row">
27
+ <div class="bk-col-label bk-title">{{$t('apiUrl')}}<span style="color:var(--danger); margin-left:2px;">*</span></div>
28
+ <div class="bk-col-field">
29
+ <input class="bk-input" v-model="form.url" type="text" />
30
+ <div class="bk-err" v-if="errUrl">{{errUrl}}</div>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="bk-row">
35
+ <div class="bk-col-label bk-title">{{$t('fieldMethod')}}</div>
36
+ <div class="bk-col-field">
37
+ <WTextSelect
38
+ style="width:160px;"
39
+ :items="methodItems"
40
+ :value="form.method"
41
+ @input="onChangeMethod"
42
+ >
43
+ <template v-slot:select="props">
44
+ {{getMethodText(props.item)}}
45
+ </template>
46
+ <template v-slot:item="props">
47
+ {{getMethodText(props.item)}}
48
+ </template>
49
+ </WTextSelect>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="bk-row">
54
+ <div class="bk-col-label bk-title">{{$t('version')}}</div>
55
+ <div class="bk-col-field">
56
+ <input class="bk-input" v-model="form.version" type="text" />
57
+ </div>
58
+ </div>
59
+
60
+ <div class="bk-row">
61
+ <div class="bk-col-label bk-title">{{$t('fieldGroup')}}</div>
62
+ <div class="bk-col-field">
63
+ <input class="bk-input" v-model="form.group" type="text" />
64
+ </div>
65
+ </div>
66
+
67
+ <div class="bk-row">
68
+ <div class="bk-col-label bk-title">{{$t('levels')}}</div>
69
+ <div class="bk-col-field">
70
+ <input class="bk-input" v-model="form.levels" type="text" />
71
+ </div>
72
+ </div>
73
+
74
+ <div class="bk-row">
75
+ <div class="bk-col-label bk-title">{{$t('keywords')}}</div>
76
+ <div class="bk-col-field">
77
+ <input class="bk-input" v-model="form.keywords" type="text" />
78
+ </div>
79
+ </div>
80
+
81
+ <div class="bk-row">
82
+ <div class="bk-col-label bk-title">{{$t('state')}}</div>
83
+ <div class="bk-col-field">
84
+ <input class="bk-input" v-model="form.state" type="text" />
85
+ </div>
86
+ </div>
87
+
88
+ <div class="bk-row">
89
+ <div class="bk-col-label bk-title">{{$t('fieldOrder')}}</div>
90
+ <div class="bk-col-field">
91
+ <input class="bk-input" v-model.number="form.order" type="number" />
92
+ </div>
93
+ </div>
94
+
95
+ <div class="bk-row">
96
+ <div class="bk-col-label bk-title">{{$t('tokens')}}</div>
97
+ <div class="bk-col-field">
98
+ <textarea class="bk-textarea" v-model="form.tokens" rows="3"></textarea>
99
+ </div>
100
+ </div>
101
+
102
+ </div>
103
+
104
+ <!-- 區塊二:文件 -->
105
+ <div style="margin-bottom:20px;">
106
+
107
+ <div class="bk-title-sec">
108
+ {{$t('secDocs')}}
109
+ </div>
110
+
111
+ <div class="bk-row">
112
+ <div class="bk-col-label bk-title">{{$t('mdInputParams')}}</div>
113
+ <div class="bk-col-field">
114
+ <textarea class="bk-textarea" v-model="form.mdInputParams" rows="6"></textarea>
115
+ <div class="bk-preview bk-item" v-if="form.mdInputParams">
116
+ <MdPanel :md="form.mdInputParams"></MdPanel>
117
+ </div>
118
+ </div>
119
+ </div>
120
+
121
+ <div class="bk-row">
122
+ <div class="bk-col-label bk-title">{{$t('inputExample')}}</div>
123
+ <div class="bk-col-field">
124
+ <textarea class="bk-textarea" v-model="form.inputExample" rows="4"></textarea>
125
+ </div>
126
+ </div>
127
+
128
+ <div class="bk-row">
129
+ <div class="bk-col-label bk-title">{{$t('mdOutputParams')}}</div>
130
+ <div class="bk-col-field">
131
+ <textarea class="bk-textarea" v-model="form.mdOutputParams" rows="6"></textarea>
132
+ <div class="bk-preview bk-item" v-if="form.mdOutputParams">
133
+ <MdPanel :md="form.mdOutputParams"></MdPanel>
134
+ </div>
135
+ </div>
136
+ </div>
137
+
138
+ <div class="bk-row">
139
+ <div class="bk-col-label bk-title">{{$t('outputExample')}}</div>
140
+ <div class="bk-col-field">
141
+ <textarea class="bk-textarea" v-model="form.outputExample" rows="6"></textarea>
142
+ </div>
143
+ </div>
144
+
145
+ </div>
146
+
147
+ <!-- 區塊三:測試設定 -->
148
+ <div style="margin-bottom:20px;">
149
+
150
+ <div class="bk-title-sec">
151
+ {{$t('secTest')}}
152
+ </div>
153
+
154
+ <div class="bk-row">
155
+ <div class="bk-col-label bk-title">{{$t('fieldTestBaseUrl')}}</div>
156
+ <div class="bk-hint">{{$t('hintTestBaseUrl')}}</div>
157
+ <div class="bk-col-field">
158
+ <input class="bk-input" v-model="form.testBaseUrl" type="text" />
159
+ </div>
160
+ </div>
161
+
162
+ <div class="bk-row">
163
+ <div class="bk-col-label bk-title">{{$t('fieldAuthType')}}</div>
164
+ <div class="bk-hint">{{$t('hintAuthType')}}</div>
165
+ <div class="bk-col-field">
166
+ <WTextSelect
167
+ style="width:200px;"
168
+ :items="authTypeItems"
169
+ :value="form.authType"
170
+ @input="onChangeAuthType"
171
+ >
172
+ <template v-slot:select="props">
173
+ {{getAuthTypeText(props.item)}}
174
+ </template>
175
+ <template v-slot:item="props">
176
+ {{getAuthTypeText(props.item)}}
177
+ </template>
178
+ </WTextSelect>
179
+ </div>
180
+ </div>
181
+
182
+ <div class="bk-row">
183
+ <div class="bk-col-label bk-title">{{$t('fieldAuthConfigJson')}}</div>
184
+ <div class="bk-hint">{{$t('hintAuthConfigJson')}}</div>
185
+ <div class="bk-col-field">
186
+ <textarea class="bk-textarea" v-model="form.authConfigJson" rows="4"></textarea>
187
+ <div class="bk-err" v-if="errJson && errJsonField==='authConfigJson'">{{errJson}}</div>
188
+ </div>
189
+ </div>
190
+
191
+ <div class="bk-row">
192
+ <div class="bk-col-label bk-title">{{$t('fieldDefaultHeadersJson')}}</div>
193
+ <div class="bk-hint">{{$t('hintDefaultHeadersJson')}}</div>
194
+ <div class="bk-col-field">
195
+ <textarea class="bk-textarea" v-model="form.defaultHeadersJson" rows="4"></textarea>
196
+ <div class="bk-err" v-if="errJson && errJsonField==='defaultHeadersJson'">{{errJson}}</div>
197
+ </div>
198
+ </div>
199
+
200
+ <div class="bk-row">
201
+ <div class="bk-col-label bk-title">{{$t('fieldDefaultQueryJson')}}</div>
202
+ <div class="bk-hint">{{$t('hintDefaultQueryJson')}}</div>
203
+ <div class="bk-col-field">
204
+ <textarea class="bk-textarea" v-model="form.defaultQueryJson" rows="4"></textarea>
205
+ <div class="bk-err" v-if="errJson && errJsonField==='defaultQueryJson'">{{errJson}}</div>
206
+ </div>
207
+ </div>
208
+
209
+ <div class="bk-row">
210
+ <div class="bk-col-label bk-title">{{$t('fieldDefaultBodyJson')}}</div>
211
+ <div class="bk-hint">{{$t('hintDefaultBodyJson')}}</div>
212
+ <div class="bk-col-field">
213
+ <textarea class="bk-textarea" v-model="form.defaultBodyJson" rows="4"></textarea>
214
+ <div class="bk-err" v-if="errJson && errJsonField==='defaultBodyJson'">{{errJson}}</div>
215
+ </div>
216
+ </div>
217
+
218
+ <div class="bk-row">
219
+ <div class="bk-col-label bk-title">{{$t('fieldContentType')}}</div>
220
+ <div class="bk-hint">{{$t('hintContentType')}}</div>
221
+ <div class="bk-col-field">
222
+ <input class="bk-input" v-model="form.contentType" type="text" />
223
+ </div>
224
+ </div>
225
+
226
+ </div>
227
+
228
+ <!-- 全域儲存錯誤 -->
229
+ <div class="bk-err" v-if="errSave" style="margin-bottom:10px;">{{errSave}}</div>
230
+
231
+ <!-- 底部按鈕列 -->
232
+ <div style="display:flex; gap:8px; padding-bottom:20px;">
233
+
234
+ <button class="btn-save" @click="onClickSave">
235
+ {{$t('save')}}
236
+ </button>
237
+
238
+ <button class="btn-delete" @click="onClickDelete" v-if="!isNew">
239
+ {{$t('delete')}}
240
+ </button>
241
+
242
+ <button class="btn-cancel" @click="onClickCancel">
243
+ {{$t('cancel')}}
244
+ </button>
245
+
246
+ </div>
247
+
248
+ </div>
249
+ </template>
250
+
251
+ <script>
252
+ import cloneDeep from 'lodash-es/cloneDeep.js'
253
+ import isestr from 'wsemi/src/isestr.mjs'
254
+ import WTextSelect from 'w-component-vue/src/components/WTextSelect.vue'
255
+ import MdPanel from './MdPanel.vue'
256
+
257
+
258
+ export default {
259
+ components: {
260
+ WTextSelect,
261
+ MdPanel,
262
+ },
263
+ props: {
264
+ item: {
265
+ type: Object,
266
+ default: function() { return {} },
267
+ },
268
+ height: {
269
+ type: Number,
270
+ default: 0,
271
+ },
272
+ isNew: {
273
+ type: Boolean,
274
+ default: false,
275
+ },
276
+ },
277
+ data: function() {
278
+ return {
279
+
280
+ form: {},
281
+
282
+ errName: '',
283
+ errUrl: '',
284
+ errJson: '',
285
+ errJsonField: '',
286
+ errSave: '',
287
+
288
+ methodItems: ['get', 'post', 'put', 'del'],
289
+ authTypeItems: ['none', 'bearer', 'apikey', 'basic'],
290
+
291
+ }
292
+ },
293
+ mounted: function() {
294
+ let vo = this
295
+ vo.form = cloneDeep(vo.item)
296
+ },
297
+ watch: {
298
+ item: {
299
+ deep: true,
300
+ immediate: true,
301
+ handler: function(val) {
302
+ let vo = this
303
+ vo.form = cloneDeep(val)
304
+ },
305
+ },
306
+ },
307
+ methods: {
308
+
309
+ getMethodText: function(item) {
310
+ // let vo = this
311
+ if (!isestr(item)) {
312
+ return ''
313
+ }
314
+ return item.toUpperCase()
315
+ },
316
+
317
+ getAuthTypeText: function(item) {
318
+ let vo = this
319
+ let kp = {
320
+ none: vo.$t('authNone'),
321
+ bearer: vo.$t('authBearer'),
322
+ apikey: vo.$t('authApikey'),
323
+ basic: vo.$t('authBasic'),
324
+ }
325
+ if (!isestr(item)) {
326
+ return ''
327
+ }
328
+ return kp[item] || item
329
+ },
330
+
331
+ onChangeMethod: function(val) {
332
+ let vo = this
333
+ vo.form.method = val
334
+ },
335
+
336
+ onChangeAuthType: function(val) {
337
+ let vo = this
338
+ vo.form.authType = val
339
+ },
340
+
341
+ onClickSave: function() {
342
+ let vo = this
343
+ vo.submitSave()
344
+ .catch(function(err) { console.log('catch', err); vo.$alert(vo.$t('anUnexpectedErrorOccurred'), 'error') })
345
+ .finally(function() { vo.$ui.updateLoading(false) })
346
+ },
347
+
348
+ submitSave: function() {
349
+ let vo = this
350
+ let core = async function() {
351
+
352
+ // 1) 清空舊 inline 錯誤
353
+ vo.errName = ''
354
+ vo.errUrl = ''
355
+ vo.errJson = ''
356
+ vo.errJsonField = ''
357
+ vo.errSave = ''
358
+
359
+ // 2) 同步檢測,開 loading 之前
360
+ if (!isestr(vo.form.name)) {
361
+ vo.errName = vo.$t('valRequired')
362
+ return
363
+ }
364
+ if (!isestr(vo.form.url)) {
365
+ vo.errUrl = vo.$t('valRequired')
366
+ return
367
+ }
368
+ let jsonFields = ['authConfigJson', 'defaultHeadersJson', 'defaultQueryJson', 'defaultBodyJson']
369
+ for (let i = 0; i < jsonFields.length; i++) {
370
+ let fk = jsonFields[i]
371
+ let fv = vo.form[fk]
372
+ if (isestr(fv)) {
373
+ let jsonOk = true
374
+ try { JSON.parse(fv) } catch (e) { jsonOk = false }
375
+ if (!jsonOk) {
376
+ vo.errJsonField = fk
377
+ vo.errJson = fk + ': ' + vo.$t('valInvalidJson')
378
+ return
379
+ }
380
+ }
381
+ }
382
+
383
+ // 3) 確定打 API 才開 loading
384
+ vo.$ui.updateLoading(true)
385
+
386
+ // 4) 執行 async,各自 catch + 旗標短路
387
+ let okSave = false
388
+ await vo.$fapi.saveApi(cloneDeep(vo.form))
389
+ .then(function() { okSave = true })
390
+ .catch(function(err) { console.log('saveApi', err); vo.errSave = vo.$transErr(err) }) //err 為後端 err-key,依 lang 反查顯示
391
+ if (!okSave) return
392
+
393
+ // 5) 全成功
394
+ vo.$emit('saved')
395
+ //成功訊息改用 confirm modal($dg.showCheckYes)而非 toast:toast 是插入後又移除的暫時 DOM,
396
+ //新版 chromium 在 insert/remove 後會把整頁 render 原點偏移 1px(headless 偶發、實測 toast 關掉即穩),
397
+ //modal 是「顯示著被截」不會有移除殘留,且對齊 w-web-sso 之成功 modal。
398
+ vo.$dg.showCheckYes(vo.$t('saveSuccess'), { type: 'success' })
399
+ return 'ok'
400
+
401
+ }
402
+ return core()
403
+ },
404
+
405
+ onClickDelete: function() {
406
+ let vo = this
407
+ vo.$dg.showCheckYesNo(vo.$t('confirmDeleteApi'))
408
+ .then(function() { vo.doDelete() })
409
+ .catch(function(err) { if (err === 'close') return; console.log('showCheckYesNo', err) })
410
+ },
411
+
412
+ doDelete: function() {
413
+ let vo = this
414
+ let core = async function() {
415
+
416
+ // 1) 清空錯誤
417
+ vo.errSave = ''
418
+
419
+ // 2) 同步檢測(無需額外檢測,id 由 item 帶入)
420
+
421
+ // 3) 開 loading
422
+ vo.$ui.updateLoading(true)
423
+
424
+ // 4) 執行 async
425
+ let okDel = false
426
+ await vo.$fapi.deleteApi(vo.form.id)
427
+ .then(function() { okDel = true })
428
+ .catch(function(err) { console.log('deleteApi', err); vo.errSave = vo.$transErr(err) }) //err 為後端 err-key,依 lang 反查顯示
429
+ if (!okDel) return
430
+
431
+ // 5) 全成功
432
+ vo.$emit('deleted')
433
+ vo.$dg.showCheckYes(vo.$t('deleteSuccess'), { type: 'success' }) //同 saveSuccess:toast → confirm modal(避免移除殘留位移、對齊 SSO);type:success 顯綠勾
434
+ return 'ok'
435
+
436
+ }
437
+ core()
438
+ .catch(function(err) { console.log('catch', err); vo.$alert(vo.$t('anUnexpectedErrorOccurred'), 'error') })
439
+ .finally(function() { vo.$ui.updateLoading(false) })
440
+ },
441
+
442
+ onClickCancel: function() {
443
+ let vo = this
444
+ vo.$emit('cancel')
445
+ },
446
+
447
+ },
448
+ }
449
+ </script>
450
+
451
+ <style scoped>
452
+ .bk-title-sec {
453
+ font-size: var(--fs-h3);
454
+ font-weight: 600;
455
+ color: var(--c-1);
456
+ padding: 6px 0px 10px 0px;
457
+ border-bottom: 1px solid var(--border);
458
+ margin-bottom: 12px;
459
+ }
460
+ .bk-title {
461
+ font-size: var(--fs-xs);
462
+ font-weight: 500;
463
+ color: var(--c-2);
464
+ padding: 0 0 6px 0;
465
+ }
466
+ .bk-item {
467
+ padding: 3px 5px;
468
+ font-size: 0.9rem;
469
+ line-height: 0.9rem;
470
+ border-radius: var(--radius-lg);
471
+ border: 1px solid var(--border);
472
+ background: var(--bg-1);
473
+ }
474
+ .bk-row {
475
+ display: flex;
476
+ flex-direction: column;
477
+ /* padding-top 大於 padding-bottom:使「組間」(上一列欄位→下一列小標) 間距明顯大於「組內」(小標→說明→欄位),小標歸屬清楚 */
478
+ padding: 12px 0 6px 0;
479
+ }
480
+ .bk-col-label {
481
+ width: auto;
482
+ flex-shrink: 0;
483
+ }
484
+ .bk-col-field {
485
+ flex: 1;
486
+ }
487
+ .bk-input {
488
+ width: 100%;
489
+ box-sizing: border-box;
490
+ padding: 5px 8px;
491
+ font-size: var(--fs-body);
492
+ border-radius: var(--radius-lg);
493
+ border: 1px solid var(--border);
494
+ background: var(--bg-1);
495
+ color: var(--c-1);
496
+ outline: none;
497
+ transition: border-color .15s, box-shadow .15s;
498
+ }
499
+ .bk-input:focus {
500
+ border-color: var(--accent);
501
+ box-shadow: var(--focus);
502
+ }
503
+ .bk-textarea {
504
+ width: 100%;
505
+ box-sizing: border-box;
506
+ padding: 5px 8px;
507
+ font-size: var(--fs-sm);
508
+ border-radius: var(--radius-lg);
509
+ border: 1px solid var(--border);
510
+ background: var(--bg-1);
511
+ color: var(--c-1);
512
+ outline: none;
513
+ resize: vertical;
514
+ font-family: var(--font-code);
515
+ transition: border-color .15s, box-shadow .15s;
516
+ }
517
+ .bk-textarea:focus {
518
+ border-color: var(--accent);
519
+ box-shadow: var(--focus);
520
+ }
521
+ .bk-preview {
522
+ margin-top: 6px;
523
+ padding: 8px 10px;
524
+ }
525
+ .bk-err {
526
+ color: var(--danger);
527
+ font-size: var(--fs-xs);
528
+ padding: 3px 2px;
529
+ }
530
+ .bk-hint {
531
+ color: var(--c-3);
532
+ font-size: var(--fs-xs);
533
+ line-height: 1.6;
534
+ margin: 0 0 8px 0;
535
+ white-space: pre-line;
536
+ }
537
+ .btn-save {
538
+ padding: 7px 18px;
539
+ font-size: var(--fs-body);
540
+ border-radius: var(--radius-lg);
541
+ border: 1px solid var(--accent);
542
+ background: var(--accent);
543
+ color: #fff;
544
+ cursor: pointer;
545
+ transition: background-color .15s, border-color .15s;
546
+ }
547
+ .btn-save:hover {
548
+ background: var(--accent-hover);
549
+ border-color: var(--accent-hover);
550
+ }
551
+ .btn-delete {
552
+ padding: 7px 18px;
553
+ font-size: var(--fs-body);
554
+ border-radius: var(--radius-lg);
555
+ border: 1px solid var(--danger);
556
+ background: transparent;
557
+ color: var(--danger);
558
+ cursor: pointer;
559
+ transition: background-color .15s;
560
+ }
561
+ .btn-delete:hover {
562
+ background: var(--danger-bg);
563
+ }
564
+ .btn-cancel {
565
+ padding: 7px 18px;
566
+ font-size: var(--fs-body);
567
+ border-radius: var(--radius-lg);
568
+ border: 1px solid var(--border);
569
+ background: transparent;
570
+ color: var(--c-2);
571
+ cursor: pointer;
572
+ transition: background-color .15s;
573
+ }
574
+ .btn-cancel:hover {
575
+ background: var(--bg-3);
576
+ }
577
+ </style>