Files
DevFlow/apps/df-miniapp/src/pages/conversations/index.vue
T
lxy 71fdaac1b4 优化: df-miniapp 跨端 AI Chat 更新(会话前基线收尾)
- ws.ts 心跳 pong 闭环 + useAiChat 看门狗治半连接挂死

- pages 新增 settings/ + chat/conversations 更新 + types events/relay + mdRenderer + tsconfig
2026-08-05 22:14:38 +08:00

233 lines
5.9 KiB
Vue

<script setup lang="ts">
import { ref, watch } from 'vue'
import { onPullDownRefresh } from '@dcloudio/uni-app'
import { useAiChat } from '@/composables/useAiChat'
import type { Conversation } from '@/types/events'
const ai = useAiChat()
/**
* mp-weixin 模板对嵌套在对象中的 reactive/ref 响应性追踪有问题。
* 用本地 ref + watch 同步,确保模板直接拿到响应式数组。
*/
const conversations = ref<Conversation[]>([])
/**
* 监听 ai.conversations.length 变化(比 deep watch 更可靠)。
* 用 `[...ai.conversations]` 创建普通数组赋值给 ref,确保每次变化都触发模板更新。
*/
watch(() => ai.conversations.value.length, () => {
conversations.value = [...ai.conversations.value]
}, { immediate: true })
/** 点击会话项切换 */
function handleSelect(convId: string): void {
ai.switchConversation(convId)
// 切换后跳回对话页
uni.switchTab({ url: '/pages/chat/index' })
}
/** 新建会话 */
function handleNew(): void {
ai.newConversation()
uni.switchTab({ url: '/pages/chat/index' })
}
/** 长按会话项重命名(对齐桌面端改会话名) */
function handleRename(conv: Conversation): void {
uni.showModal({
title: '重命名会话',
editable: true,
placeholderText: '输入新名称',
content: conv.title || '',
success: (r) => {
if (r.confirm && r.content) {
const newTitle = r.content.trim()
if (newTitle && newTitle !== conv.title) {
ai.renameConversation(conv.id, newTitle)
}
}
},
})
}
/** 下拉刷新:发 list_conversations 同步 device 会话列表(F-#95)。
* 命令异步回流经 AiConversationList 事件替换 ai.conversations(handleEvent),
* 此处仅发命令 + 收尾停止下拉动画(列表更新非同步返回,故立即停止动画即可)。 */
onPullDownRefresh(() => {
ai.refreshConversations()
uni.stopPullDownRefresh()
})
/** 格式化时间戳为可读字符串 */
function formatTime(ts?: number): string {
if (!ts) return ''
const d = new Date(ts)
const mm = String(d.getMonth() + 1).padStart(2, '0')
const dd = String(d.getDate()).padStart(2, '0')
const hh = String(d.getHours()).padStart(2, '0')
const mi = String(d.getMinutes()).padStart(2, '0')
return `${mm}-${dd} ${hh}:${mi}`
}
</script>
<template>
<view class="conv-page">
<!-- 顶部新建按钮 -->
<view class="new-bar" @click="handleNew">
<text class="new-icon">+</text>
<text class="new-text">新建会话</text>
</view>
<!-- 会话列表 -->
<scroll-view class="conv-list" scroll-y>
<!-- 加载中骨架屏 -->
<view v-if="conversations.length === 0 && ai.loading.value" class="conv-skeleton">
<view v-for="i in 5" :key="i" class="conv-sk-item">
<view class="conv-sk-line conv-sk-w70"></view>
<view class="conv-sk-line conv-sk-w40"></view>
</view>
</view>
<view v-else-if="conversations.length === 0" class="empty-hint">
<text>暂无历史会话</text>
<text class="empty-sub">在桌面端创建后,此处同步</text>
</view>
<view
v-for="conv in conversations"
:key="conv.id"
class="conv-item"
:class="{ active: conv.id === ai.activeConversationId.value }"
@click="handleSelect(conv.id)"
@longpress="handleRename(conv)"
>
<view class="conv-main">
<text class="conv-title">{{ conv.title || '(未命名会话)' }}</text>
<text v-if="conv.lastMessage" class="conv-last">{{ conv.lastMessage }}</text>
</view>
<text v-if="conv.updatedAt" class="conv-time">{{ formatTime(conv.updatedAt) }}</text>
</view>
<view v-if="conversations.length > 0" class="list-hint">
<text>长按会话可重命名</text>
</view>
</scroll-view>
</view>
</template>
<style lang="scss" scoped>
.conv-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: $uni-bg-color;
}
.new-bar {
display: flex;
align-items: center;
padding: 24rpx;
background-color: $uni-bg-color-dark;
border-bottom: 1rpx solid $uni-border-color;
}
.new-icon {
font-size: 40rpx;
color: $uni-color-primary;
margin-right: 16rpx;
}
.new-text {
font-size: $uni-font-size-base;
color: $uni-color-primary;
}
.conv-list {
flex: 1;
}
.empty-hint {
display: flex;
flex-direction: column;
align-items: center;
padding: 200rpx 24rpx;
text {
color: $uni-text-color-disable;
font-size: $uni-font-size-base;
}
.empty-sub {
margin-top: 12rpx;
font-size: $uni-font-size-sm;
}
}
.conv-item {
display: flex;
align-items: center;
padding: 24rpx;
border-bottom: 1rpx solid $uni-border-color;
&.active {
background-color: $uni-bg-color-light;
border-left: 6rpx solid $uni-color-primary;
}
}
.conv-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.conv-title {
font-size: $uni-font-size-base;
color: $uni-text-color;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conv-last {
margin-top: 8rpx;
font-size: $uni-font-size-sm;
color: $uni-text-color-grey;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conv-time {
font-size: $uni-font-size-sm;
color: $uni-text-color-disable;
margin-left: 16rpx;
}
.list-hint {
padding: 24rpx;
text-align: center;
}
.list-hint text {
color: $uni-text-color-disable;
font-size: $uni-font-size-sm;
}
/* 骨架屏(下拉刷新/冷启动占位) */
.conv-skeleton {
padding: 24rpx;
}
.conv-sk-item {
margin-bottom: 32rpx;
padding: 24rpx;
}
.conv-sk-line {
height: 24rpx;
margin: 8rpx 0;
background: linear-gradient(90deg, $uni-bg-color-light 25%, #3a3a3a 50%, $uni-bg-color-light 75%);
background-size: 200% 100%;
animation: conv-sk-shimmer 1.5s infinite;
border-radius: 8rpx;
}
.conv-sk-w70 { width: 70%; }
.conv-sk-w40 { width: 40%; }
@keyframes conv-sk-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>