微信小程序页面迁移校验之前 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,9 @@
{
"navigationBarTitleText": "业绩明细",
"enablePullDownRefresh": true,
"usingComponents": {
"ai-float-button": "/components/ai-float-button/ai-float-button",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-loading": "tdesign-miniprogram/loading/loading"
}
}

View File

@@ -0,0 +1,189 @@
import { mockPerformanceRecords } from '../../utils/mock-data'
import type { PerformanceRecord } from '../../utils/mock-data'
/** 按日期分组后的展示结构 */
interface DateGroup {
date: string
totalHours: string
totalIncome: string
records: RecordItem[]
}
interface RecordItem {
id: string
customerName: string
avatarChar: string
avatarGradient: string
timeRange: string
hours: string
courseType: string
courseTypeClass: string
location: string
income: string
}
const GRADIENT_POOL = [
'from-blue', 'from-pink', 'from-teal', 'from-green',
'from-orange', 'from-purple', 'from-violet', 'from-amber',
]
/** 根据名字首字生成稳定的渐变色 */
function nameToGradient(name: string): string {
const code = name.charCodeAt(0) || 0
return GRADIENT_POOL[code % GRADIENT_POOL.length]
}
Page({
data: {
pageState: 'loading' as 'loading' | 'empty' | 'normal',
/** Banner */
coachName: '小燕',
coachLevel: '星级',
storeName: '球会名称店',
/** 月份切换 */
currentYear: 2026,
currentMonth: 2,
monthLabel: '2026年2月',
canGoPrev: true,
canGoNext: false,
/** 统计概览 */
totalCount: '0笔',
totalHours: '0h',
totalIncome: '¥0',
/** 按日期分组的记录 */
dateGroups: [] as DateGroup[],
/** 所有记录(用于筛选) */
allRecords: [] as PerformanceRecord[],
/** 分页 */
page: 1,
pageSize: 20,
hasMore: false,
},
onLoad() {
this.loadData()
},
onPullDownRefresh() {
this.loadData(() => wx.stopPullDownRefresh())
},
onReachBottom() {
if (!this.data.hasMore) return
this.setData({ page: this.data.page + 1 })
this.loadData()
},
loadData(cb?: () => void) {
this.setData({ pageState: 'loading' })
setTimeout(() => {
// TODO: 替换为真实 API按月份请求
const allRecords = mockPerformanceRecords
// 模拟按日期分组的服务记录
const dateGroups: DateGroup[] = [
{
date: '2月7日',
totalHours: '6.0h',
totalIncome: '¥510',
records: [
{ id: 'r1', customerName: '王先生', avatarChar: '王', avatarGradient: nameToGradient('王'), timeRange: '20:00-22:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '3号台', income: '¥160' },
{ id: 'r2', customerName: '李女士', avatarChar: '李', avatarGradient: nameToGradient('李'), timeRange: '16:00-18:00', hours: '2.0h', courseType: '包厢课', courseTypeClass: 'tag-vip', location: 'VIP1号房', income: '¥190' },
{ id: 'r3', customerName: '陈女士', avatarChar: '陈', avatarGradient: nameToGradient('陈'), timeRange: '10:00-12:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '2号台', income: '¥160' },
],
},
{
date: '2月6日',
totalHours: '3.5h',
totalIncome: '¥280',
records: [
{ id: 'r4', customerName: '张先生', avatarChar: '张', avatarGradient: nameToGradient('张'), timeRange: '19:00-21:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '5号台', income: '¥160' },
{ id: 'r5', customerName: '刘先生', avatarChar: '刘', avatarGradient: nameToGradient('刘'), timeRange: '15:30-17:00', hours: '1.5h', courseType: '打赏课', courseTypeClass: 'tag-tip', location: '打赏', income: '¥120' },
],
},
{
date: '2月5日',
totalHours: '4.0h',
totalIncome: '¥320',
records: [
{ id: 'r6', customerName: '陈女士', avatarChar: '陈', avatarGradient: nameToGradient('陈'), timeRange: '20:00-22:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '2号台', income: '¥160' },
{ id: 'r7', customerName: '赵先生', avatarChar: '赵', avatarGradient: nameToGradient('赵'), timeRange: '14:00-16:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '7号台', income: '¥160' },
],
},
{
date: '2月4日',
totalHours: '4.0h',
totalIncome: '¥350',
records: [
{ id: 'r8', customerName: '孙先生', avatarChar: '孙', avatarGradient: nameToGradient('孙'), timeRange: '19:00-21:00', hours: '2.0h', courseType: '包厢课', courseTypeClass: 'tag-vip', location: 'VIP2号房', income: '¥190' },
{ id: 'r9', customerName: '吴女士', avatarChar: '吴', avatarGradient: nameToGradient('吴'), timeRange: '15:00-17:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '1号台', income: '¥160' },
],
},
{
date: '2月3日',
totalHours: '3.5h',
totalIncome: '¥280',
records: [
{ id: 'r10', customerName: '郑先生', avatarChar: '郑', avatarGradient: nameToGradient('郑'), timeRange: '20:00-22:00', hours: '2.0h', courseType: '基础课', courseTypeClass: 'tag-basic', location: '4号台', income: '¥160' },
{ id: 'r11', customerName: '黄女士', avatarChar: '黄', avatarGradient: nameToGradient('黄'), timeRange: '14:30-16:00', hours: '1.5h', courseType: '打赏课', courseTypeClass: 'tag-tip', location: '打赏', income: '¥120' },
],
},
]
this.setData({
pageState: dateGroups.length > 0 ? 'normal' : 'empty',
allRecords,
dateGroups,
totalCount: '32笔',
totalHours: '59.0h',
totalIncome: '¥4,720',
hasMore: false,
})
cb?.()
}, 500)
},
/** 切换月份 */
switchMonth(e: WechatMiniprogram.TouchEvent) {
const direction = e.currentTarget.dataset.direction as 'prev' | 'next'
let { currentYear, currentMonth } = this.data
if (direction === 'prev') {
currentMonth--
if (currentMonth < 1) {
currentMonth = 12
currentYear--
}
} else {
currentMonth++
if (currentMonth > 12) {
currentMonth = 1
currentYear++
}
}
// 不能超过当前月
const now = new Date()
const nowYear = now.getFullYear()
const nowMonth = now.getMonth() + 1
const canGoNext = currentYear < nowYear || (currentYear === nowYear && currentMonth < nowMonth)
this.setData({
currentYear,
currentMonth,
monthLabel: `${currentYear}${currentMonth}`,
canGoNext,
canGoPrev: true,
})
this.loadData()
},
})

View File

@@ -0,0 +1,104 @@
<!-- 加载态 -->
<view class="page-loading" wx:if="{{pageState === 'loading'}}">
<t-loading theme="circular" size="80rpx" text="加载中..." />
</view>
<block wx:else>
<!-- Banner -->
<view class="banner-section">
<view class="banner-bg"></view>
<view class="banner-content">
<view class="coach-info">
<view class="coach-avatar">
<text class="avatar-emoji">👤</text>
</view>
<view class="coach-meta">
<view class="coach-name-row">
<text class="coach-name">{{coachName}}</text>
<text class="coach-level-tag">{{coachLevel}}</text>
</view>
<text class="coach-store">{{storeName}}</text>
</view>
</view>
</view>
</view>
<!-- 月份切换 -->
<view class="month-switcher">
<view class="month-btn {{canGoPrev ? '' : 'month-btn-disabled'}}" data-direction="prev" bindtap="switchMonth">
<t-icon name="chevron-left" size="32rpx" />
</view>
<text class="month-label">{{monthLabel}}</text>
<view class="month-btn {{canGoNext ? '' : 'month-btn-disabled'}}" data-direction="next" bindtap="switchMonth">
<t-icon name="chevron-right" size="32rpx" />
</view>
</view>
<!-- 统计概览 -->
<view class="stats-overview">
<view class="stat-item">
<text class="stat-label">总记录</text>
<text class="stat-value">{{totalCount}}</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-label">总业绩时长</text>
<text class="stat-value stat-primary">{{totalHours}}</text>
<text class="stat-hint">预估</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-label">收入</text>
<text class="stat-value stat-success">{{totalIncome}}</text>
<text class="stat-hint">预估</text>
</view>
</view>
<!-- 空态 -->
<view class="page-empty" wx:if="{{pageState === 'empty'}}">
<t-icon name="chart-bar" size="120rpx" color="#dcdcdc" />
<text class="empty-text">暂无数据</text>
</view>
<!-- 记录列表 -->
<view class="records-container" wx:elif="{{pageState === 'normal'}}">
<view class="records-card">
<block wx:for="{{dateGroups}}" wx:key="date">
<!-- 日期分隔线 -->
<view class="date-divider">
<text class="dd-date">{{item.date}}</text>
<view class="dd-line"></view>
<text class="dd-stats">时长 {{item.totalHours}} · 预估收入 {{item.totalIncome}}</text>
</view>
<!-- 该日期下的记录 -->
<view class="record-item" wx:for="{{item.records}}" wx:for-item="rec" wx:key="id">
<view class="record-avatar avatar-{{rec.avatarGradient}}">
<text>{{rec.avatarChar}}</text>
</view>
<view class="record-content">
<view class="record-top">
<view class="record-name-time">
<text class="record-name">{{rec.customerName}}</text>
<text class="record-time">{{rec.timeRange}}</text>
</view>
<text class="record-hours">{{rec.hours}}</text>
</view>
<view class="record-bottom">
<view class="record-tags">
<text class="course-tag {{rec.courseTypeClass}}">{{rec.courseType}}</text>
<text class="record-location">{{rec.location}}</text>
</view>
<text class="record-income">我的预估收入 <text class="record-income-val">{{rec.income}}</text></text>
</view>
</view>
</view>
</block>
</view>
</view>
</block>
<!-- AI 悬浮按钮 -->
<ai-float-button bottom="{{120}}" />
<dev-fab />

View File

@@ -0,0 +1,347 @@
/* ============================================
* 加载态 / 空态
* ============================================ */
.page-loading {
display: flex;
justify-content: center;
align-items: center;
height: 60vh;
}
.page-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
gap: 24rpx;
}
.empty-text {
font-size: var(--font-sm);
color: var(--color-gray-6);
}
/* ============================================
* Banner
* ============================================ */
.banner-section {
position: relative;
width: 100%;
overflow: hidden;
}
.banner-bg {
width: 100%;
height: 280rpx;
background: linear-gradient(135deg, #ff6b4a, #ff8a65);
display: block;
}
.banner-content {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 16rpx 40rpx 32rpx;
}
.coach-info {
display: flex;
align-items: center;
gap: 24rpx;
margin-top: 16rpx;
}
.coach-avatar {
width: 112rpx;
height: 112rpx;
border-radius: 24rpx;
background: rgba(255, 255, 255, 0.2);
overflow: hidden;
flex-shrink: 0;
}
.avatar-img {
width: 100%;
height: 100%;
}
.avatar-emoji {
font-size: 56rpx;
line-height: 1;
}
.coach-meta {
flex: 1;
min-width: 0;
}
.coach-name-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 8rpx;
}
.coach-name {
font-size: 36rpx;
font-weight: 600;
color: #ffffff;
}
.coach-level-tag {
padding: 4rpx 16rpx;
background: rgba(251, 191, 36, 0.3);
color: #fef3c7;
border-radius: 24rpx;
font-size: var(--font-xs);
}
.coach-store {
font-size: var(--font-xs);
color: rgba(255, 255, 255, 0.7);
}
/* ============================================
* 月份切换
* ============================================ */
.month-switcher {
display: flex;
align-items: center;
justify-content: center;
gap: 48rpx;
padding: 24rpx 32rpx;
background: #ffffff;
border-bottom: 2rpx solid var(--color-gray-2);
}
.month-btn {
padding: 12rpx;
border-radius: 50%;
}
.month-btn-disabled {
opacity: 0.3;
pointer-events: none;
}
.month-label {
font-size: var(--font-sm);
font-weight: 600;
color: var(--color-gray-13);
}
/* ============================================
* 统计概览
* ============================================ */
.stats-overview {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 24rpx 32rpx;
background: #ffffff;
border-bottom: 2rpx solid var(--color-gray-2);
}
.stat-item {
flex: 1;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.stat-label {
font-size: 20rpx;
color: var(--color-gray-6);
margin-bottom: 4rpx;
}
.stat-value {
font-size: 36rpx;
font-weight: 700;
color: var(--color-gray-13);
font-variant-numeric: tabular-nums;
}
.stat-primary {
color: var(--color-primary);
}
.stat-success {
color: var(--color-success);
}
.stat-hint {
font-size: 20rpx;
color: var(--color-warning);
margin-top: 2rpx;
}
.stat-divider {
width: 2rpx;
height: 80rpx;
background: var(--color-gray-2);
margin-top: 4rpx;
}
/* ============================================
* 记录列表
* ============================================ */
.records-container {
padding: 24rpx;
}
.records-card {
background: #ffffff;
border-radius: 32rpx;
box-shadow: var(--shadow-lg);
overflow: hidden;
}
.date-divider {
display: flex;
align-items: center;
gap: 12rpx;
padding: 20rpx 32rpx 8rpx;
}
.dd-date {
font-size: 22rpx;
color: var(--color-gray-7);
font-weight: 500;
white-space: nowrap;
}
.dd-line {
flex: 1;
height: 2rpx;
background: var(--color-gray-4);
}
.dd-stats {
font-size: 22rpx;
color: var(--color-gray-6);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.record-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 16rpx 32rpx;
}
.record-avatar {
width: 76rpx;
height: 76rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: var(--font-sm);
font-weight: 500;
flex-shrink: 0;
}
/* 头像渐变色 */
.avatar-from-blue { background: linear-gradient(135deg, #60a5fa, #6366f1); }
.avatar-from-pink { background: linear-gradient(135deg, #f472b6, #f43f5e); }
.avatar-from-teal { background: linear-gradient(135deg, #2dd4bf, #10b981); }
.avatar-from-green { background: linear-gradient(135deg, #4ade80, #14b8a6); }
.avatar-from-orange { background: linear-gradient(135deg, #fb923c, #f59e0b); }
.avatar-from-purple { background: linear-gradient(135deg, #c084fc, #8b5cf6); }
.avatar-from-violet { background: linear-gradient(135deg, #a78bfa, #7c3aed); }
.avatar-from-amber { background: linear-gradient(135deg, #fbbf24, #eab308); }
.record-content {
flex: 1;
min-width: 0;
}
.record-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.record-name-time {
display: flex;
align-items: center;
gap: 12rpx;
min-width: 0;
}
.record-name {
font-size: var(--font-sm);
font-weight: 500;
color: var(--color-gray-13);
flex-shrink: 0;
}
.record-time {
font-size: var(--font-xs);
color: var(--color-gray-6);
}
.record-hours {
font-size: var(--font-sm);
font-weight: 700;
color: #059669;
font-variant-numeric: tabular-nums;
flex-shrink: 0;
}
.record-bottom {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8rpx;
}
.record-tags {
display: flex;
align-items: center;
gap: 12rpx;
}
.course-tag {
padding: 2rpx 12rpx;
border-radius: 8rpx;
font-size: 22rpx;
font-weight: 500;
}
.tag-basic {
background: #ecfdf5;
color: #15803d;
}
.tag-vip {
background: #eff6ff;
color: #1d4ed8;
}
.tag-tip {
background: #fffbeb;
color: #a16207;
}
.record-location {
font-size: var(--font-xs);
color: var(--color-gray-7);
}
.record-income {
font-size: 22rpx;
color: var(--color-gray-5);
flex-shrink: 0;
}
.record-income-val {
font-weight: 500;
color: var(--color-gray-9);
}