Files
Neo-ZQYY/apps/miniprogram/miniprogram/pages/coach-detail/coach-detail.wxml
Neo 2a7a5d68aa feat: 2026-04-15~04-20 累积变更基线 — 多主线合流
主线 1: rns1-customer-coach-api + 04-miniapp-core-business 后端实施
  - 新增 GET /xcx/coaches/{id}/banner 轻量接口
  - performance/records 加 coach_id 参数 + view_board_coach 权限分流
  - coach/customer/performance/board/task 服务层重构
  - fdw_queries 结算单粒度聚合 + consumption_summary 视图统一
  - task_generator 回访宽限 72h + UPSERT 替代策略 + Step 5 保底清理
  - recall_detector settle_type=3 双重限制 + 门店级 resolved

主线 2: 小程序权限分流 + 新增 coach-service-records 管理者视角业绩明细页
  - perf-progress 共享模块去重 task-list/coach-detail 动画逻辑
  - isScattered 散客标记端到端
  - foodDetail/phoneFull/creator* 字段透传

主线 3: P19 指数回测框架 Phase 1+2
  - 3 个指数表 stat_date 日快照模式
  - 新增 DWS_INDEX_BACKFILL / DWS_TASK_SIMULATION 工具任务
  - task_engine 升级 HTTP 实时 + 推演回测双模式

主线 4: Core 维度层启用
  - 新增 CORE_DIM_SYNC 任务(DWD → core 4 维度表)
  - 修复 app 视图空查询问题

主线 5: member_project_tag 改为 LAST_30_VISITS 消费次数窗口

主线 6: 2 个迁移 SQL 已执行(stat_date + member_project_tag 新窗口)
  - schema 基线与 DDL 快照同步

主线 7: 开发机路径迁移 C:\NeoZQYY → C:\Project\NeoZQYY(约 95% 改动量)

附带: 新建运维脚本(churned_customer_report / simulate_historical_tasks /
      backfill_index_snapshots)+ tools/task-analysis/ 任务分析工具

合计 157 文件。未包含中间产物(tmp/ .playwright-mcp/ inspect-* excel/sheet 分析 txt)。
审计记录见下一个 commit。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 06:32:07 +08:00

325 lines
15 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<wxs src="../../utils/format.wxs" module="fmt" />
<!-- 空态 -->
<view class="page-empty" wx:if="{{pageState === 'empty'}}">
<t-icon name="info-circle" size="120rpx" color="#c5c5c5" />
<text class="empty-text">未找到助教信息</text>
</view>
<!-- 错误态 -->
<view class="page-error" wx:elif="{{pageState === 'error'}}">
<t-icon name="close-circle" size="120rpx" color="#e34d59" />
<text class="error-text">加载失败</text>
<view class="retry-btn" bindtap="onRetry" hover-class="retry-btn--hover">点击重试</view>
</view>
<!-- 正常态 -->
<block wx:elif="{{pageState === 'normal'}}">
<!-- 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">{{fmt.safe(detail.name)}}</text>
<coach-level-tag level="{{detail.level}}" bgColor="{{levelBgColor}}" shadowColor="transparent" />
</view>
<view class="skill-row">
<text class="skill-tag" wx:for="{{detail.skills}}" wx:key="index">{{item}}</text>
</view>
</view>
<view class="info-right-stats">
<view class="right-stat">
<text class="right-stat-label">工龄</text>
<text class="right-stat-value">{{fmt.safe(detail.workYears)}}</text>
<text class="right-stat-label">年</text>
</view>
<view class="right-stat">
<text class="right-stat-label">客户</text>
<text class="right-stat-value">{{fmt.safe(detail.customerCount)}}</text>
<text class="right-stat-label">人</text>
</view>
</view>
</view>
</view>
</view>
<!-- 主体内容 -->
<view class="main-content">
<!-- 绩效概览 -->
<view class="card">
<view class="card-title-row">
<text class="section-title title-blue">绩效概览</text>
</view>
<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 class="perf-progress-box">
<view class="perf-progress-header">
<text class="perf-progress-label">绩效档位进度</text>
<text class="perf-progress-hint">距下一档还差 {{fmt.hours(perfGap)}}</text>
</view>
<perf-progress-bar
filledPct="{{pbFilledPct}}"
clampedSparkPct="{{pbClampedSparkPct}}"
currentTier="{{pbCurrentTier}}"
ticks="{{pbTicks}}"
shineRunning="{{pbShineRunning}}"
sparkRunning="{{pbSparkRunning}}"
shineDurMs="{{pbShineDurMs}}"
sparkDurMs="{{pbSparkDurMs}}"
style="--ppb-track-bg-color: rgba(59,130,246,0.25); --ppb-divider-color: rgba(255,255,255,1); --ppb-tick-done-color: #60a5fa; --ppb-tick-color: rgba(147,197,253,0.6); --ppb-tick-highlight-color: #3b82f6;"
/>
</view>
</view>
<!-- 收入明细 -->
<view class="card">
<view class="card-header">
<text class="section-title title-green">收入明细</text>
<view class="income-tabs">
<view class="income-tab {{incomeTab === 'this' ? 'active' : ''}}" data-tab="this" bindtap="onIncomeTabTap" hover-class="income-tab--hover">
<text>本月</text>
<text class="income-tab-est" wx:if="{{incomeTab === 'this'}}">预估</text>
</view>
<view class="income-tab {{incomeTab === 'last' ? 'active' : ''}}" data-tab="last" bindtap="onIncomeTabTap" hover-class="income-tab--hover">
<text>上月</text>
</view>
</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">{{fmt.safe(item.amount)}}</text>
</view>
<view class="income-total">
<text class="income-total-label">合计{{incomeTab === 'this' ? '(预估)' : ''}}</text>
<text class="income-total-value">{{fmt.safe(incomeTotal)}}</text>
</view>
</view>
</view>
<!-- 任务执行 -->
<view class="card">
<view class="card-header">
<text class="section-title title-orange">任务执行</text>
<view class="task-summary">
<text class="task-summary-label">本月完成</text>
<text class="task-summary-callback">回访<text class="task-summary-num">{{taskStats.callback}}个</text></text>
<text class="task-summary-recall">召回<text class="task-summary-num">{{taskStats.recall}}个</text></text>
</view>
</view>
<view class="task-list">
<view class="task-item task-item-{{item.typeClass}}" wx:for="{{visibleTasks}}" wx:key="index"
bindtap="onTaskItemTap" data-id="{{item.customerId}}" data-name="{{item.customerName}}" hover-class="task-item--hover">
<text class="task-tag-text {{item.typeClass}}">{{item.typeLabel}}</text>
<text class="task-customer-name">{{item.customerName}}</text>
<view class="task-note-btn" wx:if="{{item.noteCount > 0}}" catchtap="onTaskNoteTap" data-index="{{index}}" hover-class="task-note-btn--hover">
<t-icon name="chat" size="32rpx" color="#777777" />
<text class="task-note-count">{{item.noteCount}}</text>
</view>
<text class="task-pin" wx:if="{{item.pinned}}">📌</text>
</view>
</view>
<block wx:if="{{tasksExpanded}}">
<view class="task-list task-list-extra">
<view class="task-item task-item-{{item.typeClass}}" wx:for="{{hiddenTasks}}" wx:key="index"
bindtap="onTaskItemTap" data-id="{{item.customerId}}" data-name="{{item.customerName}}" hover-class="task-item--hover">
<text class="task-tag-text {{item.typeClass}}">{{item.typeLabel}}</text>
<text class="task-customer-name">{{item.customerName}}</text>
<view class="task-note-btn" wx:if="{{item.noteCount > 0}}" catchtap="onTaskNoteTap" data-hidden-index="{{index}}" hover-class="task-note-btn--hover">
<t-icon name="chat" size="32rpx" color="#777777" />
<text class="task-note-count">{{item.noteCount}}</text>
</view>
</view>
<view class="task-item task-item-abandoned" wx:for="{{abandonedTasks}}" wx:key="index">
<text class="task-abandoned-name">{{item.customerName}}</text>
<text class="task-abandoned-reason">{{item.reason}}</text>
</view>
</view>
</block>
<view class="task-toggle" bindtap="onToggleTasks" hover-class="task-toggle--hover" wx:if="{{hiddenTasks.length > 0 || abandonedTasks.length > 0}}">
<text>{{tasksExpanded ? '收起 ↑' : '展开全部 ↓'}}</text>
</view>
</view>
<!-- 客户关系 TOP20 -->
<view class="card">
<view class="card-header">
<text class="section-title title-pink">客户关系 TOP20</text>
<text class="header-hint">近60天</text>
</view>
<view class="top-customer-list">
<view
class="top-customer-item"
hover-class="top-customer-item--hover"
wx:for="{{topCustomers}}"
wx:key="index"
wx:if="{{topCustomersExpanded || index < 5}}"
data-id="{{item.id}}"
bindtap="onCustomerTap"
>
<view class="top-customer-avatar avatar-{{item.avatarGradient}}">
<text class="top-customer-avatar-text">{{item.initial}}</text>
</view>
<view class="top-customer-info">
<view class="top-customer-name-row">
<text class="top-customer-name {{item.isScattered ? 'top-customer-name--scattered' : ''}}">{{item.name}}</text>
<text class="top-customer-heart">{{item.heartEmoji}}</text>
<text class="top-customer-score top-customer-score-{{item.scoreColor}}">{{fmt.safe(item.score)}}</text>
</view>
<view class="top-customer-stats">
<text class="top-customer-stat">服务 <text class="top-customer-stat-val">{{fmt.count(item.serviceCount, '次')}}</text></text>
<text class="top-customer-stat">储值 <text class="top-customer-stat-val">{{fmt.money(item.balance)}}</text></text>
<text class="top-customer-stat">消费 <text class="top-customer-stat-val">{{fmt.money(item.consume)}}</text></text>
</view>
</view>
<t-icon name="chevron-right" size="32rpx" color="#c5c5c5" />
</view>
</view>
<view class="toggle-btn" bindtap="onToggleTopCustomers" hover-class="toggle-btn--hover" wx:if="{{topCustomers.length > 5}}">
<text>{{topCustomersExpanded ? '收起 ↑' : '查看更多 ↓'}}</text>
</view>
</view>
<!-- 近期服务明细 -->
<view class="card">
<view class="card-title-row">
<text class="section-title title-purple">近期服务明细</text>
</view>
<view class="svc-list">
<view class="svc-card" wx:for="{{serviceRecords}}" wx:key="index" wx:if="{{index < 5}}"
bindtap="onSvcCardTap" data-id="{{item.customerId}}" hover-class="svc-card--hover">
<!-- 头像列 -->
<view class="top-customer-avatar avatar-{{item.avatarGradient}}">
<text class="top-customer-avatar-text">{{item.initial}}</text>
</view>
<!-- 右侧内容列:两行垂直排列 -->
<view class="svc-content">
<!-- 第1行客户名 + 类型标签 + 日期 -->
<view class="svc-row1">
<text class="svc-customer {{item.isScattered ? 'svc-customer--scattered' : ''}}">{{item.customerName}}</text>
<text class="svc-type svc-type-{{item.typeClass}}">{{item.type}}</text>
<text class="svc-date">{{item.date}}</text>
</view>
<!-- 第2行台号 + 时长 + 绩效 + 收入 -->
<view class="svc-row2">
<view class="svc-row2-left">
<text class="svc-table-tag" wx:if="{{item.table}}">{{item.table}}</text>
<text class="svc-duration">{{item.duration}}</text>
<text class="svc-perf" wx:if="{{item.perfHours}}">定档绩效:{{item.perfHours}}</text>
</view>
<text class="svc-income">{{fmt.money(item.income)}}</text>
</view>
</view>
</view>
</view>
<view class="svc-more" bindtap="onViewMoreRecords" hover-class="svc-more--hover">
<text>查看更多服务记录 →</text>
</view>
</view>
<!-- 更多信息 -->
<view class="card">
<view class="card-title-row">
<text class="section-title title-teal">更多信息</text>
</view>
<view class="more-info-row">
<text class="more-info-label">入职日期</text>
<text class="more-info-value">{{fmt.safe(detail.hireDate)}}</text>
</view>
<view class="history-table">
<view class="history-thead">
<text class="history-th history-th-left">月份</text>
<text class="history-th">服务客户</text>
<text class="history-th">访/召完成</text>
<text class="history-th">业绩时长</text>
<text class="history-th">工资</text>
</view>
<view class="history-row {{index === 0 ? 'history-row-current' : ''}}" wx:for="{{historyMonths}}" wx:key="month">
<view class="history-td history-td-left">
<text>{{item.month}}</text>
<text class="history-est" wx:if="{{item.estimated}}">预估</text>
</view>
<text class="history-td">{{fmt.count(item.customers, '人')}}</text>
<text class="history-td">{{fmt.safe(item.callbackDone)}} | {{fmt.safe(item.recallDone)}}</text>
<text class="history-td {{index === 0 ? 'text-primary' : ''}}">{{fmt.hoursH(item.hours)}}</text>
<text class="history-td {{index === 0 ? 'text-success' : ''}}">{{fmt.money(item.salary)}}</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="edit-1" 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" hover-class="btn-chat--hover">
<t-icon name="chat" size="36rpx" color="#ffffff" />
<text>问问助手</text>
</view>
<view class="btn-note" bindtap="onAddNote" hover-class="btn-note--hover">
<t-icon name="edit-1" size="36rpx" color="#242424" />
<text>备注</text>
</view>
</view>
<!-- 备注弹窗 -->
<note-modal visible="{{noteModalVisible}}" customerName="{{detail.name}}" bind:confirm="onNoteConfirm" bind:cancel="onNoteCancel" />
<!-- 备注列表弹窗 -->
<view class="notes-popup-overlay" wx:if="{{notesPopupVisible}}" catchtap="onHideNotesPopup">
<view class="notes-popup" catchtap="">
<view class="notes-popup-header">
<text class="notes-popup-title">{{notesPopupName}} 的备注</text>
<view class="notes-popup-close" bindtap="onHideNotesPopup" hover-class="notes-popup-close--hover">
<t-icon name="close" size="40rpx" color="#8b8b8b" />
</view>
</view>
<view class="notes-popup-list">
<view class="notes-popup-item" wx:for="{{notesPopupList}}" wx:key="index">
<view class="notes-popup-item-top">
<text wx:if="{{item.pinned}}">📌</text>
<text class="notes-popup-item-date">{{item.date}}</text>
</view>
<text class="notes-popup-item-text">{{item.text}}</text>
</view>
</view>
</view>
</view>
</block>
<dev-fab />