项目初始化

This commit is contained in:
2026-03-18 15:54:43 +08:00
commit dfcd762e23
601 changed files with 57883 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>