Showing
21 changed files
with
1664 additions
and
0 deletions
src/sys/error-log/DetailModal.vue
0 → 100644
1 | +<template> | |
2 | + <BasicModal :width="800" :title="t('sys.errorLog.tableActionDesc')" v-bind="$attrs"> | |
3 | + <Description :data="info" @register="register" /> | |
4 | + </BasicModal> | |
5 | +</template> | |
6 | +<script lang="ts" setup> | |
7 | + import type { PropType } from 'vue'; | |
8 | + import type { ErrorLogInfo } from '/#/store'; | |
9 | + import { BasicModal } from '/@/components/Modal/index'; | |
10 | + import { Description, useDescription } from '/@/components/Description/index'; | |
11 | + import { useI18n } from '/@/hooks/web/useI18n'; | |
12 | + import { getDescSchema } from './data'; | |
13 | + defineProps({ | |
14 | + info: { | |
15 | + type: Object as PropType<ErrorLogInfo>, | |
16 | + default: null, | |
17 | + }, | |
18 | + }); | |
19 | + | |
20 | + const { t } = useI18n(); | |
21 | + | |
22 | + const [register] = useDescription({ | |
23 | + column: 2, | |
24 | + schema: getDescSchema()!, | |
25 | + }); | |
26 | +</script> | ... | ... |
src/sys/error-log/data.tsx
0 → 100644
1 | +import { Tag } from 'ant-design-vue'; | |
2 | +import { BasicColumn } from '/@/components/Table/index'; | |
3 | +import { ErrorTypeEnum } from '/@/enums/exceptionEnum'; | |
4 | +import { useI18n } from '/@/hooks/web/useI18n'; | |
5 | + | |
6 | +const { t } = useI18n(); | |
7 | + | |
8 | +export function getColumns(): BasicColumn[] { | |
9 | + return [ | |
10 | + { | |
11 | + dataIndex: 'type', | |
12 | + title: t('sys.errorLog.tableColumnType'), | |
13 | + width: 80, | |
14 | + customRender: ({ text }) => { | |
15 | + const color = | |
16 | + text === ErrorTypeEnum.VUE | |
17 | + ? 'green' | |
18 | + : text === ErrorTypeEnum.RESOURCE | |
19 | + ? 'cyan' | |
20 | + : text === ErrorTypeEnum.PROMISE | |
21 | + ? 'blue' | |
22 | + : ErrorTypeEnum.AJAX | |
23 | + ? 'red' | |
24 | + : 'purple'; | |
25 | + return <Tag color={color}>{() => text}</Tag>; | |
26 | + }, | |
27 | + }, | |
28 | + { | |
29 | + dataIndex: 'url', | |
30 | + title: 'URL', | |
31 | + width: 200, | |
32 | + }, | |
33 | + { | |
34 | + dataIndex: 'time', | |
35 | + title: t('sys.errorLog.tableColumnDate'), | |
36 | + width: 160, | |
37 | + }, | |
38 | + { | |
39 | + dataIndex: 'file', | |
40 | + title: t('sys.errorLog.tableColumnFile'), | |
41 | + width: 200, | |
42 | + }, | |
43 | + { | |
44 | + dataIndex: 'name', | |
45 | + title: 'Name', | |
46 | + width: 200, | |
47 | + }, | |
48 | + { | |
49 | + dataIndex: 'message', | |
50 | + title: t('sys.errorLog.tableColumnMsg'), | |
51 | + width: 300, | |
52 | + }, | |
53 | + { | |
54 | + dataIndex: 'stack', | |
55 | + title: t('sys.errorLog.tableColumnStackMsg'), | |
56 | + }, | |
57 | + ]; | |
58 | +} | |
59 | + | |
60 | +export function getDescSchema(): any { | |
61 | + return getColumns().map((column) => { | |
62 | + return { | |
63 | + field: column.dataIndex!, | |
64 | + label: column.title, | |
65 | + }; | |
66 | + }); | |
67 | +} | ... | ... |
src/sys/error-log/index.vue
0 → 100644
1 | +<template> | |
2 | + <div class="p-4"> | |
3 | + <template v-for="src in imgList" :key="src"> | |
4 | + <img :src="src" v-show="false" /> | |
5 | + </template> | |
6 | + <BasicTable @register="register" class="error-handle-table"> | |
7 | + <template #toolbar> | |
8 | + <a-button @click="fireVueError" type="primary"> | |
9 | + {{ t('sys.errorLog.fireVueError') }} | |
10 | + </a-button> | |
11 | + <a-button @click="fireResourceError" type="primary"> | |
12 | + {{ t('sys.errorLog.fireResourceError') }} | |
13 | + </a-button> | |
14 | + <!-- <a-button @click="fireAjaxError" type="primary"> | |
15 | + {{ t('sys.errorLog.fireAjaxError') }} | |
16 | + </a-button> --> | |
17 | + </template> | |
18 | + <template #action="{ record }"> | |
19 | + <TableAction | |
20 | + :actions="[ | |
21 | + { label: t('sys.errorLog.tableActionDesc'), onClick: handleDetail.bind(null, record) }, | |
22 | + ]" | |
23 | + /> | |
24 | + </template> | |
25 | + </BasicTable> | |
26 | + <DetailModal :info="rowInfo" @register="registerModal" /> | |
27 | + </div> | |
28 | +</template> | |
29 | + | |
30 | +<script lang="ts" setup> | |
31 | + import type { ErrorLogInfo } from '/#/store'; | |
32 | + import { watch, ref, nextTick } from 'vue'; | |
33 | + import DetailModal from './DetailModal.vue'; | |
34 | + import { BasicTable, useTable, TableAction } from '/@/components/Table/index'; | |
35 | + import { useModal } from '/@/components/Modal'; | |
36 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
37 | + import { useI18n } from '/@/hooks/web/useI18n'; | |
38 | + import { useErrorLogStore } from '/@/store/modules/errorLog'; | |
39 | + // import { fireErrorApi } from '/@/api/demo/error'; | |
40 | + import { getColumns } from './data'; | |
41 | + import { cloneDeep } from 'lodash-es'; | |
42 | + | |
43 | + const rowInfo = ref<ErrorLogInfo>(); | |
44 | + const imgList = ref<string[]>([]); | |
45 | + | |
46 | + const { t } = useI18n(); | |
47 | + const errorLogStore = useErrorLogStore(); | |
48 | + const [register, { setTableData }] = useTable({ | |
49 | + title: t('sys.errorLog.tableTitle'), | |
50 | + columns: getColumns(), | |
51 | + actionColumn: { | |
52 | + width: 200, | |
53 | + title: 'Action', | |
54 | + dataIndex: 'action', | |
55 | + slots: { customRender: 'action' }, | |
56 | + }, | |
57 | + }); | |
58 | + const [registerModal, { openModal }] = useModal(); | |
59 | + | |
60 | + watch( | |
61 | + () => errorLogStore.getErrorLogInfoList, | |
62 | + (list) => { | |
63 | + nextTick(() => { | |
64 | + setTableData(cloneDeep(list)); | |
65 | + }); | |
66 | + }, | |
67 | + { | |
68 | + immediate: true, | |
69 | + } | |
70 | + ); | |
71 | + const { createMessage } = useMessage(); | |
72 | + if (import.meta.env.DEV) { | |
73 | + createMessage.info(t('sys.errorLog.enableMessage')); | |
74 | + } | |
75 | + // 查看详情 | |
76 | + function handleDetail(row: ErrorLogInfo) { | |
77 | + rowInfo.value = row; | |
78 | + openModal(true); | |
79 | + } | |
80 | + | |
81 | + function fireVueError() { | |
82 | + throw new Error('fire vue error!'); | |
83 | + } | |
84 | + | |
85 | + function fireResourceError() { | |
86 | + imgList.value.push(`${new Date().getTime()}.png`); | |
87 | + } | |
88 | + | |
89 | + // async function fireAjaxError() { | |
90 | + // await fireErrorApi(); | |
91 | + // } | |
92 | +</script> | ... | ... |
src/sys/exception/Exception.vue
0 → 100644
1 | +<script lang="tsx"> | |
2 | + import type { PropType } from 'vue'; | |
3 | + import { Result, Button } from 'ant-design-vue'; | |
4 | + import { defineComponent, ref, computed, unref } from 'vue'; | |
5 | + import { ExceptionEnum } from '/@/enums/exceptionEnum'; | |
6 | + import notDataSvg from '/@/assets/svg/no-data.svg'; | |
7 | + import netWorkSvg from '/@/assets/svg/net-error.svg'; | |
8 | + import { useRoute } from 'vue-router'; | |
9 | + import { useDesign } from '/@/hooks/web/useDesign'; | |
10 | + import { useI18n } from '/@/hooks/web/useI18n'; | |
11 | + import { useGo, useRedo } from '/@/hooks/web/usePage'; | |
12 | + import { PageEnum } from '/@/enums/pageEnum'; | |
13 | + | |
14 | + interface MapValue { | |
15 | + title: string; | |
16 | + subTitle: string; | |
17 | + btnText?: string; | |
18 | + icon?: string; | |
19 | + handler?: Fn; | |
20 | + status?: string; | |
21 | + } | |
22 | + | |
23 | + export default defineComponent({ | |
24 | + name: 'ErrorPage', | |
25 | + props: { | |
26 | + // 状态码 | |
27 | + status: { | |
28 | + type: Number as PropType<number>, | |
29 | + default: ExceptionEnum.PAGE_NOT_FOUND, | |
30 | + }, | |
31 | + | |
32 | + title: { | |
33 | + type: String as PropType<string>, | |
34 | + default: '', | |
35 | + }, | |
36 | + | |
37 | + subTitle: { | |
38 | + type: String as PropType<string>, | |
39 | + default: '', | |
40 | + }, | |
41 | + | |
42 | + full: { | |
43 | + type: Boolean as PropType<boolean>, | |
44 | + default: false, | |
45 | + }, | |
46 | + }, | |
47 | + setup(props) { | |
48 | + const statusMapRef = ref(new Map<string | number, MapValue>()); | |
49 | + const { query } = useRoute(); | |
50 | + const go = useGo(); | |
51 | + const redo = useRedo(); | |
52 | + const { t } = useI18n(); | |
53 | + const { prefixCls } = useDesign('app-exception-page'); | |
54 | + | |
55 | + const getStatus = computed(() => { | |
56 | + const { status: routeStatus } = query; | |
57 | + const { status } = props; | |
58 | + return Number(routeStatus) || status; | |
59 | + }); | |
60 | + | |
61 | + const getMapValue = computed((): MapValue => { | |
62 | + return unref(statusMapRef).get(unref(getStatus)) as MapValue; | |
63 | + }); | |
64 | + | |
65 | + const backLoginI18n = t('sys.exception.backLogin'); | |
66 | + const backHomeI18n = t('sys.exception.backHome'); | |
67 | + | |
68 | + unref(statusMapRef).set(ExceptionEnum.PAGE_NOT_ACCESS, { | |
69 | + title: '403', | |
70 | + status: `${ExceptionEnum.PAGE_NOT_ACCESS}`, | |
71 | + subTitle: t('sys.exception.subTitle403'), | |
72 | + btnText: props.full ? backLoginI18n : backHomeI18n, | |
73 | + handler: () => (props.full ? go(PageEnum.BASE_LOGIN) : go()), | |
74 | + }); | |
75 | + | |
76 | + unref(statusMapRef).set(ExceptionEnum.PAGE_NOT_FOUND, { | |
77 | + title: '404', | |
78 | + status: `${ExceptionEnum.PAGE_NOT_FOUND}`, | |
79 | + subTitle: t('sys.exception.subTitle404'), | |
80 | + btnText: props.full ? backLoginI18n : backHomeI18n, | |
81 | + handler: () => (props.full ? go(PageEnum.BASE_LOGIN) : go()), | |
82 | + }); | |
83 | + | |
84 | + unref(statusMapRef).set(ExceptionEnum.ERROR, { | |
85 | + title: '500', | |
86 | + status: `${ExceptionEnum.ERROR}`, | |
87 | + subTitle: t('sys.exception.subTitle500'), | |
88 | + btnText: backHomeI18n, | |
89 | + handler: () => go(), | |
90 | + }); | |
91 | + | |
92 | + unref(statusMapRef).set(ExceptionEnum.PAGE_NOT_DATA, { | |
93 | + title: t('sys.exception.noDataTitle'), | |
94 | + subTitle: '', | |
95 | + btnText: t('common.redo'), | |
96 | + handler: () => redo(), | |
97 | + icon: notDataSvg, | |
98 | + }); | |
99 | + | |
100 | + unref(statusMapRef).set(ExceptionEnum.NET_WORK_ERROR, { | |
101 | + title: t('sys.exception.networkErrorTitle'), | |
102 | + subTitle: t('sys.exception.networkErrorSubTitle'), | |
103 | + btnText: t('common.redo'), | |
104 | + handler: () => redo(), | |
105 | + icon: netWorkSvg, | |
106 | + }); | |
107 | + | |
108 | + return () => { | |
109 | + const { title, subTitle, btnText, icon, handler, status } = unref(getMapValue) || {}; | |
110 | + return ( | |
111 | + <Result | |
112 | + class={prefixCls} | |
113 | + status={status as any} | |
114 | + title={props.title || title} | |
115 | + sub-title={props.subTitle || subTitle} | |
116 | + > | |
117 | + {{ | |
118 | + extra: () => | |
119 | + btnText && ( | |
120 | + <Button type="primary" onClick={handler}> | |
121 | + {() => btnText} | |
122 | + </Button> | |
123 | + ), | |
124 | + icon: () => (icon ? <img src={icon} /> : null), | |
125 | + }} | |
126 | + </Result> | |
127 | + ); | |
128 | + }; | |
129 | + }, | |
130 | + }); | |
131 | +</script> | |
132 | +<style lang="less"> | |
133 | + @prefix-cls: ~'@{namespace}-app-exception-page'; | |
134 | + | |
135 | + .@{prefix-cls} { | |
136 | + display: flex; | |
137 | + align-items: center; | |
138 | + flex-direction: column; | |
139 | + | |
140 | + .ant-result-icon { | |
141 | + img { | |
142 | + max-width: 400px; | |
143 | + max-height: 300px; | |
144 | + } | |
145 | + } | |
146 | + } | |
147 | +</style> | ... | ... |
src/sys/exception/index.ts
0 → 100644
1 | +export { default as Exception } from './Exception.vue'; | ... | ... |
src/sys/iframe/FrameBlank.vue
0 → 100644
src/sys/iframe/index.vue
0 → 100644
1 | +<template> | |
2 | + <div :class="prefixCls" :style="getWrapStyle"> | |
3 | + <Spin :spinning="loading" size="large" :style="getWrapStyle"> | |
4 | + <iframe | |
5 | + :src="frameSrc" | |
6 | + :class="`${prefixCls}__main`" | |
7 | + ref="frameRef" | |
8 | + @load="hideLoading" | |
9 | + ></iframe> | |
10 | + </Spin> | |
11 | + </div> | |
12 | +</template> | |
13 | +<script lang="ts" setup> | |
14 | + import type { CSSProperties } from 'vue'; | |
15 | + import { ref, unref, computed } from 'vue'; | |
16 | + import { Spin } from 'ant-design-vue'; | |
17 | + import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn'; | |
18 | + import { propTypes } from '/@/utils/propTypes'; | |
19 | + import { useDesign } from '/@/hooks/web/useDesign'; | |
20 | + import { useLayoutHeight } from '/@/layouts/default/content/useContentViewHeight'; | |
21 | + | |
22 | + defineProps({ | |
23 | + frameSrc: propTypes.string.def(''), | |
24 | + }); | |
25 | + | |
26 | + const loading = ref(true); | |
27 | + const topRef = ref(50); | |
28 | + const heightRef = ref(window.innerHeight); | |
29 | + const frameRef = ref<HTMLFrameElement>(); | |
30 | + const { headerHeightRef } = useLayoutHeight(); | |
31 | + | |
32 | + const { prefixCls } = useDesign('iframe-page'); | |
33 | + useWindowSizeFn(calcHeight, 150, { immediate: true }); | |
34 | + | |
35 | + const getWrapStyle = computed((): CSSProperties => { | |
36 | + return { | |
37 | + height: `${unref(heightRef)}px`, | |
38 | + }; | |
39 | + }); | |
40 | + | |
41 | + function calcHeight() { | |
42 | + const iframe = unref(frameRef); | |
43 | + if (!iframe) { | |
44 | + return; | |
45 | + } | |
46 | + const top = headerHeightRef.value; | |
47 | + topRef.value = top; | |
48 | + heightRef.value = window.innerHeight - top; | |
49 | + const clientHeight = document.documentElement.clientHeight - top; | |
50 | + iframe.style.height = `${clientHeight}px`; | |
51 | + } | |
52 | + | |
53 | + function hideLoading() { | |
54 | + loading.value = false; | |
55 | + calcHeight(); | |
56 | + } | |
57 | +</script> | |
58 | +<style lang="less" scoped> | |
59 | + @prefix-cls: ~'@{namespace}-iframe-page'; | |
60 | + | |
61 | + .@{prefix-cls} { | |
62 | + .ant-spin-nested-loading { | |
63 | + position: relative; | |
64 | + height: 100%; | |
65 | + | |
66 | + .ant-spin-container { | |
67 | + width: 100%; | |
68 | + height: 100%; | |
69 | + padding: 10px; | |
70 | + } | |
71 | + } | |
72 | + | |
73 | + &__mask { | |
74 | + position: absolute; | |
75 | + top: 0; | |
76 | + left: 0; | |
77 | + width: 100%; | |
78 | + height: 100%; | |
79 | + } | |
80 | + | |
81 | + &__main { | |
82 | + width: 100%; | |
83 | + height: 100%; | |
84 | + overflow: hidden; | |
85 | + background-color: @component-background; | |
86 | + border: 0; | |
87 | + box-sizing: border-box; | |
88 | + } | |
89 | + } | |
90 | +</style> | ... | ... |
src/sys/lock/LockPage.vue
0 → 100644
1 | +<template> | |
2 | + <div | |
3 | + :class="prefixCls" | |
4 | + class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center" | |
5 | + > | |
6 | + <div | |
7 | + :class="`${prefixCls}__unlock`" | |
8 | + class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2" | |
9 | + @click="handleShowForm(false)" | |
10 | + v-show="showDate" | |
11 | + > | |
12 | + <LockOutlined /> | |
13 | + <span>{{ t('sys.lock.unlock') }}</span> | |
14 | + </div> | |
15 | + | |
16 | + <div class="flex w-screen h-screen justify-center items-center"> | |
17 | + <div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5"> | |
18 | + <span>{{ hour }}</span> | |
19 | + <span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate"> | |
20 | + {{ meridiem }} | |
21 | + </span> | |
22 | + </div> | |
23 | + <div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `"> | |
24 | + <span> {{ minute }}</span> | |
25 | + </div> | |
26 | + </div> | |
27 | + <transition name="fade-slide"> | |
28 | + <div :class="`${prefixCls}-entry`" v-show="!showDate"> | |
29 | + <div :class="`${prefixCls}-entry-content`"> | |
30 | + <div :class="`${prefixCls}-entry__header enter-x`"> | |
31 | + <img :src="userinfo.avatar || headerImg" :class="`${prefixCls}-entry__header-img`" /> | |
32 | + <p :class="`${prefixCls}-entry__header-name`"> | |
33 | + {{ userinfo.realName }} | |
34 | + </p> | |
35 | + </div> | |
36 | + <InputPassword | |
37 | + :placeholder="t('sys.lock.placeholder')" | |
38 | + class="enter-x" | |
39 | + v-model:value="password" | |
40 | + /> | |
41 | + <span :class="`${prefixCls}-entry__err-msg enter-x`" v-if="errMsg"> | |
42 | + {{ t('sys.lock.alert') }} | |
43 | + </span> | |
44 | + <div :class="`${prefixCls}-entry__footer enter-x`"> | |
45 | + <a-button | |
46 | + type="link" | |
47 | + size="small" | |
48 | + class="mt-2 mr-2 enter-x" | |
49 | + :disabled="loading" | |
50 | + @click="handleShowForm(true)" | |
51 | + > | |
52 | + {{ t('common.back') }} | |
53 | + </a-button> | |
54 | + <a-button | |
55 | + type="link" | |
56 | + size="small" | |
57 | + class="mt-2 mr-2 enter-x" | |
58 | + :disabled="loading" | |
59 | + @click="goLogin" | |
60 | + > | |
61 | + {{ t('sys.lock.backToLogin') }} | |
62 | + </a-button> | |
63 | + <a-button class="mt-2" type="link" size="small" @click="unLock()" :loading="loading"> | |
64 | + {{ t('sys.lock.entry') }} | |
65 | + </a-button> | |
66 | + </div> | |
67 | + </div> | |
68 | + </div> | |
69 | + </transition> | |
70 | + | |
71 | + <div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y"> | |
72 | + <div class="text-5xl mb-4 enter-x" v-show="!showDate"> | |
73 | + {{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span> | |
74 | + </div> | |
75 | + <div class="text-2xl"> {{ year }}/{{ month }}/{{ day }} {{ week }} </div> | |
76 | + </div> | |
77 | + </div> | |
78 | +</template> | |
79 | +<script lang="ts" setup> | |
80 | + import { ref, computed } from 'vue'; | |
81 | + import { Input } from 'ant-design-vue'; | |
82 | + import { useUserStore } from '/@/store/modules/user'; | |
83 | + import { useLockStore } from '/@/store/modules/lock'; | |
84 | + import { useI18n } from '/@/hooks/web/useI18n'; | |
85 | + import { useNow } from './useNow'; | |
86 | + import { useDesign } from '/@/hooks/web/useDesign'; | |
87 | + import { LockOutlined } from '@ant-design/icons-vue'; | |
88 | + import headerImg from '/@/assets/images/header.jpg'; | |
89 | + | |
90 | + const InputPassword = Input.Password; | |
91 | + | |
92 | + const password = ref(''); | |
93 | + const loading = ref(false); | |
94 | + const errMsg = ref(false); | |
95 | + const showDate = ref(true); | |
96 | + | |
97 | + const { prefixCls } = useDesign('lock-page'); | |
98 | + const lockStore = useLockStore(); | |
99 | + const userStore = useUserStore(); | |
100 | + | |
101 | + const { hour, month, minute, meridiem, year, day, week } = useNow(true); | |
102 | + | |
103 | + const { t } = useI18n(); | |
104 | + | |
105 | + const userinfo = computed(() => { | |
106 | + return userStore.getUserInfo || {}; | |
107 | + }); | |
108 | + | |
109 | + /** | |
110 | + * @description: unLock | |
111 | + */ | |
112 | + async function unLock() { | |
113 | + if (!password.value) { | |
114 | + return; | |
115 | + } | |
116 | + let pwd = password.value; | |
117 | + try { | |
118 | + loading.value = true; | |
119 | + const res = await lockStore.unLock(pwd); | |
120 | + errMsg.value = !res; | |
121 | + } finally { | |
122 | + loading.value = false; | |
123 | + } | |
124 | + } | |
125 | + | |
126 | + function goLogin() { | |
127 | + userStore.logout(true); | |
128 | + lockStore.resetLockInfo(); | |
129 | + } | |
130 | + | |
131 | + function handleShowForm(show = false) { | |
132 | + showDate.value = show; | |
133 | + } | |
134 | +</script> | |
135 | +<style lang="less" scoped> | |
136 | + @prefix-cls: ~'@{namespace}-lock-page'; | |
137 | + | |
138 | + .@{prefix-cls} { | |
139 | + z-index: @lock-page-z-index; | |
140 | + | |
141 | + &__unlock { | |
142 | + transform: translate(-50%, 0); | |
143 | + } | |
144 | + | |
145 | + &__hour, | |
146 | + &__minute { | |
147 | + display: flex; | |
148 | + font-weight: 700; | |
149 | + color: #bababa; | |
150 | + background-color: #141313; | |
151 | + border-radius: 30px; | |
152 | + justify-content: center; | |
153 | + align-items: center; | |
154 | + | |
155 | + @media screen and (max-width: @screen-md) { | |
156 | + span:not(.meridiem) { | |
157 | + font-size: 160px; | |
158 | + } | |
159 | + } | |
160 | + | |
161 | + @media screen and (min-width: @screen-md) { | |
162 | + span:not(.meridiem) { | |
163 | + font-size: 160px; | |
164 | + } | |
165 | + } | |
166 | + | |
167 | + @media screen and (max-width: @screen-sm) { | |
168 | + span:not(.meridiem) { | |
169 | + font-size: 90px; | |
170 | + } | |
171 | + } | |
172 | + @media screen and (min-width: @screen-lg) { | |
173 | + span:not(.meridiem) { | |
174 | + font-size: 220px; | |
175 | + } | |
176 | + } | |
177 | + | |
178 | + @media screen and (min-width: @screen-xl) { | |
179 | + span:not(.meridiem) { | |
180 | + font-size: 260px; | |
181 | + } | |
182 | + } | |
183 | + @media screen and (min-width: @screen-2xl) { | |
184 | + span:not(.meridiem) { | |
185 | + font-size: 320px; | |
186 | + } | |
187 | + } | |
188 | + } | |
189 | + | |
190 | + &-entry { | |
191 | + position: absolute; | |
192 | + top: 0; | |
193 | + left: 0; | |
194 | + display: flex; | |
195 | + width: 100%; | |
196 | + height: 100%; | |
197 | + background-color: rgba(0, 0, 0, 0.5); | |
198 | + backdrop-filter: blur(8px); | |
199 | + justify-content: center; | |
200 | + align-items: center; | |
201 | + | |
202 | + &-content { | |
203 | + width: 260px; | |
204 | + } | |
205 | + | |
206 | + &__header { | |
207 | + text-align: center; | |
208 | + | |
209 | + &-img { | |
210 | + width: 70px; | |
211 | + margin: 0 auto; | |
212 | + border-radius: 50%; | |
213 | + } | |
214 | + | |
215 | + &-name { | |
216 | + margin-top: 5px; | |
217 | + font-weight: 500; | |
218 | + color: #bababa; | |
219 | + } | |
220 | + } | |
221 | + | |
222 | + &__err-msg { | |
223 | + display: inline-block; | |
224 | + margin-top: 10px; | |
225 | + color: @error-color; | |
226 | + } | |
227 | + | |
228 | + &__footer { | |
229 | + display: flex; | |
230 | + justify-content: space-between; | |
231 | + } | |
232 | + } | |
233 | + } | |
234 | +</style> | ... | ... |
src/sys/lock/index.vue
0 → 100644
1 | +<template> | |
2 | + <div> | |
3 | + <transition name="fade-bottom" mode="out-in"> | |
4 | + <LockPage v-if="getIsLock" /> | |
5 | + </transition> | |
6 | + </div> | |
7 | +</template> | |
8 | +<script lang="ts" setup> | |
9 | + import { computed } from 'vue'; | |
10 | + import LockPage from './LockPage.vue'; | |
11 | + import { useLockStore } from '/@/store/modules/lock'; | |
12 | + | |
13 | + const lockStore = useLockStore(); | |
14 | + const getIsLock = computed(() => lockStore?.getLockInfo?.isLock ?? false); | |
15 | +</script> | ... | ... |
src/sys/lock/useNow.ts
0 → 100644
1 | +import { dateUtil } from '/@/utils/dateUtil'; | |
2 | +import { reactive, toRefs } from 'vue'; | |
3 | +import { useLocaleStore } from '/@/store/modules/locale'; | |
4 | +import { tryOnMounted, tryOnUnmounted } from '@vueuse/core'; | |
5 | + | |
6 | +export function useNow(immediate = true) { | |
7 | + const localeStore = useLocaleStore(); | |
8 | + const localData = dateUtil.localeData(localeStore.getLocale); | |
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 = dateUtil(); | |
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') + 1; | |
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 | +} | ... | ... |
src/sys/login/ForgetPasswordForm.vue
0 → 100644
1 | +<template> | |
2 | + <template v-if="getShow"> | |
3 | + <LoginFormTitle class="enter-x" /> | |
4 | + <Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef"> | |
5 | + <FormItem name="mobile" class="enter-x"> | |
6 | + <Input size="large" v-model:value="formData.mobile" :placeholder="t('sys.login.mobile')" /> | |
7 | + </FormItem> | |
8 | + <FormItem name="sms" class="enter-x"> | |
9 | + <CountdownInput | |
10 | + :sendCodeApi="sendLoginSms" | |
11 | + size="large" | |
12 | + v-model:value="formData.sms" | |
13 | + :placeholder="t('sys.login.smsCode')" | |
14 | + /> | |
15 | + </FormItem> | |
16 | + <FormItem name="password" class="enter-x"> | |
17 | + <InputPassword | |
18 | + size="large" | |
19 | + v-model:value="formData.password" | |
20 | + visibilityToggle | |
21 | + :placeholder="t('sys.login.password')" | |
22 | + /> | |
23 | + </FormItem> | |
24 | + | |
25 | + <FormItem class="enter-x"> | |
26 | + <Button type="primary" size="large" block @click="handleReset" :loading="loading"> | |
27 | + {{ t('common.resetText') }} | |
28 | + </Button> | |
29 | + <Button size="large" block class="mt-4" @click="handleBackLogin"> | |
30 | + {{ t('sys.login.backSignIn') }} | |
31 | + </Button> | |
32 | + </FormItem> | |
33 | + </Form> | |
34 | + </template> | |
35 | +</template> | |
36 | +<script lang="ts" setup> | |
37 | + import { reactive, ref, computed, unref } from 'vue'; | |
38 | + import LoginFormTitle from './LoginFormTitle.vue'; | |
39 | + import { Form, Input, Button, message } from 'ant-design-vue'; | |
40 | + import { CountdownInput } from '/@/components/CountDown'; | |
41 | + import { useI18n } from '/@/hooks/web/useI18n'; | |
42 | + import { useLoginState, useFormRules, LoginStateEnum } from './useLogin'; | |
43 | + import { passwordResetCode, forgetPasswordApi } from '/@/api/sys/user'; | |
44 | + const FormItem = Form.Item; | |
45 | + const { t } = useI18n(); | |
46 | + const { handleBackLogin, getLoginState, setLoginState } = useLoginState(); | |
47 | + const { getFormRules } = useFormRules(); | |
48 | + | |
49 | + const formRef = ref(); | |
50 | + const loading = ref(false); | |
51 | + const InputPassword = Input.Password; | |
52 | + const formData = reactive({ | |
53 | + mobile: '', | |
54 | + sms: '', | |
55 | + password: '', | |
56 | + }); | |
57 | + | |
58 | + const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD); | |
59 | + | |
60 | + async function handleReset() { | |
61 | + const form = unref(formRef); | |
62 | + if (!form) return; | |
63 | + const value = await form.validate(); | |
64 | + if (!value) return; | |
65 | + const { mobile, password, sms } = value; | |
66 | + try { | |
67 | + loading.value = true; | |
68 | + await forgetPasswordApi({ | |
69 | + phoneNumber: mobile, | |
70 | + userId: sms, | |
71 | + password, | |
72 | + }); | |
73 | + } catch (e) { | |
74 | + return; | |
75 | + } finally { | |
76 | + loading.value = false; | |
77 | + } | |
78 | + message.success('密码重置成功'); | |
79 | + await form.resetFields(); | |
80 | + setLoginState(LoginStateEnum.LOGIN); | |
81 | + } | |
82 | + | |
83 | + async function sendLoginSms() { | |
84 | + const reg = | |
85 | + /^[1](([3][0-9])|([4][0,1,4-9])|([5][0-3,5-9])|([6][2,5,6,7])|([7][0-8])|([8][0-9])|([9][0-3,5-9]))[0-9]{8}$/; | |
86 | + if (reg.test(formData.mobile)) { | |
87 | + const sendRes = await passwordResetCode(formData.mobile); | |
88 | + console.log(sendRes); | |
89 | + if (sendRes === '') { | |
90 | + console.log('发送成功了'); | |
91 | + return true; | |
92 | + } | |
93 | + return false; | |
94 | + } else { | |
95 | + message.error('请输入正确手机号码'); | |
96 | + } | |
97 | + } | |
98 | +</script> | ... | ... |
src/sys/login/Login.vue
0 → 100644
1 | +<template> | |
2 | + <div :class="prefixCls" class="relative w-full h-full px-4"> | |
3 | + <AppLocalePicker | |
4 | + class="absolute text-white top-4 right-4 enter-x xl:text-gray-600" | |
5 | + :showText="false" | |
6 | + v-if="!sessionTimeout && showLocale" | |
7 | + /> | |
8 | + <AppDarkModeToggle class="absolute top-3 right-7 enter-x" v-if="!sessionTimeout" /> | |
9 | + | |
10 | + <span class="-enter-x xl:hidden"> | |
11 | + <AppLogo :alwaysShowTitle="true" /> | |
12 | + </span> | |
13 | + | |
14 | + <div class="container relative h-full py-2 mx-auto sm:px-10"> | |
15 | + <div class="flex h-full"> | |
16 | + <div class="hidden min-h-full pl-4 mr-4 xl:flex xl:flex-col xl:w-6/12"> | |
17 | + <AppLogo class="-enter-x" /> | |
18 | + <div class="my-auto"> | |
19 | + <img | |
20 | + :alt="title" | |
21 | + src="../../../assets/svg/thingskit-login-background.svg" | |
22 | + class="w-1/2 -mt-16 -enter-x" | |
23 | + /> | |
24 | + <div class="mt-10 font-medium text-white -enter-x"> | |
25 | + <span class="inline-block mt-4 text-3xl"> {{ t('sys.login.signInTitle') }}</span> | |
26 | + </div> | |
27 | + <div class="mt-5 font-normal text-white text-md dark:text-gray-500 -enter-x"> | |
28 | + {{ t('sys.login.signInDesc') }} | |
29 | + </div> | |
30 | + </div> | |
31 | + </div> | |
32 | + <div class="flex w-full h-full py-5 xl:h-auto xl:py-0 xl:my-0 xl:w-6/12"> | |
33 | + <div | |
34 | + :class="`${prefixCls}-form`" | |
35 | + class="relative w-full px-5 py-8 mx-auto my-auto rounded-md shadow-md xl:ml-16 xl:bg-transparent sm:px-8 xl:p-4 xl:shadow-none sm:w-3/4 lg:w-2/4 xl:w-auto enter-x" | |
36 | + > | |
37 | + <LoginForm /> | |
38 | + <ForgetPasswordForm /> | |
39 | + <RegisterForm /> | |
40 | + <MobileForm /> | |
41 | + </div> | |
42 | + </div> | |
43 | + </div> | |
44 | + </div> | |
45 | + </div> | |
46 | +</template> | |
47 | +<script lang="ts" setup> | |
48 | + import { computed } from 'vue'; | |
49 | + import { AppLogo } from '/@/components/Application'; | |
50 | + import { AppLocalePicker, AppDarkModeToggle } from '/@/components/Application'; | |
51 | + import LoginForm from './LoginForm.vue'; | |
52 | + import ForgetPasswordForm from './ForgetPasswordForm.vue'; | |
53 | + import RegisterForm from './RegisterForm.vue'; | |
54 | + import MobileForm from './MobileForm.vue'; | |
55 | + import { useGlobSetting } from '/@/hooks/setting'; | |
56 | + import { useI18n } from '/@/hooks/web/useI18n'; | |
57 | + import { useDesign } from '/@/hooks/web/useDesign'; | |
58 | + import { useLocaleStore } from '/@/store/modules/locale'; | |
59 | + defineProps({ | |
60 | + sessionTimeout: { | |
61 | + type: Boolean, | |
62 | + }, | |
63 | + }); | |
64 | + | |
65 | + const globSetting = useGlobSetting(); | |
66 | + const { prefixCls } = useDesign('login'); | |
67 | + const { t } = useI18n(); | |
68 | + const localeStore = useLocaleStore(); | |
69 | + const showLocale = localeStore.getShowPicker; | |
70 | + const title = computed(() => globSetting?.title ?? ''); | |
71 | +</script> | |
72 | +<style lang="less"> | |
73 | + @prefix-cls: ~'@{namespace}-login'; | |
74 | + @logo-prefix-cls: ~'@{namespace}-app-logo'; | |
75 | + @countdown-prefix-cls: ~'@{namespace}-countdown-input'; | |
76 | + @dark-bg: #293146; | |
77 | + | |
78 | + html[data-theme='dark'] { | |
79 | + .@{prefix-cls} { | |
80 | + background-color: @dark-bg; | |
81 | + | |
82 | + &::before { | |
83 | + background-image: url(/@/assets/svg/login-bg-dark.svg); | |
84 | + } | |
85 | + | |
86 | + .ant-input, | |
87 | + .ant-input-password { | |
88 | + background-color: #232a3b; | |
89 | + } | |
90 | + | |
91 | + .ant-btn:not(.ant-btn-link):not(.ant-btn-primary) { | |
92 | + border: 1px solid #4a5569; | |
93 | + } | |
94 | + | |
95 | + &-form { | |
96 | + background: transparent !important; | |
97 | + } | |
98 | + | |
99 | + .app-iconify { | |
100 | + color: #fff; | |
101 | + } | |
102 | + } | |
103 | + | |
104 | + input.fix-auto-fill, | |
105 | + .fix-auto-fill input { | |
106 | + -webkit-text-fill-color: #c9d1d9 !important; | |
107 | + box-shadow: inherit !important; | |
108 | + } | |
109 | + } | |
110 | + | |
111 | + .@{prefix-cls} { | |
112 | + min-height: 100%; | |
113 | + overflow: hidden; | |
114 | + @media (max-width: @screen-xl) { | |
115 | + background-color: #293146; | |
116 | + | |
117 | + .@{prefix-cls}-form { | |
118 | + background-color: #fff; | |
119 | + } | |
120 | + } | |
121 | + | |
122 | + &::before { | |
123 | + position: absolute; | |
124 | + top: 0; | |
125 | + left: 0; | |
126 | + width: 100%; | |
127 | + height: 100%; | |
128 | + margin-left: -48%; | |
129 | + background-image: url(/@/assets/svg/login-bg.svg); | |
130 | + background-position: 100%; | |
131 | + background-repeat: no-repeat; | |
132 | + background-size: auto 100%; | |
133 | + content: ''; | |
134 | + @media (max-width: @screen-xl) { | |
135 | + display: none; | |
136 | + } | |
137 | + } | |
138 | + | |
139 | + .@{logo-prefix-cls} { | |
140 | + position: absolute; | |
141 | + top: 12px; | |
142 | + height: 30px; | |
143 | + | |
144 | + &__title { | |
145 | + font-size: 16px; | |
146 | + color: #fff; | |
147 | + } | |
148 | + | |
149 | + img { | |
150 | + width: 32px; | |
151 | + } | |
152 | + } | |
153 | + | |
154 | + .container { | |
155 | + .@{logo-prefix-cls} { | |
156 | + display: flex; | |
157 | + width: 60%; | |
158 | + height: 80px; | |
159 | + | |
160 | + &__title { | |
161 | + font-size: 24px; | |
162 | + color: #fff; | |
163 | + } | |
164 | + | |
165 | + img { | |
166 | + width: 48px; | |
167 | + } | |
168 | + } | |
169 | + } | |
170 | + | |
171 | + &-sign-in-way { | |
172 | + .anticon { | |
173 | + font-size: 22px; | |
174 | + color: #888; | |
175 | + cursor: pointer; | |
176 | + | |
177 | + &:hover { | |
178 | + color: @primary-color; | |
179 | + } | |
180 | + } | |
181 | + } | |
182 | + | |
183 | + input:not([type='checkbox']) { | |
184 | + min-width: 360px; | |
185 | + | |
186 | + @media (max-width: @screen-xl) { | |
187 | + min-width: 320px; | |
188 | + } | |
189 | + | |
190 | + @media (max-width: @screen-lg) { | |
191 | + min-width: 260px; | |
192 | + } | |
193 | + | |
194 | + @media (max-width: @screen-md) { | |
195 | + min-width: 240px; | |
196 | + } | |
197 | + | |
198 | + @media (max-width: @screen-sm) { | |
199 | + min-width: 160px; | |
200 | + } | |
201 | + } | |
202 | + | |
203 | + .@{countdown-prefix-cls} input { | |
204 | + min-width: unset; | |
205 | + } | |
206 | + | |
207 | + .ant-divider-inner-text { | |
208 | + font-size: 12px; | |
209 | + color: @text-color-secondary; | |
210 | + } | |
211 | + } | |
212 | +</style> | ... | ... |
src/sys/login/LoginForm.vue
0 → 100644
1 | +<template> | |
2 | + <LoginFormTitle v-show="getShow" class="enter-x" /> | |
3 | + <Form | |
4 | + class="p-4 enter-x" | |
5 | + :model="formData" | |
6 | + :rules="getFormRules" | |
7 | + ref="formRef" | |
8 | + v-show="getShow" | |
9 | + @keypress.enter="handleLogin" | |
10 | + > | |
11 | + <FormItem name="account" class="enter-x"> | |
12 | + <Input | |
13 | + size="large" | |
14 | + v-model:value="formData.account" | |
15 | + :placeholder="t('sys.login.userName')" | |
16 | + class="fix-auto-fill" | |
17 | + /> | |
18 | + </FormItem> | |
19 | + <FormItem name="password" class="enter-x"> | |
20 | + <InputPassword | |
21 | + size="large" | |
22 | + visibilityToggle | |
23 | + v-model:value="formData.password" | |
24 | + :placeholder="t('sys.login.password')" | |
25 | + /> | |
26 | + </FormItem> | |
27 | + | |
28 | + <ARow class="enter-x"> | |
29 | + <ACol :span="12"> | |
30 | + <FormItem> | |
31 | + <Checkbox v-model:checked="rememberMe" size="small"> | |
32 | + {{ t('sys.login.rememberMe') }} | |
33 | + </Checkbox> | |
34 | + </FormItem> | |
35 | + </ACol> | |
36 | + <ACol :span="12"> | |
37 | + <FormItem style="text-align: right"> | |
38 | + <Button type="link" size="small" @click="setLoginState(LoginStateEnum.RESET_PASSWORD)"> | |
39 | + {{ t('sys.login.forgetPassword') }} | |
40 | + </Button> | |
41 | + </FormItem> | |
42 | + </ACol> | |
43 | + </ARow> | |
44 | + | |
45 | + <FormItem class="enter-x"> | |
46 | + <Button type="primary" size="large" block @click="handleLogin" :loading="loading"> | |
47 | + {{ t('sys.login.loginButton') }} | |
48 | + </Button> | |
49 | + </FormItem> | |
50 | + <ARow class="enter-x flex justify-between"> | |
51 | + <ACol :md="11" :xs="24"> | |
52 | + <Button block @click="setLoginState(LoginStateEnum.LOGIN)"> | |
53 | + {{ t('sys.login.userNameInFormTitle') }} | |
54 | + </Button> | |
55 | + </ACol> | |
56 | + <ACol :md="11" :xs="24"> | |
57 | + <Button block @click="setLoginState(LoginStateEnum.MOBILE)"> | |
58 | + {{ t('sys.login.mobileSignInFormTitle') }} | |
59 | + </Button> | |
60 | + </ACol> | |
61 | + </ARow> | |
62 | + </Form> | |
63 | +</template> | |
64 | +<script lang="ts" setup> | |
65 | + import { reactive, ref, unref, computed } from 'vue'; | |
66 | + import { Checkbox, Form, Input, Row, Col, Button } from 'ant-design-vue'; | |
67 | + import LoginFormTitle from './LoginFormTitle.vue'; | |
68 | + | |
69 | + import { useI18n } from '/@/hooks/web/useI18n'; | |
70 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
71 | + | |
72 | + import { useUserStore } from '/@/store/modules/user'; | |
73 | + import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin'; | |
74 | + import { useDesign } from '/@/hooks/web/useDesign'; | |
75 | + import { getPlatForm } from '/@/api/oem'; | |
76 | + import { createLocalStorage } from '/@/utils/cache'; | |
77 | + | |
78 | + const ACol = Col; | |
79 | + const ARow = Row; | |
80 | + const FormItem = Form.Item; | |
81 | + const InputPassword = Input.Password; | |
82 | + const { t } = useI18n(); | |
83 | + const { notification, createMessage } = useMessage(); | |
84 | + const { prefixCls } = useDesign('login'); | |
85 | + const userStore = useUserStore(); | |
86 | + | |
87 | + const { setLoginState, getLoginState } = useLoginState(); | |
88 | + const { getFormRules } = useFormRules(); | |
89 | + const storage = createLocalStorage(); | |
90 | + const formRef = ref(); | |
91 | + const loading = ref(false); | |
92 | + const rememberMe = ref(false); | |
93 | + const userInfo = storage.get('userInfo'); | |
94 | + const formData = reactive({ | |
95 | + account: userInfo?.account ?? '', | |
96 | + password: userInfo?.password ?? '', | |
97 | + }); | |
98 | + | |
99 | + const { validForm } = useFormValid(formRef); | |
100 | + | |
101 | + const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN); | |
102 | + | |
103 | + async function handleLogin() { | |
104 | + const data = await validForm(); | |
105 | + if (!data) return; | |
106 | + if (unref(rememberMe)) { | |
107 | + storage.set('userInfo', formData); | |
108 | + } else { | |
109 | + storage.set('userInfo', null); | |
110 | + } | |
111 | + loading.value = true; | |
112 | + const userInfo = await userStore | |
113 | + .login({ | |
114 | + password: data.password, | |
115 | + username: data.account, | |
116 | + mode: 'modal', //不要默认的错误提示 | |
117 | + }) | |
118 | + .catch((data) => { | |
119 | + //登录失败返回的html,所以提示框什么都没有 | |
120 | + //去掉提示框 | |
121 | + // createMessage.error(data.message); | |
122 | + }); | |
123 | + if (userInfo) { | |
124 | + notification.success({ | |
125 | + message: t('sys.login.loginSuccessTitle'), | |
126 | + description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realName}`, | |
127 | + duration: 3, | |
128 | + }); | |
129 | + const res = await getPlatForm(); | |
130 | + storage.set('platformInfo', res); | |
131 | + userStore.setPlatInfo(res); | |
132 | + // 设置icon | |
133 | + let link = (document.querySelector("link[rel*='icon']") || | |
134 | + document.createElement('link')) as HTMLLinkElement; | |
135 | + link.type = 'image/x-icon'; | |
136 | + link.rel = 'shortcut icon'; | |
137 | + link.href = res.icon ?? '/favicon.ico'; | |
138 | + document.getElementsByTagName('head')[0].appendChild(link); | |
139 | + | |
140 | + var _hmt = _hmt || []; | |
141 | + (function () { | |
142 | + var hm = document.createElement('script'); | |
143 | + hm.src = 'https://hm.baidu.com/hm.js?909f8e22a361b08e4f5ea3918500aede'; | |
144 | + var s = document.getElementsByTagName('script')[0]; | |
145 | + s.parentNode.insertBefore(hm, s); | |
146 | + })(); | |
147 | + } | |
148 | + loading.value = false; | |
149 | + } | |
150 | +</script> | ... | ... |
src/sys/login/LoginFormTitle.vue
0 → 100644
1 | +<template> | |
2 | + <h2 class="mb-3 text-2xl font-bold text-center xl:text-3xl enter-x xl:text-left"> | |
3 | + {{ getFormTitle }} | |
4 | + </h2> | |
5 | +</template> | |
6 | +<script lang="ts" setup> | |
7 | + import { computed, unref } from 'vue'; | |
8 | + import { useI18n } from '/@/hooks/web/useI18n'; | |
9 | + import { LoginStateEnum, useLoginState } from './useLogin'; | |
10 | + | |
11 | + const { t } = useI18n(); | |
12 | + | |
13 | + const { getLoginState } = useLoginState(); | |
14 | + | |
15 | + const getFormTitle = computed(() => { | |
16 | + const titleObj = { | |
17 | + [LoginStateEnum.RESET_PASSWORD]: t('sys.login.forgetFormTitle'), | |
18 | + [LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'), | |
19 | + [LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'), | |
20 | + [LoginStateEnum.MOBILE]: t('sys.login.mobileSignInFormTitle'), | |
21 | + [LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle'), | |
22 | + }; | |
23 | + return titleObj[unref(getLoginState)]; | |
24 | + }); | |
25 | +</script> | ... | ... |
src/sys/login/MobileForm.vue
0 → 100644
1 | +<template> | |
2 | + <template v-if="getShow"> | |
3 | + <LoginFormTitle class="enter-x" /> | |
4 | + <Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef"> | |
5 | + <FormItem name="phoneNumber" class="enter-x"> | |
6 | + <Input | |
7 | + size="large" | |
8 | + v-model:value="formData.phoneNumber" | |
9 | + :placeholder="t('sys.login.mobile')" | |
10 | + class="fix-auto-fill" | |
11 | + /> | |
12 | + </FormItem> | |
13 | + <FormItem name="code" class="enter-x"> | |
14 | + <CountdownInput | |
15 | + :sendCodeApi="sendLoginSms" | |
16 | + size="large" | |
17 | + class="fix-auto-fill" | |
18 | + v-model:value="formData.code" | |
19 | + :placeholder="t('sys.login.smsCode')" | |
20 | + /> | |
21 | + </FormItem> | |
22 | + | |
23 | + <FormItem class="enter-x"> | |
24 | + <Button type="primary" size="large" block @click="handleLogin" :loading="loading"> | |
25 | + {{ t('sys.login.loginButton') }} | |
26 | + </Button> | |
27 | + <Button size="large" block class="mt-4" @click="handleBackLogin"> | |
28 | + {{ t('sys.login.backSignIn') }} | |
29 | + </Button> | |
30 | + </FormItem> | |
31 | + </Form> | |
32 | + </template> | |
33 | +</template> | |
34 | +<script lang="ts" setup> | |
35 | + import { reactive, ref, computed, unref, toRaw } from 'vue'; | |
36 | + import { Form, Input, Button, message } from 'ant-design-vue'; | |
37 | + import { CountdownInput } from '/@/components/CountDown'; | |
38 | + import LoginFormTitle from './LoginFormTitle.vue'; | |
39 | + import { useI18n } from '/@/hooks/web/useI18n'; | |
40 | + import { useLoginState, useFormRules, useFormValid, LoginStateEnum } from './useLogin'; | |
41 | + import { SendLoginSmsCode } from '/@/api/sys/user'; | |
42 | + import { useUserStore } from '/@/store/modules/user'; | |
43 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
44 | + const { notification } = useMessage(); | |
45 | + | |
46 | + const FormItem = Form.Item; | |
47 | + const { t } = useI18n(); | |
48 | + const { handleBackLogin, getLoginState } = useLoginState(); | |
49 | + const { getFormRules } = useFormRules(); | |
50 | + | |
51 | + const formRef = ref(); | |
52 | + const loading = ref(false); | |
53 | + | |
54 | + const formData = reactive({ | |
55 | + phoneNumber: '', | |
56 | + code: '', | |
57 | + }); | |
58 | + | |
59 | + async function sendLoginSms() { | |
60 | + const reg = | |
61 | + /^[1](([3][0-9])|([4][0,1,4-9])|([5][0-3,5-9])|([6][2,5,6,7])|([7][0-8])|([8][0-9])|([9][0-3,5-9]))[0-9]{8}$/; | |
62 | + if (reg.test(formData.phoneNumber)) { | |
63 | + const sendRes = await SendLoginSmsCode(formData.phoneNumber); | |
64 | + if (!sendRes) { | |
65 | + message.error('发送失败'); | |
66 | + return false; | |
67 | + } | |
68 | + return true; | |
69 | + } else { | |
70 | + message.error('请输入正确手机号码'); | |
71 | + } | |
72 | + } | |
73 | + | |
74 | + const { validForm } = useFormValid(formRef); | |
75 | + const userStore = useUserStore(); | |
76 | + const getShow = computed(() => unref(getLoginState) === LoginStateEnum.MOBILE); | |
77 | + | |
78 | + async function handleLogin() { | |
79 | + const data = await validForm(); | |
80 | + if (!data) return; | |
81 | + | |
82 | + const userInfo = await userStore.smsCodelogin( | |
83 | + toRaw({ | |
84 | + phoneNumber: data.phoneNumber, | |
85 | + code: data.code, | |
86 | + mode: 'none', //不要默认的错误提示 | |
87 | + }) | |
88 | + ); | |
89 | + if (userInfo) { | |
90 | + notification.success({ | |
91 | + message: t('sys.login.loginSuccessTitle'), | |
92 | + description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realName}`, | |
93 | + duration: 3, | |
94 | + }); | |
95 | + } | |
96 | + } | |
97 | +</script> | ... | ... |
src/sys/login/QrCodeForm.vue
0 → 100644
1 | +<template> | |
2 | + <template v-if="getShow"> | |
3 | + <LoginFormTitle class="enter-x" /> | |
4 | + <div class="enter-x min-w-64 min-h-64"> | |
5 | + <QrCode | |
6 | + :value="qrCodeUrl" | |
7 | + class="enter-x flex justify-center xl:justify-start" | |
8 | + :width="280" | |
9 | + /> | |
10 | + <Divider class="enter-x">{{ t('sys.login.scanSign') }}</Divider> | |
11 | + <Button size="large" block class="mt-4 enter-x" @click="handleBackLogin"> | |
12 | + {{ t('sys.login.backSignIn') }} | |
13 | + </Button> | |
14 | + </div> | |
15 | + </template> | |
16 | +</template> | |
17 | +<script lang="ts" setup> | |
18 | + import { computed, unref } from 'vue'; | |
19 | + import LoginFormTitle from './LoginFormTitle.vue'; | |
20 | + import { Button, Divider } from 'ant-design-vue'; | |
21 | + import { QrCode } from '/@/components/Qrcode/index'; | |
22 | + import { useI18n } from '/@/hooks/web/useI18n'; | |
23 | + import { useLoginState, LoginStateEnum } from './useLogin'; | |
24 | + | |
25 | + const qrCodeUrl = 'https://vvbin.cn/next/login'; | |
26 | + | |
27 | + const { t } = useI18n(); | |
28 | + const { handleBackLogin, getLoginState } = useLoginState(); | |
29 | + | |
30 | + const getShow = computed(() => unref(getLoginState) === LoginStateEnum.QR_CODE); | |
31 | +</script> | ... | ... |
src/sys/login/RegisterForm.vue
0 → 100644
1 | +<template> | |
2 | + <template v-if="getShow"> | |
3 | + <LoginFormTitle class="enter-x" /> | |
4 | + <Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef"> | |
5 | + <FormItem name="account" class="enter-x"> | |
6 | + <Input | |
7 | + class="fix-auto-fill" | |
8 | + size="large" | |
9 | + v-model:value="formData.account" | |
10 | + :placeholder="t('sys.login.userName')" | |
11 | + /> | |
12 | + </FormItem> | |
13 | + <FormItem name="mobile" class="enter-x"> | |
14 | + <Input | |
15 | + size="large" | |
16 | + v-model:value="formData.mobile" | |
17 | + :placeholder="t('sys.login.mobile')" | |
18 | + class="fix-auto-fill" | |
19 | + /> | |
20 | + </FormItem> | |
21 | + <FormItem name="sms" class="enter-x"> | |
22 | + <CountdownInput | |
23 | + size="large" | |
24 | + class="fix-auto-fill" | |
25 | + v-model:value="formData.sms" | |
26 | + :placeholder="t('sys.login.smsCode')" | |
27 | + /> | |
28 | + </FormItem> | |
29 | + <FormItem name="password" class="enter-x"> | |
30 | + <StrengthMeter | |
31 | + size="large" | |
32 | + v-model:value="formData.password" | |
33 | + :placeholder="t('sys.login.password')" | |
34 | + /> | |
35 | + </FormItem> | |
36 | + <FormItem name="confirmPassword" class="enter-x"> | |
37 | + <InputPassword | |
38 | + size="large" | |
39 | + visibilityToggle | |
40 | + v-model:value="formData.confirmPassword" | |
41 | + :placeholder="t('sys.login.confirmPassword')" | |
42 | + /> | |
43 | + </FormItem> | |
44 | + | |
45 | + <FormItem class="enter-x" name="policy"> | |
46 | + <!-- No logic, you need to deal with it yourself --> | |
47 | + <Checkbox v-model:checked="formData.policy" size="small"> | |
48 | + {{ t('sys.login.policy') }} | |
49 | + </Checkbox> | |
50 | + </FormItem> | |
51 | + | |
52 | + <Button | |
53 | + type="primary" | |
54 | + class="enter-x" | |
55 | + size="large" | |
56 | + block | |
57 | + @click="handleRegister" | |
58 | + :loading="loading" | |
59 | + > | |
60 | + {{ t('sys.login.registerButton') }} | |
61 | + </Button> | |
62 | + <Button size="large" block class="mt-4 enter-x" @click="handleBackLogin"> | |
63 | + {{ t('sys.login.backSignIn') }} | |
64 | + </Button> | |
65 | + </Form> | |
66 | + </template> | |
67 | +</template> | |
68 | +<script lang="ts" setup> | |
69 | + import { reactive, ref, unref, computed } from 'vue'; | |
70 | + import LoginFormTitle from './LoginFormTitle.vue'; | |
71 | + import { Form, Input, Button, Checkbox } from 'ant-design-vue'; | |
72 | + import { StrengthMeter } from '/@/components/StrengthMeter'; | |
73 | + import { CountdownInput } from '/@/components/CountDown'; | |
74 | + import { useI18n } from '/@/hooks/web/useI18n'; | |
75 | + import { useLoginState, useFormRules, useFormValid, LoginStateEnum } from './useLogin'; | |
76 | + | |
77 | + const FormItem = Form.Item; | |
78 | + const InputPassword = Input.Password; | |
79 | + const { t } = useI18n(); | |
80 | + const { handleBackLogin, getLoginState } = useLoginState(); | |
81 | + | |
82 | + const formRef = ref(); | |
83 | + const loading = ref(false); | |
84 | + | |
85 | + const formData = reactive({ | |
86 | + account: '', | |
87 | + password: '', | |
88 | + confirmPassword: '', | |
89 | + mobile: '', | |
90 | + sms: '', | |
91 | + policy: false, | |
92 | + }); | |
93 | + | |
94 | + const { getFormRules } = useFormRules(formData); | |
95 | + const { validForm } = useFormValid(formRef); | |
96 | + | |
97 | + const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER); | |
98 | + | |
99 | + async function handleRegister() { | |
100 | + const data = await validForm(); | |
101 | + if (!data) return; | |
102 | + console.log(data); | |
103 | + } | |
104 | +</script> | ... | ... |
src/sys/login/SessionTimeoutLogin.vue
0 → 100644
1 | +<template> | |
2 | + <transition> | |
3 | + <div :class="prefixCls"> | |
4 | + <Login sessionTimeout /> | |
5 | + </div> | |
6 | + </transition> | |
7 | +</template> | |
8 | +<script lang="ts" setup> | |
9 | + import { onBeforeUnmount, onMounted, ref } from 'vue'; | |
10 | + import Login from './Login.vue'; | |
11 | + import { useDesign } from '/@/hooks/web/useDesign'; | |
12 | + import { useUserStore } from '/@/store/modules/user'; | |
13 | + import { usePermissionStore } from '/@/store/modules/permission'; | |
14 | + import { useAppStore } from '/@/store/modules/app'; | |
15 | + import { PermissionModeEnum } from '/@/enums/appEnum'; | |
16 | + | |
17 | + const { prefixCls } = useDesign('st-login'); | |
18 | + const userStore = useUserStore(); | |
19 | + const permissionStore = usePermissionStore(); | |
20 | + const appStore = useAppStore(); | |
21 | + const userId = ref<Nullable<number | string>>(0); | |
22 | + | |
23 | + const isBackMode = () => { | |
24 | + return appStore.getProjectConfig.permissionMode === PermissionModeEnum.BACK; | |
25 | + }; | |
26 | + | |
27 | + onMounted(() => { | |
28 | + // 记录当前的UserId | |
29 | + userId.value = userStore.getUserInfo?.userId; | |
30 | + console.log('Mounted', userStore.getUserInfo); | |
31 | + }); | |
32 | + | |
33 | + onBeforeUnmount(() => { | |
34 | + if (userId.value && userId.value !== userStore.getUserInfo.userId) { | |
35 | + // 登录的不是同一个用户,刷新整个页面以便丢弃之前用户的页面状态 | |
36 | + document.location.reload(); | |
37 | + } else if (isBackMode() && permissionStore.getLastBuildMenuTime === 0) { | |
38 | + // 后台权限模式下,没有成功加载过菜单,就重新加载整个页面。这通常发生在会话过期后按F5刷新整个页面后载入了本模块这种场景 | |
39 | + document.location.reload(); | |
40 | + } | |
41 | + }); | |
42 | +</script> | |
43 | +<style lang="less" scoped> | |
44 | + @prefix-cls: ~'@{namespace}-st-login'; | |
45 | + | |
46 | + .@{prefix-cls} { | |
47 | + position: fixed; | |
48 | + z-index: 9999999; | |
49 | + width: 100%; | |
50 | + height: 100%; | |
51 | + background: @component-background; | |
52 | + } | |
53 | +</style> | ... | ... |
src/sys/login/useLogin.ts
0 → 100644
1 | +import type { ValidationRule } from 'ant-design-vue/lib/form/Form'; | |
2 | +import type { RuleObject } from 'ant-design-vue/lib/form/interface'; | |
3 | +import { ref, computed, unref, Ref } from 'vue'; | |
4 | +import { useI18n } from '/@/hooks/web/useI18n'; | |
5 | + | |
6 | +export enum LoginStateEnum { | |
7 | + LOGIN, | |
8 | + REGISTER, | |
9 | + RESET_PASSWORD, | |
10 | + MOBILE, | |
11 | + QR_CODE, | |
12 | +} | |
13 | + | |
14 | +const currentState = ref(LoginStateEnum.LOGIN); | |
15 | + | |
16 | +export function useLoginState() { | |
17 | + function setLoginState(state: LoginStateEnum) { | |
18 | + currentState.value = state; | |
19 | + } | |
20 | + | |
21 | + const getLoginState = computed(() => currentState.value); | |
22 | + | |
23 | + function handleBackLogin() { | |
24 | + setLoginState(LoginStateEnum.LOGIN); | |
25 | + } | |
26 | + | |
27 | + return { setLoginState, getLoginState, handleBackLogin }; | |
28 | +} | |
29 | + | |
30 | +export function useFormValid<T extends Object = any>(formRef: Ref<any>) { | |
31 | + async function validForm() { | |
32 | + const form = unref(formRef); | |
33 | + if (!form) return; | |
34 | + const data = await form.validate(); | |
35 | + return data as T; | |
36 | + } | |
37 | + | |
38 | + return { validForm }; | |
39 | +} | |
40 | + | |
41 | +export function useFormRules(formData?: Recordable) { | |
42 | + const { t } = useI18n(); | |
43 | + | |
44 | + const getAccountFormRule = computed(() => createRule(t('sys.login.accountPlaceholder'))); | |
45 | + const getPasswordFormRule = computed(() => createRule(t('sys.login.passwordPlaceholder'))); | |
46 | + const getSmsFormRule = computed(() => createRule(t('sys.login.smsPlaceholder'))); | |
47 | + const getMobileFormRule = computed(() => createRule(t('sys.login.mobilePlaceholder'))); | |
48 | + | |
49 | + const validatePolicy = async (_: RuleObject, value: boolean) => { | |
50 | + return !value ? Promise.reject(t('sys.login.policyPlaceholder')) : Promise.resolve(); | |
51 | + }; | |
52 | + | |
53 | + const validateConfirmPassword = (password: string) => { | |
54 | + return async (_: RuleObject, value: string) => { | |
55 | + if (!value) { | |
56 | + return Promise.reject(t('sys.login.passwordPlaceholder')); | |
57 | + } | |
58 | + if (value !== password) { | |
59 | + return Promise.reject(t('sys.login.diffPwd')); | |
60 | + } | |
61 | + return Promise.resolve(); | |
62 | + }; | |
63 | + }; | |
64 | + | |
65 | + const getFormRules = computed((): { [k: string]: ValidationRule | ValidationRule[] } => { | |
66 | + const accountFormRule = unref(getAccountFormRule); | |
67 | + const passwordFormRule = unref(getPasswordFormRule); | |
68 | + const smsFormRule = unref(getSmsFormRule); | |
69 | + const mobileFormRule = unref(getMobileFormRule); | |
70 | + | |
71 | + const mobileRule = { | |
72 | + sms: smsFormRule, | |
73 | + mobile: mobileFormRule, | |
74 | + }; | |
75 | + switch (unref(currentState)) { | |
76 | + // register form rules | |
77 | + case LoginStateEnum.REGISTER: | |
78 | + return { | |
79 | + account: accountFormRule, | |
80 | + password: passwordFormRule, | |
81 | + confirmPassword: [ | |
82 | + { validator: validateConfirmPassword(formData?.password), trigger: 'change' }, | |
83 | + ], | |
84 | + policy: [{ validator: validatePolicy, trigger: 'change' }], | |
85 | + ...mobileRule, | |
86 | + }; | |
87 | + | |
88 | + // reset password form rules | |
89 | + case LoginStateEnum.RESET_PASSWORD: | |
90 | + return { | |
91 | + password: passwordFormRule, | |
92 | + ...mobileRule, | |
93 | + }; | |
94 | + | |
95 | + // mobile form rules | |
96 | + case LoginStateEnum.MOBILE: | |
97 | + return mobileRule; | |
98 | + | |
99 | + // login form rules | |
100 | + default: | |
101 | + return { | |
102 | + account: accountFormRule, | |
103 | + password: passwordFormRule, | |
104 | + }; | |
105 | + } | |
106 | + }); | |
107 | + return { getFormRules }; | |
108 | +} | |
109 | + | |
110 | +function createRule(message: string) { | |
111 | + return [ | |
112 | + { | |
113 | + required: true, | |
114 | + message, | |
115 | + trigger: 'change', | |
116 | + }, | |
117 | + ]; | |
118 | +} | ... | ... |
src/sys/redirect/index.vue
0 → 100644
1 | +<template> | |
2 | + <div></div> | |
3 | +</template> | |
4 | +<script lang="ts" setup> | |
5 | + import { unref } from 'vue'; | |
6 | + import { useRouter } from 'vue-router'; | |
7 | + | |
8 | + const { currentRoute, replace } = useRouter(); | |
9 | + | |
10 | + const { params, query } = unref(currentRoute); | |
11 | + const { path, _redirect_type = 'path' } = params; | |
12 | + | |
13 | + Reflect.deleteProperty(params, '_redirect_type'); | |
14 | + Reflect.deleteProperty(params, 'path'); | |
15 | + | |
16 | + const _path = Array.isArray(path) ? path.join('/') : path; | |
17 | + | |
18 | + if (_redirect_type === 'name') { | |
19 | + replace({ | |
20 | + name: _path, | |
21 | + query, | |
22 | + params, | |
23 | + }); | |
24 | + } else { | |
25 | + replace({ | |
26 | + path: _path.startsWith('/') ? _path : '/' + _path, | |
27 | + query, | |
28 | + }); | |
29 | + } | |
30 | +</script> | ... | ... |
... | ... | @@ -79,8 +79,10 @@ |
79 | 79 | if (updateFn) updateFn(); |
80 | 80 | }); |
81 | 81 | } |
82 | + | |
82 | 83 | const itemResize = (i: string, newH: number, newW: number, newHPx: number, newWPx: number) => { |
83 | 84 | updateSize(i, newH, newW, newHPx, newWPx); |
85 | + console.log({ i, newH, newW, newHPx, newWPx }); | |
84 | 86 | }; |
85 | 87 | |
86 | 88 | const itemResized = (i: string, newH: number, newW: number, newHPx: number, newWPx: number) => { | ... | ... |