微信小程序页面迁移校验之前 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,11 @@
{
"navigationBarTitleText": "任务详情",
"usingComponents": {
"ai-float-button": "/components/ai-float-button/ai-float-button",
"note-modal": "/components/note-modal/note-modal",
"star-rating": "/components/star-rating/star-rating",
"heart-icon": "/components/heart-icon/heart-icon",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-loading": "tdesign-miniprogram/loading/loading"
}
}

View File

@@ -0,0 +1,104 @@
import { mockTaskDetails } from '../../utils/mock-data'
import type { TaskDetail, Note } from '../../utils/mock-data'
import { sortByTimestamp } from '../../utils/sort'
Page({
data: {
/** 页面状态 */
pageState: 'loading' as 'loading' | 'empty' | 'normal',
/** 任务详情 */
detail: null as TaskDetail | null,
/** 排序后的备注列表 */
sortedNotes: [] as Note[],
/** 备注弹窗 */
noteModalVisible: false,
},
onLoad(options) {
const id = options?.id || ''
this.loadData(id)
},
loadData(id: string) {
this.setData({ pageState: 'loading' })
setTimeout(() => {
// TODO: 替换为真实 API 调用
const detail = mockTaskDetails.find((t) => t.id === id) || mockTaskDetails[0]
if (!detail) {
this.setData({ pageState: 'empty' })
return
}
const sorted = sortByTimestamp(detail.notes || []) as Note[]
this.setData({
pageState: 'normal',
detail,
sortedNotes: sorted,
})
}, 500)
},
/** 点击"添加备注" */
onAddNote() {
this.setData({ noteModalVisible: true })
},
/** 备注弹窗确认 */
onNoteConfirm(e: WechatMiniprogram.CustomEvent) {
const { score, content } = e.detail
wx.showToast({ title: '备注已保存', icon: 'success' })
this.setData({ noteModalVisible: false })
// 模拟添加到列表
const newNote: Note = {
id: `note-${Date.now()}`,
content,
tagType: 'customer',
tagLabel: `客户:${this.data.detail?.customerName || ''}`,
createdAt: new Date().toLocaleString('zh-CN', { hour12: false }),
}
const notes = [newNote, ...this.data.sortedNotes]
this.setData({ sortedNotes: notes })
},
/** 备注弹窗取消 */
onNoteCancel() {
this.setData({ noteModalVisible: false })
},
/** 放弃任务 */
onAbandon() {
wx.showModal({
title: '放弃任务',
content: '确定要放弃该客户的维护吗?此操作不可撤销。',
confirmColor: '#e34d59',
success: (res) => {
if (res.confirm) {
wx.showToast({ title: '已放弃该客户的维护', icon: 'none' })
setTimeout(() => wx.navigateBack(), 1500)
}
},
})
},
/** 问问助手 */
onAskAssistant() {
const customerId = this.data.detail?.id || ''
wx.navigateTo({
url: `/pages/chat/chat?customerId=${customerId}`,
fail: () => wx.showToast({ title: '页面跳转失败', icon: 'none' }),
})
},
/** 标记完成 */
onMarkComplete() {
wx.showToast({ title: '已标记完成', icon: 'success' })
setTimeout(() => wx.navigateBack(), 1500)
},
/** 返回 */
onBack() {
wx.navigateBack()
},
})

View File

@@ -0,0 +1,125 @@
<wxs src="../../utils/format.wxs" module="fmt" />
<!-- 加载态 -->
<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 banner-red">
<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>
<text class="nav-abandon" bindtap="onAbandon">放弃</text>
</view>
<view class="customer-info">
<view class="avatar-box">
<text class="avatar-text">{{detail.customerName[0] || '?'}}</text>
</view>
<view class="info-right">
<view class="name-row">
<text class="customer-name">{{detail.customerName}}</text>
<text class="task-type-tag">{{detail.taskTypeLabel || '高优先召回'}}</text>
</view>
<view class="sub-info">
<text class="phone">138****5678</text>
</view>
</view>
</view>
</view>
<!-- 主体内容 -->
<view class="main-content">
<!-- 与我的关系 -->
<view class="card">
<view class="card-header">
<text class="section-title title-pink">与我的关系</text>
<text class="ai-badge">AI智能洞察</text>
</view>
<view class="relationship-row">
<view class="rel-tag rel-tag-pink">
<heart-icon score="{{detail.heartScore}}" />
<text>{{detail.heartScore > 8.5 ? '非常好' : detail.heartScore > 7 ? '良好' : detail.heartScore > 5 ? '一般' : '待发展'}}</text>
</view>
<view class="rel-bar">
<view class="rel-bar-fill" style="width: {{detail.heartScore * 10}}%"></view>
</view>
<text class="rel-score">{{fmt.toFixed(detail.heartScore / 10, 2)}}</text>
</view>
<text class="card-desc">{{detail.aiAnalysis.summary}}</text>
</view>
<!-- 任务建议 -->
<view class="card">
<text class="section-title title-orange">任务建议</text>
<view class="suggestion-box">
<view class="suggestion-header">
<text class="suggestion-icon">💡 建议执行</text>
<text class="ai-badge">AI智能洞察</text>
</view>
<view class="suggestion-list">
<view class="suggestion-item" wx:for="{{detail.aiAnalysis.suggestions}}" wx:key="index">
<text>• {{item}}</text>
</view>
</view>
</view>
</view>
<!-- 我给TA的备注 -->
<view class="card">
<view class="card-header">
<text class="section-title title-blue">我给TA的备注</text>
<text class="note-count">{{sortedNotes.length}} 条备注</text>
</view>
<block wx:if="{{sortedNotes.length > 0}}">
<view class="note-item" wx:for="{{sortedNotes}}" wx:key="id">
<view class="note-top">
<text class="note-date">{{item.createdAt}}</text>
<text class="note-tag-inline {{item.tagType === 'coach' ? 'tag-coach-inline' : 'tag-customer-inline'}}">{{item.tagLabel}}</text>
</view>
<text class="note-content">{{item.content}}</text>
</view>
</block>
<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-ask" bindtap="onAskAssistant">
<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.customerName}}"
initialScore="{{0}}"
initialContent=""
bind:confirm="onNoteConfirm"
bind:cancel="onNoteCancel"
/>
<!-- AI 悬浮按钮 -->
<ai-float-button bottom="{{200}}" customerId="{{detail.id}}" />
</block>
<dev-fab />

View File

@@ -0,0 +1,288 @@
/* 加载态 & 空态 */
.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;
color: #ffffff;
padding-bottom: 48rpx;
}
.banner-red {
background: linear-gradient(135deg, #e34d59 0%, #c62828 100%);
}
.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;
}
.nav-abandon {
font-size: var(--font-sm, 28rpx);
color: rgba(255, 255, 255, 0.5);
}
/* 客户信息 */
.customer-info {
display: flex;
align-items: center;
gap: 32rpx;
padding: 16rpx 40rpx 0;
}
.avatar-box {
width: 128rpx;
height: 128rpx;
border-radius: 32rpx;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.avatar-text {
font-size: 48rpx;
font-weight: 700;
color: #ffffff;
}
.info-right {
flex: 1;
}
.name-row {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 16rpx;
}
.customer-name {
font-size: var(--font-xl, 40rpx);
font-weight: 600;
}
.task-type-tag {
font-size: var(--font-xs, 24rpx);
padding: 4rpx 16rpx;
background: rgba(255, 255, 255, 0.25);
border-radius: 100rpx;
}
.sub-info {
display: flex;
align-items: center;
gap: 32rpx;
}
.phone {
font-size: var(--font-sm, 28rpx);
color: rgba(255, 255, 255, 0.7);
}
/* 主体内容 */
.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-sm, 28rpx);
font-weight: 600;
color: var(--color-gray-13, #242424);
}
.title-pink { border-left: 6rpx solid #ec4899; padding-left: 16rpx; }
.title-orange { border-left: 6rpx solid #ed7b2f; padding-left: 16rpx; margin-bottom: 32rpx; }
.title-blue { border-left: 6rpx solid #0052d9; padding-left: 16rpx; }
.title-green { border-left: 6rpx solid #00a870; padding-left: 16rpx; }
.ai-badge {
font-size: 22rpx;
color: var(--color-primary, #0052d9);
background: var(--color-primary-light, #ecf2fe);
padding: 4rpx 16rpx;
border-radius: 100rpx;
}
/* 关系区域 */
.relationship-row {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 16rpx;
}
.rel-tag {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 24rpx;
border-radius: 24rpx;
font-size: var(--font-sm, 28rpx);
font-weight: 600;
color: #ffffff;
flex-shrink: 0;
}
.rel-tag-pink {
background: linear-gradient(135deg, #ec4899, #f43f5e);
}
.rel-bar {
flex: 1;
height: 12rpx;
background: var(--color-gray-1, #f3f3f3);
border-radius: 100rpx;
overflow: hidden;
}
.rel-bar-fill {
height: 100%;
background: linear-gradient(90deg, #f9a8d4, #f43f5e);
border-radius: 100rpx;
}
.rel-score {
font-size: var(--font-lg, 36rpx);
font-weight: 700;
color: #ec4899;
}
.card-desc {
font-size: var(--font-sm, 28rpx);
color: var(--color-gray-8, #777777);
line-height: 1.6;
}
/* 任务建议 */
.suggestion-box {
background: linear-gradient(135deg, #eff6ff, #eef2ff);
border-radius: var(--radius-lg, 24rpx);
padding: 32rpx;
border: 1rpx solid #dbeafe;
}
.suggestion-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.suggestion-icon {
font-size: var(--font-sm, 28rpx);
font-weight: 500;
color: var(--color-primary, #0052d9);
}
.suggestion-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.suggestion-item {
font-size: var(--font-sm, 28rpx);
color: var(--color-gray-9, #5e5e5e);
line-height: 1.6;
}
/* 备注 */
.note-count {
font-size: var(--font-xs, 24rpx);
color: var(--color-gray-6, #a6a6a6);
}
.note-item {
padding: 28rpx;
background: var(--color-gray-1, #f3f3f3);
border-radius: var(--radius-lg, 24rpx);
border: 1rpx solid var(--color-gray-3, #e7e7e7);
margin-bottom: 24rpx;
}
.note-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.note-date {
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;
}
.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-ask {
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);
}