/* AI_CHANGELOG | 日期 | Prompt | 变更 | |------|--------|------| | 2026-03-23 | 角色路由+页面权限守卫 | onShow 添加 checkPageAccess 权限守卫 | */ import { checkPageAccess } from '../../utils/auth-guard' import { fetchCustomerDetail, fetchAICache } from '../../services/api' interface ConsumptionRecord { id: string type: "table" | "shop" | "recharge" date: string tableName?: string startTime?: string endTime?: string duration?: string tableFee?: number tableOrigPrice?: number coaches?: Array<{ name: string level: string levelColor: string courseType: string hours: string perfHours?: string fee: number }> foodAmount?: number foodOrigPrice?: number foodDetail?: string totalAmount?: number totalOrigPrice?: number payMethod?: string rechargeAmount?: number } const mockRecords: ConsumptionRecord[] = [ { id: '', type: 'table', date: '', tableName: '', startTime: '', endTime: '', duration: '', tableFee: 0, coaches: [{ name: '', level: '', levelColor: '', courseType: '', hours: '', fee: 0 }], foodAmount: 0, totalAmount: 0, payMethod: '' }, { id: '', type: 'recharge', date: '', rechargeAmount: 0 }, ] Page({ data: { pageState: "loading" as "loading" | "empty" | "error" | "normal", detail: { id: '', name: '', avatarChar: '', phone: '', phoneFull: '', balance: null as number | null, consumption60d: null as number | null, idealInterval: null as number | null, daysSinceVisit: null as number | null, }, phoneVisible: false, aiColor: "indigo" as "red" | "orange" | "yellow" | "blue" | "indigo" | "purple", // 对齐 demo 标杆 wxml(`{{item.text}}` 单字段);color 由前端按 index 轮换。 aiInsight: { summary: '', strategies: [] as Array<{ color: string; text: string }>, }, // W1-AI-CLOSURE 组 6:clues 字段对齐 RetentionClue schema // {tag, tag_color, emoji, text, source, desc} → camelCase {tag, tagColor, emoji, text, source, desc} clues: [] as Array<{ tag: string tagColor: string emoji: string text: string source: string desc: string }>, coachTasks: [ { name: '', level: '', levelColor: '', taskType: '', taskColor: '', bgClass: '', status: '', lastService: '', metrics: [{ label: '', value: '' }] }, { name: '', level: '', levelColor: '', taskType: '', taskColor: '', bgClass: '', status: '', lastService: '', metrics: [{ label: '', value: '' }] }, ], favoriteCoaches: [ { emoji: '', name: '', relationIndex: '', indexColor: '', bgClass: '', stats: [{ label: '', value: '' }] }, ], consumptionRecords: mockRecords, loadingMore: false, noteModalVisible: false, favCoachExpanded: false, sortedNotes: [ { id: '', tagLabel: '', createdAt: '', content: '' }, { id: '', tagLabel: '', createdAt: '', content: '' }, ] as Array<{ id: string; tagLabel: string; createdAt: string; content: string }>, }, onLoad(options: any) { // 随机 AI 配色 const aiColors = ['red', 'orange', 'yellow', 'blue', 'indigo', 'purple'] as const const aiColor = aiColors[Math.floor(Math.random() * aiColors.length)] this.setData({ aiColor }) const id = options?.id || options?.customerId || '' this.loadDetail(id) }, onShow() { // 权限守卫:检查登录状态、账号禁用、角色权限 checkPageAccess('pages/customer-detail/customer-detail') }, // CHANGE 2026-03-29 | P3 联调:映射所有后端返回字段(AI 相关暂跳过) async loadDetail(id?: string) { this.setData({ pageState: 'loading' }) wx.showLoading({ title: '加载中...', mask: true }) try { if (id) { const d = await fetchCustomerDetail(id) if (d) { this.setData({ detail: { id: d.id ?? id, name: d.name || '', avatarChar: (d.name || '')[0] || '', phone: d.phone || '', phoneFull: d.phoneFull || '', balance: d.balance ?? null, // CHANGE 2026-03-29 | Pydantic 把 consumption_60d → consumption60D(大写 D) consumption60d: d.consumption60D ?? d.consumption60d ?? null, idealInterval: d.idealInterval ?? null, daysSinceVisit: d.daysSinceVisit ?? null, }, // 维客线索 clues: d.retentionClues || [], // 助教任务 coachTasks: d.coachTasks || [], // 最亲密助教 favoriteCoaches: d.favoriteCoaches || [], // 消费记录 consumptionRecords: d.consumptionRecords || [], // 备注 sortedNotes: d.notes || [], }) } } this.setData({ pageState: 'normal' }) if (id) this._loadAIInsight(id) } catch (e) { console.error('[customer-detail] loadDetail 失败:', e) this.setData({ pageState: 'error' }) } finally { wx.hideLoading() } }, async _loadAIInsight(memberId: string) { // cache_type 统一为 'app7_customer'(W1-AI-CLOSURE 组 1 数据库迁移已对齐)。 // App7Result.strategies = [{title, content}],前端拼成 demo 标杆 {color, text} 单字段。 const cache = await fetchAICache('app7_customer', memberId) if (!cache?.result_json) return const rj = cache.result_json as { summary?: string; strategies?: Array<{ title?: string; content?: string; text?: string }> } const COLORS = ['blue', 'indigo', 'purple', 'red', 'orange', 'yellow'] as const const strategies = Array.isArray(rj.strategies) ? rj.strategies.map((s, i) => { const t = (s?.title || '').trim() const c = (s?.content || '').trim() const text = s?.text || (t && c ? `${t}:${c}` : (c || t)) return { color: COLORS[i % COLORS.length], text } }) : [] this.setData({ 'aiInsight.summary': rj.summary || '', 'aiInsight.strategies': strategies, }) }, onRetry() { const id = this.data.detail?.id || '' this.loadDetail(id) }, /** 查看/隐藏手机号 */ onTogglePhone() { this.setData({ phoneVisible: !this.data.phoneVisible }) }, /** 复制手机号(复制完整号码) */ onCopyPhone() { const phone = this.data.detail.phoneFull || this.data.detail.phone wx.setClipboardData({ data: phone, success: () => { wx.showToast({ title: '手机号码已复制', icon: 'none' }) }, }) }, onViewServiceRecords() { const customerId = this.data.detail?.id || '' wx.navigateTo({ url: `/pages/customer-records/customer-records?customerId=${customerId}`, fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }), }) }, onStartChat() { const customerId = this.data.detail?.id || '' wx.navigateTo({ url: `/pages/chat/chat?customerId=${customerId}`, fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }), }) }, onAddNote() { this.setData({ noteModalVisible: true }) }, // CHANGE 2026-03-29 | 备注创建调用后端 API,保存后直接插入列表顶部 async onNoteConfirm(e: any) { const { content, score } = e.detail || {} this.setData({ noteModalVisible: false }) if (!content) return try { const { createNote } = require('../../services/api') const result = await createNote({ targetId: Number(this.data.detail.id), content, score: score || undefined, }) wx.showToast({ title: '备注已保存', icon: 'success' }) // 直接插入到列表顶部,不刷新整页 const now = new Date() const timeStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}` const newNote = { id: result?.id || Date.now(), tagLabel: '备注', createdAt: timeStr, content, } this.setData({ sortedNotes: [newNote, ...this.data.sortedNotes], }) } catch { wx.showToast({ title: '备注保存失败', icon: 'none' }) } }, onNoteCancel() { this.setData({ noteModalVisible: false }) }, onToggleFavCoaches() { this.setData({ favCoachExpanded: !this.data.favCoachExpanded }) }, onDeleteNote(e: WechatMiniprogram.TouchEvent) { const noteId = e.currentTarget.dataset.id if (!noteId) return wx.showModal({ title: '确认删除', content: '删除后不可恢复', success: async (res) => { if (!res.confirm) return try { const { deleteNote } = require('../../services/api') await deleteNote(noteId) // 从列表中移除 this.setData({ sortedNotes: this.data.sortedNotes.filter((n: any) => n.id !== noteId), }) wx.showToast({ title: '已删除', icon: 'success' }) } catch { wx.showToast({ title: '删除失败', icon: 'none' }) } }, }) }, })