|
|
|
|
@ -7,23 +7,79 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Input form -->
|
|
|
|
|
<div v-if="!isGenerating && allSchemes.length === 0" class="form-wrap">
|
|
|
|
|
<div class="form-wrap" :class="{ 'form-disabled': isGenerating }">
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<div class="fg"><label>目的地</label><input v-model="destination" placeholder="如:云南、贵州、四川" :disabled="isGenerating" value="四川"></div>
|
|
|
|
|
<div class="fg"><label>出行天数</label>
|
|
|
|
|
<select v-model="days" :disabled="isGenerating">
|
|
|
|
|
<option v-for="d in [2,3,5,7]" :key="d" :value="d">{{ d }}天</option>
|
|
|
|
|
</select>
|
|
|
|
|
<div class="combobox-wrap" ref="daysComboboxRef">
|
|
|
|
|
<input
|
|
|
|
|
v-model="daysInput"
|
|
|
|
|
@input="onDaysInput"
|
|
|
|
|
@focus="daysDropdownOpen = true"
|
|
|
|
|
@blur="onDaysBlur"
|
|
|
|
|
:disabled="isGenerating"
|
|
|
|
|
placeholder="选择或输入天数"
|
|
|
|
|
class="combobox-input"
|
|
|
|
|
>
|
|
|
|
|
<div v-if="daysDropdownOpen" class="combobox-dropdown">
|
|
|
|
|
<div
|
|
|
|
|
v-for="d in [2,3,5,7]"
|
|
|
|
|
:key="d"
|
|
|
|
|
class="combobox-option"
|
|
|
|
|
@mousedown="selectDay(d)"
|
|
|
|
|
>
|
|
|
|
|
{{ d }}天
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<div class="fg"><label>出发日期</label><input type="date" v-model="startDate" :disabled="isGenerating"></div>
|
|
|
|
|
<div class="fg"><label>出发月份</label>
|
|
|
|
|
<div class="combobox-wrap">
|
|
|
|
|
<input
|
|
|
|
|
v-model="monthDisplay"
|
|
|
|
|
@focus="monthDropdownOpen = true"
|
|
|
|
|
@blur="onMonthBlur"
|
|
|
|
|
:disabled="isGenerating"
|
|
|
|
|
placeholder="选择月份"
|
|
|
|
|
class="combobox-input"
|
|
|
|
|
readonly
|
|
|
|
|
>
|
|
|
|
|
<div v-if="monthDropdownOpen" class="combobox-dropdown">
|
|
|
|
|
<div
|
|
|
|
|
v-for="m in 12"
|
|
|
|
|
:key="m"
|
|
|
|
|
class="combobox-option"
|
|
|
|
|
@mousedown="selectMonth(m)"
|
|
|
|
|
>
|
|
|
|
|
{{ m }}月
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="fg"><label>出行方式</label>
|
|
|
|
|
<select v-model="transport" :disabled="isGenerating">
|
|
|
|
|
<option value="自驾">自驾</option>
|
|
|
|
|
<option value="公共交通">公共交通</option>
|
|
|
|
|
<option value="步行/骑行">步行/骑行</option>
|
|
|
|
|
</select>
|
|
|
|
|
<div class="combobox-wrap">
|
|
|
|
|
<input
|
|
|
|
|
v-model="transportDisplay"
|
|
|
|
|
@focus="transportDropdownOpen = true"
|
|
|
|
|
@blur="onTransportBlur"
|
|
|
|
|
:disabled="isGenerating"
|
|
|
|
|
placeholder="选择出行方式"
|
|
|
|
|
class="combobox-input"
|
|
|
|
|
readonly
|
|
|
|
|
>
|
|
|
|
|
<div v-if="transportDropdownOpen" class="combobox-dropdown">
|
|
|
|
|
<div
|
|
|
|
|
v-for="t in ['自驾', '公共交通', '步行/骑行']"
|
|
|
|
|
:key="t"
|
|
|
|
|
class="combobox-option"
|
|
|
|
|
@mousedown="selectTransport(t)"
|
|
|
|
|
>
|
|
|
|
|
{{ t }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="fg"><label>其他需求(选填)</label><input v-model="extraNeeds" placeholder="如:带老人小孩、预算范围、必去景点等" :disabled="isGenerating"></div>
|
|
|
|
|
@ -32,11 +88,11 @@
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- AI Thinking process -->
|
|
|
|
|
<!-- AI Thinking process (shown below form when generating) -->
|
|
|
|
|
<div v-if="isGenerating" class="load-panel">
|
|
|
|
|
<div class="lp-head" @click="thinkingExpanded = !thinkingExpanded">
|
|
|
|
|
<div class="lp-head">
|
|
|
|
|
<div class="spin"></div><span>AI 正在规划中...</span>
|
|
|
|
|
<span class="lp-toggle">{{ thinkingExpanded ? '收起' : '展开' }}</span>
|
|
|
|
|
<button class="btn-cancel" @click="cancelGeneration">✕ 取消</button>
|
|
|
|
|
</div>
|
|
|
|
|
<transition name="expand">
|
|
|
|
|
<div v-if="thinkingExpanded" class="lp-body">
|
|
|
|
|
@ -44,6 +100,9 @@
|
|
|
|
|
<div class="lp-step" v-else>分析用户需求中...</div>
|
|
|
|
|
</div>
|
|
|
|
|
</transition>
|
|
|
|
|
<div class="lp-toggle-area" @click="thinkingExpanded = !thinkingExpanded">
|
|
|
|
|
{{ thinkingExpanded ? '收起 ▲' : '展开 ▼' }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Scheme list with regenerate button -->
|
|
|
|
|
@ -175,12 +234,19 @@ const MAX_SCHEMES = 9
|
|
|
|
|
|
|
|
|
|
const destination = ref('')
|
|
|
|
|
const days = ref(3)
|
|
|
|
|
const startDate = ref('')
|
|
|
|
|
const daysInput = ref('3')
|
|
|
|
|
const daysDropdownOpen = ref(false)
|
|
|
|
|
const startMonth = ref(1)
|
|
|
|
|
const monthDisplay = ref('1月')
|
|
|
|
|
const monthDropdownOpen = ref(false)
|
|
|
|
|
const transport = ref('自驾')
|
|
|
|
|
const transportDisplay = ref('自驾')
|
|
|
|
|
const transportDropdownOpen = ref(false)
|
|
|
|
|
const extraNeeds = ref('')
|
|
|
|
|
const isGenerating = ref(false)
|
|
|
|
|
const thinkingText = ref('')
|
|
|
|
|
const thinkingExpanded = ref(true)
|
|
|
|
|
const abortController = ref(null)
|
|
|
|
|
|
|
|
|
|
// Preview modal
|
|
|
|
|
const previewIndex = ref(-1)
|
|
|
|
|
@ -200,25 +266,74 @@ function getBadgeClass(i) {
|
|
|
|
|
return classes[i % 3]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onDaysInput() {
|
|
|
|
|
const val = parseInt(daysInput.value)
|
|
|
|
|
if (!isNaN(val) && val >= 1 && val <= 30) {
|
|
|
|
|
days.value = val
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectDay(d) {
|
|
|
|
|
days.value = d
|
|
|
|
|
daysInput.value = String(d)
|
|
|
|
|
daysDropdownOpen.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onDaysBlur() {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
daysDropdownOpen.value = false
|
|
|
|
|
// 如果输入为空或无效,恢复为 days 的值
|
|
|
|
|
const val = parseInt(daysInput.value)
|
|
|
|
|
if (isNaN(val) || val < 1 || val > 30) {
|
|
|
|
|
daysInput.value = String(days.value)
|
|
|
|
|
}
|
|
|
|
|
}, 200)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectMonth(m) {
|
|
|
|
|
startMonth.value = m
|
|
|
|
|
monthDisplay.value = `${m}月`
|
|
|
|
|
monthDropdownOpen.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onMonthBlur() {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
monthDropdownOpen.value = false
|
|
|
|
|
}, 200)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectTransport(t) {
|
|
|
|
|
transport.value = t
|
|
|
|
|
transportDisplay.value = t
|
|
|
|
|
transportDropdownOpen.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onTransportBlur() {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
transportDropdownOpen.value = false
|
|
|
|
|
}, 200)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function generatePlan() {
|
|
|
|
|
if (!destination.value.trim() || isGenerating.value) return
|
|
|
|
|
|
|
|
|
|
isGenerating.value = true
|
|
|
|
|
thinkingText.value = ''
|
|
|
|
|
thinkingExpanded.value = true
|
|
|
|
|
abortController.value = new AbortController()
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const userInput = {
|
|
|
|
|
destination: destination.value.trim(),
|
|
|
|
|
days: days.value,
|
|
|
|
|
startDate: startDate.value || '近期',
|
|
|
|
|
startDate: `${startMonth.value}月`,
|
|
|
|
|
transport: transport.value,
|
|
|
|
|
extraNeeds: extraNeeds.value.trim() || '无特殊需求'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await quickPlan(userInput, (text) => {
|
|
|
|
|
thinkingText.value = text
|
|
|
|
|
})
|
|
|
|
|
}, abortController.value)
|
|
|
|
|
|
|
|
|
|
console.log('[generatePlan] AI 返回结果:', result)
|
|
|
|
|
console.log('[generatePlan] schemes 数量:', result.schemes?.length)
|
|
|
|
|
@ -252,10 +367,23 @@ async function generatePlan() {
|
|
|
|
|
console.log('[generatePlan] 保存方案总数:', currentSchemes.length)
|
|
|
|
|
store.saveSchemesToStore(currentSchemes, currentHistory)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
thinkingText.value = '生成失败:' + (error.message || '请重试')
|
|
|
|
|
console.error('[generatePlan] 生成失败:', error)
|
|
|
|
|
if (error.name === 'AbortError') {
|
|
|
|
|
console.log('[generatePlan] 用户取消生成')
|
|
|
|
|
thinkingText.value = '已取消生成'
|
|
|
|
|
} else {
|
|
|
|
|
thinkingText.value = '生成失败:' + (error.message || '请重试')
|
|
|
|
|
console.error('[generatePlan] 生成失败:', error)
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
isGenerating.value = false
|
|
|
|
|
abortController.value = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cancelGeneration() {
|
|
|
|
|
if (abortController.value) {
|
|
|
|
|
abortController.value.abort()
|
|
|
|
|
console.log('[cancelGeneration] 取消请求已发送')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -374,6 +502,58 @@ onMounted(() => {
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Days combobox */
|
|
|
|
|
.combobox-wrap {
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.combobox-input {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
border: 1.5px solid #dfe6e9;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
outline: none;
|
|
|
|
|
transition: border 0.2s;
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.combobox-input:focus {
|
|
|
|
|
border-color: #6c5ce7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.combobox-input:disabled {
|
|
|
|
|
background: #f5f7fa;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.combobox-dropdown {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 100%;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border: 1.5px solid #6c5ce7;
|
|
|
|
|
border-top: none;
|
|
|
|
|
border-radius: 0 0 10px 10px;
|
|
|
|
|
box-shadow: 0 4px 12px rgba(108,92,231,0.15);
|
|
|
|
|
z-index: 100;
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.combobox-option {
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #636e72;
|
|
|
|
|
transition: all 0.15s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.combobox-option:hover {
|
|
|
|
|
background: #f0f0f5;
|
|
|
|
|
color: #6c5ce7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-primary {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 13px;
|
|
|
|
|
@ -416,7 +596,42 @@ onMounted(() => {
|
|
|
|
|
gap: 10px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-cancel {
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
background: #e74c3c;
|
|
|
|
|
color: #fff;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-cancel:hover {
|
|
|
|
|
background: #c0392b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lp-toggle-area {
|
|
|
|
|
padding: 8px 20px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #b2bec3;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
border-top: 1px solid #eee;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lp-toggle-area:hover {
|
|
|
|
|
color: #6c5ce7;
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Form disabled state */
|
|
|
|
|
.form-wrap.form-disabled {
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.spin {
|
|
|
|
|
|