feat(M08-A): 接入顾客端个人中心
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
"pages/room/detail",
|
||||
"pages/orders/list",
|
||||
"pages/orders/detail",
|
||||
"pages/profile/index",
|
||||
"pages/logs/logs"
|
||||
],
|
||||
"window": {
|
||||
|
||||
@@ -69,6 +69,10 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/orders/list' })
|
||||
},
|
||||
|
||||
openProfile() {
|
||||
wx.navigateTo({ url: '/pages/profile/index' })
|
||||
},
|
||||
|
||||
async resolveScene(code, sourceType) {
|
||||
this.setData({ loading: true, errorMessage: '' })
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<scroll-view class="scrollarea" scroll-y type="list">
|
||||
<view class="container">
|
||||
<view class="title">选择门店</view>
|
||||
<button bindtap="openOrders">我的订单</button>
|
||||
<view class="quick-actions">
|
||||
<button bindtap="openOrders">我的订单</button>
|
||||
<button bindtap="openProfile">个人中心</button>
|
||||
</view>
|
||||
<button loading="{{locating}}" bindtap="locateNearby">定位附近门店</button>
|
||||
<view class="city-search">
|
||||
<input value="{{city}}" placeholder="拒绝定位时输入城市" bindinput="onCityInput" />
|
||||
|
||||
@@ -13,6 +13,16 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.quick-actions button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.city-search {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
|
||||
@@ -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())}`
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "个人中心"
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user