From c85cb97eeda50036e1058ccd7e27a4b9117a11d1 Mon Sep 17 00:00:00 2001 From: Lxy Date: Sun, 7 Jun 2026 00:03:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=8C=E5=96=84=E7=BB=86=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/QuickPlanPanel.vue | 251 +++++++++++++++++++++++++++--- src/services/aiService.js | 9 +- 2 files changed, 238 insertions(+), 22 deletions(-) diff --git a/src/components/QuickPlanPanel.vue b/src/components/QuickPlanPanel.vue index 69c70dd..3ef82d6 100644 --- a/src/components/QuickPlanPanel.vue +++ b/src/components/QuickPlanPanel.vue @@ -7,23 +7,79 @@ -
+
- +
+ +
+
+ {{ d }}天 +
+
+
-
+
+
+ +
+
+ {{ m }}月 +
+
+
+
- +
+ +
+
+ {{ t }} +
+
+
@@ -32,11 +88,11 @@
- +
-
+
AI 正在规划中... - {{ thinkingExpanded ? '收起' : '展开' }} +
@@ -44,6 +100,9 @@
分析用户需求中...
+
+ {{ thinkingExpanded ? '收起 ▲' : '展开 ▼' }} +
@@ -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 { diff --git a/src/services/aiService.js b/src/services/aiService.js index 1dd6552..03f2f5c 100644 --- a/src/services/aiService.js +++ b/src/services/aiService.js @@ -325,13 +325,14 @@ function parseAIResponse(content) { } // Shared streaming function -async function streamAIRequest(messages, onThinking = null) { +async function streamAIRequest(messages, onThinking = null, signal = null) { const apiUrl = import.meta.env.DEV ? 'http://localhost:3001/api/chat' : '/api/chat' console.log('[streamAI] Fetching:', apiUrl) const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ messages, stream: true }) + body: JSON.stringify({ messages, stream: true }), + signal }) console.log('[streamAI] Response status:', response.status) @@ -439,7 +440,7 @@ function parseJSONFromContent(content) { } } -export async function quickPlan(userInput, onThinking = null) { +export async function quickPlan(userInput, onThinking = null, signal = null) { const userMessage = `目的地:${userInput.destination} 出行天数:${userInput.days}天 出发日期:${userInput.startDate} @@ -457,7 +458,7 @@ export async function quickPlan(userInput, onThinking = null) { const content = await streamAIRequest([ { role: 'system', content: QUICK_PLAN_PROMPT }, { role: 'user', content: userMessage } - ], onThinking) + ], onThinking, signal) const parsed = parseJSONFromContent(content)