themblem/web/src/views/ai-chat.vue
2025-10-29 21:27:29 +00:00

420 lines
9.3 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-chat-container">
<div class="chat-header">
<div class="header-content">
<div>
<h4>徵象 AI 客服</h4>
<p>智能防伪验证助手</p>
</div>
<div class="mode-selector">
<select v-model="selectedMode" @change="onModeChange" class="form-control">
<option value="platform">平台客服</option>
<option
v-for="product in products"
:key="`product-${product.id}`"
:value="`product-${product.id}`"
>
产品客服: {{ product.name }}
</option>
</select>
</div>
</div>
</div>
<div class="chat-messages" ref="chatMessages">
<div
v-for="message in messages"
:key="message.id"
:class="['message', message.role]"
>
<div class="message-content">
<div class="message-text">{{ message.content }}</div>
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
</div>
</div>
<div v-if="isLoading" class="message assistant">
<div class="message-content">
<div class="message-text typing">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
<div class="chat-input">
<div class="input-group">
<input
v-model="currentMessage"
@keyup.enter="sendMessage"
:disabled="isLoading"
placeholder="输入您的问题..."
class="form-control"
/>
<button
@click="sendMessage"
:disabled="!currentMessage.trim() || isLoading"
class="btn btn-primary send-btn"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</button>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'AIChat',
data() {
return {
messages: [],
currentMessage: '',
isLoading: false,
sessionId: null,
messageId: 0,
selectedMode: 'platform',
products: []
}
},
mounted() {
this.initializeChat()
this.fetchProducts()
},
methods: {
initializeChat() {
// Generate a new session ID
this.sessionId = this.generateSessionId()
// Add welcome message
this.addMessage('assistant', '欢迎使用徵象AI客服我是您的智能防伪验证助手可以帮您解答关于产品验证、防伪技术等问题。请随时向我提问')
},
generateSessionId() {
return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
},
async sendMessage() {
if (!this.currentMessage.trim() || this.isLoading) return
const userMessage = this.currentMessage.trim()
this.currentMessage = ''
// Add user message
this.addMessage('user', userMessage)
// Show loading state
this.isLoading = true
try {
const requestData = {
message: userMessage,
session_id: this.sessionId,
chat_type: this.isProductMode ? 'product' : 'platform'
}
if (this.isProductMode) {
requestData.product_id = this.currentProductId
}
const response = await axios.post('/api/v1/ai-chat/', requestData)
// Add AI response
this.addMessage('assistant', response.data.response)
// Update session ID if provided
if (response.data.session_id) {
this.sessionId = response.data.session_id
}
} catch (error) {
console.error('AI Chat Error:', error)
this.addMessage('assistant', '抱歉,我遇到了一些技术问题。请稍后再试,或联系技术支持。')
} finally {
this.isLoading = false
this.scrollToBottom()
}
},
addMessage(role, content) {
this.messages.push({
id: ++this.messageId,
role: role,
content: content,
timestamp: new Date()
})
this.scrollToBottom()
},
scrollToBottom() {
this.$nextTick(() => {
const chatMessages = this.$refs.chatMessages
if (chatMessages) {
chatMessages.scrollTop = chatMessages.scrollHeight
}
})
},
formatTime(timestamp) {
return timestamp.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
},
async fetchProducts() {
try {
const response = await axios.get('/api/v1/products/')
this.products = response.data.items || []
} catch (error) {
console.error('Failed to fetch products:', error)
// Fallback to mock data for testing
this.products = [
{ id: 1, name: '测试产品 A' },
{ id: 2, name: '测试产品 B' },
{ id: 3, name: '测试产品 C' }
]
}
},
onModeChange() {
// Reset session when switching modes
this.sessionId = this.generateSessionId()
this.messages = []
if (this.selectedMode === 'platform') {
this.addMessage('assistant', '欢迎使用徵象AI客服我是您的智能防伪验证助手可以帮您解答关于产品验证、防伪技术等问题。请随时向我提问')
} else {
const selectedProduct = this.products.find(p => p.id === this.currentProductId)
if (selectedProduct) {
this.addMessage('assistant', `欢迎使用${selectedProduct.name}的专属客服!我可以帮您解答关于此产品的问题。请随时向我提问!`)
}
}
}
},
computed: {
isProductMode() {
return this.selectedMode !== 'platform'
},
currentProductId() {
if (!this.isProductMode) return null
const parts = this.selectedMode.split('-')
return parts.length > 1 ? parseInt(parts[1]) : null
}
}
}
</script>
<style scoped>
.ai-chat-container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
background: #fff;
}
.chat-header {
padding: 15px 20px;
border-bottom: 1px solid #e9ecef;
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%);
color: white;
flex-shrink: 0;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
}
.chat-header h4 {
margin: 0 0 5px 0;
font-weight: 600;
}
.chat-header p {
margin: 0;
color: #ffffff;
}
.mode-selector .form-control {
min-width: 200px;
border-radius: 8px;
padding: 6px 12px;
font-size: 14px;
border: none;
background: rgba(255, 255, 255, 0.95);
color: #333;
}
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background: #f8f9fa;
}
.message {
margin-bottom: 15px;
display: flex;
}
.message.user {
justify-content: flex-end;
}
.message.assistant {
justify-content: flex-start;
}
.message-content {
max-width: 70%;
padding: 12px 16px;
border-radius: 18px;
position: relative;
}
.message.user .message-content {
background: #007bff;
color: white;
border-bottom-right-radius: 4px;
}
.message.assistant .message-content {
background: white;
color: #333;
border: 1px solid #e9ecef;
border-bottom-left-radius: 4px;
}
.message-text {
margin-bottom: 4px;
line-height: 1.4;
word-wrap: break-word;
}
.message-time {
font-size: 0.75rem;
opacity: 0.7;
}
.message.user .message-time {
text-align: right;
}
.typing {
display: flex;
align-items: center;
gap: 4px;
}
.typing span {
width: 8px;
height: 8px;
border-radius: 50%;
background: #999;
animation: typing 1.4s infinite ease-in-out;
}
.typing span:nth-child(1) { animation-delay: -0.32s; }
.typing span:nth-child(2) { animation-delay: -0.16s; }
.typing span:nth-child(3) { animation-delay: 0s; }
@keyframes typing {
0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; }
40% { transform: scale(1); opacity: 1; }
}
.chat-input {
padding: 15px 20px;
border-top: 1px solid #e9ecef;
background: white;
flex-shrink: 0;
}
.input-group {
display: flex;
gap: 10px;
}
.input-group .form-control {
flex: 1;
border: 1px solid #ced4da;
border-radius: 25px;
padding: 12px 20px;
font-size: 14px;
}
.input-group .form-control:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.input-group .btn {
border-radius: 25px;
min-width: 80px;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: #4a90e2;
color: white;
transition: all 0.2s;
padding: 0 20px;
}
.input-group .btn svg {
display: block;
}
.input-group .btn:hover:not(:disabled) {
background: #357abd;
transform: scale(1.05);
}
.input-group .btn:disabled {
background: #ccc;
cursor: not-allowed;
opacity: 0.6;
}
/* Responsive design */
@media (max-width: 768px) {
.message-content {
max-width: 85%;
}
.chat-header {
padding: 12px 15px;
}
.header-content {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.mode-selector .form-control {
width: 100%;
}
.chat-messages {
padding: 15px;
}
.chat-input {
padding: 12px 15px;
}
}
</style>