2025-09-27 01:21:47 +08:00

378 lines
9.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="ai-app-chat">
<div class="chat-container">
<div class="chat-header">
<div class="app-info">
<n-avatar
:size="32"
:src="appInfo?.icon ? getAppIcon(appInfo.icon) : '/default-app-icon.png'"
round
/>
<div class="app-details">
<h3 class="app-name">{{ appInfo?.name || 'AI应用' }}</h3>
<p class="app-desc">{{ appInfo?.descr || '智能对话助手' }}</p>
</div>
</div>
<n-button @click="handleBack" quaternary>
<template #icon>
<n-icon><ArrowLeftOutlined /></n-icon>
</template>
返回
</n-button>
</div>
<div class="chat-content" ref="chatContentRef">
<div class="message-list">
<!-- 欢迎消息 -->
<div v-if="messages.length === 0 && appInfo?.prologue" class="message bot-message">
<n-avatar size="small" src="/ai-avatar.png" round />
<div class="message-content">
<div class="message-text">{{ appInfo.prologue }}</div>
</div>
</div>
<!-- 消息列表 -->
<div
v-for="(message, index) in messages"
:key="index"
class="message"
:class="message.type === 'user' ? 'user-message' : 'bot-message'"
>
<n-avatar
size="small"
:src="message.type === 'user' ? '/user-avatar.png' : '/ai-avatar.png'"
round
/>
<div class="message-content">
<div class="message-text" v-html="formatMessage(message.content)"></div>
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
</div>
</div>
<!-- 加载中 -->
<div v-if="isLoading" class="message bot-message">
<n-avatar size="small" src="/ai-avatar.png" round />
<div class="message-content">
<div class="message-text">
<n-spin size="small" />
<span style="margin-left: 8px">正在思考中...</span>
</div>
</div>
</div>
</div>
</div>
<div class="chat-input">
<!-- 预设问题 -->
<div v-if="presetQuestions.length > 0 && messages.length === 0" class="preset-questions">
<n-space>
<n-button
v-for="question in presetQuestions"
:key="question"
size="small"
@click="handlePresetQuestion(question)"
>
{{ question }}
</n-button>
</n-space>
</div>
<div class="input-area">
<n-input
v-model:value="inputMessage"
type="textarea"
placeholder="请输入您的问题..."
:rows="3"
:maxlength="1000"
show-count
@keydown.enter.prevent="handleSend"
/>
<n-button
type="primary"
:disabled="!inputMessage.trim() || isLoading"
:loading="isLoading"
@click="handleSend"
>
发送
</n-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
import { ArrowLeftOutlined } from '@vicons/antd'
import { aiAppApi } from './aiApp'
import type { AiApp } from './type/aiApp'
interface ChatMessage {
type: 'user' | 'bot'
content: string
timestamp: number
}
const route = useRoute()
const router = useRouter()
const message = useMessage()
const appId = route.params.id as string
const appInfo = ref<AiApp | null>(null)
const messages = ref<ChatMessage[]>([])
const inputMessage = ref('')
const isLoading = ref(false)
const chatContentRef = ref<HTMLElement>()
const presetQuestions = ref<string[]>([])
// 获取应用信息
const loadAppInfo = async () => {
try {
const response = await aiAppApi.getAppById({ id: appId })
if (response.success) {
appInfo.value = response.result
// 解析预设问题
if (appInfo.value.presetQuestion) {
presetQuestions.value = appInfo.value.presetQuestion.split('\n').filter(q => q.trim())
}
}
} catch (error) {
message.error('加载应用信息失败')
console.error('Load app info error:', error)
}
}
// 获取应用图标
const getAppIcon = (icon: string) => {
return icon ? `/api/sys/common/static/${icon}` : '/default-app-icon.png'
}
// 发送消息
const handleSend = async () => {
if (!inputMessage.value.trim() || isLoading.value) return
const userMessage: ChatMessage = {
type: 'user',
content: inputMessage.value.trim(),
timestamp: Date.now()
}
messages.value.push(userMessage)
const currentInput = inputMessage.value.trim()
inputMessage.value = ''
isLoading.value = true
// 滚动到底部
await nextTick()
scrollToBottom()
try {
// 这里应该调用实际的AI对话接口
// 暂时使用模拟响应
await new Promise(resolve => setTimeout(resolve, 1000))
const botMessage: ChatMessage = {
type: 'bot',
content: `您好!我收到了您的问题:"${currentInput}"。这是一个模拟回复实际的AI对话功能需要连接到具体的AI服务。`,
timestamp: Date.now()
}
messages.value.push(botMessage)
await nextTick()
scrollToBottom()
} catch (error) {
message.error('发送消息失败')
console.error('Send message error:', error)
} finally {
isLoading.value = false
}
}
// 处理预设问题
const handlePresetQuestion = (question: string) => {
inputMessage.value = question
handleSend()
}
// 返回
const handleBack = () => {
router.back()
}
// 格式化消息
const formatMessage = (content: string) => {
return content.replace(/\n/g, '<br>')
}
// 格式化时间
const formatTime = (timestamp: number) => {
const date = new Date(timestamp)
return date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
}
// 滚动到底部
const scrollToBottom = () => {
if (chatContentRef.value) {
chatContentRef.value.scrollTop = chatContentRef.value.scrollHeight
}
}
onMounted(() => {
loadAppInfo()
})
</script>
<style scoped lang="scss">
.ai-app-chat {
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f5f5;
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
max-width: 800px;
margin: 0 auto;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
background: white;
.app-info {
display: flex;
align-items: center;
gap: 12px;
.app-details {
.app-name {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.app-desc {
margin: 4px 0 0 0;
font-size: 12px;
color: #666;
}
}
}
}
.chat-content {
flex: 1;
overflow-y: auto;
padding: 16px 24px;
.message-list {
display: flex;
flex-direction: column;
gap: 16px;
.message {
display: flex;
gap: 12px;
&.user-message {
flex-direction: row-reverse;
.message-content {
background: #1890ff;
color: white;
border-radius: 18px 18px 4px 18px;
}
}
&.bot-message {
.message-content {
background: #f6f6f6;
color: #333;
border-radius: 18px 18px 18px 4px;
}
}
.message-content {
max-width: 70%;
padding: 12px 16px;
.message-text {
line-height: 1.5;
word-break: break-word;
}
.message-time {
font-size: 11px;
opacity: 0.7;
margin-top: 4px;
}
}
}
}
}
.chat-input {
padding: 16px 24px;
border-top: 1px solid #f0f0f0;
background: white;
.preset-questions {
margin-bottom: 12px;
}
.input-area {
display: flex;
gap: 12px;
align-items: flex-end;
.n-input {
flex: 1;
}
}
}
}
}
// 响应式设计
@media (max-width: 768px) {
.ai-app-chat {
.chat-container {
margin: 0;
border-radius: 0;
box-shadow: none;
.chat-header,
.chat-content,
.chat-input {
padding-left: 16px;
padding-right: 16px;
}
.chat-content {
.message-list {
.message {
.message-content {
max-width: 85%;
}
}
}
}
}
}
}
</style>