---初始化后台管理web页面项目

This commit is contained in:
2025-08-20 14:39:30 +08:00
parent ad49711a7e
commit 87545a8baf
2057 changed files with 282864 additions and 213 deletions

View File

@ -0,0 +1,98 @@
<template>
<PageWrapper title="关于">
<template #headerContent>
<div class="flex justify-between items-center">
<span class="flex-1">
<a :href="GITHUB_URL" target="_blank">{{ name }}</a>
是一个基于Vue3.0Vite Ant-Design-Vue TypeScript
的后台解决方案目标是为中大型项目开发,提供现成的开箱解决方案及丰富的示例,原则上不会限制任何代码用于商用
</span>
</div>
</template>
<Description @register="infoRegister" class="enter-y" />
<Description @register="register" class="my-4 enter-y" />
<Description @register="registerDev" class="enter-y" />
</PageWrapper>
</template>
<script lang="ts" setup>
import { h } from 'vue';
import { Tag } from 'ant-design-vue';
import { PageWrapper } from '/@/components/Page';
import { Description, DescItem, useDescription } from '/@/components/Description/index';
import { GITHUB_URL, SITE_URL, DOC_URL } from '/@/settings/siteSetting';
const { pkg, lastBuildTime } = __APP_INFO__;
const { dependencies, devDependencies, name, version } = pkg;
const schema: DescItem[] = [];
const devSchema: DescItem[] = [];
const commonTagRender = (color: string) => (curVal) => h(Tag, { color }, () => curVal);
const commonLinkRender = (text: string) => (href) => h('a', { href, target: '_blank' }, text);
const infoSchema: DescItem[] = [
{
label: '版本',
field: 'version',
render: commonTagRender('blue'),
},
{
label: '最后编译时间',
field: 'lastBuildTime',
render: commonTagRender('blue'),
},
{
label: '文档地址',
field: 'doc',
render: commonLinkRender('文档地址'),
},
{
label: '预览地址',
field: 'preview',
render: commonLinkRender('预览地址'),
},
{
label: 'Github',
field: 'github',
render: commonLinkRender('Github'),
},
];
const infoData = {
version,
lastBuildTime,
doc: DOC_URL,
preview: SITE_URL,
github: GITHUB_URL,
};
Object.keys(dependencies).forEach((key) => {
schema.push({ field: key, label: key });
});
Object.keys(devDependencies).forEach((key) => {
devSchema.push({ field: key, label: key });
});
const [register] = useDescription({
title: '生产环境依赖',
data: dependencies,
schema: schema,
column: 3,
});
const [registerDev] = useDescription({
title: '开发环境依赖',
data: devDependencies,
schema: devSchema,
column: 3,
});
const [infoRegister] = useDescription({
title: '项目信息',
data: infoData,
schema: infoSchema,
column: 2,
});
</script>

View File

@ -0,0 +1,27 @@
<template>
<BasicModal :width="800" :title="t('sys.errorLog.tableActionDesc')" v-bind="$attrs">
<Description :data="info" @register="register" />
</BasicModal>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type { ErrorLogInfo } from '/#/store';
import { BasicModal } from '/@/components/Modal/index';
import { Description, useDescription } from '/@/components/Description/index';
import { useI18n } from '/@/hooks/web/useI18n';
import { getDescSchema } from './data';
defineProps({
info: {
type: Object as PropType<ErrorLogInfo>,
default: null,
},
});
const { t } = useI18n();
const [register] = useDescription({
column: 2,
schema: getDescSchema()!,
});
</script>

View File

@ -0,0 +1,67 @@
import { Tag } from 'ant-design-vue';
import { BasicColumn } from '/@/components/Table/index';
import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
export function getColumns(): BasicColumn[] {
return [
{
dataIndex: 'type',
title: t('sys.errorLog.tableColumnType'),
width: 80,
customRender: ({ text }) => {
const color =
text === ErrorTypeEnum.VUE
? 'green'
: text === ErrorTypeEnum.RESOURCE
? 'cyan'
: text === ErrorTypeEnum.PROMISE
? 'blue'
: ErrorTypeEnum.AJAX
? 'red'
: 'purple';
return <Tag color={color}>{() => text}</Tag>;
},
},
{
dataIndex: 'url',
title: 'URL',
width: 200,
},
{
dataIndex: 'time',
title: t('sys.errorLog.tableColumnDate'),
width: 160,
},
{
dataIndex: 'file',
title: t('sys.errorLog.tableColumnFile'),
width: 200,
},
{
dataIndex: 'name',
title: 'Name',
width: 200,
},
{
dataIndex: 'message',
title: t('sys.errorLog.tableColumnMsg'),
width: 300,
},
{
dataIndex: 'stack',
title: t('sys.errorLog.tableColumnStackMsg'),
},
];
}
export function getDescSchema(): any {
return getColumns().map((column) => {
return {
field: column.dataIndex!,
label: column.title,
};
});
}

View File

@ -0,0 +1,92 @@
<template>
<div class="p-4">
<template v-for="src in imgList" :key="src">
<img :src="src" v-show="false" />
</template>
<DetailModal :info="rowInfo" @register="registerModal" />
<BasicTable @register="register" class="error-handle-table">
<template #toolbar>
<a-button @click="fireVueError" type="primary">
{{ t('sys.errorLog.fireVueError') }}
</a-button>
<a-button @click="fireResourceError" type="primary">
{{ t('sys.errorLog.fireResourceError') }}
</a-button>
<a-button @click="fireAjaxError" type="primary">
{{ t('sys.errorLog.fireAjaxError') }}
</a-button>
</template>
<template #action="{ record }">
<TableAction
:actions="[
{ label: t('sys.errorLog.tableActionDesc'), onClick: handleDetail.bind(null, record) },
]"
/>
</template>
</BasicTable>
</div>
</template>
<script lang="ts" setup>
import type { ErrorLogInfo } from '/#/store';
import { watch, ref, nextTick } from 'vue';
import DetailModal from './DetailModal.vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table/index';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { useI18n } from '/@/hooks/web/useI18n';
import { useErrorLogStore } from '/@/store/modules/errorLog';
import { fireErrorApi } from '/@/api/demo/error';
import { getColumns } from './data';
import { cloneDeep } from 'lodash-es';
const rowInfo = ref<ErrorLogInfo>();
const imgList = ref<string[]>([]);
const { t } = useI18n();
const errorLogStore = useErrorLogStore();
const [register, { setTableData }] = useTable({
title: t('sys.errorLog.tableTitle'),
columns: getColumns(),
actionColumn: {
width: 80,
title: 'Action',
dataIndex: 'action',
slots: { customRender: 'action' },
},
});
const [registerModal, { openModal }] = useModal();
watch(
() => errorLogStore.getErrorLogInfoList,
(list) => {
nextTick(() => {
setTableData(cloneDeep(list));
});
},
{
immediate: true,
},
);
const { createMessage } = useMessage();
if (import.meta.env.DEV) {
createMessage.info(t('sys.errorLog.enableMessage'));
}
// 查看详情
function handleDetail(row: ErrorLogInfo) {
rowInfo.value = row;
openModal(true);
}
function fireVueError() {
throw new Error('fire vue error!');
}
function fireResourceError() {
imgList.value.push(`${new Date().getTime()}.png`);
}
async function fireAjaxError() {
await fireErrorApi();
}
</script>

View File

@ -0,0 +1,148 @@
<script lang="tsx">
import type { PropType } from 'vue';
import { Result, Button } from 'ant-design-vue';
import { defineComponent, ref, computed, unref } from 'vue';
import { ExceptionEnum } from '/@/enums/exceptionEnum';
import notDataSvg from '/@/assets/svg/no-data.svg';
import netWorkSvg from '/@/assets/svg/net-error.svg';
import { useRoute } from 'vue-router';
import { useDesign } from '/@/hooks/web/useDesign';
import { useI18n } from '/@/hooks/web/useI18n';
import { useGo, useRedo } from '/@/hooks/web/usePage';
import { PageEnum } from '/@/enums/pageEnum';
interface MapValue {
title: string;
subTitle: string;
btnText?: string;
icon?: string;
handler?: Fn;
status?: string;
}
export default defineComponent({
name: 'ErrorPage',
props: {
// 状态码
status: {
type: Number as PropType<number>,
default: ExceptionEnum.PAGE_NOT_FOUND,
},
title: {
type: String as PropType<string>,
default: '',
},
subTitle: {
type: String as PropType<string>,
default: '',
},
full: {
type: Boolean as PropType<boolean>,
default: false,
},
},
setup(props) {
const statusMapRef = ref(new Map<string | number, MapValue>());
const { query } = useRoute();
const go = useGo();
const redo = useRedo();
const { t } = useI18n();
const { prefixCls } = useDesign('app-exception-page');
const getStatus = computed(() => {
const { status: routeStatus } = query;
const { status } = props;
return Number(routeStatus) || status;
});
const getMapValue = computed((): MapValue => {
return unref(statusMapRef).get(unref(getStatus)) as MapValue;
});
const backLoginI18n = t('返回登录');
const backHomeI18n = t('返回首页');
unref(statusMapRef).set(ExceptionEnum.PAGE_NOT_ACCESS, {
title: '403',
status: `${ExceptionEnum.PAGE_NOT_ACCESS}`,
subTitle: t('抱歉,您无权访问此页面。'),
btnText: props.full ? backLoginI18n : backHomeI18n,
handler: () => (props.full ? go(PageEnum.BASE_LOGIN) : go()),
});
unref(statusMapRef).set(ExceptionEnum.PAGE_NOT_FOUND, {
title: '404',
status: `${ExceptionEnum.PAGE_NOT_FOUND}`,
subTitle: t('抱歉,您访问的页面不存在。'),
btnText: props.full ? backLoginI18n : backHomeI18n,
handler: () => (props.full ? go(PageEnum.BASE_LOGIN) : go()),
});
unref(statusMapRef).set(ExceptionEnum.ERROR, {
title: '500',
status: `${ExceptionEnum.ERROR}`,
subTitle: t('抱歉,服务器报告错误。'),
btnText: backHomeI18n,
handler: () => go(),
});
unref(statusMapRef).set(ExceptionEnum.PAGE_NOT_DATA, {
title: t('当前页无数据'),
subTitle: '',
btnText: t('刷新'),
handler: () => redo(),
icon: notDataSvg,
});
unref(statusMapRef).set(ExceptionEnum.NET_WORK_ERROR, {
title: t('网络错误'),
subTitle: t('抱歉,您的网络连接已断开,请检查您的网络!'),
btnText: t('刷新'),
handler: () => redo(),
icon: netWorkSvg,
});
return () => {
const { title, subTitle, btnText, icon, handler, status } = unref(getMapValue) || {};
return (
<Result
class={prefixCls}
status={status as any}
title={props.title || title}
sub-title={props.subTitle || subTitle}
>
{{
extra: () =>
btnText && (
<Button type="primary" onClick={handler}>
{() => btnText}
</Button>
),
icon: () => (icon ? <img src={icon} /> : null),
}}
</Result>
);
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-app-exception-page';
.@{prefix-cls} {
display: flex;
align-items: center;
flex-direction: column;
.ant-result-icon {
img {
max-width: 400px;
max-height: 300px;
}
}
}
</style>

View File

@ -0,0 +1 @@
export { default as Exception } from './Exception.vue';

View File

@ -0,0 +1,9 @@
<template>
<div></div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'FrameBlank',
});
</script>

View File

@ -0,0 +1,90 @@
<template>
<div :class="prefixCls" :style="getWrapStyle">
<Spin :spinning="loading" size="large" :style="getWrapStyle">
<iframe
:src="frameSrc"
:class="`${prefixCls}__main`"
ref="frameRef"
@load="hideLoading"
></iframe>
</Spin>
</div>
</template>
<script lang="ts" setup>
import type { CSSProperties } from 'vue';
import { ref, unref, computed } from 'vue';
import { Spin } from 'ant-design-vue';
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
import { propTypes } from '/@/utils/propTypes';
import { useDesign } from '/@/hooks/web/useDesign';
import { useLayoutHeight } from '/@/layouts/default/content/useContentViewHeight';
defineProps({
frameSrc: propTypes.string.def(''),
});
const loading = ref(true);
const topRef = ref(50);
const heightRef = ref(window.innerHeight);
const frameRef = ref<HTMLFrameElement>();
const { headerHeightRef } = useLayoutHeight();
const { prefixCls } = useDesign('iframe-page');
useWindowSizeFn(calcHeight, 150, { immediate: true });
const getWrapStyle = computed((): CSSProperties => {
return {
height: `${unref(heightRef)}px`,
};
});
function calcHeight() {
const iframe = unref(frameRef);
if (!iframe) {
return;
}
const top = headerHeightRef.value;
topRef.value = top;
heightRef.value = window.innerHeight - top;
const clientHeight = document.documentElement.clientHeight - top;
iframe.style.height = `${clientHeight}px`;
}
function hideLoading() {
loading.value = false;
calcHeight();
}
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-iframe-page';
.@{prefix-cls} {
.ant-spin-nested-loading {
position: relative;
height: 100%;
.ant-spin-container {
width: 100%;
height: 100%;
padding: 10px;
}
}
&__mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
&__main {
width: 100%;
height: 100%;
overflow: hidden;
background-color: @component-background;
border: 0;
box-sizing: border-box;
}
}
</style>

View File

@ -0,0 +1,234 @@
<template>
<div
:class="prefixCls"
class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
>
<div
:class="`${prefixCls}__unlock`"
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"
@click="handleShowForm(false)"
v-show="showDate"
>
<LockOutlined />
<span>{{ t('sys.lock.unlock') }}</span>
</div>
<div class="flex w-screen h-screen justify-center items-center">
<div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
<span>{{ hour }}</span>
<span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
{{ meridiem }}
</span>
</div>
<div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
<span> {{ minute }}</span>
</div>
</div>
<transition name="fade-slide">
<div :class="`${prefixCls}-entry`" v-show="!showDate">
<div :class="`${prefixCls}-entry-content`">
<div :class="`${prefixCls}-entry__header enter-x`">
<img :src="userinfo.avatar || headerImg" :class="`${prefixCls}-entry__header-img`" />
<p :class="`${prefixCls}-entry__header-name`">
{{ userinfo.realName }}
</p>
</div>
<InputPassword
:placeholder="t('sys.lock.placeholder')"
class="enter-x"
v-model:value="password"
/>
<span :class="`${prefixCls}-entry__err-msg enter-x`" v-if="errMsg">
{{ t('sys.lock.alert') }}
</span>
<div :class="`${prefixCls}-entry__footer enter-x`">
<a-button
type="link"
size="small"
class="mt-2 mr-2 enter-x"
:disabled="loading"
@click="handleShowForm(true)"
>
{{ t('common.back') }}
</a-button>
<a-button
type="link"
size="small"
class="mt-2 mr-2 enter-x"
:disabled="loading"
@click="goLogin"
>
{{ t('sys.lock.backToLogin') }}
</a-button>
<a-button class="mt-2" type="link" size="small" @click="unLock()" :loading="loading">
{{ t('sys.lock.entry') }}
</a-button>
</div>
</div>
</div>
</transition>
<div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
<div class="text-5xl mb-4 enter-x" v-show="!showDate">
{{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
</div>
<div class="text-2xl">{{ year }}/{{ month }}/{{ day }} {{ week }}</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { Input } from 'ant-design-vue';
import { useUserStore } from '/@/store/modules/user';
import { useLockStore } from '/@/store/modules/lock';
import { useI18n } from '/@/hooks/web/useI18n';
import { useNow } from './useNow';
import { useDesign } from '/@/hooks/web/useDesign';
import { LockOutlined } from '@ant-design/icons-vue';
import headerImg from '/@/assets/images/header.jpg';
const InputPassword = Input.Password;
const password = ref('');
const loading = ref(false);
const errMsg = ref(false);
const showDate = ref(true);
const { prefixCls } = useDesign('lock-page');
const lockStore = useLockStore();
const userStore = useUserStore();
const { hour, month, minute, meridiem, year, day, week } = useNow(true);
const { t } = useI18n();
const userinfo = computed(() => {
return userStore.getUserInfo || {};
});
/**
* @description: unLock
*/
async function unLock() {
if (!password.value) {
return;
}
let pwd = password.value;
try {
loading.value = true;
const res = await lockStore.unLock(pwd);
errMsg.value = !res;
} finally {
loading.value = false;
}
}
function goLogin() {
userStore.logout(true);
lockStore.resetLockInfo();
}
function handleShowForm(show = false) {
showDate.value = show;
}
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-lock-page';
.@{prefix-cls} {
z-index: @lock-page-z-index;
&__unlock {
transform: translate(-50%, 0);
}
&__hour,
&__minute {
display: flex;
font-weight: 700;
color: #bababa;
background-color: #141313;
border-radius: 30px;
justify-content: center;
align-items: center;
@media screen and (max-width: @screen-md) {
span:not(.meridiem) {
font-size: 160px;
}
}
@media screen and (min-width: @screen-md) {
span:not(.meridiem) {
font-size: 160px;
}
}
@media screen and (max-width: @screen-sm) {
span:not(.meridiem) {
font-size: 90px;
}
}
@media screen and (min-width: @screen-lg) {
span:not(.meridiem) {
font-size: 220px;
}
}
@media screen and (min-width: @screen-xl) {
span:not(.meridiem) {
font-size: 260px;
}
}
@media screen and (min-width: @screen-2xl) {
span:not(.meridiem) {
font-size: 320px;
}
}
}
&-entry {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
background-color: rgb(0 0 0 / 50%);
backdrop-filter: blur(8px);
justify-content: center;
align-items: center;
&-content {
width: 260px;
}
&__header {
text-align: center;
&-img {
width: 70px;
margin: 0 auto;
border-radius: 50%;
}
&-name {
margin-top: 5px;
font-weight: 500;
color: #bababa;
}
}
&__err-msg {
display: inline-block;
margin-top: 10px;
color: @error-color;
}
&__footer {
display: flex;
justify-content: space-between;
}
}
}
</style>

View File

@ -0,0 +1,13 @@
<template>
<transition name="fade-bottom" mode="out-in">
<LockPage v-if="getIsLock" />
</transition>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import LockPage from './LockPage.vue';
import { useLockStore } from '/@/store/modules/lock';
const lockStore = useLockStore();
const getIsLock = computed(() => lockStore?.getLockInfo?.isLock ?? false);
</script>

View File

@ -0,0 +1,60 @@
import { dateUtil } from '/@/utils/dateUtil';
import { reactive, toRefs } from 'vue';
import { tryOnMounted, tryOnUnmounted } from '@vueuse/core';
export function useNow(immediate = true) {
let timer: IntervalHandle;
const state = reactive({
year: 0,
month: 0,
week: '',
day: 0,
hour: '',
minute: '',
second: 0,
meridiem: '',
});
const update = () => {
const now = dateUtil();
const h = now.format('HH');
const m = now.format('mm');
const s = now.get('s');
state.year = now.get('y');
state.month = now.get('M') + 1;
state.week = '星期' + ['日', '一', '二', '三', '四', '五', '六'][now.day()];
state.day = now.get('date');
state.hour = h;
state.minute = m;
state.second = s;
state.meridiem = now.format('A');
};
function start() {
update();
clearInterval(timer);
timer = setInterval(() => update(), 1000);
}
function stop() {
clearInterval(timer);
}
tryOnMounted(() => {
immediate && start();
});
tryOnUnmounted(() => {
stop();
});
return {
...toRefs(state),
start,
stop,
};
}

View File

@ -0,0 +1,56 @@
<template>
<template v-if="getShow">
<LoginFormTitle class="enter-x" />
<Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef">
<FormItem name="account" class="enter-x">
<Input size="large" v-model:value="formData.account" :placeholder="t('账号')" />
</FormItem>
<FormItem name="mobile" class="enter-x">
<Input size="large" v-model:value="formData.mobile" :placeholder="t('手机号码')" />
</FormItem>
<FormItem name="sms" class="enter-x">
<CountdownInput size="large" v-model:value="formData.sms" :placeholder="t('短信验证码')" />
</FormItem>
<FormItem class="enter-x">
<Button type="primary" size="large" block @click="handleReset" :loading="loading">
{{ t('重置') }}
</Button>
<Button size="large" block class="mt-4" @click="handleBackLogin">
{{ t('返回') }}
</Button>
</FormItem>
</Form>
</template>
</template>
<script lang="ts" setup>
import { reactive, ref, computed, unref } from 'vue';
import LoginFormTitle from './LoginFormTitle.vue';
import { Form, Input, Button } from 'ant-design-vue';
import { CountdownInput } from '/@/components/CountDown';
import { useI18n } from '/@/hooks/web/useI18n';
import { useLoginState, useFormRules, LoginStateEnum } from './useLogin';
const FormItem = Form.Item;
const { t } = useI18n();
const { handleBackLogin, getLoginState } = useLoginState();
const { getFormRules } = useFormRules();
const formRef = ref();
const loading = ref(false);
const formData = reactive({
account: '',
mobile: '',
sms: '',
});
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD);
async function handleReset() {
const form = unref(formRef);
if (!form) return;
await form.resetFields();
}
</script>

View File

@ -0,0 +1,306 @@
<template>
<div :class="prefixCls" class="login-box relative w-full h-full">
<div class="login-left-box">
<div class="logo-box" v-if="show">
<a href="https://fcdma.gdyditc.com/" target="_blank">
<img :src="logoConfig.loginLogoUrl || logopng" />
</a>
</div>
<img :src="logoConfig.backgroundLogoUrl || logobg" class="h-full" />
</div>
<div class="fixed-tool">
<AppDarkModeToggle v-if="!sessionTimeout" />
<AppLocalePicker
class="text-white px-4"
:showText="false"
v-if="!sessionTimeout && showLocale"
/>
</div>
<div class="right-box">
<div class="login-left-title">
<div class="title">{{ t('欢迎登录') }}</div>
<div class="sub-title">{{ t('全代码开发平台') }}</div>
</div>
<div :class="`${prefixCls}-form`">
<LoginForm />
</div>
</div>
<div class="right-top-box">
<img src="../../../assets/images/login-right.gif" />
</div>
</div>
</template>
<script lang="ts" setup>
// import { computed } from 'vue';
import { AppLocalePicker, AppDarkModeToggle } from '/@/components/Application';
import LoginForm from './LoginForm.vue';
// import { useGlobSetting } from '/@/hooks/setting';
import { useI18n } from '/@/hooks/web/useI18n';
import { useDesign } from '/@/hooks/web/useDesign';
import { useLocaleStore } from '/@/store/modules/locale';
import { useAppStore } from '/@/store/modules/app';
import logopng from '/@/assets/images/logo-dark.png';
import logobg from '/@/assets/images/login-left.gif';
import { getLogoInfo } from '/@/api/system/login';
import { ref, onMounted } from 'vue';
import { LogoConfig } from '/#/config';
import { useMessage } from '/@/hooks/web/useMessage';
const { createErrorModal } = useMessage();
onMounted(() => {
let logoutInfoData = JSON.parse(sessionStorage.getItem('logoutInfoData') as string);
if (logoutInfoData) {
createErrorModal({ title: t('错误提示'), content: logoutInfoData.logoutMessage });
sessionStorage.removeItem('logoutInfoData');
}
})
const appStore = useAppStore();
defineProps({
sessionTimeout: {
type: Boolean,
},
});
// const globSetting = useGlobSetting();
const { prefixCls } = useDesign('login');
const { t } = useI18n();
const localeStore = useLocaleStore();
const showLocale = localeStore.getShowPicker;
// const title = computed(() => globSetting?.title ?? '');
let logoConfig = ref<LogoConfig>({});
let show = ref(false);
getLogoInfo().then((res) => {
show.value = true;
logoConfig.value = {
companyName: res.companyName,
shortName: res.shortName,
refreshLogoUrl: res.refreshLogoUrl,
backgroundLogoUrl: res.backgroundLogoUrl,
designerLogoUrl: res.designerLogoUrl,
loginLogoUrl: res.loginLogoUrl,
menuLogoUrl: res.menuLogoUrl,
};
appStore.setLogoConfig(logoConfig.value);
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-login';
@logo-prefix-cls: ~'@{namespace}-app-logo';
@countdown-prefix-cls: ~'@{namespace}-countdown-input';
@dark-bg: linear-gradient(to bottom, #364876, #112049);
.vben-login-form {
padding: 180px 100px 40px;
}
.login-box {
display: flex;
}
.logo-box {
position: fixed;
top: 50px;
left: 40px;
}
.logo-box img {
width: 180px;
}
.login-left-title {
position: relative;
top: 120px;
left: 100px;
}
.login-left-title .title {
font-size: 20px;
border: none;
margin-bottom: 10px;
}
.login-left-title .sub-title {
font-size: 36px;
font-weight: bold;
color: #5e95ff;
}
.login-box .right-box {
position: absolute;
right: 0;
}
.fixed-tool {
position: fixed;
right: 20px;
top: 40px;
display: flex;
z-index: 2;
}
.right-top-box {
width: 400px;
position: fixed;
right: 0;
top: 0;
}
html[data-theme='dark'] {
.login-box {
background: @dark-bg;
}
.login-left-title {
border: none;
}
.login-left-title .title {
color: #fff;
}
.@{prefix-cls} {
background-color: @dark-bg;
// &::before {
// background-image: url(/@/assets/svg/login-bg-dark.svg);
// }
.ant-input,
.ant-input-password {
background-color: #232a3b;
}
.ant-input-affix-wrapper {
border-color: #525e7c;
background-color: #232a3b;
}
.ant-checkbox-inner {
border-color: #525e7c;
}
.ant-btn:not(.ant-btn-link, .ant-btn-primary) {
border: 1px solid #4a5569;
}
&-form {
background: transparent !important;
}
.app-iconify {
color: #fff;
}
}
input.fix-auto-fill,
.fix-auto-fill input {
-webkit-text-fill-color: #c9d1d9 !important;
box-shadow: inherit !important;
}
}
.@{prefix-cls} {
min-height: 100%;
overflow: hidden;
@media (max-width: @screen-xl) {
background-color: @dark-bg;
.@{prefix-cls}-form {
background-color: #fff;
}
}
&::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin-left: -48%;
// background-image: url(/@/assets/svg/login-bg.svg);
background-position: 100%;
background-repeat: no-repeat;
background-size: auto 100%;
content: '';
@media (max-width: @screen-xl) {
display: none;
}
}
.@{logo-prefix-cls} {
position: absolute;
top: 12px;
height: 30px;
&__title {
font-size: 16px;
color: #fff;
}
img {
width: 32px;
}
}
.container {
.@{logo-prefix-cls} {
display: flex;
width: 60%;
height: 80px;
&__title {
font-size: 24px;
color: #fff;
}
img {
width: 48px;
}
}
}
&-sign-in-way {
.anticon {
font-size: 22px;
color: #888;
cursor: pointer;
&:hover {
color: @primary-color;
}
}
}
input:not([type='checkbox']) {
min-width: 360px;
@media (max-width: @screen-xl) {
min-width: 320px;
}
@media (max-width: @screen-lg) {
min-width: 260px;
}
@media (max-width: @screen-md) {
min-width: 240px;
}
@media (max-width: @screen-sm) {
min-width: 160px;
}
}
.@{countdown-prefix-cls} input {
min-width: unset;
}
.ant-divider-inner-text {
font-size: 12px;
color: @text-color-secondary;
}
}
</style>

View File

@ -0,0 +1,335 @@
<template>
<Form
class="p-4 enter-x form-box"
:model="formData"
:rules="getFormRules"
ref="formRef"
v-show="getShow"
@keypress.enter="handleLogin"
>
<div>
<FormItem name="account" class="enter-x">
<label class="form-title"> {{ t('账号') }}</label>
<Input
size="large"
v-model:value="formData.account"
:placeholder="t('账号')"
class="fix-auto-fill"
style="height: 58px"
><template #prefix>
<IconFontSymbol icon="yonghu-xianxing" class="user-icon" />
</template>
</Input>
</FormItem>
<FormItem name="password" class="enter-x">
<label class="form-title"> {{ t('密码') }}</label>
<InputPassword
size="large"
visibilityToggle
v-model:value="formData.password"
:placeholder="t('密码')"
style="height: 58px"
><template #prefix>
<IconFontSymbol icon="mima" class="user-icon" />
</template>
</InputPassword>
</FormItem>
<FormItem v-if="getAppEnvConfig().VITE_GLOB_TENANT_ENABLED" name="tenantCode" class="enter-x">
<label class="form-title"> {{ t('租户码') }}</label>
<Input
size="large"
visibilityToggle
v-model:value="formData.tenantCode"
:placeholder="t('租户码')"
style="height: 58px"
><template #prefix>
<IconFontSymbol icon="mima" class="user-icon" />
</template>
</Input>
</FormItem>
<ARow class="enter-x">
<ACol :span="12">
<FormItem>
<!-- No logic, you need to deal with it yourself -->
<Checkbox v-model:checked="rememberMe" size="small" class="f-16">
{{ t('记住我') }}
</Checkbox>
</FormItem>
</ACol>
<!-- <ACol :span="12">
<FormItem :style="{ 'text-align': 'right' }">
No logic, you need to deal with it yourself
<Button type="link" size="small" @click="setLoginState(LoginStateEnum.RESET_PASSWORD)">
{{ t('忘记密码') }}
</Button>
</FormItem>
</ACol> -->
</ARow>
<FormItem class="enter-x">
<Button
type="primary"
class="sub-button"
block
@click="handleLogin"
:loading="loading"
:style="{ 'border-radius': '35px' }"
>
{{ t('登录') }}
</Button>
<!-- <Button size="large" class="mt-4 enter-x" block @click="handleRegister">
{{ t('注册') }}
</Button> -->
</FormItem>
<!-- <ARow class="enter-x">
<ACol :md="8" :xs="24">
<Button block @click="setLoginState(LoginStateEnum.MOBILE)">
{{ t('手机登录') }}
</Button>
</ACol>
<ACol :md="8" :xs="24" class="!my-2 !md:my-0 xs:mx-0 md:mx-2">
<Button block @click="setLoginState(LoginStateEnum.QR_CODE)">
{{ t('二维码登录') }}
</Button>
</ACol>
<ACol :md="6" :xs="24">
<Button block @click="setLoginState(LoginStateEnum.REGISTER)">
{{ t('注册') }}
</Button>
</ACol>
</ARow> -->
<a-divider class="enter-x">{{ t('其他登录方式') }}</a-divider>
</div>
<!-- <div style="height: 1000px; width: 800px" v-else>
<iframe id="iframeId" style="height: 100%; width: 100%" :src="authorizeUrl"></iframe>
</div> -->
<div class="flex justify-evenly enter-x" :class="`${prefixCls}-sign-in-way`">
<WechatFilled @click="oauthLogin('wechat_enterprise')" />
<DingtalkCircleFilled @click="oauthLogin('dingtalk')" />
</div>
</Form>
</template>
<script lang="ts" setup>
import { reactive, ref, unref, computed, onMounted } from 'vue';
import { Checkbox, Form, Input, Row, Col, Button } from 'ant-design-vue';
import { WechatFilled, DingtalkCircleFilled } from '@ant-design/icons-vue';
// import LoginFormTitle from './LoginFormTitle.vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useMessage } from '/@/hooks/web/useMessage';
import { useUserStore } from '/@/store/modules/user';
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
import { useDesign } from '/@/hooks/web/useDesign';
import { Base64 } from 'js-base64';
//import { onKeyStroke } from '@vueuse/core';
import IconFontSymbol from '/@/components/IconFontSymbol/Index.vue';
import { getOauthAuthorizeUrl } from '/@/api/system/login';
import { useRouter } from 'vue-router';
import { getAppEnvConfig } from '/@/utils/env';
// import { nextTick } from 'vue';
const ACol = Col;
const ARow = Row;
const FormItem = Form.Item;
const InputPassword = Input.Password;
const { t } = useI18n();
const { notification, createErrorModal } = useMessage();
const { prefixCls } = useDesign('login');
const userStore = useUserStore();
const { currentRoute } = useRouter();
const { getLoginState } = useLoginState();
const { getFormRules } = useFormRules();
const formRef = ref();
// const iframeRef = ref();
const loading = ref(false);
const rememberMe = ref(false);
const showFlag = ref(false);
const authorizeUrl = ref('');
const formData = reactive({
account: '',
password: '',
tenantCode: 'system',
});
onMounted(async () => {
//如果是第三方登录跳转回来 会携带token
if (currentRoute.value.query.token) {
try {
loading.value = true;
const userInfo = await userStore.oauthLogin({
token: currentRoute.value.query.token as string,
mode: 'none', //不要默认的错误提示
});
if (userInfo) {
notification.success({
message: t('登录成功'),
description: `${t('欢迎回来')}: ${userInfo.name}`,
duration: 3,
});
}
} catch (error) {
createErrorModal({
title: t('错误提示'),
content:
(error as unknown as Error).message || t('网络异常,请检查您的网络连接是否正常!'),
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body,
});
} finally {
loading.value = false;
}
}
//如果第三方登录 登录错误 会携带错误信息
if (currentRoute.value.query.error) {
createErrorModal({
title: t('错误提示'),
content: t(currentRoute.value.query.error as string),
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body,
});
}
const loginInfo = window.localStorage.getItem('USER__LOGIN__INFO__');
if (loginInfo) {
formData.account = Base64.decode(JSON.parse(loginInfo).account);
formData.password = Base64.decode(JSON.parse(loginInfo).password);
}
});
const { validForm } = useFormValid(formRef);
//onKeyStroke('Enter', handleLogin);
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN);
async function handleLogin() {
const data = await validForm();
if (!data) return;
try {
loading.value = true;
const userInfo = await userStore.login({
password: data.password,
userName: data.account,
tenantCode: data.tenantCode,
deviceType: 0, //pc-0,app-1
mode: 'none', //不要默认的错误提示
});
if (userInfo) {
notification.success({
message: t('登录成功'),
description: `${t('欢迎回来')}: ${userInfo.name}`,
duration: 3,
});
if (rememberMe.value) {
const info = {
account: Base64.encode(data.account),
password: Base64.encode(data.password),
};
window.localStorage.setItem('USER__LOGIN__INFO__', JSON.stringify(info));
} else {
window.localStorage.removeItem('USER__LOGIN__INFO__');
}
}
} catch (error) {
createErrorModal({
title: t('错误提示'),
content: (error as unknown as Error).message || t('网络异常,请检查您的网络连接是否正常!'),
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body,
});
} finally {
loading.value = false;
}
}
async function oauthLogin(source: string) {
showFlag.value = true;
authorizeUrl.value = await getOauthAuthorizeUrl(source);
window.location.href = authorizeUrl.value;
// let iframe = document.getElementById('iframeId');
// console.log(iframe, 'sssssssssss');
// var MutationObserver = window.MutationObserver;
// // 创建一个观察器实例
// var observer = new MutationObserver((item) => {
// iframeChange(item);
// });
// // 观察器的配置
// var options = {
// childList: true, // 子节点的变动(指新增,删除或者更改) Boolean
// attributes: true, // 属性的变动 Boolean
// subtree: true, //表示是否将该观察器应用于该节点的所有后代节点 Boolean
// attributeOldValue: true, // 表示观察attributes变动时是否需要记录变动前的属性 Boolean
// characterData: true, // 节点内容或节点文本的变动 Boolean
// // attributeFilter: ['src'], // 表示需要观察的特定属性 Array如['class','src', 'style']
// };
// // console.log('formRef', iframe.contentWindow.document);
// // 开始观察目标节点
// function iframeChange(item) {
// console.log(item, '节点发生变化');
// }
// observer.observe(document, options);
}
</script>
<style lang="less" scoped>
.form-box {
font-size: 16px;
}
.f-16 {
font-size: 16px;
}
:deep(.ant-checkbox-inner) {
border-color: #ced5f2;
width: 18px;
height: 18px;
border-radius: 0;
}
:deep(.ant-form-item input[type='checkbox']) {
width: 18px;
height: 18px;
}
.form-title {
line-height: 40px;
}
.sub-button {
height: 60px;
font-size: 20px;
}
.ant-form label {
font-size: 16px;
}
.user-icon {
font-size: 26px;
margin-right: 8px;
color: #707c92;
}
.ant-input-affix-wrapper {
border-color: #ced5f2;
border-style: none none solid;
}
:deep(.ant-input-password-icon) {
font-size: 20px;
color: #707c92;
margin-right: 8px;
}
:deep(.ant-input-affix-wrapper-lg) {
padding-left: 0;
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<h2 class="mb-3 text-2xl font-bold text-center xl:text-3xl enter-x xl:text-left">
{{ getFormTitle }}
</h2>
</template>
<script lang="ts" setup>
import { computed, unref } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { LoginStateEnum, useLoginState } from './useLogin';
const { t } = useI18n();
const { getLoginState } = useLoginState();
const getFormTitle = computed(() => {
const titleObj = {
[LoginStateEnum.RESET_PASSWORD]: t('重置密码'),
[LoginStateEnum.LOGIN]: t('登录'),
[LoginStateEnum.REGISTER]: t('注册'),
[LoginStateEnum.MOBILE]: t('手机登录'),
[LoginStateEnum.QR_CODE]: t('二维码登录'),
};
return titleObj[unref(getLoginState)];
});
</script>

View File

@ -0,0 +1,63 @@
<template>
<template v-if="getShow">
<LoginFormTitle class="enter-x" />
<Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef">
<FormItem name="mobile" class="enter-x">
<Input
size="large"
v-model:value="formData.mobile"
:placeholder="t('手机号码')"
class="fix-auto-fill"
/>
</FormItem>
<FormItem name="sms" class="enter-x">
<CountdownInput
size="large"
class="fix-auto-fill"
v-model:value="formData.sms"
:placeholder="t('短信验证码')"
/>
</FormItem>
<FormItem class="enter-x">
<Button type="primary" size="large" block @click="handleLogin" :loading="loading">
{{ t('登录') }}
</Button>
<Button size="large" block class="mt-4" @click="handleBackLogin">
{{ t('返回') }}
</Button>
</FormItem>
</Form>
</template>
</template>
<script lang="ts" setup>
import { reactive, ref, computed, unref } from 'vue';
import { Form, Input, Button } from 'ant-design-vue';
import { CountdownInput } from '/@/components/CountDown';
import LoginFormTitle from './LoginFormTitle.vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useLoginState, useFormRules, useFormValid, LoginStateEnum } from './useLogin';
const FormItem = Form.Item;
const { t } = useI18n();
const { handleBackLogin, getLoginState } = useLoginState();
const { getFormRules } = useFormRules();
const formRef = ref();
const loading = ref(false);
const formData = reactive({
mobile: '',
sms: '',
});
const { validForm } = useFormValid(formRef);
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.MOBILE);
async function handleLogin() {
const data = await validForm();
if (!data) return;
console.log(data);
}
</script>

View File

@ -0,0 +1,31 @@
<template>
<template v-if="getShow">
<LoginFormTitle class="enter-x" />
<div class="enter-x min-w-64 min-h-64">
<QrCode
:value="qrCodeUrl"
class="enter-x flex justify-center xl:justify-start"
:width="280"
/>
<Divider class="enter-x">{{ t('扫码后点击"确认",即可完成登录') }}</Divider>
<Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
{{ t('返回') }}
</Button>
</div>
</template>
</template>
<script lang="ts" setup>
import { computed, unref } from 'vue';
import LoginFormTitle from './LoginFormTitle.vue';
import { Button, Divider } from 'ant-design-vue';
import { QrCode } from '/@/components/Qrcode/index';
import { useI18n } from '/@/hooks/web/useI18n';
import { useLoginState, LoginStateEnum } from './useLogin';
const qrCodeUrl = 'https://vvbin.cn/next/login';
const { t } = useI18n();
const { handleBackLogin, getLoginState } = useLoginState();
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.QR_CODE);
</script>

View File

@ -0,0 +1,100 @@
<template>
<template v-if="getShow">
<LoginFormTitle class="enter-x" />
<Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef">
<FormItem name="account" class="enter-x">
<Input
class="fix-auto-fill"
size="large"
v-model:value="formData.account"
:placeholder="t('账号')"
/>
</FormItem>
<FormItem name="mobile" class="enter-x">
<Input
size="large"
v-model:value="formData.mobile"
:placeholder="t('手机号码')"
class="fix-auto-fill"
/>
</FormItem>
<FormItem name="sms" class="enter-x">
<CountdownInput
size="large"
class="fix-auto-fill"
v-model:value="formData.sms"
:placeholder="t('短信验证码')"
/>
</FormItem>
<FormItem name="password" class="enter-x">
<StrengthMeter size="large" v-model:value="formData.password" :placeholder="t('mima')" />
</FormItem>
<FormItem name="confirmPassword" class="enter-x">
<InputPassword
size="large"
visibilityToggle
v-model:value="formData.confirmPassword"
:placeholder="t('确认密码')"
/>
</FormItem>
<FormItem class="enter-x" name="policy">
<!-- No logic, you need to deal with it yourself -->
<Checkbox v-model:checked="formData.policy" size="small">
{{ t('我同意xxx隐私政策') }}
</Checkbox>
</FormItem>
<Button
type="primary"
class="enter-x"
size="large"
block
@click="handleRegister"
:loading="loading"
>
{{ t('注册') }}
</Button>
<Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
{{ t('返回') }}
</Button>
</Form>
</template>
</template>
<script lang="ts" setup>
import { reactive, ref, unref, computed } from 'vue';
import LoginFormTitle from './LoginFormTitle.vue';
import { Form, Input, Button, Checkbox } from 'ant-design-vue';
import { StrengthMeter } from '/@/components/StrengthMeter';
import { CountdownInput } from '/@/components/CountDown';
import { useI18n } from '/@/hooks/web/useI18n';
import { useLoginState, useFormRules, useFormValid, LoginStateEnum } from './useLogin';
const FormItem = Form.Item;
const InputPassword = Input.Password;
const { t } = useI18n();
const { handleBackLogin, getLoginState } = useLoginState();
const formRef = ref();
const loading = ref(false);
const formData = reactive({
account: '',
password: '',
confirmPassword: '',
mobile: '',
sms: '',
policy: false,
});
const { getFormRules } = useFormRules(formData);
const { validForm } = useFormValid(formRef);
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER);
async function handleRegister() {
const data = await validForm();
if (!data) return;
console.log(data);
}
</script>

View File

@ -0,0 +1,51 @@
<template>
<div :class="prefixCls">
<Login sessionTimeout />
</div>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref } from 'vue';
import Login from '/@/views/secondDev/Login.vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useUserStore } from '/@/store/modules/user';
import { usePermissionStore } from '/@/store/modules/permission';
import { useAppStore } from '/@/store/modules/app';
import { PermissionModeEnum } from '/@/enums/appEnum';
const { prefixCls } = useDesign('st-login');
const userStore = useUserStore();
const permissionStore = usePermissionStore();
const appStore = useAppStore();
const userId = ref<Nullable<number | string>>(0);
const isBackMode = () => {
return appStore.getProjectConfig.permissionMode === PermissionModeEnum.BACK;
};
onMounted(() => {
// 记录当前的UserId
userId.value = userStore.getUserInfo?.userId;
console.log('Mounted', userStore.getUserInfo);
});
onBeforeUnmount(() => {
if (userId.value && userId.value !== userStore.getUserInfo.userId) {
// 登录的不是同一个用户,刷新整个页面以便丢弃之前用户的页面状态
document.location.reload();
} else if (isBackMode() && permissionStore.getLastBuildMenuTime === 0) {
// 后台权限模式下没有成功加载过菜单就重新加载整个页面。这通常发生在会话过期后按F5刷新整个页面后载入了本模块这种场景
document.location.reload();
}
});
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-st-login';
.@{prefix-cls} {
position: fixed;
z-index: 9999999;
width: 100%;
height: 100%;
background: @component-background;
}
</style>

View File

@ -0,0 +1,129 @@
import type { ValidationRule } from 'ant-design-vue/lib/form/Form';
import type { RuleObject } from 'ant-design-vue/lib/form/interface';
import { ref, computed, unref, Ref } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
export enum LoginStateEnum {
LOGIN,
REGISTER,
RESET_PASSWORD,
MOBILE,
QR_CODE,
LOGIN_WITH_CAPTCHA,
}
const currentState = ref(LoginStateEnum.LOGIN);
export function useLoginState() {
function setLoginState(state: LoginStateEnum) {
currentState.value = state;
}
const getLoginState = computed(() => currentState.value);
function handleBackLogin() {
setLoginState(LoginStateEnum.LOGIN);
}
return { setLoginState, getLoginState, handleBackLogin };
}
export function useFormValid<T extends Object = any>(formRef: Ref<any>) {
async function validForm() {
const form = unref(formRef);
if (!form) return;
const data = await form.validate();
return data as T;
}
return { validForm };
}
export function useFormRules(formData?: Recordable) {
const { t } = useI18n();
const getAccountFormRule = computed(() => createRule(t('请输入账号')));
const getPasswordFormRule = computed(() => createRule(t('请输入密码')));
const getCaptchaCodeFormRule = computed(() => createRule(t('请输入图形验证码')));
const getSmsFormRule = computed(() => createRule(t('请输入验证码')));
const getMobileFormRule = computed(() => createRule(t('请输入手机号码')));
const validatePolicy = async (_: RuleObject, value: boolean) => {
return !value ? Promise.reject(t('勾选后才能注册')) : Promise.resolve();
};
const validateConfirmPassword = (password: string) => {
return async (_: RuleObject, value: string) => {
if (!value) {
return Promise.reject(t('请输入密码'));
}
if (value !== password) {
return Promise.reject(t('两次输入密码不一致'));
}
return Promise.resolve();
};
};
const getFormRules = computed((): { [k: string]: ValidationRule | ValidationRule[] } => {
const accountFormRule = unref(getAccountFormRule);
const passwordFormRule = unref(getPasswordFormRule);
const captchaCodeFormRule = unref(getCaptchaCodeFormRule);
const smsFormRule = unref(getSmsFormRule);
const mobileFormRule = unref(getMobileFormRule);
const mobileRule = {
sms: smsFormRule,
mobile: mobileFormRule,
};
switch (unref(currentState)) {
// register form rules
case LoginStateEnum.REGISTER:
return {
account: accountFormRule,
password: passwordFormRule,
confirmPassword: [
{ validator: validateConfirmPassword(formData?.password), trigger: 'change' },
],
policy: [{ validator: validatePolicy, trigger: 'change' }],
...mobileRule,
};
// reset password form rules
case LoginStateEnum.RESET_PASSWORD:
return {
account: accountFormRule,
...mobileRule,
};
// mobile form rules
case LoginStateEnum.MOBILE:
return mobileRule;
// 枚举没实现,暂不适用
case LoginStateEnum.LOGIN_WITH_CAPTCHA:
return {
account: accountFormRule,
password: passwordFormRule,
captchaCode: captchaCodeFormRule,
};
// login form rules
default:
return {
account: accountFormRule,
password: passwordFormRule,
};
}
});
return { getFormRules };
}
function createRule(message: string) {
return [
{
required: true,
message,
trigger: 'change',
},
];
}

View File

@ -0,0 +1,30 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import { unref } from 'vue';
import { useRouter } from 'vue-router';
const { currentRoute, replace } = useRouter();
const { params, query } = unref(currentRoute);
const { path, _redirect_type = 'path' } = params;
Reflect.deleteProperty(params, '_redirect_type');
Reflect.deleteProperty(params, 'path');
const _path = Array.isArray(path) ? path.join('/') : path;
if (_redirect_type === 'name') {
replace({
name: _path,
query,
params,
});
} else {
replace({
path: _path.startsWith('/') ? _path : '/' + _path,
query,
});
}
</script>