378 lines
9.0 KiB
Vue
Raw Normal View History

2025-09-27 01:21:26 +08:00
<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>