Files
hfb_sys/frontend/src/features/auth/views/MobileProfileView.vue
T
yml2213 98174c0e33 feat(quality-owner): 增加优质号主、极品专区与无忧租号保险
- 支持保证金余额支付、在线支付开通及后台手动设置审计
- 新增极品专区发布权限、赔付方案与保险结算费率快照
- 优化首页、商品卡和个人中心的优质号主身份展示
2026-11-04 15:04:35 +08:00

899 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { readError } from '@/shared/utils/error'
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useSessionStore } from '@/stores/session'
import { showDialog, showToast } from 'vant'
import MobileBottomNav from '@/components/MobileBottomNav.vue'
import { useNotificationUnreadCount } from '@/features/auth/composables/useNotificationUnreadCount'
import {
fetchWalletBalance,
fetchWalletLedger,
type WalletLedger,
} from '@/features/wallet/api/wallet'
import { fetchPostRentalNotice, type PostRentalNotice } from '@/features/orders/api/orders'
import { formatDateMinute } from '@/shared/utils/time'
import { uploadFile } from '@/shared/api/files'
import { formatCent } from '@/shared/utils/money'
import { setPassword } from '@/features/auth/api/auth'
import { fetchUserInvitationSummary } from '@/features/invitation/api/invitation'
import {
activateQualityOwner,
fetchQualityOwnerStatus,
queryQualityOwnerPayment,
startQualityOwnerPayment,
type QualityOwnerStatus,
} from '@/features/quality-owner/api/qualityOwner'
import type { PaymentPayWay } from '@/features/orders/api/orders'
import { useMobilePayWaySelect } from '@/features/orders/composables/useMobilePayWaySelect'
import { useMobilePaymentCashier } from '@/features/orders/composables/useMobilePaymentCashier'
import MobilePayWaySelectPopup from '@/features/orders/components/MobilePayWaySelectPopup.vue'
import MobilePaymentCashierPopup from '@/features/orders/components/MobilePaymentCashierPopup.vue'
import { useDistributionSiteStore } from '@/stores/distributionSite'
const session = useSessionStore()
const siteStore = useDistributionSiteStore()
const router = useRouter()
const route = useRoute()
/** 邀请返利入口右侧比例文案,取自后台返利配置。 */
const inviteRateText = ref('')
const { unreadCount: notificationUnreadCount, unreadLabel: notificationUnreadLabel } =
useNotificationUnreadCount(route)
/** 数据项 */
const availableBalanceCent = ref(0)
const qualityOwnerStatus = ref<QualityOwnerStatus | null>(null)
const qualityOwnerLoading = ref(false)
const qualityOwnerOnlineLoading = ref(false)
const qualityOwnerPayWaySelect = useMobilePayWaySelect()
const qualityOwnerCashier = useMobilePaymentCashier({
queryPayment: () => queryQualityOwnerPayment(),
paidMessage: '保证金支付成功,优质号主已开通',
})
const {
paymentPopupVisible: qualityOwnerPaymentPopupVisible,
activePayment: activeQualityOwnerPayment,
payURL: qualityOwnerPaymentPayURL,
paymentQRCodeURL: qualityOwnerPaymentQRCodeURL,
qrGenerating: qualityOwnerPaymentQRGenerating,
checkingPayment: qualityOwnerPaymentChecking,
} = qualityOwnerCashier
const showSettings = ref(false)
const showProfileEditor = ref(false)
const savingProfile = ref(false)
const profileForm = reactive({
nickname: '',
avatar_url: '',
})
const avatarFileInput = ref<HTMLInputElement | null>(null)
const uploadingAvatar = ref(false)
// 修改密码
const showPasswordEditor = ref(false)
const savingPassword = ref(false)
const passwordForm = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: '',
})
function triggerAvatarUpload() {
avatarFileInput.value?.click()
}
async function handleAvatarFileChange(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
input.value = ''
if (!file) return
uploadingAvatar.value = true
const loadingToast = showToast({
type: 'loading',
message: '上传中...',
forbidClick: true,
duration: 0,
})
try {
const uploaded = await uploadFile(file, 'avatar')
profileForm.avatar_url = uploaded.url
showToast({ message: '上传成功', icon: 'passed' })
} catch (error) {
showToast({ message: readError(error, '上传失败'), icon: 'cross' })
} finally {
loadingToast.close()
uploadingAvatar.value = false
}
}
// 收支明细列表
const showLedgers = ref(false)
const loadingLedgers = ref(false)
const ledgers = ref<WalletLedger[]>([])
// 租后须知
const showPostRentalNotice = ref(false)
const loadingPostRentalNotice = ref(false)
const postRentalNotice = ref<PostRentalNotice | null>(null)
const settingsGroups = [
{
title: '账号与安全',
items: [
{ label: '资料更改', icon: 'edit', action: 'profile' },
{ label: '实名认证', icon: 'idcard', action: 'realname' },
{ label: '注销账号', icon: 'delete-o', action: 'cancel-account' },
],
},
{
title: '法律与隐私',
items: [
{ label: '隐私政策', icon: 'shield-o', action: 'privacy' },
{ label: '用户协议', icon: 'description', action: 'terms' },
],
},
]
onMounted(async () => {
if (!isLoggedIn.value) {
router.replace({ path: '/m/login', query: { redirect: route.fullPath } })
} else {
try {
await session.loadMe()
} catch {
// 保留本地会话展示,避免个人中心因网络短暂异常整页不可用。
}
void loadBalance()
void loadQualityOwnerStatus()
void loadInviteRate()
}
})
async function loadQualityOwnerStatus() {
try {
qualityOwnerStatus.value = await fetchQualityOwnerStatus()
} catch {
qualityOwnerStatus.value = null
}
}
async function handleActivateQualityOwner() {
const status = qualityOwnerStatus.value
if (!status || status.active) return
try {
await showDialog({
title: '开通优质号主',
message: `确认从可用余额中扣除 ¥${formatCent(
status.required_deposit_cent
)} 作为保证金?取消资格时会退回余额。`,
showCancelButton: true,
confirmButtonText: '确认缴纳',
cancelButtonText: '取消',
})
} catch {
return
}
qualityOwnerLoading.value = true
try {
qualityOwnerStatus.value = await activateQualityOwner()
await session.loadMe()
await loadBalance()
showToast({ message: '优质号主已开通', icon: 'passed' })
} catch (error) {
showToast({ message: readError(error, '开通失败,请确认余额充足'), icon: 'cross' })
} finally {
qualityOwnerLoading.value = false
}
}
async function handleOnlineQualityOwnerPayment() {
const payWay = await qualityOwnerPayWaySelect.select()
if (!payWay) return
qualityOwnerOnlineLoading.value = true
try {
const payment = await startQualityOwnerPayment(payWay as PaymentPayWay)
if (payment.paid) {
await handleQualityOwnerPaid()
return
}
await qualityOwnerCashier.openMobilePaymentCashier(payment, handleQualityOwnerPaid)
} catch (error) {
showToast({ message: readError(error, '发起在线支付失败'), icon: 'cross' })
} finally {
qualityOwnerOnlineLoading.value = false
}
}
async function handleQualityOwnerPaid() {
qualityOwnerStatus.value = await fetchQualityOwnerStatus()
await session.loadMe()
await loadBalance()
}
async function loadBalance() {
try {
const wallet = await fetchWalletBalance()
availableBalanceCent.value = wallet.available_balance_cent
} catch {
// 失败静默处理,显示为默认0
}
}
// 邀请返利入口右侧文案取后台实时配置的首月比例,不能写死。
async function loadInviteRate() {
try {
const summary = await fetchUserInvitationSummary()
inviteRateText.value = `首月 ${Math.round(summary.first_month_rate * 100)}% 收益`
} catch {
// 取不到配置时不展示数字,避免与后台实际比例不一致。
inviteRateText.value = ''
}
}
async function openLedgers() {
showLedgers.value = true
loadingLedgers.value = true
try {
const res = await fetchWalletLedger(1, 20)
ledgers.value = res.items
} catch {
showToast({ message: '无法获取账单明细', icon: 'cross' })
} finally {
loadingLedgers.value = false
}
}
async function openPostRentalNotice() {
showPostRentalNotice.value = true
if (!postRentalNotice.value) {
loadingPostRentalNotice.value = true
try {
postRentalNotice.value = await fetchPostRentalNotice()
} catch {
showToast({ message: '无法获取租后须知', icon: 'cross' })
} finally {
loadingPostRentalNotice.value = false
}
}
}
function handleWithdraw() {
router.push('/m/wallet/withdrawal')
}
function goOrders(tabKey: string) {
router.push({ path: '/m/orders', query: { tab: tabKey } })
}
function onSettingClick(action: string) {
showSettings.value = false
switch (action) {
case 'profile':
openProfileEditor()
break
case 'realname':
router.push('/m/realname')
break
case 'cancel-account':
showDialog({
title: '注销账号',
message: '注销后账号数据将无法恢复,确认注销吗?',
showCancelButton: true,
confirmButtonText: '确认注销',
confirmButtonColor: '#ee0a24',
cancelButtonText: '再想想',
})
.then(() => {
showToast({ message: '注销功能开发中', icon: 'info-o' })
})
.catch(() => {})
break
case 'privacy':
showToast({ message: '隐私政策页面开发中', icon: 'info-o' })
break
case 'terms':
showToast({ message: '用户协议页面开发中', icon: 'info-o' })
break
}
}
function openProfileEditor() {
profileForm.nickname = session.nickname || defaultName.value
profileForm.avatar_url = session.avatarUrl || ''
showProfileEditor.value = true
}
async function saveProfile() {
const nickname = profileForm.nickname.trim()
const avatarURL = profileForm.avatar_url.trim()
if (!nickname) {
showToast({ message: '请输入昵称', icon: 'warning-o' })
return
}
if (nickname.length > 24) {
showToast({ message: '昵称不能超过 24 个字符', icon: 'warning-o' })
return
}
savingProfile.value = true
try {
await session.updateProfile({ nickname, avatar_url: avatarURL })
showProfileEditor.value = false
showToast({ message: '资料已更新', icon: 'passed' })
await loadBalance()
} catch (error) {
showToast({ message: readError(error, '资料更新失败'), icon: 'cross' })
} finally {
savingProfile.value = false
}
}
function openPasswordEditor() {
passwordForm.oldPassword = ''
passwordForm.newPassword = ''
passwordForm.confirmPassword = ''
showPasswordEditor.value = true
}
async function savePassword() {
const { newPassword, confirmPassword } = passwordForm
if (!newPassword) {
showToast({ message: '请输入新密码', icon: 'warning-o' })
return
}
if (newPassword.length < 8 || newPassword.length > 20) {
showToast({ message: '密码长度应为 8-20 位', icon: 'warning-o' })
return
}
if (newPassword !== confirmPassword) {
showToast({ message: '两次输入的密码不一致', icon: 'warning-o' })
return
}
savingPassword.value = true
try {
await setPassword(newPassword, passwordForm.oldPassword || undefined)
showToast({ message: '密码已更新', icon: 'passed' })
showPasswordEditor.value = false
} catch (error) {
showToast({ message: readError(error, '密码更新失败'), icon: 'cross' })
} finally {
savingPassword.value = false
}
}
function confirmLogout() {
showSettings.value = false
showDialog({
title: '退出登录',
message: '确定要退出当前账号吗?',
showCancelButton: true,
confirmButtonText: '退出',
confirmButtonColor: '#ee0a24',
cancelButtonText: '取消',
})
.then(async () => {
await session.logout()
router.replace('/m')
})
.catch(() => {})
}
const isLoggedIn = computed(() => Boolean(session.token))
const isRealnameVerified = computed(() => session.realnameStatus === 'verified')
const isRealnamePending = computed(() => session.realnameStatus === 'pending')
const defaultName = computed(() =>
session.phone ? `用户${session.phone.slice(-6)}` : `用户${session.userId || 883099}`
)
const displayName = computed(() => session.nickname || defaultName.value)
const displayId = computed(() => session.userId || 138865)
const maskedPhone = computed(() =>
session.phone ? `${session.phone.slice(0, 3)}****${session.phone.slice(-4)}` : '登录后查看手机号'
)
const avatarText = computed(() => displayName.value.slice(0, 1))
function resolveAvatarURL(url: string | undefined | null) {
if (!url) return ''
if (url.includes('/api/files/object?key=avatar/')) {
return url.replace('/api/files/object', '/api/public/files/object')
}
return url
}
</script>
<template>
<main class="mobile-profile-shell">
<!-- 已登录区 -->
<template v-if="isLoggedIn">
<!-- Hero 顶部蓝色横幅 -->
<section class="profile-hero">
<div class="hero-top-row">
<span class="app-title">个人中心</span>
<button class="hero-setting-btn" @click="showSettings = true">
<van-icon name="setting-o" :size="20" color="#374151" />
</button>
</div>
<div class="profile-hero-content">
<div class="avatar-wrap">
<div class="avatar-circle">
<img v-if="session.avatarUrl" :src="resolveAvatarURL(session.avatarUrl)" alt="" />
<span v-else>{{ avatarText }}</span>
</div>
</div>
<div class="hero-user">
<div class="name-row">
<h1 class="user-nickname">{{ displayName }}</h1>
<van-tag
v-if="isRealnameVerified"
type="success"
size="medium"
round
class="verified-tag"
>
<van-icon name="shield" /> 已实名
</van-tag>
<van-tag
v-else-if="isRealnamePending"
type="warning"
size="medium"
round
class="verified-tag"
>
审核中
</van-tag>
<van-tag
v-else
type="danger"
size="medium"
round
class="unverified-tag"
@click="router.push('/m/realname')"
>
未实名
</van-tag>
<van-tag v-if="qualityOwnerStatus?.active" round class="owner-quality-tag">
<van-icon name="award-o" /> 优质号主
</van-tag>
</div>
<p class="user-meta">ID: {{ displayId }} · {{ maskedPhone }}</p>
</div>
</div>
</section>
<section v-if="siteStore.isDistribution" class="growth-card is-distribution">
<div>
<span>所属商城</span>
<strong>{{ siteStore.siteName || '官方商城' }}</strong>
</div>
<div>
<span>会员特权</span>
<strong>渠道直租专享</strong>
</div>
</section>
<section v-else class="growth-card">
<div>
<span>租客成长等级</span>
<strong>{{ session.renterGrowthLevelName }}</strong>
</div>
<div>
<span>当前积分</span>
<strong>{{ session.renterGrowthPoints }}</strong>
</div>
</section>
<section class="quality-owner-card" :class="{ 'is-active': qualityOwnerStatus?.active }">
<div class="quality-owner-heading">
<div>
<span>优质号主</span>
<strong>{{ qualityOwnerStatus?.active ? '已开通' : '解锁极品专区发布权限' }}</strong>
</div>
<van-tag :type="qualityOwnerStatus?.active ? 'success' : 'default'" round>
{{ qualityOwnerStatus?.active ? '优质号主' : '未开通' }}
</van-tag>
</div>
<div class="quality-owner-info">
<div>
<span>保证金</span>
<strong>¥{{ formatCent(qualityOwnerStatus?.required_deposit_cent || 20000) }}</strong>
</div>
<div>
<span>极品专区保险费率</span>
<strong
>{{
((qualityOwnerStatus?.premium_insurance_fee_bps || 400) / 100).toFixed(1)
}}%</strong
>
</div>
</div>
<div v-if="!qualityOwnerStatus?.active" class="quality-owner-actions">
<van-button
block
round
type="primary"
color="#7c3aed"
:loading="qualityOwnerLoading"
:disabled="
(qualityOwnerStatus?.available_balance_cent || 0) <
(qualityOwnerStatus?.required_deposit_cent || 20000)
"
@click="handleActivateQualityOwner"
>
余额支付开通
</van-button>
<van-button
block
round
plain
type="primary"
:loading="qualityOwnerOnlineLoading"
@click="handleOnlineQualityOwnerPayment"
>
微信 / 支付宝支付开通
</van-button>
</div>
<p v-else>
{{
qualityOwnerStatus?.deposit_source === 'manual'
? '已由后台开通优质号主。'
: '保证金已冻结,取消优质号主资格时退回可用余额。'
}}
</p>
</section>
<!-- 余额卡片 - 银行卡质感暗色卡片 -->
<div class="balance-card">
<div class="balance-row">
<div class="balance-info">
<span class="balance-label">账户可用余额(元)</span>
<strong class="balance-value">¥{{ formatCent(availableBalanceCent) }}</strong>
</div>
<button class="withdraw-btn" @click="handleWithdraw">提现</button>
</div>
<div class="balance-footer">
<button class="detail-btn" @click="openLedgers">
查看收支明细 <van-icon name="arrow" :size="10" />
</button>
</div>
</div>
<!-- 买家服务面板 -->
<div class="menu-card">
<div class="card-header">
<h3>买家服务</h3>
<span class="see-all" @click="goOrders('all')">
全部订单 <van-icon name="arrow" :size="10" />
</span>
</div>
<div class="grid-menu col-5">
<div class="grid-item" @click="goOrders('pending_payment')">
<div class="icon-wrap warning"><van-icon name="bill-o" :size="22" /></div>
<span>待支付</span>
</div>
<div class="grid-item" @click="goOrders('pending_handoff')">
<div class="icon-wrap info"><van-icon name="logistics" :size="22" /></div>
<span>待交接</span>
</div>
<div class="grid-item" @click="goOrders('renting')">
<div class="icon-wrap primary"><van-icon name="play-circle-o" :size="22" /></div>
<span>使用中</span>
</div>
<div class="grid-item" @click="goOrders('completed')">
<div class="icon-wrap success"><van-icon name="smile-o" :size="22" /></div>
<span>已完成</span>
</div>
<div class="grid-item" @click="router.push('/m/listings/collections')">
<div class="icon-wrap orange"><van-icon name="star-o" :size="22" /></div>
<span>我的足迹</span>
</div>
</div>
</div>
<!-- 卖家服务面板 -->
<div class="menu-card">
<div class="card-header">
<h3>卖家服务</h3>
</div>
<div class="grid-menu col-3">
<div class="grid-item" @click="router.push('/m/seller/listings/create')">
<div class="icon-wrap orange"><van-icon name="plus" :size="22" /></div>
<span>发布商品</span>
</div>
<div class="grid-item" @click="router.push('/m/seller/listings')">
<div class="icon-wrap purple"><van-icon name="shop-o" :size="22" /></div>
<span>我的商品</span>
</div>
<div class="grid-item" @click="router.push('/m/wallet/withdrawal')">
<div class="icon-wrap teal"><van-icon name="balance-o" :size="22" /></div>
<span>提现/账单</span>
</div>
</div>
</div>
<!-- 常用功能 Cell Group -->
<div class="menu-card cell-card">
<van-cell-group :border="false">
<van-cell title="消息中心" icon="chat-o" is-link to="/m/messages" />
<van-cell title="站内信" icon="envelop-o" is-link to="/m/notifications">
<template #value>
<span v-if="notificationUnreadCount > 0" class="unread-cell-value">
{{ notificationUnreadLabel }} 未读
</span>
</template>
</van-cell>
<van-cell title="公告中心" icon="volume-o" is-link to="/m/announcements" />
<van-cell title="资料修改" icon="edit" is-link @click="openProfileEditor" />
<van-cell title="修改密码" icon="lock" is-link @click="openPasswordEditor" />
<van-cell title="实名认证" icon="idcard" is-link to="/m/realname">
<template #value>
<span :class="isRealnameVerified ? 'verified-color' : 'unverified-color'">
{{ isRealnameVerified ? '已认证' : isRealnamePending ? '审核中' : '未认证' }}
</span>
</template>
</van-cell>
<van-cell
title="邀请返利"
icon="gold-coin-o"
is-link
to="/m/invitations"
:value="inviteRateText || '阶梯返利'"
/>
<van-cell title="租后须知" icon="info-o" is-link @click="openPostRentalNotice" />
<van-cell title="系统设置" icon="setting-o" is-link @click="showSettings = true" />
</van-cell-group>
</div>
</template>
<MobilePayWaySelectPopup
v-model:show="qualityOwnerPayWaySelect.visible.value"
@choose="qualityOwnerPayWaySelect.choose"
@closed="qualityOwnerPayWaySelect.handleClosed"
/>
<MobilePaymentCashierPopup
v-model:show="qualityOwnerPaymentPopupVisible"
:payment="activeQualityOwnerPayment"
:pay-url="qualityOwnerPaymentPayURL"
:qr-code-url="qualityOwnerPaymentQRCodeURL"
:qr-generating="qualityOwnerPaymentQRGenerating"
:checking="qualityOwnerPaymentChecking"
@refresh="qualityOwnerCashier.refreshPaymentStatus(false)"
/>
<!-- 账单明细 Popup -->
<van-popup
v-model:show="showLedgers"
position="bottom"
round
class="ledgers-popup"
:style="{ height: '65%' }"
>
<header class="popup-header">
<h3>收支明细</h3>
<button class="popup-close" @click="showLedgers = false">✕</button>
</header>
<div class="popup-body">
<van-loading v-if="loadingLedgers" class="center-loading" vertical>加载中...</van-loading>
<van-empty v-else-if="ledgers.length === 0" description="暂无收支记录" />
<div v-else class="ledger-list">
<div v-for="item in ledgers" :key="item.id" class="ledger-item">
<div class="ledger-left">
<strong class="ledger-remark">{{ item.remark || item.biz_type }}</strong>
<span class="ledger-time">{{ formatDateMinute(item.created_at) }}</span>
</div>
<div class="ledger-right" :class="item.direction === 'in' ? 'in-color' : 'out-color'">
{{ item.direction === 'in' ? '+' : '-' }}¥{{ formatCent(item.amount_cent) }}
</div>
</div>
</div>
</div>
</van-popup>
<!-- 租后须知 Popup -->
<van-popup
v-model:show="showPostRentalNotice"
position="bottom"
round
class="notice-popup"
:style="{ height: '75%' }"
>
<header class="popup-header">
<h3>{{ postRentalNotice?.title || '租后须知' }}</h3>
<button class="popup-close" @click="showPostRentalNotice = false">✕</button>
</header>
<div class="popup-body">
<van-loading v-if="loadingPostRentalNotice" class="center-loading" vertical
>加载中...</van-loading
>
<div v-else-if="postRentalNotice" class="notice-content">
{{ postRentalNotice.content }}
</div>
<van-empty v-else description="暂无租后须知" />
</div>
</van-popup>
<!-- 设置面板 - Popup -->
<van-popup
v-model:show="showSettings"
position="right"
:style="{ width: '80%', height: '100%' }"
>
<div class="settings-panel">
<header class="settings-header">
<h2>设置</h2>
<button class="settings-close" @click="showSettings = false">
<van-icon name="cross" :size="20" />
</button>
</header>
<div v-for="group in settingsGroups" :key="group.title" class="settings-group">
<h3 class="settings-group-title">{{ group.title }}</h3>
<van-cell-group :border="false">
<van-cell
v-for="item in group.items"
:key="item.action"
:title="item.label"
:icon="item.icon"
is-link
@click="onSettingClick(item.action)"
/>
</van-cell-group>
</div>
<div class="settings-logout">
<van-button plain type="danger" block round @click="confirmLogout"> 退出登录 </van-button>
</div>
</div>
</van-popup>
<!-- 个人资料编辑 Popup -->
<van-popup
v-model:show="showProfileEditor"
position="bottom"
round
:style="{ maxWidth: '430px', margin: '0 auto', left: 0, right: 0 }"
>
<section class="profile-editor">
<header class="profile-editor-header">
<h2>资料更改</h2>
<button type="button" @click="showProfileEditor = false">
<van-icon name="cross" :size="20" />
</button>
</header>
<div class="profile-preview">
<div class="profile-preview-avatar clickable" @click="triggerAvatarUpload">
<img
v-if="profileForm.avatar_url"
:src="resolveAvatarURL(profileForm.avatar_url)"
alt=""
/>
<span v-else>{{ (profileForm.nickname || defaultName).slice(0, 1) }}</span>
<div class="avatar-upload-overlay">
<van-icon name="photograph" :size="16" />
</div>
<van-loading v-if="uploadingAvatar" size="20px" class="avatar-loading" />
</div>
<div class="preview-info">
<strong>{{ profileForm.nickname || defaultName }}</strong>
<p>{{ maskedPhone }}</p>
<van-button
size="mini"
type="primary"
plain
round
icon="photograph"
class="upload-btn"
:loading="uploadingAvatar"
@click="triggerAvatarUpload"
>
上传本地照片
</van-button>
</div>
<!-- 隐藏的文件选择框 -->
<input
ref="avatarFileInput"
type="file"
accept="image/*"
style="display: none"
@change="handleAvatarFileChange"
/>
</div>
<label class="editor-field">
<span>昵称</span>
<input v-model="profileForm.nickname" maxlength="24" placeholder="请输入昵称" />
</label>
<label class="editor-field">
<span>头像地址</span>
<input
v-model="profileForm.avatar_url"
maxlength="512"
placeholder="可填写图片 URL,留空使用文字头像"
/>
</label>
<van-button
type="primary"
block
round
class="profile-save-btn"
:loading="savingProfile"
loading-text="保存中..."
@click="saveProfile"
>
保存资料
</van-button>
</section>
</van-popup>
<!-- 修改密码 Popup -->
<van-popup
v-model:show="showPasswordEditor"
position="bottom"
round
:style="{ maxWidth: '430px', margin: '0 auto', left: 0, right: 0 }"
>
<section class="profile-editor">
<header class="profile-editor-header">
<h2>修改密码</h2>
<button type="button" @click="showPasswordEditor = false">
<van-icon name="cross" :size="20" />
</button>
</header>
<p class="password-hint">未设置过密码的老用户,"原密码"可留空直接设置新密码。</p>
<label class="editor-field">
<span>原密码</span>
<input
v-model="passwordForm.oldPassword"
type="password"
maxlength="20"
placeholder="未设置过密码可留空"
/>
</label>
<label class="editor-field">
<span>新密码</span>
<input
v-model="passwordForm.newPassword"
type="password"
maxlength="20"
placeholder="8-20 位"
/>
</label>
<label class="editor-field">
<span>确认新密码</span>
<input
v-model="passwordForm.confirmPassword"
type="password"
maxlength="20"
placeholder="再次输入新密码"
/>
</label>
<van-button
type="primary"
block
round
class="profile-save-btn"
:loading="savingPassword"
loading-text="保存中..."
@click="savePassword"
>
保存密码
</van-button>
</section>
</van-popup>
<MobileBottomNav />
</main>
</template>
<style scoped src="./MobileProfileView.css"></style>