微信小程序页面迁移校验之前 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,12 @@
{
"navigationBarTitleText": "助教详情",
"usingComponents": {
"banner": "/components/banner/banner",
"note-modal": "/components/note-modal/note-modal",
"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",
"t-tag": "tdesign-miniprogram/tag/tag"
}
}

View File

@@ -0,0 +1,217 @@
import { mockCoaches } from '../../utils/mock-data'
import { sortByTimestamp } from '../../utils/sort'
/** 助教详情(含绩效、备注等扩展数据) */
interface CoachDetail {
id: string
name: string
avatar: string
level: string
skills: string[]
workYears: number
customerCount: number
/** 绩效指标 */
performance: {
monthlyHours: number
monthlySalary: number
customerBalance: number
tasksCompleted: number
}
/** 收入明细 */
income: {
thisMonth: IncomeItem[]
lastMonth: IncomeItem[]
}
/** 备注列表 */
notes: NoteItem[]
}
interface IncomeItem {
label: string
amount: string
color: string
}
interface NoteItem {
id: string
content: string
timestamp: string
score: number
customerName: string
}
/** 内联 Mock 数据:助教详情扩展 */
const mockCoachDetail: CoachDetail = {
id: 'coach-001',
name: '小燕',
avatar: '/assets/images/avatar-default.png',
level: '星级',
skills: ['中🎱', '🎯 斯诺克'],
workYears: 3,
customerCount: 68,
performance: {
monthlyHours: 87.5,
monthlySalary: 6950,
customerBalance: 86200,
tasksCompleted: 38,
},
income: {
thisMonth: [
{ label: '基础课时费', amount: '¥3,500', color: 'primary' },
{ label: '激励课时费', amount: '¥1,800', color: 'success' },
{ label: '充值提成', amount: '¥1,200', color: 'warning' },
{ label: '酒水提成', amount: '¥450', color: 'purple' },
],
lastMonth: [
{ label: '基础课时费', amount: '¥3,800', color: 'primary' },
{ label: '激励课时费', amount: '¥1,900', color: 'success' },
{ label: '充值提成', amount: '¥1,100', color: 'warning' },
{ label: '酒水提成', amount: '¥400', color: 'purple' },
],
},
notes: [
{ id: 'n1', content: '本月表现优秀,客户好评率高', timestamp: '2026-03-05 14:30', score: 9, customerName: '管理员' },
{ id: 'n2', content: '需要加强斯诺克教学技巧', timestamp: '2026-02-28 10:00', score: 7, customerName: '管理员' },
{ id: 'n3', content: '客户王先生反馈服务态度很好', timestamp: '2026-02-20 16:45', score: 8, customerName: '王先生' },
],
}
Page({
data: {
/** 页面状态 */
pageState: 'loading' as 'loading' | 'empty' | 'normal',
/** 助教 ID */
coachId: '',
/** 助教详情 */
detail: null as CoachDetail | null,
/** Banner 指标 */
bannerMetrics: [] as Array<{ label: string; value: string }>,
/** 绩效指标卡片 */
perfCards: [] as Array<{ label: string; value: string; unit: string; sub: string; bgClass: string; valueColor: string }>,
/** 收入明细 Tab */
incomeTab: 'this' as 'this' | 'last',
/** 当前收入明细 */
currentIncome: [] as IncomeItem[],
/** 当前收入合计 */
incomeTotal: '',
/** 排序后的备注列表 */
sortedNotes: [] as NoteItem[],
/** 备注弹窗 */
noteModalVisible: false,
},
onLoad(options) {
const id = options?.id || ''
this.setData({ coachId: id })
this.loadData(id)
},
loadData(id: string) {
this.setData({ pageState: 'loading' })
setTimeout(() => {
// TODO: 替换为真实 API 调用 GET /api/coaches/:id
const basicCoach = mockCoaches.find((c) => c.id === id)
const detail: CoachDetail = basicCoach
? { ...mockCoachDetail, id: basicCoach.id, name: basicCoach.name }
: mockCoachDetail
if (!detail) {
this.setData({ pageState: 'empty' })
return
}
const bannerMetrics = [
{ label: '工龄', value: `${detail.workYears}` },
{ label: '客户', value: `${detail.customerCount}` },
]
const perfCards = [
{ label: '本月定档业绩', value: `${detail.performance.monthlyHours}`, unit: 'h', sub: '折算前 89.0h', bgClass: 'perf-blue', valueColor: 'text-primary' },
{ label: '本月工资(预估)', value: `¥${detail.performance.monthlySalary.toLocaleString()}`, unit: '', sub: '含预估部分', bgClass: 'perf-green', valueColor: 'text-success' },
{ label: '客源储值余额', value: `¥${detail.performance.customerBalance.toLocaleString()}`, unit: '', sub: `${detail.customerCount}位客户合计`, bgClass: 'perf-orange', valueColor: 'text-warning' },
{ label: '本月任务完成', value: `${detail.performance.tasksCompleted}`, unit: '个', sub: '覆盖 22 位客户', bgClass: 'perf-purple', valueColor: 'text-purple' },
]
const sorted = sortByTimestamp(detail.notes || [], 'timestamp') as NoteItem[]
this.setData({
pageState: 'normal',
detail,
bannerMetrics,
perfCards,
sortedNotes: sorted,
})
this.switchIncomeTab('this')
}, 500)
},
/** 切换收入明细 Tab */
switchIncomeTab(tab: 'this' | 'last') {
const detail = this.data.detail
if (!detail) return
const items = tab === 'this' ? detail.income.thisMonth : detail.income.lastMonth
// 计算合计(从格式化金额中提取数字)
const total = items.reduce((sum, item) => {
const num = parseFloat(item.amount.replace(/[¥,]/g, ''))
return sum + (isNaN(num) ? 0 : num)
}, 0)
this.setData({
incomeTab: tab,
currentIncome: items,
incomeTotal: `¥${total.toLocaleString()}`,
})
},
/** 点击收入 Tab */
onIncomeTabTap(e: WechatMiniprogram.CustomEvent) {
const tab = e.currentTarget.dataset.tab as 'this' | 'last'
this.switchIncomeTab(tab)
},
/** 打开备注弹窗 */
onAddNote() {
this.setData({ noteModalVisible: true })
},
/** 备注弹窗确认 */
onNoteConfirm(e: WechatMiniprogram.CustomEvent<{ score: number; content: string }>) {
const { score, content } = e.detail
// TODO: 替换为真实 API 调用 POST /api/xcx/notes
const newNote: NoteItem = {
id: `n-${Date.now()}`,
content,
timestamp: new Date().toISOString().slice(0, 16).replace('T', ' '),
score,
customerName: '我',
}
const notes = [newNote, ...this.data.sortedNotes]
this.setData({
noteModalVisible: false,
sortedNotes: notes,
})
wx.showToast({ title: '备注已保存', icon: 'success' })
},
/** 备注弹窗取消 */
onNoteCancel() {
this.setData({ noteModalVisible: false })
},
/** 返回 */
onBack() {
wx.navigateBack()
},
/** 问问助手 */
onStartChat() {
const id = this.data.coachId || this.data.detail?.id || ''
wx.navigateTo({
url: `/pages/chat/chat?coachId=${id}`,
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
})
},
})

View File

@@ -0,0 +1,142 @@
<!-- 加载态 -->
<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-icon name="info-circle" size="120rpx" color="#c5c5c5" />
<text class="empty-text">未找到助教信息</text>
</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="coach-header">
<view class="avatar-box">
<image class="avatar-img" src="{{detail.avatar}}" mode="aspectFill" />
</view>
<view class="info-middle">
<view class="name-row">
<text class="coach-name">{{detail.name}}</text>
<t-tag variant="light" size="small" theme="warning">{{detail.level}}</t-tag>
</view>
<view class="skill-row">
<text class="skill-tag" wx:for="{{detail.skills}}" wx:key="*this">{{item}}</text>
</view>
</view>
<view class="info-right-stats">
<view class="right-stat">
<text class="right-stat-label">工龄</text>
<text class="right-stat-value">{{detail.workYears}}年</text>
</view>
<view class="right-stat">
<text class="right-stat-label">客户</text>
<text class="right-stat-value">{{detail.customerCount}}人</text>
</view>
</view>
</view>
</view>
</view>
<!-- 主体内容 -->
<view class="main-content">
<!-- 绩效概览 -->
<view class="card">
<text class="section-title title-blue">绩效概览</text>
<view class="perf-grid">
<view class="perf-card {{item.bgClass}}" wx:for="{{perfCards}}" wx:key="label">
<text class="perf-label">{{item.label}}</text>
<view class="perf-value-row">
<text class="perf-value {{item.valueColor}}">{{item.value}}</text>
<text class="perf-unit" wx:if="{{item.unit}}">{{item.unit}}</text>
</view>
<text class="perf-sub">{{item.sub}}</text>
</view>
</view>
</view>
<!-- 收入明细 -->
<view class="card">
<view class="card-header">
<text class="section-title title-green">收入明细</text>
<view class="income-tabs">
<text class="income-tab {{incomeTab === 'this' ? 'active' : ''}}"
data-tab="this" bindtap="onIncomeTabTap">本月</text>
<text class="income-tab {{incomeTab === 'last' ? 'active' : ''}}"
data-tab="last" bindtap="onIncomeTabTap">上月</text>
</view>
</view>
<view class="income-list">
<view class="income-item" wx:for="{{currentIncome}}" wx:key="label">
<view class="income-dot dot-{{item.color}}"></view>
<text class="income-label">{{item.label}}</text>
<text class="income-amount">{{item.amount}}</text>
</view>
<view class="income-total">
<text class="income-total-label">合计{{incomeTab === 'this' ? '(预估)' : ''}}</text>
<text class="income-total-value">{{incomeTotal}}</text>
</view>
</view>
</view>
<!-- 备注列表 -->
<view class="card">
<view class="card-header">
<text class="section-title title-orange">备注记录</text>
<text class="header-hint">共 {{sortedNotes.length}} 条</text>
</view>
<view class="note-list" wx:if="{{sortedNotes.length > 0}}">
<view class="note-item" wx:for="{{sortedNotes}}" wx:key="id">
<view class="note-top">
<text class="note-author">{{item.tagLabel}}</text>
<text class="note-time">{{item.createdAt}}</text>
</view>
<text class="note-content">{{item.content}}</text>
</view>
</view>
<view class="note-empty" wx:else>
<t-icon name="info-circle" size="80rpx" color="#dcdcdc" />
<text class="empty-hint">暂无备注</text>
</view>
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-bar safe-area-bottom">
<view class="btn-chat" bindtap="onStartChat">
<t-icon name="chat" size="40rpx" color="#ffffff" />
<text>问问助手</text>
</view>
<view class="btn-note" bindtap="onAddNote">
<t-icon name="edit-1" size="40rpx" color="#242424" />
<text>添加备注</text>
</view>
</view>
<!-- 备注弹窗 -->
<note-modal
visible="{{noteModalVisible}}"
customerName="{{detail.name}}"
bind:confirm="onNoteConfirm"
bind:cancel="onNoteCancel"
/>
<!-- AI 悬浮按钮 -->
<ai-float-button bottom="{{120}}" customerId="{{detail.id}}" />
</block>
<dev-fab />

View File

@@ -0,0 +1,431 @@
/* 加载态 & 空态 */
.page-loading,
.page-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
gap: 24rpx;
}
.empty-text {
font-size: var(--font-sm, 28rpx);
color: var(--color-gray-6, #a6a6a6);
}
/* ========== Banner ========== */
.banner-area {
position: relative;
overflow: hidden;
}
.banner-bg {
width: 100%;
height: 380rpx;
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;
}
/* 助教头部信息 */
.coach-header {
display: flex;
align-items: center;
gap: 24rpx;
padding: 16rpx 40rpx 32rpx;
}
.avatar-box {
width: 112rpx;
height: 112rpx;
border-radius: 32rpx;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(8px);
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: 12rpx;
margin-bottom: 12rpx;
}
.coach-name {
font-size: var(--font-lg, 36rpx);
font-weight: 600;
color: #ffffff;
}
.skill-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.skill-tag {
padding: 4rpx 16rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 8rpx;
font-size: var(--font-xs, 24rpx);
color: rgba(255, 255, 255, 0.9);
}
.info-right-stats {
flex-shrink: 0;
text-align: right;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.right-stat {
display: flex;
align-items: baseline;
gap: 8rpx;
justify-content: flex-end;
}
.right-stat-label {
font-size: var(--font-xs, 24rpx);
color: rgba(255, 255, 255, 0.7);
}
.right-stat-value {
font-size: var(--font-base, 32rpx);
font-weight: 700;
color: #ffffff;
}
/* ========== 主体内容 ========== */
.main-content {
padding: 32rpx;
padding-bottom: 200rpx;
display: flex;
flex-direction: column;
gap: 32rpx;
}
/* ========== 通用卡片 ========== */
.card {
background: #ffffff;
border-radius: var(--radius-xl, 32rpx);
padding: 40rpx;
box-shadow: var(--shadow-lg, 0 8rpx 32rpx rgba(0, 0, 0, 0.06));
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32rpx;
}
.section-title {
font-size: var(--font-base, 32rpx);
font-weight: 600;
color: var(--color-gray-13, #242424);
position: relative;
padding-left: 20rpx;
margin-bottom: 32rpx;
}
.card-header .section-title {
margin-bottom: 0;
}
.section-title::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 28rpx;
border-radius: 4rpx;
}
.title-blue::before {
background: linear-gradient(180deg, #0052d9, #5b9cf8);
}
.title-green::before {
background: linear-gradient(180deg, #00a870, #4cd964);
}
.title-orange::before {
background: linear-gradient(180deg, #ed7b2f, #ffc107);
}
.header-hint {
font-size: var(--font-xs, 24rpx);
color: var(--color-gray-6, #a6a6a6);
}
/* ========== 绩效概览 ========== */
.perf-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24rpx;
}
.perf-card {
border-radius: var(--radius-lg, 24rpx);
padding: 24rpx;
border: 1rpx solid transparent;
}
.perf-blue {
background: linear-gradient(135deg, #eff6ff, #eef2ff);
border-color: rgba(191, 219, 254, 0.5);
}
.perf-green {
background: linear-gradient(135deg, #ecfdf5, #d1fae5);
border-color: rgba(167, 243, 208, 0.5);
}
.perf-orange {
background: linear-gradient(135deg, #fff7ed, #fef3c7);
border-color: rgba(253, 186, 116, 0.5);
}
.perf-purple {
background: linear-gradient(135deg, #faf5ff, #ede9fe);
border-color: rgba(196, 181, 253, 0.5);
}
.perf-label {
font-size: var(--font-xs, 24rpx);
color: var(--color-gray-6, #a6a6a6);
display: block;
margin-bottom: 8rpx;
}
.perf-value-row {
display: flex;
align-items: baseline;
gap: 4rpx;
}
.perf-value {
font-size: 48rpx;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.perf-unit {
font-size: var(--font-xs, 24rpx);
color: var(--color-gray-6, #a6a6a6);
}
.perf-sub {
font-size: 22rpx;
color: var(--color-gray-5, #c5c5c5);
display: block;
margin-top: 4rpx;
}
/* ========== 收入明细 ========== */
.income-tabs {
display: flex;
align-items: center;
gap: 8rpx;
}
.income-tab {
font-size: var(--font-xs, 24rpx);
padding: 8rpx 20rpx;
border-radius: 100rpx;
color: var(--color-gray-7, #8b8b8b);
background: var(--color-gray-1, #f3f3f3);
}
.income-tab.active {
color: var(--color-primary, #0052d9);
background: var(--color-primary-light, #ecf2fe);
font-weight: 500;
}
.income-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.income-item {
display: flex;
align-items: center;
gap: 16rpx;
}
.income-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
flex-shrink: 0;
}
.dot-primary {
background: var(--color-primary, #0052d9);
}
.dot-success {
background: var(--color-success, #00a870);
}
.dot-warning {
background: var(--color-warning, #ed7b2f);
}
.dot-purple {
background: #7c3aed;
}
.income-label {
flex: 1;
font-size: var(--font-base, 32rpx);
color: var(--color-gray-9, #5e5e5e);
}
.income-amount {
font-size: var(--font-base, 32rpx);
font-weight: 700;
color: var(--color-gray-13, #242424);
font-variant-numeric: tabular-nums;
}
.income-total {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 20rpx;
border-top: 1rpx solid var(--color-gray-2, #eeeeee);
}
.income-total-label {
font-size: var(--font-base, 32rpx);
font-weight: 600;
color: var(--color-gray-9, #5e5e5e);
}
.income-total-value {
font-size: var(--font-base, 32rpx);
font-weight: 700;
color: var(--color-success, #00a870);
font-variant-numeric: tabular-nums;
}
/* ========== 备注列表 ========== */
.note-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.note-item {
background: #fafafa;
border-radius: var(--radius-lg, 24rpx);
border: 1rpx solid var(--color-gray-3, #e7e7e7);
padding: 24rpx;
}
.note-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.note-author {
font-size: var(--font-sm, 28rpx);
font-weight: 500;
color: var(--color-gray-13, #242424);
}
.note-time {
font-size: var(--font-xs, 24rpx);
color: var(--color-gray-6, #a6a6a6);
}
.note-content {
font-size: var(--font-sm, 28rpx);
color: var(--color-gray-9, #5e5e5e);
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.note-score {
display: flex;
align-items: center;
gap: 8rpx;
margin-top: 12rpx;
}
.note-star {
font-size: 28rpx;
}
.note-score-value {
font-size: var(--font-xs, 24rpx);
color: var(--color-warning, #ed7b2f);
font-weight: 500;
}
.note-empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 48rpx 0;
gap: 16rpx;
}
.empty-hint {
font-size: var(--font-sm, 28rpx);
color: var(--color-gray-5, #c5c5c5);
}
/* ========== 底部操作栏 ========== */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 128rpx;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-top: 1rpx solid var(--color-gray-2, #eeeeee);
display: flex;
align-items: center;
gap: 24rpx;
padding: 0 32rpx;
z-index: 100;
}
.btn-chat {
flex: 1;
height: 88rpx;
background: linear-gradient(135deg, #0052d9, #3b82f6);
color: #ffffff;
font-weight: 500;
border-radius: var(--radius-lg, 24rpx);
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
font-size: var(--font-base, 32rpx);
box-shadow: 0 8rpx 24rpx rgba(0, 82, 217, 0.3);
}
.btn-note {
flex: 1;
height: 88rpx;
background: var(--color-gray-1, #f3f3f3);
color: var(--color-gray-13, #242424);
font-weight: 500;
border-radius: var(--radius-lg, 24rpx);
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
font-size: var(--font-base, 32rpx);
}
/* ========== 颜色工具类 ========== */
.text-primary {
color: var(--color-primary, #0052d9) !important;
}
.text-success {
color: var(--color-success, #00a870) !important;
}
.text-warning {
color: var(--color-warning, #ed7b2f) !important;
}
.text-purple {
color: #7c3aed !important;
}