微信小程序页面迁移校验之前 P5任务处理之前

This commit is contained in:
Neo
2026-03-09 01:19:21 +08:00
parent 263bf96035
commit 6e20987d2f
1112 changed files with 153824 additions and 219694 deletions

View File

@@ -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"
}
}

View File

@@ -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()
},
})

View File

@@ -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 />

View File

@@ -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;
}