first commit

This commit is contained in:
2026-04-16 14:16:41 +08:00
commit 5c33491ae2
586 changed files with 58153 additions and 0 deletions

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>