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,8 +79,10 @@ | ||
79 | if (updateFn) updateFn(); | 79 | if (updateFn) updateFn(); |
80 | }); | 80 | }); |
81 | } | 81 | } |
82 | + | ||
82 | const itemResize = (i: string, newH: number, newW: number, newHPx: number, newWPx: number) => { | 83 | const itemResize = (i: string, newH: number, newW: number, newHPx: number, newWPx: number) => { |
83 | updateSize(i, newH, newW, newHPx, newWPx); | 84 | updateSize(i, newH, newW, newHPx, newWPx); |
85 | + console.log({ i, newH, newW, newHPx, newWPx }); | ||
84 | }; | 86 | }; |
85 | 87 | ||
86 | const itemResized = (i: string, newH: number, newW: number, newHPx: number, newWPx: number) => { | 88 | const itemResized = (i: string, newH: number, newW: number, newHPx: number, newWPx: number) => { |