fix: 完善细节

refactor
Lxy 1 week ago
parent f94177c6ed
commit c85cb97eed

@ -7,23 +7,79 @@
</div> </div>
<!-- Input form --> <!-- 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="form-row">
<div class="fg"><label>目的地</label><input v-model="destination" placeholder="如:云南、贵州、四川" :disabled="isGenerating" value="四川"></div> <div class="fg"><label>目的地</label><input v-model="destination" placeholder="如:云南、贵州、四川" :disabled="isGenerating" value="四川"></div>
<div class="fg"><label>出行天数</label> <div class="fg"><label>出行天数</label>
<select v-model="days" :disabled="isGenerating"> <div class="combobox-wrap" ref="daysComboboxRef">
<option v-for="d in [2,3,5,7]" :key="d" :value="d">{{ d }}</option> <input
</select> 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> </div>
<div class="form-row"> <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> <div class="fg"><label>出行方式</label>
<select v-model="transport" :disabled="isGenerating"> <div class="combobox-wrap">
<option value="自驾">自驾</option> <input
<option value="公共交通">公共交通</option> v-model="transportDisplay"
<option value="步行/骑行">步行/骑行</option> @focus="transportDropdownOpen = true"
</select> @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> </div>
<div class="fg"><label>其他需求选填</label><input v-model="extraNeeds" placeholder="如:带老人小孩、预算范围、必去景点等" :disabled="isGenerating"></div> <div class="fg"><label>其他需求选填</label><input v-model="extraNeeds" placeholder="如:带老人小孩、预算范围、必去景点等" :disabled="isGenerating"></div>
@ -32,11 +88,11 @@
</button> </button>
</div> </div>
<!-- AI Thinking process --> <!-- AI Thinking process (shown below form when generating) -->
<div v-if="isGenerating" class="load-panel"> <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> <div class="spin"></div><span>AI 正在规划中...</span>
<span class="lp-toggle">{{ thinkingExpanded ? '收起' : '展开' }}</span> <button class="btn-cancel" @click="cancelGeneration"> </button>
</div> </div>
<transition name="expand"> <transition name="expand">
<div v-if="thinkingExpanded" class="lp-body"> <div v-if="thinkingExpanded" class="lp-body">
@ -44,6 +100,9 @@
<div class="lp-step" v-else>...</div> <div class="lp-step" v-else>...</div>
</div> </div>
</transition> </transition>
<div class="lp-toggle-area" @click="thinkingExpanded = !thinkingExpanded">
{{ thinkingExpanded ? '收起 ▲' : '展开 ▼' }}
</div>
</div> </div>
<!-- Scheme list with regenerate button --> <!-- Scheme list with regenerate button -->
@ -175,12 +234,19 @@ const MAX_SCHEMES = 9
const destination = ref('') const destination = ref('')
const days = ref(3) 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 transport = ref('自驾')
const transportDisplay = ref('自驾')
const transportDropdownOpen = ref(false)
const extraNeeds = ref('') const extraNeeds = ref('')
const isGenerating = ref(false) const isGenerating = ref(false)
const thinkingText = ref('') const thinkingText = ref('')
const thinkingExpanded = ref(true) const thinkingExpanded = ref(true)
const abortController = ref(null)
// Preview modal // Preview modal
const previewIndex = ref(-1) const previewIndex = ref(-1)
@ -200,25 +266,74 @@ function getBadgeClass(i) {
return classes[i % 3] 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() { async function generatePlan() {
if (!destination.value.trim() || isGenerating.value) return if (!destination.value.trim() || isGenerating.value) return
isGenerating.value = true isGenerating.value = true
thinkingText.value = '' thinkingText.value = ''
thinkingExpanded.value = true thinkingExpanded.value = true
abortController.value = new AbortController()
try { try {
const userInput = { const userInput = {
destination: destination.value.trim(), destination: destination.value.trim(),
days: days.value, days: days.value,
startDate: startDate.value || '近期', startDate: `${startMonth.value}`,
transport: transport.value, transport: transport.value,
extraNeeds: extraNeeds.value.trim() || '无特殊需求' extraNeeds: extraNeeds.value.trim() || '无特殊需求'
} }
const result = await quickPlan(userInput, (text) => { const result = await quickPlan(userInput, (text) => {
thinkingText.value = text thinkingText.value = text
}) }, abortController.value)
console.log('[generatePlan] AI 返回结果:', result) console.log('[generatePlan] AI 返回结果:', result)
console.log('[generatePlan] schemes 数量:', result.schemes?.length) console.log('[generatePlan] schemes 数量:', result.schemes?.length)
@ -252,10 +367,23 @@ async function generatePlan() {
console.log('[generatePlan] 保存方案总数:', currentSchemes.length) console.log('[generatePlan] 保存方案总数:', currentSchemes.length)
store.saveSchemesToStore(currentSchemes, currentHistory) store.saveSchemesToStore(currentSchemes, currentHistory)
} catch (error) { } catch (error) {
if (error.name === 'AbortError') {
console.log('[generatePlan] 用户取消生成')
thinkingText.value = '已取消生成'
} else {
thinkingText.value = '生成失败:' + (error.message || '请重试') thinkingText.value = '生成失败:' + (error.message || '请重试')
console.error('[generatePlan] 生成失败:', error) console.error('[generatePlan] 生成失败:', error)
}
} finally { } finally {
isGenerating.value = false 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; 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 { .btn-primary {
width: 100%; width: 100%;
padding: 13px; padding: 13px;
@ -416,7 +596,42 @@ onMounted(() => {
gap: 10px; gap: 10px;
font-weight: 600; font-weight: 600;
font-size: 14px; 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; 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 { .spin {

@ -325,13 +325,14 @@ function parseAIResponse(content) {
} }
// Shared streaming function // 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' const apiUrl = import.meta.env.DEV ? 'http://localhost:3001/api/chat' : '/api/chat'
console.log('[streamAI] Fetching:', apiUrl) console.log('[streamAI] Fetching:', apiUrl)
const response = await fetch(apiUrl, { const response = await fetch(apiUrl, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, 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) 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} const userMessage = `目的地:${userInput.destination}
出行天数${userInput.days} 出行天数${userInput.days}
出发日期${userInput.startDate} 出发日期${userInput.startDate}
@ -457,7 +458,7 @@ export async function quickPlan(userInput, onThinking = null) {
const content = await streamAIRequest([ const content = await streamAIRequest([
{ role: 'system', content: QUICK_PLAN_PROMPT }, { role: 'system', content: QUICK_PLAN_PROMPT },
{ role: 'user', content: userMessage } { role: 'user', content: userMessage }
], onThinking) ], onThinking, signal)
const parsed = parseJSONFromContent(content) const parsed = parseJSONFromContent(content)

Loading…
Cancel
Save