Files
gao-customer-h5/src/pages/ai/components/AiChatPanel.vue
T
2026-09-16 21:32:03 +08:00

187 lines
4.4 KiB
Vue

<template>
<section class="ai-chat-panel" :aria-label="`${props.storeName || '当前门店'}经营助手`">
<ai-chat-message-list
ref="messageList"
:messages="messages"
:is-generating="isGenerating"
:quick-question-list="aiQuickQuestionList"
@select-question="selectQuestion"
/>
<div class="bottom-area">
<div v-if="activeToolStatus" class="tool-status">
<t-icon name="loading" size="1rem" />
<span v-text="activeToolStatus" />
</div>
<ai-chat-composer
v-model="draft"
:is-generating="isGenerating"
:disabled="isLoadingConversation"
:last-error="lastError"
:can-retry="canRetry"
@send="submitMessage"
@stop="stopGeneration"
@retry="retryLastMessage"
/>
</div>
</section>
<ai-conversation-list
v-model:visible="conversationVisible"
:active-conversation-id="conversationId"
:disabled="isBusy"
:session-key="props.sessionKey"
@select="selectConversation"
@delete="handleConversationDelete"
/>
</template>
<script setup lang="ts">
import { useAiChat } from "@/composables/useAiChat";
import type { GaoAiConversation } from "@/api/GaoAiAPI";
import AiChatComposer from "@/pages/ai/components/AiChatComposer.vue";
import AiConversationList from "@/pages/ai/components/AiConversationList.vue";
import AiChatMessageList from "@/pages/ai/components/AiChatMessageList.vue";
import { aiQuickQuestionList } from "@/pages/ai/aiChatQuestions";
import { useNavBarStore } from "@/store/navBarStore";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
defineOptions({
name: "AiChatPanel"
});
interface Props {
storeName: string;
sessionKey: string;
storeId?: string;
}
const props = defineProps<Props>();
const navBarStore = useNavBarStore();
type MessageListExpose = {
scrollToBottom: () => Promise<void>;
};
const messageListRef = useTemplateRef<MessageListExpose>("messageList");
const draft = shallowRef("");
const conversationVisible = ref(false);
const {
messages,
conversationId,
isGenerating,
isLoadingConversation,
activeToolStatus,
lastError,
canRetry,
sendMessage,
loadConversation,
stopGeneration,
retryLastMessage,
resetConversation
} = useAiChat({
sessionKey: () => props.sessionKey,
storeId: () => props.storeId,
scrollToBottom
});
const isBusy = computed(() => isGenerating.value || isLoadingConversation.value);
async function submitMessage(message: string): Promise<void> {
await sendMessage(message);
}
function openConversationList(): void {
if (!isBusy.value) {
conversationVisible.value = true;
}
}
async function selectConversation(conversation: GaoAiConversation): Promise<void> {
if (isBusy.value || !conversation.id) {
return;
}
try {
await loadConversation(conversation.id);
} catch (error) {
Message.error(getErrorMessage(error, "历史会话读取失败"));
}
}
function handleConversationDelete(deletedConversationId: string): void {
if (deletedConversationId === conversationId.value) {
resetConversation();
}
}
function selectQuestion(question: string): void {
draft.value = question;
}
async function scrollToBottom(): Promise<void> {
await messageListRef.value?.scrollToBottom();
}
onMounted(() => {
navBarStore.clearRight();
navBarStore.setRightText("历史", "var(--td-brand-color)", openConversationList);
});
watch(isBusy, value => navBarStore.setRightDisabled(value));
onUnmounted(() => navBarStore.clearRight());
</script>
<style scoped lang="less">
.ai-chat-panel {
height: 100%;
display: flex;
position: relative;
overflow: hidden;
min-height: 0;
background: var(--ai-background);
padding-top: var(--app-nav-height);
flex-direction: column;
.bottom-area {
inset: auto 1rem calc(1rem + env(safe-area-inset-bottom, 0px));
width: auto;
margin: 0 auto;
z-index: 2;
position: absolute;
max-width: 40rem;
.tool-status {
gap: .375rem;
margin-bottom: .5rem;
color: #0f766e;
padding: .5rem .75rem;
border-radius: .75rem;
display: flex;
font-size: .75rem;
background: var(--ai-soft);
align-items: center;
.t-icon {
animation: rotate 1s linear infinite;
}
}
}
}
@keyframes rotate {
to {
transform: rotate(360deg);
}
}
:global(:root) {
--ai-background: rgba(255, 255, 255, .98);
--ai-bubble: #f5f6f8;
--ai-input: white;
--ai-soft: #f0fdfa;
}
:global(html.theme-dark) {
--ai-background: rgba(15, 23, 42, .98);
--ai-bubble: #1e293b;
--ai-input: #111827;
--ai-soft: rgba(20, 184, 166, .12);
}
</style>