Files
qiming/qiming-mobile/pages/index/conversation-list-content/conversation-list-content.uvue

503 lines
13 KiB
Plaintext

<template>
<view class="conversation-list h-full">
<scroll-view
class="h-full"
scroll-y
@scrolltolower="handleLoadMore"
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="handleRefresh"
:show-scrollbar="false"
>
<!-- 会话列表 -->
<template v-if="conversationList?.length">
<view class="list-wrapper">
<view
class="conversation-item"
v-for="item in conversationList"
:key="item.id"
@click="handleItemClick(item)"
@longpress="handleLongPress(item)"
>
<view class="item-content">
<view class="title-row">
<text class="conversation-title">{{
item.topic || t("Mobile.Conversation.unnamed")
}}</text>
<text class="time-label">{{
formatTimeAgo(item.modified)
}}</text>
</view>
<view class="bottom-row">
<text class="agent-name">{{
item.agent?.name || t("Mobile.Conversation.unknownAgent")
}}</text>
<view
v-if="item.taskStatus === TaskStatus.EXECUTING"
class="status-tag"
>
<text class="status-text">{{
t("Mobile.Conversation.executing")
}}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<!-- loading状态或刷新状态时隐藏空状态 -->
<template v-else-if="!loading && !refreshing">
<view class="empty-state h-full">
<image
:src="noData"
class="empty-image"
:alt="t('Mobile.Common.noDataImageAlt')"
/>
<text class="empty-text">{{ t("Mobile.Common.noData") }}</text>
</view>
</template>
<!-- 刷新状态时隐藏加载状态 -->
<template v-else-if="!refreshing">
<view class="loading-state">
<text class="loading-text">{{ t("Mobile.Page.loadingMore") }}</text>
</view>
</template>
<!-- 加载状态时隐藏没有更多数据状态 -->
<template v-if="showNoMore">
<view class="no-more-state">
<text class="no-more-text">{{ t("Mobile.Common.noMoreData") }}</text>
</view>
</template>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import {
apiAgentConversationList,
apiAgentConversationUpdate,
apiAgentConversationDelete,
} from "@/servers/conversation";
import type {
ConversationListParams,
ConversationInfo,
} from "@/types/interfaces/conversationInfo";
import type { AgentConversationUpdateParams } from "@/types/interfaces/agent";
import { SUCCESS_CODE } from "@/constants/codes.constants.uts";
import { formatTimeAgo } from "@/utils/common";
import noData from "@/static/assets/no_data.png";
import { TaskStatus } from "@/types/enums/agent";
import { useI18n } from "@/utils/i18n";
const { t } = useI18n();
// Props 定义
interface Props {
isLoggedIn?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
isLoggedIn: false,
});
// Emits 定义
const emit = defineEmits<{
(e: "conversation-click", item: ConversationInfo): void;
}>();
// 会话列表
const conversationList = ref<ConversationInfo[]>([]);
// 加载状态
const loading = ref<boolean>(false);
// 刷新状态
const refreshing = ref<boolean>(false);
// 是否还有更多
const hasMore = ref<boolean>(false);
// 最后一条记录ID
const lastId = ref<number | null>(null);
// 是否显示没有更多数据
const showNoMore = computed(() => {
return (
!loading.value && !hasMore.value && conversationList.value?.length > 8
);
});
// 获取会话列表
const fetchConversationList = async (isLoadMore: boolean = false) => {
if (!props.isLoggedIn) {
loading.value = false;
refreshing.value = false;
return;
}
try {
const params: ConversationListParams = {
agentId: null,
lastId: isLoadMore ? lastId.value : null,
limit: 20,
};
const res = await apiAgentConversationList(params);
loading.value = false;
const { code, data } = res || {};
if (code === SUCCESS_CODE) {
const newList = (data || []) as ConversationInfo[];
if (isLoadMore) {
// 追加数据
conversationList.value = [...conversationList.value, ...newList];
} else {
// 替换数据
conversationList.value = newList;
}
// 更新lastId和hasMore
if (data && data.length > 0) {
lastId.value = data[data.length - 1].id;
hasMore.value = newList.length >= 20;
} else {
hasMore.value = false;
}
return data;
}
} catch (error) {
loading.value = false;
return false;
}
};
// 下拉刷新
const handleRefresh = async () => {
if (refreshing.value) {
return;
}
refreshing.value = true;
lastId.value = null;
hasMore.value = true;
await fetchConversationList(false);
setTimeout(() => {
refreshing.value = false;
}, 500);
};
// 加载更多
const handleLoadMore = () => {
if (hasMore.value && !loading.value) {
loading.value = true;
fetchConversationList(true);
}
};
// 点击会话项
const handleItemClick = (item: ConversationInfo) => {
emit("conversation-click", item);
};
/**
* 长按会话项
*/
const handleLongPress = (item: ConversationInfo) => {
const options = [
t("Mobile.Components.HistoryConversationList.rename"),
t("Mobile.Components.HistoryConversationList.delete"),
];
uni.showActionSheet({
itemList: options,
cancelText: t("Mobile.Common.cancel"),
success: (res) => {
if (res.tapIndex === 0) {
handleRename(item);
} else if (res.tapIndex === 1) {
handleDelete(item);
}
},
});
};
/**
* 修改名称
*/
const handleRename = (item: ConversationInfo) => {
uni.showModal({
title: t("Mobile.Components.HistoryConversationList.renameModalTitle"),
editable: true,
placeholderText: t(
"Mobile.Components.HistoryConversationList.renamePlaceholder",
),
content: item.topic || "",
cancelText: t("Mobile.Common.cancel"),
confirmText: t("Mobile.Common.confirm"),
success: async (res) => {
if (res.confirm) {
const newName = res.content?.trim();
if (!newName) {
uni.showToast({
title: t(
"Mobile.Components.HistoryConversationList.renameTitleEmpty",
),
icon: "none",
});
return;
}
if (newName.length > 50) {
uni.showToast({
title: t(
"Mobile.Components.HistoryConversationList.renameTitleTooLong",
),
icon: "none",
});
return;
}
try {
const params: AgentConversationUpdateParams = {
id: item.id,
topic: newName,
};
const updateRes = await apiAgentConversationUpdate(params);
if (updateRes.code === SUCCESS_CODE) {
uni.showToast({
title: t(
"Mobile.Components.HistoryConversationList.renameSuccess",
),
icon: "success",
});
// 刷新当前项显示
item.topic = newName;
} else {
uni.showToast({
title: updateRes.message || t("Mobile.Common.operationFailed"),
icon: "none",
});
}
} catch (error) {
uni.showToast({
title: t("Mobile.Common.operationFailed"),
icon: "none",
});
}
}
},
});
};
/**
* 删除会话
*/
const handleDelete = (item: ConversationInfo) => {
uni.showModal({
title: t("Mobile.Components.HistoryConversationList.deleteModalTitle"),
content: t(
"Mobile.Components.HistoryConversationList.deleteModalContent",
),
cancelText: t("Mobile.Common.cancel"),
confirmText: t("Mobile.Common.confirm"),
confirmColor: "#ff4d4f",
success: async (res) => {
if (res.confirm) {
try {
const deleteRes = await apiAgentConversationDelete(item.id);
if (deleteRes.code === SUCCESS_CODE) {
uni.showToast({
title: t(
"Mobile.Components.HistoryConversationList.deleteSuccess",
),
icon: "success",
});
// 从列表中移除
conversationList.value = conversationList.value.filter(
(c) => c.id !== item.id,
);
} else {
uni.showToast({
title: deleteRes.message || t("Mobile.Common.operationFailed"),
icon: "none",
});
}
} catch (error) {
uni.showToast({
title: t("Mobile.Common.operationFailed"),
icon: "none",
});
}
}
},
});
};
// 初始化加载数据
const loadData = async () => {
loading.value = true;
await fetchConversationList(false);
};
// 暴露方法供父组件调用
defineExpose({
loadData,
conversationList,
});
</script>
<style lang="scss" scoped>
.conversation-list {
background: #f5f5f5;
.list-wrapper {
padding: 0;
.conversation-item {
background: #fff;
border-bottom: 2rpx solid #f0f0f0;
padding: 32rpx 24rpx;
transition: background-color 0.2s;
&:active {
background-color: #f8f8f8;
}
.item-content {
.title-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
.conversation-title {
flex: 1;
font-size: 30rpx;
font-weight: 600;
color: #333;
line-height: 42rpx;
margin-right: 16rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.time-label {
flex-shrink: 0;
font-size: 24rpx;
color: #999;
line-height: 42rpx;
}
}
.bottom-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.agent-name {
flex: 1;
font-size: 26rpx;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-tag {
flex-shrink: 0;
// background: #e6f7ff;
// border: 1rpx solid #F0F0F0;
// border-radius: 4rpx;
padding: 4rpx 0rpx;
margin-left: 16rpx;
.status-text {
font-size: 22rpx;
color: rgba(0, 0, 0, 0.45);
line-height: 28rpx;
}
}
}
}
}
}
}
.h-full {
height: 100%;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.empty-image {
width: 170rpx;
height: 170rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 32rpx;
}
}
/* 加载状态 */
.loading-state {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.loading-text {
font-size: 28rpx;
color: #666;
display: flex;
align-items: center;
flex-direction: row;
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
&::before {
content: "";
width: 32rpx;
height: 32rpx;
border: 4rpx solid #e8e8e8;
border-top: 4rpx solid #8b5cf6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 16rpx;
}
}
}
/* 没有更多数据状态 */
.no-more-state {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
.no-more-text {
font-size: 24rpx;
color: #999;
}
}
</style>