Commit 19f9834501add2ef31e25d8319a7872ed2df1010

Authored by ww
1 parent e644b8df

chore: reset miskaken delete file

  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>
... ...
  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 +}
... ...
  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>
... ...
  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>
... ...
  1 +export { default as Exception } from './Exception.vue';
... ...
  1 +<template>
  2 + <div></div>
  3 +</template>
  4 +<script lang="ts">
  5 + import { defineComponent } from 'vue';
  6 + export default defineComponent({
  7 + name: 'FrameBlank',
  8 + });
  9 +</script>
... ...
  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>
... ...
  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>
... ...
  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>
... ...
  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 +}
... ...
  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>
... ...
  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>
... ...
  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>
... ...
  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>
... ...
  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>
... ...
  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>
... ...
  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>
... ...
  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>
... ...
  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 +}
... ...
  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) => {
... ...