Commit 4ce1d526c80cd859c815b9e629ae1838f441cf0b
1 parent
e9c28319
refactor(lock-page): refactor lock page
Showing
23 changed files
with
426 additions
and
192 deletions
CHANGELOG.zh_CN.md
src/api/sys/user.ts
@@ -5,6 +5,7 @@ import { | @@ -5,6 +5,7 @@ import { | ||
5 | GetUserInfoByUserIdParams, | 5 | GetUserInfoByUserIdParams, |
6 | GetUserInfoByUserIdModel, | 6 | GetUserInfoByUserIdModel, |
7 | } from './model/userModel'; | 7 | } from './model/userModel'; |
8 | +import { ErrorMessageMode } from '/@/utils/http/axios/types'; | ||
8 | 9 | ||
9 | enum Api { | 10 | enum Api { |
10 | Login = '/login', | 11 | Login = '/login', |
@@ -15,7 +16,7 @@ enum Api { | @@ -15,7 +16,7 @@ enum Api { | ||
15 | /** | 16 | /** |
16 | * @description: user login api | 17 | * @description: user login api |
17 | */ | 18 | */ |
18 | -export function loginApi(params: LoginParams) { | 19 | +export function loginApi(params: LoginParams, mode: ErrorMessageMode = 'modal') { |
19 | return defHttp.request<LoginResultModel>( | 20 | return defHttp.request<LoginResultModel>( |
20 | { | 21 | { |
21 | url: Api.Login, | 22 | url: Api.Login, |
@@ -23,7 +24,7 @@ export function loginApi(params: LoginParams) { | @@ -23,7 +24,7 @@ export function loginApi(params: LoginParams) { | ||
23 | params, | 24 | params, |
24 | }, | 25 | }, |
25 | { | 26 | { |
26 | - errorMessageMode: 'modal', | 27 | + errorMessageMode: mode, |
27 | } | 28 | } |
28 | ); | 29 | ); |
29 | } | 30 | } |
src/assets/images/lock-page.jpg deleted
100644 → 0
218 KB
src/design/mixins.less
@@ -90,6 +90,7 @@ | @@ -90,6 +90,7 @@ | ||
90 | @content(); | 90 | @content(); |
91 | } | 91 | } |
92 | } | 92 | } |
93 | + | ||
93 | .respond-to (xsmall-and-small, @content) { | 94 | .respond-to (xsmall-and-small, @content) { |
94 | @media only screen and (max-width: @screen-sm-max) { | 95 | @media only screen and (max-width: @screen-sm-max) { |
95 | @content(); | 96 | @content(); |
src/design/var/breakpoint.less
@@ -26,9 +26,13 @@ | @@ -26,9 +26,13 @@ | ||
26 | @screen-xxl: 1600px; | 26 | @screen-xxl: 1600px; |
27 | @screen-xxl-min: @screen-xxl; | 27 | @screen-xxl-min: @screen-xxl; |
28 | 28 | ||
29 | +@screen-xxxl: 1900px; | ||
30 | +@screen-xxxl-min: @screen-xxxl; | ||
31 | + | ||
29 | // provide a maximum | 32 | // provide a maximum |
30 | @screen-xs-max: (@screen-sm-min - 1px); | 33 | @screen-xs-max: (@screen-sm-min - 1px); |
31 | @screen-sm-max: (@screen-md-min - 1px); | 34 | @screen-sm-max: (@screen-md-min - 1px); |
32 | @screen-md-max: (@screen-lg-min - 1px); | 35 | @screen-md-max: (@screen-lg-min - 1px); |
33 | @screen-lg-max: (@screen-xl-min - 1px); | 36 | @screen-lg-max: (@screen-xl-min - 1px); |
34 | @screen-xl-max: (@screen-xxl-min - 1px); | 37 | @screen-xl-max: (@screen-xxl-min - 1px); |
38 | +@screen-xxl-max: (@screen-xxxl-min - 1px); |
src/hooks/setting/useRootSetting.ts
@@ -42,6 +42,8 @@ const getColorWeak = computed(() => unref(getRootSetting).colorWeak); | @@ -42,6 +42,8 @@ const getColorWeak = computed(() => unref(getRootSetting).colorWeak); | ||
42 | 42 | ||
43 | const getGrayMode = computed(() => unref(getRootSetting).grayMode); | 43 | const getGrayMode = computed(() => unref(getRootSetting).grayMode); |
44 | 44 | ||
45 | +const getLockTime = computed(() => unref(getRootSetting).lockTime); | ||
46 | + | ||
45 | const getLayoutContentMode = computed(() => | 47 | const getLayoutContentMode = computed(() => |
46 | unref(getRootSetting).contentMode === ContentEnum.FULL ? ContentEnum.FULL : ContentEnum.FIXED | 48 | unref(getRootSetting).contentMode === ContentEnum.FULL ? ContentEnum.FULL : ContentEnum.FIXED |
47 | ); | 49 | ); |
@@ -71,5 +73,6 @@ export function useRootSetting() { | @@ -71,5 +73,6 @@ export function useRootSetting() { | ||
71 | getShowSettingButton, | 73 | getShowSettingButton, |
72 | getShowFooter, | 74 | getShowFooter, |
73 | getContentMode, | 75 | getContentMode, |
76 | + getLockTime, | ||
74 | }; | 77 | }; |
75 | } | 78 | } |
src/hooks/web/useLockPage.ts
1 | -import { computed, onUnmounted, watchEffect } from 'vue'; | 1 | +import { computed, onUnmounted, unref, watchEffect } from 'vue'; |
2 | import { useThrottle } from '/@/hooks/core/useThrottle'; | 2 | import { useThrottle } from '/@/hooks/core/useThrottle'; |
3 | 3 | ||
4 | import { appStore } from '/@/store/modules/app'; | 4 | import { appStore } from '/@/store/modules/app'; |
5 | +import { lockStore } from '/@/store/modules/lock'; | ||
5 | import { userStore } from '/@/store/modules/user'; | 6 | import { userStore } from '/@/store/modules/user'; |
7 | +import { useRootSetting } from '../setting/useRootSetting'; | ||
6 | 8 | ||
7 | export function useLockPage() { | 9 | export function useLockPage() { |
10 | + const { getLockTime } = useRootSetting(); | ||
8 | let timeId: TimeoutHandle; | 11 | let timeId: TimeoutHandle; |
9 | 12 | ||
10 | function clear(): void { | 13 | function clear(): void { |
@@ -30,7 +33,7 @@ export function useLockPage() { | @@ -30,7 +33,7 @@ export function useLockPage() { | ||
30 | } | 33 | } |
31 | 34 | ||
32 | function lockPage(): void { | 35 | function lockPage(): void { |
33 | - appStore.commitLockInfoState({ | 36 | + lockStore.commitLockInfoState({ |
34 | isLock: true, | 37 | isLock: true, |
35 | pwd: undefined, | 38 | pwd: undefined, |
36 | }); | 39 | }); |
@@ -54,8 +57,7 @@ export function useLockPage() { | @@ -54,8 +57,7 @@ export function useLockPage() { | ||
54 | const [keyupFn] = useThrottle(resetCalcLockTimeout, 2000); | 57 | const [keyupFn] = useThrottle(resetCalcLockTimeout, 2000); |
55 | 58 | ||
56 | return computed(() => { | 59 | return computed(() => { |
57 | - const openLockPage = appStore.getProjectConfig.lockTime; | ||
58 | - if (openLockPage) { | 60 | + if (unref(getLockTime)) { |
59 | return { onKeyup: keyupFn, onMousemove: keyupFn }; | 61 | return { onKeyup: keyupFn, onMousemove: keyupFn }; |
60 | } else { | 62 | } else { |
61 | clear(); | 63 | clear(); |
@@ -63,3 +65,9 @@ export function useLockPage() { | @@ -63,3 +65,9 @@ export function useLockPage() { | ||
63 | } | 65 | } |
64 | }); | 66 | }); |
65 | } | 67 | } |
68 | + | ||
69 | +export const getIsLock = computed(() => { | ||
70 | + const { getLockInfo } = lockStore; | ||
71 | + const { isLock } = getLockInfo; | ||
72 | + return isLock; | ||
73 | +}); |
src/layouts/default/index.tsx
@@ -6,7 +6,7 @@ import LayoutHeader from './header/LayoutHeader'; | @@ -6,7 +6,7 @@ import LayoutHeader from './header/LayoutHeader'; | ||
6 | 6 | ||
7 | import LayoutContent from './content'; | 7 | import LayoutContent from './content'; |
8 | import LayoutFooter from './footer'; | 8 | import LayoutFooter from './footer'; |
9 | -import LayoutLockPage from './lock'; | 9 | +import LayoutLockPage from './lock/index.vue'; |
10 | import LayoutSideBar from './sider'; | 10 | import LayoutSideBar from './sider'; |
11 | import SettingBtn from './setting/index.vue'; | 11 | import SettingBtn from './setting/index.vue'; |
12 | import LayoutMultipleHeader from './header/LayoutMultipleHeader'; | 12 | import LayoutMultipleHeader from './header/LayoutMultipleHeader'; |
src/layouts/default/lock/LockAction.tsx
@@ -7,9 +7,9 @@ import { BasicForm, useForm } from '/@/components/Form/index'; | @@ -7,9 +7,9 @@ import { BasicForm, useForm } from '/@/components/Form/index'; | ||
7 | 7 | ||
8 | import headerImg from '/@/assets/images/header.jpg'; | 8 | import headerImg from '/@/assets/images/header.jpg'; |
9 | 9 | ||
10 | -import { appStore } from '/@/store/modules/app'; | ||
11 | import { userStore } from '/@/store/modules/user'; | 10 | import { userStore } from '/@/store/modules/user'; |
12 | import { useI18n } from '/@/hooks/web/useI18n'; | 11 | import { useI18n } from '/@/hooks/web/useI18n'; |
12 | +import { lockStore } from '/@/store/modules/lock'; | ||
13 | 13 | ||
14 | const prefixCls = 'lock-modal'; | 14 | const prefixCls = 'lock-modal'; |
15 | export default defineComponent({ | 15 | export default defineComponent({ |
@@ -30,24 +30,16 @@ export default defineComponent({ | @@ -30,24 +30,16 @@ export default defineComponent({ | ||
30 | ], | 30 | ], |
31 | }); | 31 | }); |
32 | 32 | ||
33 | - async function lock(valid = true) { | ||
34 | - let password: string | undefined = ''; | 33 | + async function lock() { |
34 | + const values = (await validateFields()) as any; | ||
35 | + const password: string | undefined = values.password; | ||
36 | + closeModal(); | ||
35 | 37 | ||
36 | - try { | ||
37 | - if (!valid) { | ||
38 | - password = undefined; | ||
39 | - } else { | ||
40 | - const values = (await validateFields()) as any; | ||
41 | - password = values.password; | ||
42 | - } | ||
43 | - closeModal(); | ||
44 | - | ||
45 | - appStore.commitLockInfoState({ | ||
46 | - isLock: true, | ||
47 | - pwd: password, | ||
48 | - }); | ||
49 | - await resetFields(); | ||
50 | - } catch (error) {} | 38 | + lockStore.commitLockInfoState({ |
39 | + isLock: true, | ||
40 | + pwd: password, | ||
41 | + }); | ||
42 | + await resetFields(); | ||
51 | } | 43 | } |
52 | 44 | ||
53 | return () => ( | 45 | return () => ( |
@@ -71,9 +63,6 @@ export default defineComponent({ | @@ -71,9 +63,6 @@ export default defineComponent({ | ||
71 | <Button type="primary" block class="mt-2" onClick={lock}> | 63 | <Button type="primary" block class="mt-2" onClick={lock}> |
72 | {() => t('layout.header.lockScreenBtn')} | 64 | {() => t('layout.header.lockScreenBtn')} |
73 | </Button> | 65 | </Button> |
74 | - <Button block class="mt-2" onClick={lock.bind(null, false)}> | ||
75 | - {() => t('layout.header.notLockScreenPassword')} | ||
76 | - </Button> | ||
77 | </div> | 66 | </div> |
78 | </div> | 67 | </div> |
79 | )} | 68 | )} |
src/layouts/default/lock/index.tsx deleted
100644 → 0
1 | -import { defineComponent, unref, computed } from 'vue'; | ||
2 | -import { appStore } from '/@/store/modules/app'; | ||
3 | -import LockPage from '/@/views/sys/lock/index.vue'; | ||
4 | - | ||
5 | -export default defineComponent({ | ||
6 | - name: 'LayoutLockPage', | ||
7 | - setup() { | ||
8 | - const getIsLockRef = computed(() => { | ||
9 | - const { getLockInfo } = appStore; | ||
10 | - const { isLock } = getLockInfo; | ||
11 | - return isLock; | ||
12 | - }); | ||
13 | - return () => { | ||
14 | - return unref(getIsLockRef) ? <LockPage /> : null; | ||
15 | - }; | ||
16 | - }, | ||
17 | -}); |
src/layouts/default/lock/index.vue
0 → 100644
1 | +<template> | ||
2 | + <transition name="fade-bottom"> | ||
3 | + <LockPage v-if="getIsLock" /> | ||
4 | + </transition> | ||
5 | +</template> | ||
6 | +<script lang="ts"> | ||
7 | + import { defineComponent } from 'vue'; | ||
8 | + import LockPage from '/@/views/sys/lock/index.vue'; | ||
9 | + import { getIsLock } from '/@/hooks/web/useLockPage'; | ||
10 | + export default defineComponent({ | ||
11 | + name: 'LayoutLockPage', | ||
12 | + components: { LockPage }, | ||
13 | + setup() { | ||
14 | + return { getIsLock }; | ||
15 | + }, | ||
16 | + }); | ||
17 | +</script> |
src/locales/lang/en/layout/header.ts
@@ -14,7 +14,6 @@ export default { | @@ -14,7 +14,6 @@ export default { | ||
14 | lockScreenPassword: 'Lock screen password', | 14 | lockScreenPassword: 'Lock screen password', |
15 | lockScreen: 'Lock screen', | 15 | lockScreen: 'Lock screen', |
16 | lockScreenBtn: 'Locking', | 16 | lockScreenBtn: 'Locking', |
17 | - notLockScreenPassword: 'No password lock screen', | ||
18 | 17 | ||
19 | home: 'Home', | 18 | home: 'Home', |
20 | }; | 19 | }; |
src/locales/lang/en/sys/lock.ts
1 | export default { | 1 | export default { |
2 | + unlock: 'Click to unlock', | ||
2 | alert: 'Lock screen password error', | 3 | alert: 'Lock screen password error', |
3 | backToLogin: 'Back to login', | 4 | backToLogin: 'Back to login', |
5 | + back: 'Back', | ||
4 | entry: 'Enter the system', | 6 | entry: 'Enter the system', |
5 | placeholder: 'Please enter the lock screen password or user password', | 7 | placeholder: 'Please enter the lock screen password or user password', |
6 | }; | 8 | }; |
src/locales/lang/zh_CN/layout/header.ts
@@ -15,7 +15,6 @@ export default { | @@ -15,7 +15,6 @@ export default { | ||
15 | lockScreenPassword: '锁屏密码', | 15 | lockScreenPassword: '锁屏密码', |
16 | lockScreen: '锁定屏幕', | 16 | lockScreen: '锁定屏幕', |
17 | lockScreenBtn: '锁定', | 17 | lockScreenBtn: '锁定', |
18 | - notLockScreenPassword: '不设置密码锁屏', | ||
19 | 18 | ||
20 | home: '首页', | 19 | home: '首页', |
21 | }; | 20 | }; |
src/locales/lang/zh_CN/sys/lock.ts
src/store/modules/app.ts
@@ -3,16 +3,10 @@ import type { ProjectConfig } from '/@/types/config'; | @@ -3,16 +3,10 @@ import type { ProjectConfig } from '/@/types/config'; | ||
3 | import { VuexModule, getModule, Module, Mutation, Action } from 'vuex-module-decorators'; | 3 | import { VuexModule, getModule, Module, Mutation, Action } from 'vuex-module-decorators'; |
4 | import store from '/@/store'; | 4 | import store from '/@/store'; |
5 | 5 | ||
6 | -import { PROJ_CFG_KEY, LOCK_INFO_KEY } from '/@/enums/cacheEnum'; | 6 | +import { PROJ_CFG_KEY } from '/@/enums/cacheEnum'; |
7 | 7 | ||
8 | import { hotModuleUnregisterModule } from '/@/utils/helper/vuexHelper'; | 8 | import { hotModuleUnregisterModule } from '/@/utils/helper/vuexHelper'; |
9 | -import { | ||
10 | - setLocal, | ||
11 | - getLocal, | ||
12 | - removeLocal, | ||
13 | - clearSession, | ||
14 | - clearLocal, | ||
15 | -} from '/@/utils/helper/persistent'; | 9 | +import { setLocal, getLocal, clearSession, clearLocal } from '/@/utils/helper/persistent'; |
16 | import { deepMerge } from '/@/utils'; | 10 | import { deepMerge } from '/@/utils'; |
17 | 11 | ||
18 | import { resetRouter } from '/@/router'; | 12 | import { resetRouter } from '/@/router'; |
@@ -37,9 +31,6 @@ class App extends VuexModule { | @@ -37,9 +31,6 @@ class App extends VuexModule { | ||
37 | // project config | 31 | // project config |
38 | private projectConfigState: ProjectConfig | null = getLocal(PROJ_CFG_KEY); | 32 | private projectConfigState: ProjectConfig | null = getLocal(PROJ_CFG_KEY); |
39 | 33 | ||
40 | - // lock info | ||
41 | - private lockInfoState: LockInfo | null = getLocal(LOCK_INFO_KEY); | ||
42 | - | ||
43 | // set main overflow hidden | 34 | // set main overflow hidden |
44 | private lockMainScrollState = false; | 35 | private lockMainScrollState = false; |
45 | 36 | ||
@@ -51,10 +42,6 @@ class App extends VuexModule { | @@ -51,10 +42,6 @@ class App extends VuexModule { | ||
51 | return this.lockMainScrollState; | 42 | return this.lockMainScrollState; |
52 | } | 43 | } |
53 | 44 | ||
54 | - get getLockInfo(): LockInfo { | ||
55 | - return this.lockInfoState || ({} as LockInfo); | ||
56 | - } | ||
57 | - | ||
58 | get getProjectConfig(): ProjectConfig { | 45 | get getProjectConfig(): ProjectConfig { |
59 | return this.projectConfigState || ({} as ProjectConfig); | 46 | return this.projectConfigState || ({} as ProjectConfig); |
60 | } | 47 | } |
@@ -75,18 +62,6 @@ class App extends VuexModule { | @@ -75,18 +62,6 @@ class App extends VuexModule { | ||
75 | setLocal(PROJ_CFG_KEY, this.projectConfigState); | 62 | setLocal(PROJ_CFG_KEY, this.projectConfigState); |
76 | } | 63 | } |
77 | 64 | ||
78 | - @Mutation | ||
79 | - commitLockInfoState(info: LockInfo): void { | ||
80 | - this.lockInfoState = Object.assign({}, this.lockInfoState, info); | ||
81 | - setLocal(LOCK_INFO_KEY, this.lockInfoState); | ||
82 | - } | ||
83 | - | ||
84 | - @Mutation | ||
85 | - resetLockInfo(): void { | ||
86 | - removeLocal(LOCK_INFO_KEY); | ||
87 | - this.lockInfoState = null; | ||
88 | - } | ||
89 | - | ||
90 | @Action | 65 | @Action |
91 | async resumeAllState() { | 66 | async resumeAllState() { |
92 | resetRouter(); | 67 | resetRouter(); |
@@ -111,39 +86,5 @@ class App extends VuexModule { | @@ -111,39 +86,5 @@ class App extends VuexModule { | ||
111 | clearTimeout(timeId); | 86 | clearTimeout(timeId); |
112 | } | 87 | } |
113 | } | 88 | } |
114 | - | ||
115 | - /** | ||
116 | - * @description: unlock page | ||
117 | - */ | ||
118 | - @Action | ||
119 | - public async unLockAction({ password, valid = true }: { password: string; valid?: boolean }) { | ||
120 | - if (!valid) { | ||
121 | - this.resetLockInfo(); | ||
122 | - return true; | ||
123 | - } | ||
124 | - const tryLogin = async () => { | ||
125 | - try { | ||
126 | - const username = userStore.getUserInfoState.username; | ||
127 | - const res = await userStore.login({ username, password }, false); | ||
128 | - if (res) { | ||
129 | - this.resetLockInfo(); | ||
130 | - } | ||
131 | - return res; | ||
132 | - } catch (error) { | ||
133 | - return false; | ||
134 | - } | ||
135 | - }; | ||
136 | - | ||
137 | - if (this.getLockInfo) { | ||
138 | - if (this.getLockInfo.pwd === password) { | ||
139 | - this.resetLockInfo(); | ||
140 | - return true; | ||
141 | - } | ||
142 | - const res = await tryLogin(); | ||
143 | - return res; | ||
144 | - } | ||
145 | - const res = await tryLogin(); | ||
146 | - return res; | ||
147 | - } | ||
148 | } | 89 | } |
149 | export const appStore = getModule<App>(App); | 90 | export const appStore = getModule<App>(App); |
src/store/modules/lock.ts
0 → 100644
1 | +import { VuexModule, getModule, Module, Mutation, Action } from 'vuex-module-decorators'; | ||
2 | +import store from '/@/store'; | ||
3 | + | ||
4 | +import { LOCK_INFO_KEY } from '/@/enums/cacheEnum'; | ||
5 | + | ||
6 | +import { hotModuleUnregisterModule } from '/@/utils/helper/vuexHelper'; | ||
7 | +import { setLocal, getLocal, removeLocal } from '/@/utils/helper/persistent'; | ||
8 | + | ||
9 | +import { userStore } from './user'; | ||
10 | + | ||
11 | +export interface LockInfo { | ||
12 | + pwd: string | undefined; | ||
13 | + isLock: boolean; | ||
14 | +} | ||
15 | + | ||
16 | +const NAME = 'lock'; | ||
17 | +hotModuleUnregisterModule(NAME); | ||
18 | +@Module({ dynamic: true, namespaced: true, store, name: NAME }) | ||
19 | +class Lock extends VuexModule { | ||
20 | + // lock info | ||
21 | + private lockInfoState: LockInfo | null = getLocal(LOCK_INFO_KEY); | ||
22 | + | ||
23 | + get getLockInfo(): LockInfo { | ||
24 | + return this.lockInfoState || ({} as LockInfo); | ||
25 | + } | ||
26 | + | ||
27 | + @Mutation | ||
28 | + commitLockInfoState(info: LockInfo): void { | ||
29 | + this.lockInfoState = Object.assign({}, this.lockInfoState, info); | ||
30 | + setLocal(LOCK_INFO_KEY, this.lockInfoState); | ||
31 | + } | ||
32 | + | ||
33 | + @Mutation | ||
34 | + resetLockInfo(): void { | ||
35 | + removeLocal(LOCK_INFO_KEY); | ||
36 | + this.lockInfoState = null; | ||
37 | + } | ||
38 | + | ||
39 | + /** | ||
40 | + * @description: unlock page | ||
41 | + */ | ||
42 | + @Action | ||
43 | + public async unLockAction({ password }: { password: string }) { | ||
44 | + const tryLogin = async () => { | ||
45 | + try { | ||
46 | + const username = userStore.getUserInfoState.username; | ||
47 | + const res = await userStore.login({ username, password, goHome: false, mode: 'none' }); | ||
48 | + if (res) { | ||
49 | + this.resetLockInfo(); | ||
50 | + } | ||
51 | + return res; | ||
52 | + } catch (error) { | ||
53 | + return false; | ||
54 | + } | ||
55 | + }; | ||
56 | + | ||
57 | + if (this.getLockInfo?.pwd === password) { | ||
58 | + this.resetLockInfo(); | ||
59 | + return true; | ||
60 | + } | ||
61 | + return await tryLogin(); | ||
62 | + } | ||
63 | +} | ||
64 | +export const lockStore = getModule<Lock>(Lock); |
src/store/modules/user.ts
@@ -21,6 +21,7 @@ import { loginApi, getUserInfoById } from '/@/api/sys/user'; | @@ -21,6 +21,7 @@ import { loginApi, getUserInfoById } from '/@/api/sys/user'; | ||
21 | import { setLocal, getLocal, getSession, setSession } from '/@/utils/helper/persistent'; | 21 | import { setLocal, getLocal, getSession, setSession } from '/@/utils/helper/persistent'; |
22 | import { useProjectSetting } from '/@/hooks/setting'; | 22 | import { useProjectSetting } from '/@/hooks/setting'; |
23 | import { useI18n } from '/@/hooks/web/useI18n'; | 23 | import { useI18n } from '/@/hooks/web/useI18n'; |
24 | +import { ErrorMessageMode } from '/@/utils/http/axios/types'; | ||
24 | 25 | ||
25 | export type UserInfo = Omit<GetUserInfoByUserIdModel, 'roles'>; | 26 | export type UserInfo = Omit<GetUserInfoByUserIdModel, 'roles'>; |
26 | 27 | ||
@@ -94,9 +95,16 @@ class User extends VuexModule { | @@ -94,9 +95,16 @@ class User extends VuexModule { | ||
94 | * @description: login | 95 | * @description: login |
95 | */ | 96 | */ |
96 | @Action | 97 | @Action |
97 | - async login(params: LoginParams, goHome = true): Promise<GetUserInfoByUserIdModel | null> { | 98 | + async login( |
99 | + params: LoginParams & { | ||
100 | + goHome?: boolean; | ||
101 | + mode?: ErrorMessageMode; | ||
102 | + } | ||
103 | + ): Promise<GetUserInfoByUserIdModel | null> { | ||
98 | try { | 104 | try { |
99 | - const data = await loginApi(params); | 105 | + const { goHome = true, mode, ...loginParams } = params; |
106 | + const data = await loginApi(loginParams, mode); | ||
107 | + | ||
100 | const { token, userId } = data; | 108 | const { token, userId } = data; |
101 | // get user info | 109 | // get user info |
102 | const userInfo = await this.getUserInfoAction({ userId }); | 110 | const userInfo = await this.getUserInfoAction({ userId }); |
@@ -106,7 +114,7 @@ class User extends VuexModule { | @@ -106,7 +114,7 @@ class User extends VuexModule { | ||
106 | 114 | ||
107 | // const name = FULL_PAGE_NOT_FOUND_ROUTE.name; | 115 | // const name = FULL_PAGE_NOT_FOUND_ROUTE.name; |
108 | // name && router.removeRoute(name); | 116 | // name && router.removeRoute(name); |
109 | - goHome && router.push(PageEnum.BASE_HOME); | 117 | + goHome && router.replace(PageEnum.BASE_HOME); |
110 | return userInfo; | 118 | return userInfo; |
111 | } catch (error) { | 119 | } catch (error) { |
112 | return null; | 120 | return null; |
src/utils/http/axios/Axios.ts
@@ -80,6 +80,7 @@ export class VAxios { | @@ -80,6 +80,7 @@ export class VAxios { | ||
80 | 80 | ||
81 | // 请求拦截器配置处理 | 81 | // 请求拦截器配置处理 |
82 | this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => { | 82 | this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => { |
83 | + // If cancel repeat request is turned on, then cancel repeat request is prohibited | ||
83 | const { headers: { ignoreCancelToken } = { ignoreCancelToken: false } } = config; | 84 | const { headers: { ignoreCancelToken } = { ignoreCancelToken: false } } = config; |
84 | !ignoreCancelToken && axiosCanceler.addPending(config); | 85 | !ignoreCancelToken && axiosCanceler.addPending(config); |
85 | if (requestInterceptors && isFunction(requestInterceptors)) { | 86 | if (requestInterceptors && isFunction(requestInterceptors)) { |
src/utils/http/axios/index.ts
@@ -58,7 +58,7 @@ const transform: AxiosTransform = { | @@ -58,7 +58,7 @@ const transform: AxiosTransform = { | ||
58 | // errorMessageMode=‘modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误 | 58 | // errorMessageMode=‘modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误 |
59 | if (options.errorMessageMode === 'modal') { | 59 | if (options.errorMessageMode === 'modal') { |
60 | createErrorModal({ title: t('sys.api.errorTip'), content: message }); | 60 | createErrorModal({ title: t('sys.api.errorTip'), content: message }); |
61 | - } else { | 61 | + } else if (options.errorMessageMode === 'message') { |
62 | createMessage.error(message); | 62 | createMessage.error(message); |
63 | } | 63 | } |
64 | } | 64 | } |
@@ -201,7 +201,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) { | @@ -201,7 +201,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) { | ||
201 | // 格式化提交参数时间 | 201 | // 格式化提交参数时间 |
202 | formatDate: true, | 202 | formatDate: true, |
203 | // 消息提示类型 | 203 | // 消息提示类型 |
204 | - errorMessageMode: 'none', | 204 | + errorMessageMode: 'message', |
205 | // 接口地址 | 205 | // 接口地址 |
206 | apiUrl: globSetting.apiUrl, | 206 | apiUrl: globSetting.apiUrl, |
207 | }, | 207 | }, |
src/utils/http/axios/types.ts
1 | import type { AxiosRequestConfig } from 'axios'; | 1 | import type { AxiosRequestConfig } from 'axios'; |
2 | import { AxiosTransform } from './axiosTransform'; | 2 | import { AxiosTransform } from './axiosTransform'; |
3 | 3 | ||
4 | +export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined; | ||
5 | + | ||
4 | export interface RequestOptions { | 6 | export interface RequestOptions { |
5 | // 请求参数拼接到url | 7 | // 请求参数拼接到url |
6 | joinParamsToUrl?: boolean; | 8 | joinParamsToUrl?: boolean; |
@@ -13,7 +15,7 @@ export interface RequestOptions { | @@ -13,7 +15,7 @@ export interface RequestOptions { | ||
13 | // 接口地址, 不填则使用默认apiUrl | 15 | // 接口地址, 不填则使用默认apiUrl |
14 | apiUrl?: string; | 16 | apiUrl?: string; |
15 | // 错误消息提示类型 | 17 | // 错误消息提示类型 |
16 | - errorMessageMode?: 'none' | 'modal'; | 18 | + errorMessageMode?: ErrorMessageMode; |
17 | } | 19 | } |
18 | 20 | ||
19 | export interface CreateAxiosOptions extends AxiosRequestConfig { | 21 | export interface CreateAxiosOptions extends AxiosRequestConfig { |
src/views/sys/lock/index.vue
1 | <template> | 1 | <template> |
2 | - <div class="lock-page"> | ||
3 | - <div class="lock-page__entry"> | ||
4 | - <div class="lock-page__header"> | ||
5 | - <img src="../../../assets/images/header.jpg" class="lock-page__header-img" /> | ||
6 | - <p class="lock-page__header-name">{{ realName }}</p> | 2 | + <div :class="prefixCls"> |
3 | + <div :class="`${prefixCls}__unlock`" @click="handleShowForm(false)" v-show="showDate"> | ||
4 | + <LockOutlined /> | ||
5 | + <span>{{ t('sys.lock.unlock') }}</span> | ||
6 | + </div> | ||
7 | + | ||
8 | + <div :class="`${prefixCls}__date`"> | ||
9 | + <div :class="`${prefixCls}__hour`"> | ||
10 | + {{ hour }} | ||
11 | + <span class="meridiem" v-show="showDate">{{ meridiem }}</span> | ||
7 | </div> | 12 | </div> |
8 | - <BasicForm @register="register" v-if="!getIsNotPwd" /> | ||
9 | - <Alert v-if="errMsgRef" type="error" :message="t('alert')" banner /> | ||
10 | - <div class="lock-page__footer"> | ||
11 | - <a-button type="default" class="mt-2 mr-2" @click="goLogin" v-if="!getIsNotPwd"> | ||
12 | - {{ t('sys.lock.backToLogin') }} | ||
13 | - </a-button> | ||
14 | - <a-button type="primary" class="mt-2" @click="unLock(!getIsNotPwd)" :loading="loadingRef"> | ||
15 | - {{ t('sys.lock.entry') }} | ||
16 | - </a-button> | 13 | + <div :class="`${prefixCls}__minute`">{{ minute }} </div> |
14 | + </div> | ||
15 | + <transition name="fade-slide"> | ||
16 | + <div :class="`${prefixCls}-entry`" v-show="!showDate"> | ||
17 | + <div :class="`${prefixCls}-entry-content`"> | ||
18 | + <div :class="`${prefixCls}-entry__header`"> | ||
19 | + <img src="/@/assets/images/header.jpg" :class="`${prefixCls}-entry__header-img`" /> | ||
20 | + <p :class="`${prefixCls}-entry__header-name`">{{ realName }}</p> | ||
21 | + </div> | ||
22 | + <InputPassword :placeholder="t('sys.lock.placeholder')" v-model:value="password" /> | ||
23 | + <span :class="`${prefixCls}-entry__err-msg`" v-if="errMsgRef"> | ||
24 | + {{ t('sys.lock.alert') }} | ||
25 | + </span> | ||
26 | + <div :class="`${prefixCls}-entry__footer`"> | ||
27 | + <a-button | ||
28 | + type="link" | ||
29 | + size="small" | ||
30 | + class="mt-2 mr-2" | ||
31 | + :disabled="loadingRef" | ||
32 | + @click="handleShowForm(true)" | ||
33 | + > | ||
34 | + {{ t('sys.lock.back') }} | ||
35 | + </a-button> | ||
36 | + <a-button | ||
37 | + type="link" | ||
38 | + size="small" | ||
39 | + class="mt-2 mr-2" | ||
40 | + :disabled="loadingRef" | ||
41 | + @click="goLogin" | ||
42 | + > | ||
43 | + {{ t('sys.lock.backToLogin') }} | ||
44 | + </a-button> | ||
45 | + <a-button class="mt-2" type="link" size="small" @click="unLock()" :loading="loadingRef"> | ||
46 | + {{ t('sys.lock.entry') }} | ||
47 | + </a-button> | ||
48 | + </div> | ||
49 | + </div> | ||
17 | </div> | 50 | </div> |
51 | + </transition> | ||
52 | + | ||
53 | + <div :class="`${prefixCls}__footer-date`"> | ||
54 | + <div class="time" v-show="!showDate"> | ||
55 | + {{ hour }}:{{ minute }} <span class="meridiem">{{ meridiem }}</span> | ||
56 | + </div> | ||
57 | + <div class="date"> {{ year }}/{{ month }}/{{ day }} {{ week }} </div> | ||
18 | </div> | 58 | </div> |
19 | </div> | 59 | </div> |
20 | </template> | 60 | </template> |
21 | <script lang="ts"> | 61 | <script lang="ts"> |
22 | import { defineComponent, ref, computed } from 'vue'; | 62 | import { defineComponent, ref, computed } from 'vue'; |
23 | - import { Alert } from 'ant-design-vue'; | ||
24 | - | ||
25 | - import { BasicForm, useForm } from '/@/components/Form'; | 63 | + import { Alert, Input } from 'ant-design-vue'; |
26 | 64 | ||
27 | import { userStore } from '/@/store/modules/user'; | 65 | import { userStore } from '/@/store/modules/user'; |
28 | - import { appStore } from '/@/store/modules/app'; | 66 | + import { lockStore } from '/@/store/modules/lock'; |
29 | import { useI18n } from '/@/hooks/web/useI18n'; | 67 | import { useI18n } from '/@/hooks/web/useI18n'; |
30 | 68 | ||
69 | + import { useNow } from './useNow'; | ||
70 | + import { useDesign } from '/@/hooks/web/useDesign'; | ||
71 | + | ||
72 | + import { LockOutlined } from '@ant-design/icons-vue'; | ||
73 | + | ||
31 | export default defineComponent({ | 74 | export default defineComponent({ |
32 | name: 'LockPage', | 75 | name: 'LockPage', |
33 | - components: { Alert, BasicForm }, | 76 | + components: { Alert, LockOutlined, InputPassword: Input.Password }, |
34 | 77 | ||
35 | setup() { | 78 | setup() { |
79 | + const passwordRef = ref(''); | ||
36 | const loadingRef = ref(false); | 80 | const loadingRef = ref(false); |
37 | const errMsgRef = ref(false); | 81 | const errMsgRef = ref(false); |
82 | + const showDate = ref(true); | ||
83 | + | ||
84 | + const { prefixCls } = useDesign('lock-page'); | ||
85 | + | ||
86 | + const { start, stop, ...state } = useNow(true); | ||
38 | 87 | ||
39 | const { t } = useI18n(); | 88 | const { t } = useI18n(); |
40 | - const [register, { validateFields }] = useForm({ | ||
41 | - showActionButtonGroup: false, | ||
42 | - schemas: [ | ||
43 | - { | ||
44 | - field: 'password', | ||
45 | - label: '', | ||
46 | - component: 'InputPassword', | ||
47 | - componentProps: { | ||
48 | - style: { width: '100%' }, | ||
49 | - placeholder: t('sys.lock.placeholder'), | ||
50 | - }, | ||
51 | - rules: [{ required: true }], | ||
52 | - }, | ||
53 | - ], | ||
54 | - }); | 89 | + |
55 | const realName = computed(() => { | 90 | const realName = computed(() => { |
56 | const { realName } = userStore.getUserInfoState || {}; | 91 | const { realName } = userStore.getUserInfoState || {}; |
57 | return realName; | 92 | return realName; |
58 | }); | 93 | }); |
59 | 94 | ||
60 | - const getIsNotPwd = computed(() => { | ||
61 | - if (!appStore.getLockInfo) { | ||
62 | - return true; | ||
63 | - } | ||
64 | - return appStore.getLockInfo.pwd === undefined; | ||
65 | - }); | ||
66 | - | ||
67 | /** | 95 | /** |
68 | * @description: unLock | 96 | * @description: unLock |
69 | */ | 97 | */ |
70 | - async function unLock(valid = true) { | ||
71 | - let password = ''; | ||
72 | - if (valid) { | ||
73 | - try { | ||
74 | - const values = (await validateFields()) as any; | ||
75 | - password = values.password; | ||
76 | - } catch (error) { | ||
77 | - return; | ||
78 | - } | 98 | + async function unLock() { |
99 | + if (!passwordRef.value) { | ||
100 | + return; | ||
79 | } | 101 | } |
102 | + let password = passwordRef.value; | ||
80 | try { | 103 | try { |
81 | loadingRef.value = true; | 104 | loadingRef.value = true; |
82 | - const res = await appStore.unLockAction({ password, valid }); | 105 | + const res = await lockStore.unLockAction({ password }); |
83 | errMsgRef.value = !res; | 106 | errMsgRef.value = !res; |
84 | } finally { | 107 | } finally { |
85 | loadingRef.value = false; | 108 | loadingRef.value = false; |
@@ -88,67 +111,190 @@ | @@ -88,67 +111,190 @@ | ||
88 | 111 | ||
89 | function goLogin() { | 112 | function goLogin() { |
90 | userStore.loginOut(true); | 113 | userStore.loginOut(true); |
91 | - appStore.resetLockInfo(); | 114 | + lockStore.resetLockInfo(); |
115 | + } | ||
116 | + | ||
117 | + function handleShowForm(show = false) { | ||
118 | + showDate.value = show; | ||
92 | } | 119 | } |
93 | 120 | ||
94 | return { | 121 | return { |
95 | - register, | ||
96 | - getIsNotPwd, | ||
97 | goLogin, | 122 | goLogin, |
98 | realName, | 123 | realName, |
99 | unLock, | 124 | unLock, |
100 | errMsgRef, | 125 | errMsgRef, |
101 | loadingRef, | 126 | loadingRef, |
102 | t, | 127 | t, |
128 | + prefixCls, | ||
129 | + showDate, | ||
130 | + password: passwordRef, | ||
131 | + handleShowForm, | ||
132 | + ...state, | ||
103 | }; | 133 | }; |
104 | }, | 134 | }, |
105 | }); | 135 | }); |
106 | </script> | 136 | </script> |
107 | <style lang="less" scoped> | 137 | <style lang="less" scoped> |
108 | @import (reference) '../../../design/index.less'; | 138 | @import (reference) '../../../design/index.less'; |
139 | + @prefix-cls: ~'@{namespace}-lock-page'; | ||
109 | 140 | ||
110 | - .lock-page { | 141 | + .@{prefix-cls} { |
111 | position: fixed; | 142 | position: fixed; |
112 | top: 0; | 143 | top: 0; |
144 | + right: 0; | ||
145 | + bottom: 0; | ||
113 | left: 0; | 146 | left: 0; |
114 | - z-index: 999999; | 147 | + z-index: 3000; |
115 | display: flex; | 148 | display: flex; |
116 | width: 100vw; | 149 | width: 100vw; |
117 | height: 100vh; | 150 | height: 100vh; |
118 | - background: url(../../../assets/images/lock-page.jpg) no-repeat; | ||
119 | - background-size: 100% 100%; | 151 | + // background: rgba(23, 27, 41); |
152 | + background: #000; | ||
120 | align-items: center; | 153 | align-items: center; |
121 | - justify-content: flex-end; | 154 | + justify-content: center; |
155 | + | ||
156 | + &__unlock { | ||
157 | + position: absolute; | ||
158 | + top: 0; | ||
159 | + left: 50%; | ||
160 | + display: flex; | ||
161 | + height: 50px; | ||
162 | + padding-top: 20px; | ||
163 | + font-size: 18px; | ||
164 | + color: #fff; | ||
165 | + cursor: pointer; | ||
166 | + transform: translate(-50%, 0); | ||
167 | + flex-direction: column; | ||
168 | + align-items: center; | ||
169 | + justify-content: space-between; | ||
170 | + transition: all 0.3s; | ||
171 | + } | ||
172 | + | ||
173 | + &__date { | ||
174 | + display: flex; | ||
175 | + width: 100vw; | ||
176 | + height: 100vh; | ||
177 | + align-items: center; | ||
178 | + justify-content: center; | ||
179 | + } | ||
122 | 180 | ||
123 | - &__entry { | 181 | + &__hour { |
124 | position: relative; | 182 | position: relative; |
125 | - width: 400px; | ||
126 | - // height: 260px; | ||
127 | - padding: 80px 50px 50px 50px; | ||
128 | - margin-right: 50px; | ||
129 | - background: #fff; | ||
130 | - border-radius: 6px; | 183 | + margin-right: 80px; |
184 | + | ||
185 | + .meridiem { | ||
186 | + position: absolute; | ||
187 | + top: 20px; | ||
188 | + left: 20px; | ||
189 | + font-size: 26px; | ||
190 | + } | ||
191 | + @media (max-width: @screen-xs) { | ||
192 | + margin-right: 20px; | ||
193 | + } | ||
131 | } | 194 | } |
132 | 195 | ||
133 | - &__header { | 196 | + &__hour, |
197 | + &__minute { | ||
198 | + display: flex; | ||
199 | + width: 40%; | ||
200 | + height: 74%; | ||
201 | + // font-size: 50em; | ||
202 | + font-weight: 700; | ||
203 | + color: #bababa; | ||
204 | + background: #141313; | ||
205 | + border-radius: 30px; | ||
206 | + justify-content: center; | ||
207 | + align-items: center; | ||
208 | + // .respond-to(large-only, { font-size: 25em;}); | ||
209 | + // .respond-to(large-only, { font-size: 30em;}); | ||
210 | + @media (min-width: @screen-xxxl-min) { | ||
211 | + font-size: 46em; | ||
212 | + } | ||
213 | + @media (min-width: @screen-xl-max) and (max-width: @screen-xxl-max) { | ||
214 | + font-size: 38em; | ||
215 | + } | ||
216 | + | ||
217 | + @media (min-width: @screen-lg-max) and (max-width: @screen-xl-max) { | ||
218 | + font-size: 30em; | ||
219 | + } | ||
220 | + @media (min-width: @screen-md-max) and (max-width: @screen-lg-max) { | ||
221 | + font-size: 23em; | ||
222 | + } | ||
223 | + @media (min-width: @screen-sm-max) and (max-width: @screen-md-max) { | ||
224 | + font-size: 19em; | ||
225 | + } | ||
226 | + @media (min-width: @screen-xs-max) and (max-width: @screen-sm-max) { | ||
227 | + font-size: 13em; | ||
228 | + } | ||
229 | + @media (max-width: @screen-xs) { | ||
230 | + height: 50%; | ||
231 | + font-size: 6em; | ||
232 | + border-radius: 20px; | ||
233 | + } | ||
234 | + } | ||
235 | + | ||
236 | + &__footer-date { | ||
134 | position: absolute; | 237 | position: absolute; |
135 | - top: -35px; | ||
136 | - left: calc(50% - 45px); | ||
137 | - width: auto; | ||
138 | - text-align: center; | 238 | + bottom: 20px; |
239 | + left: 50%; | ||
240 | + font-family: helvetica; | ||
241 | + color: #bababa; | ||
242 | + transform: translate(-50%, 0); | ||
139 | 243 | ||
140 | - &-img { | ||
141 | - width: 70px; | ||
142 | - border-radius: 50%; | 244 | + .time { |
245 | + font-size: 50px; | ||
246 | + | ||
247 | + .meridiem { | ||
248 | + font-size: 32px; | ||
249 | + } | ||
143 | } | 250 | } |
144 | 251 | ||
145 | - &-name { | ||
146 | - margin-top: 5px; | 252 | + .date { |
253 | + font-size: 26px; | ||
147 | } | 254 | } |
148 | } | 255 | } |
149 | 256 | ||
150 | - &__footer { | ||
151 | - text-align: center; | 257 | + &-entry { |
258 | + position: absolute; | ||
259 | + top: 0; | ||
260 | + left: 0; | ||
261 | + display: flex; | ||
262 | + width: 100%; | ||
263 | + height: 100%; | ||
264 | + background: rgba(0, 0, 0, 0.5); | ||
265 | + backdrop-filter: blur(10px); | ||
266 | + justify-content: center; | ||
267 | + align-items: center; | ||
268 | + | ||
269 | + &-content { | ||
270 | + width: 260px; | ||
271 | + } | ||
272 | + | ||
273 | + &__header { | ||
274 | + text-align: center; | ||
275 | + | ||
276 | + &-img { | ||
277 | + width: 70px; | ||
278 | + border-radius: 50%; | ||
279 | + } | ||
280 | + | ||
281 | + &-name { | ||
282 | + margin-top: 5px; | ||
283 | + font-weight: 500; | ||
284 | + color: #bababa; | ||
285 | + } | ||
286 | + } | ||
287 | + | ||
288 | + &__err-msg { | ||
289 | + display: inline-block; | ||
290 | + margin-top: 10px; | ||
291 | + color: @error-color; | ||
292 | + } | ||
293 | + | ||
294 | + &__footer { | ||
295 | + display: flex; | ||
296 | + justify-content: space-between; | ||
297 | + } | ||
152 | } | 298 | } |
153 | } | 299 | } |
154 | </style> | 300 | </style> |
src/views/sys/lock/useNow.ts
0 → 100644
1 | +import moment from 'moment'; | ||
2 | +import { reactive, toRefs } from 'vue'; | ||
3 | +import { tryOnMounted, tryOnUnmounted } from '/@/utils/helper/vueHelper'; | ||
4 | +import { useLocaleSetting } from '/@/hooks/setting/useLocaleSetting'; | ||
5 | + | ||
6 | +export function useNow(immediate = true) { | ||
7 | + const { getLang } = useLocaleSetting(); | ||
8 | + const localData = moment.localeData(getLang.value); | ||
9 | + let timer: IntervalHandle; | ||
10 | + | ||
11 | + const state = reactive({ | ||
12 | + year: 0, | ||
13 | + month: 0, | ||
14 | + week: '', | ||
15 | + day: 0, | ||
16 | + hour: '', | ||
17 | + minute: '', | ||
18 | + second: 0, | ||
19 | + meridiem: '', | ||
20 | + }); | ||
21 | + | ||
22 | + const update = () => { | ||
23 | + const now = moment(); | ||
24 | + | ||
25 | + const h = now.format('HH'); | ||
26 | + const m = now.format('mm'); | ||
27 | + const s = now.get('s'); | ||
28 | + | ||
29 | + state.year = now.get('y'); | ||
30 | + state.month = now.get('M'); | ||
31 | + state.week = localData.weekdays()[now.day()]; | ||
32 | + state.day = now.get('D'); | ||
33 | + state.hour = h; | ||
34 | + state.minute = m; | ||
35 | + state.second = s; | ||
36 | + | ||
37 | + state.meridiem = localData.meridiem(Number(h), Number(h), true); | ||
38 | + }; | ||
39 | + | ||
40 | + function start() { | ||
41 | + update(); | ||
42 | + clearInterval(timer); | ||
43 | + timer = setInterval(() => update(), 1000); | ||
44 | + } | ||
45 | + | ||
46 | + function stop() { | ||
47 | + clearInterval(timer); | ||
48 | + } | ||
49 | + | ||
50 | + tryOnMounted(() => { | ||
51 | + immediate && start(); | ||
52 | + }); | ||
53 | + | ||
54 | + tryOnUnmounted(() => { | ||
55 | + stop(); | ||
56 | + }); | ||
57 | + | ||
58 | + return { | ||
59 | + ...toRefs(state), | ||
60 | + start, | ||
61 | + stop, | ||
62 | + }; | ||
63 | +} |