mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-01-02 19:15:26 +08:00
前端和后端源码,合并到一个git仓库中,方便用户下载,避免前后端不匹配的问题
This commit is contained in:
100
jeecgboot-vue3/src/views/sys/about/index.vue
Normal file
100
jeecgboot-vue3/src/views/sys/about/index.vue
Normal 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.x,SpringCloud,Ant Design&Vue,Mybatis-plus,Shiro,JWT,支持微服务。强大的代码生成器让前后端代码一键生成,实现低代码开发! 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>
|
||||
27
jeecgboot-vue3/src/views/sys/error-log/DetailModal.vue
Normal file
27
jeecgboot-vue3/src/views/sys/error-log/DetailModal.vue
Normal 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>
|
||||
67
jeecgboot-vue3/src/views/sys/error-log/data.tsx
Normal file
67
jeecgboot-vue3/src/views/sys/error-log/data.tsx
Normal 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,
|
||||
};
|
||||
});
|
||||
}
|
||||
88
jeecgboot-vue3/src/views/sys/error-log/index.vue
Normal file
88
jeecgboot-vue3/src/views/sys/error-log/index.vue
Normal 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>
|
||||
143
jeecgboot-vue3/src/views/sys/exception/Exception.vue
Normal file
143
jeecgboot-vue3/src/views/sys/exception/Exception.vue
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
5
jeecgboot-vue3/src/views/sys/exception/index.ts
Normal file
5
jeecgboot-vue3/src/views/sys/exception/index.ts
Normal 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';
|
||||
96
jeecgboot-vue3/src/views/sys/forget-password/step1.vue
Normal file
96
jeecgboot-vue3/src/views/sys/forget-password/step1.vue
Normal 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>
|
||||
103
jeecgboot-vue3/src/views/sys/forget-password/step2.vue
Normal file
103
jeecgboot-vue3/src/views/sys/forget-password/step2.vue
Normal 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>
|
||||
71
jeecgboot-vue3/src/views/sys/forget-password/step3.vue
Normal file
71
jeecgboot-vue3/src/views/sys/forget-password/step3.vue
Normal 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>
|
||||
9
jeecgboot-vue3/src/views/sys/iframe/FrameBlank.vue
Normal file
9
jeecgboot-vue3/src/views/sys/iframe/FrameBlank.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
export default defineComponent({
|
||||
name: 'FrameBlank',
|
||||
});
|
||||
</script>
|
||||
85
jeecgboot-vue3/src/views/sys/iframe/index.vue
Normal file
85
jeecgboot-vue3/src/views/sys/iframe/index.vue
Normal 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>
|
||||
241
jeecgboot-vue3/src/views/sys/lock/LockPage.vue
Normal file
241
jeecgboot-vue3/src/views/sys/lock/LockPage.vue
Normal 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>
|
||||
13
jeecgboot-vue3/src/views/sys/lock/index.vue
Normal file
13
jeecgboot-vue3/src/views/sys/lock/index.vue
Normal 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>
|
||||
60
jeecgboot-vue3/src/views/sys/lock/useNow.ts
Normal file
60
jeecgboot-vue3/src/views/sys/lock/useNow.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
68
jeecgboot-vue3/src/views/sys/login/ForgetPasswordForm.vue
Normal file
68
jeecgboot-vue3/src/views/sys/login/ForgetPasswordForm.vue
Normal 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>
|
||||
208
jeecgboot-vue3/src/views/sys/login/Login.vue
Normal file
208
jeecgboot-vue3/src/views/sys/login/Login.vue
Normal 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>
|
||||
196
jeecgboot-vue3/src/views/sys/login/LoginForm.vue
Normal file
196
jeecgboot-vue3/src/views/sys/login/LoginForm.vue
Normal 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>
|
||||
25
jeecgboot-vue3/src/views/sys/login/LoginFormTitle.vue
Normal file
25
jeecgboot-vue3/src/views/sys/login/LoginFormTitle.vue
Normal 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>
|
||||
332
jeecgboot-vue3/src/views/sys/login/LoginSelect.vue
Normal file
332
jeecgboot-vue3/src/views/sys/login/LoginSelect.vue
Normal 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>
|
||||
89
jeecgboot-vue3/src/views/sys/login/MobileForm.vue
Normal file
89
jeecgboot-vue3/src/views/sys/login/MobileForm.vue
Normal 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>
|
||||
87
jeecgboot-vue3/src/views/sys/login/OAuth2Login.vue
Normal file
87
jeecgboot-vue3/src/views/sys/login/OAuth2Login.vue
Normal 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>
|
||||
83
jeecgboot-vue3/src/views/sys/login/QrCodeForm.vue
Normal file
83
jeecgboot-vue3/src/views/sys/login/QrCodeForm.vue
Normal 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>
|
||||
114
jeecgboot-vue3/src/views/sys/login/RegisterForm.vue
Normal file
114
jeecgboot-vue3/src/views/sys/login/RegisterForm.vue
Normal 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>
|
||||
53
jeecgboot-vue3/src/views/sys/login/SessionTimeoutLogin.vue
Normal file
53
jeecgboot-vue3/src/views/sys/login/SessionTimeoutLogin.vue
Normal 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>
|
||||
64
jeecgboot-vue3/src/views/sys/login/ThirdModal.vue
Normal file
64
jeecgboot-vue3/src/views/sys/login/ThirdModal.vue
Normal 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>
|
||||
218
jeecgboot-vue3/src/views/sys/login/TokenLoginPage.vue
Normal file
218
jeecgboot-vue3/src/views/sys/login/TokenLoginPage.vue
Normal 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>
|
||||
193
jeecgboot-vue3/src/views/sys/login/useLogin.ts
Normal file
193
jeecgboot-vue3/src/views/sys/login/useLogin.ts
Normal 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登录------------
|
||||
30
jeecgboot-vue3/src/views/sys/redirect/index.vue
Normal file
30
jeecgboot-vue3/src/views/sys/redirect/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user