feat: batch update - gift card breakdown spec, backend APIs, miniprogram pages, ETL finance recharge, docs & migrations

This commit is contained in:
Neo
2026-03-20 01:43:48 +08:00
parent 075caf067f
commit 79f9a0e1da
437 changed files with 118603 additions and 976 deletions

View File

@@ -0,0 +1,13 @@
{
"navigationBarTitleText": "业绩明细",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": true,
"usingComponents": {
"coach-level-tag": "/components/coach-level-tag/coach-level-tag",
"ai-float-button": "/components/ai-float-button/ai-float-button",
"dev-fab": "/components/dev-fab/dev-fab",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-loading": "tdesign-miniprogram/loading/loading"
}
}

View File

@@ -0,0 +1,272 @@
import { mockPerformanceRecords } from '../../utils/mock-data'
import type { PerformanceRecord } from '../../utils/mock-data'
import { nameToAvatarColor } from '../../utils/avatar-color'
import { formatMoney, formatCount } from '../../utils/money'
import { formatHours } from '../../utils/time'
/** 按日期分组后的展示结构 */
interface DateGroup {
date: string
totalHours: number
totalIncome: number
totalHoursLabel: string
totalIncomeLabel: string
records: RecordItem[]
}
interface RecordItem {
id: string
customerName: string
avatarChar: string
avatarColor: string
timeRange: string
hours: number // 折算后课时小时number
hoursRaw?: number // 折算前课时小时number可选
courseType: string
courseTypeClass: string
location: string
income: number // 收入(元,整数)
}
Page({
data: {
pageState: 'loading' as 'loading' | 'empty' | 'error' | 'normal',
/** Banner */
coachName: '小燕',
coachLevel: '星级',
storeName: '球会名称店',
/** 月份切换 */
currentYear: 2026,
currentMonth: 2,
monthLabel: '2026年2月',
canGoPrev: true,
canGoNext: false,
/** 统计概览 */
totalCount: 0,
totalHours: 0,
totalIncome: 0,
totalCountLabel: '--',
totalHoursLabel: '--',
totalHoursRawLabel: '',
totalIncomeLabel: '--',
/** 按日期分组的记录 */
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(() => {
try {
// TODO: 替换为真实 API按月份请求
const allRecords = mockPerformanceRecords
const dateGroups: DateGroup[] = [
{
date: '2月7日',
totalHours: 6.0, totalHoursLabel: formatHours(6.0), totalIncomeLabel: formatMoney(510),
totalIncome: 510,
records: [
{ id: 'r1', customerName: '王先生', avatarChar: '王', avatarColor: nameToAvatarColor('王'), timeRange: '20:00-22:00', hours: 2.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '3号台', income: 160 },
{ id: 'r2', customerName: '李女士', avatarChar: '李', avatarColor: nameToAvatarColor('李'), timeRange: '16:00-18:00', hours: 2.0, courseType: '包厢课', courseTypeClass: 'tag-vip', location: 'VIP1号房', income: 190 },
{ id: 'r3', customerName: '陈女士', avatarChar: '陈', avatarColor: nameToAvatarColor('陈'), timeRange: '10:00-12:00', hours: 2.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '2号台', income: 160 },
],
},
{
date: '2月6日',
totalHours: 3.5, totalHoursLabel: formatHours(3.5), totalIncomeLabel: formatMoney(280),
totalIncome: 280,
records: [
{ id: 'r4', customerName: '张先生', avatarChar: '张', avatarColor: nameToAvatarColor('张'), timeRange: '19:00-21:00', hours: 2.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '5号台', income: 160 },
{ id: 'r5', customerName: '刘先生', avatarChar: '刘', avatarColor: nameToAvatarColor('刘'), timeRange: '15:30-17:00', hours: 1.5, courseType: '打赏课', courseTypeClass: 'tag-tip', location: '打赏', income: 120 },
],
},
{
date: '2月5日',
totalHours: 4.0, totalHoursLabel: formatHours(4.0), totalIncomeLabel: formatMoney(320),
totalIncome: 320,
records: [
{ id: 'r6', customerName: '陈女士', avatarChar: '陈', avatarColor: nameToAvatarColor('陈'), timeRange: '20:00-22:00', hours: 2.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '2号台', income: 160 },
{ id: 'r7', customerName: '赵先生', avatarChar: '赵', avatarColor: nameToAvatarColor('赵'), timeRange: '14:00-16:00', hours: 2.0, hoursRaw: 2.5, courseType: '基础课', courseTypeClass: 'tag-basic', location: '7号台', income: 160 },
],
},
{
date: '2月4日',
totalHours: 4.0, totalHoursLabel: formatHours(4.0), totalIncomeLabel: formatMoney(320),
totalIncome: 350,
records: [
{ id: 'r8', customerName: '孙先生', avatarChar: '孙', avatarColor: nameToAvatarColor('孙'), timeRange: '19:00-21:00', hours: 2.0, courseType: '包厢课', courseTypeClass: 'tag-vip', location: 'VIP2号房', income: 190 },
{ id: 'r9', customerName: '吴女士', avatarChar: '吴', avatarColor: nameToAvatarColor('吴'), timeRange: '15:00-17:00', hours: 2.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '1号台', income: 160 },
],
},
{
date: '2月3日',
totalHours: 3.5, totalHoursLabel: formatHours(3.5), totalIncomeLabel: formatMoney(280),
totalIncome: 280,
records: [
{ id: 'r10', customerName: '郑先生', avatarChar: '郑', avatarColor: nameToAvatarColor('郑'), timeRange: '20:00-22:00', hours: 2.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '4号台', income: 160 },
{ id: 'r11', customerName: '黄女士', avatarChar: '黄', avatarColor: nameToAvatarColor('黄'), timeRange: '14:30-16:00', hours: 1.5, courseType: '打赏课', courseTypeClass: 'tag-tip', location: '打赏', income: 120 },
],
},
{
date: '2月2日',
totalHours: 4.0, totalHoursLabel: formatHours(4.0), totalIncomeLabel: formatMoney(320),
totalIncome: 350,
records: [
{ id: 'r12', customerName: '林先生', avatarChar: '林', avatarColor: nameToAvatarColor('林'), timeRange: '19:00-21:00', hours: 2.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '6号台', income: 160 },
{ id: 'r13', customerName: '何女士', avatarChar: '何', avatarColor: nameToAvatarColor('何'), timeRange: '13:00-15:00', hours: 2.0, courseType: '包厢课', courseTypeClass: 'tag-vip', location: 'VIP3号房', income: 190 },
],
},
{
date: '2月1日',
totalHours: 6.0, totalHoursLabel: formatHours(6.0), totalIncomeLabel: formatMoney(510),
totalIncome: 510,
records: [
{ id: 'r14', customerName: '王先生', avatarChar: '王', avatarColor: nameToAvatarColor('王'), timeRange: '20:30-22:30', hours: 2.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '3号台', income: 160 },
{ id: 'r15', customerName: '马先生', avatarChar: '马', avatarColor: nameToAvatarColor('马'), timeRange: '16:00-18:00', hours: 2.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '8号台', income: 160 },
{ id: 'r16', customerName: '罗女士', avatarChar: '罗', avatarColor: nameToAvatarColor('罗'), timeRange: '12:30-14:30', hours: 2.0, hoursRaw: 2.5, courseType: '基础课', courseTypeClass: 'tag-basic', location: '2号台', income: 160 },
{ id: 'r17', customerName: '梁先生', avatarChar: '梁', avatarColor: nameToAvatarColor('梁'), timeRange: '10:00-12:00', hours: 2.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '5号台', income: 160 },
{ id: 'r18', customerName: '宋女士', avatarChar: '宋', avatarColor: nameToAvatarColor('宋'), timeRange: '8:30-10:00', hours: 1.5, courseType: '打赏课', courseTypeClass: 'tag-tip', location: '打赏', income: 120 },
{ id: 'r19', customerName: '谢先生', avatarChar: '谢', avatarColor: nameToAvatarColor('谢'), timeRange: '7:00-8:00', hours: 1.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '1号台', income: 80 },
],
},
{
date: '1月31日',
totalHours: 5.5, totalHoursLabel: formatHours(5.5), totalIncome: 470, totalIncomeLabel: formatMoney(470),
records: [
{ id: 'r20', customerName: '韩女士', avatarChar: '韩', avatarColor: nameToAvatarColor('韩'), timeRange: '21:00-23:00', hours: 2.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '4号台', income: 160 },
{ id: 'r21', customerName: '唐先生', avatarChar: '唐', avatarColor: nameToAvatarColor('唐'), timeRange: '18:30-20:30', hours: 2.0, hoursRaw: 2.5, courseType: '包厢课', courseTypeClass: 'tag-vip', location: 'VIP2号房', income: 190 },
{ id: 'r22', customerName: '冯女士', avatarChar: '冯', avatarColor: nameToAvatarColor('冯'), timeRange: '14:00-16:00', hours: 2.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '6号台', income: 160 },
],
},
{
date: '1月30日',
totalHours: 3.5, totalHoursLabel: formatHours(3.5), totalIncomeLabel: formatMoney(280),
totalIncome: 280,
records: [
{ id: 'r23', customerName: '张先生', avatarChar: '张', avatarColor: nameToAvatarColor('张'), timeRange: '19:30-21:30', hours: 2.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '5号台', income: 160 },
{ id: 'r24', customerName: '刘先生', avatarChar: '刘', avatarColor: nameToAvatarColor('刘'), timeRange: '14:30-16:00', hours: 1.5, courseType: '打赏课', courseTypeClass: 'tag-tip', location: '打赏', income: 120 },
],
},
{
date: '1月29日',
totalHours: 4.0, totalHoursLabel: formatHours(4.0), totalIncomeLabel: formatMoney(320),
totalIncome: 320,
records: [
{ id: 'r25', customerName: '陈女士', avatarChar: '陈', avatarColor: nameToAvatarColor('陈'), timeRange: '20:00-22:00', hours: 2.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '2号台', income: 160 },
{ id: 'r26', customerName: '李女士', avatarChar: '李', avatarColor: nameToAvatarColor('李'), timeRange: '13:00-15:00', hours: 2.0, courseType: '包厢课', courseTypeClass: 'tag-vip', location: 'VIP1号房', income: 190 },
],
},
{
date: '1月28日',
totalHours: 4.0, totalHoursLabel: formatHours(4.0), totalIncomeLabel: formatMoney(320),
totalIncome: 350,
records: [
{ id: 'r27', customerName: '赵先生', avatarChar: '赵', avatarColor: nameToAvatarColor('赵'), timeRange: '19:00-21:00', hours: 2.0, hoursRaw: 2.5, courseType: '基础课', courseTypeClass: 'tag-basic', location: '7号台', income: 160 },
{ id: 'r28', customerName: '董先生', avatarChar: '董', avatarColor: nameToAvatarColor('董'), timeRange: '15:00-17:00', hours: 2.0, courseType: '基础课', courseTypeClass: 'tag-basic', location: '3号台', income: 160 },
],
},
{
date: '1月27日',
totalHours: 4.0, totalHoursLabel: formatHours(4.0), totalIncomeLabel: formatMoney(320),
totalIncome: 350,
records: [
{ id: 'r29', customerName: '孙先生', avatarChar: '孙', avatarColor: nameToAvatarColor('孙'), timeRange: '20:00-22:00', hours: 2.0, courseType: '包厢课', courseTypeClass: 'tag-vip', location: 'VIP2号房', income: 190 },
{ id: 'r30', customerName: '黄女士', avatarChar: '黄', avatarColor: nameToAvatarColor('黄'), timeRange: '14:30-16:30', hours: 1.5, courseType: '打赏课', courseTypeClass: 'tag-tip', location: '打赏', income: 120 },
],
},
]
this.setData({
pageState: dateGroups.length > 0 ? 'normal' : 'empty',
allRecords,
dateGroups,
totalCount: 32,
totalHours: 59.0,
totalIncome: 4720,
totalCountLabel: formatCount(32, '笔'),
totalHoursLabel: formatHours(59.0),
totalHoursRawLabel: formatHours(63.5),
totalIncomeLabel: formatMoney(4720),
hasMore: false,
})
} catch (_err) {
this.setData({ pageState: 'error' })
}
cb?.()
}, 500)
},
/** 重试加载 */
onRetry() {
this.loadData()
},
/** 返回上一页 */
onNavBack() {
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/task-list/task-list' }) })
},
/** 切换月份 */
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,130 @@
<wxs src="../../utils/format.wxs" module="fmt" />
<!-- toast 加载浮层fixed不销毁内容不白屏 -->
<view class="g-toast-loading" wx:if="{{pageState === 'loading'}}">
<view class="g-toast-loading-inner">
<t-loading theme="circular" size="40rpx" />
<text class="g-toast-loading-text">加载中...</text>
</view>
</view>
<!-- 错误态(全屏,仅在 error 时展示) -->
<view class="page-error" wx:if="{{pageState === 'error'}}">
<t-icon name="close-circle" size="120rpx" color="#e34d59" />
<text class="error-text">加载失败,请点击重试</text>
<view class="retry-btn" hover-class="retry-btn--hover" bindtap="onRetry">
<text>重试</text>
</view>
</view>
<!-- 主体内容(始终挂载,不随 loading 销毁) -->
<block wx:if="{{pageState !== 'error'}}">
<!-- Banner 区域(复用助教详情样式) -->
<view class="banner-section">
<image class="banner-bg-img" src="/assets/images/banner-bg-coral-aurora.svg" mode="widthFix" />
<view class="banner-overlay">
<view class="coach-header">
<view class="avatar-box">
<image class="avatar-img" src="/assets/images/avatar-coach.png" mode="aspectFill" />
</view>
<view class="info-middle">
<view class="name-row">
<text class="coach-name">{{coachName}}</text>
<coach-level-tag level="{{coachLevel}}" />
</view>
<view class="skill-row">
<text class="store-name-text">{{storeName}}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 月份切换 -->
<view class="month-switcher">
<view class="month-btn {{canGoPrev ? '' : 'month-btn-disabled'}}" hover-class="month-btn--hover" 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'}}" hover-class="month-btn--hover" 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">{{totalCountLabel}}</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-label">总业绩时长</text>
<text class="stat-value stat-primary">{{totalHoursLabel}}</text>
<text class="stat-hint">预估</text>
<text class="stat-hours-raw" wx:if="{{totalHoursRawLabel}}">折前 {{totalHoursRawLabel}}</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-label">收入</text>
<text class="stat-value stat-success">{{totalIncomeLabel}}</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' || pageState === 'loading'}}">
<view class="records-card">
<block wx:for="{{dateGroups}}" wx:key="date">
<!-- 日期分隔线 -->
<view class="date-divider">
<text decode class="dd-date">{{item.date}}&nbsp;&nbsp;—</text>
<text decode class="dd-stats" wx:if="{{item.totalHoursLabel}}">{{item.totalHoursLabel}}&nbsp;&nbsp;·&nbsp;&nbsp;预估 {{item.totalIncomeLabel}}&nbsp;&nbsp;&nbsp;&nbsp;</text>
<view class="dd-line"></view>
</view>
<!-- 该日期下的记录 -->
<view class="record-item" wx:for="{{item.records}}" wx:for-item="rec" wx:key="id"
hover-class="record-item--hover">
<view class="record-avatar avatar-{{rec.avatarColor}}">
<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>
<view class="record-hours-wrap">
<text class="record-hours">{{fmt.hours(rec.hours)}}</text>
<text class="record-hours-deduct" wx:if="{{rec.hoursRaw}}">(折后 {{fmt.hours(rec.hoursRaw)}}</text>
</view>
</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">{{fmt.money(rec.income)}}</text></text>
</view>
</view>
</view>
</block>
<!-- 列表底部提示 -->
<view class="list-end-hint">
<text>— 已加载全部记录 —</text>
</view>
</view>
</view>
</block>
<!-- AI 悬浮按钮 -->
<ai-float-button />
<dev-fab />

View File

@@ -0,0 +1,492 @@
/* pages/performance-records/performance-records.wxss */
page {
background-color: #f3f3f3;
line-height: 1.5;
}
view {
line-height: inherit;
}
/* ============================================
* 加载态 / 空态 / 错误态
* ============================================ */
/* toast 风格加载(浮在页面中央,不全屏变白) */
.toast-loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
pointer-events: none;
}
.toast-loading-inner {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
background: rgba(36, 36, 36, 0.75);
border-radius: 24rpx;
padding: 36rpx 48rpx;
pointer-events: auto;
}
.toast-loading-text {
font-size: 24rpx;
color: #ffffff;
line-height: 32rpx;
}
.page-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
gap: 24rpx;
}
.empty-text {
font-size: 26rpx;
color: #a6a6a6;
}
.page-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 60vh;
gap: 24rpx;
}
.error-text {
font-size: 28rpx;
color: #a6a6a6;
}
.retry-btn {
padding: 16rpx 48rpx;
background: #0052d9;
color: #ffffff;
border-radius: 16rpx;
font-size: 28rpx;
}
.retry-btn--hover {
opacity: 0.7;
}
/* ============================================
* Banner复用助教详情
* ============================================ */
.banner-section {
position: relative;
width: 100%;
overflow: hidden;
height: 100%;
}
.banner-bg-img {
position: absolute;
top: -50rpx;
left: 0;
width: 100%;
height: auto;
z-index: 0;
}
.banner-overlay {
position: relative;
z-index: 2;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
padding: 36rpx 40rpx;
}
.coach-header {
display: flex;
align-items: center;
gap: 32rpx;
}
.avatar-box {
width: 98rpx;
height: 98rpx;
border-radius: 32rpx;
background: rgba(255, 255, 255, 0.2);
overflow: hidden;
flex-shrink: 0;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
}
.avatar-img {
width: 100%;
height: 100%;
}
.info-middle {
flex: 1;
min-width: 0;
}
.name-row {
display: flex;
align-items: center;
gap: 14rpx;
margin-bottom: 8rpx;
}
.coach-name {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
}
.skill-row {
display: flex;
align-items: center;
gap: 14rpx;
}
.skill-tag {
padding: 4rpx 16rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 8rpx;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.9);
}
/* 助教等级 tag — 遵循 VI 规范§5 助教等级配色) */
.coach-level-tag {
padding: 4rpx 16rpx;
border-radius: 24rpx;
font-size: 22rpx;
font-weight: 500;
flex-shrink: 0;
line-height: 29rpx;
}
.coach-level-star { color: #fbbf24; background: #fffef0; }
.coach-level-senior { color: #e91e63; background: #ffe6e8; }
.coach-level-middle { color: #ed7b2f; background: #fff3e6; }
.coach-level-junior { color: #0052d9; background: #ecf2fe; }
/* 球会名称 — 纯文字,无背景 */
.store-name-text {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.75);
line-height: 29rpx;
}
/* ============================================
* 月份切换
* ============================================ */
.month-switcher {
display: flex;
align-items: center;
justify-content: center;
gap: 48rpx;
padding: 24rpx 32rpx;
background: #ffffff;
border-bottom: 2rpx solid #eeeeee;
}
.month-btn {
padding: 12rpx;
border-radius: 50%;
}
.month-btn-disabled {
opacity: 0.3;
pointer-events: none;
}
.month-btn--hover {
opacity: 0.6;
}
.month-label {
font-size: 28rpx;
font-weight: 600;
color: #242424;
}
/* ============================================
* 统计概览
* ============================================ */
.stats-overview {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 24rpx 32rpx;
background: #ffffff;
border-bottom: 2rpx solid #eeeeee;
}
.stat-item {
flex: 1;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.stat-label {
font-size: 20rpx;
color: #a6a6a6;
margin-bottom: 4rpx;
}
.stat-value {
font-size: 36rpx;
font-weight: 700;
color: #242424;
font-variant-numeric: tabular-nums;
}
.stat-primary {
color: #0052d9;
}
.stat-success {
color: #00a870;
}
.stat-sub-hint {
font-size: 20rpx;
color: #c5c5c5;
margin-top: 2rpx;
line-height: 26rpx;
}
.stat-hours-raw {
font-size: 20rpx;
color: #a6a6a6;
margin-top: 2rpx;
line-height: 26rpx;
}
.stat-hint {
font-size: 20rpx;
color: #ed7b2f;
margin-top: 2rpx;
}
.stat-divider {
width: 2rpx;
height: 80rpx;
background: #eeeeee;
margin-top: 4rpx;
}
/* ============================================
* 记录列表
* ============================================ */
.records-container {
padding: 24rpx;
padding-bottom: 40rpx;
}
.records-card {
background: #ffffff;
border-radius: 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.date-divider {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx 32rpx 8rpx;
}
.dd-date {
font-size: 22rpx;
color: #8b8b8b;
font-weight: 500;
white-space: nowrap;
line-height: 29rpx;
}
.dd-line {
flex: 1;
height: 2rpx;
background: #dcdcdc;
}
.dd-stats {
font-size: 22rpx;
color: #a6a6a6;
font-variant-numeric: tabular-nums;
white-space: nowrap;
line-height: 29rpx;
}
.record-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 16rpx 32rpx;
}
.record-item--hover {
background: #f7f7f7;
}
/* 头像 */
.record-avatar {
width: 76rpx;
height: 76rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 30rpx;
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); }
.avatar-from-sky { background: linear-gradient(135deg, #38bdf8, #0ea5e9); }
.avatar-from-lime { background: linear-gradient(135deg, #a3e635, #65a30d); }
.avatar-from-rose { background: linear-gradient(135deg, #fb7185, #e11d48); }
.avatar-from-fuchsia{ background: linear-gradient(135deg, #e879f9, #a21caf); }
.avatar-from-slate { background: linear-gradient(135deg, #94a3b8, #475569); }
.avatar-from-indigo { background: linear-gradient(135deg, #818cf8, #4338ca); }
.avatar-from-cyan { background: linear-gradient(135deg, #22d3ee, #0891b2); }
.avatar-from-yellow { background: linear-gradient(135deg, #facc15, #ca8a04); }
/* 内容区 */
.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: 26rpx;
font-weight: 500;
color: #242424;
flex-shrink: 0;
line-height: 36rpx;
}
.record-time {
font-size: 22rpx;
color: #a6a6a6;
line-height: 29rpx;
}
.record-hours-wrap {
display: flex;
align-items: baseline;
gap: 6rpx;
flex-shrink: 0;
}
.record-hours {
font-size: 26rpx;
font-weight: 700;
color: #059669;
font-variant-numeric: tabular-nums;
line-height: 36rpx;
}
.record-hours-deduct {
font-size: 20rpx;
color: #a6a6a6;
line-height: 29rpx;
}
.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;
line-height: 29rpx;
}
.tag-basic {
background: #ecfdf5;
color: #15803d;
}
.tag-vip {
background: #eff6ff;
color: #1d4ed8;
}
.tag-tip {
background: #fffbeb;
color: #a16207;
}
.record-location {
font-size: 22rpx;
color: #8b8b8b;
line-height: 29rpx;
}
.record-income {
font-size: 22rpx;
color: #c5c5c5;
flex-shrink: 0;
line-height: 29rpx;
}
.record-income-val {
font-weight: 500;
color: #5e5e5e;
}
/* 列表底部提示 */
.list-end-hint {
text-align: center;
padding: 24rpx 0 28rpx;
font-size: 22rpx;
color: #c5c5c5;
}