Browse Source

智能体对话页联调

master
kola-web 6 days ago
parent
commit
7ca41235b6
  1. 6
      src/app.json
  2. 2
      src/custom-tab-bar/index.ts
  3. 7
      src/pages/chat/index.json
  4. 45
      src/pages/chat/index.scss
  5. 413
      src/pages/chat/index.ts
  6. 55
      src/pages/chat/index.wxml
  7. 6
      src/pages/chatHistory/index.json
  8. 83
      src/pages/chatHistory/index.scss
  9. 119
      src/pages/chatHistory/index.ts
  10. 36
      src/pages/chatHistory/index.wxml

6
src/app.json

@ -64,6 +64,12 @@ @@ -64,6 +64,12 @@
"resolveAlias": {
"@/*": "/*"
},
"plugins": {
"WechatSI": {
"version": "0.3.7",
"provider": "wx069ba97219f66d99"
}
},
"usingComponents": {
"navbar": "/components/navbar/index"
},

2
src/custom-tab-bar/index.ts

@ -71,7 +71,7 @@ Component({ @@ -71,7 +71,7 @@ Component({
// 获取通知未读数
async fetchUnreadCount() {
try {
await app.waitLogin({ type: 1 })
await app.waitLogin({ type: 0 })
const res = await wx.ajax({
url: '/notification/unread-count',
method: 'GET',

7
src/pages/chat/index.json

@ -1,7 +1,12 @@ @@ -1,7 +1,12 @@
{
"navigationBarTitleText": "智能助手",
"requiredPrivateInfos": ["getRecorderManager"],
"permission": {
"scope.record": {
"desc": "需要使用你的麦克风进行语音识别"
}
},
"usingComponents": {
"van-icon": "@vant/weapp/icon/index"
}
}

45
src/pages/chat/index.scss

@ -18,10 +18,14 @@ page { @@ -18,10 +18,14 @@ page {
/* ========== 聊天消息列表 ========== */
.chat-scroll {
flex: 1;
min-height: 0;
overflow: hidden;
position: relative;
padding: 160rpx 28rpx 0;
box-sizing: border-box;
padding: 0 28rpx 0;
box-sizing: border-box;
.scroll-top-safe {
height: 160px;
}
.history {
width: 167rpx;
@ -70,6 +74,7 @@ page { @@ -70,6 +74,7 @@ page {
font-size: 32rpx;
color: #fff;
line-height: 56rpx;
word-break: break-all;
}
.ai-message {
clear: both;
@ -82,6 +87,7 @@ page { @@ -82,6 +87,7 @@ page {
color: rgba(71, 85, 105, 1);
background-color: #fff;
border-radius: 24rpx;
word-break: break-all;
.high {
display: inline;
color: rgba(74, 184, 253, 1);
@ -90,7 +96,7 @@ page { @@ -90,7 +96,7 @@ page {
}
.scroll-bottom-safe {
clear: both;
height: 223rpx;
height: 283rpx;
}
}
@ -105,6 +111,19 @@ page { @@ -105,6 +111,19 @@ page {
border-radius: 95rpx;
background-color: #fff;
box-shadow: 0px 15px 30px 0px rgba(0, 96, 143, 0.09);
.go-act-draft {
position: absolute;
top: -20rpx;
left: 0;
padding: 12rpx 28rpx;
transform: translateY(-100%);
font-size: 32rpx;
color: rgba(71, 85, 105, 1);
border-radius: 69rpx;
box-shadow: 0rpx 11rpx 21rpx 0rpx rgba(0, 96, 143, 0.09);
background: linear-gradient(0deg, #ffffff 0%, #f8fdff 100%);
line-height: 44rpx;
}
.freetext {
padding-left: 26rpx;
height: 100%;
@ -113,6 +132,22 @@ page { @@ -113,6 +132,22 @@ page {
.input {
flex: 1;
}
.send-btn {
flex-shrink: 0;
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: rgba(226, 232, 240, 1);
display: flex;
align-items: center;
justify-content: center;
margin-right: 10rpx;
color: #fff;
&--active {
background: linear-gradient(135deg, #9ddffd 0%, #4ab8fd 100%);
color: #fff;
}
}
.icon {
padding-left: 26rpx;
flex-shrink: 0;
@ -137,7 +172,11 @@ page { @@ -137,7 +172,11 @@ page {
height: 60rpx;
}
}
.voice--hidden {
visibility: hidden;
}
.voiceing {
margin: 0 -18rpx;
padding: 213rpx 0 0;
position: fixed;
left: 0;

413
src/pages/chat/index.ts

@ -1,12 +1,16 @@ @@ -1,12 +1,16 @@
/**
* Chat
* Chat
*/
const app = getApp<IAppOption>()
const plugin = requirePlugin('WechatSI')
const recognitionManager = plugin.getRecordRecognitionManager()
interface Message {
id: string
role: 'user' | 'ai'
content: string
timestamp: number
type: 'u_msg' | 'a_msg'
role: 'user' | 'agent'
msg: string
time: string
}
Page({
@ -17,52 +21,109 @@ Page({ @@ -17,52 +21,109 @@ Page({
inputText: '',
/** 输入模式:text 或 voice */
inputMode: 'voice' as 'text' | 'voice',
/** 是否正在录音 */
/** 是否正在录音识别 */
isRecording: false,
/** 录音提示文本 */
recordingTip: '松手发送,上移取消',
/** 录音管理器 */
recorderManager: null as WechatMiniprogram.RecorderManager | null,
recordingTip: '松手结束,上移取消',
/** 是否取消本次识别 */
cancelled: false,
/** 滚动到指定消息 */
scrollToView: '',
/** 页面顶部距离 */
pageTop: 0,
/** 导航栏背景 */
background: 'transparent',
/** 当前会话ID,为空则新建 */
sessionId: '',
/** 是否正在等待AI回复 */
sending: false,
/** 接口返回的活动ID,有值时显示草稿入口 */
activityId: '',
},
onLoad() {
// 初始化录音管理器
const recorderManager = wx.getRecorderManager()
recorderManager.onStart(() => {
this.setData({ isRecording: true, recordingTip: '松手发送,上移取消' })
})
recorderManager.onStop((res) => {
_sessionId: '',
onLoad(options: Record<string, string>) {
const sessionId = options.session_id || ''
this._sessionId = sessionId
this.setData({ sessionId })
// 语音识别回调
recognitionManager.onStart = () => {
this.setData({ cancelled: false, recordingTip: '松手结束,上移取消' })
}
recognitionManager.onStop = (res: any) => {
this.setData({ isRecording: false })
if (res.duration < 1000) {
wx.showToast({ title: '录音时间太短', icon: 'none' })
return
if (this.data.cancelled) return
const result = (res.result || '').trim()
if (result) {
const separator = this.data.inputText ? ' ' : ''
this.setData({ inputText: this.data.inputText + separator + result, inputMode: 'text' })
}
// 发送语音消息
this.sendVoiceMessage(res.tempFilePath, res.duration)
})
recorderManager.onError((err) => {
}
recognitionManager.onError = (err: any) => {
this.setData({ isRecording: false })
wx.showToast({ title: '录音失败', icon: 'none' })
console.error('录音错误:', err)
if (err.errCode === -30003) return
wx.showToast({ title: '语音识别失败', icon: 'none' })
}
// 登录后加载历史
app.waitLogin({ type: 1 }).then(() => {
if (sessionId) {
this.loadSessionDetail(sessionId)
}
})
this.setData({ recorderManager })
},
// 获取系统信息设置页面顶部距离
const systemInfo = wx.getSystemInfoSync()
this.setData({ pageTop: systemInfo.statusBarHeight + 44 })
/** 从 a_msg 的 msg 字段中提取展示文本(可能为 JSON 字符串) */
parseAgentMsg(raw: string): string {
if (!raw) return ''
try {
const obj = JSON.parse(raw)
if (obj && typeof obj.msg === 'string') return obj.msg
} catch {}
return raw
},
/**
*
*/
/** 从接口33加载会话详情(历史消息) */
async loadSessionDetail(sessionId: string) {
try {
const res = await wx.ajax({
url: '/session/detail',
method: 'GET',
data: { session_id: sessionId },
})
if (res && res.messages) {
const messages = this.handleRemoveFirstMessage(res.messages).map((m: Message) => ({
...m,
msg: m.type === 'a_msg' ? this.parseAgentMsg(m.msg) : m.msg,
}))
this.setData({ messages })
}
if (res?.session?.activityId) {
this.setData({ activityId: res.session.activityId })
}
this.scrollToBottom()
} catch (err) {
console.error('加载会话详情失败:', err)
}
},
handleRemoveFirstMessage(messages: Message[]): Message[] {
if (messages.length === 0) return messages
return messages.slice(1)
},
/** 点击"最近对话"按钮 */
goHistory() {
wx.navigateTo({ url: '/pages/chatHistory/index' })
},
/** 点击"查看当前草稿"按钮 */
goActDraft() {
if (this.data.activityId) {
wx.navigateTo({ url: `/pages/actAdd/index?id=${this.data.activityId}` })
}
},
/** 切换到语音模式 */
switchToVoice() {
// 如果 input 有内容,不允许切换到语音模式
if (this.data.inputText.trim()) {
wx.showToast({ title: '请先发送或清空内容', icon: 'none' })
return
@ -70,181 +131,185 @@ Page({ @@ -70,181 +131,185 @@ Page({
this.setData({ inputMode: 'voice' })
},
/**
*
*/
/** 切换到文本模式 */
switchToText() {
this.setData({ inputMode: 'text' })
},
/**
*
*/
onInputChange(e: WechatMiniprogram.InputEvent) {
/** 输入框内容变化 */
onInputChange(e: any) {
this.setData({ inputText: e.detail.value })
},
/**
*
*/
/** 发送文本消息 */
onSendText() {
const { inputText } = this.data
if (!inputText.trim()) return
const { inputText, sending } = this.data
if (!inputText.trim() || sending) return
this.doInteract(inputText.trim())
},
/** 调用接口31: 活动智能体交互 */
async doInteract(uMsg: string) {
this.setData({ sending: true, inputText: '' })
// 添加用户消息
// 先将用户消息追加到列表
const userMessage: Message = {
id: `user_${Date.now()}`,
id: `temp_u_${Date.now()}`,
type: 'u_msg',
role: 'user',
content: inputText.trim(),
timestamp: Date.now(),
msg: uMsg,
time: this.formatTime(new Date()),
}
this.setData({
messages: [...this.data.messages, userMessage],
inputText: '',
const messages = [...this.handleRemoveFirstMessage(this.data.messages), userMessage]
this.setData({ messages })
this.scrollToBottom()
try {
const reqData: Record<string, any> = { u_msg: uMsg }
if (this._sessionId) {
reqData.session_id = this._sessionId
}
const res = await wx.ajax({
url: '/activity/interact',
method: 'POST',
data: reqData,
})
// 模拟 AI 回复
this.simulateAIResponse()
},
// 更新sessionId
if (res.session?.session_id) {
this._sessionId = res.session.session_id
this.setData({ sessionId: res.session.session_id })
}
/**
* chat-input
*/
onChatInputLongPress() {
// 文本模式下有内容时,不允许录音
if (this.data.inputMode === 'text' && this.data.inputText.trim()) {
wx.showToast({ title: '请先发送或清空内容', icon: 'none' })
// 内容不合规提示
if (res.resp === 'RESP_ILLEGAL') {
wx.showToast({ title: '内容不合规,请重新输入', icon: 'none' })
this.setData({ sending: false })
return
}
const { recorderManager } = this.data
if (!recorderManager) return
// 提取活动ID
if (res.data.activity_id) {
this.setData({ activityId: res.data.activity_id })
}
recorderManager.start({
duration: 60000,
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 48000,
format: 'mp3',
})
},
// 用服务端返回的消息列表替换
if (res.session && res.session.msg) {
const messages = this.handleRemoveFirstMessage(res.session.msg).map((m: Message) => ({
...m,
msg: m.type === 'a_msg' ? this.parseAgentMsg(m.msg) : m.msg,
}))
this.setData({ messages })
}
/**
* chat-input
*/
onChatInputStart() {
// 用于检测长按,不做实际操作
// 长按逻辑由 bindlongpress 处理
this.scrollToBottom()
} catch (err: any) {
// AI服务异常时,服务端可能返回session_id用于重试
if (err?.data?.session_id) {
this._sessionId = err.data.session_id
this.setData({ sessionId: err.data.session_id })
}
wx.showToast({ title: '发送失败,请重试', icon: 'none' })
} finally {
this.setData({ sending: false })
}
},
/**
* chat-input
*/
onChatInputEnd() {
const { recorderManager, isRecording } = this.data
if (!recorderManager || !isRecording) return
recorderManager.stop()
/** 重置当前会话 */
resetSession() {
if (!this._sessionId) return
wx.showModal({
title: '提示',
content: '确定要重置当前对话吗?',
success: async (res) => {
if (!res.confirm) return
try {
await wx.ajax({
url: '/activity/interact',
method: 'POST',
data: {
session_id: this._sessionId,
reset: true,
},
})
this._sessionId = ''
this.setData({ sessionId: '', messages: [], activityId: '' })
wx.showToast({ title: '已重置', icon: 'success' })
} catch {
wx.showToast({ title: '重置失败', icon: 'none' })
}
},
})
},
/**
* chat-input
*/
onChatInputCancel() {
const { recorderManager, isRecording } = this.data
if (!recorderManager || !isRecording) return
recorderManager.stop()
this.setData({ isRecording: false, recordingTip: '录音已取消' })
wx.showToast({ title: '录音已取消', icon: 'none' })
/** 格式化时间 */
formatTime(date: Date): string {
const pad = (n: number) => n.toString().padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
},
/**
* voiceing
*/
onVoiceingTouchMove(e: WechatMiniprogram.TouchEvent) {
const { isRecording, recordingTip } = this.data
if (!isRecording) return
/** 滚动到底部 */
scrollToBottom() {
setTimeout(() => {
this.setData({ scrollToView: 'scroll-bottom-safe' })
}, 100)
},
// 获取触摸点位置
const touch = e.touches[0]
const systemInfo = wx.getSystemInfoSync()
const screenHeight = systemInfo.screenHeight
/** 阻止滚动穿透 */
preventTouchMove() {},
// voiceing 区域高度为 448rpx,约 224px
// 当手指 Y 坐标小于 (屏幕高度 - voiceing高度) 时,说明手指移出了区域
const voiceingHeight = 224 // 448rpx ≈ 224px
const threshold = screenHeight - voiceingHeight
// ===== 语音识别相关 =====
onVoiceTouchStart() {},
onVoiceLongPress() {
wx.authorize({ scope: 'scope.record' })
.then(() => {
this.setData({ isRecording: true, cancelled: false, recordingTip: '松手结束,上移取消' })
recognitionManager.start({ lang: 'zh_CN' })
})
.catch(() => {
wx.showModal({
title: '提示',
content: '需要录音权限才能使用语音输入,是否前往设置开启?',
success: (res) => {
if (res.confirm) wx.openSetting()
},
})
})
},
if (touch.clientY < threshold) {
// 手指移出区域,准备取消
if (recordingTip !== '上移取消录音') {
this.setData({ recordingTip: '上移取消录音' })
}
} else {
// 手指在区域内,恢复提示
if (recordingTip !== '松手发送,上移取消') {
this.setData({ recordingTip: '松手发送,上移取消' })
}
}
isAboveCancelZone(clientY: number) {
const { screenHeight } = wx.getWindowInfo()
return clientY < screenHeight - 224
},
/**
* voiceing
*/
onVoiceingTouchEnd(e: WechatMiniprogram.TouchEvent) {
const { recorderManager, isRecording, recordingTip } = this.data
if (!recorderManager || !isRecording) return
// 获取触摸点位置
const changedTouch = e.changedTouches[0]
const systemInfo = wx.getSystemInfoSync()
const screenHeight = systemInfo.screenHeight
const voiceingHeight = 224 // 448rpx ≈ 224px
const threshold = screenHeight - voiceingHeight
// 如果手指移出区域,取消录音
if (changedTouch.clientY < threshold || recordingTip === '上移取消录音') {
recorderManager.stop()
this.setData({ isRecording: false, recordingTip: '录音已取消' })
wx.showToast({ title: '录音已取消', icon: 'none' })
} else {
// 手指在区域内,正常结束录音并发送
recorderManager.stop()
onVoiceTouchMove(e: WechatMiniprogram.TouchEvent) {
if (!this.data.isRecording) return
const above = this.isAboveCancelZone(e.touches[0].clientY)
const tip = above ? '松手取消识别' : '松手结束,上移取消'
if (this.data.recordingTip !== tip) {
this.setData({ recordingTip: tip })
}
},
/**
*
*/
sendVoiceMessage(filePath: string, duration: number) {
const userMessage: Message = {
id: `user_${Date.now()}`,
role: 'user',
content: `[语音 ${Math.ceil(duration / 1000)}秒]`,
timestamp: Date.now(),
onVoiceTouchEnd(e: WechatMiniprogram.TouchEvent) {
if (!this.data.isRecording) return
const cancel = this.isAboveCancelZone(e.changedTouches[0].clientY) || this.data.recordingTip === '松手取消识别'
if (cancel) {
this.setData({ cancelled: true, inputText: '', isRecording: false, recordingTip: '识别已取消' })
recognitionManager.stop()
wx.showToast({ title: '识别已取消', icon: 'none' })
} else {
recognitionManager.stop()
}
this.setData({
messages: [...this.data.messages, userMessage],
})
// 模拟 AI 回复
this.simulateAIResponse()
},
/**
* AI
*/
simulateAIResponse() {
setTimeout(() => {
const aiMessage: Message = {
id: `ai_${Date.now()}`,
role: 'ai',
content: '好的,我已经收到您的消息,正在为您处理中...',
timestamp: Date.now(),
}
this.setData({
messages: [...this.data.messages, aiMessage],
})
}, 1000)
onVoiceTouchCancel() {
if (!this.data.isRecording) return
this.setData({ cancelled: true, inputText: '', isRecording: false, recordingTip: '识别已取消' })
recognitionManager.stop()
wx.showToast({ title: '识别已取消', icon: 'none' })
},
})

55
src/pages/chat/index.wxml

@ -3,64 +3,69 @@ @@ -3,64 +3,69 @@
style="background: url('{{imageUrl}}bg3.png?t={{Timestamp}}') no-repeat top center/100% 556rpx;padding-top: {{pageTop}}px;"
>
<navbar fixed customStyle="background:{{background}};">
<van-icon class="page-back" name="arrow-left" slot="left" />
<van-icon class="page-back" name="arrow-left" slot="left" bind:tap="goHistory" />
</navbar>
<scroll-view class="chat-scroll" scroll-y scroll-into-view="{{scrollToView}}" enhanced show-scrollbar="{{false}}">
<view class="history">最近对话</view>
<view class="scroll-top-safe" ></view>
<view class="history" bind:tap="goHistory">最近对话</view>
<view class="first-card">
你好呀~我是你的活动创建小助手你可以直接告诉我想创建什么活动,比如
<view class="high">“下周五下午3点在报告厅办一场心理健康讲座”</view>
<view class="high">"下周五下午3点在报告厅办一场心理健康讲座"</view>
我会帮你自动整理活动信息。
</view>
<view class="tip">特别说明:内容均为ai生成,仅供参考</view>
<view class="tip" wx:if="{{messages.length === 0}}">特别说明:内容均为ai生成,仅供参考</view>
<view class="chat-messages">
<block wx:for="{{4}}" wx:key="index">
<view class="user-message">请帮我创建一个下周活动,活动主题是新生音乐会,活动地点是学校音乐厅</view>
<view class="ai-message">
信息我已经整理好~我还需要再确认一点信息:
<view class="high">您的活动打算什么时候开始呢?告诉我开始和结束的时间,我来帮您创建~</view>
<block wx:for="{{messages}}" wx:key="id">
<view class="user-message" wx:if="{{item.type === 'u_msg'}}" id="msg-{{item.id}}">{{item.msg}}</view>
<view class="ai-message" wx:elif="{{item.type === 'a_msg'}}" id="msg-{{item.id}}">
<block wx:if="{{item.msg}}">
<text>{{item.msg}}</text>
</block>
</view>
</block>
</view>
<view class="scroll-bottom-safe"></view>
<view class="scroll-bottom-safe" id="scroll-bottom-safe"></view>
</scroll-view>
<view
class="chat-input"
bindtouchstart="onChatInputStart"
bindtouchend="onChatInputEnd"
bindtouchcancel="onChatInputCancel"
bindlongpress="onChatInputLongPress"
>
<view class="chat-input">
<view class="go-act-draft" wx:if="{{activityId}}" bind:tap="goActDraft">查看当前草稿<van-icon name="arrow" /></view>
<!-- 文本输入模式 -->
<view class="freetext" wx:if="{{inputMode === 'text'}}">
<input
class="input"
placeholder-class="input-place"
type="text"
cursor-spacing="{{20}}"
placeholder="发消息或按住说话..."
value="{{inputText}}"
disabled="{{isRecording}}"
bindinput="onInputChange"
bindconfirm="onSendText"
disabled="{{sending}}"
/>
<!-- 切换到语音模式按钮 -->
<image class="icon" src="{{imageUrl}}icon44.png?t={{Timestamp}}" bindtap="switchToVoice" catchtap></image>
<image class="icon" src="{{imageUrl}}icon44.png?t={{Timestamp}}" catch:tap="switchToVoice"></image>
</view>
<!-- 语音输入模式(未录音状态) -->
<view class="voice" wx:if="{{inputMode === 'voice' && !isRecording}}">
<!-- 语音输入模式 -->
<view
class="voice {{isRecording ? 'voice--hidden' : ''}}"
wx:if="{{inputMode === 'voice'}}"
bindtouchstart="onVoiceTouchStart"
bindlongpress="onVoiceLongPress"
catchtouchmove="onVoiceTouchMove"
bindtouchend="onVoiceTouchEnd"
bindtouchcancel="onVoiceTouchCancel"
>
<view class="content">按住说话</view>
<!-- 切换到文本模式按钮 -->
<image class="icon" src="{{imageUrl}}icon45.png?t={{Timestamp}}" bindtap="switchToText" catchtap></image>
<image class="icon" src="{{imageUrl}}icon45.png?t={{Timestamp}}" catch:tap="switchToText"></image>
</view>
<!-- 录音进行中状态 -->
<!-- 录音浮层 -->
<view
class="voiceing"
wx:if="{{isRecording}}"
style="background: url('{{imageUrl}}bg4.png?t={{Timestamp}}') no-repeat top center/100%"
bindtouchmove="onVoiceingTouchMove"
bindtouchend="onVoiceingTouchEnd"
catchtouchmove="preventTouchMove"
>
<view class="tip">{{recordingTip}}</view>
<image class="ani" src="{{imageUrl}}gif1.gif?t={{Timestamp}}"></image>

6
src/pages/chatHistory/index.json

@ -1,5 +1,9 @@ @@ -1,5 +1,9 @@
{
"navigationBarTitleText": "最近对话",
"navigationStyle": "default",
"usingComponents": {}
"enablePullDownRefresh": true,
"usingComponents": {
"van-icon": "@vant/weapp/icon/index",
"van-loading": "@vant/weapp/loading/index"
}
}

83
src/pages/chatHistory/index.scss

@ -1,29 +1,100 @@ @@ -1,29 +1,100 @@
page {
background-color: rgba(247, 248, 250, 1);
}
.page {
padding: 30rpx;
.card {
padding-bottom: 180rpx;
min-height: 100vh;
box-sizing: border-box;
}
.card {
padding: 32rpx;
background-color: #fff;
border-radius: 24rpx;
display: flex;
gap: 24rpx;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
.wrap {
flex: 1;
min-width: 0;
.title {
font-size: 36rpx;
color: rgba(17, 24, 39, 1);
line-height: 56rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.date{
.desc {
margin-top: 14rpx;
font-size: 32rpx;
font-size: 28rpx;
color: rgba(148, 163, 184, 1);
}
}
.status{
font-size: 32rpx;
.meta {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 12rpx;
.count {
font-size: 28rpx;
color: rgba(203, 213, 225, 1);
}
}
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 240rpx;
.empty-text {
font-size: 32rpx;
color: rgba(148, 163, 184, 1);
}
.empty-btn {
margin-top: 40rpx;
padding: 18rpx 64rpx;
background: linear-gradient(90deg, #9ddffd 0%, #4ab8fd 100%);
color: #fff;
font-size: 32rpx;
border-radius: 54rpx;
}
}
.loading {
display: flex;
justify-content: center;
padding: 40rpx 0;
}
.no-more {
text-align: center;
padding: 30rpx 0;
font-size: 28rpx;
color: rgba(203, 213, 225, 1);
}
.fab {
position: fixed;
right: 40rpx;
bottom: 120rpx;
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: linear-gradient(135deg, #9ddffd 0%, #4ab8fd 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(74, 184, 253, 0.35);
}

119
src/pages/chatHistory/index.ts

@ -1,8 +1,119 @@ @@ -1,8 +1,119 @@
const _app = getApp<IAppOption>();
const app = getApp<IAppOption>()
interface ChatSession {
session_id: string
uid: string
agent_type: string
title: string
last_message: string
last_time: string
created_at: string
msg_count: number
}
Page({
data: {},
onLoad() {},
});
data: {
list: [] as ChatSession[],
total: 0,
offset: 0,
limit: 20,
loading: false,
noMore: false,
},
onLoad() {
app.waitLogin({ type: 1 }).then(() => {
this.fetchList(true)
})
},
onPullDownRefresh() {
this.fetchList(true).then(() => {
wx.stopPullDownRefresh()
})
},
onReachBottom() {
if (this.data.loading || this.data.noMore) return
this.fetchList()
},
async fetchList(refresh = false) {
if (this.data.loading) return
const offset = refresh ? 0 : this.data.offset
this.setData({ loading: true })
try {
const res = await wx.ajax({
url: '/session/list',
method: 'GET',
data: {
agent_type: 'activity',
limit: this.data.limit,
offset,
},
})
const newList = res.list || []
const total = res.total || 0
const list = refresh ? newList : [...this.data.list, ...newList]
this.setData({
list,
total,
offset: offset + newList.length,
noMore: list.length >= total,
})
} catch (err) {
console.error('获取会话列表失败:', err)
} finally {
this.setData({ loading: false })
}
},
/** 点击会话卡片,跳转到聊天页恢复会话 */
onTapSession(e: WechatMiniprogram.TouchEvent) {
const { sessionId } = e.currentTarget.dataset
wx.navigateTo({ url: `/pages/chat/index?session_id=${sessionId}` })
},
/** 删除会话 */
onLongPressSession(e: WechatMiniprogram.TouchEvent) {
const { sessionId, index } = e.currentTarget.dataset
wx.showModal({
title: '提示',
content: '确定要删除该对话吗?',
success: async (res) => {
if (!res.confirm) return
try {
await wx.ajax({
url: '/activity/interact',
method: 'POST',
data: {
session_id: sessionId,
reset: true,
},
})
wx.showToast({ title: '删除成功', icon: 'success' })
const list = [...this.data.list]
list.splice(index, 1)
const total = this.data.total - 1
this.setData({ list, total })
} catch (err: any) {
wx.showToast({ title: '删除失败', icon: 'none' })
}
},
})
},
/** 新建对话 */
onCreateChat() {
wx.navigateTo({ url: '/pages/chat/index' })
},
})
export {}

36
src/pages/chatHistory/index.wxml

@ -1,9 +1,37 @@ @@ -1,9 +1,37 @@
<view class="page">
<view class="card">
<view class="list">
<view
class="card"
wx:for="{{list}}"
wx:key="session_id"
data-session-id="{{item.session_id}}"
data-index="{{index}}"
bind:tap="onTapSession"
bindlongpress="onLongPressSession"
>
<view class="wrap">
<view class="title">新生音乐会</view>
<view class="date">2026/6/15</view>
<view class="title">{{item.title || '新对话'}}</view>
<view class="desc">{{item.last_time}}</view>
</view>
<view class="status">草稿</view>
<view class="meta">
<view class="count">{{item.msg_count}}条对话</view>
<van-icon name="arrow" color="rgba(203, 213, 225, 1)" />
</view>
</view>
</view>
<view class="empty" wx:if="{{!loading && list.length === 0}}">
<view class="empty-text">暂无对话记录</view>
<view class="empty-btn" bind:tap="onCreateChat">发起对话</view>
</view>
<view class="loading" wx:if="{{loading}}">
<van-loading size="24px" color="rgba(74, 184, 253, 1)">加载中...</van-loading>
</view>
<view class="no-more" wx:if="{{noMore && list.length > 0}}">没有更多了</view>
<view class="fab" bind:tap="onCreateChat">
<van-icon name="edit" size="20px" color="#fff" />
</view>
</view>

Loading…
Cancel
Save