899 lines
29 KiB
Vue
899 lines
29 KiB
Vue
<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>
|