w-web-perm 1.0.47 → 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 (96) 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 +48 -24
  65. package/src/components/LayoutContentPemis.vue +48 -24
  66. package/src/components/LayoutContentStaInfor.vue +536 -0
  67. package/src/components/LayoutContentTargets.vue +48 -24
  68. package/src/components/LayoutContentUsers.vue +48 -24
  69. package/src/components/LayoutState.vue +5 -0
  70. package/src/components/VeCgrups.vue +62 -20
  71. package/src/components/VeCpemis.vue +62 -20
  72. package/src/components/VeCrules.vue +58 -16
  73. package/src/components/VeGrupBlngUsers.vue +77 -32
  74. package/src/components/VePemiBlngGrups.vue +77 -32
  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 +15 -7
  80. package/src/store/mutations.mjs +1 -1
  81. package/srv.mjs +76 -101
  82. package/tableTags.json +1 -1
  83. package/db/wperm/grups/data.mdb +0 -0
  84. package/db/wperm/grups/lock.mdb +0 -0
  85. package/db/wperm/pemis/data.mdb +0 -0
  86. package/db/wperm/pemis/lock.mdb +0 -0
  87. package/db/wperm/targets/data.mdb +0 -0
  88. package/db/wperm/targets/lock.mdb +0 -0
  89. package/db/wperm/users/data.mdb +0 -0
  90. package/db/wperm/users/lock.mdb +0 -0
  91. package/dist/css/app.30e81bfe.css +0 -1
  92. package/dist/js/app-legacy.f1277a07.js +0 -1
  93. package/dist/js/app.f1277a07.js +0 -1
  94. package/dist/js/chunk-vendors-legacy.e8703728.js +0 -21
  95. package/dist/js/chunk-vendors.e8703728.js +0 -21
  96. 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
 
@@ -346,12 +348,22 @@ export default {
346
348
  vo.widthTargetId = get(vo, 'webInfor.widthTargetId', '')
347
349
  vo.widthTargetDescription = get(vo, 'webInfor.widthTargetDescription', '')
348
350
 
349
- vo.firstSetting = false
351
+ //會觸發數據變更再導致opt變更導致觸發rowsChange等事件, 故得要延遲, 供組件偵測初始設定數據初始化之用
352
+ setTimeout(() => {
353
+ vo.firstSetting = false
354
+ // console.log('firstSetting', vo.firstSetting)
355
+ }, 1)
356
+
350
357
  }
351
358
 
352
359
  },
353
360
  computed: {
354
361
 
362
+ syncState: function() {
363
+ let vo = this
364
+ return get(vo, '$store.state.syncState')
365
+ },
366
+
355
367
  webInfor: function() {
356
368
  let wi = get(this, `$store.state.webInfor`)
357
369
  return wi
@@ -618,6 +630,11 @@ export default {
618
630
  // console.log('rowsChange', rs)
619
631
  // console.log('rowsChange cloneDeep(vo.opt.rows)', cloneDeep(vo.opt.rows))
620
632
 
633
+ //check
634
+ if (!vo.syncState || vo.firstLoading || vo.firstSetting) {
635
+ return
636
+ }
637
+
621
638
  //isModified
622
639
  vo.isModified = true
623
640
 
@@ -807,20 +824,28 @@ export default {
807
824
 
808
825
  },
809
826
 
810
- saveTargets: function() {
811
- // console.log('method saveTargets')
827
+ saveTargets: function(msg) {
812
828
 
813
829
  let vo = this
814
830
 
815
- async function core() {
816
- let errTemp = null
831
+ //第一行立刻釋放按鈕視覺鎖
832
+ msg.pm.resolve()
817
833
 
818
- //show loading
819
- vo.$ui.updateLoading(true)
834
+ //fire-and-forget, 不 await
835
+ vo.doSaveTargets()
820
836
 
821
- //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 之前)
822
847
  if (isestr(vo.isError)) {
823
- vo.$alert(`${vo.isError}`, { type: 'error' })
848
+ await vo.$dg.showCheckYes(`${vo.isError}`)
824
849
  return
825
850
  }
826
851
 
@@ -829,35 +854,34 @@ export default {
829
854
 
830
855
  //check
831
856
  if (size(rows) === 0) {
832
- vo.$alert(`${vo.$t('targetAddEmpty')}`, { type: 'error' })
857
+ await vo.$dg.showCheckYes(`${vo.$t('targetAddEmpty')}`)
833
858
  return
834
859
  }
835
860
 
836
- //updateTargets
861
+ //2) 確定打 API 才開 loading
862
+ vo.$ui.updateLoading(true)
863
+
864
+ //3) updateTargets, 各自 catch + 旗標短路
865
+ let okSave = false
837
866
  await vo.$fapi.updateTargets(rows)
838
- .catch((err) => {
839
- 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)}`)
840
871
  })
841
-
842
- //check
843
- if (errTemp !== null) {
844
- vo.$alert(`${vo.$t('targetSaveTargetsFail')}: ${errTemp}`, { type: 'error' })
845
- return
846
- }
872
+ if (!okSave) return
847
873
 
848
874
  //isModified
849
875
  vo.isModified = false
850
876
 
851
- //alert
852
- 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' })
853
880
 
854
881
  }
855
882
 
856
883
  //core
857
884
  core()
858
- // .then((res) => {
859
- // console.log('then', res)
860
- // })
861
885
  .catch((err) => {
862
886
  console.log('catch', err)
863
887
  vo.$alert(vo.$t('anUnexpectedErrorOccurred'), { type: 'error' })