w-web-api 1.0.82 → 1.0.84

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.
@@ -64,7 +64,7 @@
64
64
 
65
65
  <div
66
66
  v-for="(row, idx) in req.query"
67
- :key="idx"
67
+ :key="row.id"
68
68
  class="kv-row"
69
69
  >
70
70
  <div class="kv-col-ck kv-center">
@@ -123,7 +123,7 @@
123
123
 
124
124
  <div
125
125
  v-for="(row, idx) in req.headers"
126
- :key="idx"
126
+ :key="row.id"
127
127
  class="kv-row"
128
128
  >
129
129
  <div class="kv-col-ck kv-center">
@@ -239,6 +239,15 @@ let kpColorMethod = {
239
239
  }
240
240
 
241
241
 
242
+ //mkRow:Query/Headers 表列之唯一建立點(改造前 10 處各自手寫 `{ on, key, value }` 字面值)。
243
+ //id 為單調遞增序號、供 v-for :key(改造前 :key=idx——增刪列後 Vue 依索引重用 DOM,列之瞬時 DOM 狀態(焦點/IME 組字)會錯位)。
244
+ let seqKvRow = 0
245
+ function mkRow(key = '', value = '') {
246
+ seqKvRow += 1
247
+ return { id: seqKvRow, on: true, key, value }
248
+ }
249
+
250
+
242
251
  export default {
243
252
  components: {
244
253
  WTextSelect,
@@ -246,7 +255,9 @@ export default {
246
255
  props: {
247
256
  item: {
248
257
  type: Object,
249
- default: function() { return {} },
258
+ default: function() {
259
+ return {}
260
+ },
250
261
  },
251
262
  height: {
252
263
  type: Number,
@@ -255,12 +266,12 @@ export default {
255
266
  },
256
267
  data: function() {
257
268
  return {
258
- methodItems: ['GET', 'POST', 'PUT', 'DEL'],
269
+ methodItems: this.$s.METHODS.map((m) => m.toUpperCase()), //顯示層大寫;單一來源 mShare.METHODS(與編輯表單共用)
259
270
  req: {
260
271
  method: 'GET',
261
272
  url: '',
262
- query: [{ on: true, key: '', value: '' }],
263
- headers: [{ on: true, key: '', value: '' }],
273
+ query: [mkRow()],
274
+ headers: [mkRow()],
264
275
  body: '',
265
276
  },
266
277
  res: null,
@@ -320,48 +331,14 @@ export default {
320
331
  if (vo.res === null) {
321
332
  return ''
322
333
  }
323
- // 取得原始字串
324
- let raw = ''
325
- if (isobj(vo.res.data)) {
326
- try {
327
- raw = JSON.stringify(vo.res.data, null, 2)
328
- }
329
- catch (e) {
330
- raw = String(vo.res.data)
331
- }
332
- }
333
- else {
334
- raw = String(vo.res.data)
334
+ //物件/陣列 → 縮排 JSON + 語法高亮;其餘(HTML/純文字/數字…)→ 原文(僅 escape、不上色:純文字中的數字/true 不是 JSON token)。
335
+ //型別表見 spec/流程_測試API.md「回應 data 型別 → 顯示形式」;高亮實作與 docs 分頁共用 mShare.jsonHighlight
336
+ //(改造前本處自寫規則以 &quot; 比對但 escape 未換 ",鍵值永遠不上色、只有數字/布林被上色,且對 HTML 原文亦上色)
337
+ let raw = vo.$s.dataToText(vo.res.data)
338
+ if (!vo.$s.isJsonContainer(vo.res.data)) {
339
+ return vo.$s.escapeHtml(raw)
335
340
  }
336
- // HTML escape
337
- let escaped = raw
338
- .replace(/&/g, '&amp;')
339
- .replace(/</g, '&lt;')
340
- .replace(/>/g, '&gt;')
341
- // 簡易 JSON 語法高亮(逐 token 替換)
342
- // key: "xxx":
343
- // string value: "xxx"
344
- // number: bare number
345
- // bool/null: true/false/null
346
- let highlighted = escaped.replace(
347
- /(&quot;[^&]*&quot;)\s*(:)|(&quot;[^&]*&quot;)|(\b-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)|(\b(?:true|false|null)\b)/g,
348
- function(match, keyStr, colon, strVal, numVal, boolVal) {
349
- if (keyStr && colon) {
350
- return '<span class="k">' + keyStr + '</span>' + colon
351
- }
352
- if (strVal) {
353
- return '<span class="s">' + strVal + '</span>'
354
- }
355
- if (numVal !== undefined) {
356
- return '<span class="n">' + numVal + '</span>'
357
- }
358
- if (boolVal !== undefined) {
359
- return '<span class="b">' + boolVal + '</span>'
360
- }
361
- return match
362
- }
363
- )
364
- return highlighted
341
+ return vo.$s.jsonHighlight(raw)
365
342
  },
366
343
 
367
344
  },
@@ -383,7 +360,7 @@ export default {
383
360
  return rows
384
361
  }
385
362
  each(obj, function(v, k) {
386
- rows.push({ on: true, key: k, value: String(v) })
363
+ rows.push(mkRow(k, String(v)))
387
364
  })
388
365
  return rows
389
366
  },
@@ -416,7 +393,7 @@ export default {
416
393
  // { "token": "xxx" };留空則退用 tokens 欄第一個(向後相容舊資料)
417
394
  let token = isestr(authCfg.token) ? authCfg.token : (isestr(itm.tokens) ? itm.tokens.split(';')[0] : '')
418
395
  if (isestr(token)) {
419
- headerRows.push({ on: true, key: 'Authorization', value: 'Bearer ' + token })
396
+ headerRows.push(mkRow('Authorization', 'Bearer ' + token))
420
397
  }
421
398
  }
422
399
  else if (authType === 'apikey') {
@@ -425,10 +402,10 @@ export default {
425
402
  let value = isestr(authCfg.value) ? authCfg.value : ''
426
403
  if (isestr(value)) {
427
404
  if (authCfg.in === 'query') {
428
- queryRows.push({ on: true, key: name, value: value })
405
+ queryRows.push(mkRow(name, value))
429
406
  }
430
407
  else {
431
- headerRows.push({ on: true, key: name, value: value })
408
+ headerRows.push(mkRow(name, value))
432
409
  }
433
410
  }
434
411
  }
@@ -437,22 +414,25 @@ export default {
437
414
  let u = isestr(authCfg.username) ? authCfg.username : ''
438
415
  let p = isestr(authCfg.password) ? authCfg.password : ''
439
416
  if (isestr(u) || isestr(p)) {
440
- headerRows.push({ on: true, key: 'Authorization', value: 'Basic ' + btoa(u + ':' + p) })
417
+ headerRows.push(mkRow('Authorization', 'Basic ' + btoa(u + ':' + p)))
441
418
  }
442
419
  }
443
420
 
444
- // content-type 標頭
445
- if (isestr(itm.contentType)) {
446
- headerRows.push({ on: true, key: 'Content-Type', value: itm.contentType })
421
+ // content-type 標頭:預設標頭(defaultHeadersJson)已明給 Content-Type(不分大小寫)者以其為準、不再補一列
422
+ //(對標 Postman:使用者明給之標頭優先於自動標頭。改造前雙列並存:送出時同鍵收斂使後列覆蓋前列,
423
+ //改為有序鍵值對後兩列會被 fetch 合併成 'a, b',皆非使用者所見)
424
+ let hasContentType = headerRows.some((r) => r.key.toLowerCase() === 'content-type')
425
+ if (isestr(itm.contentType) && !hasContentType) {
426
+ headerRows.push(mkRow('Content-Type', itm.contentType))
447
427
  }
448
428
 
449
429
  if (headerRows.length === 0) {
450
- headerRows.push({ on: true, key: '', value: '' })
430
+ headerRows.push(mkRow())
451
431
  }
452
432
  vo.req.headers = headerRows
453
433
 
454
434
  if (queryRows.length === 0) {
455
- queryRows.push({ on: true, key: '', value: '' })
435
+ queryRows.push(mkRow())
456
436
  }
457
437
  vo.req.query = queryRows
458
438
 
@@ -474,27 +454,27 @@ export default {
474
454
 
475
455
  addQueryRow: function() {
476
456
  let vo = this
477
- vo.req.query.push({ on: true, key: '', value: '' })
457
+ vo.req.query.push(mkRow())
478
458
  },
479
459
 
480
460
  removeQueryRow: function(idx) {
481
461
  let vo = this
482
462
  vo.req.query.splice(idx, 1)
483
463
  if (vo.req.query.length === 0) {
484
- vo.req.query.push({ on: true, key: '', value: '' })
464
+ vo.req.query.push(mkRow())
485
465
  }
486
466
  },
487
467
 
488
468
  addHeaderRow: function() {
489
469
  let vo = this
490
- vo.req.headers.push({ on: true, key: '', value: '' })
470
+ vo.req.headers.push(mkRow())
491
471
  },
492
472
 
493
473
  removeHeaderRow: function(idx) {
494
474
  let vo = this
495
475
  vo.req.headers.splice(idx, 1)
496
476
  if (vo.req.headers.length === 0) {
497
- vo.req.headers.push({ on: true, key: '', value: '' })
477
+ vo.req.headers.push(mkRow())
498
478
  }
499
479
  },
500
480
 
@@ -523,45 +503,33 @@ export default {
523
503
  return
524
504
  }
525
505
 
526
- // 3) 開啟本地 loading
506
+ // 3) loading:本地 sending(送出鈕禁用)+ 頁面層全頁 overlay(CLAUDE.md 三層雙擊防護之頁面層;
507
+ // 送出對目標可能有副作用(POST/PUT/DELETE),與編輯分頁之存/刪同級;改造前只有本地 sending)
527
508
  vo.sending = true
509
+ vo.$ui.updateLoading(true)
528
510
 
529
- // 4) 組 spec 並送出
530
- let method = vo.req.method
531
-
532
- let query = {}
533
- each(vo.req.query, function(row) {
534
- if (row.on && isestr(row.key)) {
535
- query[row.key] = row.value
536
- }
537
- })
511
+ // 4) 組 spec 並送出(方法代號 → HTTP 標準動詞;顯示層之 DEL 須送 DELETE,見 mShare 說明)
512
+ let method = vo.$s.methodToHttpVerb(vo.req.method)
538
513
 
539
- let headers = {}
540
- each(vo.req.headers, function(row) {
541
- if (row.on && isestr(row.key)) {
542
- headers[row.key] = row.value
543
- }
544
- })
514
+ //Query/Headers:僅 on 且鍵非空之列,以有序鍵值對 [[key, value], ...] 送出、重複鍵各自保留
515
+ //(mShare.kvRowsToPairs;改造前以物件收斂使同鍵後列靜默覆蓋前列)
516
+ let query = vo.$s.kvRowsToPairs(vo.req.query)
517
+ let headers = vo.$s.kvRowsToPairs(vo.req.headers)
545
518
 
546
- let body = undefined
519
+ //請求內容原文送出(同 Scalar / Postman:打什麼送什麼,不解析、不驗證、不代換;Content-Type 以標頭表為準),
520
+ //空白則不帶 body;GET/HEAD 不帶。見 spec/流程_測試API.md「規則摘要」契約與 ADR-029。
521
+ let body
547
522
  if (method !== 'GET' && method !== 'HEAD') {
548
523
  let rawBody = vo.req.body
549
- let ct = get(vo.item, 'contentType', '')
550
- if (isestr(ct) && ct.toLowerCase().indexOf('json') >= 0) {
551
- let parsed = j2o(rawBody)
552
- body = isobj(parsed) ? parsed : rawBody
553
- }
554
- else {
555
- body = rawBody
556
- }
524
+ body = isestr(rawBody) ? rawBody : undefined
557
525
  }
558
526
 
559
527
  let spec = {
560
- method: method,
528
+ method,
561
529
  url: vo.req.url,
562
- headers: headers,
563
- query: query,
564
- body: body,
530
+ headers,
531
+ query,
532
+ body,
565
533
  timeout: 30000,
566
534
  }
567
535
 
@@ -586,11 +554,15 @@ export default {
586
554
 
587
555
  core()
588
556
  .catch(function(err) {
557
+ //非預期例外:先關 overlay、再以 showCheckYes modal 通知(CLAUDE.md:失敗通知走 showCheckYes、之前先 updateLoading(false);
558
+ //非 success 之 type 顯示警示圖標);不用會自動消失之 $alert toast(ADR-005 modal 政策)
589
559
  console.log('catch', err)
590
- vo.$alert(vo.$t('anUnexpectedErrorOccurred'), { type: 'error' })
560
+ vo.$ui.updateLoading(false)
561
+ vo.$dg.showCheckYes(vo.$t('anUnexpectedErrorOccurred'), { type: 'error' })
591
562
  })
592
563
  .finally(function() {
593
564
  vo.sending = false
565
+ vo.$ui.updateLoading(false)
594
566
  })
595
567
 
596
568
  },
package/src/main.js CHANGED
@@ -13,6 +13,7 @@ import domDragDrop from 'w-component-vue/src/js/domDragDrop.mjs'
13
13
  import App from './App.vue'
14
14
  import store from './store/index.mjs'
15
15
  import ui from './plugins/mUI.mjs'
16
+ import * as s from './plugins/mShare.mjs'
16
17
  import ds from './schema/index.mjs'
17
18
  // console.log('globalState', globalState)
18
19
  // console.log('ds', ds)
@@ -33,6 +34,7 @@ Vue.prototype.$alert = function() {
33
34
  //prototype
34
35
  Vue.prototype.$ui = ui
35
36
  Vue.prototype.$t = ui.getKpText
37
+ Vue.prototype.$s = s
36
38
  //$transErr:後端 reject 之 err-key → 依當前 lang 反查顯示文字。
37
39
  //非字串或查無對應 key(getKpText 查不到會原樣回 key)則 fallback 'anUnexpectedErrorOccurred',避免顯示生 key。
38
40
  Vue.prototype.$transErr = (err) => {
@@ -51,7 +53,7 @@ Vue.directive('domdragdrop', domDragDrop())
51
53
  //WServHapiClient
52
54
  // let bFirstSync = false //不需要bFirstSync, 由getWebInfor結束代表第1次完成同步
53
55
  WServHapiClient({
54
- showLog: false,
56
+ useShowLog: false,
55
57
  url: window.location.origin + window.location.pathname,
56
58
  useWaitToken: true,
57
59
  apiName: 'api',
@@ -0,0 +1,105 @@
1
+ //前後端共用函數區
2
+ import isobj from 'wsemi/src/isobj.mjs'
3
+ import isarr from 'wsemi/src/isarr.mjs'
4
+
5
+
6
+ function isJsonContainer(v) {
7
+ //值是否為 JSON 容器(純物件或陣列)。
8
+ //why 不只用 isobj:wsemi 之 isobj 以 Object.prototype.toString 判 '[object Object]',陣列為 '[object Array]' 回 false;
9
+ //測試分頁「回應內容」與「請求 body 解析」兩站點都要把陣列與物件同等看待(見 spec/流程_測試API.md「回應 data 型別 → 顯示形式」)。
10
+ return isobj(v) || isarr(v)
11
+ }
12
+
13
+
14
+ function dataToText(v) {
15
+ //回應 data → 顯示用純文字。容器(物件/陣列)→ 縮排 2 之 JSON;其餘(字串/數字/布林/null/空)→ String(v)。
16
+ //JSON.stringify 拋錯(如循環參照;經 JSON 傳輸之回應實務上不可達)時退回 String(v)。
17
+ if (isJsonContainer(v)) {
18
+ try {
19
+ return JSON.stringify(v, null, 2)
20
+ }
21
+ catch (e) {
22
+ return String(v)
23
+ }
24
+ }
25
+ return String(v)
26
+ }
27
+
28
+
29
+ //methodToHttpVerb:把 UI/資料層的方法代號轉成 HTTP 標準動詞。
30
+ //why:本系統以 'del'(資料層)/'DEL'(顯示層)表示刪除,但 `DEL` **不是** HTTP 動詞——實測以 fetch 送出 `DEL`
31
+ //時目標伺服器無法解析、於 HTTP 層直接回 400 且 handler 完全不會執行,導致所有刪除類 API 無法測試(種子資料即有
32
+ //3 支)。故送出前與產生 cURL 前一律經此轉換。其餘動詞僅正規化為大寫;空值或非字串退回 'GET'(同 proxy 之預設)。
33
+ function methodToHttpVerb(v) {
34
+ if (typeof v !== 'string' || v === '') {
35
+ return 'GET'
36
+ }
37
+ let m = v.toUpperCase()
38
+ return (m === 'DEL') ? 'DELETE' : m
39
+ }
40
+
41
+
42
+ //METHODS:本系統之方法代號清單(資料層小寫;顯示層以 toUpperCase 呈現;送出時經 methodToHttpVerb 轉 HTTP 動詞)。
43
+ //單一來源:編輯表單與測試分頁之方法下拉共用(改造前兩處各自手寫一份清單)。
44
+ let METHODS = ['get', 'post', 'put', 'del']
45
+
46
+
47
+ //escapeHtml:文字 → 可放入 HTML 文字節點之字串(只 escape & < > 三者;文字節點內之引號不需 escape,
48
+ //且 jsonHighlight 之 token 規則依賴保留原始 ")。
49
+ function escapeHtml(text) {
50
+ let s = (text === null || text === undefined) ? '' : String(text)
51
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
52
+ }
53
+
54
+
55
+ //jsonHighlight:JSON 文字 → 語法高亮 HTML(class k=鍵、s=字串值、n=數字、b=true/false/null;樣式為 src/assets/scalar-ui.css 之 .code)。
56
+ //docs 分頁(outputExample)與測試分頁(回應內容)共用同一實作,兩處行為一致。
57
+ //why 單趟 tokenizer:改造前兩處各自手寫——測試分頁以 &quot; 比對但 escape 只換 & < >,字串永遠不命中、鍵值一律不上色;
58
+ //docs 版逐規則多次 replace,字串內之數字/true 亦被誤上色。此處字串 token(含 \" 跳脫)優先吞掉整段,
59
+ //字串內容不再被後續規則比對;字串後緊接 : 者為鍵,否則為值。
60
+ function jsonHighlight(text) {
61
+ let s = escapeHtml(text)
62
+ return s.replace(/("(?:[^"\\]|\\.)*")(\s*:)?|(-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)|\b(true|false|null)\b/g, function(m, str, colon, num, kw) {
63
+ if (str !== undefined) {
64
+ if (colon !== undefined) {
65
+ return '<span class="k">' + str + '</span>' + colon
66
+ }
67
+ return '<span class="s">' + str + '</span>'
68
+ }
69
+ if (num !== undefined) {
70
+ return '<span class="n">' + num + '</span>'
71
+ }
72
+ return '<span class="b">' + kw + '</span>'
73
+ })
74
+ }
75
+
76
+
77
+ //kvRowsToPairs:請求建構器之 Query/Headers 表列([{ on, key, value }])→ 有序鍵值對 [[key, value], ...]。
78
+ //規則(spec/流程_測試API.md「規則摘要」):僅 on 且鍵非空之列納入;重複鍵各自保留一對、不收斂成物件
79
+ //(重複之 query 鍵須各自送達目標,如 ?id=1&id=2;重複之標頭由 proxy 依 Fetch 標準合併);值一律轉字串。
80
+ //why:改造前以 `obj[key] = value` 收斂,同鍵之後列靜默覆蓋前列,使用者看到兩列卻只送出一列。
81
+ function kvRowsToPairs(rows) {
82
+ let pairs = []
83
+ if (!isarr(rows)) {
84
+ return pairs
85
+ }
86
+ for (let row of rows) {
87
+ if (!isobj(row) || !row.on || typeof row.key !== 'string' || row.key === '') {
88
+ continue
89
+ }
90
+ let v = (row.value === null || row.value === undefined) ? '' : String(row.value)
91
+ pairs.push([row.key, v])
92
+ }
93
+ return pairs
94
+ }
95
+
96
+
97
+ export {
98
+ METHODS,
99
+ isJsonContainer,
100
+ dataToText,
101
+ methodToHttpVerb,
102
+ escapeHtml,
103
+ jsonHighlight,
104
+ kvRowsToPairs
105
+ }
@@ -30,6 +30,7 @@ let state = {
30
30
  email: '', //admin@example.com
31
31
  isAdmin: 'n',
32
32
  },
33
+ userSelf: {}, //UpdateUserSelf 寫入之使用者自身資訊;Vue 2 須於初始 state 宣告才具響應性(同 sso 之 userSelf: {})
33
34
 
34
35
  lang: 'eng', //'eng', 'cht'
35
36
  kpText: {}, //一定要放在vuex內, 否則無法用broadcast驅動變更語系文字
package/srv.mjs CHANGED
@@ -3,7 +3,6 @@ import WWebApi from './server/WWebApi.mjs'
3
3
  import getSettings from './g_getSettings.mjs'
4
4
  import get from 'lodash-es/get.js'
5
5
  import iseobj from 'wsemi/src/iseobj.mjs'
6
- import axios from 'axios'
7
6
  import JSON5 from 'json5'
8
7
  import fs from 'fs'
9
8
 
@@ -39,6 +38,7 @@ let opt = {
39
38
  //log 資料夾與輪檔間隔(srLog 寫入 + staEvent 統計皆讀此)。e2e 可用 genTempSettings({logFd}) 指向合成 log 達 hermetic。
40
39
  logFd: get(stApp, 'logFd', './logs'),
41
40
  logInterval: get(stApp, 'logInterval', 'hr'),
41
+ logNumKeep: get(stApp, 'logNumKeep', null), //null=未給, srLog 採 w-syslog 預設; 給非正整數則 srLog 啟動即 throw (fail-fast)
42
42
 
43
43
  webName: {
44
44
  'eng': 'API Service',
@@ -68,9 +68,14 @@ let getUserByToken = async (token) => {
68
68
  //呼叫 SSO 解析 token -> user
69
69
  try {
70
70
  let url = `${ssoBaseUrl}/api/getSsoUserInfor?token=${encodeURIComponent(ssoAppToken)}&key=token&value=${encodeURIComponent(token)}`
71
- let res = await axios.get(url)
72
- let state = get(res, 'data.state', '')
73
- let u = get(res, 'data.msg', null)
71
+ let res = await fetch(url) //Node 內建 fetch(改造前 axios);非 2xx 視同失敗
72
+ if (!res.ok) {
73
+ console.log('SSO getSsoUserInfor HTTP', res.status)
74
+ return {}
75
+ }
76
+ let data = await res.json()
77
+ let state = get(data, 'state', '')
78
+ let u = get(data, 'msg', null)
74
79
  if (state !== 'success' || !iseobj(u)) {
75
80
  console.log('SSO getSsoUserInfor 失敗', state)
76
81
  return {}
@@ -0,0 +1 @@
1
+ {"apis":"2026-09-13T00:43:23+08:00|NCZLhv"}