微信小程序页面迁移校验之前 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,7 @@
{
"navigationBarTitleText": "AI 助手",
"usingComponents": {
"t-loading": "tdesign-miniprogram/loading/loading",
"t-icon": "tdesign-miniprogram/icon/icon"
}
}

View File

@@ -0,0 +1,168 @@
// pages/chat/chat.ts — AI 对话页
import { mockChatMessages } from '../../utils/mock-data'
import type { ChatMessage } from '../../utils/mock-data'
import { simulateStreamOutput } from '../../utils/chat'
/** 将 referenceCard.data (Record<string,string>) 转为数组供 WXML wx:for 渲染 */
function toDataList(data?: Record<string, string>): Array<{ key: string; value: string }> {
if (!data) return []
return Object.keys(data).map((k) => ({ key: k, value: data[k] }))
}
/** 为消息列表中的 referenceCard 补充 dataList 字段 */
function enrichMessages(msgs: ChatMessage[]) {
return msgs.map((m) => ({
...m,
referenceCard: m.referenceCard
? { ...m.referenceCard, dataList: toDataList(m.referenceCard.data) }
: undefined,
}))
}
/** Mock AI 回复模板 */
const mockAIReplies = [
'根据数据分析,这位客户近期消费频次有所下降,建议安排一次主动回访,了解具体原因。',
'好的,我已经记录了你的需求。下次回访时我会提醒你。',
'这位客户偏好中式台球,最近对斯诺克也产生了兴趣,可以推荐相关课程。',
]
Page({
data: {
/** 页面状态 */
pageState: 'loading' as 'loading' | 'empty' | 'normal',
/** 消息列表 */
messages: [] as Array<ChatMessage & { referenceCard?: ChatMessage['referenceCard'] & { dataList?: Array<{ key: string; value: string }> } }>,
/** 输入框内容 */
inputText: '',
/** AI 正在流式回复 */
isStreaming: false,
/** 流式输出中的内容 */
streamingContent: '',
/** 滚动锚点 */
scrollToId: '',
/** 页面顶部引用卡片(从其他页面跳转时) */
referenceCard: null as { title: string; summary: string } | null,
/** 客户 ID */
customerId: '',
},
/** 消息计数器,用于生成唯一 ID */
_msgCounter: 0,
onLoad(options) {
const customerId = options?.customerId || ''
this.setData({ customerId })
this.loadMessages(customerId)
},
/** 加载消息Mock */
loadMessages(customerId: string) {
this.setData({ pageState: 'loading' })
setTimeout(() => {
// TODO: 替换为真实 API 调用
const messages = enrichMessages(mockChatMessages)
this._msgCounter = messages.length
// 如果携带 customerId显示引用卡片
const referenceCard = customerId
? { title: '客户详情', summary: `正在查看客户 ${customerId} 的相关信息` }
: null
const isEmpty = messages.length === 0 && !referenceCard
this.setData({
pageState: isEmpty ? 'empty' : 'normal',
messages,
referenceCard,
})
// 滚动到底部
this.scrollToBottom()
}, 500)
},
/** 输入框内容变化 */
onInputChange(e: WechatMiniprogram.Input) {
this.setData({ inputText: e.detail.value })
},
/** 发送消息 */
onSendMessage() {
const text = this.data.inputText.trim()
if (!text || this.data.isStreaming) return
this._msgCounter++
const userMsg = {
id: `msg-user-${this._msgCounter}`,
role: 'user' as const,
content: text,
timestamp: new Date().toISOString(),
}
const messages = [...this.data.messages, userMsg]
this.setData({
messages,
inputText: '',
pageState: 'normal',
})
this.scrollToBottom()
// 模拟 AI 回复
setTimeout(() => {
this.triggerAIReply()
}, 300)
},
/** 触发 AI 流式回复 */
triggerAIReply() {
this._msgCounter++
const aiMsgId = `msg-ai-${this._msgCounter}`
const replyText = mockAIReplies[this._msgCounter % mockAIReplies.length]
// 先添加空的 AI 消息占位
const aiMsg = {
id: aiMsgId,
role: 'assistant' as const,
content: '',
timestamp: new Date().toISOString(),
}
const messages = [...this.data.messages, aiMsg]
this.setData({
messages,
isStreaming: true,
streamingContent: '',
})
this.scrollToBottom()
// 流式输出
const aiIndex = messages.length - 1
simulateStreamOutput(replyText, (partial: string) => {
const key = `messages[${aiIndex}].content`
this.setData({
[key]: partial,
streamingContent: partial,
})
this.scrollToBottom()
}).then(() => {
this.setData({
isStreaming: false,
streamingContent: '',
})
})
},
/** 滚动到底部 */
scrollToBottom() {
// 使用 nextTick 确保 DOM 更新后再滚动
setTimeout(() => {
this.setData({ scrollToId: '' })
setTimeout(() => {
this.setData({ scrollToId: 'scroll-bottom' })
}, 50)
}, 50)
},
})

View File

@@ -0,0 +1,125 @@
<!-- pages/chat/chat.wxml — AI 对话页 -->
<!-- 加载态 -->
<view class="loading-container" wx:if="{{pageState === 'loading'}}">
<t-loading theme="circular" size="48rpx" text="加载中..." />
</view>
<!-- 正常态 -->
<view class="chat-page" wx:elif="{{pageState === 'normal' || pageState === 'empty'}}">
<!-- 消息列表 -->
<scroll-view
class="message-list"
scroll-y
scroll-into-view="{{scrollToId}}"
scroll-with-animation
enhanced
show-scrollbar="{{false}}"
>
<!-- 引用卡片(从其他页面跳转时显示) -->
<view class="reference-card" wx:if="{{referenceCard}}">
<view class="reference-header">
<t-icon name="file-copy" size="32rpx" color="var(--color-gray-7)" />
<text class="reference-source">来源:{{referenceCard.title}}</text>
</view>
<text class="reference-summary">{{referenceCard.summary}}</text>
</view>
<!-- 空对话提示 -->
<view class="empty-hint" wx:if="{{pageState === 'empty' && messages.length === 0}}">
<view class="empty-icon">🤖</view>
<text class="empty-text">你好,我是 AI 助手</text>
<text class="empty-sub">有什么可以帮你的?</text>
</view>
<!-- 消息气泡列表 -->
<block wx:for="{{messages}}" wx:key="id">
<!-- 用户消息:右对齐蓝色 -->
<view
class="message-row message-user"
wx:if="{{item.role === 'user'}}"
id="msg-{{item.id}}"
>
<view class="bubble bubble-user">
<text class="bubble-text">{{item.content}}</text>
</view>
</view>
<!-- AI 消息:左对齐白色 -->
<view
class="message-row message-assistant"
wx:else
id="msg-{{item.id}}"
>
<view class="ai-avatar">
<text class="ai-avatar-emoji">🤖</text>
</view>
<view class="bubble-wrapper">
<view class="bubble bubble-assistant">
<text class="bubble-text">{{item.content}}</text>
</view>
<!-- 引用卡片AI 消息内联) -->
<view class="inline-ref-card" wx:if="{{item.referenceCard}}">
<view class="inline-ref-header">
<text class="inline-ref-type">{{item.referenceCard.type === 'customer' ? '👤 客户' : '📋 记录'}}</text>
<text class="inline-ref-title">{{item.referenceCard.title}}</text>
</view>
<text class="inline-ref-summary">{{item.referenceCard.summary}}</text>
<view class="inline-ref-data">
<view
class="ref-data-item"
wx:for="{{item.referenceCard.dataList}}"
wx:for-item="entry"
wx:key="key"
>
<text class="ref-data-key">{{entry.key}}</text>
<text class="ref-data-value">{{entry.value}}</text>
</view>
</view>
</view>
</view>
</view>
</block>
<!-- AI 正在输入指示器 -->
<view class="message-row message-assistant" wx:if="{{isStreaming && !streamingContent}}" id="msg-typing">
<view class="ai-avatar">
<text class="ai-avatar-emoji">🤖</text>
</view>
<view class="bubble bubble-assistant typing-bubble">
<view class="typing-dots">
<view class="dot dot-1"></view>
<view class="dot dot-2"></view>
<view class="dot dot-3"></view>
</view>
</view>
</view>
<!-- 底部占位,确保最后一条消息不被输入框遮挡 -->
<view class="scroll-bottom-spacer" id="scroll-bottom"></view>
</scroll-view>
<!-- 底部输入区域 -->
<view class="input-bar safe-area-bottom">
<view class="input-wrapper">
<input
class="chat-input"
value="{{inputText}}"
placeholder="输入消息..."
placeholder-class="input-placeholder"
confirm-type="send"
bindinput="onInputChange"
bindconfirm="onSendMessage"
disabled="{{isStreaming}}"
/>
</view>
<view
class="send-btn {{inputText.length > 0 && !isStreaming ? 'send-btn-active' : 'send-btn-disabled'}}"
bindtap="onSendMessage"
>
<t-icon name="send" size="40rpx" color="{{inputText.length > 0 && !isStreaming ? '#ffffff' : 'var(--color-gray-6)'}}" />
</view>
</view>
</view>
<dev-fab />

View File

@@ -0,0 +1,289 @@
/* pages/chat/chat.wxss — AI 对话页样式 */
/* 加载态 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
/* 页面容器 */
.chat-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: var(--color-gray-1);
}
/* ========== 消息列表 ========== */
.message-list {
flex: 1;
padding: 24rpx 32rpx;
padding-bottom: 0;
overflow-y: auto;
}
.scroll-bottom-spacer {
height: 32rpx;
}
/* ========== 引用卡片(页面顶部) ========== */
.reference-card {
background-color: var(--color-gray-2, #eeeeee);
border-radius: var(--radius-lg);
padding: 24rpx;
margin-bottom: 24rpx;
}
.reference-header {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 8rpx;
}
.reference-source {
font-size: var(--font-xs);
color: var(--color-gray-7);
}
.reference-summary {
font-size: var(--font-sm);
color: var(--color-gray-9);
line-height: 1.5;
}
/* ========== 空对话提示 ========== */
.empty-hint {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 200rpx;
}
.empty-icon {
font-size: 96rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: var(--font-lg);
color: var(--color-gray-13);
font-weight: 500;
margin-bottom: 8rpx;
}
.empty-sub {
font-size: var(--font-sm);
color: var(--color-gray-7);
}
/* ========== 消息行 ========== */
.message-row {
display: flex;
margin-bottom: 24rpx;
}
.message-user {
justify-content: flex-end;
}
.message-assistant {
justify-content: flex-start;
gap: 16rpx;
}
/* ========== AI 头像 ========== */
.ai-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: linear-gradient(135deg, var(--color-primary), #4d8ff7);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.ai-avatar-emoji {
font-size: 36rpx;
line-height: 1;
}
/* ========== 气泡 ========== */
.bubble-wrapper {
max-width: 80%;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.bubble {
padding: 20rpx 28rpx;
line-height: 1.6;
word-break: break-all;
}
.bubble-text {
font-size: var(--font-sm);
line-height: 1.6;
}
/* 用户气泡:蓝色,右上角方角 */
.bubble-user {
max-width: 80%;
background-color: var(--color-primary);
border-radius: 32rpx 8rpx 32rpx 32rpx;
}
.bubble-user .bubble-text {
color: #ffffff;
}
/* AI 气泡:白色,左上角方角 */
.bubble-assistant {
background-color: #ffffff;
border-radius: 8rpx 32rpx 32rpx 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.bubble-assistant .bubble-text {
color: var(--color-gray-13);
}
/* ========== AI 引用卡片(内联) ========== */
.inline-ref-card {
background-color: #ffffff;
border-radius: var(--radius-lg);
padding: 20rpx 24rpx;
border-left: 6rpx solid var(--color-primary);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.inline-ref-header {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 8rpx;
}
.inline-ref-type {
font-size: var(--font-xs);
color: var(--color-primary);
font-weight: 500;
}
.inline-ref-title {
font-size: var(--font-sm);
color: var(--color-gray-13);
font-weight: 500;
}
.inline-ref-summary {
font-size: var(--font-xs);
color: var(--color-gray-8);
margin-bottom: 12rpx;
display: block;
}
.inline-ref-data {
display: flex;
flex-wrap: wrap;
gap: 8rpx 24rpx;
}
.ref-data-item {
display: flex;
align-items: center;
gap: 8rpx;
}
.ref-data-key {
font-size: var(--font-xs);
color: var(--color-gray-7);
}
.ref-data-value {
font-size: var(--font-xs);
color: var(--color-gray-13);
font-weight: 500;
}
/* ========== 打字指示器 ========== */
.typing-bubble {
padding: 20rpx 32rpx;
}
.typing-dots {
display: flex;
align-items: center;
gap: 8rpx;
}
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: var(--color-gray-6);
animation: typingBounce 1.4s infinite ease-in-out;
}
.dot-1 { animation-delay: 0s; }
.dot-2 { animation-delay: 0.2s; }
.dot-3 { animation-delay: 0.4s; }
@keyframes typingBounce {
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
40% { transform: scale(1); opacity: 1; }
}
/* ========== 底部输入区域 ========== */
.input-bar {
display: flex;
align-items: center;
gap: 16rpx;
padding: 16rpx 24rpx;
background-color: #ffffff;
border-top: 1rpx solid var(--color-gray-2, #eeeeee);
}
.input-wrapper {
flex: 1;
background-color: var(--color-gray-1);
border-radius: 48rpx;
padding: 16rpx 28rpx;
}
.chat-input {
width: 100%;
font-size: var(--font-sm);
color: var(--color-gray-13);
line-height: 1.4;
}
.input-placeholder {
color: var(--color-gray-6);
font-size: var(--font-sm);
}
/* 发送按钮 */
.send-btn {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: background-color 0.2s;
}
.send-btn-active {
background-color: var(--color-primary);
}
.send-btn-disabled {
background-color: var(--color-gray-1);
}