w-web-perm 1.0.48 → 1.0.49

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 (95) hide show
  1. package/.github/workflows/ci-test.yml +2 -2
  2. package/_gh-pages-deploy/.nojekyll +0 -0
  3. package/_gh-pages-deploy/WWebPerm.html +938 -0
  4. package/_gh-pages-deploy/WWebPerm.mjs.html +1228 -0
  5. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  6. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  7. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  8. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  9. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  10. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  11. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  12. package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  13. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  14. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  15. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  16. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  17. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  18. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  19. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  20. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  21. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  22. package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  23. package/_gh-pages-deploy/index.html +84 -0
  24. package/_gh-pages-deploy/scripts/collapse.js +39 -0
  25. package/_gh-pages-deploy/scripts/commonNav.js +28 -0
  26. package/_gh-pages-deploy/scripts/linenumber.js +25 -0
  27. package/_gh-pages-deploy/scripts/nav.js +12 -0
  28. package/_gh-pages-deploy/scripts/polyfill.js +4 -0
  29. package/_gh-pages-deploy/scripts/prettify/Apache-License-2.0.txt +202 -0
  30. package/_gh-pages-deploy/scripts/prettify/lang-css.js +2 -0
  31. package/_gh-pages-deploy/scripts/prettify/prettify.js +28 -0
  32. package/_gh-pages-deploy/scripts/search.js +99 -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.d8b16216.css +1 -0
  36. package/dist/css/{chunk-vendors.f149e124.css → chunk-vendors.2d3ef2ba.css} +1 -1
  37. package/dist/index.tmp +2 -2
  38. package/dist/js/app-legacy.fb48310a.js +1 -0
  39. package/dist/js/app.fb48310a.js +1 -0
  40. package/dist/js/chunk-vendors-legacy.8fdd4435.js +21 -0
  41. package/dist/js/chunk-vendors.8fdd4435.js +21 -0
  42. package/dist/w-web-perm.umd.js +2 -2
  43. package/dist/w-web-perm.umd.js.map +1 -1
  44. package/docs/WWebPerm.html +2 -2
  45. package/docs/WWebPerm.mjs.html +138 -199
  46. package/docs/index.html +1 -1
  47. package/package.json +15 -11
  48. package/public/index.html +2 -2
  49. package/server/WWebPerm.mjs +137 -198
  50. package/server/procCore.mjs +169 -0
  51. package/server/procLang.mjs +197 -0
  52. package/server/procStaInfor.mjs +118 -0
  53. package/server/provideTabs.mjs +1 -1
  54. package/server/srLog.mjs +30 -0
  55. package/server/staLogs/staEvent.callWorker.mjs +46 -0
  56. package/server/staLogs/staEvent.mjs +116 -0
  57. package/server/staLogs/staEvent.shellWorker.mjs +19 -0
  58. package/server/staLogs/staEventTable.mjs +100 -0
  59. package/server/staLogs/test_staEvent.mjs +22 -0
  60. package/settings.json +40 -0
  61. package/src/components/Common/CheckYes.vue +18 -1
  62. package/src/components/Layout.vue +1 -1
  63. package/src/components/LayoutContent.vue +13 -1
  64. package/src/components/LayoutContentGrups.vue +32 -23
  65. package/src/components/LayoutContentPemis.vue +32 -23
  66. package/src/components/LayoutContentStaInfor.vue +536 -0
  67. package/src/components/LayoutContentTargets.vue +32 -23
  68. package/src/components/LayoutContentUsers.vue +32 -23
  69. package/src/components/LayoutState.vue +5 -0
  70. package/src/components/VeCgrups.vue +12 -3
  71. package/src/components/VeCpemis.vue +12 -3
  72. package/src/components/VeCrules.vue +12 -3
  73. package/src/components/VeGrupBlngUsers.vue +18 -6
  74. package/src/components/VePemiBlngGrups.vue +18 -6
  75. package/src/getPerm.mjs +7 -17
  76. package/src/getPermUserInfor.mjs +8 -18
  77. package/src/main.js +1 -0
  78. package/src/plugins/mShare.mjs +8 -13
  79. package/src/plugins/mUI.mjs +11 -0
  80. package/srv.mjs +76 -101
  81. package/tableTags.json +1 -1
  82. package/db/wperm/grups/data.mdb +0 -0
  83. package/db/wperm/grups/lock.mdb +0 -0
  84. package/db/wperm/pemis/data.mdb +0 -0
  85. package/db/wperm/pemis/lock.mdb +0 -0
  86. package/db/wperm/targets/data.mdb +0 -0
  87. package/db/wperm/targets/lock.mdb +0 -0
  88. package/db/wperm/users/data.mdb +0 -0
  89. package/db/wperm/users/lock.mdb +0 -0
  90. package/dist/css/app.30e81bfe.css +0 -1
  91. package/dist/js/app-legacy.3324392e.js +0 -1
  92. package/dist/js/app.3324392e.js +0 -1
  93. package/dist/js/chunk-vendors-legacy.e8703728.js +0 -21
  94. package/dist/js/chunk-vendors.e8703728.js +0 -21
  95. package/test/getUserRules.test.mjs +0 -899
@@ -0,0 +1,536 @@
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
+ panelWidth: 100,
183
+ panelHeight: 100,
184
+
185
+ firstLoading: true,
186
+ errMsg: '',
187
+
188
+ timeLength: 7,
189
+ timeInterval: 'hr',
190
+
191
+ selectedEvents: [],
192
+ showTotal: false,
193
+
194
+ freqEvent: [],
195
+
196
+ optEvent: null,
197
+
198
+ eventTable: [],
199
+
200
+ }
201
+ },
202
+ mounted: function() {
203
+ // console.log('mounted')
204
+
205
+ let vo = this
206
+
207
+ //firstLoading, errMsg
208
+ vo.firstLoading = true
209
+ vo.errMsg = ''
210
+
211
+ //getAndRelaData
212
+ vo.getAndRelaData()
213
+ .catch((err) => {
214
+ console.log(err)
215
+ vo.errMsg = vo.$t('getDataError')
216
+ })
217
+ .finally(() => {
218
+ vo.firstLoading = false
219
+ })
220
+
221
+ },
222
+ computed: {
223
+
224
+ lang: function() {
225
+ let vo = this
226
+ return get(vo, `$store.state.lang`, '')
227
+ },
228
+
229
+ changeLang: function() {
230
+ let vo = this
231
+ let lang = vo.lang
232
+ vo.updateChartsDebounce(lang)
233
+ return ''
234
+ },
235
+
236
+ //allEvents: 從 freqEvent 各桶 data 取 event 名 union (排除 count), 排序
237
+ allEvents: function() {
238
+ let vo = this
239
+ let evs = []
240
+ map(vo.freqEvent, (entry) => {
241
+ let evNames = keys(omit(get(entry, 'data', {}), 'count'))
242
+ evs = union(evs, evNames)
243
+ })
244
+ return evs.sort()
245
+ },
246
+
247
+ //displayEventTable: 後端已依 last1Day 降序, 前端保險再 sortBy last1Day desc
248
+ displayEventTable: function() {
249
+ let vo = this
250
+ return reverse(sortBy(vo.eventTable.slice(), 'last1Day'))
251
+ },
252
+
253
+ },
254
+ methods: {
255
+
256
+ resizePanel: function(msg) {
257
+ // console.log('methods resizePanel', msg)
258
+
259
+ let vo = this
260
+
261
+ //panelWidth, panelHeight
262
+ vo.panelWidth = msg.snew.offsetWidth
263
+ vo.panelHeight = msg.snew.offsetHeight
264
+
265
+ },
266
+
267
+ genOpt: function(title, arr, keysPick) {
268
+ let vo = this
269
+
270
+ //check
271
+ if (size(arr) === 0) {
272
+ return null
273
+ }
274
+
275
+ //keysPick to array
276
+ if (!isearr(keysPick)) {
277
+ keysPick = [keysPick]
278
+ }
279
+
280
+ //colors
281
+ let colors = [
282
+ '#3b82f6', // blue-500
283
+ '#ef4444', // red-500
284
+ '#10b981', // emerald-500
285
+ '#f97316', // orange-500
286
+ '#8b5cf6', // violet-500
287
+ '#6366f1', // indigo-500
288
+ '#eab308', // yellow-500
289
+ '#ec4899', // pink-500
290
+ '#14b8a6', // teal-500
291
+ '#a855f7', // purple-500
292
+ ]
293
+ function getColor(k) {
294
+ return colors[k % colors.length]
295
+ }
296
+
297
+ //getName: count 系列顯示為「全部」, 其餘 event 名照原字串
298
+ let getName = (valueKey) => {
299
+ if (valueKey === 'count') {
300
+ return vo.$t('staTotal')
301
+ }
302
+ return valueKey
303
+ }
304
+
305
+ //series: 折線圖, 每個 keysPick 一條系列, 缺值補 0
306
+ let series = map(keysPick, (valueKey, k) => {
307
+ let name = getName(valueKey)
308
+ let color = getColor(k)
309
+ return {
310
+ name,
311
+ type: 'line',
312
+ smooth: true,
313
+ data: map(arr, item => get(item, `data.${valueKey}`, 0)),
314
+ itemStyle: {
315
+ color,
316
+ },
317
+ emphasis: {
318
+ focus: 'series'
319
+ },
320
+ }
321
+ })
322
+
323
+ //formatX: hr 顯示 MM/DD HH:mm, day 顯示 MM/DD
324
+ let formatX = 'MM/DD'
325
+ if (vo.timeInterval === 'hr') {
326
+ formatX = 'MM/DD\nHH:mm'
327
+ }
328
+
329
+ //opt
330
+ let opt = {
331
+ title: {
332
+ text: title,
333
+ show: false, // 不顯示標題,由組件的HTML提供
334
+ },
335
+ tooltip: {
336
+ trigger: 'axis',
337
+ },
338
+ grid: {
339
+ left: '3%',
340
+ right: '4%',
341
+ bottom: '3%',
342
+ containLabel: true
343
+ },
344
+ legend: {
345
+ show: true,
346
+ },
347
+ xAxis: [
348
+ {
349
+ type: 'category',
350
+ boundaryGap: false,
351
+ data: map(arr, item => ot(item.time).format(formatX)),
352
+ axisTick: {
353
+ alignWithLabel: true
354
+ }
355
+ }
356
+ ],
357
+ yAxis: [
358
+ {
359
+ type: 'value'
360
+ }
361
+ ],
362
+ series,
363
+ }
364
+ return opt
365
+ },
366
+
367
+ grStaEvent: async function() {
368
+ let vo = this
369
+
370
+ //getStaEvent
371
+ await vo.$fapi.getStaEvent(vo.timeLength, vo.timeInterval)
372
+ .then((res) => {
373
+ // console.log('getStaEvent(freqEvent)', res)
374
+ vo.freqEvent = res
375
+ //首次資料載入後 selectedEvents 為空 → 預設全選
376
+ if (size(vo.selectedEvents) === 0) {
377
+ vo.selectedEvents = vo.allEvents.slice()
378
+ }
379
+ vo.updateCharts()
380
+ })
381
+ .catch((err) => {
382
+ console.log('getStaEvent catch', err)
383
+ throw err
384
+ })
385
+
386
+ },
387
+
388
+ grStaEventTable: async function() {
389
+ let vo = this
390
+
391
+ //getStaEventTable
392
+ await vo.$fapi.getStaEventTable()
393
+ .then((res) => {
394
+ // console.log('getStaEventTable(eventTable)', res)
395
+ vo.eventTable = res
396
+ })
397
+ .catch((err) => {
398
+ console.log('getStaEventTable catch', err)
399
+ throw err
400
+ })
401
+
402
+ },
403
+
404
+ getAndRelaData: async function() {
405
+ let vo = this
406
+
407
+ //grStaEvent
408
+ await vo.grStaEvent()
409
+
410
+ //grStaEventTable
411
+ await vo.grStaEventTable()
412
+
413
+ },
414
+
415
+ updateCharts: function() {
416
+ let vo = this
417
+
418
+ //ksEvent: showTotal 時前置 count 加總線, 其後接所選各 event (各一條系列)
419
+ let ksEvent = [
420
+ ...(vo.showTotal ? ['count'] : []),
421
+ ...vo.selectedEvents,
422
+ ]
423
+
424
+ //optEvent
425
+ vo.optEvent = vo.genOpt(vo.$t('staEventTitle'), vo.freqEvent, ksEvent)
426
+
427
+ },
428
+
429
+ updateChartsDebounce: function() {
430
+ let vo = this
431
+ vo.dbc(() => {
432
+ vo.updateCharts()
433
+ })
434
+ },
435
+
436
+ selectAllEvents: function() {
437
+ let vo = this
438
+ vo.selectedEvents = vo.allEvents.slice()
439
+ vo.updateChartsDebounce()
440
+ },
441
+
442
+ clearEvents: function() {
443
+ let vo = this
444
+ vo.selectedEvents = []
445
+ vo.updateChartsDebounce()
446
+ },
447
+
448
+ formatNumber: function(num) {
449
+ if (num === null || num === undefined) {
450
+ return 0
451
+ }
452
+ return num.toLocaleString()
453
+ },
454
+
455
+ changeTimeInterval: function() {
456
+ let vo = this
457
+
458
+ //切換時間分組須重新向後端取數據 (hr/day 對應不同 time 粒度)
459
+ vo.firstLoading = true
460
+ vo.errMsg = ''
461
+ vo.getAndRelaData()
462
+ .catch((err) => {
463
+ console.log(err)
464
+ vo.errMsg = vo.$t('getDataError')
465
+ })
466
+ .finally(() => {
467
+ vo.firstLoading = false
468
+ })
469
+
470
+ },
471
+
472
+ }
473
+ }
474
+ </script>
475
+
476
+ <style scoped>
477
+ /* === Tailwind utility (非響應式) 同名定義: LayoutContentStaInfor; 由實際 Tailwind v3 產生規則抽出, 與 CDN 逐字一致 === */
478
+ .bg-gray-100 { --tw-bg-opacity: 1; background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); }
479
+ .bg-gray-50 { --tw-bg-opacity: 1; background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); }
480
+ .bg-white { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); }
481
+ .border { border-width: 1px; }
482
+ .border-collapse { border-collapse: collapse; }
483
+ .border-solid { border-style: solid; }
484
+ .border-gray-200 { --tw-border-opacity: 1; border-color: rgb(229 231 235 / var(--tw-border-opacity, 1)); }
485
+ .border-gray-300 { --tw-border-opacity: 1; border-color: rgb(209 213 219 / var(--tw-border-opacity, 1)); }
486
+ .flex { display: flex; }
487
+ .focus\:ring-blue-500:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1)); }
488
+ .font-medium { font-weight: 500; }
489
+ .font-semibold { font-weight: 600; }
490
+ .h-4 { height: 1rem; }
491
+ .hover\:bg-gray-50:hover { --tw-bg-opacity: 1; background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); }
492
+ .items-center { align-items: center; }
493
+ .justify-end { justify-content: flex-end; }
494
+ .mb-8 { margin-bottom: 2rem; }
495
+ .ml-2 { margin-left: 0.5rem; }
496
+ .mr-2 { margin-right: 0.5rem; }
497
+ .mt-2 { margin-top: 0.5rem; }
498
+ .overflow-x-auto { overflow-x: auto; }
499
+ .p-5 { padding: 1.25rem; }
500
+ .pb-2 { padding-bottom: 0.5rem; }
501
+ .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
502
+ .px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
503
+ .py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
504
+ .py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
505
+ .py-4 { padding-top: 1rem; padding-bottom: 1rem; }
506
+ .relative { position: relative; }
507
+ .rounded { border-radius: 0.25rem; }
508
+ .rounded-lg { border-radius: 0.5rem; }
509
+ .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); }
510
+ .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))); }
511
+ .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)); }
512
+ .text-blue-600 { --tw-text-opacity: 1; color: rgb(37 99 235 / var(--tw-text-opacity, 1)); }
513
+ .text-gray-500 { --tw-text-opacity: 1; color: rgb(107 114 128 / var(--tw-text-opacity, 1)); }
514
+ .text-gray-700 { --tw-text-opacity: 1; color: rgb(55 65 81 / var(--tw-text-opacity, 1)); }
515
+ .text-gray-900 { --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity, 1)); }
516
+ .text-left { text-align: left; }
517
+ .text-lg { font-size: 1.125rem; line-height: 1.75rem; }
518
+ .text-purple-600 { --tw-text-opacity: 1; color: rgb(147 51 234 / var(--tw-text-opacity, 1)); }
519
+ .text-sm { font-size: 0.875rem; line-height: 1.25rem; }
520
+ .text-xs { font-size: 0.75rem; line-height: 1rem; }
521
+ .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; }
522
+ .w-4 { width: 1rem; }
523
+ .w-full { width: 100%; }
524
+
525
+ /* === 控制區 (事件多選 checklist + 全部加總 + 時間分組), RWD flex-wrap === */
526
+ .staCtrl { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: flex-start; gap: 12px; padding: 8px 0; }
527
+ .staEventSel { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; flex: 1 1 auto; min-width: 0; }
528
+ .staEventList { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; }
529
+ .staEventItem { display: inline-flex; align-items: center; cursor: pointer; }
530
+ .staEventBtns { display: inline-flex; align-items: center; gap: 6px; }
531
+ .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; }
532
+ .staBtn:hover { background: rgb(243 244 246); }
533
+ .staCtrlRight { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; }
534
+ .mr-2 { margin-right: 0.5rem; }
535
+ .ml-1 { margin-left: 0.25rem; }
536
+ </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
 
@@ -822,20 +824,28 @@ export default {
822
824
 
823
825
  },
824
826
 
825
- saveTargets: function() {
826
- // console.log('method saveTargets')
827
+ saveTargets: function(msg) {
827
828
 
828
829
  let vo = this
829
830
 
830
- async function core() {
831
- let errTemp = null
831
+ //第一行立刻釋放按鈕視覺鎖
832
+ msg.pm.resolve()
832
833
 
833
- //show loading
834
- vo.$ui.updateLoading(true)
834
+ //fire-and-forget, 不 await
835
+ vo.doSaveTargets()
835
836
 
836
- //check
837
+ },
838
+
839
+ doSaveTargets: function() {
840
+ // console.log('method doSaveTargets')
841
+
842
+ let vo = this
843
+
844
+ async function core() {
845
+
846
+ //1) 同步檢測 (在開 loading 之前)
837
847
  if (isestr(vo.isError)) {
838
- vo.$alert(`${vo.isError}`, { type: 'error' })
848
+ await vo.$dg.showCheckYes(`${vo.isError}`)
839
849
  return
840
850
  }
841
851
 
@@ -844,35 +854,34 @@ export default {
844
854
 
845
855
  //check
846
856
  if (size(rows) === 0) {
847
- vo.$alert(`${vo.$t('targetAddEmpty')}`, { type: 'error' })
857
+ await vo.$dg.showCheckYes(`${vo.$t('targetAddEmpty')}`)
848
858
  return
849
859
  }
850
860
 
851
- //updateTargets
861
+ //2) 確定打 API 才開 loading
862
+ vo.$ui.updateLoading(true)
863
+
864
+ //3) updateTargets, 各自 catch + 旗標短路
865
+ let okSave = false
852
866
  await vo.$fapi.updateTargets(rows)
853
- .catch((err) => {
854
- errTemp = err
867
+ .then(() => { okSave = true })
868
+ .catch(async (err) => {
869
+ vo.$ui.updateLoading(false) //showCheckYes 前關 loading(modal 阻斷期間避免 loading 疊在底下)
870
+ await vo.$dg.showCheckYes(`${vo.$t('targetSaveTargetsFail')}: ${vo.$tErr(err)}`)
855
871
  })
856
-
857
- //check
858
- if (errTemp !== null) {
859
- vo.$alert(`${vo.$t('targetSaveTargetsFail')}: ${errTemp}`, { type: 'error' })
860
- return
861
- }
872
+ if (!okSave) return
862
873
 
863
874
  //isModified
864
875
  vo.isModified = false
865
876
 
866
- //alert
867
- vo.$alert(vo.$t('targetSaveTargetsSuccess'))
877
+ //alert(showCheckYes 前關 loading,避免 modal 等待期間 loading 疊在底下)
878
+ vo.$ui.updateLoading(false)
879
+ await vo.$dg.showCheckYes(vo.$t('targetSaveTargetsSuccess'), { type: 'success' })
868
880
 
869
881
  }
870
882
 
871
883
  //core
872
884
  core()
873
- // .then((res) => {
874
- // console.log('then', res)
875
- // })
876
885
  .catch((err) => {
877
886
  console.log('catch', err)
878
887
  vo.$alert(vo.$t('anUnexpectedErrorOccurred'), { type: 'error' })