You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
595 lines
17 KiB
595 lines
17 KiB
<template>
|
|
<view class="lf-page account-page">
|
|
<view class="lf-nav">
|
|
<view>
|
|
<view class="lf-nav__title">我的账号</view>
|
|
<view class="lf-nav__sub">维护个人资料和账号安全</view>
|
|
</view>
|
|
<view class="lf-pill">{{ account.roleName || '-' }}</view>
|
|
</view>
|
|
|
|
<view class="profile-card lf-card lf-card--ink">
|
|
<view class="profile-card__top">
|
|
<image v-if="account.wechatAvatar" class="profile-card__avatar-img" :src="account.wechatAvatar" mode="aspectFill" />
|
|
<view v-else class="profile-card__avatar">{{ (account.name || '-').slice(0, 1) }}</view>
|
|
<view>
|
|
<view class="profile-card__name">{{ account.name }}</view>
|
|
<view class="profile-card__phone">{{ account.phone || '未绑定手机号' }}</view>
|
|
</view>
|
|
</view>
|
|
<view v-if="isAdmin && (account.permissions || []).length" class="permission-strip">
|
|
<text v-for="permission in account.permissions || []" :key="permission">{{ permission }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="lf-card action-card">
|
|
<view class="lf-row">
|
|
<view class="lf-section-title">个人资料</view>
|
|
<button class="mini-action" @click="toggleEdit">{{ editing ? '收起' : '编辑' }}</button>
|
|
</view>
|
|
<view v-if="!editing" class="profile-list">
|
|
<view class="profile-line">
|
|
<text>姓名</text>
|
|
<text>{{ account.name || '-' }}</text>
|
|
</view>
|
|
<view class="profile-line">
|
|
<text>孩子</text>
|
|
<text>{{ account.child?.name || '-' }}</text>
|
|
</view>
|
|
<view class="profile-line">
|
|
<text>年级</text>
|
|
<text>{{ account.child?.grade || '-' }}</text>
|
|
</view>
|
|
<view class="profile-line">
|
|
<text>关注点</text>
|
|
<text>{{ account.child?.focus || '-' }}</text>
|
|
</view>
|
|
<view v-if="account.role === 'parent'" class="profile-line">
|
|
<text>试听次数</text>
|
|
<text>剩余 {{ account.trialBookingRemaining ?? account.trialBookingLimit ?? 1 }} / {{ account.trialBookingLimit ?? 1 }} 次</text>
|
|
</view>
|
|
</view>
|
|
<view v-else class="edit-form">
|
|
<!-- #ifdef MP-WEIXIN -->
|
|
<button class="avatar-button" open-type="chooseAvatar" @chooseavatar="chooseAvatar">
|
|
<image v-if="form.wechatAvatar" :src="form.wechatAvatar" mode="aspectFill" />
|
|
<text v-else>选头像</text>
|
|
</button>
|
|
<!-- #endif -->
|
|
<view class="form-field">
|
|
<text>姓名</text>
|
|
<input v-model="form.name" maxlength="20" placeholder="请输入姓名" />
|
|
</view>
|
|
<view class="form-field">
|
|
<text>孩子姓名</text>
|
|
<input v-model="form.childName" maxlength="20" placeholder="请输入孩子姓名" />
|
|
</view>
|
|
<view class="form-field">
|
|
<text>孩子年级</text>
|
|
<input v-model="form.childGrade" maxlength="20" placeholder="例如:三年级" />
|
|
</view>
|
|
<view class="form-field">
|
|
<text>学习关注点</text>
|
|
<input v-model="form.childFocus" maxlength="30" placeholder="例如:数学思维 / 阅读表达" />
|
|
</view>
|
|
<button class="save-button" :disabled="saving" @click="saveProfile">{{ saving ? '保存中' : '保存资料' }}</button>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="lf-card action-card">
|
|
<view class="lf-section-title">账号安全</view>
|
|
<view class="entry-row" @click="togglePasswordPanel">
|
|
<view>
|
|
<text>{{ account.hasPassword ? '修改密码' : '设置密码' }}</text>
|
|
<text>{{ account.hasPassword ? '用于账号密码登录' : '设置后可使用账号密码登录' }}</text>
|
|
</view>
|
|
<u-icon name="arrow-right" color="#0b4f46" size="22"></u-icon>
|
|
</view>
|
|
<view v-if="passwordPanel" class="security-panel">
|
|
<view v-if="account.hasPassword" class="form-field">
|
|
<text>原密码</text>
|
|
<input v-model="passwordForm.oldPassword" password maxlength="32" placeholder="请输入原密码" />
|
|
</view>
|
|
<view class="form-field">
|
|
<text>新密码</text>
|
|
<input v-model="passwordForm.password" password maxlength="32" placeholder="至少 6 位" />
|
|
</view>
|
|
<view class="form-field">
|
|
<text>确认密码</text>
|
|
<input v-model="passwordForm.confirmPassword" password maxlength="32" placeholder="请再次输入新密码" />
|
|
</view>
|
|
<button class="save-button" :disabled="securitySaving" @click="savePassword">
|
|
{{ securitySaving ? '保存中' : account.hasPassword ? '修改密码' : '设置密码' }}
|
|
</button>
|
|
</view>
|
|
|
|
<view class="entry-row" @click="togglePhonePanel">
|
|
<view>
|
|
<text>换绑手机号</text>
|
|
<text>当前:{{ account.phone || '未绑定' }}</text>
|
|
</view>
|
|
<u-icon name="arrow-right" color="#0b4f46" size="22"></u-icon>
|
|
</view>
|
|
<view v-if="phonePanel" class="security-panel">
|
|
<view class="form-field">
|
|
<text>新手机号</text>
|
|
<input v-model="phoneForm.phone" type="number" maxlength="11" placeholder="请输入新手机号" />
|
|
</view>
|
|
<view class="form-field form-field--code">
|
|
<view class="code-input">
|
|
<text>验证码</text>
|
|
<input v-model="phoneForm.code" type="number" maxlength="6" placeholder="请输入验证码" />
|
|
</view>
|
|
<button class="code-button" :disabled="phoneCountdown > 0" @click="requestPhoneCode">
|
|
{{ phoneCountdown > 0 ? `${phoneCountdown}s` : '获取验证码' }}
|
|
</button>
|
|
</view>
|
|
<button class="save-button" :disabled="securitySaving" @click="savePhone">
|
|
{{ securitySaving ? '保存中' : '确认换绑' }}
|
|
</button>
|
|
</view>
|
|
|
|
<view class="entry-row entry-row--danger" @click="confirmLogout">
|
|
<view>
|
|
<text>退出登录</text>
|
|
<text>退出后需要重新登录当前账号</text>
|
|
</view>
|
|
<u-icon name="arrow-right" color="#b54732" size="22"></u-icon>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="isAdmin" class="lf-card action-card">
|
|
<view class="lf-section-title">管理入口</view>
|
|
<view class="entry-row" @click="go('/pages/admin/index')">
|
|
<view>
|
|
<text>移动管理台</text>
|
|
<text>线索推进、积分管理、试听课程管理</text>
|
|
</view>
|
|
<u-icon name="arrow-right" color="#0b4f46" size="22"></u-icon>
|
|
</view>
|
|
</view>
|
|
|
|
<u-toast ref="toast"></u-toast>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import { sendSmsCode, updateBoundPhone, updatePassword, updateProfile, uploadAvatar } from '../../services/educationApi'
|
|
import { clearStoredAccount, getStoredAccount, isAdminAccount, refreshStoredAccount, setStoredAccount } from '../../services/accountSession'
|
|
|
|
function createProfileForm(account = {}) {
|
|
return {
|
|
name: account.name || '',
|
|
childName: account.child?.name || '',
|
|
childGrade: account.child?.grade || '',
|
|
childFocus: account.child?.focus || '',
|
|
wechatAvatar: account.wechatAvatar || '',
|
|
}
|
|
}
|
|
|
|
function createPasswordForm() {
|
|
return {
|
|
oldPassword: '',
|
|
password: '',
|
|
confirmPassword: '',
|
|
}
|
|
}
|
|
|
|
export default {
|
|
data() {
|
|
const account = getStoredAccount() || {}
|
|
return {
|
|
account,
|
|
editing: false,
|
|
saving: false,
|
|
securitySaving: false,
|
|
passwordPanel: false,
|
|
phonePanel: false,
|
|
phoneCountdown: 0,
|
|
phoneTimer: null,
|
|
form: createProfileForm(account),
|
|
passwordForm: createPasswordForm(),
|
|
phoneForm: {
|
|
phone: '',
|
|
code: '',
|
|
},
|
|
}
|
|
},
|
|
computed: {
|
|
isAdmin() {
|
|
return isAdminAccount(this.account)
|
|
},
|
|
},
|
|
async onShow() {
|
|
const account = await refreshStoredAccount()
|
|
if (!account) {
|
|
uni.reLaunch({ url: '/pages/login/index' })
|
|
return
|
|
}
|
|
this.account = account
|
|
this.form = createProfileForm(account)
|
|
},
|
|
async onPullDownRefresh() {
|
|
await this.onShow()
|
|
uni.stopPullDownRefresh()
|
|
},
|
|
onUnload() {
|
|
if (this.phoneTimer) clearInterval(this.phoneTimer)
|
|
},
|
|
methods: {
|
|
toggleEdit() {
|
|
this.editing = !this.editing
|
|
if (this.editing) this.form = createProfileForm(this.account)
|
|
},
|
|
async chooseAvatar(event) {
|
|
const localPath = event.detail.avatarUrl
|
|
if (!localPath) return
|
|
const res = await uploadAvatar(localPath)
|
|
if (res.ok === false) {
|
|
this.$refs.toast.show({ message: res.message || '头像上传失败', type: 'error' })
|
|
return
|
|
}
|
|
this.form.wechatAvatar = res.data?.url || res.url || localPath
|
|
this.$refs.toast.show({ message: '头像已上传', type: 'success' })
|
|
},
|
|
async saveProfile() {
|
|
if (!this.form.name.trim()) {
|
|
this.$refs.toast.show({ message: '请填写姓名', type: 'error' })
|
|
return
|
|
}
|
|
this.saving = true
|
|
try {
|
|
const res = await updateProfile(this.account.id, this.form)
|
|
if (res.ok === false) {
|
|
this.$refs.toast.show({ message: res.message || '保存失败', type: 'error' })
|
|
return
|
|
}
|
|
this.applyAccount(res.data || res)
|
|
this.editing = false
|
|
this.$refs.toast.show({ message: '资料已保存', type: 'success' })
|
|
} finally {
|
|
this.saving = false
|
|
}
|
|
},
|
|
togglePasswordPanel() {
|
|
this.passwordPanel = !this.passwordPanel
|
|
this.passwordForm = createPasswordForm()
|
|
if (this.passwordPanel) this.phonePanel = false
|
|
},
|
|
togglePhonePanel() {
|
|
this.phonePanel = !this.phonePanel
|
|
this.phoneForm = { phone: '', code: '' }
|
|
if (this.phonePanel) this.passwordPanel = false
|
|
},
|
|
async requestPhoneCode() {
|
|
if (!/^1\d{10}$/.test(this.phoneForm.phone)) {
|
|
this.$refs.toast.show({ message: '请先输入正确手机号', type: 'error' })
|
|
return
|
|
}
|
|
const res = await sendSmsCode(this.phoneForm.phone, 'update_phone')
|
|
if (res.ok === false) {
|
|
this.$refs.toast.show({ message: res.message || '验证码发送失败', type: 'error' })
|
|
return
|
|
}
|
|
const data = res.data || res
|
|
this.startPhoneCountdown(data.cooldown || 60)
|
|
this.$refs.toast.show({ message: res.message || data.message || '验证码已发送', type: 'success' })
|
|
},
|
|
startPhoneCountdown(seconds = 60) {
|
|
if (this.phoneTimer) clearInterval(this.phoneTimer)
|
|
this.phoneCountdown = Number(seconds || 60)
|
|
this.phoneTimer = setInterval(() => {
|
|
this.phoneCountdown -= 1
|
|
if (this.phoneCountdown <= 0) {
|
|
clearInterval(this.phoneTimer)
|
|
this.phoneTimer = null
|
|
}
|
|
}, 1000)
|
|
},
|
|
async savePassword() {
|
|
if (this.account.hasPassword && !this.passwordForm.oldPassword) {
|
|
this.$refs.toast.show({ message: '请输入原密码', type: 'error' })
|
|
return
|
|
}
|
|
if (this.passwordForm.password.length < 6) {
|
|
this.$refs.toast.show({ message: '密码至少需要 6 位', type: 'error' })
|
|
return
|
|
}
|
|
if (this.passwordForm.password !== this.passwordForm.confirmPassword) {
|
|
this.$refs.toast.show({ message: '两次输入的密码不一致', type: 'error' })
|
|
return
|
|
}
|
|
this.securitySaving = true
|
|
try {
|
|
const res = await updatePassword(this.account.id, this.passwordForm)
|
|
if (res.ok === false) {
|
|
this.$refs.toast.show({ message: res.message || '密码保存失败', type: 'error' })
|
|
return
|
|
}
|
|
this.applyAccount(res.data || res)
|
|
this.passwordPanel = false
|
|
this.passwordForm = createPasswordForm()
|
|
this.$refs.toast.show({ message: '密码已更新', type: 'success' })
|
|
} finally {
|
|
this.securitySaving = false
|
|
}
|
|
},
|
|
async savePhone() {
|
|
if (!/^1\d{10}$/.test(this.phoneForm.phone)) {
|
|
this.$refs.toast.show({ message: '请输入正确手机号', type: 'error' })
|
|
return
|
|
}
|
|
if (!this.phoneForm.code) {
|
|
this.$refs.toast.show({ message: '请输入验证码', type: 'error' })
|
|
return
|
|
}
|
|
this.securitySaving = true
|
|
try {
|
|
const res = await updateBoundPhone(this.account.id, this.phoneForm)
|
|
if (res.ok === false) {
|
|
this.$refs.toast.show({ message: res.message || '换绑失败', type: 'error' })
|
|
return
|
|
}
|
|
this.applyAccount(res.data || res)
|
|
this.phonePanel = false
|
|
this.phoneForm = { phone: '', code: '' }
|
|
this.$refs.toast.show({ message: '手机号已换绑', type: 'success' })
|
|
} finally {
|
|
this.securitySaving = false
|
|
}
|
|
},
|
|
applyAccount(account) {
|
|
this.account = account
|
|
this.form = createProfileForm(account)
|
|
setStoredAccount(account)
|
|
},
|
|
confirmLogout() {
|
|
uni.showModal({
|
|
title: '确认退出登录',
|
|
content: '退出后需要重新登录当前账号,是否继续?',
|
|
confirmText: '退出',
|
|
confirmColor: '#b54732',
|
|
success: (res) => {
|
|
if (!res.confirm) return
|
|
clearStoredAccount()
|
|
uni.reLaunch({ url: '/pages/login/index' })
|
|
},
|
|
})
|
|
},
|
|
go(url) {
|
|
uni.navigateTo({ url })
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.profile-card {
|
|
margin-top: 18rpx;
|
|
padding: 32rpx;
|
|
}
|
|
|
|
.profile-card__top {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 22rpx;
|
|
}
|
|
|
|
.profile-card__avatar,
|
|
.profile-card__avatar-img {
|
|
width: 92rpx;
|
|
height: 92rpx;
|
|
border-radius: 24rpx;
|
|
}
|
|
|
|
.profile-card__avatar {
|
|
display: grid;
|
|
place-items: center;
|
|
color: #0b2f2a;
|
|
background: #f5c451;
|
|
font-size: 42rpx;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.profile-card__name {
|
|
color: #ffffff;
|
|
font-size: 36rpx;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.profile-card__phone {
|
|
margin-top: 8rpx;
|
|
color: rgba(255, 255, 255, 0.68);
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.permission-strip {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12rpx;
|
|
margin-top: 28rpx;
|
|
}
|
|
|
|
.permission-strip text {
|
|
padding: 8rpx 14rpx;
|
|
border-radius: 999rpx;
|
|
color: #dff8ee;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
font-size: 20rpx;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.action-card {
|
|
margin-top: 22rpx;
|
|
padding: 28rpx;
|
|
}
|
|
|
|
.mini-action {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 96rpx;
|
|
height: 52rpx;
|
|
padding: 0 18rpx;
|
|
border-radius: 999rpx;
|
|
color: #0b4f46;
|
|
background: #dff8ee;
|
|
font-size: 22rpx;
|
|
font-weight: 900;
|
|
line-height: normal;
|
|
}
|
|
|
|
.profile-list {
|
|
margin-top: 18rpx;
|
|
}
|
|
|
|
.profile-line {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 24rpx;
|
|
padding: 20rpx 0;
|
|
border-bottom: 1rpx solid #edf1e9;
|
|
}
|
|
|
|
.profile-line:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.profile-line text:first-child {
|
|
color: #6f7c78;
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.profile-line text:last-child {
|
|
color: #17312d;
|
|
font-size: 26rpx;
|
|
font-weight: 900;
|
|
text-align: right;
|
|
}
|
|
|
|
.edit-form,
|
|
.security-panel {
|
|
margin-top: 20rpx;
|
|
padding: 22rpx;
|
|
border: 1rpx solid #dfe9dc;
|
|
border-radius: 16rpx;
|
|
background: #fbfdf8;
|
|
}
|
|
|
|
.avatar-button {
|
|
display: grid;
|
|
place-items: center;
|
|
width: 112rpx;
|
|
height: 112rpx;
|
|
margin-bottom: 22rpx;
|
|
border-radius: 28rpx;
|
|
color: #0b4f46;
|
|
background: #dff8ee;
|
|
font-size: 22rpx;
|
|
font-weight: 900;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.avatar-button image {
|
|
width: 112rpx;
|
|
height: 112rpx;
|
|
}
|
|
|
|
.form-field {
|
|
margin-top: 18rpx;
|
|
}
|
|
|
|
.form-field:first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.form-field text {
|
|
display: block;
|
|
margin-bottom: 10rpx;
|
|
color: #6f7c78;
|
|
font-size: 22rpx;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.form-field input {
|
|
height: 78rpx;
|
|
padding: 0 22rpx;
|
|
border: 1rpx solid #dfe9dc;
|
|
border-radius: 14rpx;
|
|
color: #17312d;
|
|
background: #f8faf5;
|
|
font-size: 26rpx;
|
|
line-height: 78rpx;
|
|
}
|
|
|
|
.form-field--code {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) 190rpx;
|
|
align-items: end;
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.code-button,
|
|
.save-button {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 78rpx;
|
|
border-radius: 16rpx;
|
|
font-size: 26rpx;
|
|
font-weight: 900;
|
|
line-height: normal;
|
|
}
|
|
|
|
.code-button {
|
|
color: #0b4f46;
|
|
border: 1rpx solid #b9d8d2;
|
|
background: #dff8ee;
|
|
}
|
|
|
|
.code-button[disabled] {
|
|
color: #7d8b86;
|
|
background: #eef3ec;
|
|
}
|
|
|
|
.save-button {
|
|
margin-top: 24rpx;
|
|
color: #ffffff;
|
|
background: #0b4f46;
|
|
}
|
|
|
|
.entry-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 18rpx;
|
|
padding: 26rpx 0;
|
|
border-bottom: 1rpx solid #edf1e9;
|
|
}
|
|
|
|
.entry-row:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.entry-row--danger text:first-child {
|
|
color: #b54732;
|
|
}
|
|
|
|
.entry-row text {
|
|
display: block;
|
|
}
|
|
|
|
.entry-row text:first-child {
|
|
color: #17312d;
|
|
font-size: 28rpx;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.entry-row text:last-child {
|
|
margin-top: 8rpx;
|
|
color: #6f7c78;
|
|
font-size: 22rpx;
|
|
line-height: 1.45;
|
|
}
|
|
</style>
|