Phase 2.3 chat 上下文捕获链路从未真正激活到完整工作: - 14 处 ai-float-button 补 sourcePage,chat.ts 三分支同步设 pageFilters.contextId - 后端 page_context 4 层 BUG 修(列名错位 + RLS site_id 未重设) - xcx_chat filters.pop 破坏 body.page_context 引用 — dict() 浅拷贝隔离 - chat 流式 markdown 实时解析(表格/标题/列表/加粗 + KPI 富卡) - reference_card KPI 富卡接入 SSE 路径,db 真写入 - 维客线索 source 显示规则:AI 来源用机器人 icon 替代长文字 数据库: - public.member_retention_clue 加 emoji + runtime_mode + sandbox_instance_id - biz.ai_run_logs 加 assistant_id + 复合索引 - chk_ai_cache_type CHECK 约束 8 类应用名 - cache_type / app_type 命名统一(app6_note / app7_customer / app8_consolidation) - 历史 emoji 抽取脚本 44/44 成功 后端 silent failure 修: - cleanup_service WHERE app_type → cache_type(90 天清理 + 20K 上限重新生效) - _build_ai_insight 字段错位修复(app4 → app7 + 字段对齐 prompt schema) - task_manager talkingPoints 改 app5_tactics + tactics 字段 - task_manager aiSuggestion 改取 one_line_summary - cache_service.CACHE_EXPIRY_DAYS 加 app2a_finance_area - WS /ws/ai-cache 加 token + JWT + site_id 校验(P0 信息泄露漏洞) - internal_ai token 改 hmac.compare_digest 工具/文档: - main.py 加 RotatingFileHandler logs/backend.log + uvicorn /health 过滤 - 新建 utils/clue_category.py(VI 6 类配色 + emoji fallback + source 显示规则) - 新建 utils/markdown.ts(轻量 md 转 rich-text 解析 + streaming 容错) - audit + 数据库变更说明 + backlog §七 #14 收口 + #15-#38 残余子任务 - backlog 追加 §十一 App1 参数/MCP/沙箱审计 + §十二 百炼/SQL MCP 主任务线 实地 MCP 走查:14 入口数据层 + 5 代表入口 sourcePage 注入 + customer-detail 全模块 + chat md 渲染 + reference_card 富卡 都已验证。9 项预先 BUG/UX 登记 §七 #29-#38 后续修复。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
332 lines
16 KiB
Plaintext
332 lines
16 KiB
Plaintext
<!-- pages/customer-detail/customer-detail.wxml -->
|
|
<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 区域 — SVG 做渐变底图 -->
|
|
<view class="banner-section">
|
|
<image class="banner-bg-img" src="/assets/images/banner-bg-dark-gold-aurora.svg" mode="widthFix" />
|
|
<view class="banner-overlay">
|
|
<!-- 客户头部信息 -->
|
|
<view class="customer-header">
|
|
<view class="avatar-box">
|
|
<text class="avatar-text">{{detail.avatarChar}}</text>
|
|
</view>
|
|
<view class="info-right">
|
|
<view class="name-row">
|
|
<text class="customer-name">{{detail.name}}</text>
|
|
</view>
|
|
<view class="sub-info">
|
|
<text class="phone">{{phoneVisible ? detail.phoneFull : detail.phone}}</text>
|
|
<view class="phone-toggle-btn" bindtap="{{phoneVisible ? 'onCopyPhone' : 'onTogglePhone'}}" hover-class="phone-toggle-btn--hover">
|
|
<text class="phone-toggle-text">{{phoneVisible ? '复制' : '查看'}}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Banner 统计 -->
|
|
<view class="banner-stats">
|
|
<view class="stat-item stat-border">
|
|
<text class="stat-value stat-green">{{fmt.money(detail.balance)}}</text>
|
|
<text class="stat-label">储值余额</text>
|
|
</view>
|
|
<view class="stat-item stat-border">
|
|
<text class="stat-value">{{fmt.money(detail.consumption60d)}}</text>
|
|
<text class="stat-label">60天消费</text>
|
|
</view>
|
|
<view class="stat-item stat-border">
|
|
<text class="stat-value">{{fmt.days(detail.idealInterval)}}</text>
|
|
<text class="stat-label">理想间隔</text>
|
|
</view>
|
|
<view class="stat-item">
|
|
<text class="stat-value stat-amber">{{fmt.days(detail.daysSinceVisit)}}</text>
|
|
<text class="stat-label">距今到店</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 主体内容 -->
|
|
<view class="main-content" >
|
|
<!-- AI 智能洞察(W1-AI-CLOSURE 复盘:cache miss 时整段不渲染,避免空白卡) -->
|
|
<view class="ai-insight-card" wx:if="{{aiInsight.summary || aiInsight.strategies.length > 0}}">
|
|
<view class="ai-insight-header">
|
|
<view class="ai-icon-box">
|
|
<image class="ai-icon-img" src="/assets/icons/ai-robot.svg" mode="aspectFit" />
|
|
</view>
|
|
<text class="ai-insight-label">AI 智能洞察</text>
|
|
</view>
|
|
<view class="ai-insight-summary-v">
|
|
<text class="ai-insight-summary">{{fmt.safe(aiInsight.summary)}}</text>
|
|
</view>
|
|
<view class="ai-strategy-box">
|
|
<text class="strategy-title">当前推荐策略</text>
|
|
<view class="strategy-list">
|
|
<view class="strategy-item strategy-item-{{item.color}}" wx:for="{{aiInsight.strategies}}" wx:key="index" wx:if="{{index < aiInsight.strategies.length - 1}}">
|
|
<text class="strategy-text">{{item.text}}</text>
|
|
</view>
|
|
<view class="strategy-item strategy-item-{{item.color}} strategy-item-last" wx:for="{{aiInsight.strategies}}" wx:key="index" wx:if="{{index === aiInsight.strategies.length - 1}}">
|
|
<text class="strategy-text">{{item.text}}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 维客线索 -->
|
|
<view class="card" wx:if="{{clues.length > 0}}">
|
|
<view class="card-header">
|
|
<text class="section-title title-green">维客线索</text>
|
|
<ai-title-badge color="{{aiColor}}" />
|
|
</view>
|
|
<view class="clue-list">
|
|
<clue-card
|
|
wx:for="{{clues}}"
|
|
wx:key="index"
|
|
tag="{{item.tag}}"
|
|
category="{{item.tagColor}}"
|
|
emoji="{{item.emoji}}"
|
|
title="{{item.text}}"
|
|
source="{{item.source}}"
|
|
content="{{item.desc}}"
|
|
/>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 助教任务 -->
|
|
<view class="card">
|
|
<view class="card-header">
|
|
<text class="section-title title-blue">助教任务分配</text>
|
|
<text class="header-hint">当前进行中</text>
|
|
</view>
|
|
<view class="coach-task-list">
|
|
<view class="coach-task-card {{item.bgClass}}" wx:for="{{coachTasks}}" wx:key="index">
|
|
<view class="coach-task-top">
|
|
<view class="coach-name-row">
|
|
<heart-icon score="{{item.heartScore}}" />
|
|
<text class="coach-name">{{item.name}}</text>
|
|
<coach-level-tag level="{{item.level}}" shadowColor="rgba(0,0,0,0)" />
|
|
</view>
|
|
<view class="coach-task-right">
|
|
<text class="coach-task-type type-{{item.taskColor}}">{{item.taskType}}</text>
|
|
<text class="coach-task-status status-{{item.status}}" wx:if="{{item.status !== 'normal'}}">
|
|
<text wx:if="{{item.status === 'pinned'}}">📌 置顶</text>
|
|
<text wx:elif="{{item.status === 'abandoned'}}">❌ 已放弃</text>
|
|
</text>
|
|
</view>
|
|
</view>
|
|
<text class="coach-last-service">上次服务:{{fmt.safe(item.lastService)}}</text>
|
|
<view class="coach-metrics">
|
|
<view class="coach-metric" wx:for="{{item.metrics}}" wx:for-item="m" wx:key="label">
|
|
<text class="metric-label">{{m.label}}</text>
|
|
<text class="metric-value {{m.color ? 'text-' + m.color : ''}}">{{fmt.safe(m.value)}}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 最喜欢的助教 -->
|
|
<view class="card">
|
|
<view class="card-header">
|
|
<text class="section-title title-pink">最喜欢的助教</text>
|
|
<text class="header-hint">近60天</text>
|
|
</view>
|
|
<view class="fav-coach-list">
|
|
<!-- CHANGE 2026-03-29 | 前 3 个始终显示,其余折叠 -->
|
|
<block wx:for="{{favoriteCoaches}}" wx:key="index">
|
|
<view class="fav-coach-card {{item.bgClass}}" wx:if="{{index < 3 || favCoachExpanded}}">
|
|
<view class="fav-coach-top">
|
|
<view class="fav-coach-name">
|
|
<!-- CHANGE 2026-03-29 | 统一格式:爱心 - 名称 - 等级 -->
|
|
<heart-icon score="{{item.heartScore}}" />
|
|
<text class="fav-name">{{item.name}}</text>
|
|
<coach-level-tag level="{{item.level}}" shadowColor="rgba(0,0,0,0)" />
|
|
</view>
|
|
<view class="fav-index">
|
|
<text class="fav-index-label">关系指数</text>
|
|
<text class="fav-index-value" style="color:{{item.indexColor}}">{{fmt.safe(item.relationIndex)}}</text>
|
|
</view>
|
|
</view>
|
|
<text class="fav-period">近60天</text>
|
|
<view class="fav-stats">
|
|
<view class="fav-stat" wx:for="{{item.stats}}" wx:for-item="s" wx:key="label">
|
|
<text class="fav-stat-label">{{s.label}}</text>
|
|
<text class="fav-stat-value {{s.color ? 'text-' + s.color : ''}}">{{fmt.safe(s.value)}}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</block>
|
|
<!-- 展开/收起按钮 -->
|
|
<view class="expand-btn" wx:if="{{favoriteCoaches.length > 3}}" bindtap="onToggleFavCoaches">
|
|
<text class="expand-btn-text">{{favCoachExpanded ? '收起' : '展开更多 (' + (favoriteCoaches.length - 3) + ')'}}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 消费记录 -->
|
|
<view class="card">
|
|
<view class="card-header" bindtap="onViewServiceRecords" hover-class="card-header--hover">
|
|
<text class="section-title title-orange">消费记录</text>
|
|
<t-icon name="chevron-right" size="40rpx" color="#a6a6a6" />
|
|
</view>
|
|
|
|
<view class="record-list" wx:if="{{consumptionRecords.length > 0}}">
|
|
<block wx:for="{{consumptionRecords}}" wx:key="id">
|
|
|
|
<!-- 台桌结账 -->
|
|
<view class="record-card" wx:if="{{item.type === 'table'}}">
|
|
<view class="record-card-header record-header-blue">
|
|
<view class="record-project">
|
|
<view class="record-dot record-dot-blue"></view>
|
|
<text class="record-project-name record-name-blue">{{fmt.safe(item.tableName)}}</text>
|
|
</view>
|
|
<text class="record-date">{{fmt.safe(item.date)}}</text>
|
|
</view>
|
|
<view class="record-time-row">
|
|
<view class="record-time-left">
|
|
<text class="record-time-text">{{fmt.safe(item.startTime)}}</text>
|
|
<text class="record-time-arrow">→</text>
|
|
<text class="record-time-text">{{fmt.safe(item.endTime)}}</text>
|
|
<text class="record-duration-tag">{{fmt.safe(item.duration)}}</text>
|
|
</view>
|
|
<view class="record-fee-right">
|
|
<text class="record-fee-amount">{{fmt.money(item.tableFee)}}</text>
|
|
<text class="record-fee-orig" wx:if="{{item.tableOrigPrice}}">{{fmt.money(item.tableOrigPrice)}}</text>
|
|
</view>
|
|
</view>
|
|
<view class="record-coaches" wx:if="{{item.coaches && item.coaches.length > 0}}">
|
|
<view class="record-coach-grid">
|
|
<view class="record-coach-card" wx:for="{{item.coaches}}" wx:for-item="c" wx:key="name">
|
|
<view class="record-coach-name-row">
|
|
<text class="record-coach-name">{{fmt.safe(c.name)}}</text>
|
|
<coach-level-tag level="{{c.level}}" shadowColor="rgba(0,0,0,0)" />
|
|
</view>
|
|
<text class="record-coach-type">{{fmt.safe(c.courseType)}} · {{fmt.hours(c.hours)}}</text>
|
|
<view class="record-coach-bottom">
|
|
<text class="record-coach-perf" wx:if="{{c.perfHours}}">定档绩效:{{fmt.hours(c.perfHours)}}</text>
|
|
<text class="record-coach-fee">{{fmt.money(c.fee)}}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="record-food-row" wx:if="{{item.foodAmount > 0}}">
|
|
<text class="record-food-label">🍷 {{item.foodDetail || '食品酒水'}}</text>
|
|
<view class="record-food-right">
|
|
<text class="record-food-amount">{{fmt.money(item.foodAmount)}}</text>
|
|
<text class="record-fee-orig" wx:if="{{item.foodOrigPrice}}">{{fmt.money(item.foodOrigPrice)}}</text>
|
|
</view>
|
|
</view>
|
|
<view class="record-total-row" wx:if="{{item.totalAmount}}">
|
|
<text class="record-total-label">总金额</text>
|
|
<view class="record-total-right">
|
|
<text class="record-total-amount">{{fmt.money(item.totalAmount)}}</text>
|
|
<text class="record-fee-orig" wx:if="{{item.totalOrigPrice}}">{{fmt.money(item.totalOrigPrice)}}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 商城订单 -->
|
|
<view class="record-card" wx:elif="{{item.type === 'shop'}}">
|
|
<view class="record-card-header record-header-green">
|
|
<view class="record-project">
|
|
<view class="record-dot record-dot-green"></view>
|
|
<text class="record-project-name record-name-green">商城订单</text>
|
|
</view>
|
|
<text class="record-date">{{fmt.safe(item.date)}}</text>
|
|
</view>
|
|
<view class="record-coaches" wx:if="{{item.coaches && item.coaches.length > 0}}">
|
|
<view class="record-coach-grid">
|
|
<view class="record-coach-card" wx:for="{{item.coaches}}" wx:for-item="c" wx:key="name">
|
|
<view class="record-coach-name-row">
|
|
<text class="record-coach-name">{{fmt.safe(c.name)}}</text>
|
|
<coach-level-tag level="{{c.level}}" shadowColor="rgba(0,0,0,0)" />
|
|
</view>
|
|
<text class="record-coach-type">{{fmt.safe(c.courseType)}} · {{fmt.hours(c.hours)}}</text>
|
|
<view class="record-coach-bottom">
|
|
<text class="record-coach-perf" wx:if="{{c.perfHours}}">定档绩效:{{fmt.hours(c.perfHours)}}</text>
|
|
<text class="record-coach-fee">{{fmt.money(c.fee)}}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="record-food-row" wx:if="{{item.foodAmount > 0}}">
|
|
<text class="record-food-label">🍷 {{item.foodDetail || '食品酒水'}}</text>
|
|
<text class="record-food-amount">{{fmt.money(item.foodAmount)}}</text>
|
|
</view>
|
|
<view class="record-total-row" wx:if="{{item.totalAmount}}">
|
|
<text class="record-total-label">总金额</text>
|
|
<text class="record-total-amount">{{fmt.money(item.totalAmount)}}</text>
|
|
</view>
|
|
</view>
|
|
|
|
</block>
|
|
</view>
|
|
|
|
<view class="record-loading-more" wx:if="{{loadingMore}}">
|
|
<t-loading theme="circular" size="40rpx" text="加载更多..." />
|
|
</view>
|
|
|
|
<view class="record-empty" wx:if="{{consumptionRecords.length === 0 && !loadingMore}}">
|
|
<t-icon name="info-circle" size="80rpx" color="#dcdcdc" />
|
|
<text class="empty-hint">暂无消费记录</text>
|
|
</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.creatorName || item.tagLabel}}{{item.creatorRole ? ' · ' + item.creatorRole : ''}}</text>
|
|
<view class="note-top-right">
|
|
<text class="note-time">{{fmt.safe(item.createdAt)}}</text>
|
|
<view class="note-delete-btn" data-id="{{item.id}}" bindtap="onDeleteNote" hover-class="note-delete-btn--hover">
|
|
<t-icon name="delete" size="32rpx" color="#c5c5c5" />
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<text class="note-content">{{fmt.safe(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}}" showExpandBtn="{{false}}" showRating="{{false}}" bind:confirm="onNoteConfirm" bind:cancel="onNoteCancel" />
|
|
|
|
<!-- AI 悬浮按钮(W1-AI-CLOSURE 组 6:补 sourcePage 让 chat 上下文捕获生效) -->
|
|
<ai-float-button customerId="{{detail.id}}" sourcePage="customer-detail" />
|
|
</block>
|
|
|
|
<dev-fab /> |