420 lines
9.3 KiB
Vue
420 lines
9.3 KiB
Vue
<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>
|