You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

400 lines
8.7 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="chat-interface">
<div class="chat-header">
<h1>🧭 智能行程规划</h1>
<p>告诉我你的旅行想法我来为你规划完美行程</p>
</div>
<!-- Chat messages -->
<div class="chat-messages" ref="messagesContainer">
<div class="message bot">
<div class="avatar">🤖</div>
<div class="bubble">
你好我是你的行程规划助手请告诉我<br/>
想去哪里<br/>
几月出发<br/>
几天行程<br/>
交通方式自驾/公共交通/步行<br/>
有什么特殊需求带老人/小孩/特定景点
</div>
</div>
<div v-for="(msg, i) in messages" :key="i" class="message" :class="msg.role">
<div class="avatar">{{ msg.role === 'user' ? '👤' : '🤖' }}</div>
<div class="bubble">{{ msg.content }}</div>
</div>
<div v-if="isGenerating" class="message bot">
<div class="avatar">🤖</div>
<div class="bubble thinking">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
正在生成方案...
</div>
</div>
</div>
<!-- Input area -->
<div class="chat-input-area">
<input
v-model="inputText"
@keyup.enter="sendMessage"
placeholder="例如9月去云南5天自驾带老人"
/>
<button @click="sendMessage" :disabled="!inputText.trim() || isGenerating">发送</button>
</div>
<!-- Scheme cards (appear after AI response) -->
<transition name="slide-up">
<div v-if="showCards" class="scheme-cards">
<h3>📋 为你生成了 3 个方案</h3>
<div class="cards-grid">
<div
v-for="(scheme, i) in schemes"
:key="i"
class="scheme-card"
@click="selectScheme(i)"
>
<div class="card-header">
<span class="card-badge">{{ scheme.badge }}</span>
<h4>{{ scheme.name }}</h4>
</div>
<div class="card-stats">
<span>🚗 {{ scheme.km }}</span>
<span>⏱️ {{ scheme.driveTime }}</span>
<span>💰 {{ scheme.budget }}</span>
</div>
<div class="card-highlights">
<span v-for="(h, j) in scheme.highlights" :key="j" class="highlight-tag">{{ h }}</span>
</div>
<div class="card-mini-map">
<span>🗺️ {{ routePreview(i) }}</span>
</div>
<div class="card-action"> </div>
</div>
</div>
</div>
</transition>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
import { useItineraryStore } from '../stores/itinerary'
const store = useItineraryStore()
const messages = ref([])
const inputText = ref('')
const isGenerating = ref(false)
const showCards = ref(false)
const schemes = ref([
{
badge: '方案A',
name: '经典环线:昆明-普者黑-大理-丽江',
km: '~1450km',
driveTime: '14h',
budget: '¥4000',
highlights: ['喀斯特地貌', '苍山洱海', '古城文化', '玉龙雪山'],
route: '昆明 → 普者黑 → 坝美 → 大理 → 丽江'
},
{
badge: '方案B',
name: '南线深度:昆明-建水-元阳-普者黑',
km: '~980km',
driveTime: '11h',
budget: '¥3200',
highlights: ['元阳梯田', '建水古城', '抚仙湖', '少打卡多体验'],
route: '昆明 → 抚仙湖 → 建水 → 元阳 → 普者黑'
},
{
badge: '方案C',
name: '西线探险:昆明-大理-沙溪-香格里拉',
km: '~1200km',
driveTime: '16h',
budget: '¥4800',
highlights: ['茶马古道', '高原秘境', '松赞林寺', '纳帕海'],
route: '昆明 → 大理 → 沙溪 → 香格里拉 → 返程'
}
])
const routePreview = (i) => schemes.value[i].route
const messagesContainer = ref(null)
const scrollToBottom = async () => {
await nextTick()
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
}
const sendMessage = async () => {
if (!inputText.value.trim() || isGenerating.value) return
const userMsg = inputText.value.trim()
messages.value.push({ role: 'user', content: userMsg })
inputText.value = ''
await scrollToBottom()
isGenerating.value = true
// Simulate AI response
setTimeout(async () => {
isGenerating.value = false
messages.value.push({
role: 'bot',
content: `根据你的需求"${userMsg}"我为你精心设计了以下3个方案。每个方案在路线、体验和预算上有所侧重请点击方案卡片进入对比页面。`
})
showCards.value = true
await scrollToBottom()
}, 2000)
}
const selectScheme = (index) => {
store.setPhase('comparison')
}
</script>
<style scoped>
.chat-interface {
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f5f5;
position: relative;
}
.chat-header {
text-align: center;
padding: 30px 20px;
background: linear-gradient(135deg, #1b4332, #2d6a4f);
color: #fff;
}
.chat-header h1 {
margin: 0 0 8px;
font-size: 24px;
}
.chat-header p {
margin: 0;
opacity: 0.8;
font-size: 14px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
display: flex;
gap: 12px;
max-width: 80%;
}
.message.bot { align-self: flex-start; }
.message.user { align-self: flex-end; flex-direction: row-reverse; }
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
background: #e8f5e9;
flex-shrink: 0;
}
.message.user .avatar { background: #e3f2fd; }
.bubble {
padding: 12px 16px;
border-radius: 16px;
font-size: 14px;
line-height: 1.5;
}
.message.bot .bubble {
background: #fff;
color: #333;
border-bottom-left-radius: 4px;
}
.message.user .bubble {
background: #2a9d8f;
color: #fff;
border-bottom-right-radius: 4px;
}
.thinking {
display: flex;
align-items: center;
gap: 6px;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #2a9d8f;
animation: pulse 1.4s infinite;
}
.dot:nth-child(2) { animation-delay: 0.2s; }
.dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes pulse {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1); }
}
.chat-input-area {
display: flex;
gap: 8px;
padding: 16px 20px;
background: #fff;
border-top: 1px solid #eee;
}
.chat-input-area input {
flex: 1;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 24px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.chat-input-area input:focus { border-color: #2a9d8f; }
.chat-input-area button {
padding: 12px 24px;
background: #2a9d8f;
color: #fff;
border: none;
border-radius: 24px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.chat-input-area button:hover:not(:disabled) { background: #21867a; }
.chat-input-area button:disabled { opacity: 0.5; cursor: not-allowed; }
/* Scheme cards */
.scheme-cards {
position: absolute;
bottom: 80px;
left: 20px;
right: 20px;
background: #fff;
border-radius: 16px;
padding: 20px;
box-shadow: 0 -4px 20px rgba(0,0,0,0.1);
}
.scheme-cards h3 {
margin: 0 0 16px;
font-size: 16px;
color: #1b4332;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.scheme-card {
border: 2px solid #e0e0e0;
border-radius: 12px;
padding: 16px;
cursor: pointer;
transition: all 0.2s;
}
.scheme-card:hover {
border-color: #2a9d8f;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(42,157,143,0.2);
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.card-badge {
background: #f4a261;
color: #1b4332;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.card-header h4 {
margin: 0;
font-size: 14px;
color: #333;
}
.card-stats {
display: flex;
gap: 12px;
margin-bottom: 8px;
font-size: 12px;
color: #666;
}
.card-highlights {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 8px;
}
.highlight-tag {
background: #e8f5e9;
color: #2a9d8f;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
}
.card-mini-map {
font-size: 11px;
color: #888;
margin-bottom: 12px;
padding: 6px;
background: #f8f9fa;
border-radius: 6px;
}
.card-action {
text-align: center;
color: #2a9d8f;
font-weight: 600;
font-size: 13px;
}
.slide-up-enter-active, .slide-up-leave-active {
transition: all 0.3s ease;
}
.slide-up-enter-from, .slide-up-leave-to {
transform: translateY(20px);
opacity: 0;
}
</style>