微信小程序页面迁移校验之前 P5任务处理之前
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"navigationBarTitleText": "服务记录",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {
|
||||
"ai-float-button": "/components/ai-float-button/ai-float-button",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-empty": "tdesign-miniprogram/empty/empty"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { mockCustomerDetail, mockCustomers } from '../../utils/mock-data'
|
||||
import type { ConsumptionRecord } from '../../utils/mock-data'
|
||||
import { sortByTimestamp } from '../../utils/sort'
|
||||
|
||||
/** 服务记录(含月份分组信息) */
|
||||
interface ServiceRecord extends ConsumptionRecord {
|
||||
/** 格式化后的日期,如 "2月5日" */
|
||||
dateLabel: string
|
||||
/** 时间段,如 "15:00 - 17:00" */
|
||||
timeRange: string
|
||||
/** 时长文本,如 "2.0h" */
|
||||
durationText: string
|
||||
/** 课程类型标签 */
|
||||
typeLabel: string
|
||||
/** 类型样式 class */
|
||||
typeClass: string
|
||||
/** 台号/房间 */
|
||||
tableNo: string
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
/** 页面状态 */
|
||||
pageState: 'loading' as 'loading' | 'empty' | 'normal',
|
||||
/** 客户 ID */
|
||||
customerId: '',
|
||||
/** 客户名 */
|
||||
customerName: '',
|
||||
/** 客户名首字 */
|
||||
customerInitial: '',
|
||||
/** 客户电话 */
|
||||
customerPhone: '139****5678',
|
||||
/** 累计服务次数 */
|
||||
totalServiceCount: 0,
|
||||
/** 关系指数 */
|
||||
relationIndex: '0.85',
|
||||
/** 当前月份标签 */
|
||||
monthLabel: '',
|
||||
/** 当前年 */
|
||||
currentYear: 2026,
|
||||
/** 当前月 */
|
||||
currentMonth: 2,
|
||||
/** 最小年月(数据起始) */
|
||||
minYearMonth: 202601,
|
||||
/** 最大年月(当前月) */
|
||||
maxYearMonth: 202602,
|
||||
/** 是否可切换上月 */
|
||||
canPrev: true,
|
||||
/** 是否可切换下月 */
|
||||
canNext: false,
|
||||
/** 月度统计 */
|
||||
monthCount: '6次',
|
||||
monthHours: '11.5h',
|
||||
monthRelation: '0.85',
|
||||
/** 当前月的服务记录 */
|
||||
records: [] as ServiceRecord[],
|
||||
/** 所有记录(原始) */
|
||||
allRecords: [] as ConsumptionRecord[],
|
||||
/** 是否还有更多 */
|
||||
hasMore: false,
|
||||
/** 加载更多中 */
|
||||
loadingMore: false,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const id = options?.customerId || options?.id || ''
|
||||
this.setData({ customerId: id })
|
||||
this.loadData(id)
|
||||
},
|
||||
|
||||
/** 加载数据 */
|
||||
loadData(id: string) {
|
||||
this.setData({ pageState: 'loading' })
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO: 替换为真实 API 调用
|
||||
const customer = mockCustomers.find((c) => c.id === id)
|
||||
const detail = customer
|
||||
? { ...mockCustomerDetail, id: customer.id, name: customer.name }
|
||||
: mockCustomerDetail
|
||||
|
||||
const allRecords = sortByTimestamp(detail.consumptionRecords || [], 'date') as ConsumptionRecord[]
|
||||
const name = detail.name || '客户'
|
||||
|
||||
this.setData({
|
||||
customerName: name,
|
||||
customerInitial: name[0] || '?',
|
||||
allRecords,
|
||||
totalServiceCount: allRecords.length,
|
||||
})
|
||||
|
||||
this.updateMonthView()
|
||||
}, 400)
|
||||
},
|
||||
|
||||
/** 根据当前月份筛选并更新视图 */
|
||||
updateMonthView() {
|
||||
const { currentYear, currentMonth, allRecords } = this.data
|
||||
const monthLabel = `${currentYear}年${currentMonth}月`
|
||||
|
||||
// 筛选当月记录
|
||||
const monthPrefix = `${currentYear}-${String(currentMonth).padStart(2, '0')}`
|
||||
const monthRecords = allRecords.filter((r) => r.date.startsWith(monthPrefix))
|
||||
|
||||
// 转换为展示格式
|
||||
const records: ServiceRecord[] = monthRecords.map((r) => {
|
||||
const d = new Date(r.date)
|
||||
const month = d.getMonth() + 1
|
||||
const day = d.getDate()
|
||||
return {
|
||||
...r,
|
||||
dateLabel: `${month}月${day}日`,
|
||||
timeRange: this.generateTimeRange(r.duration),
|
||||
durationText: (r.duration / 60).toFixed(1) + 'h',
|
||||
typeLabel: this.getTypeLabel(r.project),
|
||||
typeClass: this.getTypeClass(r.project),
|
||||
tableNo: this.getTableNo(r.id),
|
||||
}
|
||||
})
|
||||
|
||||
// 月度统计
|
||||
const totalMinutes = monthRecords.reduce((sum, r) => sum + r.duration, 0)
|
||||
const monthCount = monthRecords.length + '次'
|
||||
const monthHours = (totalMinutes / 60).toFixed(1) + 'h'
|
||||
|
||||
// 边界判断
|
||||
const yearMonth = currentYear * 100 + currentMonth
|
||||
const canPrev = yearMonth > this.data.minYearMonth
|
||||
const canNext = yearMonth < this.data.maxYearMonth
|
||||
|
||||
const isEmpty = records.length === 0 && allRecords.length === 0
|
||||
|
||||
this.setData({
|
||||
monthLabel,
|
||||
records,
|
||||
monthCount,
|
||||
monthHours,
|
||||
canPrev,
|
||||
canNext,
|
||||
pageState: isEmpty ? 'empty' : 'normal',
|
||||
})
|
||||
},
|
||||
|
||||
/** 生成模拟时间段 */
|
||||
generateTimeRange(durationMin: number): string {
|
||||
const startHour = 14 + Math.floor(Math.random() * 6)
|
||||
const endMin = startHour * 60 + durationMin
|
||||
const endHour = Math.floor(endMin / 60)
|
||||
const endMinute = endMin % 60
|
||||
return `${startHour}:00 - ${endHour}:${String(endMinute).padStart(2, '0')}`
|
||||
},
|
||||
|
||||
/** 课程类型标签 */
|
||||
getTypeLabel(project: string): string {
|
||||
if (project.includes('小组')) return '小组课'
|
||||
if (project.includes('1v1')) return '基础课'
|
||||
if (project.includes('充值')) return '充值'
|
||||
if (project.includes('斯诺克')) return '斯诺克'
|
||||
return '基础课'
|
||||
},
|
||||
|
||||
/** 课程类型样式 */
|
||||
getTypeClass(project: string): string {
|
||||
if (project.includes('充值')) return 'type-tip'
|
||||
if (project.includes('小组')) return 'type-vip'
|
||||
if (project.includes('斯诺克')) return 'type-vip'
|
||||
return 'type-basic'
|
||||
},
|
||||
|
||||
/** 模拟台号 */
|
||||
getTableNo(id: string): string {
|
||||
const tables = ['A12号台', '3号台', 'VIP1号房', '5号台', 'VIP2号房', '8号台']
|
||||
const idx = parseInt(id.replace(/\D/g, '') || '0', 10) % tables.length
|
||||
return tables[idx]
|
||||
},
|
||||
|
||||
/** 切换到上一月 */
|
||||
onPrevMonth() {
|
||||
if (!this.data.canPrev) return
|
||||
let { currentYear, currentMonth } = this.data
|
||||
currentMonth--
|
||||
if (currentMonth < 1) {
|
||||
currentMonth = 12
|
||||
currentYear--
|
||||
}
|
||||
this.setData({ currentYear, currentMonth })
|
||||
this.updateMonthView()
|
||||
},
|
||||
|
||||
/** 切换到下一月 */
|
||||
onNextMonth() {
|
||||
if (!this.data.canNext) return
|
||||
let { currentYear, currentMonth } = this.data
|
||||
currentMonth++
|
||||
if (currentMonth > 12) {
|
||||
currentMonth = 1
|
||||
currentYear++
|
||||
}
|
||||
this.setData({ currentYear, currentMonth })
|
||||
this.updateMonthView()
|
||||
},
|
||||
|
||||
/** 下拉刷新 */
|
||||
onPullDownRefresh() {
|
||||
this.loadData(this.data.customerId)
|
||||
setTimeout(() => wx.stopPullDownRefresh(), 600)
|
||||
},
|
||||
|
||||
/** 触底加载 */
|
||||
onReachBottom() {
|
||||
// Mock 阶段数据有限,不做分页
|
||||
if (this.data.loadingMore || !this.data.hasMore) return
|
||||
this.setData({ loadingMore: true })
|
||||
setTimeout(() => {
|
||||
this.setData({ loadingMore: false, hasMore: false })
|
||||
}, 500)
|
||||
},
|
||||
|
||||
/** 返回上一页 */
|
||||
onBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
<!-- 加载态 -->
|
||||
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
|
||||
<t-loading theme="circular" size="80rpx" text="加载中..." />
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view class="page-empty" wx:elif="{{pageState === 'empty'}}">
|
||||
<t-empty description="暂无服务记录" />
|
||||
</view>
|
||||
|
||||
<!-- 正常态 -->
|
||||
<block wx:elif="{{pageState === 'normal'}}">
|
||||
<!-- Banner 区域 -->
|
||||
<view class="banner-area">
|
||||
<view class="banner-bg"></view>
|
||||
<view class="banner-overlay">
|
||||
<!-- 导航栏 -->
|
||||
<view class="banner-nav">
|
||||
<view class="nav-back" bindtap="onBack">
|
||||
<t-icon name="chevron-left" size="48rpx" color="#ffffff" />
|
||||
</view>
|
||||
<text class="nav-title">服务记录</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<!-- 客户信息 -->
|
||||
<view class="customer-header">
|
||||
<view class="avatar-box">
|
||||
<text class="avatar-text">{{customerInitial}}</text>
|
||||
</view>
|
||||
<view class="info-right">
|
||||
<view class="name-row">
|
||||
<text class="customer-name">{{customerName}}</text>
|
||||
<text class="phone-text">{{customerPhone}}</text>
|
||||
</view>
|
||||
<view class="sub-stats">
|
||||
<text class="sub-stat">累计服务 <text class="stat-highlight">{{totalServiceCount}}</text> 次</text>
|
||||
<text class="sub-stat">关系指数 <text class="stat-highlight">{{relationIndex}}</text></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 月份切换 -->
|
||||
<view class="month-switcher">
|
||||
<view class="month-btn {{canPrev ? '' : 'disabled'}}" bindtap="onPrevMonth">
|
||||
<t-icon name="chevron-left" size="32rpx" color="{{canPrev ? '#777777' : '#dcdcdc'}}" />
|
||||
</view>
|
||||
<text class="month-label">{{monthLabel}}</text>
|
||||
<view class="month-btn {{canNext ? '' : 'disabled'}}" bindtap="onNextMonth">
|
||||
<t-icon name="chevron-right" size="32rpx" color="{{canNext ? '#777777' : '#dcdcdc'}}" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 月度统计概览 -->
|
||||
<view class="month-summary">
|
||||
<view class="summary-item">
|
||||
<text class="summary-label">本月服务</text>
|
||||
<text class="summary-value">{{monthCount}}</text>
|
||||
</view>
|
||||
<view class="summary-divider"></view>
|
||||
<view class="summary-item">
|
||||
<text class="summary-label">服务时长</text>
|
||||
<text class="summary-value value-primary">{{monthHours}}</text>
|
||||
</view>
|
||||
<view class="summary-divider"></view>
|
||||
<view class="summary-item">
|
||||
<text class="summary-label">关系指数</text>
|
||||
<text class="summary-value value-warning">{{monthRelation}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 记录列表 -->
|
||||
<view class="records-container">
|
||||
<view class="record-card" wx:for="{{records}}" wx:key="id">
|
||||
<view class="record-top">
|
||||
<view class="record-date-time">
|
||||
<text class="record-date">{{item.dateLabel}}</text>
|
||||
<text class="record-time">{{item.timeRange}}</text>
|
||||
</view>
|
||||
<text class="record-duration">{{item.durationText}}</text>
|
||||
</view>
|
||||
<view class="record-bottom">
|
||||
<text class="record-table">{{item.tableNo}}</text>
|
||||
<text class="record-type {{item.typeClass}}">{{item.typeLabel}}</text>
|
||||
<text class="record-income">¥{{item.amount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 无当月记录 -->
|
||||
<view class="no-month-data" wx:if="{{records.length === 0}}">
|
||||
<text class="no-month-text">本月暂无服务记录</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部提示 -->
|
||||
<view class="list-footer" wx:if="{{records.length > 0}}">
|
||||
<text class="footer-text">— 已加载全部记录 —</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view class="loading-more" wx:if="{{loadingMore}}">
|
||||
<t-loading theme="circular" size="40rpx" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI 悬浮按钮 -->
|
||||
<ai-float-button bottom="{{120}}" customerId="{{customerId}}" />
|
||||
</block>
|
||||
|
||||
<dev-fab />
|
||||
@@ -0,0 +1,283 @@
|
||||
/* ========== 加载态 & 空态 ========== */
|
||||
.page-loading,
|
||||
.page-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
/* ========== Banner ========== */
|
||||
.banner-area {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.banner-bg {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
background: linear-gradient(135deg, #ff6b4a, #ff8a65);
|
||||
display: block;
|
||||
}
|
||||
.banner-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.banner-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
.nav-back {
|
||||
padding: 8rpx;
|
||||
}
|
||||
.nav-title {
|
||||
font-size: var(--font-base, 32rpx);
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
.nav-placeholder {
|
||||
width: 64rpx;
|
||||
}
|
||||
|
||||
/* 客户头部 */
|
||||
.customer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 8rpx 40rpx 32rpx;
|
||||
}
|
||||
.avatar-box {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 32rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.avatar-text {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
.info-right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.customer-name {
|
||||
font-size: var(--font-lg, 36rpx);
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
.phone-text {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.sub-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
}
|
||||
.sub-stat {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.stat-highlight {
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
}
|
||||
|
||||
/* ========== 月份切换 ========== */
|
||||
.month-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 48rpx;
|
||||
background: #ffffff;
|
||||
padding: 24rpx 32rpx;
|
||||
border-bottom: 1rpx solid var(--color-gray-2, #eeeeee);
|
||||
}
|
||||
.month-btn {
|
||||
padding: 12rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.month-btn.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
.month-label {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
|
||||
/* ========== 月度统计 ========== */
|
||||
.month-summary {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background: #ffffff;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid var(--color-gray-2, #eeeeee);
|
||||
}
|
||||
.summary-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.summary-label {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
.summary-value {
|
||||
display: block;
|
||||
font-size: var(--font-lg, 36rpx);
|
||||
font-weight: 700;
|
||||
color: var(--color-gray-13, #242424);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.value-primary {
|
||||
color: var(--color-primary, #0052d9);
|
||||
}
|
||||
.value-warning {
|
||||
color: var(--color-warning, #ed7b2f);
|
||||
}
|
||||
.summary-divider {
|
||||
width: 1rpx;
|
||||
height: 64rpx;
|
||||
background: var(--color-gray-2, #eeeeee);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* ========== 记录列表 ========== */
|
||||
.records-container {
|
||||
padding: 24rpx 32rpx;
|
||||
padding-bottom: 160rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.record-card {
|
||||
background: #ffffff;
|
||||
border-radius: 28rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.04);
|
||||
border: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.record-card:active {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.record-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.record-date-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.record-date {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-13, #242424);
|
||||
}
|
||||
.record-time {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
.record-duration {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: var(--color-success, #00a870);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.record-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.record-table {
|
||||
font-size: var(--font-xs, 24rpx);
|
||||
color: var(--color-primary, #0052d9);
|
||||
background: var(--color-primary-light, #ecf2fe);
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.record-type {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.type-basic {
|
||||
background: rgba(0, 168, 112, 0.1);
|
||||
color: var(--color-success, #00a870);
|
||||
}
|
||||
.type-vip {
|
||||
background: rgba(0, 82, 217, 0.1);
|
||||
color: var(--color-primary, #0052d9);
|
||||
}
|
||||
.type-tip {
|
||||
background: rgba(237, 123, 47, 0.1);
|
||||
color: var(--color-warning, #ed7b2f);
|
||||
}
|
||||
.record-income {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
font-weight: 700;
|
||||
color: var(--color-gray-13, #242424);
|
||||
margin-left: auto;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* 无当月数据 */
|
||||
.no-month-data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 0;
|
||||
}
|
||||
.no-month-text {
|
||||
font-size: var(--font-sm, 28rpx);
|
||||
color: var(--color-gray-6, #a6a6a6);
|
||||
}
|
||||
|
||||
/* 底部提示 */
|
||||
.list-footer {
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
.footer-text {
|
||||
font-size: 20rpx;
|
||||
color: var(--color-gray-5, #c5c5c5);
|
||||
}
|
||||
|
||||
/* 加载更多 */
|
||||
.loading-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
Reference in New Issue
Block a user