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,323 @@
<!-- pages/customer-detail/customer-detail.wxml -->
<!-- 加载态toast 浮层,不白屏) -->
<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>
<view class="page-empty" wx:elif="{{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.phone : '138****5678'}}</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">¥{{detail.balance}}</text>
<text class="stat-label">储值余额</text>
</view>
<view class="stat-item stat-border">
<text class="stat-value">¥{{detail.consumption60d}}</text>
<text class="stat-label">60天消费</text>
</view>
<view class="stat-item stat-border">
<text class="stat-value">{{detail.idealInterval}}</text>
<text class="stat-label">理想间隔</text>
</view>
<view class="stat-item">
<text class="stat-value stat-amber">{{detail.daysSinceVisit}}</text>
<text class="stat-label">距今到店</text>
</view>
</view>
</view>
</view>
<!-- 主体内容 -->
<view class="main-content" >
<!-- AI 智能洞察 -->
<view class="ai-insight-card">
<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">{{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">
<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.category}}"
category="{{item.categoryColor}}"
emoji=""
title="{{item.text}}"
source="By:{{item.source}}"
content="{{item.detail}}"
/>
</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">
<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">上次服务:{{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 : ''}}">{{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">
<view class="fav-coach-card {{item.bgClass}}" wx:for="{{favoriteCoaches}}" wx:key="index">
<view class="fav-coach-top">
<view class="fav-coach-name">
<text class="fav-emoji">{{item.emoji}}</text>
<text class="fav-name">{{item.name}}</text>
</view>
<view class="fav-index">
<text class="fav-index-label">关系指数</text>
<text class="fav-index-value text-{{item.indexColor}}">{{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 : ''}}">{{s.value}}</text>
</view>
</view>
</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">{{item.tableName}}</text>
</view>
<text class="record-date">{{item.date}}</text>
</view>
<view class="record-time-row">
<view class="record-time-left">
<text class="record-time-text">{{item.startTime}}</text>
<text class="record-time-arrow">→</text>
<text class="record-time-text">{{item.endTime}}</text>
<text class="record-duration-tag">{{item.duration}}</text>
</view>
<view class="record-fee-right">
<text class="record-fee-amount">¥{{item.tableFee}}</text>
<text class="record-fee-orig" wx:if="{{item.tableOrigPrice}}">¥{{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">{{c.name}}</text>
<coach-level-tag level="{{c.level}}" shadowColor="rgba(0,0,0,0)" />
</view>
<text class="record-coach-type">{{c.courseType}} · {{c.hours}}</text>
<view class="record-coach-bottom">
<text class="record-coach-perf" wx:if="{{c.perfHours}}">定档绩效:{{c.perfHours}}</text>
<text class="record-coach-fee">¥{{c.fee}}</text>
</view>
</view>
</view>
</view>
<view class="record-food-row" wx:if="{{item.foodAmount > 0}}">
<text class="record-food-label">🍷 食品酒水</text>
<view class="record-food-right">
<text class="record-food-amount">¥{{item.foodAmount}}</text>
<text class="record-fee-orig" wx:if="{{item.foodOrigPrice}}">¥{{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">¥{{item.totalAmount}}</text>
<text class="record-fee-orig" wx:if="{{item.totalOrigPrice}}">¥{{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">{{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">{{c.name}}</text>
<coach-level-tag level="{{c.level}}" shadowColor="rgba(0,0,0,0)" />
</view>
<text class="record-coach-type">{{c.courseType}} · {{c.hours}}</text>
<view class="record-coach-bottom">
<text class="record-coach-perf" wx:if="{{c.perfHours}}">定档绩效:{{c.perfHours}}</text>
<text class="record-coach-fee">¥{{c.fee}}</text>
</view>
</view>
</view>
</view>
<view class="record-food-row" wx:if="{{item.foodAmount > 0}}">
<text class="record-food-label">🍷 食品酒水</text>
<text class="record-food-amount">¥{{item.foodAmount}}</text>
</view>
<view class="record-total-row" wx:if="{{item.totalAmount}}">
<text class="record-total-label">总金额</text>
<text class="record-total-amount">¥{{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.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}}" showExpandBtn="{{false}}" showRating="{{false}}" bind:confirm="onNoteConfirm" bind:cancel="onNoteCancel" />
<!-- AI 悬浮按钮 -->
<ai-float-button customerId="{{detail.id}}" />
</block>
<dev-fab />