项目初始化
This commit is contained in:
146
web/src/views/frontend/index.vue
Normal file
146
web/src/views/frontend/index.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div>
|
||||
<Header />
|
||||
<el-container class="container">
|
||||
<el-main class="main">
|
||||
<div class="main-container">
|
||||
<div class="main-left">
|
||||
<div class="main-title">{{ siteConfig.siteName }}</div>
|
||||
<div class="main-content">
|
||||
{{ $t('index.Steve Jobs') }}
|
||||
</div>
|
||||
<el-button
|
||||
v-if="memberCenter.state.open"
|
||||
@click="$router.push(memberCenterBaseRoutePath)"
|
||||
class="container-button"
|
||||
color="#ffffff"
|
||||
size="large"
|
||||
>
|
||||
{{ $t('Member Center') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="main-right">
|
||||
<img :src="indexCover" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import indexCover from '/@/assets/index/index-cover.svg'
|
||||
import { useSiteConfig } from '/@/stores/siteConfig'
|
||||
import { useMemberCenter } from '/@/stores/memberCenter'
|
||||
import Header from '/@/layouts/frontend/components/header.vue'
|
||||
import Footer from '/@/layouts/frontend/components/footer.vue'
|
||||
import { memberCenterBaseRoutePath } from '/@/router/static/memberCenterBase'
|
||||
|
||||
const siteConfig = useSiteConfig()
|
||||
const memberCenter = useMemberCenter()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.container-button {
|
||||
margin: 0 15px 15px 0;
|
||||
}
|
||||
.container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: url(/@/assets/bg.jpg) repeat;
|
||||
color: var(--el-color-white);
|
||||
.main {
|
||||
height: calc(100vh - 120px);
|
||||
padding: 0;
|
||||
.main-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 66%;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.main-left {
|
||||
padding-right: 50px;
|
||||
.main-title {
|
||||
font-size: 45px;
|
||||
}
|
||||
.main-content {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 40px;
|
||||
font-size: var(--el-font-size-large);
|
||||
}
|
||||
}
|
||||
.main-right {
|
||||
img {
|
||||
width: 380px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.header {
|
||||
background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
:deep(.header-logo) {
|
||||
span {
|
||||
padding-left: 4px;
|
||||
color: var(--el-color-white);
|
||||
}
|
||||
}
|
||||
:deep(.frontend-header-menu) {
|
||||
background: transparent;
|
||||
.el-menu-item,
|
||||
.el-sub-menu .el-sub-menu__title {
|
||||
color: var(--el-color-white);
|
||||
&.is-active {
|
||||
color: var(--el-color-white) !important;
|
||||
}
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
color: var(--el-menu-hover-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.footer {
|
||||
color: var(--el-text-color-secondary);
|
||||
background-color: transparent !important;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.container {
|
||||
.main {
|
||||
height: unset;
|
||||
}
|
||||
}
|
||||
.main-container {
|
||||
width: 90% !important;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
justify-content: center !important;
|
||||
.main-right {
|
||||
padding-top: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 375px) {
|
||||
.main-right img {
|
||||
width: 300px !important;
|
||||
}
|
||||
}
|
||||
@media screen and (max-height: 650px) {
|
||||
.main-right img {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@at-root html.dark {
|
||||
.container {
|
||||
background: url(/@/assets/bg-dark.jpg) repeat;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
122
web/src/views/frontend/user/account/balance.vue
Normal file
122
web/src/views/frontend/user/account/balance.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="user-views">
|
||||
<el-card class="user-views-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('user.account.balance.Balance change record') }}</span>
|
||||
<span class="right-title">{{ $t('user.account.balance.Current balance') + ' ' + userInfo.money }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-loading="state.pageLoading" class="logs">
|
||||
<div class="log-item" v-for="(item, idx) in state.logs" :key="idx">
|
||||
<div class="log-title">{{ item.memo }}</div>
|
||||
<div v-if="item.money > 0" class="log-change-amount increase">{{ $t('Balance') + ':+' + item.money }}</div>
|
||||
<div v-else class="log-change-amount reduce">{{ $t('Balance') + ':' + item.money }}</div>
|
||||
<div class="log-after">{{ $t('user.account.balance.Balance after change') + ':' + item.after }}</div>
|
||||
<div class="log-change-time">{{ $t('user.account.balance.Change time') + ':' + timeFormat(item.create_time) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="state.total > 0" class="log-footer">
|
||||
<el-pagination
|
||||
:currentPage="state.currentPage"
|
||||
:page-size="state.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
background
|
||||
:layout="memberCenter.state.shrink ? 'prev, next, jumper' : 'sizes, ->, prev, pager, next, jumper'"
|
||||
:total="state.total"
|
||||
@size-change="onTableSizeChange"
|
||||
@current-change="onTableCurrentChange"
|
||||
></el-pagination>
|
||||
</div>
|
||||
<el-empty v-else />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted } from 'vue'
|
||||
import { getBalanceLog } from '/@/api/frontend/user/index'
|
||||
import { useMemberCenter } from '/@/stores/memberCenter'
|
||||
import { timeFormat } from '/@/utils/common'
|
||||
import { useUserInfo } from '/@/stores/userInfo'
|
||||
|
||||
const userInfo = useUserInfo()
|
||||
const memberCenter = useMemberCenter()
|
||||
const state: {
|
||||
logs: {
|
||||
memo: string
|
||||
create_time: number
|
||||
money: number
|
||||
after: number
|
||||
}[]
|
||||
currentPage: number
|
||||
total: number
|
||||
pageSize: number
|
||||
pageLoading: boolean
|
||||
} = reactive({
|
||||
logs: [],
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
pageSize: 10,
|
||||
pageLoading: true,
|
||||
})
|
||||
|
||||
const onTableSizeChange = (val: number) => {
|
||||
state.pageSize = val
|
||||
loadData()
|
||||
}
|
||||
const onTableCurrentChange = (val: number) => {
|
||||
state.currentPage = val
|
||||
loadData()
|
||||
}
|
||||
|
||||
const loadData = () => {
|
||||
getBalanceLog(state.currentPage, state.pageSize).then((res) => {
|
||||
state.pageLoading = false
|
||||
state.logs = res.data.list
|
||||
state.total = res.data.total
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.user-views-card :deep(.el-card__body) {
|
||||
padding-top: 0;
|
||||
}
|
||||
.right-title {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.log-item {
|
||||
border-bottom: 1px solid var(--ba-bg-color);
|
||||
padding: 15px 0;
|
||||
div {
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
.log-title {
|
||||
font-size: var(--el-font-size-medium);
|
||||
}
|
||||
.log-change-amount.increase {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
.log-change-amount.reduce {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
.log-after,
|
||||
.log-change-time {
|
||||
font-size: var(--el-font-size-small);
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.log-footer {
|
||||
padding-top: 20px;
|
||||
}
|
||||
</style>
|
||||
116
web/src/views/frontend/user/account/changePassword.vue
Normal file
116
web/src/views/frontend/user/account/changePassword.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="user-views">
|
||||
<el-card class="user-views-card" shadow="hover" :header="t('user.account.changePassword.Change Password')">
|
||||
<div class="change-password">
|
||||
<el-form :model="state.form" :rules="state.rules" label-position="top" ref="formRef" @keyup.enter="onSubmit()">
|
||||
<FormItem
|
||||
:label="t('user.account.changePassword.Old password')"
|
||||
type="password"
|
||||
v-model="state.form.oldPassword"
|
||||
prop="oldPassword"
|
||||
:input-attr="{ showPassword: true }"
|
||||
:placeholder="t('user.account.changePassword.Please enter your current password')"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('user.account.changePassword.New password')"
|
||||
type="password"
|
||||
v-model="state.form.newPassword"
|
||||
prop="newPassword"
|
||||
:input-attr="{ showPassword: true }"
|
||||
:placeholder="t('Please input field', { field: t('user.account.changePassword.New password') })"
|
||||
/>
|
||||
<FormItem
|
||||
:label="t('user.account.changePassword.Confirm new password')"
|
||||
type="password"
|
||||
v-model="state.form.confirmPassword"
|
||||
prop="confirmPassword"
|
||||
:input-attr="{
|
||||
showPassword: true,
|
||||
}"
|
||||
:placeholder="t('Please input field', { field: t('user.account.changePassword.Confirm new password') })"
|
||||
/>
|
||||
<el-form-item class="submit-buttons">
|
||||
<el-button @click="onResetForm(formRef)">{{ $t('Reset') }}</el-button>
|
||||
<el-button type="primary" :loading="state.formSubmitLoading" @click="onSubmit()">{{ $t('Save') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, useTemplateRef } from 'vue'
|
||||
import { onResetForm } from '/@/utils/common'
|
||||
import { buildValidatorData } from '/@/utils/validate'
|
||||
import { changePassword } from '/@/api/frontend/user/index'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import { useUserInfo } from '/@/stores/userInfo'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const userInfo = useUserInfo()
|
||||
const formRef = useTemplateRef('formRef')
|
||||
|
||||
const state = reactive({
|
||||
formSubmitLoading: false,
|
||||
form: {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
},
|
||||
rules: {
|
||||
oldPassword: [buildValidatorData({ name: 'required', title: t('user.account.changePassword.Old password') })],
|
||||
newPassword: [
|
||||
buildValidatorData({ name: 'required', title: t('user.account.changePassword.New password') }),
|
||||
buildValidatorData({ name: 'password' }),
|
||||
],
|
||||
confirmPassword: [
|
||||
buildValidatorData({ name: 'required', title: t('user.account.changePassword.Confirm new password') }),
|
||||
buildValidatorData({ name: 'password' }),
|
||||
{
|
||||
validator: (rule: any, val: string, callback: Function) => {
|
||||
if (state.form.newPassword || state.form.confirmPassword) {
|
||||
if (state.form.newPassword == state.form.confirmPassword) {
|
||||
callback()
|
||||
} else {
|
||||
callback(new Error(t('user.account.changePassword.The duplicate password does not match the new password')))
|
||||
}
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = () => {
|
||||
formRef.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
state.formSubmitLoading = true
|
||||
changePassword(state.form)
|
||||
.then((res) => {
|
||||
state.formSubmitLoading = false
|
||||
if (res.code == 1) {
|
||||
userInfo.logout()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
state.formSubmitLoading = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.change-password {
|
||||
width: 360px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.submit-buttons :deep(.el-form-item__content) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
124
web/src/views/frontend/user/account/integral.vue
Normal file
124
web/src/views/frontend/user/account/integral.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div class="user-views">
|
||||
<el-card class="user-views-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('user.account.integral.Score change record') }}</span>
|
||||
<span class="right-title">{{ $t('user.account.integral.Current points') + ' ' + userInfo.score }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-loading="state.pageLoading" class="logs">
|
||||
<div class="log-item" v-for="(item, idx) in state.logs" :key="idx">
|
||||
<div class="log-title">{{ item.memo }}</div>
|
||||
<div v-if="item.score > 0" class="log-change-amount increase">
|
||||
{{ $t('Integral') + ':+' + item.score }}
|
||||
</div>
|
||||
<div v-else class="log-change-amount reduce">{{ $t('Integral') + ':' + item.score }}</div>
|
||||
<div class="log-after">{{ $t('user.account.integral.Points after change') + ':' + item.after }}</div>
|
||||
<div class="log-change-time">{{ $t('user.account.integral.Change time') + ':' + timeFormat(item.create_time) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="state.total > 0" class="log-footer">
|
||||
<el-pagination
|
||||
:currentPage="state.currentPage"
|
||||
:page-size="state.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
background
|
||||
:layout="memberCenter.state.shrink ? 'prev, next, jumper' : 'sizes, ->, prev, pager, next, jumper'"
|
||||
:total="state.total"
|
||||
@size-change="onTableSizeChange"
|
||||
@current-change="onTableCurrentChange"
|
||||
></el-pagination>
|
||||
</div>
|
||||
<el-empty v-else />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted } from 'vue'
|
||||
import { getIntegralLog } from '/@/api/frontend/user/index'
|
||||
import { useMemberCenter } from '/@/stores/memberCenter'
|
||||
import { timeFormat } from '/@/utils/common'
|
||||
import { useUserInfo } from '/@/stores/userInfo'
|
||||
|
||||
const userInfo = useUserInfo()
|
||||
const memberCenter = useMemberCenter()
|
||||
const state: {
|
||||
logs: {
|
||||
memo: string
|
||||
create_time: number
|
||||
score: number
|
||||
after: number
|
||||
}[]
|
||||
currentPage: number
|
||||
total: number
|
||||
pageSize: number
|
||||
pageLoading: boolean
|
||||
} = reactive({
|
||||
logs: [],
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
pageSize: 10,
|
||||
pageLoading: true,
|
||||
})
|
||||
|
||||
const onTableSizeChange = (val: number) => {
|
||||
state.pageSize = val
|
||||
loadData()
|
||||
}
|
||||
const onTableCurrentChange = (val: number) => {
|
||||
state.currentPage = val
|
||||
loadData()
|
||||
}
|
||||
|
||||
const loadData = () => {
|
||||
getIntegralLog(state.currentPage, state.pageSize).then((res) => {
|
||||
state.pageLoading = false
|
||||
state.logs = res.data.list
|
||||
state.total = res.data.total
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.user-views-card :deep(.el-card__body) {
|
||||
padding-top: 0;
|
||||
}
|
||||
.right-title {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.log-item {
|
||||
border-bottom: 1px solid var(--ba-bg-color);
|
||||
padding: 15px 0;
|
||||
div {
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
.log-title {
|
||||
font-size: var(--el-font-size-medium);
|
||||
}
|
||||
.log-change-amount.increase {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
.log-change-amount.reduce {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
.log-after,
|
||||
.log-change-time {
|
||||
font-size: var(--el-font-size-small);
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.log-footer {
|
||||
padding-top: 20px;
|
||||
}
|
||||
</style>
|
||||
299
web/src/views/frontend/user/account/overview.vue
Normal file
299
web/src/views/frontend/user/account/overview.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<div class="user-views">
|
||||
<el-card class="user-views-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('user.account.overview.Account information') }}</span>
|
||||
<el-button @click="router.push({ name: 'account/profile' })" type="info" v-blur plain>
|
||||
{{ $t('user.account.overview.profile') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="overview-userinfo">
|
||||
<div class="user-avatar">
|
||||
<img :src="fullUrl(userInfo.avatar)" alt="" />
|
||||
<div class="user-avatar-icons">
|
||||
<div @click="router.push({ name: 'account/profile' })" class="avatar-icon-item">
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
placement="right"
|
||||
:content="
|
||||
(userInfo.mobile ? $t('user.account.overview.Filled in') : $t('user.account.overview.Not filled in')) +
|
||||
$t('user.account.overview.mobile')
|
||||
"
|
||||
>
|
||||
<Icon
|
||||
name="fa fa-tablet"
|
||||
size="16"
|
||||
:color="userInfo.mobile ? 'var(--el-color-primary)' : 'var(--el-text-color-secondary)'"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div @click="router.push({ name: 'account/profile' })" class="avatar-icon-item">
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
placement="right"
|
||||
:content="
|
||||
(userInfo.email ? $t('user.account.overview.Filled in') : $t('user.account.overview.Not filled in')) +
|
||||
$t('user.account.overview.email')
|
||||
"
|
||||
>
|
||||
<Icon
|
||||
name="fa fa-envelope-square"
|
||||
size="14"
|
||||
:color="userInfo.email ? 'var(--el-color-primary)' : 'var(--el-text-color-secondary)'"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-data">
|
||||
<div class="welcome-words">{{ userInfo.nickname + $t('utils.comma') + getGreet() }}</div>
|
||||
<el-row class="data-item">
|
||||
<el-col :span="4">{{ $t('Integral') }}</el-col>
|
||||
<el-col :span="8">
|
||||
<el-link @click="router.push({ name: 'account/integral' })" type="primary">{{ userInfo.score }}</el-link>
|
||||
</el-col>
|
||||
<el-col :span="4">{{ $t('Balance') }}</el-col>
|
||||
<el-col :span="8">
|
||||
<el-link @click="router.push({ name: 'account/balance' })" type="primary">{{ userInfo.money }}</el-link>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="data-item">
|
||||
<el-col class="lastlogin title" :span="4">{{ $t('user.account.overview.Last login') }}</el-col>
|
||||
<el-col class="lastlogin value" :span="8">{{ timeFormat(userInfo.last_login_time) }}</el-col>
|
||||
<el-col class="lastip" :span="4">{{ $t('user.account.overview.Last login IP') }}</el-col>
|
||||
<el-col class="lastip" :span="8">{{ userInfo.last_login_ip }}</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="user-views-card" shadow="hover" :header="$t('user.account.overview.Growth statistics')">
|
||||
<div class="account-growth" ref="accountGrowthChartRef"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import * as echarts from 'echarts'
|
||||
import { nextTick, onActivated, onBeforeMount, onMounted, reactive, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { overview } from '/@/api/frontend/user/index'
|
||||
import { useUserInfo } from '/@/stores/userInfo'
|
||||
import { fullUrl, getGreet, timeFormat } from '/@/utils/common'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const userInfo = useUserInfo()
|
||||
const accountGrowthChartRef = useTemplateRef('accountGrowthChartRef')
|
||||
|
||||
const state: {
|
||||
days: string[]
|
||||
score: number[]
|
||||
money: number[]
|
||||
charts: any[]
|
||||
} = reactive({
|
||||
days: [],
|
||||
score: [],
|
||||
money: [],
|
||||
charts: [],
|
||||
})
|
||||
|
||||
const initUserGrowthChart = () => {
|
||||
const userGrowthChart = echarts.init(accountGrowthChartRef.value!)
|
||||
const option = {
|
||||
grid: {
|
||||
top: 40,
|
||||
right: 0,
|
||||
bottom: 20,
|
||||
left: 50,
|
||||
},
|
||||
xAxis: {
|
||||
data: state.days,
|
||||
},
|
||||
yAxis: {},
|
||||
legend: {
|
||||
data: [t('Integral'), t('Balance')],
|
||||
top: 0,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: t('Integral'),
|
||||
data: state.score,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
show: false,
|
||||
color: '#f56c6c',
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
areaStyle: {},
|
||||
},
|
||||
{
|
||||
name: t('Balance'),
|
||||
data: state.money,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
show: false,
|
||||
color: '#409eff',
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.4,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
userGrowthChart.setOption(option)
|
||||
state.charts.push(userGrowthChart)
|
||||
}
|
||||
|
||||
const echartsResize = () => {
|
||||
nextTick(() => {
|
||||
for (const key in state.charts) {
|
||||
state.charts[key].resize()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
echartsResize()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
overview().then((res) => {
|
||||
state.days = res.data.days
|
||||
state.score = res.data.score
|
||||
state.money = res.data.money
|
||||
initUserGrowthChart()
|
||||
})
|
||||
useEventListener(window, 'resize', echartsResize)
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
for (const key in state.charts) {
|
||||
state.charts[key].dispose()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.overview-userinfo {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
overflow: hidden;
|
||||
.user-avatar {
|
||||
width: 100px;
|
||||
padding: 0 20px;
|
||||
margin: 20px 0;
|
||||
border-right: 1px solid var(--el-border-color-light);
|
||||
img {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
.user-avatar-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 4px;
|
||||
}
|
||||
.avatar-icon-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 50%;
|
||||
margin: 3px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
border: 1px solid var(--el-color-primary);
|
||||
}
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
.user-data {
|
||||
padding: 0 20px;
|
||||
margin: 20px 0;
|
||||
width: calc(100% - 100px);
|
||||
}
|
||||
.welcome-words {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: var(--el-font-size-medium);
|
||||
padding: 20px 0;
|
||||
}
|
||||
.data-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--el-font-size-base);
|
||||
padding: 3px 0;
|
||||
}
|
||||
}
|
||||
.account-growth {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
@media screen and (max-width: 992px) {
|
||||
.user-data {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
.overview-userinfo .welcome-words {
|
||||
padding-top: 0;
|
||||
}
|
||||
.user-avatar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1280px) and (min-width: 992px) {
|
||||
.lastip {
|
||||
display: none;
|
||||
}
|
||||
.lastlogin.title {
|
||||
width: 42%;
|
||||
max-width: 42%;
|
||||
flex: 0 0 42%;
|
||||
}
|
||||
.lastlogin.value {
|
||||
width: 58%;
|
||||
max-width: 58%;
|
||||
flex: 0 0 58%;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 460px) {
|
||||
.lastip {
|
||||
display: none;
|
||||
}
|
||||
.lastlogin.title {
|
||||
width: 42%;
|
||||
max-width: 42%;
|
||||
flex: 0 0 42%;
|
||||
}
|
||||
.lastlogin.value {
|
||||
width: 58%;
|
||||
max-width: 58%;
|
||||
flex: 0 0 58%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
533
web/src/views/frontend/user/account/profile.vue
Normal file
533
web/src/views/frontend/user/account/profile.vue
Normal file
@@ -0,0 +1,533 @@
|
||||
<template>
|
||||
<div class="user-views">
|
||||
<el-card class="user-views-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('user.account.profile.profile') }}</span>
|
||||
<el-button @click="router.push({ name: 'account/changePassword' })" type="info" v-blur plain>
|
||||
{{ $t('user.account.profile.Change Password') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="user-profile">
|
||||
<el-form
|
||||
:label-position="memberCenter.state.shrink ? 'top' : 'right'"
|
||||
:model="state.form"
|
||||
:rules="state.rules"
|
||||
:label-width="100"
|
||||
ref="formRef"
|
||||
@keyup.enter="onSubmit()"
|
||||
>
|
||||
<FormItem
|
||||
:label="$t('user.account.profile.avatar')"
|
||||
:input-attr="{
|
||||
hideSelectFile: true,
|
||||
}"
|
||||
type="image"
|
||||
v-model="state.form.avatar"
|
||||
prop="avatar"
|
||||
/>
|
||||
<FormItem
|
||||
:label="$t('user.account.profile.User name')"
|
||||
type="string"
|
||||
v-model="state.form.username"
|
||||
:placeholder="$t('Please input field', { field: $t('user.account.profile.User name') })"
|
||||
prop="username"
|
||||
/>
|
||||
<FormItem
|
||||
:label="$t('user.account.profile.User nickname')"
|
||||
type="string"
|
||||
v-model="state.form.nickname"
|
||||
:placeholder="$t('Please input field', { field: $t('user.account.profile.User nickname') })"
|
||||
prop="nickname"
|
||||
/>
|
||||
<el-form-item v-if="state.accountVerificationType.includes('email')" :label="t('user.account.profile.email')">
|
||||
<el-input v-model="state.form.email" readonly :placeholder="t('user.account.profile.Operation via right button')">
|
||||
<template #append>
|
||||
<el-button type="primary" @click="onChangeBindInfo('email')">
|
||||
{{ state.form.email ? t('user.account.profile.Click Modify') : t('user.account.profile.bind') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="state.accountVerificationType.includes('mobile')" :label="t('user.account.profile.mobile')">
|
||||
<el-input v-model="state.form.mobile" readonly :placeholder="t('user.account.profile.Operation via right button')">
|
||||
<template #append>
|
||||
<el-button type="primary" @click="onChangeBindInfo('mobile')">
|
||||
{{ state.form.mobile ? t('user.account.profile.Click Modify') : t('user.account.profile.bind') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<FormItem
|
||||
:label="$t('user.account.profile.Gender')"
|
||||
type="radio"
|
||||
v-model="state.form.gender"
|
||||
:input-attr="{
|
||||
border: true,
|
||||
content: {
|
||||
'0': $t('user.account.profile.secrecy'),
|
||||
'1': $t('user.account.profile.male'),
|
||||
'2': $t('user.account.profile.female'),
|
||||
},
|
||||
}"
|
||||
/>
|
||||
<FormItem :label="$t('user.account.profile.birthday')" type="date" v-model="state.form.birthday" />
|
||||
<FormItem
|
||||
:label="$t('user.account.profile.Personal signature')"
|
||||
type="textarea"
|
||||
:placeholder="$t('Please input field', { field: $t('user.account.profile.Personal signature') })"
|
||||
v-model="state.form.motto"
|
||||
:input-attr="{ showWordLimit: true, maxlength: 120, rows: 3 }"
|
||||
/>
|
||||
<UserProfileMixin />
|
||||
<el-form-item class="submit-buttons">
|
||||
<el-button @click="onResetForm(formRef)">{{ $t('Reset') }}</el-button>
|
||||
<el-button type="primary" :loading="state.formSubmitLoading" @click="onSubmit()">{{ $t('Save') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 账户验证 -->
|
||||
<el-dialog
|
||||
:title="t('user.account.profile.Account verification')"
|
||||
v-model="state.dialog.verification.show"
|
||||
class="ba-change-bind-dialog ba-verification-dialog"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
width="30%"
|
||||
>
|
||||
<el-form
|
||||
:model="state.dialog.verification.form"
|
||||
:rules="state.dialog.verification.rules"
|
||||
:label-position="'top'"
|
||||
ref="verificationFormRef"
|
||||
@keyup.enter="onSubmitVerification()"
|
||||
>
|
||||
<FormItem
|
||||
:label="t('user.account.profile.Account password verification')"
|
||||
type="password"
|
||||
v-model="state.dialog.verification.form.password"
|
||||
prop="password"
|
||||
:input-attr="{ showPassword: true }"
|
||||
:placeholder="$t('Please input field', { field: $t('user.account.profile.password') })"
|
||||
/>
|
||||
<el-form-item prop="captcha">
|
||||
<template #label>
|
||||
<span v-if="state.dialog.type == 'email'">
|
||||
{{ t('user.account.profile.Mail verification') }}
|
||||
({{ t('user.account.profile.accept') + t('user.account.profile.mail') + ':' + userInfo.email }})
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('user.account.profile.SMS verification') }}
|
||||
({{ t('user.account.profile.accept') + t('user.account.profile.mobile') + ':' + userInfo.mobile }})
|
||||
</span>
|
||||
</template>
|
||||
<el-row class="w100" :gutter="10">
|
||||
<el-col :span="18">
|
||||
<el-input
|
||||
v-model="state.dialog.verification.form.captcha"
|
||||
:placeholder="t('Please input field', { field: t('user.account.profile.Verification Code') })"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col class="captcha-box" :span="6">
|
||||
<el-button
|
||||
@click="sendVerificationCaptchaPre"
|
||||
:loading="state.dialog.sendCaptchaLoading"
|
||||
:disabled="state.dialog.codeSendCountdown <= 0 ? false : true"
|
||||
type="primary"
|
||||
>
|
||||
{{
|
||||
state.dialog.codeSendCountdown <= 0
|
||||
? t('user.account.profile.send')
|
||||
: state.dialog.codeSendCountdown + t('user.account.profile.seconds')
|
||||
}}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div :style="'width: calc(100% - 20px)'">
|
||||
<el-button @click="state.dialog.verification.show = false">{{ t('Cancel') }}</el-button>
|
||||
<el-button v-blur :loading="state.dialog.submitLoading" @click="onSubmitVerification()" type="primary">
|
||||
{{ t('user.account.profile.next step') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 绑定 -->
|
||||
<el-dialog
|
||||
:title="t('user.account.profile.bind') + t('user.account.profile.' + state.dialog.type)"
|
||||
v-model="state.dialog.bind.show"
|
||||
class="ba-change-bind-dialog ba-bind-dialog"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
width="30%"
|
||||
>
|
||||
<el-form
|
||||
:model="state.dialog.bind.form"
|
||||
:rules="state.dialog.bind.rules"
|
||||
:label-position="'top'"
|
||||
ref="bindFormRef"
|
||||
@keyup.enter="onSubmitBind()"
|
||||
>
|
||||
<FormItem
|
||||
v-if="!state.dialog.verification.accountVerificationToken"
|
||||
:label="t('user.account.profile.Account password verification')"
|
||||
type="password"
|
||||
v-model="state.dialog.bind.form.password"
|
||||
prop="password"
|
||||
:input-attr="{ showPassword: true }"
|
||||
:placeholder="$t('Please input field', { field: $t('user.account.profile.password') })"
|
||||
/>
|
||||
<FormItem
|
||||
v-if="state.dialog.type == 'email'"
|
||||
:label="t('user.account.profile.New ' + state.dialog.type)"
|
||||
type="string"
|
||||
v-model="state.dialog.bind.form.email"
|
||||
prop="email"
|
||||
:placeholder="$t('Please input field', { field: t('user.account.profile.New ' + state.dialog.type) })"
|
||||
/>
|
||||
<FormItem
|
||||
v-if="state.dialog.type == 'mobile'"
|
||||
:label="t('user.account.profile.New ' + state.dialog.type)"
|
||||
type="string"
|
||||
v-model="state.dialog.bind.form.mobile"
|
||||
prop="mobile"
|
||||
:placeholder="$t('Please input field', { field: t('user.account.profile.New ' + state.dialog.type) })"
|
||||
/>
|
||||
<el-form-item
|
||||
:label="state.dialog.type == 'email' ? t('user.account.profile.Mail verification') : t('user.account.profile.SMS verification')"
|
||||
prop="captcha"
|
||||
>
|
||||
<el-row class="w100" :gutter="10">
|
||||
<el-col :span="18">
|
||||
<el-input
|
||||
v-model="state.dialog.bind.form.captcha"
|
||||
:placeholder="t('Please input field', { field: t('user.account.profile.Verification Code') })"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col class="captcha-box" :span="6">
|
||||
<el-button
|
||||
@click="sendBindCaptchaPre"
|
||||
:loading="state.dialog.sendCaptchaLoading"
|
||||
:disabled="state.dialog.codeSendCountdown <= 0 ? false : true"
|
||||
type="primary"
|
||||
>
|
||||
{{
|
||||
state.dialog.codeSendCountdown <= 0
|
||||
? t('user.account.profile.send')
|
||||
: state.dialog.codeSendCountdown + t('user.account.profile.seconds')
|
||||
}}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div :style="'width: calc(100% - 20px)'">
|
||||
<el-button @click="state.dialog.bind.show = false">{{ t('Cancel') }}</el-button>
|
||||
<el-button v-blur :loading="state.dialog.submitLoading" @click="onSubmitBind()" type="primary">
|
||||
{{ t('user.account.profile.bind') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, useTemplateRef } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { FormItemRule } from 'element-plus'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import { useUserInfo } from '/@/stores/userInfo'
|
||||
import { onResetForm } from '/@/utils/common'
|
||||
import { buildValidatorData } from '/@/utils/validate'
|
||||
import { getProfile, postProfile, postVerification, postChangeBind } from '/@/api/frontend/user/index'
|
||||
import UserProfileMixin from '/@/components/mixins/userProfile.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { sendEms, sendSms } from '/@/api/common'
|
||||
import { uuid } from '/@/utils/random'
|
||||
import clickCaptcha from '/@/components/clickCaptcha'
|
||||
import { useMemberCenter } from '/@/stores/memberCenter'
|
||||
let timer: number
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const userInfo = useUserInfo()
|
||||
const memberCenter = useMemberCenter()
|
||||
|
||||
const formRef = useTemplateRef('formRef')
|
||||
const bindFormRef = useTemplateRef('bindFormRef')
|
||||
const verificationFormRef = useTemplateRef('verificationFormRef')
|
||||
|
||||
const state: {
|
||||
formSubmitLoading: boolean
|
||||
form: anyObj
|
||||
rules: Partial<Record<string, FormItemRule[]>>
|
||||
accountVerificationType: string[]
|
||||
dialog: {
|
||||
type: 'email' | 'mobile'
|
||||
submitLoading: boolean
|
||||
sendCaptchaLoading: boolean
|
||||
codeSendCountdown: number
|
||||
captchaId: string
|
||||
verification: {
|
||||
show: boolean
|
||||
rules: Partial<Record<string, FormItemRule[]>>
|
||||
form: {
|
||||
password: string
|
||||
captcha: string
|
||||
}
|
||||
accountVerificationToken: string
|
||||
}
|
||||
bind: {
|
||||
show: boolean
|
||||
rules: Partial<Record<string, FormItemRule[]>>
|
||||
form: {
|
||||
password: string
|
||||
email: string
|
||||
mobile: string
|
||||
captcha: string
|
||||
}
|
||||
}
|
||||
}
|
||||
} = reactive({
|
||||
formSubmitLoading: false,
|
||||
form: userInfo.$state,
|
||||
rules: {
|
||||
username: [buildValidatorData({ name: 'required', title: t('user.account.profile.User name') }), buildValidatorData({ name: 'account' })],
|
||||
nickname: [buildValidatorData({ name: 'required', title: t('user.account.profile.nickname') })],
|
||||
},
|
||||
accountVerificationType: [],
|
||||
dialog: {
|
||||
type: 'email',
|
||||
submitLoading: false,
|
||||
sendCaptchaLoading: false,
|
||||
codeSendCountdown: 0,
|
||||
captchaId: uuid(),
|
||||
verification: {
|
||||
show: false,
|
||||
rules: {
|
||||
password: [
|
||||
buildValidatorData({ name: 'required', title: t('user.account.profile.password') }),
|
||||
buildValidatorData({ name: 'password' }),
|
||||
],
|
||||
captcha: [buildValidatorData({ name: 'required', title: t('user.account.profile.Verification Code') })],
|
||||
},
|
||||
form: {
|
||||
password: '',
|
||||
captcha: '',
|
||||
},
|
||||
accountVerificationToken: '',
|
||||
},
|
||||
bind: {
|
||||
show: false,
|
||||
rules: {
|
||||
password: [
|
||||
buildValidatorData({ name: 'required', title: t('user.account.profile.password') }),
|
||||
buildValidatorData({ name: 'password' }),
|
||||
],
|
||||
email: [
|
||||
buildValidatorData({ name: 'required', title: t('user.account.profile.email') }),
|
||||
buildValidatorData({ name: 'email', title: t('user.account.profile.email') }),
|
||||
],
|
||||
mobile: [
|
||||
buildValidatorData({ name: 'required', title: t('user.account.profile.mobile') }),
|
||||
buildValidatorData({ name: 'mobile', title: t('user.account.profile.mobile') }),
|
||||
],
|
||||
captcha: [buildValidatorData({ name: 'required', title: t('user.account.profile.Verification Code') })],
|
||||
},
|
||||
form: {
|
||||
password: '',
|
||||
email: '',
|
||||
mobile: '',
|
||||
captcha: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const startTiming = (seconds: number) => {
|
||||
state.dialog.codeSendCountdown = seconds
|
||||
timer = window.setInterval(() => {
|
||||
state.dialog.codeSendCountdown--
|
||||
if (state.dialog.codeSendCountdown <= 0) {
|
||||
endTiming()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
const endTiming = () => {
|
||||
state.dialog.codeSendCountdown = 0
|
||||
clearInterval(timer)
|
||||
}
|
||||
|
||||
const onChangeBindInfo = (type: 'email' | 'mobile') => {
|
||||
if ((type == 'email' && userInfo.email) || (type == 'mobile' && userInfo.mobile)) {
|
||||
state.dialog.verification.show = true
|
||||
} else {
|
||||
state.dialog.bind.show = true
|
||||
}
|
||||
state.dialog.type = type
|
||||
}
|
||||
|
||||
const sendVerificationCaptchaPre = () => {
|
||||
if (state.dialog.codeSendCountdown > 0) return
|
||||
verificationFormRef.value!.validateField('password').then((res) => {
|
||||
if (!res) return
|
||||
clickCaptcha(state.dialog.captchaId, (captchaInfo: string) => sendVerificationCaptcha(captchaInfo))
|
||||
})
|
||||
}
|
||||
const sendVerificationCaptcha = (captchaInfo: string) => {
|
||||
state.dialog.sendCaptchaLoading = true
|
||||
const func = state.dialog.type == 'email' ? sendEms : sendSms
|
||||
func(userInfo[state.dialog.type], `user_${state.dialog.type}_verify`, {
|
||||
password: state.dialog.verification.form.password,
|
||||
captchaId: state.dialog.captchaId,
|
||||
captchaInfo,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code == 1) startTiming(60)
|
||||
})
|
||||
.finally(() => {
|
||||
state.dialog.sendCaptchaLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
const sendBindCaptchaPre = () => {
|
||||
if (state.dialog.codeSendCountdown > 0) return
|
||||
bindFormRef.value!.validateField(state.dialog.type).then((res) => {
|
||||
if (!res) return
|
||||
clickCaptcha(state.dialog.captchaId, (captchaInfo: string) => sendBindCaptcha(captchaInfo))
|
||||
})
|
||||
}
|
||||
const sendBindCaptcha = (captchaInfo: string) => {
|
||||
state.dialog.sendCaptchaLoading = true
|
||||
const func = state.dialog.type == 'email' ? sendEms : sendSms
|
||||
func(state.dialog.bind.form[state.dialog.type], `user_change_${state.dialog.type}`, {
|
||||
captchaId: state.dialog.captchaId,
|
||||
captchaInfo,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code == 1) startTiming(60)
|
||||
})
|
||||
.finally(() => {
|
||||
state.dialog.sendCaptchaLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
const onSubmitVerification = () => {
|
||||
verificationFormRef.value?.validate((res) => {
|
||||
if (res) {
|
||||
state.dialog.submitLoading = true
|
||||
postVerification({
|
||||
type: state.dialog.type,
|
||||
captcha: state.dialog.verification.form.captcha,
|
||||
})
|
||||
.then((res) => {
|
||||
endTiming()
|
||||
state.dialog.bind.show = true
|
||||
state.dialog.type = res.data.type
|
||||
state.dialog.verification.show = false
|
||||
state.dialog.verification.accountVerificationToken = res.data.accountVerificationToken
|
||||
})
|
||||
.finally(() => {
|
||||
state.dialog.submitLoading = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onSubmitBind = () => {
|
||||
bindFormRef.value?.validate((res) => {
|
||||
if (res) {
|
||||
state.dialog.submitLoading = true
|
||||
postChangeBind({
|
||||
type: state.dialog.type,
|
||||
accountVerificationToken: state.dialog.verification.accountVerificationToken,
|
||||
...state.dialog.bind.form,
|
||||
})
|
||||
.then(() => {
|
||||
endTiming()
|
||||
state.dialog.bind.show = false
|
||||
userInfo[state.dialog.type] = state.dialog.bind.form[state.dialog.type]
|
||||
})
|
||||
.finally(() => {
|
||||
state.dialog.submitLoading = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
formRef.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
state.formSubmitLoading = true
|
||||
postProfile(state.form)
|
||||
.then(() => {
|
||||
state.formSubmitLoading = false
|
||||
})
|
||||
.catch(() => {
|
||||
state.formSubmitLoading = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getProfile().then((res) => {
|
||||
state.accountVerificationType = res.data.accountVerificationType
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.user-profile {
|
||||
width: 400px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.submit-buttons :deep(.el-form-item__content) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
:deep(.el-upload-list--picture-card) {
|
||||
--el-upload-list-picture-card-size: 100px;
|
||||
}
|
||||
:deep(.el-upload--picture-card) {
|
||||
--el-upload-picture-card-size: 100px;
|
||||
}
|
||||
.captcha-box {
|
||||
margin-left: auto;
|
||||
.el-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
:deep(.ba-verification-dialog) .el-dialog__body {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
:deep(.ba-change-bind-dialog) {
|
||||
--el-dialog-width: 50% !important;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
:deep(.ba-change-bind-dialog) {
|
||||
--el-dialog-width: 70% !important;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 600px) {
|
||||
:deep(.ba-change-bind-dialog) {
|
||||
--el-dialog-width: 92% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
590
web/src/views/frontend/user/login.vue
Normal file
590
web/src/views/frontend/user/login.vue
Normal file
@@ -0,0 +1,590 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<el-container class="is-vertical">
|
||||
<Header />
|
||||
<el-main class="frontend-footer-brother">
|
||||
<el-row justify="center">
|
||||
<el-col :span="16" :xs="24">
|
||||
<div v-if="memberCenter.state.open" class="login-box">
|
||||
<div class="login-title">
|
||||
{{ t('user.login.' + state.form.tab) + t('user.login.reach') + siteConfig.siteName }}
|
||||
</div>
|
||||
<el-form ref="formRef" @keyup.enter="onSubmitPre" :rules="rules" :model="state.form">
|
||||
<!-- 注册验证方式 -->
|
||||
<el-form-item v-if="state.form.tab == 'register'">
|
||||
<el-radio-group size="large" v-model="state.form.registerType">
|
||||
<el-radio
|
||||
class="register-verification-radio"
|
||||
value="email"
|
||||
:disabled="!state.accountVerificationType.includes('email')"
|
||||
border
|
||||
>
|
||||
{{ t('user.login.Via email') + t('user.login.register') }}
|
||||
</el-radio>
|
||||
<el-radio
|
||||
class="register-verification-radio"
|
||||
value="mobile"
|
||||
:disabled="!state.accountVerificationType.includes('mobile')"
|
||||
border
|
||||
>
|
||||
{{ t('user.login.Via mobile number') + t('user.login.register') }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 登录注册用户名 -->
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="state.form.username"
|
||||
:placeholder="
|
||||
state.form.tab == 'register'
|
||||
? t('Please input field', { field: t('user.login.User name') })
|
||||
: t('Please input field', { field: t('user.login.account') })
|
||||
"
|
||||
:clearable="true"
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon name="fa fa-user" size="16" color="var(--el-input-icon-color)" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 登录注册密码 -->
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="state.form.password"
|
||||
:placeholder="t('Please input field', { field: t('user.login.password') })"
|
||||
type="password"
|
||||
show-password
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon name="fa fa-unlock-alt" size="16" color="var(--el-input-icon-color)" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 注册手机号 -->
|
||||
<el-form-item v-if="state.form.tab == 'register' && state.form.registerType == 'mobile'" prop="mobile">
|
||||
<el-input
|
||||
v-model="state.form.mobile"
|
||||
:placeholder="t('Please input field', { field: t('user.login.mobile') })"
|
||||
:clearable="true"
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon name="fa fa-tablet" size="16" color="var(--el-input-icon-color)" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 注册邮箱 -->
|
||||
<el-form-item v-if="state.form.tab == 'register' && state.form.registerType == 'email'" prop="email">
|
||||
<el-input
|
||||
v-model="state.form.email"
|
||||
:placeholder="t('Please input field', { field: t('user.login.email') })"
|
||||
:clearable="true"
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon name="fa fa-envelope" size="16" color="var(--el-input-icon-color)" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 注册验证码 -->
|
||||
<el-form-item v-if="state.form.tab == 'register'" prop="captcha">
|
||||
<el-row class="w100">
|
||||
<el-col :span="16">
|
||||
<el-input
|
||||
size="large"
|
||||
v-model="state.form.captcha"
|
||||
:placeholder="t('Please input field', { field: t('user.login.Verification Code') })"
|
||||
autocomplete="off"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon name="fa fa-ellipsis-h" size="16" color="var(--el-input-icon-color)" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col class="captcha-box" :span="8">
|
||||
<el-button
|
||||
size="large"
|
||||
@click="sendRegisterCaptchaPre"
|
||||
:loading="state.sendCaptchaLoading"
|
||||
:disabled="state.codeSendCountdown <= 0 ? false : true"
|
||||
type="primary"
|
||||
>
|
||||
{{
|
||||
state.codeSendCountdown <= 0
|
||||
? t('user.login.send')
|
||||
: state.codeSendCountdown + t('user.login.seconds')
|
||||
}}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<div v-if="state.form.tab != 'register'" class="form-footer">
|
||||
<el-checkbox v-model="state.form.keep" :label="t('user.login.Remember me')" size="default"></el-checkbox>
|
||||
<div
|
||||
v-if="state.accountVerificationType.length > 0"
|
||||
@click="state.showRetrievePasswordDialog = true"
|
||||
class="forgot-password"
|
||||
>
|
||||
{{ t('user.login.Forgot your password?') }}
|
||||
</div>
|
||||
</div>
|
||||
<el-form-item class="form-buttons">
|
||||
<el-button class="login-btn" @click="onSubmitPre" :loading="state.formLoading" round type="primary" size="large">
|
||||
{{ t('user.login.' + state.form.tab) }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="state.form.tab == 'register'"
|
||||
@click="switchTab(formRef, 'login')"
|
||||
round
|
||||
plain
|
||||
type="info"
|
||||
size="large"
|
||||
>
|
||||
{{ t('user.login.Back to login') }}
|
||||
</el-button>
|
||||
<el-button v-else @click="switchTab(formRef, 'register')" round plain type="info" size="large">
|
||||
{{ t('user.login.No account yet? Click Register') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<LoginFooterMixin />
|
||||
</el-form>
|
||||
</div>
|
||||
<el-alert v-else :center="true" :title="$t('Member center disabled')" type="error" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-main>
|
||||
<Footer />
|
||||
</el-container>
|
||||
|
||||
<el-dialog
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
v-model="state.showRetrievePasswordDialog"
|
||||
:title="t('user.login.Retrieve password')"
|
||||
:width="state.dialogWidth + '%'"
|
||||
:draggable="true"
|
||||
>
|
||||
<div class="retrieve-password-form">
|
||||
<el-form
|
||||
ref="retrieveFormRef"
|
||||
@keyup.enter="onSubmitRetrieve()"
|
||||
:rules="retrieveRules"
|
||||
:model="state.retrievePasswordForm"
|
||||
:label-width="100"
|
||||
>
|
||||
<el-form-item :label="t('user.login.Retrieval method')">
|
||||
<el-radio-group v-model="state.retrievePasswordForm.type">
|
||||
<el-radio value="email" :disabled="!state.accountVerificationType.includes('email')" border>
|
||||
{{ t('user.login.Via email') }}
|
||||
</el-radio>
|
||||
<el-radio value="mobile" :disabled="!state.accountVerificationType.includes('mobile')" border>
|
||||
{{ t('user.login.Via mobile number') }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item prop="account" :label="state.retrievePasswordForm.type == 'email' ? t('user.login.email') : t('user.login.mobile')">
|
||||
<el-input
|
||||
v-model="state.retrievePasswordForm.account"
|
||||
:placeholder="
|
||||
t('Please input field', {
|
||||
field: state.retrievePasswordForm.type == 'email' ? t('user.login.email') : t('user.login.mobile'),
|
||||
})
|
||||
"
|
||||
:clearable="true"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon name="fa fa-user" size="16" color="var(--el-input-icon-color)" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="captcha" :label="t('user.login.Verification Code')">
|
||||
<el-row class="w100">
|
||||
<el-col :span="16">
|
||||
<el-input
|
||||
v-model="state.retrievePasswordForm.captcha"
|
||||
:placeholder="t('Please input field', { field: t('user.login.Verification Code') })"
|
||||
autocomplete="off"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon name="fa fa-ellipsis-h" size="16" color="var(--el-input-icon-color)" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col class="captcha-box" :span="8">
|
||||
<el-button
|
||||
@click="sendRetrieveCaptchaPre"
|
||||
:loading="state.sendCaptchaLoading"
|
||||
:disabled="state.codeSendCountdown <= 0 ? false : true"
|
||||
type="primary"
|
||||
>
|
||||
{{ state.codeSendCountdown <= 0 ? t('user.login.send') : state.codeSendCountdown + t('user.login.seconds') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password" :label="t('user.login.New password')">
|
||||
<el-input
|
||||
v-model="state.retrievePasswordForm.password"
|
||||
:placeholder="t('Please input field', { field: t('user.login.New password') })"
|
||||
show-password
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon name="fa fa-unlock-alt" size="16" color="var(--el-input-icon-color)" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="state.showRetrievePasswordDialog = false">{{ t('Cancel') }}</el-button>
|
||||
<el-button :loading="state.submitRetrieveLoading" @click="onSubmitRetrieve()" type="primary">
|
||||
{{ t('user.login.second') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, onUnmounted, useTemplateRef } from 'vue'
|
||||
import Header from '/@/layouts/frontend/components/header.vue'
|
||||
import Footer from '/@/layouts/frontend/components/footer.vue'
|
||||
import { useSiteConfig } from '/@/stores/siteConfig'
|
||||
import { useMemberCenter } from '/@/stores/memberCenter'
|
||||
import { sendEms, sendSms } from '/@/api/common'
|
||||
import { uuid } from '/@/utils/random'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { buildValidatorData, validatorAccount } from '/@/utils/validate'
|
||||
import { checkIn, retrievePassword } from '/@/api/frontend/user/index'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { onResetForm } from '/@/utils/common'
|
||||
import { useUserInfo } from '/@/stores/userInfo'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRoute } from 'vue-router'
|
||||
import loginMounted from '/@/components/mixins/loginMounted'
|
||||
import LoginFooterMixin from '/@/components/mixins/loginFooter.vue'
|
||||
import type { FormItemRule, FormInstance } from 'element-plus'
|
||||
import clickCaptcha from '/@/components/clickCaptcha'
|
||||
let timer: number
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userInfo = useUserInfo()
|
||||
const siteConfig = useSiteConfig()
|
||||
const memberCenter = useMemberCenter()
|
||||
const formRef = useTemplateRef('formRef')
|
||||
const retrieveFormRef = useTemplateRef('retrieveFormRef')
|
||||
|
||||
interface State {
|
||||
form: {
|
||||
tab: 'login' | 'register'
|
||||
email: string
|
||||
mobile: string
|
||||
username: string
|
||||
password: string
|
||||
captcha: string
|
||||
keep: boolean
|
||||
captchaId: string
|
||||
captchaInfo: string
|
||||
registerType: 'email' | 'mobile'
|
||||
}
|
||||
formLoading: boolean
|
||||
showRetrievePasswordDialog: boolean
|
||||
retrievePasswordForm: {
|
||||
type: 'email' | 'mobile'
|
||||
account: string
|
||||
captcha: string
|
||||
password: string
|
||||
}
|
||||
dialogWidth: number
|
||||
userLoginCaptchaSwitch: boolean
|
||||
accountVerificationType: string[]
|
||||
codeSendCountdown: number
|
||||
submitRetrieveLoading: boolean
|
||||
sendCaptchaLoading: boolean
|
||||
to: string
|
||||
}
|
||||
|
||||
const state: State = reactive({
|
||||
form: {
|
||||
tab: 'login',
|
||||
email: '',
|
||||
mobile: '',
|
||||
username: '',
|
||||
password: '',
|
||||
captcha: '',
|
||||
keep: false,
|
||||
captchaId: uuid(),
|
||||
captchaInfo: '',
|
||||
registerType: 'email',
|
||||
},
|
||||
formLoading: false,
|
||||
showRetrievePasswordDialog: false,
|
||||
retrievePasswordForm: {
|
||||
type: 'email',
|
||||
account: '',
|
||||
captcha: '',
|
||||
password: '',
|
||||
},
|
||||
dialogWidth: 36,
|
||||
userLoginCaptchaSwitch: true,
|
||||
accountVerificationType: [],
|
||||
codeSendCountdown: 0,
|
||||
submitRetrieveLoading: false,
|
||||
sendCaptchaLoading: false,
|
||||
// 登录成功后要跳转的URL
|
||||
to: route.query.to as string,
|
||||
})
|
||||
|
||||
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
email: [
|
||||
buildValidatorData({ name: 'required', title: t('user.login.email') }),
|
||||
buildValidatorData({ name: 'email', title: t('user.login.email') }),
|
||||
],
|
||||
username: [
|
||||
buildValidatorData({ name: 'required', title: t('user.login.User name') }),
|
||||
{
|
||||
validator: (rule: any, val: string, callback: Function) => {
|
||||
if (state.form.tab == 'register') {
|
||||
return validatorAccount(rule, val, callback)
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
password: [buildValidatorData({ name: 'required', title: t('user.login.password') }), buildValidatorData({ name: 'password' })],
|
||||
mobile: [buildValidatorData({ name: 'required', title: t('user.login.mobile') }), buildValidatorData({ name: 'mobile' })],
|
||||
captcha: [buildValidatorData({ name: 'required', title: t('user.login.Verification Code') })],
|
||||
})
|
||||
|
||||
const retrieveRules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
account: [buildValidatorData({ name: 'required', title: t('user.login.Account name') })],
|
||||
captcha: [buildValidatorData({ name: 'required', title: t('user.login.Verification Code') })],
|
||||
password: [buildValidatorData({ name: 'required', title: t('user.login.password') }), buildValidatorData({ name: 'password' })],
|
||||
})
|
||||
|
||||
const resize = () => {
|
||||
let clientWidth = document.documentElement.clientWidth
|
||||
let width = 36
|
||||
if (clientWidth <= 790) {
|
||||
width = 92
|
||||
} else if (clientWidth <= 910) {
|
||||
width = 56
|
||||
} else if (clientWidth <= 1260) {
|
||||
width = 46
|
||||
}
|
||||
state.dialogWidth = width
|
||||
}
|
||||
|
||||
const onSubmitPre = () => {
|
||||
formRef.value?.validate((valid) => {
|
||||
if (!valid) return
|
||||
if (state.form.tab == 'login' && state.userLoginCaptchaSwitch) {
|
||||
clickCaptcha(state.form.captchaId, (captchaInfo: string) => onSubmit(captchaInfo))
|
||||
} else {
|
||||
onSubmit()
|
||||
}
|
||||
})
|
||||
}
|
||||
const onSubmit = (captchaInfo = '') => {
|
||||
state.formLoading = true
|
||||
state.form.captchaInfo = captchaInfo
|
||||
checkIn('post', state.form)
|
||||
.then((res) => {
|
||||
userInfo.dataFill(res.data.userInfo, false)
|
||||
if (state.to) return (location.href = state.to)
|
||||
router.push({ path: res.data.routePath })
|
||||
})
|
||||
.finally(() => {
|
||||
state.formLoading = false
|
||||
})
|
||||
}
|
||||
const onSubmitRetrieve = () => {
|
||||
if (!retrieveFormRef.value) return
|
||||
retrieveFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
state.submitRetrieveLoading = true
|
||||
retrievePassword(state.retrievePasswordForm)
|
||||
.then((res) => {
|
||||
state.submitRetrieveLoading = false
|
||||
if (res.code == 1) {
|
||||
state.showRetrievePasswordDialog = false
|
||||
endTiming()
|
||||
onResetForm(retrieveFormRef.value)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
state.submitRetrieveLoading = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const sendRegisterCaptchaPre = () => {
|
||||
if (state.codeSendCountdown > 0) return
|
||||
formRef.value!.validateField([state.form.registerType, 'username', 'password']).then((valid) => {
|
||||
if (!valid) return
|
||||
clickCaptcha(state.form.captchaId, (captchaInfo: string) => sendRegisterCaptcha(captchaInfo))
|
||||
})
|
||||
}
|
||||
const sendRegisterCaptcha = (captchaInfo: string) => {
|
||||
state.sendCaptchaLoading = true
|
||||
const func = state.form.registerType == 'email' ? sendEms : sendSms
|
||||
func(state.form[state.form.registerType], 'user_register', {
|
||||
captchaInfo,
|
||||
captchaId: state.form.captchaId,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code == 1) startTiming(60)
|
||||
})
|
||||
.finally(() => {
|
||||
state.sendCaptchaLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
const sendRetrieveCaptchaPre = () => {
|
||||
if (state.codeSendCountdown > 0) return
|
||||
retrieveFormRef.value!.validateField('account').then((valid) => {
|
||||
if (!valid) return
|
||||
clickCaptcha(state.form.captchaId, (captchaInfo: string) => sendRetrieveCaptcha(captchaInfo))
|
||||
})
|
||||
}
|
||||
const sendRetrieveCaptcha = (captchaInfo: string) => {
|
||||
state.sendCaptchaLoading = true
|
||||
const func = state.retrievePasswordForm.type == 'email' ? sendEms : sendSms
|
||||
func(state.retrievePasswordForm.account, 'user_retrieve_pwd', {
|
||||
captchaInfo,
|
||||
captchaId: state.form.captchaId,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code == 1) startTiming(60)
|
||||
})
|
||||
.finally(() => {
|
||||
state.sendCaptchaLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
const switchTab = (formRef: FormInstance | null | undefined = undefined, tab: 'login' | 'register') => {
|
||||
state.form.tab = tab
|
||||
if (tab == 'register') state.form.username = ''
|
||||
if (formRef) formRef.clearValidate()
|
||||
}
|
||||
|
||||
const startTiming = (seconds: number) => {
|
||||
state.codeSendCountdown = seconds
|
||||
timer = window.setInterval(() => {
|
||||
state.codeSendCountdown--
|
||||
if (state.codeSendCountdown <= 0) {
|
||||
endTiming()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
const endTiming = () => {
|
||||
state.codeSendCountdown = 0
|
||||
clearInterval(timer)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (await loginMounted()) return
|
||||
|
||||
resize()
|
||||
useEventListener(window, 'resize', resize)
|
||||
|
||||
checkIn('get').then((res) => {
|
||||
state.userLoginCaptchaSwitch = res.data.userLoginCaptchaSwitch
|
||||
state.accountVerificationType = res.data.accountVerificationType
|
||||
state.retrievePasswordForm.type = res.data.accountVerificationType.length > 0 ? res.data.accountVerificationType[0] : ''
|
||||
})
|
||||
|
||||
if (route.query.type == 'register') state.form.tab = 'register'
|
||||
})
|
||||
onUnmounted(() => {
|
||||
state.codeSendCountdown = 0
|
||||
endTiming()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.login-box {
|
||||
width: 460px;
|
||||
margin: 40px auto;
|
||||
padding: 10px 60px 20px 60px;
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
}
|
||||
.login-title {
|
||||
text-align: center;
|
||||
font-size: var(--el-font-size-large);
|
||||
line-height: 100px;
|
||||
user-select: none;
|
||||
}
|
||||
:deep(.el-input--large) .el-input__wrapper {
|
||||
padding: 4px 15px;
|
||||
}
|
||||
.form-buttons {
|
||||
padding-top: 20px;
|
||||
.el-button {
|
||||
width: 100%;
|
||||
letter-spacing: 2px;
|
||||
font-weight: 300;
|
||||
margin-top: 20px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.register-verification-radio {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.captcha-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
.el-button {
|
||||
width: 90%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.form-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.forgot-password {
|
||||
color: var(--ba-color-primary-light);
|
||||
margin-left: auto;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.retrieve-password-form {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-right: 50px;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
.login-box {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.retrieve-password-form {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 暗黑样式
|
||||
@at-root .dark {
|
||||
.form-buttons {
|
||||
.login-btn {
|
||||
--el-button-bg-color: var(--el-color-primary-light-5);
|
||||
--el-button-border-color: rgba(240, 252, 241, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user