w-web-perm 1.0.48 → 1.0.50

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 (111) hide show
  1. package/.github/workflows/ci-test.yml +2 -2
  2. package/README.md +5 -5
  3. package/dist/css/app.8781355a.css +1 -0
  4. package/dist/css/{chunk-vendors.f149e124.css → chunk-vendors.58917286.css} +1 -1
  5. package/dist/index.tmp +2 -2
  6. package/dist/js/app-legacy.b2754bb1.js +1 -0
  7. package/dist/js/app.b2754bb1.js +1 -0
  8. package/dist/js/chunk-vendors-legacy.c14a1ed1.js +21 -0
  9. package/dist/js/chunk-vendors.c14a1ed1.js +21 -0
  10. package/dist/w-web-perm.umd.js +2 -2
  11. package/dist/w-web-perm.umd.js.map +1 -1
  12. package/docs/WWebPerm.html +3 -3
  13. package/docs/WWebPerm.mjs.html +162 -210
  14. package/docs/index.html +1 -1
  15. package/logs/2026-06-19T16.log +191 -0
  16. package/logs/2026-06-19T23.log +0 -0
  17. package/logs/2026-06-20T00.log +207 -0
  18. package/logs/2026-06-20T11.log +221 -0
  19. package/logs/2026-06-20T12.log +146 -0
  20. package/logs/2026-06-20T13.log +134 -0
  21. package/logs/2026-06-20T14.log +296 -0
  22. package/logs/2026-06-20T15.log +225 -0
  23. package/logs/2026-06-20T16.log +174 -0
  24. package/logs/2026-06-21T16.log +110 -0
  25. package/logs/2026-06-21T17.log +301 -0
  26. package/logs/2026-06-21T18.log +238 -0
  27. package/logs/2026-06-21T19.log +141 -0
  28. package/logs/2026-06-21T20.log +99 -0
  29. package/logs/2026-06-21T21.log +143 -0
  30. package/logs/2026-06-21T23.log +26 -0
  31. package/logs/2026-06-22T00.log +33 -0
  32. package/logs/2026-06-22T08.log +235 -0
  33. package/logs/2026-06-22T09.log +246 -0
  34. package/logs/2026-06-22T10.log +301 -0
  35. package/logs/2026-06-22T11.log +140 -0
  36. package/logs/2026-06-22T12.log +22 -0
  37. package/logs/2026-06-22T14.log +44 -0
  38. package/logs/2026-06-22T23.log +951 -0
  39. package/logs/2026-06-23T00.log +263 -0
  40. package/logs/2026-06-23T08.log +611 -0
  41. package/logs/2026-06-30T21.log +3609 -0
  42. package/logs/2026-06-30T22.log +3682 -0
  43. package/logs/2026-06-30T23.log +592 -0
  44. package/logs/2026-07-01T12.log +0 -0
  45. package/logs/2026-07-09T22.log +2220 -0
  46. package/logs/2026-07-09T23.log +1140 -0
  47. package/logs/2026-07-10T12.log +6 -0
  48. package/logs/2026-07-10T13.log +351 -0
  49. package/logs/2026-07-10T16.log +1272 -0
  50. package/logs/2026-07-10T19.log +6 -0
  51. package/logs/2026-07-10T20.log +1883 -0
  52. package/logs/2026-07-10T21.log +3505 -0
  53. package/logs/2026-07-10T22.log +3395 -0
  54. package/logs/2026-07-11T00.log +1338 -0
  55. package/logs/2026-07-11T01.log +2492 -0
  56. package/logs/2026-07-11T17.log +19 -0
  57. package/package.json +23 -16
  58. package/public/index.html +2 -2
  59. package/server/WWebPerm.mjs +161 -209
  60. package/server/procCore.mjs +187 -0
  61. package/server/procLang.mjs +276 -60
  62. package/server/procStaInfor.mjs +128 -0
  63. package/server/provideTabs.mjs +1 -1
  64. package/server/srLog.mjs +30 -0
  65. package/server/staLogs/filterVpfsByWindow.mjs +27 -0
  66. package/server/staLogs/staEvent.callWorker.mjs +46 -0
  67. package/server/staLogs/staEvent.mjs +118 -0
  68. package/server/staLogs/staEvent.shellWorker.mjs +19 -0
  69. package/server/staLogs/staEventTable.mjs +102 -0
  70. package/settings.json +43 -0
  71. package/src/components/Common/CheckYes.vue +18 -1
  72. package/src/components/Layout.vue +1 -1
  73. package/src/components/LayoutContent.vue +13 -3
  74. package/src/components/LayoutContentGrups.vue +32 -25
  75. package/src/components/LayoutContentPemis.vue +32 -25
  76. package/src/components/LayoutContentStaInfor.vue +534 -0
  77. package/src/components/LayoutContentTargets.vue +35 -28
  78. package/src/components/LayoutContentUsers.vue +32 -25
  79. package/src/components/LayoutState.vue +5 -0
  80. package/src/components/VeCgrups.vue +19 -9
  81. package/src/components/VeCpemis.vue +19 -9
  82. package/src/components/VeCrules.vue +19 -9
  83. package/src/components/VeGrupBlngUsers.vue +35 -29
  84. package/src/components/VePemiBlngGrups.vue +35 -29
  85. package/src/getPerm.mjs +7 -17
  86. package/src/getPermUserInfor.mjs +8 -18
  87. package/src/main.js +4 -9
  88. package/src/perm.mjs +3 -4
  89. package/src/plugins/mShare.mjs +8 -13
  90. package/src/plugins/mUI.mjs +7 -44
  91. package/src/store/mutations.mjs +0 -10
  92. package/src/store/types.mjs +0 -2
  93. package/srv.mjs +76 -101
  94. package/tableTags.json +1 -1
  95. package/z/344/277/256/346/255/243/350/246/217/345/212/203/346/233/270.md +450 -0
  96. package/db/wperm/grups/data.mdb +0 -0
  97. package/db/wperm/grups/lock.mdb +0 -0
  98. package/db/wperm/pemis/data.mdb +0 -0
  99. package/db/wperm/pemis/lock.mdb +0 -0
  100. package/db/wperm/targets/data.mdb +0 -0
  101. package/db/wperm/targets/lock.mdb +0 -0
  102. package/db/wperm/users/data.mdb +0 -0
  103. package/db/wperm/users/lock.mdb +0 -0
  104. package/dist/css/app.30e81bfe.css +0 -1
  105. package/dist/js/app-legacy.3324392e.js +0 -1
  106. package/dist/js/app.3324392e.js +0 -1
  107. package/dist/js/chunk-vendors-legacy.e8703728.js +0 -21
  108. package/dist/js/chunk-vendors.e8703728.js +0 -21
  109. package/src/plugins/mDataSelectorSchema.mjs +0 -30
  110. package/tableTags-web-perm.json +0 -1
  111. package/test/getUserRules.test.mjs +0 -899
@@ -0,0 +1,534 @@
1
+ <template>
2
+ <div
3
+ style="height:100%;"
4
+ v-domresize
5
+ @domresize="resizePanel"
6
+ :changeLang="changeLang"
7
+ >
8
+
9
+ <div
10
+ style="height:100%; background:#f9fafb; overflow-y:auto;"
11
+ v-if="!firstLoading && !errMsg"
12
+ >
13
+
14
+ <div style="padding:20px 30px;">
15
+
16
+ <!-- 頁面標題 -->
17
+ <div class="mb-8">
18
+ <div style="font-size:1.5rem; font-weight:600;">{{$t('mmStaInfor')}}</div>
19
+ </div>
20
+
21
+ <div class="space-y-8">
22
+
23
+ <!-- 事件發生頻率區塊 -->
24
+ <div>
25
+ <div class="bg-white p-5 rounded-lg shadow-sm border border-gray-200">
26
+ <div class="pb-2 flex items-center font-semibold text-gray-700">
27
+ <WIcon :icon="mdiChartBoxOutline" :size="24" :color="'currentColor'" :colorHover="'currentColor'" class="text-purple-600 mr-2"></WIcon>
28
+ <span class="text-lg">{{$t('staEventTitle')}}</span>
29
+ </div>
30
+ <!-- 控制區: 事件多選 checklist + 全選/清除 + 全部加總 + 時間分組 -->
31
+ <div class="staCtrl">
32
+
33
+ <!-- 事件多選 checklist -->
34
+ <div class="staEventSel" v-if="allEvents.length > 0">
35
+ <span class="text-sm font-medium text-gray-700 mr-2">{{$t('staSelectEvents')}}</span>
36
+ <div class="staEventList">
37
+ <label
38
+ v-for="ev in allEvents"
39
+ :key="ev"
40
+ class="staEventItem"
41
+ >
42
+ <input type="checkbox" :value="ev" v-model="selectedEvents" @change="updateChartsDebounce" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500">
43
+ <span class="ml-1 text-sm text-gray-900">{{ev}}</span>
44
+ </label>
45
+ </div>
46
+ <div class="staEventBtns">
47
+ <button type="button" @click="selectAllEvents" class="staBtn">{{$t('staSelectAll')}}</button>
48
+ <button type="button" @click="clearEvents" class="staBtn">{{$t('staClear')}}</button>
49
+ </div>
50
+ </div>
51
+
52
+ <!-- 全部加總 + 時間分組 -->
53
+ <div class="staCtrlRight">
54
+ <div class="flex items-center">
55
+ <input id="staShowTotal" type="checkbox" v-model="showTotal" @change="updateChartsDebounce" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500">
56
+ <label for="staShowTotal" class="ml-2 text-sm font-medium text-gray-900">{{$t('staTotal')}}</label>
57
+ </div>
58
+ <select v-model="timeInterval" @change="changeTimeInterval" class="border rounded px-2 py-1 text-sm">
59
+ <option value="hr">{{$t('staTimeHr')}}</option>
60
+ <option value="day">{{$t('staTimeDay')}}</option>
61
+ </select>
62
+ </div>
63
+
64
+ </div>
65
+
66
+ <WEchartsVueDyn
67
+ style="width:100%; height:300px;"
68
+ :options="optEvent"
69
+ v-if="optEvent"
70
+ ></WEchartsVueDyn>
71
+
72
+ <div
73
+ style="padding:0px; font-size:0.8rem; color:#777;"
74
+ v-else
75
+ >
76
+ {{$t('staNoData')}}
77
+ </div>
78
+
79
+ </div>
80
+ </div>
81
+
82
+ <!-- 事件統計表區塊 -->
83
+ <div>
84
+ <div class="bg-white p-5 rounded-lg shadow-sm border border-gray-200">
85
+ <div class="pb-2 flex items-center font-semibold text-gray-700">
86
+ <WIcon :icon="mdiTableLarge" :size="24" :color="'currentColor'" :colorHover="'currentColor'" class="text-purple-600 mr-2"></WIcon>
87
+ <span class="text-lg">{{$t('staTableTitle')}}</span>
88
+ </div>
89
+
90
+ <div v-if="eventTable.length > 0" class="overflow-x-auto relative shadow-md mt-2">
91
+ <table class="w-full text-sm text-left text-gray-500 border-collapse">
92
+ <thead class="text-xs text-gray-700 bg-gray-50">
93
+ <tr>
94
+ <th scope="col" class="py-3 px-6 border border-solid border-gray-200">{{$t('staColEvent')}}</th>
95
+ <th scope="col" class="py-3 px-6 border border-solid border-gray-200">{{$t('staColLast1Day')}}</th>
96
+ <th scope="col" class="py-3 px-6 border border-solid border-gray-200">{{$t('staColLast8Hour')}}</th>
97
+ <th scope="col" class="py-3 px-6 border border-solid border-gray-200">{{$t('staColLast4Hour')}}</th>
98
+ <th scope="col" class="py-3 px-6 border border-solid border-gray-200">{{$t('staColLast1Hour')}}</th>
99
+ </tr>
100
+ </thead>
101
+ <tbody>
102
+ <tr v-for="(row, index) in displayEventTable" :key="index" class="hover:bg-gray-50 transition-colors">
103
+ <td class="py-4 px-6 border border-solid border-gray-200">{{ row.event }}</td>
104
+ <td class="py-4 px-6 border border-solid border-gray-200">{{ formatNumber(row.last1Day) }}</td>
105
+ <td class="py-4 px-6 border border-solid border-gray-200">{{ formatNumber(row.last8Hour) }}</td>
106
+ <td class="py-4 px-6 border border-solid border-gray-200">{{ formatNumber(row.last4Hour) }}</td>
107
+ <td class="py-4 px-6 border border-solid border-gray-200">{{ formatNumber(row.last1Hour) }}</td>
108
+ </tr>
109
+ </tbody>
110
+ </table>
111
+ </div>
112
+
113
+ <div
114
+ style="padding:0px; font-size:0.8rem; color:#777;"
115
+ v-else
116
+ >
117
+ {{$t('staNoData')}}
118
+ </div>
119
+
120
+ </div>
121
+ </div>
122
+
123
+ </div>
124
+
125
+ </div>
126
+
127
+ </div>
128
+ <template v-else>
129
+ <div
130
+ style="padding:10px 15px; font-size:0.8rem;"
131
+ v-if="firstLoading"
132
+ >
133
+ {{$t('waitingData')}}
134
+ </div>
135
+ <div
136
+ style="padding:10px 15px; font-size:0.8rem;"
137
+ v-if="errMsg"
138
+ >
139
+ {{errMsg}}
140
+ </div>
141
+ </template>
142
+
143
+ </div>
144
+ </template>
145
+
146
+ <script>
147
+ import ot from 'dayjs'
148
+ import get from 'lodash-es/get.js'
149
+ import map from 'lodash-es/map.js'
150
+ import size from 'lodash-es/size.js'
151
+ import keys from 'lodash-es/keys.js'
152
+ import union from 'lodash-es/union.js'
153
+ import omit from 'lodash-es/omit.js'
154
+ import sortBy from 'lodash-es/sortBy.js'
155
+ import reverse from 'lodash-es/reverse.js'
156
+ import debounce from 'wsemi/src/debounce.mjs'
157
+ import isearr from 'wsemi/src/isearr.mjs'
158
+ import WEchartsVueDyn from 'w-component-vue/src/components/WEchartsVueDyn.vue'
159
+ import { mdiChartBoxOutline, mdiTableLarge } from '@mdi/js/mdi.js'
160
+ import WIcon from 'w-component-vue/src/components/WIcon.vue'
161
+
162
+
163
+ export default {
164
+ components: {
165
+ WEchartsVueDyn,
166
+ WIcon,
167
+ },
168
+ props: {
169
+ drawer: {
170
+ type: Boolean,
171
+ default: false,
172
+ },
173
+ },
174
+ data: function() {
175
+ return {
176
+
177
+ mdiChartBoxOutline,
178
+ mdiTableLarge,
179
+
180
+ dbc: debounce(300),
181
+
182
+ panelHeight: 100,
183
+
184
+ firstLoading: true,
185
+ errMsg: '',
186
+
187
+ timeLength: 7,
188
+ timeInterval: 'hr',
189
+
190
+ selectedEvents: [],
191
+ showTotal: false,
192
+
193
+ freqEvent: [],
194
+
195
+ optEvent: null,
196
+
197
+ eventTable: [],
198
+
199
+ }
200
+ },
201
+ mounted: function() {
202
+ // console.log('mounted')
203
+
204
+ let vo = this
205
+
206
+ //firstLoading, errMsg
207
+ vo.firstLoading = true
208
+ vo.errMsg = ''
209
+
210
+ //getAndRelaData
211
+ vo.getAndRelaData()
212
+ .catch((err) => {
213
+ console.log(err)
214
+ vo.errMsg = vo.$t('getDataError')
215
+ })
216
+ .finally(() => {
217
+ vo.firstLoading = false
218
+ })
219
+
220
+ },
221
+ computed: {
222
+
223
+ lang: function() {
224
+ let vo = this
225
+ return get(vo, `$store.state.lang`, '')
226
+ },
227
+
228
+ changeLang: function() {
229
+ let vo = this
230
+ let lang = vo.lang
231
+ vo.updateChartsDebounce(lang)
232
+ return ''
233
+ },
234
+
235
+ //allEvents: 從 freqEvent 各桶 data 取 event 名 union (排除 count), 排序
236
+ allEvents: function() {
237
+ let vo = this
238
+ let evs = []
239
+ map(vo.freqEvent, (entry) => {
240
+ let evNames = keys(omit(get(entry, 'data', {}), 'count'))
241
+ evs = union(evs, evNames)
242
+ })
243
+ return evs.sort()
244
+ },
245
+
246
+ //displayEventTable: 後端已依 last1Day 降序, 前端保險再 sortBy last1Day desc
247
+ displayEventTable: function() {
248
+ let vo = this
249
+ return reverse(sortBy(vo.eventTable.slice(), 'last1Day'))
250
+ },
251
+
252
+ },
253
+ methods: {
254
+
255
+ resizePanel: function(msg) {
256
+ // console.log('methods resizePanel', msg)
257
+
258
+ let vo = this
259
+
260
+ //panelWidth, panelHeight
261
+ vo.panelHeight = msg.snew.offsetHeight
262
+
263
+ },
264
+
265
+ genOpt: function(title, arr, keysPick) {
266
+ let vo = this
267
+
268
+ //check
269
+ if (size(arr) === 0) {
270
+ return null
271
+ }
272
+
273
+ //keysPick to array
274
+ if (!isearr(keysPick)) {
275
+ keysPick = [keysPick]
276
+ }
277
+
278
+ //colors
279
+ let colors = [
280
+ '#3b82f6', // blue-500
281
+ '#ef4444', // red-500
282
+ '#10b981', // emerald-500
283
+ '#f97316', // orange-500
284
+ '#8b5cf6', // violet-500
285
+ '#6366f1', // indigo-500
286
+ '#eab308', // yellow-500
287
+ '#ec4899', // pink-500
288
+ '#14b8a6', // teal-500
289
+ '#a855f7', // purple-500
290
+ ]
291
+ function getColor(k) {
292
+ return colors[k % colors.length]
293
+ }
294
+
295
+ //getName: count 系列顯示為「全部」, 其餘 event 名照原字串
296
+ let getName = (valueKey) => {
297
+ if (valueKey === 'count') {
298
+ return vo.$t('staTotal')
299
+ }
300
+ return valueKey
301
+ }
302
+
303
+ //series: 折線圖, 每個 keysPick 一條系列, 缺值補 0
304
+ let series = map(keysPick, (valueKey, k) => {
305
+ let name = getName(valueKey)
306
+ let color = getColor(k)
307
+ return {
308
+ name,
309
+ type: 'line',
310
+ smooth: true,
311
+ data: map(arr, item => get(item, `data.${valueKey}`, 0)),
312
+ itemStyle: {
313
+ color,
314
+ },
315
+ emphasis: {
316
+ focus: 'series'
317
+ },
318
+ }
319
+ })
320
+
321
+ //formatX: hr 顯示 MM/DD HH:mm, day 顯示 MM/DD
322
+ let formatX = 'MM/DD'
323
+ if (vo.timeInterval === 'hr') {
324
+ formatX = 'MM/DD\nHH:mm'
325
+ }
326
+
327
+ //opt
328
+ let opt = {
329
+ title: {
330
+ text: title,
331
+ show: false, // 不顯示標題,由組件的HTML提供
332
+ },
333
+ tooltip: {
334
+ trigger: 'axis',
335
+ },
336
+ grid: {
337
+ left: '3%',
338
+ right: '4%',
339
+ bottom: '3%',
340
+ containLabel: true
341
+ },
342
+ legend: {
343
+ show: true,
344
+ },
345
+ xAxis: [
346
+ {
347
+ type: 'category',
348
+ boundaryGap: false,
349
+ data: map(arr, item => ot(item.time).format(formatX)),
350
+ axisTick: {
351
+ alignWithLabel: true
352
+ }
353
+ }
354
+ ],
355
+ yAxis: [
356
+ {
357
+ type: 'value'
358
+ }
359
+ ],
360
+ series,
361
+ }
362
+ return opt
363
+ },
364
+
365
+ grStaEvent: async function() {
366
+ let vo = this
367
+
368
+ //getStaEvent
369
+ await vo.$fapi.getStaEvent(vo.timeLength, vo.timeInterval)
370
+ .then((res) => {
371
+ // console.log('getStaEvent(freqEvent)', res)
372
+ vo.freqEvent = res
373
+ //首次資料載入後 selectedEvents 為空 → 預設全選
374
+ if (size(vo.selectedEvents) === 0) {
375
+ vo.selectedEvents = vo.allEvents.slice()
376
+ }
377
+ vo.updateCharts()
378
+ })
379
+ .catch((err) => {
380
+ console.log('getStaEvent catch', err)
381
+ throw err
382
+ })
383
+
384
+ },
385
+
386
+ grStaEventTable: async function() {
387
+ let vo = this
388
+
389
+ //getStaEventTable
390
+ await vo.$fapi.getStaEventTable()
391
+ .then((res) => {
392
+ // console.log('getStaEventTable(eventTable)', res)
393
+ vo.eventTable = res
394
+ })
395
+ .catch((err) => {
396
+ console.log('getStaEventTable catch', err)
397
+ throw err
398
+ })
399
+
400
+ },
401
+
402
+ getAndRelaData: async function() {
403
+ let vo = this
404
+
405
+ //grStaEvent
406
+ await vo.grStaEvent()
407
+
408
+ //grStaEventTable
409
+ await vo.grStaEventTable()
410
+
411
+ },
412
+
413
+ updateCharts: function() {
414
+ let vo = this
415
+
416
+ //ksEvent: showTotal 時前置 count 加總線, 其後接所選各 event (各一條系列)
417
+ let ksEvent = [
418
+ ...(vo.showTotal ? ['count'] : []),
419
+ ...vo.selectedEvents,
420
+ ]
421
+
422
+ //optEvent
423
+ vo.optEvent = vo.genOpt(vo.$t('staEventTitle'), vo.freqEvent, ksEvent)
424
+
425
+ },
426
+
427
+ updateChartsDebounce: function() {
428
+ let vo = this
429
+ vo.dbc(() => {
430
+ vo.updateCharts()
431
+ })
432
+ },
433
+
434
+ selectAllEvents: function() {
435
+ let vo = this
436
+ vo.selectedEvents = vo.allEvents.slice()
437
+ vo.updateChartsDebounce()
438
+ },
439
+
440
+ clearEvents: function() {
441
+ let vo = this
442
+ vo.selectedEvents = []
443
+ vo.updateChartsDebounce()
444
+ },
445
+
446
+ formatNumber: function(num) {
447
+ if (num === null || num === undefined) {
448
+ return 0
449
+ }
450
+ return num.toLocaleString()
451
+ },
452
+
453
+ changeTimeInterval: function() {
454
+ let vo = this
455
+
456
+ //切換時間分組須重新向後端取數據 (hr/day 對應不同 time 粒度)
457
+ vo.firstLoading = true
458
+ vo.errMsg = ''
459
+ vo.getAndRelaData()
460
+ .catch((err) => {
461
+ console.log(err)
462
+ vo.errMsg = vo.$t('getDataError')
463
+ })
464
+ .finally(() => {
465
+ vo.firstLoading = false
466
+ })
467
+
468
+ },
469
+
470
+ }
471
+ }
472
+ </script>
473
+
474
+ <style scoped>
475
+ /* === Tailwind utility (非響應式) 同名定義: LayoutContentStaInfor; 由實際 Tailwind v3 產生規則抽出, 與 CDN 逐字一致 === */
476
+ .bg-gray-100 { --tw-bg-opacity: 1; background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); }
477
+ .bg-gray-50 { --tw-bg-opacity: 1; background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); }
478
+ .bg-white { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); }
479
+ .border { border-width: 1px; }
480
+ .border-collapse { border-collapse: collapse; }
481
+ .border-solid { border-style: solid; }
482
+ .border-gray-200 { --tw-border-opacity: 1; border-color: rgb(229 231 235 / var(--tw-border-opacity, 1)); }
483
+ .border-gray-300 { --tw-border-opacity: 1; border-color: rgb(209 213 219 / var(--tw-border-opacity, 1)); }
484
+ .flex { display: flex; }
485
+ .focus\:ring-blue-500:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1)); }
486
+ .font-medium { font-weight: 500; }
487
+ .font-semibold { font-weight: 600; }
488
+ .h-4 { height: 1rem; }
489
+ .hover\:bg-gray-50:hover { --tw-bg-opacity: 1; background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); }
490
+ .items-center { align-items: center; }
491
+ .justify-end { justify-content: flex-end; }
492
+ .mb-8 { margin-bottom: 2rem; }
493
+ .ml-2 { margin-left: 0.5rem; }
494
+ .mr-2 { margin-right: 0.5rem; }
495
+ .mt-2 { margin-top: 0.5rem; }
496
+ .overflow-x-auto { overflow-x: auto; }
497
+ .p-5 { padding: 1.25rem; }
498
+ .pb-2 { padding-bottom: 0.5rem; }
499
+ .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
500
+ .px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
501
+ .py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
502
+ .py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
503
+ .py-4 { padding-top: 1rem; padding-bottom: 1rem; }
504
+ .relative { position: relative; }
505
+ .rounded { border-radius: 0.25rem; }
506
+ .rounded-lg { border-radius: 0.5rem; }
507
+ .shadow-md { --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); }
508
+ .space-x-4 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(1rem * var(--tw-space-x-reverse)); margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); }
509
+ .space-y-8 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse))); margin-bottom: calc(2rem * var(--tw-space-y-reverse)); }
510
+ .text-blue-600 { --tw-text-opacity: 1; color: rgb(37 99 235 / var(--tw-text-opacity, 1)); }
511
+ .text-gray-500 { --tw-text-opacity: 1; color: rgb(107 114 128 / var(--tw-text-opacity, 1)); }
512
+ .text-gray-700 { --tw-text-opacity: 1; color: rgb(55 65 81 / var(--tw-text-opacity, 1)); }
513
+ .text-gray-900 { --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity, 1)); }
514
+ .text-left { text-align: left; }
515
+ .text-lg { font-size: 1.125rem; line-height: 1.75rem; }
516
+ .text-purple-600 { --tw-text-opacity: 1; color: rgb(147 51 234 / var(--tw-text-opacity, 1)); }
517
+ .text-sm { font-size: 0.875rem; line-height: 1.25rem; }
518
+ .text-xs { font-size: 0.75rem; line-height: 1rem; }
519
+ .transition-colors { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, -webkit-text-decoration-color; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
520
+ .w-4 { width: 1rem; }
521
+ .w-full { width: 100%; }
522
+
523
+ /* === 控制區 (事件多選 checklist + 全部加總 + 時間分組), RWD flex-wrap === */
524
+ .staCtrl { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: flex-start; gap: 12px; padding: 8px 0; }
525
+ .staEventSel { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; flex: 1 1 auto; min-width: 0; }
526
+ .staEventList { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; }
527
+ .staEventItem { display: inline-flex; align-items: center; cursor: pointer; }
528
+ .staEventBtns { display: inline-flex; align-items: center; gap: 6px; }
529
+ .staBtn { font-size: 0.8rem; color: rgb(37 99 235); border: 1px solid rgb(209 213 219); border-radius: 0.25rem; padding: 2px 8px; background: #fff; cursor: pointer; }
530
+ .staBtn:hover { background: rgb(243 244 246); }
531
+ .staCtrlRight { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; }
532
+ .mr-2 { margin-right: 0.5rem; }
533
+ .ml-1 { margin-left: 0.25rem; }
534
+ </style>
@@ -39,6 +39,7 @@
39
39
 
40
40
  <!-- 功能區 -->
41
41
  <div
42
+ data-fmid="targets-toolbar"
42
43
  style="padding:5px; border-top:1px solid #ddd; display:flex; align-items:center;"
43
44
  _v-if="showIsEditable || isEditable"
44
45
  >
@@ -185,6 +186,7 @@
185
186
  :iconColor="'#eee'"
186
187
  :iconColorHover="'#fff'"
187
188
  :shadow="false"
189
+ :promiseUnlock="true"
188
190
  @click="saveTargets"
189
191
  ></WButtonCircle>
190
192
 
@@ -282,7 +284,6 @@ export default {
282
284
  mdiPencilOutline,
283
285
  mdiContentCopy,
284
286
 
285
- panelWidth: 100,
286
287
  panelHeight: 100,
287
288
  headHeight: 100,
288
289
 
@@ -487,9 +488,9 @@ export default {
487
488
 
488
489
  let c = ''
489
490
  let b = false
490
- b = iseobj(vo.errItemsByName)
491
+ b = iseobj(vo.errItemsById)
491
492
  if (b) {
492
- c = vo.$t('errInNames')
493
+ c = vo.$t('errInIds')
493
494
  }
494
495
 
495
496
  return c
@@ -520,7 +521,6 @@ export default {
520
521
  let vo = this
521
522
 
522
523
  //panelWidth, panelHeight
523
- vo.panelWidth = msg.snew.offsetWidth
524
524
  vo.panelHeight = msg.snew.offsetHeight
525
525
 
526
526
  },
@@ -608,7 +608,7 @@ export default {
608
608
  defHeadMinWidth: 150,
609
609
  kpHeadWidth: {
610
610
  'id': isnum(vo.widthTargetId) ? cdbl(vo.widthTargetId) : 600,
611
- 'description': isnum(vo.widthTargetId) ? cdbl(vo.widthTargetId) : 600,
611
+ 'description': isnum(vo.widthTargetDescription) ? cdbl(vo.widthTargetDescription) : 600,
612
612
  'timeCreate': 220,
613
613
  'timeUpdate': 220,
614
614
  },
@@ -822,20 +822,28 @@ export default {
822
822
 
823
823
  },
824
824
 
825
- saveTargets: function() {
826
- // console.log('method saveTargets')
825
+ saveTargets: function(msg) {
827
826
 
828
827
  let vo = this
829
828
 
830
- async function core() {
831
- let errTemp = null
829
+ //第一行立刻釋放按鈕視覺鎖
830
+ msg.pm.resolve()
832
831
 
833
- //show loading
834
- vo.$ui.updateLoading(true)
832
+ //fire-and-forget, 不 await
833
+ vo.doSaveTargets()
835
834
 
836
- //check
835
+ },
836
+
837
+ doSaveTargets: function() {
838
+ // console.log('method doSaveTargets')
839
+
840
+ let vo = this
841
+
842
+ async function core() {
843
+
844
+ //1) 同步檢測 (在開 loading 之前)
837
845
  if (isestr(vo.isError)) {
838
- vo.$alert(`${vo.isError}`, { type: 'error' })
846
+ await vo.$dg.showCheckYes(`${vo.isError}`)
839
847
  return
840
848
  }
841
849
 
@@ -844,35 +852,34 @@ export default {
844
852
 
845
853
  //check
846
854
  if (size(rows) === 0) {
847
- vo.$alert(`${vo.$t('targetAddEmpty')}`, { type: 'error' })
855
+ await vo.$dg.showCheckYes(`${vo.$t('targetAddEmpty')}`)
848
856
  return
849
857
  }
850
858
 
851
- //updateTargets
859
+ //2) 確定打 API 才開 loading
860
+ vo.$ui.updateLoading(true)
861
+
862
+ //3) updateTargets, 各自 catch + 旗標短路
863
+ let okSave = false
852
864
  await vo.$fapi.updateTargets(rows)
853
- .catch((err) => {
854
- errTemp = err
865
+ .then(() => { okSave = true })
866
+ .catch(async (err) => {
867
+ vo.$ui.updateLoading(false) //showCheckYes 前關 loading(modal 阻斷期間避免 loading 疊在底下)
868
+ await vo.$dg.showCheckYes(`${vo.$t('targetSaveTargetsFail')}: ${vo.$tErr(err)}`)
855
869
  })
856
-
857
- //check
858
- if (errTemp !== null) {
859
- vo.$alert(`${vo.$t('targetSaveTargetsFail')}: ${errTemp}`, { type: 'error' })
860
- return
861
- }
870
+ if (!okSave) return
862
871
 
863
872
  //isModified
864
873
  vo.isModified = false
865
874
 
866
- //alert
867
- vo.$alert(vo.$t('targetSaveTargetsSuccess'))
875
+ //alert(showCheckYes 前關 loading,避免 modal 等待期間 loading 疊在底下)
876
+ vo.$ui.updateLoading(false)
877
+ await vo.$dg.showCheckYes(vo.$t('targetSaveTargetsSuccess'), { type: 'success' })
868
878
 
869
879
  }
870
880
 
871
881
  //core
872
882
  core()
873
- // .then((res) => {
874
- // console.log('then', res)
875
- // })
876
883
  .catch((err) => {
877
884
  console.log('catch', err)
878
885
  vo.$alert(vo.$t('anUnexpectedErrorOccurred'), { type: 'error' })