mirror of
https://github.com/fumiama/paper-manager.git
synced 2026-06-11 11:40:23 +08:00
add frontend/vben from vben-admin-thin
This commit is contained in:
98
frontend/vben/src/views/sys/about/index.vue
Normal file
98
frontend/vben/src/views/sys/about/index.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<PageWrapper title="关于">
|
||||
<template #headerContent>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="flex-1">
|
||||
<a :href="GITHUB_URL" target="_blank">{{ name }}</a>
|
||||
是一个基于Vue3.0、Vite、 Ant-Design-Vue 、TypeScript
|
||||
的后台解决方案,目标是为中大型项目开发,提供现成的开箱解决方案及丰富的示例,原则上不会限制任何代码用于商用。
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<Description @register="infoRegister" class="enter-y" />
|
||||
<Description @register="register" class="my-4 enter-y" />
|
||||
<Description @register="registerDev" class="enter-y" />
|
||||
</PageWrapper>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { h } from 'vue'
|
||||
import { Tag } from 'ant-design-vue'
|
||||
import { PageWrapper } from '/@/components/Page'
|
||||
import { Description, DescItem, useDescription } from '/@/components/Description/index'
|
||||
import { GITHUB_URL, SITE_URL, DOC_URL } from '/@/settings/siteSetting'
|
||||
|
||||
const { pkg, lastBuildTime } = __APP_INFO__
|
||||
|
||||
const { dependencies, devDependencies, name, version } = pkg
|
||||
|
||||
const schema: DescItem[] = []
|
||||
const devSchema: DescItem[] = []
|
||||
|
||||
const commonTagRender = (color: string) => (curVal) => h(Tag, { color }, () => curVal)
|
||||
const commonLinkRender = (text: string) => (href) => h('a', { href, target: '_blank' }, text)
|
||||
|
||||
const infoSchema: DescItem[] = [
|
||||
{
|
||||
label: '版本',
|
||||
field: 'version',
|
||||
render: commonTagRender('blue'),
|
||||
},
|
||||
{
|
||||
label: '最后编译时间',
|
||||
field: 'lastBuildTime',
|
||||
render: commonTagRender('blue'),
|
||||
},
|
||||
{
|
||||
label: '文档地址',
|
||||
field: 'doc',
|
||||
render: commonLinkRender('文档地址'),
|
||||
},
|
||||
{
|
||||
label: '预览地址',
|
||||
field: 'preview',
|
||||
render: commonLinkRender('预览地址'),
|
||||
},
|
||||
{
|
||||
label: 'Github',
|
||||
field: 'github',
|
||||
render: commonLinkRender('Github'),
|
||||
},
|
||||
]
|
||||
|
||||
const infoData = {
|
||||
version,
|
||||
lastBuildTime,
|
||||
doc: DOC_URL,
|
||||
preview: SITE_URL,
|
||||
github: GITHUB_URL,
|
||||
}
|
||||
|
||||
Object.keys(dependencies).forEach((key) => {
|
||||
schema.push({ field: key, label: key })
|
||||
})
|
||||
|
||||
Object.keys(devDependencies).forEach((key) => {
|
||||
devSchema.push({ field: key, label: key })
|
||||
})
|
||||
|
||||
const [register] = useDescription({
|
||||
title: '生产环境依赖',
|
||||
data: dependencies,
|
||||
schema: schema,
|
||||
column: 3,
|
||||
})
|
||||
|
||||
const [registerDev] = useDescription({
|
||||
title: '开发环境依赖',
|
||||
data: devDependencies,
|
||||
schema: devSchema,
|
||||
column: 3,
|
||||
})
|
||||
|
||||
const [infoRegister] = useDescription({
|
||||
title: '项目信息',
|
||||
data: infoData,
|
||||
schema: infoSchema,
|
||||
column: 2,
|
||||
})
|
||||
</script>
|
||||
148
frontend/vben/src/views/sys/exception/Exception.vue
Normal file
148
frontend/vben/src/views/sys/exception/Exception.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<script lang="tsx">
|
||||
import type { PropType } from 'vue'
|
||||
import { Result, Button } from 'ant-design-vue'
|
||||
import { defineComponent, ref, computed, unref } from 'vue'
|
||||
import { ExceptionEnum } from '/@/enums/exceptionEnum'
|
||||
import notDataSvg from '/@/assets/svg/no-data.svg'
|
||||
import netWorkSvg from '/@/assets/svg/net-error.svg'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useDesign } from '/@/hooks/web/useDesign'
|
||||
import { useI18n } from '/@/hooks/web/useI18n'
|
||||
import { useGo, useRedo } from '/@/hooks/web/usePage'
|
||||
import { PageEnum } from '/@/enums/pageEnum'
|
||||
|
||||
interface MapValue {
|
||||
title: string
|
||||
subTitle: string
|
||||
btnText?: string
|
||||
icon?: string
|
||||
handler?: Fn
|
||||
status?: string
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ErrorPage',
|
||||
props: {
|
||||
// 状态码
|
||||
status: {
|
||||
type: Number as PropType<number>,
|
||||
default: ExceptionEnum.PAGE_NOT_FOUND,
|
||||
},
|
||||
|
||||
title: {
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
|
||||
subTitle: {
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
|
||||
full: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const statusMapRef = ref(new Map<string | number, MapValue>())
|
||||
|
||||
const { query } = useRoute()
|
||||
const go = useGo()
|
||||
const redo = useRedo()
|
||||
const { t } = useI18n()
|
||||
const { prefixCls } = useDesign('app-exception-page')
|
||||
|
||||
const getStatus = computed(() => {
|
||||
const { status: routeStatus } = query
|
||||
const { status } = props
|
||||
return Number(routeStatus) || status
|
||||
})
|
||||
|
||||
const getMapValue = computed((): MapValue => {
|
||||
return unref(statusMapRef).get(unref(getStatus)) as MapValue
|
||||
})
|
||||
|
||||
const backLoginI18n = t('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>
|
||||
1
frontend/vben/src/views/sys/exception/index.ts
Normal file
1
frontend/vben/src/views/sys/exception/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Exception } from './Exception.vue'
|
||||
64
frontend/vben/src/views/sys/login/ForgetPasswordForm.vue
Normal file
64
frontend/vben/src/views/sys/login/ForgetPasswordForm.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<template v-if="getShow">
|
||||
<LoginFormTitle class="enter-x" />
|
||||
<Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef">
|
||||
<FormItem name="account" class="enter-x">
|
||||
<Input
|
||||
size="large"
|
||||
v-model:value="formData.account"
|
||||
:placeholder="t('sys.login.userName')"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<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')"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem class="enter-x">
|
||||
<Button type="primary" size="large" block @click="handleReset" :loading="loading">
|
||||
{{ t('common.resetText') }}
|
||||
</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 } from 'vue'
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import { Form, Input, Button } from 'ant-design-vue'
|
||||
import { CountdownInput } from '/@/components/CountDown'
|
||||
import { useI18n } from '/@/hooks/web/useI18n'
|
||||
import { useLoginState, useFormRules, LoginStateEnum } from './useLogin'
|
||||
|
||||
const FormItem = Form.Item
|
||||
const { t } = useI18n()
|
||||
const { handleBackLogin, getLoginState } = useLoginState()
|
||||
const { getFormRules } = useFormRules()
|
||||
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
account: '',
|
||||
mobile: '',
|
||||
sms: '',
|
||||
})
|
||||
|
||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD)
|
||||
|
||||
async function handleReset() {
|
||||
const form = unref(formRef)
|
||||
if (!form) return
|
||||
await form.resetFields()
|
||||
}
|
||||
</script>
|
||||
215
frontend/vben/src/views/sys/login/Login.vue
Normal file
215
frontend/vben/src/views/sys/login/Login.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div :class="prefixCls" class="relative w-full h-full px-4">
|
||||
<div class="flex items-center absolute right-4 top-4">
|
||||
<AppDarkModeToggle class="enter-x mr-2" v-if="!sessionTimeout" />
|
||||
<AppLocalePicker
|
||||
class="text-white enter-x xl:text-gray-600"
|
||||
:show-text="false"
|
||||
v-if="!sessionTimeout && showLocale"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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 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 />
|
||||
</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 { useGlobSetting } from '/@/hooks/setting'
|
||||
import { useI18n } from '/@/hooks/web/useI18n'
|
||||
import { useDesign } from '/@/hooks/web/useDesign'
|
||||
import { useLocaleStore } from '/@/store/modules/locale'
|
||||
|
||||
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 ?? '')
|
||||
</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>
|
||||
137
frontend/vben/src/views/sys/login/LoginForm.vue
Normal file
137
frontend/vben/src/views/sys/login/LoginForm.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<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>
|
||||
<!-- 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="6" :xs="24">
|
||||
<Button block @click="setLoginState(LoginStateEnum.REGISTER)">
|
||||
{{ t('sys.login.registerButton') }}
|
||||
</Button>
|
||||
</ACol>
|
||||
</ARow>
|
||||
</Form>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, unref, computed } from 'vue'
|
||||
|
||||
import { Checkbox, Form, Input, Row, Col, Button } from 'ant-design-vue'
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
|
||||
import { useI18n } from '/@/hooks/web/useI18n'
|
||||
import { useMessage } from '/@/hooks/web/useMessage'
|
||||
|
||||
import { useUserStore } from '/@/store/modules/user'
|
||||
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin'
|
||||
import { useDesign } from '/@/hooks/web/useDesign'
|
||||
//import { onKeyStroke } from '@vueuse/core';
|
||||
|
||||
const ACol = Col
|
||||
const ARow = Row
|
||||
const FormItem = Form.Item
|
||||
const InputPassword = Input.Password
|
||||
const { t } = useI18n()
|
||||
const { notification, createErrorModal } = useMessage()
|
||||
const { prefixCls } = useDesign('login')
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { setLoginState, getLoginState } = useLoginState()
|
||||
const { getFormRules } = useFormRules()
|
||||
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
const rememberMe = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
account: 'vben',
|
||||
password: '123456',
|
||||
})
|
||||
|
||||
const { validForm } = useFormValid(formRef)
|
||||
|
||||
//onKeyStroke('Enter', handleLogin);
|
||||
|
||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
|
||||
|
||||
async function handleLogin() {
|
||||
const data = await validForm()
|
||||
if (!data) return
|
||||
try {
|
||||
loading.value = true
|
||||
const userInfo = await userStore.login({
|
||||
password: data.password,
|
||||
username: data.account,
|
||||
mode: 'none', //不要默认的错误提示
|
||||
})
|
||||
if (userInfo) {
|
||||
notification.success({
|
||||
message: t('sys.login.loginSuccessTitle'),
|
||||
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realName}`,
|
||||
duration: 3,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
createErrorModal({
|
||||
title: t('sys.api.errorTip'),
|
||||
content: (error as unknown as Error).message || t('sys.api.networkExceptionMsg'),
|
||||
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body,
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
24
frontend/vben/src/views/sys/login/LoginFormTitle.vue
Normal file
24
frontend/vben/src/views/sys/login/LoginFormTitle.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<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'),
|
||||
}
|
||||
return titleObj[unref(getLoginState)]
|
||||
})
|
||||
</script>
|
||||
63
frontend/vben/src/views/sys/login/MobileForm.vue
Normal file
63
frontend/vben/src/views/sys/login/MobileForm.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<template v-if="getShow">
|
||||
<LoginFormTitle class="enter-x" />
|
||||
<Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef">
|
||||
<FormItem name="mobile" class="enter-x">
|
||||
<Input
|
||||
size="large"
|
||||
v-model:value="formData.mobile"
|
||||
:placeholder="t('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')"
|
||||
/>
|
||||
</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 } from 'vue'
|
||||
import { Form, Input, Button } from 'ant-design-vue'
|
||||
import { CountdownInput } from '/@/components/CountDown'
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import { useI18n } from '/@/hooks/web/useI18n'
|
||||
import { useLoginState, useFormRules, useFormValid, LoginStateEnum } from './useLogin'
|
||||
|
||||
const FormItem = Form.Item
|
||||
const { t } = useI18n()
|
||||
const { handleBackLogin, getLoginState } = useLoginState()
|
||||
const { getFormRules } = useFormRules()
|
||||
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
mobile: '',
|
||||
sms: '',
|
||||
})
|
||||
|
||||
const { validForm } = useFormValid(formRef)
|
||||
|
||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.MOBILE)
|
||||
|
||||
async function handleLogin() {
|
||||
const data = await validForm()
|
||||
if (!data) return
|
||||
console.log(data)
|
||||
}
|
||||
</script>
|
||||
104
frontend/vben/src/views/sys/login/RegisterForm.vue
Normal file
104
frontend/vben/src/views/sys/login/RegisterForm.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<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')"
|
||||
/>
|
||||
</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 } from 'vue'
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import { Form, Input, Button, Checkbox } from 'ant-design-vue'
|
||||
import { StrengthMeter } from '/@/components/StrengthMeter'
|
||||
import { CountdownInput } from '/@/components/CountDown'
|
||||
import { useI18n } from '/@/hooks/web/useI18n'
|
||||
import { useLoginState, useFormRules, useFormValid, LoginStateEnum } from './useLogin'
|
||||
|
||||
const FormItem = Form.Item
|
||||
const InputPassword = Input.Password
|
||||
const { t } = useI18n()
|
||||
const { handleBackLogin, getLoginState } = useLoginState()
|
||||
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
account: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
mobile: '',
|
||||
sms: '',
|
||||
policy: false,
|
||||
})
|
||||
|
||||
const { getFormRules } = useFormRules(formData)
|
||||
const { validForm } = useFormValid(formRef)
|
||||
|
||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)
|
||||
|
||||
async function handleRegister() {
|
||||
const data = await validForm()
|
||||
if (!data) return
|
||||
console.log(data)
|
||||
}
|
||||
</script>
|
||||
53
frontend/vben/src/views/sys/login/SessionTimeoutLogin.vue
Normal file
53
frontend/vben/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>
|
||||
118
frontend/vben/src/views/sys/login/useLogin.ts
Normal file
118
frontend/vben/src/views/sys/login/useLogin.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { ValidationRule } from 'ant-design-vue/lib/form/Form'
|
||||
import type { RuleObject } from 'ant-design-vue/lib/form/interface'
|
||||
import { ref, computed, unref, Ref } from 'vue'
|
||||
import { useI18n } from '/@/hooks/web/useI18n'
|
||||
|
||||
export enum LoginStateEnum {
|
||||
LOGIN,
|
||||
REGISTER,
|
||||
RESET_PASSWORD,
|
||||
MOBILE,
|
||||
QR_CODE,
|
||||
}
|
||||
|
||||
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 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 mobileRule = {
|
||||
sms: smsFormRule,
|
||||
mobile: mobileFormRule,
|
||||
}
|
||||
switch (unref(currentState)) {
|
||||
// register form rules
|
||||
case LoginStateEnum.REGISTER:
|
||||
return {
|
||||
account: accountFormRule,
|
||||
password: passwordFormRule,
|
||||
confirmPassword: [
|
||||
{ validator: validateConfirmPassword(formData?.password), trigger: 'change' },
|
||||
],
|
||||
policy: [{ validator: validatePolicy, trigger: 'change' }],
|
||||
...mobileRule,
|
||||
}
|
||||
|
||||
// reset password form rules
|
||||
case LoginStateEnum.RESET_PASSWORD:
|
||||
return {
|
||||
account: accountFormRule,
|
||||
...mobileRule,
|
||||
}
|
||||
|
||||
// mobile form rules
|
||||
case LoginStateEnum.MOBILE:
|
||||
return mobileRule
|
||||
|
||||
// login form rules
|
||||
default:
|
||||
return {
|
||||
account: accountFormRule,
|
||||
password: passwordFormRule,
|
||||
}
|
||||
}
|
||||
})
|
||||
return { getFormRules }
|
||||
}
|
||||
|
||||
function createRule(message: string) {
|
||||
return [
|
||||
{
|
||||
required: true,
|
||||
message,
|
||||
trigger: 'change',
|
||||
},
|
||||
]
|
||||
}
|
||||
30
frontend/vben/src/views/sys/redirect/index.vue
Normal file
30
frontend/vben/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'
|
||||
|
||||
const { currentRoute, replace } = useRouter()
|
||||
|
||||
const { params, query } = unref(currentRoute)
|
||||
const { path, _redirect_type = 'path' } = params
|
||||
|
||||
Reflect.deleteProperty(params, '_redirect_type')
|
||||
Reflect.deleteProperty(params, 'path')
|
||||
|
||||
const _path = Array.isArray(path) ? path.join('/') : path
|
||||
|
||||
if (_redirect_type === 'name') {
|
||||
replace({
|
||||
name: _path,
|
||||
query,
|
||||
params,
|
||||
})
|
||||
} else {
|
||||
replace({
|
||||
path: _path.startsWith('/') ? _path : '/' + _path,
|
||||
query,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user