first commit
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user