sv
This commit is contained in:
24
src/views/Basic/Swagger/index.vue
Normal file
24
src/views/Basic/Swagger/index.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<IFrame :src="src" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import * as ConfigApi from '@/api/infra/config'
|
||||
|
||||
defineOptions({ name: 'InfraSwagger' })
|
||||
|
||||
const loading = ref(true) // 是否加载中
|
||||
const src = ref(import.meta.env.VITE_BASE_URL + '/doc.html') // Knife4j UI
|
||||
// const src = ref(import.meta.env.VITE_BASE_URL + '/swagger-ui') // Swagger UI
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await ConfigApi.getConfigKey('url.swagger')
|
||||
if (data && data.length > 0) {
|
||||
src.value = data
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
6
src/views/Error/403.vue
Normal file
6
src/views/Error/403.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<Error type="403" @error-click="push('/Home/index')" />
|
||||
</template>
|
||||
<script lang="ts" name="Error403" setup>
|
||||
const { push } = useRouter()
|
||||
</script>
|
||||
6
src/views/Error/404.vue
Normal file
6
src/views/Error/404.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<Error @error-click="push('/Home/index')" />
|
||||
</template>
|
||||
<script lang="ts" name="Error404" setup>
|
||||
const { push } = useRouter()
|
||||
</script>
|
||||
6
src/views/Error/500.vue
Normal file
6
src/views/Error/500.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<Error type="500" @error-click="push('/Home/index')" />
|
||||
</template>
|
||||
<script lang="ts" name="Error500" setup>
|
||||
const { push } = useRouter()
|
||||
</script>
|
||||
7
src/views/Home/index.vue
Normal file
7
src/views/Home/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div> 首页 </div>
|
||||
</template>
|
||||
|
||||
<script setup name="Home"></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
192
src/views/Login/Login.vue
Normal file
192
src/views/Login/Login.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div
|
||||
:class="prefixCls"
|
||||
class="h-[100%] relative dark:bg-v-dark <sm:px-10px <xl:px-10px <md:px-10px"
|
||||
>
|
||||
<div class="relative h-full flex mx-auto">
|
||||
<div :class="`${prefixCls}__left w-[30%] <lg:w-[100%] left-box relative p-30px <xl:p-20px`">
|
||||
<!-- 左上角的 logo + 系统标题 -->
|
||||
<div class="flex items-center relative">
|
||||
<h2 class="mb-3 text-2xl font-bold">
|
||||
欢迎使用 {{ underlineToHump(config.description) }}
|
||||
</h2>
|
||||
</div>
|
||||
<!-- 右边的登录界面 -->
|
||||
<Transition appear enter-active-class="animate__animated animate__bounceInRight">
|
||||
<div
|
||||
class="flex items-center m-auto w-[100%] @2xl:max-w-500px @xl:max-w-500px @md:max-w-500px @lg:max-w-500px <xl:mt-20px"
|
||||
>
|
||||
<!-- 账号登录 -->
|
||||
<LoginForm class="p-40px <xl:p-10px h-auto m-auto" :tenantName="defaultTenantName" />
|
||||
<!-- 手机登录 -->
|
||||
<!-- <MobileForm class="p-20px h-auto m-auto <xl:(rounded-3xl light:bg-white)" /> -->
|
||||
<!-- 二维码登录 -->
|
||||
<!-- <QrCodeForm class="p-20px h-auto m-auto <xl:(rounded-3xl light:bg-white)" /> -->
|
||||
<!-- 注册 -->
|
||||
<!-- <RegisterForm class="p-20px h-auto m-auto <xl:(rounded-3xl light:bg-white)" /> -->
|
||||
<!-- 三方登录 -->
|
||||
<!-- <SSOLoginVue class="p-20px h-auto m-auto <xl:(rounded-3xl light:bg-white)" /> -->
|
||||
</div>
|
||||
</Transition>
|
||||
<!-- 左边的背景图 + 欢迎语 -->
|
||||
<div class="flex justify-center">
|
||||
<TransitionGroup
|
||||
appear
|
||||
enter-active-class="animate__animated animate__bounceInLeft"
|
||||
tag="div"
|
||||
>
|
||||
<!-- <img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" /> -->
|
||||
<img key="1" alt="" class="w-[50%] m-auto" src="@/assets/imgs/login2.gif" />
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
|
||||
<div class="landIn">
|
||||
<transition-group name="text-animation">
|
||||
<span v-for="(char, index) in chars" :key="index">{{ char }}</span>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-box flex-1 p-30px <sm:p-10px dark:bg-v-dark <lg:hidden relative">
|
||||
<!-- 右上角的主题、语言选择 -->
|
||||
<div class="flex justify-between items-center text-white @2xl:justify-end @xl:justify-end">
|
||||
<div class="flex items-center @2xl:hidden @xl:hidden"> </div>
|
||||
<div class="flex justify-end items-center space-x-10px">
|
||||
<ThemeSwitch />
|
||||
</div>
|
||||
</div>
|
||||
<img src="@/assets/imgs/shisong.jpg" width="80%" alt="" srcset="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script name="Login" setup>
|
||||
import config from '../../../package.json'
|
||||
import { underlineToHump } from '@/utils'
|
||||
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
|
||||
|
||||
import { LoginForm } from './components'
|
||||
|
||||
import soups from './components/soup.js'
|
||||
|
||||
import * as authUtil from '@/utils/auth'
|
||||
|
||||
// const { t } = useI18n()
|
||||
const { currentRoute } = useRouter()
|
||||
const { getPrefixCls } = useDesign()
|
||||
const prefixCls = getPrefixCls('login')
|
||||
|
||||
const currentCharIndex = ref(0)
|
||||
const typingInterval = ref(null)
|
||||
|
||||
const randomIdx = parseInt(soups.length * Math.random())
|
||||
|
||||
const text = soups[randomIdx]
|
||||
|
||||
const chars = computed(() => {
|
||||
// return t('login.message')
|
||||
return text.slice(0, currentCharIndex.value)
|
||||
})
|
||||
|
||||
// function init() {
|
||||
// authUtil.setTenantId(res)
|
||||
|
||||
// const appId = currentRoute.value?.query?.appId
|
||||
// authUtil.setAppId(appId)
|
||||
// }
|
||||
|
||||
const defaultTenantName = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
startTyping()
|
||||
if (currentRoute.value?.query?.tenantName) {
|
||||
defaultTenantName.value = currentRoute.value.query.tenantName
|
||||
authUtil.setTenantId(defaultTenantName.value)
|
||||
// init()
|
||||
}
|
||||
})
|
||||
|
||||
function startTyping() {
|
||||
typingInterval.value = setInterval(() => {
|
||||
if (currentCharIndex.value < text.length) {
|
||||
currentCharIndex.value++
|
||||
} else {
|
||||
clearInterval(typingInterval.value)
|
||||
}
|
||||
}, 100) // 每100毫秒展示一个字符
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$prefix-cls: #{$namespace}-login;
|
||||
|
||||
.#{$prefix-cls} {
|
||||
&__left {
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// background-image: url('@/assets/svgs/login-bg.svg');
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .left-box {
|
||||
// background: #f3f4fb;
|
||||
// background-image: url('@/assets/imgs/login-left-bg.png');
|
||||
// background-repeat: no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// }
|
||||
|
||||
.login-box {
|
||||
background-image: url('@/assets/imgs/shisong.jpg');
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 80% 80%;
|
||||
}
|
||||
|
||||
.text-animation-enter-active,
|
||||
.text-animation-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.text-animation-enter,
|
||||
.text-animation-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.landIn {
|
||||
margin: 10px auto;
|
||||
width: 80%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: 1rem;
|
||||
line-height: 1.3;
|
||||
font-family: Lora, serif;
|
||||
white-space: pre;
|
||||
font-weight: 600;
|
||||
|
||||
span {
|
||||
animation: landIn 0.8s ease-out both;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes landIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30%);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
261
src/views/Login/components/LoginForm.vue
Normal file
261
src/views/Login/components/LoginForm.vue
Normal file
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<el-form
|
||||
v-show="getShow"
|
||||
ref="formLogin"
|
||||
:model="loginData.loginForm"
|
||||
:rules="LoginRules"
|
||||
class="login-form"
|
||||
label-position="top"
|
||||
label-width="120px"
|
||||
size="large"
|
||||
>
|
||||
<el-row style="margin-left: -10px; margin-right: -10px">
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<slot name="title"></slot>
|
||||
</el-col>
|
||||
<el-col v-if="!props.tenantName" :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item prop="tenantName">
|
||||
<el-input
|
||||
v-model="loginData.loginForm.tenantName"
|
||||
:placeholder="t('login.tenantNamePlaceholder')"
|
||||
:prefix-icon="iconHouse"
|
||||
type="primary"
|
||||
link
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginData.loginForm.username"
|
||||
:placeholder="t('login.usernamePlaceholder')"
|
||||
:prefix-icon="iconAvatar"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginData.loginForm.password"
|
||||
:placeholder="t('login.passwordPlaceholder')"
|
||||
:prefix-icon="iconLock"
|
||||
show-password
|
||||
type="password"
|
||||
@keyup.enter="getCode()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col
|
||||
:span="24"
|
||||
style="padding-left: 10px; padding-right: 10px; margin-top: -20px; margin-bottom: -20px"
|
||||
>
|
||||
<el-form-item>
|
||||
<el-row justify="space-between" style="width: 100%">
|
||||
<el-col :span="6">
|
||||
<el-checkbox v-model="loginData.loginForm.rememberMe">
|
||||
{{ t('login.remember') }}
|
||||
</el-checkbox>
|
||||
</el-col>
|
||||
<!-- <el-col :offset="6" :span="12">
|
||||
<el-link style="float: right" type="primary">{{ t('login.forgetPassword') }}</el-link>
|
||||
</el-col> -->
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item>
|
||||
<XButton
|
||||
:loading="loginLoading"
|
||||
:title="t('login.login')"
|
||||
class="w-[100%]"
|
||||
type="primary"
|
||||
@click="getCode()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<Verify
|
||||
ref="verify"
|
||||
:captchaType="captchaType"
|
||||
:imgSize="{ width: '400px', height: '200px' }"
|
||||
mode="pop"
|
||||
@success="handleLogin"
|
||||
/>
|
||||
<el-col v-if="showOtherLogin" :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item>
|
||||
<el-row :gutter="5" justify="space-between" style="width: 100%">
|
||||
<el-col :span="12">
|
||||
<XButton
|
||||
:title="t('login.btnMobile')"
|
||||
class="w-[100%]"
|
||||
@click="setLoginState(LoginStateEnum.MOBILE)"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<XButton
|
||||
:title="t('login.btnQRCode')"
|
||||
class="w-[100%]"
|
||||
@click="setLoginState(LoginStateEnum.QR_CODE)"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
<script name="LoginForm" setup>
|
||||
import { ElLoading } from 'element-plus'
|
||||
// import LoginFormTitle from './LoginFormTitle.vue'
|
||||
|
||||
import { useIcon } from '@/hooks/web/useIcon'
|
||||
|
||||
import * as authUtil from '@/utils/auth'
|
||||
import * as LoginApi from '@/api/login'
|
||||
import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
|
||||
|
||||
const { t } = useI18n()
|
||||
const iconHouse = useIcon({ icon: 'ep:house' })
|
||||
const iconAvatar = useIcon({ icon: 'ep:avatar' })
|
||||
const iconLock = useIcon({ icon: 'ep:lock' })
|
||||
const formLogin = ref()
|
||||
const { validForm } = useFormValid(formLogin)
|
||||
const { setLoginState, getLoginState } = useLoginState()
|
||||
const { currentRoute, push } = useRouter()
|
||||
const redirect = ref('')
|
||||
const loginLoading = ref(false)
|
||||
const verify = ref()
|
||||
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
|
||||
|
||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
|
||||
const showOtherLogin = false
|
||||
|
||||
const props = defineProps({
|
||||
tenantName: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const LoginRules = {
|
||||
tenantName: [required],
|
||||
username: [required],
|
||||
password: [required]
|
||||
}
|
||||
const loginData = reactive({
|
||||
isShowPassword: false,
|
||||
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
|
||||
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
|
||||
loginForm: {
|
||||
tenantName: undefined,
|
||||
instanceId: undefined,
|
||||
username: '',
|
||||
password: '',
|
||||
captchaVerification: '',
|
||||
rememberMe: true
|
||||
}
|
||||
})
|
||||
|
||||
// 获取验证码
|
||||
const getCode = async () => {
|
||||
// 情况一,未开启:则直接登录
|
||||
if (loginData.captchaEnable === 'false') {
|
||||
await handleLogin({})
|
||||
} else {
|
||||
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
|
||||
// 弹出验证码
|
||||
verify.value.show()
|
||||
}
|
||||
}
|
||||
// 记住我
|
||||
const getCookie = () => {
|
||||
const loginForm = authUtil.getLoginForm()
|
||||
if (loginForm) {
|
||||
loginData.loginForm = {
|
||||
...loginData.loginForm,
|
||||
username: loginForm.username ? loginForm.username : loginData.loginForm.username,
|
||||
password: loginForm.password ? loginForm.password : loginData.loginForm.password,
|
||||
rememberMe: loginForm.rememberMe ? true : false,
|
||||
tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 登录
|
||||
const handleLogin = async (params) => {
|
||||
loginLoading.value = true
|
||||
try {
|
||||
const data = await validForm()
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
loginData.loginForm.captchaVerification = params.captchaVerification
|
||||
const res = await LoginApi.login(loginData.loginForm)
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
ElLoading.service({
|
||||
lock: true,
|
||||
text: '正在加载系统中...',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
if (loginData.loginForm.rememberMe) {
|
||||
authUtil.setLoginForm(loginData.loginForm)
|
||||
} else {
|
||||
authUtil.removeLoginForm()
|
||||
}
|
||||
authUtil.setToken(res)
|
||||
if (!redirect.value) {
|
||||
redirect.value = '/'
|
||||
}
|
||||
// 判断是否为SSO登录
|
||||
if (redirect.value.indexOf('sso') !== -1) {
|
||||
window.location.href = window.location.href.replace('/login?redirect=', '')
|
||||
} else {
|
||||
push({ path: redirect.value || '/index' })
|
||||
}
|
||||
} catch {
|
||||
loginLoading.value = false
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
const loadingInstance = ElLoading.service()
|
||||
loadingInstance.close()
|
||||
}, 400)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => currentRoute.value,
|
||||
(route) => {
|
||||
redirect.value = route?.query?.redirect
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
onMounted(() => {
|
||||
loginData.loginForm.tenantName = props.tenantName
|
||||
getCookie()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.anticon) {
|
||||
&:hover {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.login-code {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
float: right;
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
max-width: 100px;
|
||||
height: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
24
src/views/Login/components/LoginFormTitle.vue
Normal file
24
src/views/Login/components/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-center">
|
||||
{{ getFormTitle }}
|
||||
</h2>
|
||||
</template>
|
||||
<script name="LoginFormTitle" setup>
|
||||
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'),
|
||||
[LoginStateEnum.SSO]: t('sys.login.ssoFormTitle')
|
||||
}
|
||||
return titleObj[unref(getLoginState)]
|
||||
})
|
||||
</script>
|
||||
213
src/views/Login/components/MobileForm.vue
Normal file
213
src/views/Login/components/MobileForm.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<el-form
|
||||
v-show="getShow"
|
||||
ref="formSmsLogin"
|
||||
:model="loginData.loginForm"
|
||||
:rules="rules"
|
||||
class="login-form"
|
||||
label-position="top"
|
||||
label-width="120px"
|
||||
size="large"
|
||||
>
|
||||
<el-row style="margin-left: -10px; margin-right: -10px">
|
||||
<!-- 租户名 -->
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item>
|
||||
<LoginFormTitle style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
|
||||
<el-input
|
||||
v-model="loginData.loginForm.tenantName"
|
||||
:placeholder="t('login.tenantNamePlaceholder')"
|
||||
:prefix-icon="iconHouse"
|
||||
type="primary"
|
||||
link
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- 手机号 -->
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item prop="mobileNumber">
|
||||
<el-input
|
||||
v-model="loginData.loginForm.mobileNumber"
|
||||
:placeholder="t('login.mobileNumberPlaceholder')"
|
||||
:prefix-icon="iconCellphone"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- 验证码 -->
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item prop="code">
|
||||
<el-row :gutter="5" justify="space-between" style="width: 100%">
|
||||
<el-col :span="24">
|
||||
<el-input
|
||||
v-model="loginData.loginForm.code"
|
||||
:placeholder="t('login.codePlaceholder')"
|
||||
:prefix-icon="iconCircleCheck"
|
||||
>
|
||||
<!-- <el-button class="w-[100%]"> -->
|
||||
<template #append>
|
||||
<span
|
||||
v-if="mobileCodeTimer <= 0"
|
||||
class="getMobileCode"
|
||||
style="cursor: pointer"
|
||||
@click="getSmsCode"
|
||||
>
|
||||
{{ t('login.getSmsCode') }}
|
||||
</span>
|
||||
<span v-if="mobileCodeTimer > 0" class="getMobileCode" style="cursor: pointer">
|
||||
{{ mobileCodeTimer }}秒后可重新获取
|
||||
</span>
|
||||
</template>
|
||||
</el-input>
|
||||
<!-- </el-button> -->
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- 登录按钮 / 返回按钮 -->
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item>
|
||||
<XButton
|
||||
:loading="loginLoading"
|
||||
:title="t('login.login')"
|
||||
class="w-[100%]"
|
||||
type="primary"
|
||||
@click="signIn()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-form-item>
|
||||
<XButton
|
||||
:loading="loginLoading"
|
||||
:title="t('login.backLogin')"
|
||||
class="w-[100%]"
|
||||
@click="handleBackLogin()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
<script lang="ts" name="MobileForm" setup>
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
import { useIcon } from '@/hooks/web/useIcon'
|
||||
|
||||
import { setTenantId, setToken } from '@/utils/auth'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { getTenantIdByName, sendSmsCode, smsLogin } from '@/api/login'
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const permissionStore = usePermissionStore()
|
||||
const { currentRoute, push } = useRouter()
|
||||
const formSmsLogin = ref()
|
||||
const loginLoading = ref(false)
|
||||
const iconHouse = useIcon({ icon: 'ep:house' })
|
||||
const iconCellphone = useIcon({ icon: 'ep:cellphone' })
|
||||
const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
|
||||
const { validForm } = useFormValid(formSmsLogin)
|
||||
const { handleBackLogin, getLoginState } = useLoginState()
|
||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.MOBILE)
|
||||
|
||||
const rules = {
|
||||
tenantName: [required],
|
||||
mobileNumber: [required],
|
||||
code: [required]
|
||||
}
|
||||
const loginData = reactive({
|
||||
codeImg: '',
|
||||
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
|
||||
token: '',
|
||||
loading: {
|
||||
signIn: false
|
||||
},
|
||||
loginForm: {
|
||||
uuid: '',
|
||||
tenantName: '莳松',
|
||||
mobileNumber: '',
|
||||
code: ''
|
||||
}
|
||||
})
|
||||
const smsVO = reactive({
|
||||
smsCode: {
|
||||
mobile: '',
|
||||
scene: 21
|
||||
},
|
||||
loginSms: {
|
||||
mobile: '',
|
||||
code: ''
|
||||
}
|
||||
})
|
||||
const mobileCodeTimer = ref(0)
|
||||
const redirect = ref<string>('')
|
||||
const getSmsCode = async () => {
|
||||
await getTenantId()
|
||||
smsVO.smsCode.mobile = loginData.loginForm.mobileNumber
|
||||
await sendSmsCode(smsVO.smsCode).then(async () => {
|
||||
message.success(t('login.SmsSendMsg'))
|
||||
// 设置倒计时
|
||||
mobileCodeTimer.value = 60
|
||||
let msgTimer = setInterval(() => {
|
||||
mobileCodeTimer.value = mobileCodeTimer.value - 1
|
||||
if (mobileCodeTimer.value <= 0) {
|
||||
clearInterval(msgTimer)
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
watch(
|
||||
() => currentRoute.value,
|
||||
(route: RouteLocationNormalizedLoaded) => {
|
||||
redirect.value = route?.query?.redirect as string
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
// 获取租户 ID
|
||||
const getTenantId = async () => {
|
||||
if (loginData.tenantEnable === 'true') {
|
||||
const res = await getTenantIdByName(loginData.loginForm.tenantName)
|
||||
setTenantId(res)
|
||||
}
|
||||
}
|
||||
// 登录
|
||||
const signIn = async () => {
|
||||
await getTenantId()
|
||||
const data = await validForm()
|
||||
if (!data) return
|
||||
loginLoading.value = true
|
||||
smsVO.loginSms.mobile = loginData.loginForm.mobileNumber
|
||||
smsVO.loginSms.code = loginData.loginForm.code
|
||||
await smsLogin(smsVO.loginSms)
|
||||
.then(async (res) => {
|
||||
setToken(res?.token)
|
||||
if (!redirect.value) {
|
||||
redirect.value = '/'
|
||||
}
|
||||
push({ path: redirect.value || permissionStore.addRouters[0].path })
|
||||
})
|
||||
.finally(() => {
|
||||
loginLoading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.anticon) {
|
||||
&:hover {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.smsbtn {
|
||||
margin-top: 33px;
|
||||
}
|
||||
</style>
|
||||
31
src/views/Login/components/QrCodeForm.vue
Normal file
31
src/views/Login/components/QrCodeForm.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<el-row v-show="getShow" style="maring-left: -10px; maring-right: -10px">
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<LoginFormTitle style="width: 100%" />
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<el-card class="mb-10px text-center" shadow="hover">
|
||||
<!-- <Qrcode :logo="logoUrl" /> -->
|
||||
<Qrcode />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-divider class="enter-x">{{ t('login.qrcode') }}</el-divider>
|
||||
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
|
||||
<div class="w-[100%] mt-15px">
|
||||
<XButton :title="t('login.backLogin')" class="w-[100%]" @click="handleBackLogin()" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<script lang="ts" name="QrCodeForm" setup>
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import { LoginStateEnum, useLoginState } from './useLogin'
|
||||
// import { useAppStore } from '@/store/modules/app'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { handleBackLogin, getLoginState } = useLoginState()
|
||||
// const appStore = useAppStore()
|
||||
|
||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.QR_CODE)
|
||||
// const logoUrl = computed(() => appStore.getAppInfo?.instanceIcon)
|
||||
</script>
|
||||
140
src/views/Login/components/RegisterForm.vue
Normal file
140
src/views/Login/components/RegisterForm.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<Form
|
||||
v-show="getShow"
|
||||
:rules="rules"
|
||||
:schema="schema"
|
||||
class="dark:(border-1 border-[var(--el-border-color)] border-solid)"
|
||||
hide-required-asterisk
|
||||
label-position="top"
|
||||
size="large"
|
||||
@register="register"
|
||||
>
|
||||
<template #title>
|
||||
<LoginFormTitle style="width: 100%" />
|
||||
</template>
|
||||
|
||||
<template #code="form">
|
||||
<div class="w-[100%] flex">
|
||||
<el-input v-model="form['code']" :placeholder="t('login.codePlaceholder')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #register>
|
||||
<div class="w-[100%]">
|
||||
<XButton
|
||||
:loading="loading"
|
||||
:title="t('login.register')"
|
||||
class="w-[100%]"
|
||||
type="primary"
|
||||
@click="loginRegister()"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-[100%] mt-15px">
|
||||
<XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" />
|
||||
</div>
|
||||
</template>
|
||||
</Form>
|
||||
</template>
|
||||
<script lang="ts" name="RegisterForm" setup>
|
||||
import type { FormRules } from 'element-plus'
|
||||
|
||||
import { useForm } from '@/hooks/web/useForm'
|
||||
import { useValidator } from '@/hooks/web/useValidator'
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import { LoginStateEnum, useLoginState } from './useLogin'
|
||||
import { FormSchema } from '@/types/form'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { required } = useValidator()
|
||||
const { register, elFormRef } = useForm()
|
||||
const { handleBackLogin, getLoginState } = useLoginState()
|
||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)
|
||||
|
||||
const schema = reactive<FormSchema[]>([
|
||||
{
|
||||
field: 'title',
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'username',
|
||||
label: t('login.username'),
|
||||
value: '',
|
||||
component: 'Input',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
componentProps: {
|
||||
placeholder: t('login.usernamePlaceholder')
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'password',
|
||||
label: t('login.password'),
|
||||
value: '',
|
||||
component: 'InputPassword',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '100%'
|
||||
},
|
||||
strength: true,
|
||||
placeholder: t('login.passwordPlaceholder')
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'check_password',
|
||||
label: t('login.checkPassword'),
|
||||
value: '',
|
||||
component: 'InputPassword',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '100%'
|
||||
},
|
||||
strength: true,
|
||||
placeholder: t('login.passwordPlaceholder')
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
label: t('login.code'),
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'register',
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const rules: FormRules = {
|
||||
username: [required()],
|
||||
password: [required()],
|
||||
check_password: [required()],
|
||||
code: [required()]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const loginRegister = async () => {
|
||||
const formRef = unref(elFormRef)
|
||||
formRef?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
loading.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
186
src/views/Login/components/SSOLogin.vue
Normal file
186
src/views/Login/components/SSOLogin.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div v-show="ssoVisible" class="form-cont">
|
||||
<!-- 应用名 -->
|
||||
<LoginFormTitle style="width: 100%" />
|
||||
<el-tabs class="form" style="float: none" value="uname">
|
||||
<el-tab-pane :label="client.name" name="uname" />
|
||||
</el-tabs>
|
||||
<div>
|
||||
<el-form :model="formData" class="login-form">
|
||||
<!-- 授权范围的选择 -->
|
||||
此第三方应用请求获得以下权限:
|
||||
<el-form-item prop="scopes">
|
||||
<el-checkbox-group v-model="formData.scopes">
|
||||
<el-checkbox
|
||||
v-for="scope in queryParams.scopes"
|
||||
:key="scope"
|
||||
:label="scope"
|
||||
style="display: block; margin-bottom: -10px"
|
||||
>
|
||||
{{ formatScope(scope) }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<!-- 下方的登录按钮 -->
|
||||
<el-form-item class="w-1/1">
|
||||
<el-button
|
||||
:loading="formLoading"
|
||||
class="w-6/10"
|
||||
type="primary"
|
||||
@click.prevent="handleAuthorize(true)"
|
||||
>
|
||||
<span v-if="!formLoading">同意授权</span>
|
||||
<span v-else>授 权 中...</span>
|
||||
</el-button>
|
||||
<el-button class="w-3/10" @click.prevent="handleAuthorize(false)">拒绝</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" name="SSOLogin" setup>
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import * as OAuth2Api from '@/api/login/oauth2'
|
||||
import { LoginStateEnum, useLoginState } from './useLogin'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
const route = useRoute() // 路由
|
||||
const { currentRoute } = useRouter() // 路由
|
||||
const { getLoginState, setLoginState } = useLoginState()
|
||||
|
||||
const client = ref({
|
||||
// 客户端信息
|
||||
name: '',
|
||||
logo: ''
|
||||
})
|
||||
const queryParams = reactive({
|
||||
// URL 上的 client_id、scope 等参数
|
||||
responseType: '',
|
||||
clientId: '',
|
||||
redirectUri: '',
|
||||
state: '',
|
||||
scopes: [] // 优先从 query 参数获取;如果未传递,从后端获取
|
||||
})
|
||||
const ssoVisible = computed(() => unref(getLoginState) === LoginStateEnum.SSO) // 是否展示 SSO 登录的表单
|
||||
const formData = reactive({
|
||||
scopes: [] // 已选中的 scope 数组
|
||||
})
|
||||
const formLoading = ref(false) // 表单是否提交中
|
||||
|
||||
/** 初始化授权信息 */
|
||||
const init = async () => {
|
||||
// 防止在没有登录的情况下循环弹窗
|
||||
if (typeof route.query.client_id === 'undefined') return
|
||||
// 解析参数
|
||||
// 例如说【自动授权不通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write
|
||||
// 例如说【自动授权通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read
|
||||
queryParams.responseType = route.query.response_type as string
|
||||
queryParams.clientId = route.query.client_id as string
|
||||
queryParams.redirectUri = route.query.redirect_uri as string
|
||||
queryParams.state = route.query.state as string
|
||||
if (route.query.scope) {
|
||||
queryParams.scopes = (route.query.scope as string).split(' ')
|
||||
}
|
||||
|
||||
// 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。
|
||||
if (queryParams.scopes.length > 0) {
|
||||
const data = await doAuthorize(true, queryParams.scopes, [])
|
||||
if (data) {
|
||||
location.href = data
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 获取授权页的基本信息
|
||||
const data = await OAuth2Api.getAuthorize(queryParams.clientId)
|
||||
client.value = data.client
|
||||
// 解析 scope
|
||||
let scopes
|
||||
// 1.1 如果 params.scope 非空,则过滤下返回的 scopes
|
||||
if (queryParams.scopes.length > 0) {
|
||||
scopes = []
|
||||
for (const scope of data.scopes) {
|
||||
if (queryParams.scopes.indexOf(scope.key) >= 0) {
|
||||
scopes.push(scope)
|
||||
}
|
||||
}
|
||||
// 1.2 如果 params.scope 为空,则使用返回的 scopes 设置它
|
||||
} else {
|
||||
scopes = data.scopes
|
||||
for (const scope of scopes) {
|
||||
queryParams.scopes.push(scope.key)
|
||||
}
|
||||
}
|
||||
// 生成已选中的 checkedScopes
|
||||
for (const scope of scopes) {
|
||||
if (scope.value) {
|
||||
formData.scopes.push(scope.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理授权的提交 */
|
||||
const handleAuthorize = async (approved) => {
|
||||
// 计算 checkedScopes + uncheckedScopes
|
||||
let checkedScopes
|
||||
let uncheckedScopes
|
||||
if (approved) {
|
||||
// 同意授权,按照用户的选择
|
||||
checkedScopes = formData.scopes
|
||||
uncheckedScopes = queryParams.scopes.filter((item) => checkedScopes.indexOf(item) === -1)
|
||||
} else {
|
||||
// 拒绝,则都是取消
|
||||
checkedScopes = []
|
||||
uncheckedScopes = queryParams.scopes
|
||||
}
|
||||
// 提交授权的请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = await doAuthorize(false, checkedScopes, uncheckedScopes)
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
location.href = data
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 调用授权 API 接口 */
|
||||
const doAuthorize = (autoApprove, checkedScopes, uncheckedScopes) => {
|
||||
return OAuth2Api.authorize(
|
||||
queryParams.responseType,
|
||||
queryParams.clientId,
|
||||
queryParams.redirectUri,
|
||||
queryParams.state,
|
||||
autoApprove,
|
||||
checkedScopes,
|
||||
uncheckedScopes
|
||||
)
|
||||
}
|
||||
|
||||
/** 格式化 scope 文本 */
|
||||
const formatScope = (scope) => {
|
||||
// 格式化 scope 授权范围,方便用户理解。
|
||||
// 这里仅仅是一个 demo,可以考虑录入到字典数据中,例如说字典类型 "system_oauth2_scope",它的每个 scope 都是一条字典数据。
|
||||
switch (scope) {
|
||||
case 'user.read':
|
||||
return '访问你的个人信息'
|
||||
case 'user.write':
|
||||
return '修改你的个人信息'
|
||||
default:
|
||||
return scope
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听当前路由为 SSOLogin 时,进行数据的初始化 */
|
||||
watch(
|
||||
() => currentRoute.value,
|
||||
(route: RouteLocationNormalizedLoaded) => {
|
||||
if (route.name === 'SSOLogin') {
|
||||
setLoginState(LoginStateEnum.SSO)
|
||||
init()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
8
src/views/Login/components/index.ts
Normal file
8
src/views/Login/components/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import LoginForm from './LoginForm.vue'
|
||||
import MobileForm from './MobileForm.vue'
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import RegisterForm from './RegisterForm.vue'
|
||||
import QrCodeForm from './QrCodeForm.vue'
|
||||
import SSOLoginVue from './SSOLogin.vue'
|
||||
|
||||
export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue }
|
||||
79
src/views/Login/components/soup.js
Normal file
79
src/views/Login/components/soup.js
Normal file
@@ -0,0 +1,79 @@
|
||||
export default [
|
||||
'每一滴汗水,都是通往成功的阶梯,坚持不懈,你终将登上梦想的顶峰。',
|
||||
'无论生活给你多少曲折,只要你心怀希望,就能找到前行的力量。',
|
||||
'每一个梦想都值得追求,每一次努力都不会被辜负,相信自己,你定能创造辉煌。',
|
||||
'勇敢面对挑战,笑对人生百态,你的坚强与乐观,将点亮前行的道路。',
|
||||
'梦想是心中的灯塔,指引我们穿越黑暗,勇往直前,终会抵达光明的彼岸。',
|
||||
'人生没有彩排,每一天都是现场直播,让我们用满腔热情,演绎属于自己的精彩。',
|
||||
'不要因为一时的困难而退缩,要相信,你的潜力无穷,未来可期。',
|
||||
'心中有阳光,脚下有力量,让我们携手共进,创造更美好的明天。',
|
||||
'勇敢追梦,不畏艰难,你的努力与汗水,将化作最耀眼的星辰。',
|
||||
'人生路上,难免风雨兼程,但只要心中有信念,脚下有方向,你就能无畏前行。',
|
||||
'每一次挑战都是成长的契机,每一滴汗水都是成功的种子。相信自己,你定能绽放出最耀眼的光芒!',
|
||||
'勇敢追梦,不畏将来。你的每一个努力,都在铺就通往辉煌的道路。加油,未来的英雄!',
|
||||
'生活中的每一个瞬间都值得珍惜,每一次尝试都值得鼓掌。不要害怕失败,因为成功就在下一个转角等待你。',
|
||||
'微风轻拂,阳光正好,生活的小确幸就在身边,让我们一起感受这份美好。',
|
||||
'泡一杯香浓的咖啡,享受一段悠闲的时光,让疲惫一扫而光。',
|
||||
'看着窗外的蓝天白云,心情瞬间变得轻松愉悦,仿佛整个世界都充满了希望。',
|
||||
'和好友相聚,分享彼此的喜怒哀乐,让友谊在欢笑中更加深厚。',
|
||||
'捧着一束鲜花,闻着花香,让心情变得如花儿般绽放。',
|
||||
'偶尔放慢脚步,欣赏路边的风景,让生活的美好瞬间留在心间。',
|
||||
'雨天,窝在家里看一部温馨电影,也是一种小确幸。',
|
||||
'偶尔给自己放个假,去旅行看看外面的世界,让生活充满新鲜感。',
|
||||
'和喜欢的人一起散步,手牵手,聊着天,享受这份简单的幸福。',
|
||||
'偶尔给自己买一束鲜花,放在床头,让花香伴你入眠,美好又浪漫。',
|
||||
'微笑是生活的调味品,它能让苦涩变得甜美,让疲惫变得轻松。无论何时何地,都不要忘记给自己一个微笑,让快乐从心而生。',
|
||||
'生活中的每一个瞬间都值得我们去珍惜和感恩,无论是阳光明媚的日子,还是阴雨绵绵的时刻。用心去感受,用爱去拥抱,生活就会充满温暖与美好。',
|
||||
'不要把烦恼放在心上,让它随风而去。学会释放压力,让自己轻松自在。人生苦短,何必让忧虑占据我们的心灵呢?',
|
||||
'每个人都是独一无二的,都有自己的闪光点和魅力。不要羡慕他人,要相信自己的价值,勇敢地做自己,活出自己的精彩。',
|
||||
'生活中的挫折和困难是成长的催化剂,它们让我们变得更加坚强和成熟。不要害怕失败,要敢于面对挑战,相信自己能够战胜一切。',
|
||||
'每一天都是新的开始,不要让昨天的阴影影响今天的心情。用积极的心态去迎接每一个清晨,让阳光洒满心田,温暖每一个角落。',
|
||||
'学会放下过去的不快和遗憾,让心灵得到解脱。把注意力放在当下和未来,用爱和希望去填满生活的每一个角落。',
|
||||
'与人为善,心怀感恩,是我们应该秉持的人生态度。用一颗善良的心去对待他人,你会发现世界变得更加美好和温暖。',
|
||||
'不要让忙碌的生活压垮了你的身心,要学会调整自己的节奏,给自己留出一些休息和放松的时间。只有身心愉悦,才能更好地面对生活的挑战。',
|
||||
'珍惜身边的人和事,感恩每一个相遇和别离。人生如一场旅行,有起有落,有聚有散。用一颗感恩的心去体会这一切,你会发现生活充满了奇迹和美好。',
|
||||
'生活中的每一个瞬间,都值得我们去珍惜和感激。让我们用一颗轻松愉悦的心,去迎接每一个美好的明天。',
|
||||
'放下过去的烦恼,拥抱未来的希望,让心灵在轻松愉悦中自由飞翔。',
|
||||
'笑一笑,十年少。用微笑面对生活的挑战,让心灵沐浴在轻松愉悦的阳光里。',
|
||||
'人生就像一场旅行,不在乎目的地,只在乎沿途的风景和心情。保持轻松愉悦,享受每一段旅程的美好。',
|
||||
'别以为你每天都很忙,其实你只是看起来很努力,但离成功却还有十万八千里。不过没关系,至少你看起来很充实。',
|
||||
'生活就像一杯毒鸡汤,虽然难喝,但喝下去后,你会发现自己变得更加坚强了,因为连毒都能扛,还有什么好怕的?',
|
||||
'与其抱怨生活的艰辛,不如用一颗感恩的心去体验每一份快乐。轻松愉悦地活着,就是最美的风景。',
|
||||
'梦想还是要有的,万一实现了呢?但记得别只是想想而已,毕竟连想想都很累。',
|
||||
'让心灵沐浴在轻松愉悦的氛围中,感受生活的美好与宁静。',
|
||||
'谁说努力就一定能成功?别傻了,成功的人都在偷偷摸摸地努力,而你却大张旗鼓地宣扬。',
|
||||
'有时候觉得自己一无是处,别灰心,至少你还有个优点——善于自我安慰。',
|
||||
'每一次深呼吸,都是对心灵的洗礼。放下疲惫,拥抱轻松愉悦。',
|
||||
'人生短暂,何必让烦恼占据心灵。用轻松愉悦的态度去面对生活,你会发现世界如此美好。',
|
||||
'无论遇到什么困难,都要保持一颗轻松愉悦的心。因为,生活总会在不经意间给你带来惊喜。',
|
||||
'有些人表面看起来光鲜亮丽,其实背地里都在熬夜加班。所以,别羡慕别人的成功,先看看他们付出了多少努力。',
|
||||
'失败并不可怕,可怕的是你一直在失败,却从未尝试过成功。',
|
||||
'真正的勇士,敢于直面惨淡的人生,敢于正视淋漓的鲜血。而你,只是敢于直面手机的屏幕。',
|
||||
'让心灵在轻松愉悦中翩翩起舞,感受生活的无限美好。',
|
||||
'每一个清晨,都是新的开始。用轻松愉悦的心情去迎接每一个新的挑战。',
|
||||
'轻松愉悦地生活,是对自己最好的奖赏。',
|
||||
'有时候觉得自己聪明绝顶,但一遇到难题就傻眼了。看来,聪明和傻只在一念之间。',
|
||||
'谁说爱情是甜蜜的?那只不过是你还没尝过失恋的滋味罢了。',
|
||||
'人生就像一场戏,因为有缘才相聚。别等到散场时才后悔,没好好享受过程。',
|
||||
'谁说岁月是把杀猪刀?它明明是把美容刀,只是看你愿不愿意接受它的“改造”罢了。',
|
||||
'有人说,人生就像一场马拉松。但我觉得更像是一场接力赛,只不过你的队友都是猪队友。',
|
||||
'有时候觉得自己很幸运,但一想起那些比我更幸运的人,瞬间就不觉得自己幸运了。',
|
||||
'都说人生如戏,全靠演技。但别忘了,有时候演技再好,也敌不过一个烂剧本。',
|
||||
'放下包袱,轻松前行。用一颗愉悦的心去体验生活的精彩。',
|
||||
'有些人总是抱怨生活不公平,但你有没有想过,或许是你自己不够努力?',
|
||||
'都说失败是成功之母,但别忘了,有时候失败多了,就成了成功的奶奶。',
|
||||
'人生就像一盒巧克力,你永远不知道下一颗是什么味道。但你可以确定的是,它肯定不是你想吃的那种。',
|
||||
'谁说梦想一定要有实现的可能?有时候,它只是我们用来逃避现实的借口罢了。',
|
||||
'别以为你手机里的自拍很美,其实你只是滤镜用得好,现实里的你,恐怕连狗都不想多看一眼。',
|
||||
'谁说失败是成功之母?那只是失败者给自己找的借口,成功者可是从没错过!',
|
||||
'有时候觉得自己挺聪明的,但一照镜子,就明白了什么叫“聪明绝顶”。',
|
||||
'生活就像一场戏,我们都是演员,只不过有的人演技好,有的人演得像猴。',
|
||||
'谁说努力就能成功?别傻了,你看过哪个彩票得主是靠努力中奖的?',
|
||||
'梦想还是要有的,万一别人问起来你没有,岂不是很尴尬?',
|
||||
'减肥真的是件很简单的事,只要你管得住嘴,迈得开腿,当然,还有一颗坚强的心,去面对美食的诱惑。',
|
||||
'人家说你丑,你别伤心,至少你的丑,给这个世界增添了一份多样性。',
|
||||
'有时候觉得自己挺幸运的,但一想到那些比我更幸运的人,我就觉得自己其实也挺不幸的。',
|
||||
'有时候觉得自己挺有才华的,但一开口说话,就明白了什么叫“才疏学浅”。',
|
||||
'生活就像海洋,只有意志坚强的人,才能到达彼岸。而我就不一样了,我是坐船的。',
|
||||
'努力了这么多年,唯一的收获就是体重。'
|
||||
]
|
||||
42
src/views/Login/components/useLogin.ts
Normal file
42
src/views/Login/components/useLogin.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Ref } from 'vue'
|
||||
|
||||
export enum LoginStateEnum {
|
||||
LOGIN,
|
||||
REGISTER,
|
||||
RESET_PASSWORD,
|
||||
MOBILE,
|
||||
QR_CODE,
|
||||
SSO
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
60
src/views/Profile/Index.vue
Normal file
60
src/views/Profile/Index.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<el-card class="w-1/3 user" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ t('profile.user.title') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<ProfileUser />
|
||||
</el-card>
|
||||
<el-card class="w-2/3 user ml-3" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ t('profile.info.title') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<el-tabs v-model="activeName" tab-position="top" style="height: 400px" class="profile-tabs">
|
||||
<el-tab-pane :label="t('profile.info.basicInfo')" name="basicInfo">
|
||||
<BasicInfo />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('profile.info.resetPwd')" name="resetPwd">
|
||||
<ResetPwd />
|
||||
</el-tab-pane>
|
||||
<!-- <el-tab-pane :label="t('profile.info.userSocial')" name="userSocial">
|
||||
<UserSocial />
|
||||
</el-tab-pane> -->
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" name="Profile">
|
||||
import { BasicInfo, ProfileUser, ResetPwd } from './components/'
|
||||
const { t } = useI18n()
|
||||
|
||||
const activeName = ref('basicInfo')
|
||||
</script>
|
||||
<style scoped>
|
||||
.user {
|
||||
max-height: 960px;
|
||||
padding: 15px 20px 20px 20px;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
:deep(.el-card .el-card__header, .el-card .el-card__body) {
|
||||
padding: 15px !important;
|
||||
}
|
||||
.profile-tabs > .el-tabs__content {
|
||||
padding: 32px;
|
||||
color: #6b778c;
|
||||
font-weight: 600;
|
||||
}
|
||||
.el-tabs--left .el-tabs__content {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
7
src/views/Profile/NotifyMessage.vue
Normal file
7
src/views/Profile/NotifyMessage.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div> 站内信 </div>
|
||||
</template>
|
||||
|
||||
<script setup name="NotifyMessage"></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
94
src/views/Profile/components/BasicInfo.vue
Normal file
94
src/views/Profile/components/BasicInfo.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<Form ref="formRef" :labelWidth="80" :rules="rules" :schema="schema">
|
||||
<template #sex="form">
|
||||
<el-radio-group v-model="form['sex']">
|
||||
<el-radio :value="1">{{ t('profile.user.man') }}</el-radio>
|
||||
<el-radio :value="2">{{ t('profile.user.woman') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
</Form>
|
||||
<XButton :title="t('common.save')" @click="submit()" />
|
||||
<XButton :title="t('common.reset')" type="danger" @click="init()" />
|
||||
</template>
|
||||
<script lang="ts" name="BasicInfo" setup>
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import { FormSchema } from '@/types/form'
|
||||
import type { FormExpose } from '@/components/Form'
|
||||
import {
|
||||
getUserProfile,
|
||||
updateUserProfile,
|
||||
UserProfileUpdateReqVO
|
||||
} from '@/api/system/user/profile'
|
||||
|
||||
const { t } = useI18n()
|
||||
// 表单校验
|
||||
const rules = reactive<FormRules>({
|
||||
nickname: [{ required: true, message: t('profile.rules.nickname'), trigger: 'blur' }],
|
||||
// email: [
|
||||
// { required: true, message: t('profile.rules.mail'), trigger: 'blur' },
|
||||
// {
|
||||
// type: 'email',
|
||||
// message: t('profile.rules.truemail'),
|
||||
// trigger: ['blur', 'change']
|
||||
// }
|
||||
// ],
|
||||
mobile: [
|
||||
{ required: true, message: t('profile.rules.phone'), trigger: 'blur' },
|
||||
{
|
||||
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
|
||||
message: t('profile.rules.truephone'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
})
|
||||
const schema = reactive<FormSchema[]>([
|
||||
{
|
||||
field: 'nickname',
|
||||
label: t('profile.user.nickname'),
|
||||
component: 'Input'
|
||||
},
|
||||
{
|
||||
field: 'mobile',
|
||||
label: t('profile.user.mobile'),
|
||||
component: 'Input'
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
label: t('profile.user.email'),
|
||||
component: 'Input'
|
||||
},
|
||||
{
|
||||
field: 'wxAlias',
|
||||
label: '微信号',
|
||||
component: 'Input'
|
||||
},
|
||||
{
|
||||
field: 'sex',
|
||||
label: t('profile.user.sex'),
|
||||
component: 'InputNumber',
|
||||
value: 0
|
||||
}
|
||||
])
|
||||
const formRef = ref<FormExpose>() // 表单 Ref
|
||||
const submit = () => {
|
||||
const elForm = unref(formRef)?.getElFormRef()
|
||||
if (!elForm) return
|
||||
elForm.validate(async (valid) => {
|
||||
if (valid) {
|
||||
const data = unref(formRef)?.formModel as UserProfileUpdateReqVO
|
||||
await updateUserProfile(data)
|
||||
ElMessage.success(t('common.updateSuccess'))
|
||||
await init()
|
||||
}
|
||||
})
|
||||
}
|
||||
const init = async () => {
|
||||
const res = await getUserProfile()
|
||||
unref(formRef)?.setValues(res)
|
||||
}
|
||||
onMounted(async () => {
|
||||
await init()
|
||||
})
|
||||
</script>
|
||||
97
src/views/Profile/components/ProfileUser.vue
Normal file
97
src/views/Profile/components/ProfileUser.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<UserAvatar :img="userInfo?.avatar" />
|
||||
</div>
|
||||
<ul class="list-group list-group-striped">
|
||||
<li class="list-group-item">
|
||||
<Icon class="mr-5px" icon="ep:user" />
|
||||
{{ t('profile.user.username') }}
|
||||
<div class="pull-right">{{ userInfo?.username }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<Icon class="mr-5px" icon="ep:phone" />
|
||||
{{ t('profile.user.mobile') }}
|
||||
<div class="pull-right">{{ userInfo?.mobile }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<Icon class="mr-5px" icon="fontisto:email" />
|
||||
{{ t('profile.user.email') }}
|
||||
<div class="pull-right">{{ userInfo?.email }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<Icon class="mr-5px" icon="carbon:tree-view-alt" />
|
||||
{{ t('profile.user.dept') }}
|
||||
<div v-if="userInfo?.dept" class="pull-right">{{ userInfo?.dept.name }}</div>
|
||||
</li>
|
||||
<!-- <li class="list-group-item">
|
||||
<Icon class="mr-5px" icon="ep:suitcase" />
|
||||
{{ t('profile.user.posts') }}
|
||||
<div v-if="userInfo?.posts" class="pull-right">
|
||||
{{ userInfo?.posts.map((post) => post.name).join(',') }}
|
||||
</div>
|
||||
</li> -->
|
||||
<li class="list-group-item">
|
||||
<Icon class="mr-5px" icon="icon-park-outline:peoples" />
|
||||
{{ t('profile.user.roles') }}
|
||||
<div v-if="userInfo?.roles" class="pull-right">
|
||||
{{ userInfo?.roles.map((role) => role.name).join(',') }}
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<Icon class="mr-5px" icon="ep:calendar" />
|
||||
{{ t('profile.user.createTime') }}
|
||||
<div class="pull-right">{{ formatDate(userInfo?.createTime) }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" name="ProfileUser" setup>
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import UserAvatar from './UserAvatar.vue'
|
||||
|
||||
import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
|
||||
|
||||
const { t } = useI18n()
|
||||
const userInfo = ref<ProfileVO>()
|
||||
const getUserInfo = async () => {
|
||||
const users = await getUserProfile()
|
||||
userInfo.value = users
|
||||
}
|
||||
onMounted(async () => {
|
||||
await getUserInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-center {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.list-group-striped > .list-group-item {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-radius: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.list-group {
|
||||
padding-left: 0px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
border-bottom: 1px solid #e7eaec;
|
||||
border-top: 1px solid #e7eaec;
|
||||
margin-bottom: -1px;
|
||||
padding: 11px 0px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
float: right !important;
|
||||
}
|
||||
</style>
|
||||
68
src/views/Profile/components/ResetPwd.vue
Normal file
68
src/views/Profile/components/ResetPwd.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<el-form ref="formRef" :model="password" :rules="rules" label-width="80px">
|
||||
<el-form-item :label="t('profile.password.oldPassword')">
|
||||
<InputPassword v-model="password.oldPassword" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('profile.password.newPassword')">
|
||||
<InputPassword v-model="password.newPassword" strength />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('profile.password.confirmPassword')">
|
||||
<InputPassword v-model="password.confirmPassword" strength />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<XButton :title="t('common.save')" type="primary" @click="submit(formRef)" />
|
||||
<XButton :title="t('common.reset')" type="danger" @click="reset(formRef)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
<script lang="ts" name="ResetPwd" setup>
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
import { InputPassword } from '@/components/InputPassword'
|
||||
import { updateUserPassword } from '@/api/system/user/profile'
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const formRef = ref<FormInstance>()
|
||||
const password = reactive({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
// 表单校验
|
||||
const equalToPassword = (value, callback) => {
|
||||
if (password.newPassword !== value) {
|
||||
callback(new Error(t('profile.password.diffPwd')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
const rules = reactive<FormRules>({
|
||||
oldPassword: [
|
||||
{ required: true, message: t('profile.password.oldPwdMsg'), trigger: 'blur' },
|
||||
{ min: 3, max: 5, message: t('profile.password.pwdRules'), trigger: 'blur' }
|
||||
],
|
||||
newPassword: [
|
||||
{ required: true, message: t('profile.password.newPwdMsg'), trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: t('profile.password.pwdRules'), trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: t('profile.password.cfPwdMsg'), trigger: 'blur' },
|
||||
{ required: true, validator: equalToPassword, trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
const submit = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
await updateUserPassword(password.oldPassword, password.newPassword)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
})
|
||||
}
|
||||
const reset = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.resetFields()
|
||||
}
|
||||
</script>
|
||||
39
src/views/Profile/components/UserAvatar.vue
Normal file
39
src/views/Profile/components/UserAvatar.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="change-avatar">
|
||||
<CropperAvatar
|
||||
ref="cropperRef"
|
||||
:btnProps="{ preIcon: 'ant-design:cloud-upload-outlined' }"
|
||||
:showBtn="false"
|
||||
:value="avatar"
|
||||
width="120px"
|
||||
@change="handelUpload"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" name="UserAvatar" setup>
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { uploadAvatar } from '@/api/system/user/profile'
|
||||
|
||||
const props = defineProps({
|
||||
img: propTypes.string.def('')
|
||||
})
|
||||
const avatar = computed(() => {
|
||||
return props.img
|
||||
})
|
||||
|
||||
const cropperRef = ref()
|
||||
const handelUpload = async ({ data }) => {
|
||||
await uploadAvatar({ avatarFile: data })
|
||||
cropperRef.value.close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.change-avatar {
|
||||
img {
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
94
src/views/Profile/components/UserSocial.vue
Normal file
94
src/views/Profile/components/UserSocial.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<el-table :data="socialUsers" :show-header="false">
|
||||
<el-table-column fixed="left" title="序号" type="seq" width="60" />
|
||||
<el-table-column align="left" label="社交平台" width="120">
|
||||
<template #default="{ row }">
|
||||
<img :src="row.img" alt="" class="h-5 align-middle" />
|
||||
<p class="mr-5">{{ row.title }}</p>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="操作">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.openid">
|
||||
已绑定
|
||||
<XTextButton class="mr-5" title="(解绑)" type="primary" @click="unbind(row)" />
|
||||
</template>
|
||||
<template v-else>
|
||||
未绑定
|
||||
<XTextButton class="mr-5" title="(绑定)" type="primary" @click="bind(row)" />
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<script lang="ts" name="UserSocial" setup>
|
||||
import { SystemUserSocialTypeEnum } from '@/utils/constants'
|
||||
import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
|
||||
import { socialAuthRedirect, socialBind, socialUnbind } from '@/api/system/user/socialUser'
|
||||
|
||||
const message = useMessage()
|
||||
const socialUsers = ref<any[]>([])
|
||||
const userInfo = ref<ProfileVO>()
|
||||
|
||||
const initSocial = async () => {
|
||||
const res = await getUserProfile()
|
||||
userInfo.value = res
|
||||
for (const i in SystemUserSocialTypeEnum) {
|
||||
const socialUser = { ...SystemUserSocialTypeEnum[i] }
|
||||
socialUsers.value.push(socialUser)
|
||||
if (userInfo.value?.socialUsers) {
|
||||
for (const j in userInfo.value.socialUsers) {
|
||||
if (socialUser.type === userInfo.value.socialUsers[j].type) {
|
||||
socialUser.openid = userInfo.value.socialUsers[j].openid
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const route = useRoute()
|
||||
const bindSocial = () => {
|
||||
// 社交绑定
|
||||
const type = route.query.type
|
||||
const code = route.query.code
|
||||
const state = route.query.state
|
||||
if (!code) {
|
||||
return
|
||||
}
|
||||
socialBind(type, code, state).then(() => {
|
||||
message.success('绑定成功')
|
||||
initSocial()
|
||||
})
|
||||
}
|
||||
const bind = (row) => {
|
||||
message.info('暂未开放,敬请期待')
|
||||
return
|
||||
const redirectUri = location.origin + '/user/profile?type=' + row.type
|
||||
// 进行跳转
|
||||
socialAuthRedirect(row.type, encodeURIComponent(redirectUri)).then((res) => {
|
||||
window.location.href = res
|
||||
})
|
||||
}
|
||||
const unbind = async (row) => {
|
||||
const res = await socialUnbind(row.type, row.openid)
|
||||
if (res) {
|
||||
row.openid = undefined
|
||||
}
|
||||
message.success('解绑成功')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initSocial()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route,
|
||||
(newRoute) => {
|
||||
bindSocial()
|
||||
console.log(newRoute)
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
7
src/views/Profile/components/index.ts
Normal file
7
src/views/Profile/components/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import BasicInfo from './BasicInfo.vue'
|
||||
import ProfileUser from './ProfileUser.vue'
|
||||
import ResetPwd from './ResetPwd.vue'
|
||||
import UserAvatarVue from './UserAvatar.vue'
|
||||
import UserSocial from './UserSocial.vue'
|
||||
|
||||
export { BasicInfo, ProfileUser, ResetPwd, UserAvatarVue, UserSocial }
|
||||
26
src/views/Redirect/Redirect.vue
Normal file
26
src/views/Redirect/Redirect.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
<script setup lang="ts" name="Redirect">
|
||||
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