前端和后端源码,合并到一个git仓库中,方便用户下载,避免前后端不匹配的问题

This commit is contained in:
JEECG
2024-06-23 10:39:52 +08:00
parent bb918b742e
commit 0325e34dcb
1439 changed files with 171106 additions and 0 deletions

View File

@ -0,0 +1,100 @@
<template>
<PageWrapper title="关于">
<template #headerContent>
<div class="flex justify-between items-center">
<span class="flex-1">
<a :href="GITHUB_URL" target="_blank">
<!-- {{ name }}-->
JeecgBoot
</a>
是一款基于BPM的低代码开发平台前后端分离架构 SpringBoot 2.xSpringCloudAnt Design&VueMybatis-plusShiroJWT支持微服务。强大的代码生成器让前后端代码一键生成实现低代码开发 JeecgBoot引领新低代码开发模式 OnlineCoding-> 代码生成器-> 手工MERGE 帮助Java项目解决70%的重复工作让开发更多关注业务既能快速提高效率节省研发成本同时又不失灵活性一系列低代码能力Online表单、Online报表、Online图表、表单设计、流程设计、报表设计、大屏设计 等等...。
</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,88 @@
<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,143 @@
<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('sys.exception.backLogin');
const backHomeI18n = t('sys.exception.backHome');
unref(statusMapRef).set(ExceptionEnum.PAGE_NOT_ACCESS, {
title: '403',
status: `${ExceptionEnum.PAGE_NOT_ACCESS}`,
subTitle: t('sys.exception.subTitle403'),
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('sys.exception.subTitle404'),
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('sys.exception.subTitle500'),
btnText: backHomeI18n,
handler: () => go(),
});
unref(statusMapRef).set(ExceptionEnum.PAGE_NOT_DATA, {
title: t('sys.exception.noDataTitle'),
subTitle: '',
btnText: t('common.redo'),
handler: () => redo(),
icon: notDataSvg,
});
unref(statusMapRef).set(ExceptionEnum.NET_WORK_ERROR, {
title: t('sys.exception.networkErrorTitle'),
subTitle: t('sys.exception.networkErrorSubTitle'),
btnText: t('common.redo'),
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,11 @@
<template>
<Exception :status="status" />
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { Exception } from '/@/views/sys/exception/index';
import { ExceptionEnum } from '/@/enums/exceptionEnum';
const status = ref(ExceptionEnum.NET_WORK_ERROR);
</script>

View File

@ -0,0 +1,11 @@
<template>
<Exception :status="status" />
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { Exception } from '/@/views/sys/exception/index';
import { ExceptionEnum } from '/@/enums/exceptionEnum';
const status = ref(ExceptionEnum.PAGE_NOT_ACCESS);
</script>

View File

@ -0,0 +1,11 @@
<template>
<Exception :status="status" />
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { Exception } from '/@/views/sys/exception/index';
import { ExceptionEnum } from '/@/enums/exceptionEnum';
const status = ref(ExceptionEnum.PAGE_NOT_DATA);
</script>

View File

@ -0,0 +1,11 @@
<template>
<Exception :status="status" />
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { Exception } from '/@/views/sys/exception/index';
import { ExceptionEnum } from '/@/enums/exceptionEnum';
const status = ref(ExceptionEnum.ERROR);
</script>

View File

@ -0,0 +1,5 @@
export { default as Exception } from './Exception.vue';
export { default as NotAccessException } from './NotAccessException.vue';
export { default as NetworkErrorException } from './NetworkErrorException.vue';
export { default as NotDataErrorException } from './NotDataErrorException.vue';
export { default as ServerErrorException } from './ServerErrorException.vue';

View File

@ -0,0 +1,96 @@
<template>
<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('sys.login.mobile')" />
</FormItem>
<FormItem name="sms" class="enter-x">
<CountdownInput size="large" v-model:value="formData.sms" :placeholder="t('sys.login.smsCode')" :sendCodeApi="sendCodeApi" />
</FormItem>
<FormItem class="enter-x">
<Button type="primary" size="large" block @click="handleNext" :loading="loading"> 下一步 </Button>
<Button size="large" block class="mt-4" @click="handleBackLogin">
{{ t('sys.login.backSignIn') }}
</Button>
</FormItem>
</Form>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, computed, unref, toRaw } from 'vue';
import { Form, Input, Button, steps } from 'ant-design-vue';
import { CountdownInput } from '/@/components/CountDown';
import { useI18n } from '/@/hooks/web/useI18n';
import { useMessage } from '/@/hooks/web/useMessage';
import { useLoginState, useFormRules, useFormValid, LoginStateEnum, SmsEnum } from '../login/useLogin';
import { phoneVerify, getCaptcha } from '/@/api/sys/user';
export default defineComponent({
name: 'step1',
components: {
Button,
Form,
FormItem: Form.Item,
Input,
CountdownInput,
},
emits: ['nextStep'],
setup(_, { emit }) {
const { t } = useI18n();
const { handleBackLogin } = useLoginState();
const { notification } = useMessage();
const formRef = ref();
const { validForm } = useFormValid(formRef);
const { getFormRules } = useFormRules();
const loading = ref(false);
const formData = reactive({
mobile: '',
sms: '',
});
/**
* 下一步
*/
async function handleNext() {
const data = await validForm();
if (!data) return;
const resultInfo = await phoneVerify(
toRaw({
phone: data.mobile,
smscode: data.sms,
})
);
if (resultInfo.success) {
let accountInfo = {
username: resultInfo.result.username,
phone: data.mobile,
smscode: resultInfo.result.smscode,
};
emit('nextStep', accountInfo);
} else {
notification.error({
message: t('sys.api.errorTip'),
description: resultInfo.message || t('sys.api.networkExceptionMsg'),
duration: 3,
});
}
}
//倒计时执行前的函数
function sendCodeApi() {
return getCaptcha({ mobile: formData.mobile, smsmode: SmsEnum.FORGET_PASSWORD });
}
return {
t,
formRef,
formData,
getFormRules,
handleNext,
loading,
handleBackLogin,
sendCodeApi,
};
},
});
</script>

View File

@ -0,0 +1,103 @@
<template>
<Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef">
<FormItem name="username" class="enter-x">
<Input size="large" v-model:value="formData.username" :placeholder="t('sys.login.userName')" disabled />
</FormItem>
<FormItem name="password" class="enter-x">
<StrengthMeter size="large" v-model:value="formData.password" :placeholder="t('sys.login.password')" />
</FormItem>
<FormItem name="confirmPassword" class="enter-x">
<InputPassword size="large" visibilityToggle v-model:value="formData.confirmPassword" :placeholder="t('sys.login.confirmPassword')" />
</FormItem>
<FormItem class="enter-x">
<Button type="primary" size="large" block @click="handlePrev"> 上一步 </Button>
<Button size="large" block class="mt-4" @click="handleNext"> 下一步 </Button>
</FormItem>
</Form>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, computed, unref, toRaw, toRefs } from 'vue';
import { Form, Input, Button } from 'ant-design-vue';
import { StrengthMeter } from '/@/components/StrengthMeter';
import { useI18n } from '/@/hooks/web/useI18n';
import { useMessage } from '/@/hooks/web/useMessage';
import { useFormRules, useFormValid } from '../login/useLogin';
import { passwordChange } from '/@/api/sys/user';
export default defineComponent({
name: 'step2',
components: {
Button,
Form,
FormItem: Form.Item,
InputPassword: Input.Password,
Input,
StrengthMeter,
},
props: {
accountInfo: {
type: Object,
default: () => ({}),
},
},
emits: ['prevStep', 'nextStep'],
setup(props, { emit }) {
const { t } = useI18n();
const { createErrorModal } = useMessage();
const { accountInfo } = props;
const formRef = ref();
const formData = reactive({
username: accountInfo.obj.username || '',
password: '',
confirmPassword: '',
});
const { getFormRules } = useFormRules(formData);
const { validForm } = useFormValid(formRef);
/**
* 上一步
*/
function handlePrev() {
emit('prevStep', accountInfo.obj);
}
/**
* 下一步
*/
async function handleNext() {
const data = await validForm();
if (!data) return;
const resultInfo = await passwordChange(
toRaw({
username: data.username,
password: data.password,
smscode: accountInfo.obj.smscode,
phone: accountInfo.obj.phone,
})
);
if (resultInfo.success) {
//修改密码
emit('nextStep', accountInfo.obj);
} else {
//错误提示
createErrorModal({
title: t('sys.api.errorTip'),
content: resultInfo.message || t('sys.api.networkExceptionMsg'),
});
}
}
return {
t,
formRef,
formData,
getFormRules,
handleNext,
handlePrev,
};
},
});
</script>

View File

@ -0,0 +1,71 @@
<template>
<Result status="success" title="更改密码成功" :sub-title="getSubTitle">
<template #extra>
<a-button key="console" type="primary" @click="finish"> 返回登录 </a-button>
</template>
</Result>
</template>
<script lang="ts">
import { defineComponent, ref, computed, unref, onMounted, watchEffect, watch } from 'vue';
import { Form, Input, Button, Result } from 'ant-design-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useLoginState } from '../login/useLogin';
import { useCountdown } from '/@/components/CountDown/src/useCountdown';
import { propTypes } from '/@/utils/propTypes';
export default defineComponent({
name: 'step3',
components: {
Button,
Form,
FormItem: Form.Item,
Input,
Result,
},
props: {
accountInfo: {
type: Object,
default: () => ({}),
},
count: propTypes.number.def(5),
},
emits: ['finish'],
setup(props, { emit }) {
const { t } = useI18n();
const { accountInfo } = props;
const { handleBackLogin } = useLoginState();
const { currentCount, start } = useCountdown(props.count);
const getSubTitle = computed(() => {
return t('sys.login.subTitleText', [unref(currentCount)]);
});
/**
* 倒计时
*/
watchEffect(() => {
if (unref(currentCount) === 1) {
setTimeout(() => {
finish();
}, 500);
}
});
/**
* 结束回调
*/
function finish() {
handleBackLogin();
emit('finish');
}
onMounted(() => {
start();
});
return {
getSubTitle,
finish,
};
},
});
</script>

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,85 @@
<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,241 @@
<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 @change="unLock('change')" @keyup.enter="unLock('enter')" :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`" style="justify-content:center;margin-top: 4px">
<!-- <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" :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, onMounted, onUnmounted} 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
*
* @param type enter 回车 change input值发生改变 不提示锁屏密码错误
*/
async function unLock(type) {
if (!password.value) {
return;
}
let pwd = password.value;
try {
loading.value = true;
const res = await lockStore.unLock(pwd);
if(type === 'enter'){
errMsg.value = !res;
}
} finally {
loading.value = false;
}
}
function goLogin() {
userStore.logout(true);
lockStore.resetLockInfo();
}
function handleShowForm(show = false) {
showDate.value = show;
}
/**
* 监听键盘触发事件
*
* @param event
*/
function handleKeyDown(event) {
if (event.key === 'Escape') {
// 处理回车键按下事件
handleShowForm(true);
password.value = '';
}
}
onMounted(()=>{
window.addEventListener('keydown', handleKeyDown);
})
onUnmounted(()=>{
window.removeEventListener('keydown', handleKeyDown);
})
</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: rgba(0, 0, 0, 0.5);
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,68 @@
<template>
<template v-if="getShow">
<!--节点-->
<a-steps style="margin-bottom: 20px" :current="currentTab">
<a-step title="手机验证" />
<a-step title="更改密码" />
<a-step title="完成" />
</a-steps>
<!--组件-->
<div>
<step1 v-if="currentTab === 0" @nextStep="nextStep" />
<step2 v-if="currentTab === 1" @nextStep="nextStep" @prevStep="prevStep" :accountInfo="accountInfo" />
<step3 v-if="currentTab === 2" @prevStep="prevStep" @finish="finish" />
</div>
</template>
</template>
<script lang="ts" setup>
import { reactive, ref, computed, unref } from 'vue';
import { useLoginState, useFormRules, LoginStateEnum } from './useLogin';
import step1 from '../forget-password/step1.vue';
import step2 from '../forget-password/step2.vue';
import step3 from '../forget-password/step3.vue';
const { handleBackLogin, getLoginState } = useLoginState();
const { getFormRules } = useFormRules();
const formRef = ref();
const loading = ref(false);
const currentTab = ref(0);
const formData = reactive({
account: '',
mobile: '',
sms: '',
});
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD);
const accountInfo = reactive({
obj: {
username: '',
phone: '',
smscode: '',
},
});
/**
* 下一步
* @param data
*/
function nextStep(data) {
accountInfo.obj = data;
if (currentTab.value < 4) {
currentTab.value += 1;
}
}
/**
* 上一步
* @param data
*/
function prevStep(data) {
accountInfo.obj = data;
if (currentTab.value > 0) {
currentTab.value -= 1;
}
}
/**
* 结束
*/
function finish() {
currentTab.value = 0;
}
</script>

View File

@ -0,0 +1,208 @@
<template>
<div :class="prefixCls" class="relative w-full h-full px-4">
<AppLocalePicker class="absolute text-white top-4 right-4 enter-x xl:text-gray-600" :showText="false" v-if="!sessionTimeout && showLocale" />
<AppDarkModeToggle class="absolute top-3 right-7 enter-x" v-if="!sessionTimeout" />
<span class="-enter-x xl:hidden">
<AppLogo :alwaysShowTitle="true" />
</span>
<div class="container relative h-full py-2 mx-auto sm:px-10">
<div class="flex h-full">
<div class="hidden min-h-full pl-4 mr-4 xl:flex xl:flex-col xl:w-6/12">
<AppLogo class="-enter-x" />
<div class="my-auto">
<img :alt="title" src="../../../assets/svg/login-box-bg.svg" class="w-1/2 -mt-16 -enter-x" />
<div class="mt-10 font-medium text-white -enter-x">
<span class="inline-block mt-4 text-3xl"> {{ t('sys.login.signInTitle') }}</span>
</div>
<div class="mt-5 font-normal text-white text-md dark:text-gray-500 -enter-x">
{{ t('sys.login.signInDesc') }}
</div>
</div>
</div>
<div class="flex w-full h-full py-5 xl:h-auto xl:py-0 xl:my-0 xl:w-6/12">
<div
:class="`${prefixCls}-form`"
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"
>
<LoginForm />
<ForgetPasswordForm />
<RegisterForm />
<MobileForm />
<QrCodeForm />
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { AppLogo } from '/@/components/Application';
import { AppLocalePicker, AppDarkModeToggle } from '/@/components/Application';
import LoginForm from './LoginForm.vue';
import ForgetPasswordForm from './ForgetPasswordForm.vue';
import RegisterForm from './RegisterForm.vue';
import MobileForm from './MobileForm.vue';
import QrCodeForm from './QrCodeForm.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 { useLoginState, LoginStateEnum } from './useLogin';
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 ?? '');
const { handleBackLogin } = useLoginState();
handleBackLogin();
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-login';
@logo-prefix-cls: ~'@{namespace}-app-logo';
@countdown-prefix-cls: ~'@{namespace}-countdown-input';
@dark-bg: #293146;
html[data-theme='dark'] {
.@{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-btn:not(.ant-btn-link):not(.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: #293146;
.@{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,196 @@
<template>
<LoginFormTitle v-show="getShow" class="enter-x" />
<Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef" v-show="getShow" @keypress.enter="handleLogin">
<FormItem name="account" class="enter-x">
<Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" class="fix-auto-fill" />
</FormItem>
<FormItem name="password" class="enter-x">
<InputPassword size="large" visibilityToggle v-model:value="formData.password" :placeholder="t('sys.login.password')" />
</FormItem>
<!--验证码-->
<ARow class="enter-x">
<ACol :span="12">
<FormItem name="inputCode" class="enter-x">
<Input size="large" v-model:value="formData.inputCode" :placeholder="t('sys.login.inputCode')" style="min-width: 100px" />
</FormItem>
</ACol>
<ACol :span="8">
<FormItem :style="{ 'text-align': 'right', 'margin-left': '20px' }" class="enter-x">
<img
v-if="randCodeData.requestCodeSuccess"
style="margin-top: 2px; max-width: initial"
:src="randCodeData.randCodeImage"
@click="handleChangeCheckCode"
/>
<img v-else style="margin-top: 2px; max-width: initial" src="../../../assets/images/checkcode.png" @click="handleChangeCheckCode" />
</FormItem>
</ACol>
</ARow>
<ARow class="enter-x">
<ACol :span="12">
<FormItem>
<!-- No logic, you need to deal with it yourself -->
<Checkbox v-model:checked="rememberMe" size="small">
{{ t('sys.login.rememberMe') }}
</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('sys.login.forgetPassword') }}
</Button>
</FormItem>
</ACol>
</ARow>
<FormItem class="enter-x">
<Button type="primary" size="large" block @click="handleLogin" :loading="loading">
{{ t('sys.login.loginButton') }}
</Button>
<!-- <Button size="large" class="mt-4 enter-x" block @click="handleRegister">
{{ t('sys.login.registerButton') }}
</Button> -->
</FormItem>
<ARow class="enter-x">
<ACol :md="8" :xs="24">
<Button block @click="setLoginState(LoginStateEnum.MOBILE)">
{{ t('sys.login.mobileSignInFormTitle') }}
</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('sys.login.qrSignInFormTitle') }}
</Button>
</ACol>
<ACol :md="7" :xs="24">
<Button block @click="setLoginState(LoginStateEnum.REGISTER)">
{{ t('sys.login.registerButton') }}
</Button>
</ACol>
</ARow>
<Divider class="enter-x">{{ t('sys.login.otherSignIn') }}</Divider>
<div class="flex justify-evenly enter-x" :class="`${prefixCls}-sign-in-way`">
<a @click="onThirdLogin('github')" title="github"><GithubFilled /></a>
<a @click="onThirdLogin('wechat_enterprise')" title="企业微信"> <icon-font class="item-icon" type="icon-qiyeweixin3" /></a>
<a @click="onThirdLogin('dingtalk')" title="钉钉"><DingtalkCircleFilled /></a>
<a @click="onThirdLogin('wechat_open')" title="微信"><WechatFilled /></a>
</div>
</Form>
<!-- 第三方登录相关弹框 -->
<ThirdModal ref="thirdModalRef"></ThirdModal>
</template>
<script lang="ts" setup>
import { reactive, ref, toRaw, unref, computed, onMounted } from 'vue';
import { Checkbox, Form, Input, Row, Col, Button, Divider } from 'ant-design-vue';
import { GithubFilled, WechatFilled, DingtalkCircleFilled, createFromIconfontCN } from '@ant-design/icons-vue';
import LoginFormTitle from './LoginFormTitle.vue';
import ThirdModal from './ThirdModal.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 { getCodeInfo } from '/@/api/sys/user';
//import { onKeyStroke } from '@vueuse/core';
const ACol = Col;
const ARow = Row;
const FormItem = Form.Item;
const InputPassword = Input.Password;
const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/font_2316098_umqusozousr.js',
});
const { t } = useI18n();
const { notification, createErrorModal } = useMessage();
const { prefixCls } = useDesign('login');
const userStore = useUserStore();
const { setLoginState, getLoginState } = useLoginState();
const { getFormRules } = useFormRules();
const formRef = ref();
const thirdModalRef = ref();
const loading = ref(false);
const rememberMe = ref(false);
const formData = reactive({
account: 'admin',
password: '123456',
inputCode: '',
});
const randCodeData = reactive({
randCodeImage: '',
requestCodeSuccess: false,
checkKey: null,
});
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(
toRaw({
password: data.password,
username: data.account,
captcha: data.inputCode,
checkKey: randCodeData.checkKey,
mode: 'none', //不要默认的错误提示
})
);
if (userInfo) {
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realname}`,
duration: 3,
});
}
} catch (error) {
notification.error({
message: t('sys.api.errorTip'),
description: error.message || t('sys.api.networkExceptionMsg'),
duration: 3,
});
loading.value = false;
//update-begin-author:taoyan date:2022-5-3 for: issues/41 登录页面,当输入验证码错误时,验证码图片要刷新一下,而不是保持旧的验证码图片不变
handleChangeCheckCode();
//update-end-author:taoyan date:2022-5-3 for: issues/41 登录页面,当输入验证码错误时,验证码图片要刷新一下,而不是保持旧的验证码图片不变
}
}
function handleChangeCheckCode() {
formData.inputCode = '';
//TODO 兼容mock和接口暂时这样处理
randCodeData.checkKey = 1629428467008; //new Date().getTime();
getCodeInfo(randCodeData.checkKey).then((res) => {
randCodeData.randCodeImage = res;
randCodeData.requestCodeSuccess = true;
});
}
/**
* 第三方登录
* @param type
*/
function onThirdLogin(type) {
thirdModalRef.value.onThirdLogin(type);
}
//初始化验证码
onMounted(() => {
handleChangeCheckCode();
});
</script>

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('sys.login.forgetFormTitle'),
[LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'),
[LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'),
[LoginStateEnum.MOBILE]: t('sys.login.mobileSignInFormTitle'),
[LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle'),
};
return titleObj[unref(getLoginState)];
});
</script>

View File

@ -0,0 +1,332 @@
<template>
<BasicModal v-bind="config" @register="registerModal" :title="currTitle" wrapClassName="loginSelectModal" v-model:visible="visible">
<a-form ref="formRef" :model="formState" :rules="rules" v-bind="layout" :colon="false" class="loginSelectForm">
<!--多租户选择-->
<a-form-item v-if="isMultiTenant" name="tenantId" :validate-status="validate_status">
<!--label内容-->
<template #label>
<a-tooltip placement="topLeft">
<template #title>
<span>您隶属于多租户,请选择登录租户</span>
</template>
<a-avatar style="background-color: #87d068" :size="30"> 租户 </a-avatar>
</a-tooltip>
</template>
<template #extra v-if="validate_status == 'error'">
<span style="color: #ed6f6f">请选择登录租户</span>
</template>
<!--租户下拉内容-->
<a-select
v-model:value="formState.tenantId"
@change="handleTenantChange"
placeholder="请选择登录租户"
:class="{ 'valid-error': validate_status == 'error' }"
>
<template v-for="tenant in tenantList" :key="tenant.id">
<a-select-option :value="tenant.id">{{ tenant.name }}</a-select-option>
</template>
</a-select>
</a-form-item>
<!--多部门选择-->
<a-form-item v-if="isMultiDepart" :validate-status="validate_status1" :colon="false">
<!--label内容-->
<template #label>
<a-tooltip placement="topLeft">
<template #title>
<span>您隶属于多部门,请选择登录部门</span>
</template>
<a-avatar style="background-color: rgb(104, 208, 203)" :size="30"> 部门 </a-avatar>
</a-tooltip>
</template>
<template #extra v-if="validate_status1 == 'error'">
<span style="color: #ed6f6f">请选择登录部门</span>
</template>
<!--部门下拉内容-->
<a-select
v-model:value="formState.orgCode"
@change="handleDepartChange"
placeholder="请选择登录部门"
:class="{ 'valid-error': validate_status1 == 'error' }"
>
<template v-for="depart in departList" :key="depart.orgCode">
<a-select-option :value="depart.orgCode">{{ depart.departName }}</a-select-option>
</template>
</a-select>
</a-form-item>
</a-form>
<template #footer>
<a-button @click="handleSubmit" type="primary">确认</a-button>
</template>
</BasicModal>
</template>
<script lang="ts">
import { defineComponent, ref, computed, watch, unref, reactive, UnwrapRef } from 'vue';
import { Avatar } from 'ant-design-vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { useUserStore } from '/@/store/modules/user';
import { defHttp } from '/@/utils/http/axios';
interface FormState {
orgCode: string | undefined;
tenantId: number;
}
export default defineComponent({
name: 'loginSelect',
components: {
Avatar,
BasicModal,
},
emits: ['success', 'register'],
setup(props, { emit }) {
const userStore = useUserStore();
const { notification } = useMessage();
//租户配置
const isMultiTenant = ref(false);
const tenantList = ref([]);
const validate_status = ref('');
//部门配置
const isMultiDepart = ref(false);
const departList = ref([]);
const validate_status1 = ref('');
//弹窗显隐
const visible = ref(false);
//登录用户
const username = ref('');
//表单
const formRef = ref();
//选择的租户部门信息
const formState: UnwrapRef<FormState> = reactive({
orgCode: undefined,
tenantId: null,
});
const config = {
maskClosable: false,
closable: false,
canFullscreen: false,
width: '500px',
minHeight: 20,
maxHeight: 20,
};
//弹窗操作
const [registerModal, { closeModal }] = useModalInner();
//当前标题
const currTitle = computed(() => {
if (unref(isMultiDepart) && unref(isMultiTenant)) {
return '请选择租户和部门';
} else if (unref(isMultiDepart) && !unref(isMultiTenant)) {
return '请选择部门';
} else if (!unref(isMultiDepart) && unref(isMultiTenant)) {
return '请选择租户';
}
});
const rules = ref({
tenantId: [{ required: unref(isMultiTenant), type: 'number', message: '请选择租户', trigger: 'change' }],
orgCode: [{ required: unref(isMultiDepart), message: '请选择部门', trigger: 'change' }],
});
const layout = {
labelCol: { span: 4 },
wrapperCol: { span: 18 },
};
/**
* 处理部门情况
*/
function bizDepart(loginResult) {
//如果登录接口返回了用户上次登录租户ID则不需要重新选择
if(loginResult.userInfo?.orgCode && loginResult.userInfo?.orgCode!==''){
isMultiDepart.value = false;
return;
}
let multi_depart = loginResult.multi_depart;
//0:无部门 1:一个部门 2:多个部门
if (multi_depart == 0) {
notification.warn({
message: '提示',
description: `您尚未归属部门,请确认账号信息`,
duration: 3,
});
isMultiDepart.value = false;
} else if (multi_depart == 2) {
isMultiDepart.value = true;
departList.value = loginResult.departs;
} else {
isMultiDepart.value = false;
}
}
/**
* 处理租户情况
*/
function bizTenantList(loginResult) {
//如果登录接口返回了用户上次登录租户ID则不需要重新选择
if(loginResult.userInfo?.loginTenantId && loginResult.userInfo?.loginTenantId!==0){
isMultiTenant.value = false;
return;
}
let tenantArr = loginResult.tenantList;
if (Array.isArray(tenantArr)) {
if (tenantArr.length === 0) {
isMultiTenant.value = false;
userStore.setTenant(formState.tenantId);
} else if (tenantArr.length === 1) {
formState.tenantId = tenantArr[0].id;
isMultiTenant.value = false;
userStore.setTenant(formState.tenantId);
} else {
isMultiTenant.value = true;
tenantList.value = tenantArr;
}
}
}
/**
* 确认选中的租户和部门信息
*/
function handleSubmit() {
if (unref(isMultiTenant) && !formState.tenantId) {
validate_status.value = 'error';
return false;
}
if (unref(isMultiDepart) && !formState.orgCode) {
validate_status1.value = 'error';
return false;
}
formRef.value
.validate()
.then(() => {
departResolve()
.then(() => {
userStore.setTenant(formState.tenantId);
emit('success');
})
.catch((e) => {
console.log('登录选择出现问题', e);
})
.finally(() => {
close();
});
})
.catch((err) => {
console.log('表单校验未通过error', err);
});
}
/**
*切换选择部门
*/
function departResolve() {
return new Promise((resolve, reject) => {
if (!unref(isMultiDepart) && !unref(isMultiTenant)) {
resolve();
} else {
let params = { orgCode: formState.orgCode,loginTenantId: formState.tenantId, username: unref(username) };
defHttp.put({ url: '/sys/selectDepart', params }).then((res) => {
if (res.userInfo) {
userStore.setUserInfo(res.userInfo);
resolve();
} else {
requestFailed(res);
userStore.logout();
reject();
}
});
}
});
}
/**
* 请求失败处理
*/
function requestFailed(err) {
notification.error({
message: '登录失败',
description: ((err.response || {}).data || {}).message || err.message || '请求出现错误,请稍后再试',
duration: 4,
});
}
/**
* 关闭model
*/
function close() {
closeModal();
reset();
}
/**
* 弹窗打开前处理
*/
async function show(loginResult) {
if (loginResult) {
username.value = userStore.username;
await reset();
await bizDepart(loginResult);
await bizTenantList(loginResult);
if (!unref(isMultiDepart) && !unref(isMultiTenant)) {
emit('success', userStore.getUserInfo);
} else {
visible.value = true;
}
}
//登录弹窗完成后将登录的标识设置成false
loginResult.isLogin = false;
userStore.setLoginInfo(loginResult);
}
/**
*重置数据
*/
function reset() {
tenantList.value = [];
validate_status.value = '';
departList.value = [];
validate_status1.value = '';
}
function handleTenantChange(e) {
validate_status.value = '';
}
function handleDepartChange(e) {
validate_status1.value = '';
}
return {
registerModal,
visible,
tenantList,
isMultiTenant,
validate_status,
isMultiDepart,
departList,
validate_status1,
formState,
rules,
layout,
formRef,
currTitle,
config,
handleTenantChange,
handleDepartChange,
show,
handleSubmit,
};
},
});
</script>
<style lang="less" scoped>
.loginSelectForm {
margin-bottom: -20px;
}
.loginSelectModal {
top: 10px;
}
</style>

View File

@ -0,0 +1,89 @@
<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('sys.login.mobile')" 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('sys.login.smsCode')"
:sendCodeApi="sendCodeApi"
/>
</FormItem>
<FormItem class="enter-x">
<Button type="primary" size="large" block @click="handleLogin" :loading="loading">
{{ t('sys.login.loginButton') }}
</Button>
<Button size="large" block class="mt-4" @click="handleBackLogin">
{{ t('sys.login.backSignIn') }}
</Button>
</FormItem>
</Form>
</template>
</template>
<script lang="ts" setup>
import { reactive, ref, computed, unref, toRaw } 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 { useMessage } from '/@/hooks/web/useMessage';
import { useLoginState, useFormRules, useFormValid, LoginStateEnum, SmsEnum } from './useLogin';
import { useUserStore } from '/@/store/modules/user';
import { getCaptcha } from '/@/api/sys/user';
const FormItem = Form.Item;
const { t } = useI18n();
const { handleBackLogin, getLoginState } = useLoginState();
const { getFormRules } = useFormRules();
const { notification, createErrorModal } = useMessage();
const userStore = useUserStore();
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;
try {
loading.value = true;
const userInfo = await userStore.phoneLogin(
toRaw({
mobile: data.mobile,
captcha: data.sms,
mode: 'none', //不要默认的错误提示
})
);
if (userInfo) {
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realname}`,
duration: 3,
});
}
} catch (error) {
notification.error({
message: t('sys.api.errorTip'),
description: error.message || t('sys.api.networkExceptionMsg'),
duration: 3,
});
} finally {
loading.value = false;
}
}
//倒计时执行前的函数
function sendCodeApi() {
return getCaptcha({ mobile: formData.mobile, smsmode: SmsEnum.FORGET_PASSWORD });
}
</script>

View File

@ -0,0 +1,87 @@
<template>
<div> </div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { isOAuth2AppEnv, sysOAuth2Login } from '/@/views/sys/login/useLogin';
import { useRouter } from 'vue-router';
import { PageEnum } from '/@/enums/pageEnum';
import { router } from '/@/router';
import { useUserStore } from '/@/store/modules/user';
import { useMessage } from '/@/hooks/web/useMessage';
import { useI18n } from '/@/hooks/web/useI18n';
import {getTenantId} from "/@/utils/auth";
const isOAuth = ref<boolean>(isOAuth2AppEnv());
const env = ref<any>({ thirdApp: false, wxWork: false, dingtalk: false });
const { currentRoute } = useRouter();
const route = currentRoute.value;
if (!isOAuth2AppEnv()) {
router.replace({ path: PageEnum.BASE_LOGIN, query: route.query });
}
if (isOAuth.value) {
checkEnv();
}
/**
* 检测当前的环境
*/
function checkEnv() {
// 判断当时是否是企业微信环境
if (/wxwork/i.test(navigator.userAgent)) {
env.value.thirdApp = true;
env.value.wxWork = true;
}
// 判断当时是否是钉钉环境
if (/dingtalk/i.test(navigator.userAgent)) {
env.value.thirdApp = true;
env.value.dingtalk = true;
}
doOAuth2Login();
}
/**
* 进行OAuth2登录操作
*/
function doOAuth2Login() {
if (env.value.thirdApp) {
// 判断是否携带了Token是就说明登录成功
if (route.query.oauth2LoginToken) {
let token = route.query.oauth2LoginToken;
//执行登录操作
thirdLogin({ token, thirdType: route.query.thirdType,tenantId: getTenantId });
} else if (env.value.wxWork) {
sysOAuth2Login('wechat_enterprise');
} else if (env.value.dingtalk) {
sysOAuth2Login('dingtalk');
}
}
}
/**
* 第三方登录
* @param params
*/
function thirdLogin(params) {
const userStore = useUserStore();
const { notification } = useMessage();
const { t } = useI18n();
userStore.ThirdLogin(params).then((res) => {
if (res && res.userInfo) {
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${res.userInfo.realname}`,
duration: 3,
});
} else {
notification.error({
message: '登录失败',
description: ((res.response || {}).data || {}).message || res.message || '请求出现错误请稍后再试',
duration: 4,
});
}
});
}
</script>

View File

@ -0,0 +1,83 @@
<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">{{ scanContent }}</Divider>
<Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
{{ t('sys.login.backSignIn') }}
</Button>
</div>
</template>
</template>
<script lang="ts" setup>
import { computed, onMounted, unref, ref, watch } from 'vue';
import LoginFormTitle from './LoginFormTitle.vue';
import { Button, Divider } from 'ant-design-vue';
import { QrCode } from '/@/components/Qrcode/index';
import { useUserStore } from '/@/store/modules/user';
import { useI18n } from '/@/hooks/web/useI18n';
import { useLoginState, LoginStateEnum } from './useLogin';
import { getLoginQrcode, getQrcodeToken } from '/@/api/sys/user';
const qrCodeUrl = ref('');
let timer: IntervalHandle;
const { t } = useI18n();
const userStore = useUserStore();
const { handleBackLogin, getLoginState } = useLoginState();
const state = ref('0');
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.QR_CODE);
const scanContent = computed(() => {
return unref(state) === '0' ? t('sys.login.scanSign') : t('sys.login.scanSuccess');
});
//加载二维码信息
function loadQrCode() {
state.value = '0';
getLoginQrcode().then((res) => {
qrCodeUrl.value = res.qrcodeId;
if (res.qrcodeId) {
openTimer(res.qrcodeId);
}
});
}
//监控扫码状态
function watchQrcodeToken(qrcodeId) {
getQrcodeToken({ qrcodeId: qrcodeId }).then((res) => {
let token = res.token;
if (token == '-2') {
//二维码过期重新获取
loadQrCode();
clearInterval(timer);
}
//扫码成功
if (res.success) {
state.value = '2';
clearInterval(timer);
setTimeout(() => {
userStore.qrCodeLogin(token);
}, 500);
}
});
}
/** 开启定时器 */
function openTimer(qrcodeId) {
watchQrcodeToken(qrcodeId);
closeTimer();
timer = setInterval(() => {
watchQrcodeToken(qrcodeId);
}, 1500);
}
/** 关闭定时器 */
function closeTimer() {
if (timer) clearInterval(timer);
}
watch(getShow, (v) => {
if (v) {
loadQrCode();
} else {
closeTimer();
}
});
</script>

View File

@ -0,0 +1,114 @@
<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('sys.login.userName')" />
</FormItem>
<FormItem name="mobile" class="enter-x">
<Input size="large" v-model:value="formData.mobile" :placeholder="t('sys.login.mobile')" 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('sys.login.smsCode')"
:sendCodeApi="sendCodeApi"
/>
</FormItem>
<FormItem name="password" class="enter-x">
<StrengthMeter size="large" v-model:value="formData.password" :placeholder="t('sys.login.password')" />
</FormItem>
<FormItem name="confirmPassword" class="enter-x">
<InputPassword size="large" visibilityToggle v-model:value="formData.confirmPassword" :placeholder="t('sys.login.confirmPassword')" />
</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('sys.login.policy') }}
</Checkbox>
</FormItem>
<Button type="primary" class="enter-x" size="large" block @click="handleRegister" :loading="loading">
{{ t('sys.login.registerButton') }}
</Button>
<Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
{{ t('sys.login.backSignIn') }}
</Button>
</Form>
</template>
</template>
<script lang="ts" setup>
import { reactive, ref, unref, computed, toRaw } 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 { useMessage } from '/@/hooks/web/useMessage';
import { useLoginState, useFormRules, useFormValid, LoginStateEnum, SmsEnum } from './useLogin';
import { register, getCaptcha } from '/@/api/sys/user';
const FormItem = Form.Item;
const InputPassword = Input.Password;
const { t } = useI18n();
const { handleBackLogin, getLoginState } = useLoginState();
const { notification, createErrorModal } = useMessage();
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;
try {
loading.value = true;
const resultInfo = await register(
toRaw({
username: data.account,
password: data.password,
phone: data.mobile,
smscode: data.sms,
})
);
if (resultInfo && resultInfo.data.success) {
notification.success({
description: resultInfo.data.message || t('sys.api.registerMsg'),
duration: 3,
});
handleBackLogin();
} else {
notification.warning({
message: t('sys.api.errorTip'),
description: resultInfo.data.message || t('sys.api.networkExceptionMsg'),
duration: 3,
});
}
} catch (error) {
notification.error({
message: t('sys.api.errorTip'),
description: error.message || t('sys.api.networkExceptionMsg'),
duration: 3,
});
} finally {
loading.value = false;
}
}
//发送验证码的函数
function sendCodeApi() {
return getCaptcha({ mobile: formData.mobile, smsmode: SmsEnum.REGISTER });
}
</script>

View File

@ -0,0 +1,53 @@
<template>
<transition>
<div :class="prefixCls">
<Login sessionTimeout />
</div>
</transition>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref } from 'vue';
import Login from './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,64 @@
<template>
<!-- 第三方登录绑定账号密码输入弹框 -->
<a-modal title="请输入密码" v-model:open="thirdPasswordShow" @ok="thirdLoginCheckPassword" @cancel="thirdLoginNoPassword">
<a-input-password placeholder="请输入密码" v-model:value="thirdLoginPassword" style="margin: 15px; width: 80%" />
</a-modal>
<!-- 第三方登录提示是否绑定账号弹框 -->
<a-modal :footer="null" :closable="false" v-model:open="thirdConfirmShow" :class="'ant-modal-confirm'">
<div class="ant-modal-confirm-body-wrapper">
<div class="ant-modal-confirm-body">
<QuestionCircleFilled style="color: #faad14" />
<span class="ant-modal-confirm-title">提示</span>
<div class="ant-modal-confirm-content"> 已有同名账号存在,请确认是否绑定该账号? </div>
</div>
<div class="ant-modal-confirm-btns">
<a-button @click="thirdLoginUserCreate" :loading="thirdCreateUserLoding">创建新账号</a-button>
<a-button @click="thirdLoginUserBind" type="primary">确认绑定</a-button>
</div>
</div>
</a-modal>
<!-- 第三方登录绑定手机号 -->
<a-modal title="绑定手机号" v-model:open="bindingPhoneModal" :maskClosable="false">
<Form class="p-4 enter-x" style="margin: 15px 10px">
<FormItem class="enter-x">
<a-input size="large" placeholder="请输入手机号" v-model:value="thirdPhone" class="fix-auto-fill">
<template #prefix>
<Icon icon="ant-design:mobile-outlined" :style="{ color: 'rgba(0,0,0,.25)' }"></Icon>
</template>
</a-input>
</FormItem>
<FormItem name="sms" class="enter-x">
<CountdownInput size="large" class="fix-auto-fill" v-model:value="thirdCaptcha" placeholder="请输入验证码" :sendCodeApi="sendCodeApi">
<template #prefix>
<Icon icon="ant-design:mail-outlined" :style="{ color: 'rgba(0,0,0,.25)' }"></Icon>
</template>
</CountdownInput>
</FormItem>
</Form>
<template #footer>
<a-button type="primary" @click="thirdHandleOk">确定</a-button>
</template>
</a-modal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { Form, Input } from 'ant-design-vue';
import { CountdownInput } from '/@/components/CountDown';
import { useThirdLogin } from '/@/hooks/system/useThirdLogin';
import { QuestionCircleFilled } from '@ant-design/icons-vue';
const FormItem = Form.Item;
const InputPassword = Input.Password;
export default defineComponent({
name: 'ThirdModal',
components: { FormItem, Form, InputPassword, CountdownInput, QuestionCircleFilled },
setup() {
return {
...useThirdLogin(),
};
},
});
</script>

View File

@ -0,0 +1,218 @@
<template>
<div class="app-loading">
<div class="app-loading-wrap">
<img src="/resource/img/logo.png" class="app-loading-logo" alt="Logo">
<div class="app-loading-dots">
<span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
</div>
<div class="app-loading-title">JeecgBoot 企业级低代码平台</div>
</div>
</div>
</template>
<script lang="ts">
/**
* 地址中携带token跳转至此页面进行登录操作
*/
import { useRoute, useRouter } from 'vue-router';
import { useMessage } from '/@/hooks/web/useMessage';
import { useUserStore } from '/@/store/modules/user';
import { useI18n } from '/@/hooks/web/useI18n';
export default {
name: "TokenLogin",
setup(){
const route = useRoute();
let router = useRouter();
const {createMessage, notification} = useMessage()
const {t} = useI18n();
const routeQuery:any = route.query;
if(!routeQuery){
createMessage.warning('参数无效')
}
const token = routeQuery['loginToken'];
if(!token){
createMessage.warning('token无效')
}
const userStore = useUserStore();
userStore.ThirdLogin({ token, thirdType:'email', goHome: false }).then(res => {
console.log("res====>doThirdLogin",res)
if(res && res.userInfo){
requestSuccess(res)
}else{
requestFailed(res)
}
});
function requestFailed (err) {
notification.error({
message: '登录失败',
description: ((err.response || {}).data || {}).message || err.message || "请求出现错误请稍后再试",
duration: 4,
});
}
function requestSuccess(res){
let info = routeQuery.info;
if(info){
let query = JSON.parse(info);
//update-begin-author:taoyan date:2023-4-27 for: QQYUN-4882【简流】节点消息通知 邮箱 点击办理跳到了应用首页
let path = '';
if(query.isLowApp === 1){
path = '/myapps/personalOffice/myTodo'
}else{
let taskId = query.taskId;
path = '/task/handle/' + taskId
}
//update-end-author:taoyan date:2023-4-27 for: QQYUN-4882【简流】节点消息通知 邮箱 点击办理跳到了应用首页
router.replace({ path, query });
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${res.userInfo.realname}`,
duration: 3,
});
}else{
notification.error({
message: '参数失效',
description: "页面跳转参数丢失请查看日志",
duration: 4,
});
}
}
}
}
</script>
<style scoped>
html[data-theme='dark'] .app-loading {
background-color: #2c344a;
}
html[data-theme='dark'] .app-loading .app-loading-title {
color: rgba(255, 255, 255, 0.85);
}
.app-loading {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
flex-direction: column;
background-color: #f4f7f9;
}
.app-loading .app-loading-wrap {
position: absolute;
top: 50%;
left: 50%;
display: flex;
-webkit-transform: translate3d(-50%, -50%, 0);
transform: translate3d(-50%, -50%, 0);
justify-content: center;
align-items: center;
flex-direction: column;
}
.app-loading .dots {
display: flex;
padding: 98px;
justify-content: center;
align-items: center;
}
.app-loading .app-loading-title {
display: flex;
margin-top: 30px;
font-size: 30px;
color: rgba(0, 0, 0, 0.85);
justify-content: center;
align-items: center;
}
.app-loading .app-loading-logo {
display: block;
width: 90px;
margin: 0 auto;
margin-bottom: 20px;
}
.dot {
position: relative;
display: inline-block;
width: 48px;
height: 48px;
margin-top: 30px;
font-size: 32px;
transform: rotate(45deg);
box-sizing: border-box;
animation: antRotate 1.2s infinite linear;
}
.dot i {
position: absolute;
display: block;
width: 20px;
height: 20px;
background-color: #0065cc;
border-radius: 100%;
opacity: 0.3;
transform: scale(0.75);
animation: antSpinMove 1s infinite linear alternate;
transform-origin: 50% 50%;
}
.dot i:nth-child(1) {
top: 0;
left: 0;
}
.dot i:nth-child(2) {
top: 0;
right: 0;
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
.dot i:nth-child(3) {
right: 0;
bottom: 0;
-webkit-animation-delay: 0.8s;
animation-delay: 0.8s;
}
.dot i:nth-child(4) {
bottom: 0;
left: 0;
-webkit-animation-delay: 1.2s;
animation-delay: 1.2s;
}
@keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@-webkit-keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@keyframes antSpinMove {
to {
opacity: 1;
}
}
@-webkit-keyframes antSpinMove {
to {
opacity: 1;
}
}
</style>

View File

@ -0,0 +1,193 @@
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';
import { checkOnlyUser } from '/@/api/sys/user';
import { defHttp } from '/@/utils/http/axios';
import { OAUTH2_THIRD_LOGIN_TENANT_ID } from "/@/enums/cacheEnum";
import { getAuthCache } from "/@/utils/auth";
export enum LoginStateEnum {
LOGIN,
REGISTER,
RESET_PASSWORD,
MOBILE,
QR_CODE,
}
export enum SmsEnum {
LOGIN = '0',
REGISTER = '1',
FORGET_PASSWORD = '2',
}
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('sys.login.accountPlaceholder')));
const getPasswordFormRule = computed(() => createRule(t('sys.login.passwordPlaceholder')));
const getSmsFormRule = computed(() => createRule(t('sys.login.smsPlaceholder')));
const getMobileFormRule = computed(() => createRule(t('sys.login.mobilePlaceholder')));
const getRegisterAccountRule = computed(() => createRegisterAccountRule('account'));
const getRegisterMobileRule = computed(() => createRegisterAccountRule('mobile'));
const validatePolicy = async (_: RuleObject, value: boolean) => {
return !value ? Promise.reject(t('sys.login.policyPlaceholder')) : Promise.resolve();
};
const validateConfirmPassword = (password: string) => {
return async (_: RuleObject, value: string) => {
if (!value) {
return Promise.reject(t('sys.login.passwordPlaceholder'));
}
if (value !== password) {
return Promise.reject(t('sys.login.diffPwd'));
}
return Promise.resolve();
};
};
const getFormRules = computed((): { [k: string]: ValidationRule | ValidationRule[] } => {
const accountFormRule = unref(getAccountFormRule);
const passwordFormRule = unref(getPasswordFormRule);
const smsFormRule = unref(getSmsFormRule);
const mobileFormRule = unref(getMobileFormRule);
const registerAccountRule = unref(getRegisterAccountRule);
const registerMobileRule = unref(getRegisterMobileRule);
const mobileRule = {
sms: smsFormRule,
mobile: mobileFormRule,
};
switch (unref(currentState)) {
// register form rules
case LoginStateEnum.REGISTER:
return {
account: registerAccountRule,
password: passwordFormRule,
mobile: registerMobileRule,
sms: smsFormRule,
confirmPassword: [{ validator: validateConfirmPassword(formData?.password), trigger: 'change' }],
policy: [{ validator: validatePolicy, trigger: 'change' }],
};
// reset password form rules
case LoginStateEnum.RESET_PASSWORD:
return {
username: accountFormRule,
confirmPassword: [{ validator: validateConfirmPassword(formData?.password), trigger: 'change' }],
...mobileRule,
};
// mobile form rules
case LoginStateEnum.MOBILE:
return mobileRule;
// login form rules
default:
return {
account: accountFormRule,
password: passwordFormRule,
};
}
});
return { getFormRules };
}
function createRule(message: string) {
return [
{
required: true,
message,
trigger: 'change',
},
];
}
function createRegisterAccountRule(type) {
return [
{
validator: type == 'account' ? checkUsername : checkPhone,
trigger: 'change',
},
];
}
function checkUsername(rule, value, callback) {
const { t } = useI18n();
if (!value) {
return Promise.reject(t('sys.login.accountPlaceholder'));
} else {
return new Promise((resolve, reject) => {
checkOnlyUser({ username: value }).then((res) => {
res.success ? resolve() : reject('用户名已存在!');
});
});
}
}
async function checkPhone(rule, value, callback) {
const { t } = useI18n();
var reg = /^1[3456789]\d{9}$/;
if (!reg.test(value)) {
return Promise.reject(new Error('请输入正确手机号'));
} else {
return new Promise((resolve, reject) => {
checkOnlyUser({ phone: value }).then((res) => {
res.success ? resolve() : reject('手机号已存在!');
});
});
}
}
//update-begin---author:wangshuai ---date:20220629 for[issues/I5BG1I]vue3不支持auth2登录------------
/**
* 判断是否是OAuth2APP环境
*/
export function isOAuth2AppEnv() {
return /wxwork|dingtalk/i.test(navigator.userAgent);
}
/**
* 后台构造oauth2登录地址
* @param source
* @param tenantId
*/
export function sysOAuth2Login(source) {
let url = `${window._CONFIG['domianURL']}/sys/thirdLogin/oauth2/${source}/login`;
url += `?state=${encodeURIComponent(window.location.origin)}`;
//update-begin---author:wangshuai ---date:20230224 for[QQYUN-3440]新建企业微信和钉钉配置表,通过租户模式隔离------------
let tenantId = getAuthCache(OAUTH2_THIRD_LOGIN_TENANT_ID);
if(tenantId){
url += `&tenantId=${tenantId}`;
}
//update-end---author:wangshuai ---date:20230224 for[QQYUN-3440]新建企业微信和钉钉配置表,通过租户模式隔离------------
window.location.href = url;
}
//update-end---author:wangshuai ---date:20220629 for[issues/I5BG1I]vue3不支持auth2登录------------

View File

@ -0,0 +1,30 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import { unref } from 'vue';
import { useRouter } from 'vue-router';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
// update-begin--author:liaozhiyang---date:20231123---for【QQYUN-7099】动态路由匹配右键重新加载404
const { currentRoute, replace } = useRouter();
const { params, query } = unref(currentRoute);
const { path } = params;
const tabStore = useMultipleTabStore();
const redirectPageParam = tabStore.redirectPageParam;
const _path = Array.isArray(path) ? path.join('/') : path;
if (redirectPageParam) {
if (redirectPageParam.redirect_type === 'name') {
replace({
name: redirectPageParam.name,
query: redirectPageParam.query,
params: redirectPageParam.params,
});
} else {
replace({
path: _path.startsWith('/') ? _path : '/' + _path,
query,
});
}
}
// update-end--author:liaozhiyang---date:20231123---for【QQYUN-7099】动态路由匹配右键重新加载404
</script>