xsh_1997 3 päivää sitten
vanhempi
commit
c6bbe20125

+ 1 - 1
doc/大屏/免密登录/大屏免密登录技术方案.md

@@ -177,7 +177,7 @@ async function screenAutoLogin(apiBase) {
177 177
 - [x] `ScreenLoginService` + `ScreenLoginRsaSupport` + `ScreenLoginProperties`
178 178
 - [x] `application.yml` → `bigscreen.login.*`
179 179
 - [x] 单元测试:`ScreenLoginRsaSupportTest`、`ScreenLoginServiceTest`、`ScreenLoginControllerApiTest`
180
-- [ ] `ruoyi-screen` 对接自动登录(按本方案调用公钥 + 登录接口)
180
+- [x] `ruoyi-screen` 对接自动登录(按本方案调用公钥 + 登录接口)
181 181
 
182 182
 ---
183 183
 

+ 3 - 0
ruoyi-screen/.env.development

@@ -14,3 +14,6 @@ VITE_PROXY_TARGET=http://192.168.1.6:8010
14 14
 VITE_WEATHER_APP_ID=36793262
15 15
 VITE_WEATHER_APP_SECRET=t6a6z0H8
16 16
 VITE_WEATHER_CITY=巴青
17
+
18
+# 大屏免密登录(RSA 加密 UUID,见 doc/大屏/免密登录/大屏免密登录技术方案.md)
19
+VITE_SCREEN_AUTO_LOGIN=true

+ 3 - 0
ruoyi-screen/.env.production

@@ -15,3 +15,6 @@ VITE_WEATHER_APP_ID=36793262
15 15
 VITE_WEATHER_APP_SECRET=t6a6z0H8
16 16
 VITE_WEATHER_CITY=巴青
17 17
 
18
+# 大屏免密登录(生产需后端 bigscreen.login.enabled=true 且配置 RSA 密钥)
19
+VITE_SCREEN_AUTO_LOGIN=true
20
+

+ 11 - 0
ruoyi-screen/package-lock.json

@@ -11,6 +11,7 @@
11 11
         "axios": "^1.16.1",
12 12
         "echarts": "^5.5.1",
13 13
         "echarts-wordcloud": "^2.1.0",
14
+        "jsencrypt": "^3.0.0-rc.1",
14 15
         "vue": "^3.5.13",
15 16
         "vue-router": "^4.6.4"
16 17
       },
@@ -883,6 +884,11 @@
883 884
         "node": ">= 6"
884 885
       }
885 886
     },
887
+    "node_modules/jsencrypt": {
888
+      "version": "3.0.0-rc.1",
889
+      "resolved": "https://registry.npmmirror.com/jsencrypt/-/jsencrypt-3.0.0-rc.1.tgz",
890
+      "integrity": "sha512-gcvGaqerlUJy1Kq6tNgPYteVEoWNemu+9hBe2CdsCIz4rVcwjoTQ72iD1W76/PRMlnkzG0yVh7nwOOMOOUfKmg=="
891
+    },
886 892
     "node_modules/magic-string": {
887 893
       "version": "0.30.21",
888 894
       "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
@@ -1639,6 +1645,11 @@
1639 1645
         "debug": "4"
1640 1646
       }
1641 1647
     },
1648
+    "jsencrypt": {
1649
+      "version": "3.0.0-rc.1",
1650
+      "resolved": "https://registry.npmmirror.com/jsencrypt/-/jsencrypt-3.0.0-rc.1.tgz",
1651
+      "integrity": "sha512-gcvGaqerlUJy1Kq6tNgPYteVEoWNemu+9hBe2CdsCIz4rVcwjoTQ72iD1W76/PRMlnkzG0yVh7nwOOMOOUfKmg=="
1652
+    },
1642 1653
     "magic-string": {
1643 1654
       "version": "0.30.21",
1644 1655
       "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",

+ 1 - 0
ruoyi-screen/package.json

@@ -15,6 +15,7 @@
15 15
     "axios": "^1.16.1",
16 16
     "echarts": "^5.5.1",
17 17
     "echarts-wordcloud": "^2.1.0",
18
+    "jsencrypt": "^3.0.0-rc.1",
18 19
     "vue": "^3.5.13",
19 20
     "vue-router": "^4.6.4"
20 21
   },

+ 19 - 0
ruoyi-screen/src/api/login.js

@@ -27,3 +27,22 @@ export function logout() {
27 27
     method: 'post'
28 28
   })
29 29
 }
30
+
31
+/** 大屏免密登录:获取 RSA 公钥 */
32
+export function getScreenPublicKey() {
33
+  return request({
34
+    url: '/bigScreen/publicKey',
35
+    method: 'get',
36
+    headers: { isToken: false }
37
+  })
38
+}
39
+
40
+/** 大屏免密登录:提交 RSA 密文换取 JWT */
41
+export function screenAutoLogin(encryptedToken) {
42
+  return request({
43
+    url: '/bigScreen/login',
44
+    method: 'post',
45
+    headers: { isToken: false },
46
+    data: { token: encryptedToken }
47
+  })
48
+}

+ 2 - 0
ruoyi-screen/src/components/ScreenNavBar.vue

@@ -46,6 +46,7 @@ import { computed } from "vue";
46 46
 import { useRoute, useRouter } from "vue-router";
47 47
 import { logout } from "@/api/login";
48 48
 import { removeToken } from "@/utils/auth";
49
+import { resetScreenAutoLoginState } from "@/utils/screenAutoLogin";
49 50
 
50 51
 const router = useRouter();
51 52
 const route = useRoute();
@@ -101,6 +102,7 @@ function goNav(item) {
101 102
 function onLogout() {
102 103
   logout().catch(() => {});
103 104
   removeToken();
105
+  resetScreenAutoLoginState();
104 106
   router.replace({ path: "/login" });
105 107
 }
106 108
 </script>

+ 25 - 4
ruoyi-screen/src/router/index.js

@@ -1,6 +1,7 @@
1 1
 import { createRouter, createWebHistory } from "vue-router"
2 2
 import ScreenLayout from "../layout/ScreenLayout.vue"
3 3
 import { getToken } from "../utils/auth"
4
+import { ensureScreenAutoLogin, isScreenAutoLoginEnabled } from "../utils/screenAutoLogin"
4 5
 
5 6
 /**
6 7
  * 大屏路由表(路径与后端 /bigScreen/* 业务对应,便于联调)
@@ -87,15 +88,35 @@ const router = createRouter({
87 88
   routes
88 89
 })
89 90
 
90
-/** 未登录跳转登录页;已登录访问 /login 则进首页 */
91
-router.beforeEach((to, _from, next) => {
91
+/** 未登录跳转登录页;已登录访问 /login 则进首页;启用免密时先尝试自动登录 */
92
+router.beforeEach(async (to, _from, next) => {
92 93
   const token = getToken()
93 94
   if (to.path === "/login") {
94
-    next(token ? { path: "/home" } : undefined)
95
+    if (token) {
96
+      next({ path: "/home" })
97
+      return
98
+    }
99
+    if (isScreenAutoLoginEnabled()) {
100
+      const ok = await ensureScreenAutoLogin()
101
+      if (ok) {
102
+        const redirect =
103
+          typeof to.query.redirect === "string" ? to.query.redirect : "/home"
104
+        next({ path: redirect })
105
+        return
106
+      }
107
+    }
108
+    next()
95 109
     return
96 110
   }
97 111
   const needAuth = to.matched.some((r) => r.meta.requiresAuth)
98
-  if (needAuth && !token) {
112
+  if (needAuth && !getToken()) {
113
+    if (isScreenAutoLoginEnabled()) {
114
+      const ok = await ensureScreenAutoLogin()
115
+      if (ok) {
116
+        next()
117
+        return
118
+      }
119
+    }
99 120
     next({ path: "/login", query: { redirect: to.fullPath } })
100 121
     return
101 122
   }

+ 2 - 0
ruoyi-screen/src/utils/request.js

@@ -1,6 +1,7 @@
1 1
 import axios from 'axios'
2 2
 import router from '../router'
3 3
 import { getToken, removeToken } from './auth'
4
+import { resetScreenAutoLoginState } from './screenAutoLogin'
4 5
 import { tansParams } from './ruoyi'
5 6
 
6 7
 /** 避免多个接口同时 401 重复跳转登录 */
@@ -11,6 +12,7 @@ function redirectToLogin() {
11 12
   if (router.currentRoute.value.path === '/login') return
12 13
   isRedirectingLogin = true
13 14
   removeToken()
15
+  resetScreenAutoLoginState()
14 16
   const redirect = router.currentRoute.value.fullPath
15 17
   router
16 18
     .replace({

+ 78 - 0
ruoyi-screen/src/utils/screenAutoLogin.js

@@ -0,0 +1,78 @@
1
+import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
2
+import { getScreenPublicKey, screenAutoLogin } from '@/api/login'
3
+import { getToken, setToken } from '@/utils/auth'
4
+
5
+/** 同一会话内仅尝试一次免密登录,避免反复请求 */
6
+let autoLoginState = null
7
+let autoLoginPromise = null
8
+
9
+export function isScreenAutoLoginEnabled() {
10
+  return import.meta.env.VITE_SCREEN_AUTO_LOGIN === 'true'
11
+}
12
+
13
+export function resetScreenAutoLoginState() {
14
+  autoLoginState = null
15
+  autoLoginPromise = null
16
+}
17
+
18
+/**
19
+ * RSA 加密 UUID 换取 JWT(见 doc/大屏/免密登录/大屏免密登录技术方案.md)
20
+ * @returns {Promise<string>} JWT
21
+ */
22
+export async function tryScreenAutoLogin() {
23
+  const keyRes = await getScreenPublicKey()
24
+  const publicKey = keyRes.data?.publicKey
25
+  if (!publicKey) {
26
+    throw new Error('未获取到 RSA 公钥')
27
+  }
28
+
29
+  const uuid = crypto.randomUUID()
30
+  const encryptor = new JSEncrypt()
31
+  encryptor.setPublicKey(publicKey)
32
+  const cipher = encryptor.encrypt(uuid)
33
+  if (!cipher) {
34
+    throw new Error('RSA 加密失败')
35
+  }
36
+
37
+  const loginRes = await screenAutoLogin(cipher)
38
+  if (!loginRes.token) {
39
+    throw new Error('免密登录未返回 token')
40
+  }
41
+  setToken(loginRes.token)
42
+  return loginRes.token
43
+}
44
+
45
+/**
46
+ * 路由守卫用:已登录直接通过;未启用免密则 false;否则尝试一次免密登录
47
+ * @returns {Promise<boolean>}
48
+ */
49
+export async function ensureScreenAutoLogin() {
50
+  if (getToken()) {
51
+    return true
52
+  }
53
+  if (!isScreenAutoLoginEnabled()) {
54
+    return false
55
+  }
56
+  if (autoLoginState === 'fail') {
57
+    return false
58
+  }
59
+  if (autoLoginState === 'ok') {
60
+    return !!getToken()
61
+  }
62
+  if (autoLoginState === 'pending' && autoLoginPromise) {
63
+    return autoLoginPromise
64
+  }
65
+
66
+  autoLoginState = 'pending'
67
+  autoLoginPromise = tryScreenAutoLogin()
68
+    .then(() => {
69
+      autoLoginState = 'ok'
70
+      return true
71
+    })
72
+    .catch(() => {
73
+      autoLoginState = 'fail'
74
+      return false
75
+    })
76
+
77
+  return autoLoginPromise
78
+}

+ 2 - 2
ruoyi-ui/src/layout/components/Sidebar/Logo.vue

@@ -6,7 +6,7 @@
6 6
         <h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' && navType !== 3 ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
7 7
       </router-link>
8 8
       <router-link v-else key="expand" class="sidebar-logo-link" to="/">
9
-        <img v-if="logo" :src="logo" class="sidebar-logo" />
9
+        <!-- <img v-if="logo" :src="logo" class="sidebar-logo" /> -->
10 10
         <h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' && navType !== 3 ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
11 11
       </router-link>
12 12
     </transition>
@@ -80,7 +80,7 @@ export default {
80 80
       color: #fff;
81 81
       font-weight: 600;
82 82
       line-height: 50px;
83
-      font-size: 14px;
83
+      font-size: 11px;
84 84
       font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
85 85
       vertical-align: middle;
86 86
     }

+ 58 - 11
ruoyi-ui/src/views/diseaseTreatment/medicalResource/index.vue

@@ -207,7 +207,7 @@
207 207
               :placeholder="dtT('formServiceStart')"
208 208
               :picker-options="timePickerOptions"
209 209
               style="width: 160px"
210
-              @change="validateFormField('serviceStartTime')"
210
+              @change="handleServiceStartTimeChange"
211 211
             />
212 212
             <span class="time-sep">{{ dtCommon("timeSeparator") }}</span>
213 213
             <el-time-select
@@ -215,7 +215,7 @@
215 215
               :placeholder="dtT('formServiceEnd')"
216 216
               :picker-options="timePickerOptionsEnd"
217 217
               style="width: 160px"
218
-              @change="validateFormField('serviceStartTime')"
218
+              @change="handleServiceEndTimeChange"
219 219
             />
220 220
           </el-form-item>
221 221
           <el-form-item prop="serviceWeekdaysList" :rules="formRules.serviceWeekdaysList">
@@ -282,7 +282,7 @@
282 282
               :placeholder="dtT('formServiceStart')"
283 283
               :picker-options="timePickerOptions"
284 284
               style="width: 160px"
285
-              @change="validateFormField('serviceStartTime')"
285
+              @change="handleServiceStartTimeChange"
286 286
             />
287 287
             <span class="time-sep">{{ dtCommon("timeSeparator") }}</span>
288 288
             <el-time-select
@@ -290,7 +290,7 @@
290 290
               :placeholder="dtT('formServiceEnd')"
291 291
               :picker-options="timePickerOptionsEnd"
292 292
               style="width: 160px"
293
-              @change="validateFormField('serviceStartTime')"
293
+              @change="handleServiceEndTimeChange"
294 294
             />
295 295
           </el-form-item>
296 296
           <el-form-item prop="serviceWeekdaysList" :rules="formRules.serviceWeekdaysList">
@@ -526,12 +526,11 @@ export default {
526 526
       return !!(this.form.photoFilePath || this.form.photoFileUrl)
527 527
     },
528 528
     timePickerOptionsEnd() {
529
-      const start = this.form.serviceStartTime
530 529
       return {
531
-        start: start || "00:00",
530
+        start: "00:00",
532 531
         step: "00:15",
533 532
         end: "23:45",
534
-        minTime: start
533
+        minTime: this.form.serviceStartTime || "00:00"
535 534
       }
536 535
     },
537 536
     formRules() {
@@ -831,8 +830,47 @@ export default {
831 830
     allConsultModeValues() {
832 831
       return this.consultModeOptions.map((item) => item.value)
833 832
     },
833
+    hasServiceScheduleType(resourceType) {
834
+      const t = this.normalizeResourceType(resourceType)
835
+      return this.isVetType(t) || this.isOrgType(t)
836
+    },
837
+    defaultServiceStartTime() {
838
+      return "08:00"
839
+    },
840
+    defaultServiceEndTime() {
841
+      return "18:00"
842
+    },
843
+    defaultServiceWeekdaysList() {
844
+      return [1, 2, 3, 4, 5]
845
+    },
846
+    applyDefaultServiceSchedule(resourceType) {
847
+      if (!this.hasServiceScheduleType(resourceType)) {
848
+        return
849
+      }
850
+      this.form.serviceStartTime = this.defaultServiceStartTime()
851
+      this.form.serviceWeekdaysList = this.defaultServiceWeekdaysList()
852
+      this.$nextTick(() => {
853
+        this.form.serviceEndTime = this.defaultServiceEndTime()
854
+      })
855
+    },
856
+    handleServiceStartTimeChange() {
857
+      const start = this.form.serviceStartTime
858
+      const end = this.form.serviceEndTime
859
+      if (start && (!end || end <= start)) {
860
+        this.$nextTick(() => {
861
+          this.form.serviceEndTime = this.defaultServiceEndTime()
862
+          this.validateFormField("serviceStartTime")
863
+        })
864
+        return
865
+      }
866
+      this.validateFormField("serviceStartTime")
867
+    },
868
+    handleServiceEndTimeChange() {
869
+      this.validateFormField("serviceStartTime")
870
+    },
834 871
     resetFormModel() {
835 872
       const resourceType = TAB_TYPE_MAP[this.activeTab]
873
+      const hasServiceSchedule = this.hasServiceScheduleType(resourceType)
836 874
       this.form = {
837 875
         id: undefined,
838 876
         resourceType,
@@ -846,9 +884,9 @@ export default {
846 884
         consultModesList: this.isVetType(resourceType) ? this.allConsultModeValues() : [],
847 885
         serviceArea: undefined,
848 886
         feeStandard: undefined,
849
-        serviceStartTime: undefined,
850
-        serviceEndTime: undefined,
851
-        serviceWeekdaysList: [],
887
+        serviceStartTime: hasServiceSchedule ? this.defaultServiceStartTime() : undefined,
888
+        serviceEndTime: hasServiceSchedule ? this.defaultServiceEndTime() : undefined,
889
+        serviceWeekdaysList: hasServiceSchedule ? this.defaultServiceWeekdaysList() : [],
852 890
         maxDailyAppointments: this.isOrgType(resourceType) ? 30 : undefined,
853 891
         establishDate: undefined,
854 892
         teamSize: undefined,
@@ -863,7 +901,14 @@ export default {
863 901
       this.resetFormModel()
864 902
       this.dialogEdit = false
865 903
       this.open = true
866
-      this.$nextTick(() => this.resetForm("form"))
904
+      this.$nextTick(() => {
905
+        if (this.hasServiceScheduleType(this.form.resourceType)) {
906
+          this.form.serviceEndTime = this.defaultServiceEndTime()
907
+        }
908
+        if (this.$refs.form) {
909
+          this.$refs.form.clearValidate()
910
+        }
911
+      })
867 912
     },
868 913
     fillFormFromApi(data) {
869 914
       const row = this.normalizeMedicalResourceRow(data)
@@ -988,6 +1033,8 @@ export default {
988 1033
         this.form.serviceStartTime = undefined
989 1034
         this.form.serviceEndTime = undefined
990 1035
         this.form.serviceWeekdaysList = []
1036
+      } else {
1037
+        this.applyDefaultServiceSchedule(t)
991 1038
       }
992 1039
       if (!this.isVetType(t) && !this.isTeamType(t) && !this.isEquipmentType(t)) {
993 1040
         this.form.affiliatedUnit = undefined

+ 67 - 11
ruoyi-ui/src/views/techService/techResource/index.vue

@@ -271,7 +271,7 @@
271 271
               :placeholder="tsT('formServiceStart')"
272 272
               :picker-options="timePickerOptions"
273 273
               style="width: 160px"
274
-              @change="validateFormField('serviceStartTime')"
274
+              @change="handleServiceStartTimeChange"
275 275
             />
276 276
             <span class="time-sep">{{ tsCommon("timeSeparator") }}</span>
277 277
             <el-time-select
@@ -279,7 +279,7 @@
279 279
               :placeholder="tsT('formServiceEnd')"
280 280
               :picker-options="timePickerOptionsEnd"
281 281
               style="width: 160px"
282
-              @change="validateFormField('serviceStartTime')"
282
+              @change="handleServiceEndTimeChange"
283 283
             />
284 284
           </el-form-item>
285 285
           <el-form-item prop="serviceWeekdaysList" :rules="formRules.serviceWeekdaysList">
@@ -329,6 +329,7 @@
329 329
               :placeholder="tsT('formServiceStart')"
330 330
               :picker-options="timePickerOptions"
331 331
               style="width: 160px"
332
+              @change="handleServiceStartTimeChange"
332 333
             />
333 334
             <span class="time-sep">{{ tsCommon("timeSeparator") }}</span>
334 335
             <el-time-select
@@ -336,11 +337,12 @@
336 337
               :placeholder="tsT('formServiceEnd')"
337 338
               :picker-options="timePickerOptionsEnd"
338 339
               style="width: 160px"
340
+              @change="handleServiceEndTimeChange"
339 341
             />
340 342
           </el-form-item>
341 343
           <el-form-item prop="serviceWeekdaysList" :rules="formRules.serviceWeekdaysList">
342 344
             <template slot="label">{{ tsT("formServiceWeekdays") }}</template>
343
-            <el-checkbox-group v-model="form.serviceWeekdaysList">
345
+            <el-checkbox-group v-model="form.serviceWeekdaysList" @change="validateFormField('serviceWeekdaysList')">
344 346
               <el-checkbox v-for="item in weekdayOptions" :key="item.value" :label="item.value">{{ item.label }}</el-checkbox>
345 347
             </el-checkbox-group>
346 348
           </el-form-item>
@@ -598,12 +600,11 @@ export default {
598 600
       return !!(this.form.coverFilePath || this.form.coverFileUrl)
599 601
     },
600 602
     timePickerOptionsEnd() {
601
-      const start = this.form.serviceStartTime
602 603
       return {
603
-        start: start || "00:00",
604
+        start: "00:00",
604 605
         step: "00:15",
605 606
         end: "23:45",
606
-        minTime: start
607
+        minTime: this.form.serviceStartTime || "00:00"
607 608
       }
608 609
     },
609 610
     formRules() {
@@ -871,9 +872,11 @@ export default {
871 872
       this.handleQuery()
872 873
     },
873 874
     resetFormModel() {
875
+      const resourceType = TAB_TYPE_MAP[this.activeTab]
876
+      const hasServiceSchedule = this.hasServiceScheduleType(resourceType)
874 877
       this.form = {
875 878
         id: undefined,
876
-        resourceType: TAB_TYPE_MAP[this.activeTab],
879
+        resourceType,
877 880
         resourceName: undefined,
878 881
         photoFileUrl: undefined,
879 882
         photoFilePath: undefined,
@@ -893,9 +896,9 @@ export default {
893 896
         serviceArea: undefined,
894 897
         feeStandard: undefined,
895 898
         borrowFee: undefined,
896
-        serviceStartTime: undefined,
897
-        serviceEndTime: undefined,
898
-        serviceWeekdaysList: [],
899
+        serviceStartTime: hasServiceSchedule ? this.defaultServiceStartTime() : undefined,
900
+        serviceEndTime: hasServiceSchedule ? this.defaultServiceEndTime() : undefined,
901
+        serviceWeekdaysList: hasServiceSchedule ? this.defaultServiceWeekdaysList() : [],
899 902
         instrumentModel: undefined,
900 903
         storageLocation: undefined,
901 904
         reservationProcess: undefined,
@@ -909,7 +912,14 @@ export default {
909 912
       this.resetFormModel()
910 913
       this.dialogEdit = false
911 914
       this.open = true
912
-      this.$nextTick(() => this.resetForm("form"))
915
+      this.$nextTick(() => {
916
+        if (this.hasServiceScheduleType(this.form.resourceType)) {
917
+          this.form.serviceEndTime = this.defaultServiceEndTime()
918
+        }
919
+        if (this.$refs.form) {
920
+          this.$refs.form.clearValidate()
921
+        }
922
+      })
913 923
     },
914 924
     fillFormFromApi(data) {
915 925
       this.form = {
@@ -1022,12 +1032,58 @@ export default {
1022 1032
         .catch(() => {})
1023 1033
     },
1024 1034
     handleFormTypeChange() {
1035
+      const t = this.normalizeResourceType(this.form.resourceType)
1036
+      if (this.hasServiceScheduleType(t)) {
1037
+        this.applyDefaultServiceSchedule(t)
1038
+      } else {
1039
+        this.form.serviceStartTime = undefined
1040
+        this.form.serviceEndTime = undefined
1041
+        this.form.serviceWeekdaysList = []
1042
+      }
1025 1043
       this.$nextTick(() => {
1026 1044
         if (this.$refs.form) {
1027 1045
           this.$refs.form.clearValidate()
1028 1046
         }
1029 1047
       })
1030 1048
     },
1049
+    hasServiceScheduleType(resourceType) {
1050
+      const t = this.normalizeResourceType(resourceType)
1051
+      return this.isExpertType(t) || this.isInstrumentType(t)
1052
+    },
1053
+    defaultServiceStartTime() {
1054
+      return "08:00"
1055
+    },
1056
+    defaultServiceEndTime() {
1057
+      return "18:00"
1058
+    },
1059
+    defaultServiceWeekdaysList() {
1060
+      return [1, 2, 3, 4, 5]
1061
+    },
1062
+    applyDefaultServiceSchedule(resourceType) {
1063
+      if (!this.hasServiceScheduleType(resourceType)) {
1064
+        return
1065
+      }
1066
+      this.form.serviceStartTime = this.defaultServiceStartTime()
1067
+      this.form.serviceWeekdaysList = this.defaultServiceWeekdaysList()
1068
+      this.$nextTick(() => {
1069
+        this.form.serviceEndTime = this.defaultServiceEndTime()
1070
+      })
1071
+    },
1072
+    handleServiceStartTimeChange() {
1073
+      const start = this.form.serviceStartTime
1074
+      const end = this.form.serviceEndTime
1075
+      if (start && (!end || end <= start)) {
1076
+        this.$nextTick(() => {
1077
+          this.form.serviceEndTime = this.defaultServiceEndTime()
1078
+          this.validateFormField("serviceStartTime")
1079
+        })
1080
+        return
1081
+      }
1082
+      this.validateFormField("serviceStartTime")
1083
+    },
1084
+    handleServiceEndTimeChange() {
1085
+      this.validateFormField("serviceStartTime")
1086
+    },
1031 1087
     extOf(fileName) {
1032 1088
       if (!fileName || fileName.lastIndexOf(".") < 0) {
1033 1089
         return ""