feat(M08-A): 接入顾客端个人中心

This commit is contained in:
Codex
2026-06-25 11:43:25 +08:00
parent c7c869c18b
commit 7446852cca
14 changed files with 476 additions and 4 deletions
+82
View File
@@ -0,0 +1,82 @@
const { request, ensureLogin, cents } = require('../../utils/api.js')
Page({
data: {
loading: false,
errorMessage: '',
profile: null,
recentLedger: [],
},
onLoad() {
this.loadProfile()
},
async loadProfile() {
this.setData({ loading: true, errorMessage: '' })
try {
await ensureLogin()
const response = await request('/profile')
const profile = formatProfile(response.data)
this.setData({
profile,
recentLedger: (response.data.recentLedger || []).map(formatLedger),
})
} catch (error) {
this.setData({ errorMessage: error.message || '个人中心加载失败' })
} finally {
this.setData({ loading: false })
}
},
openOrders() {
wx.navigateTo({ url: '/pages/orders/list' })
},
})
function formatProfile(profile) {
return {
...profile,
registeredText: formatDate(profile.registeredAt),
lastLoginText: profile.lastLoginAt ? formatDate(profile.lastLoginAt) : '暂无',
wallet: {
...profile.wallet,
cashText: cents(profile.wallet.cashBalanceCents),
giftText: cents(profile.wallet.giftBalanceCents),
totalText: cents(profile.wallet.totalBalanceCents),
},
benefits: {
...profile.benefits,
packageAmountText: cents(profile.benefits.packageAmountCents),
},
recharge: {
...profile.recharge,
creditedText: cents(profile.recharge.creditedRechargeCents),
giftedText: cents(profile.recharge.giftedRechargeCents),
},
orders: {
...profile.orders,
paidAmountText: cents(profile.orders.paidAmountCents),
},
}
}
function formatLedger(item) {
return {
...item,
deltaText: `${signedCents(item.cashDeltaCents)} / ${signedCents(item.giftDeltaCents)}`,
balanceText: `${cents(item.cashBalanceAfterCents)} / ${cents(item.giftBalanceAfterCents)}`,
createdText: formatDate(item.createdAt),
}
}
function signedCents(value) {
const amount = Number(value || 0)
return `${amount >= 0 ? '+' : '-'}${cents(Math.abs(amount))}`
}
function formatDate(value) {
const date = new Date(value)
const pad = (input) => String(input).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
}
+3
View File
@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "个人中心"
}
+88
View File
@@ -0,0 +1,88 @@
<scroll-view class="scrollarea" scroll-y type="list">
<view class="page">
<view class="title">个人中心</view>
<view wx:if="{{errorMessage}}" class="error">{{errorMessage}}</view>
<view wx:if="{{profile}}" class="card">
<view class="member-name">{{profile.nickname || '微信用户'}}</view>
<view class="muted">{{profile.maskedPhone || '未绑定手机号'}}</view>
<view class="muted">注册:{{profile.registeredText}}</view>
<view class="muted">最近登录:{{profile.lastLoginText}}</view>
</view>
<view wx:if="{{profile}}" class="balance-band">
<view>
<view class="label">总余额</view>
<view class="balance">{{profile.wallet.totalText}}</view>
</view>
<view class="balance-split">
<view>现金 {{profile.wallet.cashText}}</view>
<view>赠送 {{profile.wallet.giftText}}</view>
</view>
</view>
<view wx:if="{{profile}}" class="metric-grid">
<view class="metric">
<view class="metric-value">{{profile.benefits.availableCoupons}}</view>
<view class="metric-label">可用券</view>
</view>
<view class="metric">
<view class="metric-value">{{profile.benefits.activePackages}}</view>
<view class="metric-label">套餐</view>
</view>
<view class="metric">
<view class="metric-value">{{profile.benefits.packageMinutes}}</view>
<view class="metric-label">分钟</view>
</view>
<view class="metric">
<view class="metric-value">{{profile.orders.orderCount}}</view>
<view class="metric-label">订单</view>
</view>
</view>
<view wx:if="{{profile}}" class="card">
<view class="section-title">权益</view>
<view class="row">
<text>冻结券</text>
<text>{{profile.benefits.frozenCoupons}}</text>
</view>
<view class="row">
<text>冻结套餐</text>
<text>{{profile.benefits.frozenPackages}}</text>
</view>
<view class="row">
<text>套餐余额</text>
<text>{{profile.benefits.packageAmountText}}</text>
</view>
</view>
<view wx:if="{{profile}}" class="card">
<view class="section-title">充值与消费</view>
<view class="row">
<text>已充值</text>
<text>{{profile.recharge.creditedText}}</text>
</view>
<view class="row">
<text>已赠送</text>
<text>{{profile.recharge.giftedText}}</text>
</view>
<view class="row">
<text>已消费</text>
<text>{{profile.orders.paidAmountText}}</text>
</view>
<button bindtap="openOrders">查看订单</button>
</view>
<view class="section-title ledger-title">最近账单</view>
<view wx:if="{{!loading && recentLedger.length === 0}}" class="empty">暂无账单</view>
<view wx:for="{{recentLedger}}" wx:key="ledgerId" class="ledger-item">
<view class="row">
<text>{{item.entryType}}</text>
<text>{{item.deltaText}}</text>
</view>
<view class="muted">{{item.businessType}} {{item.businessId}}</view>
<view class="muted">余额 {{item.balanceText}}</view>
<view class="muted">{{item.createdText}}</view>
</view>
</view>
</scroll-view>
+112
View File
@@ -0,0 +1,112 @@
.scrollarea {
height: 100vh;
background: #f5f6f8;
}
.page {
padding: 32rpx;
}
.title {
margin-bottom: 24rpx;
font-size: 40rpx;
font-weight: 600;
}
.card,
.ledger-item {
margin-top: 20rpx;
padding: 28rpx;
border-radius: 16rpx;
background: #ffffff;
}
.member-name,
.section-title {
font-size: 32rpx;
font-weight: 600;
}
.muted {
margin-top: 8rpx;
color: #667085;
}
.balance-band {
display: flex;
justify-content: space-between;
gap: 24rpx;
margin-top: 20rpx;
padding: 32rpx;
border-radius: 16rpx;
background: #174a3f;
color: #ffffff;
}
.label,
.balance-split {
color: rgba(255, 255, 255, 0.76);
}
.balance {
margin-top: 8rpx;
font-size: 48rpx;
font-weight: 700;
}
.balance-split {
display: flex;
flex-direction: column;
justify-content: center;
gap: 8rpx;
text-align: right;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12rpx;
margin-top: 20rpx;
}
.metric {
min-height: 112rpx;
padding: 18rpx 8rpx;
border-radius: 12rpx;
background: #ffffff;
text-align: center;
}
.metric-value {
color: #1f6feb;
font-size: 32rpx;
font-weight: 700;
}
.metric-label {
margin-top: 6rpx;
color: #667085;
font-size: 24rpx;
}
.row {
display: flex;
justify-content: space-between;
gap: 24rpx;
margin-top: 16rpx;
}
.ledger-title {
margin-top: 28rpx;
}
.empty {
padding: 80rpx 0;
color: #888888;
text-align: center;
}
.error {
margin: 16rpx 0;
color: #c73535;
}