feat: 重构初步可用,但是功能缺失

refactor
Lxy 1 week ago
parent 3f13a42fb1
commit 91cf8f5d44

@ -0,0 +1,464 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能行程规划系统 - 交互设计 v2</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;background:#f5f7fa;color:#1a1a2e;line-height:1.6}
.top-bar{background:#fff;padding:12px 32px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 1px 3px rgba(0,0,0,0.06);position:sticky;top:0;z-index:100}
.top-bar .brand{display:flex;align-items:center;gap:10px;font-size:17px;font-weight:700;color:#2d3436}
.top-bar .brand span.icon{font-size:24px}
.top-bar .steps{display:flex;align-items:center;gap:6px}
.top-bar .step{display:flex;align-items:center;gap:6px;font-size:13px;color:#b2bec3}
.top-bar .step.active{color:#6c5ce7;font-weight:600}
.top-bar .step .num{width:22px;height:22px;border-radius:50%;background:#dfe6e9;color:#636e72;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600}
.top-bar .step.active .num{background:#6c5ce7;color:#fff}
.top-bar .step .line{width:28px;height:2px;background:#dfe6e9}
.top-bar .step.active .line{background:#6c5ce7}
.top-bar .step.done .num{background:#00b894;color:#fff}
.top-bar .step.done .line{background:#00b894}
.top-bar .back-btn{background:none;border:none;color:#636e72;cursor:pointer;font-size:14px;padding:6px 12px;border-radius:6px}
.top-bar .back-btn:hover{background:#f5f7fa}
.container{max-width:1100px;margin:0 auto;padding:24px 32px}
.page{display:none}.page.active{display:block}
.home-header{text-align:center;padding:48px 0 36px}
.home-header h1{font-size:28px;font-weight:800;color:#2d3436;margin-bottom:8px}
.home-header p{color:#636e72;font-size:15px}
.mode-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:28px}
.mode-card{background:#fff;border-radius:14px;padding:28px 22px;cursor:pointer;transition:all 0.25s;border:2px solid transparent;box-shadow:0 2px 8px rgba(0,0,0,0.04)}
.mode-card:hover{transform:translateY(-3px);box-shadow:0 8px 24px rgba(108,92,231,0.12);border-color:#6c5ce7}
.mode-card .mc-icon{width:56px;height:56px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-size:26px;margin-bottom:14px}
.mode-card:nth-child(1) .mc-icon{background:#e8e4ff}
.mode-card:nth-child(2) .mc-icon{background:#fff3e0}
.mode-card:nth-child(3) .mc-icon{background:#e0f7fa}
.mode-card h3{font-size:17px;font-weight:700;margin-bottom:6px;color:#2d3436}
.mode-card p{font-size:13px;color:#636e72;margin-bottom:14px;min-height:36px}
.mode-card .mc-tags{display:flex;flex-wrap:wrap;gap:6px}
.mode-card .mc-tag{font-size:11px;padding:3px 10px;border-radius:12px;background:#f0f0f5;color:#636e72}
.form-wrap{max-width:580px;margin:0 auto;background:#fff;border-radius:16px;padding:32px;box-shadow:0 4px 20px rgba(0,0,0,0.05)}
.form-wrap h2{font-size:20px;font-weight:700;margin-bottom:24px;color:#2d3436;text-align:center}
.fg{margin-bottom:18px}
.fg label{display:block;font-size:13px;font-weight:600;color:#2d3436;margin-bottom:6px}
.fg input,.fg select,.fg textarea{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}
.fg input:focus,.fg select:focus,.fg textarea:focus{border-color:#6c5ce7}
.fg textarea{min-height:120px;resize:vertical}
.fg .hint{font-size:12px;color:#b2bec3;margin-top:4px}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.btn-primary{width:100%;padding:13px;background:linear-gradient(135deg,#6c5ce7,#a29bfe);color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;transition:all 0.2s}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 14px rgba(108,92,231,0.35)}
.btn-primary:disabled{opacity:0.5;cursor:not-allowed;transform:none}
.load-panel{max-width:580px;margin:20px auto 0;background:#fff;border-radius:14px;box-shadow:0 2px 12px rgba(0,0,0,0.05);overflow:hidden}
.load-panel .lp-head{padding:14px 20px;background:#f8f9fa;border-bottom:1px solid #eee;display:flex;align-items:center;gap:10px;font-weight:600;font-size:14px}
.load-panel .lp-head .spin{width:18px;height:18px;border:2.5px solid #dfe6e9;border-top-color:#6c5ce7;border-radius:50%;animation:spin 0.7s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.load-panel .lp-body{padding:16px 20px;max-height:200px;overflow-y:auto}
.load-panel .lp-step{display:flex;gap:8px;font-size:13px;color:#636e72;margin-bottom:8px}
.load-panel .lp-step .ck{color:#00b894;font-weight:700}
.plan-list{max-width:760px;margin:0 auto}
.plan-list .pl-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}
.plan-list .pl-head h2{font-size:20px;font-weight:700}
.plan-card{background:#fff;border-radius:14px;padding:22px;margin-bottom:14px;box-shadow:0 2px 8px rgba(0,0,0,0.04);border:2px solid #eee;transition:all 0.2s;cursor:pointer}
.plan-card:hover{border-color:#6c5ce7;box-shadow:0 4px 16px rgba(108,92,231,0.1)}
.plan-card .pc-top{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.plan-card .pc-badge{font-size:11px;padding:3px 10px;border-radius:6px;font-weight:700}
.plan-card .pc-badge.a{background:#fff3e0;color:#e17055}
.plan-card .pc-badge.b{background:#e0f2fe;color:#0984e3}
.plan-card .pc-badge.c{background:#d1fae5;color:#00b894}
.plan-card .pc-title{font-size:17px;font-weight:700;color:#2d3436}
.plan-card .pc-meta{display:flex;gap:16px;font-size:13px;color:#636e72;margin-bottom:10px}
.plan-card .pc-route{background:#f8f9fa;padding:8px 12px;border-radius:8px;font-size:13px;color:#636e72;margin-bottom:10px}
.plan-card .pc-tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px}
.plan-card .pc-tag{font-size:12px;padding:3px 10px;border-radius:6px;background:#f0f0f5;color:#636e72}
.plan-card .pc-actions{text-align:right}
.plan-card .pc-btn{padding:8px 22px;background:linear-gradient(135deg,#6c5ce7,#a29bfe);color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer}
.chat-wrap{max-width:800px;margin:0 auto;display:flex;flex-direction:column;height:calc(100vh - 130px)}
.chat-msgs{flex:1;overflow-y:auto;padding:20px 0;display:flex;flex-direction:column;gap:18px}
.chat-bubble{display:flex;gap:10px;max-width:75%}
.chat-bubble.bot{align-self:flex-start}
.chat-bubble.user{align-self:flex-end;flex-direction:row-reverse}
.chat-bubble .cb-av{width:34px;height:34px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}
.chat-bubble.bot .cb-av{background:#e8e4ff}
.chat-bubble.user .cb-av{background:#d1fae5}
.chat-bubble .cb-body{padding:12px 16px;border-radius:14px;font-size:14px;line-height:1.6}
.chat-bubble.bot .cb-body{background:#fff;color:#2d3436;border-bottom-left-radius:4px;box-shadow:0 2px 6px rgba(0,0,0,0.04)}
.chat-bubble.user .cb-body{background:linear-gradient(135deg,#6c5ce7,#a29bfe);color:#fff;border-bottom-right-radius:4px}
.chat-input-bar{padding:14px 0;display:flex;gap:10px}
.chat-input-bar input{flex:1;padding:12px 18px;border:1.5px solid #dfe6e9;border-radius:24px;font-size:14px;outline:none}
.chat-input-bar input:focus{border-color:#6c5ce7}
.chat-input-bar button{width:44px;height:44px;border-radius:50%;background:linear-gradient(135deg,#6c5ce7,#a29bfe);color:#fff;border:none;cursor:pointer;font-size:16px}
.chat-cards{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:10px}
.chat-cards .cc{background:#fff;border-radius:12px;padding:16px;border:1.5px solid #eee;cursor:pointer;transition:all 0.2s}
.chat-cards .cc:hover{border-color:#6c5ce7}
.chat-cards .cc .cc-badge{font-size:11px;padding:2px 8px;border-radius:4px;background:#fff3e0;color:#e17055;font-weight:600}
.chat-cards .cc h4{font-size:14px;margin:6px 0 4px}
.chat-cards .cc .cc-meta{font-size:12px;color:#636e72}
.chat-cards .cc .cc-link{margin-top:10px;font-size:12px;color:#6c5ce7;font-weight:600}
.wb-wrap{display:grid;grid-template-columns:220px 300px 1fr;gap:14px;height:calc(100vh - 120px)}
.wb-left{background:#2d3436;border-radius:12px;padding:16px;overflow-y:auto}
.wb-left .wl-title{color:#fff;font-size:13px;font-weight:600;margin-bottom:14px}
.wb-left .wl-item{display:flex;gap:10px;padding:8px 6px;cursor:pointer;color:rgba(255,255,255,0.5);transition:all 0.2s;border-radius:6px}
.wb-left .wl-item:hover,.wb-left .wl-item.active{color:#fff;background:rgba(108,92,231,0.3)}
.wb-left .wl-dot{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,0.3);margin-top:7px}
.wb-left .wl-item.active .wl-dot{background:#6c5ce7}
.wb-left .wl-name{font-size:13px}
.wb-left .wl-day{font-size:11px;opacity:0.5}
.wb-left .wl-nav{margin-top:16px;display:flex;gap:8px}
.wb-left .wl-nav button{flex:1;padding:8px;border-radius:8px;border:none;font-size:12px;cursor:pointer}
.wb-left .wl-nav .prev{background:rgba(255,255,255,0.1);color:#fff}
.wb-left .wl-nav .next{background:#6c5ce7;color:#fff}
.wb-mid{background:#fff;border-radius:12px;overflow-y:auto}
.wb-mid .wm-hero{height:140px;background:linear-gradient(135deg,#6c5ce7,#a29bfe);position:relative}
.wb-mid .wm-hero .wm-ov{position:absolute;bottom:0;left:0;right:0;padding:14px 16px;background:linear-gradient(transparent,rgba(0,0,0,0.5));color:#fff}
.wb-mid .wm-hero .wm-ov h3{font-size:16px}
.wb-mid .wm-hero .wm-ov p{font-size:12px;opacity:0.8}
.wb-mid .wm-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;padding:14px}
.wb-mid .wm-stat{text-align:center;padding:10px;background:#f8f9fa;border-radius:10px}
.wb-mid .wm-stat .ws-val{font-size:15px;font-weight:700;color:#6c5ce7}
.wb-mid .wm-stat .ws-lbl{font-size:11px;color:#636e72}
.wb-mid .wm-sec{padding:14px 16px;border-top:1px solid #f0f0f0}
.wb-mid .wm-sec h4{font-size:13px;margin-bottom:8px}
.wb-mid .wm-si{display:flex;gap:8px;font-size:12px;margin-bottom:6px}
.wb-mid .wm-si .si-time{color:#6c5ce7;font-weight:600;min-width:32px}
.wb-mid .wm-foods,.wb-mid .wm-spots{display:flex;flex-wrap:wrap;gap:5px}
.wb-mid .wm-foods span,.wb-mid .wm-spots span{font-size:11px;padding:3px 8px;border-radius:5px;background:#f0f0f5;color:#636e72}
.wb-mid .wm-tips{background:#fff8e1;border-left:3px solid #f39c12;padding:10px 12px;border-radius:0 8px 8px 0;font-size:12px;color:#856404}
.wb-right{background:#dfe6e9;border-radius:12px;display:flex;align-items:center;justify-content:center;color:#636e72;font-size:14px;position:relative;overflow:hidden}
.wb-right .wb-map-demo{width:100%;height:100%;background:#e8e8e8}
.wb-top{grid-column:1/-1;background:#fff;padding:12px 20px;border-radius:10px;display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
.wb-top .wb-plan-selector{display:flex;gap:8px}
.wb-top .wb-plan-btn{padding:6px 16px;border-radius:6px;font-size:12px;cursor:pointer;border:1.5px solid #dfe6e9;background:#fff;color:#636e72;transition:all 0.2s}
.wb-top .wb-plan-btn.active{background:#6c5ce7;color:#fff;border-color:#6c5ce7}
.wb-top .wb-back-btn{padding:8px 16px;border-radius:6px;font-size:13px;cursor:pointer;border:1.5px solid #6c5ce7;background:#fff;color:#6c5ce7;transition:all 0.2s}
.wb-top .wb-back-btn:hover{background:#6c5ce7;color:#fff}
.divider{margin:50px 0 24px;text-align:center;padding-top:30px;border-top:2px dashed #dfe6e9}
.divider h2{font-size:20px;color:#2d3436;margin-bottom:6px}
.divider p{color:#636e72;font-size:13px}
.btn-sm{padding:6px 16px;border-radius:6px;font-size:12px;cursor:pointer;border:none;font-weight:500}
.btn-sm.outline{background:#fff;border:1.5px solid #dfe6e9;color:#636e72}
.btn-sm.outline:hover{border-color:#6c5ce7;color:#6c5ce7}
</style>
</head>
<body>
<nav class="top-bar">
<div class="brand"><span class="icon">🗺️</span> 智能行程规划</div>
<div class="steps" id="navSteps">
<div class="step active" id="s1"><div class="num">1</div><span>选择方式</span><div class="line"></div></div>
<div class="step" id="s2"><div class="num">2</div><span>输入需求</span><div class="line"></div></div>
<div class="step" id="s3"><div class="num">3</div><span>方案选择</span><div class="line"></div></div>
<div class="step" id="s4"><div class="num">4</div><span>工作台</span></div>
</div>
<button class="back-btn" onclick="goHome()">← 首页</button>
</nav>
<div class="container">
<!-- 页面1首页 -->
<div class="page active" id="p-home">
<div class="home-header">
<h1>智能行程规划</h1>
<p>选择一种方式开始你的旅行规划</p>
</div>
<div class="mode-grid">
<div class="mode-card" onclick="goTo('p-quick');setStep(2)">
<div class="mc-icon"></div>
<h3>快速规划</h3>
<p>输入目的地和天数,一键生成多个旅行方案供你选择</p>
<div class="mc-tags"><span class="mc-tag">快速生成</span><span class="mc-tag">多方案对比</span><span class="mc-tag">即输即得</span></div>
</div>
<div class="mode-card" onclick="goTo('p-chat');setStep(2)">
<div class="mc-icon">💬</div>
<h3>聊天问答式</h3>
<p>通过对话交流,我逐步了解你的需求,为你量身定制完美行程</p>
<div class="mc-tags"><span class="mc-tag">多轮对话</span><span class="mc-tag">逐步收集信息</span><span class="mc-tag">个性化规划</span></div>
</div>
<div class="mode-card" onclick="goTo('p-custom');setStep(2)">
<div class="mc-icon">✏️</div>
<h3>自定义行程</h3>
<p>输入你已规划的行程AI 评估合理性并给出优化建议</p>
<div class="mc-tags"><span class="mc-tag">行程评估</span><span class="mc-tag">智能建议</span><span class="mc-tag">优化方案</span></div>
</div>
</div>
</div>
<!-- 页面2快速规划表单 -->
<div class="page" id="p-quick">
<div class="home-header"><h1>⚡ 快速规划</h1><p>输入目的地和天数,立即生成多个旅行方案</p></div>
<div class="form-wrap">
<div class="form-row">
<div class="fg"><label>目的地</label><input placeholder="如:云南、贵州、四川" value="四川"></div>
<div class="fg"><label>出行天数</label><select><option>2天</option><option selected>3天</option><option>5天</option><option>7天</option></select></div>
</div>
<div class="form-row">
<div class="fg"><label>出发日期</label><input type="date" value="2026-07-02"></div>
<div class="fg"><label>出行方式</label><select><option selected>自驾</option><option>公共交通</option><option>步行/骑行</option></select></div>
</div>
<div class="fg"><label>其他需求(选填)</label><input placeholder="如:带老人小孩、预算范围、必去景点等"></div>
<button class="btn-primary" onclick="simulateLoading()">立即生成方案</button>
</div>
<div class="load-panel" id="quick-load" style="display:none">
<div class="lp-head"><div class="spin"></div><span>AI 正在规划中...</span><span style="margin-left:auto;font-size:12px;color:#b2bec3">收起</span></div>
<div class="lp-body">
<div class="lp-step"><span class="ck"></span> 分析用户需求目的地四川3天自驾</div>
<div class="lp-step"><span class="ck"></span> 检索当地景点与路线数据</div>
<div class="lp-step"><span class="ck"></span> 计算交通时间与距离</div>
<div class="lp-step"><span class="ck"></span> 生成 3 个差异化方案</div>
<div class="lp-step" style="opacity:0.5">⏳ 正在优化方案详情...</div>
</div>
</div>
</div>
<!-- 页面3快速规划方案列表 -->
<div class="page" id="p-quick-plans">
<div class="plan-list">
<div class="pl-head"><h2>📋 为你生成了 3 个方案</h2><button class="btn-sm outline" onclick="goTo('p-quick')">🔄 重新生成</button></div>
<div class="plan-card" onclick="goToWorkbench('方案A')">
<div class="pc-top"><span class="pc-badge a">方案A</span><span class="pc-title">经典熊猫与古堰休闲游</span></div>
<div class="pc-meta"><span> 3天</span><span>🚗 ~650km</span><span>💰 ¥3200</span></div>
<div class="pc-route">成都→都江堰→青城山→成都</div>
<div class="pc-tags"><span class="pc-tag">世界遗产都江堰水利工程</span><span class="pc-tag">青城山道教清幽</span><span class="pc-tag">成都熊猫基地</span></div>
<div class="pc-actions"><button class="pc-btn">选择此方案 →</button></div>
</div>
<div class="plan-card" onclick="goToWorkbench('方案B')">
<div class="pc-top"><span class="pc-badge b">方案B</span><span class="pc-title">川西深度生态游</span></div>
<div class="pc-meta"><span>📅 3天</span><span>🚗 ~1200km</span><span>💰 ¥4500</span></div>
<div class="pc-route">成都→九寨沟→黄龙→若尔盖→成都</div>
<div class="pc-tags"><span class="pc-tag">九寨沟彩林与瀑布群</span><span class="pc-tag">黄龙钙华池与雪山</span><span class="pc-tag">若尔盖湿地草原星空</span></div>
<div class="pc-actions"><button class="pc-btn">选择此方案 →</button></div>
</div>
<div class="plan-card" onclick="goToWorkbench('方案C')">
<div class="pc-top"><span class="pc-badge c">方案C</span><span class="pc-title">川南秘境探索线</span></div>
<div class="pc-meta"><span>📅 3天</span><span>🚗 ~850km</span><span>💰 ¥2800</span></div>
<div class="pc-route">成都→宜宾→蜀南竹海→兴文石海→成都</div>
<div class="pc-tags"><span class="pc-tag">蜀南竹海翡翠长廊</span><span class="pc-tag">兴文天坑溶洞奇观</span><span class="pc-tag">宜宾长江第一城文化</span></div>
<div class="pc-actions"><button class="pc-btn">选择此方案 →</button></div>
</div>
</div>
</div>
<!-- 页面4聊天问答 -->
<div class="page" id="p-chat">
<div class="chat-wrap">
<div class="chat-msgs">
<div class="chat-bubble bot">
<div class="cb-av">🤖</div>
<div class="cb-body">你好!我是你的行程规划助手。请告诉我:<br>• 想去哪里?(单个地点或多个地点组合)<br>• 几月出发?几天行程?<br>• 交通方式?(自驾/公共交通/步行)<br>• 有什么特殊需求?(带老人/小孩/特定景点)<br><br><em style="color:#b2bec3">示例9月去云南5天自驾带老人</em></div>
</div>
<div class="chat-bubble user">
<div class="cb-av">👤</div>
<div class="cb-body">9月去四川大概5天时间自驾</div>
</div>
<div class="chat-bubble bot">
<div class="cb-av">🤖</div>
<div class="cb-body">已为你生成 2 个方案,请查看下方卡片。<br><br><strong>【方案A】九寨黄龙秋韵线</strong><br>路线:成都-绵阳-九寨沟-黄龙-松潘-成都<br>5天 · 1150km · ¥3500<br><br><strong>【方案B】四姑娘山丹巴环线</strong><br>路线:成都-四姑娘山-丹巴甲居藏寨-康定-成都<br>5天 · 980km · ¥3200</div>
</div>
</div>
<div class="chat-cards">
<div class="cc" onclick="goToWorkbench('方案A')">
<span class="cc-badge">方案A</span>
<h4>九寨黄龙秋韵线</h4>
<div class="cc-meta">🚗 ~1150km &nbsp; ⏱ 18h &nbsp; 📅 5天 &nbsp; 💰 ¥3500</div>
<div class="cc-link">选择此方案进入工作台 →</div>
</div>
<div class="cc" onclick="goToWorkbench('方案B')">
<span class="cc-badge" style="background:#e0f2fe;color:#0984e3">方案B</span>
<h4>四姑娘山丹巴环线</h4>
<div class="cc-meta">🚗 ~980km &nbsp; ⏱ 16h &nbsp; 📅 5天 &nbsp; 💰 ¥3200</div>
<div class="cc-link">选择此方案进入工作台 →</div>
</div>
</div>
<div class="chat-input-bar">
<input placeholder="例如9月去云南5天自驾">
<button>发送</button>
</div>
</div>
</div>
<!-- 页面5自定义行程 -->
<div class="page" id="p-custom">
<div class="home-header">
<h1>✏️ 自定义行程</h1>
<p>输入你已规划的行程AI 评估合理性并给出优化建议</p>
</div>
<div class="form-wrap">
<div class="fg">
<label>行程安排</label>
<textarea class="form-textarea" placeholder="例如:&#10;Day 1: 昆明 -> 大理,上午出发,下午逛大理古城&#10;Day 2: 大理 -> 丽江,上午游览洱海,下午前往丽江&#10;Day 3: 丽江一日游,游览玉龙雪山、束河古镇&#10;Day 4: 丽江返程&#10;&#10;也可以自由描述AI 会自动解析..."></textarea>
</div>
<div class="form-row">
<div class="fg">
<label>出行方式</label>
<select>
<option selected>自驾</option>
<option>公共交通</option>
<option>步行/骑行</option>
</select>
</div>
<div class="fg">
<label>同行人员</label>
<select>
<option selected>独自</option>
<option>情侣/夫妻</option>
<option>家庭(带老人小孩)</option>
<option>朋友结伴</option>
</select>
</div>
</div>
<button class="btn-primary" onclick="alert('AI 正在评估行程合理性...')">AI 评估行程</button>
</div>
</div>
<!-- 页面6工作台沉浸式编辑 -->
<div class="page" id="p-workbench">
<div class="wb-wrap">
<!-- 工作台顶部:方案选择 + 返回按钮 -->
<div class="wb-top">
<div class="wb-plan-selector">
<button class="wb-plan-btn active" onclick="switchPlan(this,'A')">方案A</button>
<button class="wb-plan-btn" onclick="switchPlan(this,'B')">方案B</button>
<button class="wb-plan-btn" onclick="switchPlan(this,'C')">方案C</button>
</div>
<button class="wb-back-btn" onclick="goBackFromWorkbench()">← 返回方案列表</button>
</div>
<!-- 左侧:时间线 -->
<div class="wb-left">
<div class="wl-title">📋 行程时间线</div>
<div class="wl-item active">
<div class="wl-dot"></div>
<div>
<div class="wl-name">成都市区</div>
<div class="wl-day">Day 1 · 0km</div>
</div>
</div>
<div class="wl-item">
<div class="wl-dot"></div>
<div>
<div class="wl-name">都江堰景区</div>
<div class="wl-day">Day 2 · 60km</div>
</div>
</div>
<div class="wl-item">
<div class="wl-dot"></div>
<div>
<div class="wl-name">青城山</div>
<div class="wl-day">Day 3 · 25km</div>
</div>
</div>
<div class="wl-nav">
<button class="prev">← 上一站</button>
<button class="next">下一站 →</button>
</div>
</div>
<!-- 中间:详情面板 -->
<div class="wb-mid">
<div class="wm-hero">
<div class="wm-ov">
<h3>成都市区</h3>
<p>天府之国核心,美食与慢生活之都</p>
</div>
</div>
<div class="wm-stats">
<div class="wm-stat">
<div class="ws-val">0km</div>
<div class="ws-lbl">行驶里程</div>
</div>
<div class="wm-stat">
<div class="ws-val"></div>
<div class="ws-lbl">驾驶时间</div>
</div>
<div class="wm-stat">
<div class="ws-val">Day 1</div>
<div class="ws-lbl">第几天</div>
</div>
</div>
<div class="wm-sec">
<h4> 行程安排</h4>
<div class="wm-si"><span class="si-time">上午</span><span>参观大熊猫繁育研究基地</span></div>
<div class="wm-si"><span class="si-time">下午</span><span>逛宽窄巷子、武侯祠,品尝火锅</span></div>
</div>
<div class="wm-sec">
<h4>🍽️ 美食推荐</h4>
<div class="wm-foods">
<span>担担面</span><span>龙抄手</span><span>麻辣火锅</span>
</div>
</div>
<div class="wm-sec">
<h4>🏨 住宿推荐</h4>
<div style="font-size:13px;color:#636e72">春熙路商圈精品酒店</div>
</div>
<div class="wm-sec">
<h4>💡 注意事项</h4>
<div class="wm-tips">市区早晚高峰拥堵,建议错峰出行</div>
</div>
</div>
<div class="wb-right">
<div class="wb-map-demo">🗺️ 地图交互区域</div>
</div>
</div>
</div>
<!-- 底部说明 -->
<div class="divider">
<h2>交互设计说明</h2>
<p>本原型展示了从需求输入到方案生成的完整流程,风格统一、简洁。</p>
</div>
<script>
function goTo(pageId) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
const target = document.getElementById(pageId);
if (target) target.classList.add('active');
}
function goHome() {
goTo('p-home');
setStep(1);
}
function setStep(n) {
const steps = document.querySelectorAll('.top-bar .step');
steps.forEach((s, i) => {
s.classList.remove('active', 'done');
if (i + 1 === n) s.classList.add('active');
else if (i + 1 < n) s.classList.add('done');
});
if (n === 4) {
steps[0].classList.add('done');
steps[1].classList.add('done');
steps[2].classList.add('done');
steps[3].classList.add('active');
}
}
function simulateLoading() {
const loadPanel = document.getElementById('quick-load');
loadPanel.style.display = 'block';
setTimeout(() => {
loadPanel.style.display = 'none';
goTo('p-quick-plans');
setStep(3);
}, 2000);
}
function goToWorkbench(scheme) {
goTo('p-workbench');
setStep(4);
}
function goBackFromWorkbench() {
goTo('p-quick-plans');
setStep(3);
}
function switchPlan(btn, scheme) {
document.querySelectorAll('.wb-plan-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
console.log("切换到: " + scheme);
}
</script>
</body>
</html>

@ -1,33 +1,27 @@
<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/>
<br/>
<span class="example">示例9月去云南5自驾带老人</span><br/>
<span class="example">示例成都-九寨沟-重庆7天自驾游</span>
<div class="chat-msgs" ref="messagesContainer">
<div class="chat-bubble bot">
<div class="cb-av"></div>
<div class="cb-body">
你好我是你的行程规划助手请告诉我<br>
&bull; 想去哪里单个地点或多个地点组合<br>
&bull; 几月出发几天行程<br>
&bull; 交通方式自驾/公共交通/步行<br>
&bull; 有什么特殊需求带老人/小孩/特定景点<br>
<br>
<em style="color:#b2bec3">示例9月去云南5自驾带老人</em>
</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">
<div v-for="(msg, i) in messages" :key="i" class="chat-bubble" :class="msg.role">
<div class="cb-av" :class="msg.role === 'user' ? 'user-av' : 'bot-av'"></div>
<div class="cb-body">
<!-- Show thinking process for bot messages -->
<div v-if="msg.thinking" class="thinking-process" :class="{ expanded: msg.thinkingExpanded }">
<div class="thinking-header" @click="msg.thinkingExpanded = !msg.thinkingExpanded">
<span class="thinking-icon">🧠</span>
<span class="thinking-icon"></span>
<span>AI 思考过程</span>
<span class="thinking-toggle">{{ msg.thinkingExpanded ? '收起' : '展开' }}</span>
</div>
@ -36,67 +30,42 @@
<div class="message-content">{{ msg.content }}</div>
<!-- Error message -->
<div v-if="msg.error" class="error-hint">
💡 {{ msg.error }}
{{ msg.error }}
</div>
</div>
</div>
<!-- Loading indicator -->
<div v-if="isGenerating" class="message bot">
<div class="avatar">🤖</div>
<div class="bubble thinking">
<div v-if="isGenerating" class="chat-bubble bot">
<div class="cb-av bot-av"></div>
<div class="cb-body thinking">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
{{ thinkingStatus }}
</div>
</div>
</div>
<!-- Scheme cards (appear after AI response) -->
<div v-if="schemes.length > 0" class="chat-cards">
<div v-for="(scheme, i) in schemes" :key="i" class="cc" @click="selectScheme(i)">
<span class="cc-badge" :class="i === 1 ? 'badge-b' : ''">{{ getSchemeLabel(i) }}</span>
<h4>{{ scheme.name }}</h4>
<div class="cc-meta"> ~{{ scheme.totalKm }}km &nbsp; {{ scheme.totalDriveTime }}h &nbsp; {{ scheme.days }} &nbsp; {{ scheme.budget }}</div>
<div class="cc-link">选择此方案进入工作台</div>
</div>
</div>
<!-- Input area -->
<div class="chat-input-area">
<div class="chat-input-bar">
<input
v-model="inputText"
@keyup.enter="sendMessage"
placeholder="例如9月去云南5天自驾"
:disabled="isGenerating"
/>
<button @click="sendMessage" :disabled="!inputText.trim() || isGenerating">
{{ isGenerating ? '生成中...' : '发送' }}
</button>
</div>
<!-- Scheme cards (appear after AI response) -->
<transition name="slide-up">
<div v-if="schemes.length > 0" class="scheme-cards">
<h3>📋 为你生成了 {{ schemes.length }} 个方案</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">{{ '方案' + String.fromCharCode(65 + i) }}</span>
<h4>{{ scheme.name }}</h4>
</div>
<div class="card-stats">
<span>🚗 ~{{ scheme.totalKm }}km</span>
<span> {{ scheme.totalDriveTime }}h</span>
<span>📅 {{ scheme.days }}</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-route">
<span>🗺 {{ scheme.route }}</span>
</div>
<div class="card-action">选择此方案进入工作台 </div>
<button @click="sendMessage" :disabled="!inputText.trim() || isGenerating"></button>
</div>
</div>
</div>
</transition>
</div>
</template>
<script setup>
@ -120,6 +89,10 @@ const scrollToBottom = async () => {
}
}
function getSchemeLabel(i) {
return String.fromCharCode(65 + (i % 26))
}
const sendMessage = async () => {
if (!inputText.value.trim() || isGenerating.value) return
@ -136,31 +109,27 @@ const sendMessage = async () => {
let thinkingContent = ''
const result = await chatWithAI(userMsg, (streamContent) => {
// Parse streaming content for thinking
thinkingContent = streamContent
// Update thinking status based on progress
if (streamContent.includes('thinking')) {
thinkingStatus.value = '🧠 AI 正在思考并搜索目的地信息...'
thinkingStatus.value = 'AI 正在思考并搜索目的地信息...'
} else if (streamContent.includes('schemes')) {
thinkingStatus.value = '📋 正在生成旅行方案...'
thinkingStatus.value = '正在生成旅行方案...'
} else if (streamContent.includes('points')) {
thinkingStatus.value = '📍 正在规划详细行程...'
thinkingStatus.value = '正在规划详细行程...'
}
})
isGenerating.value = false
// Add bot response message with thinking process
const botMsg = {
role: 'bot',
content: `已为你生成 ${result.schemes.length} 个方案,请查看下方卡片。\n\n${result.schemes.map((s, i) => `方案${String.fromCharCode(65 + i)}${s.name}\n路线${s.route}\n${s.days}· ${s.totalKm}km · ${s.budget}`).join('\n\n')}`,
content: `已为你生成 ${result.schemes.length} 个方案,请查看下方卡片。\n\n${result.schemes.map((s, i) => `${getSchemeLabel(i)}${s.name}\n路线${s.route}\n${s.days}\u00b7 ${s.totalKm}km \u00b7 ${s.budget}`).join('\n\n')}`,
thinking: result.thinking,
thinkingExpanded: false
}
messages.value.push(botMsg)
// Set schemes for card display
schemes.value = result.schemes
await scrollToBottom()
@ -171,7 +140,7 @@ const sendMessage = async () => {
role: 'bot',
content: '抱歉,生成方案时遇到了问题。',
error: error.message.includes('API Key')
? '请先访问 /settings 配置你的 AI API Key'
? '请先访问设置页面配置你的 AI API Key'
: error.message + ',请重试'
})
await scrollToBottom()
@ -182,7 +151,6 @@ const selectScheme = (index) => {
const scheme = schemes.value[index]
if (!scheme) return
// Load the selected scheme into the itinerary store
store.loadFromAI(scheme)
store.setPhase('workbench')
}
@ -192,75 +160,79 @@ const selectScheme = (index) => {
.chat-interface {
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f5f5;
position: relative;
height: calc(100vh - 130px);
max-width: 800px;
margin: 0 auto;
padding: 24px 32px;
}
.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 {
.chat-msgs {
flex: 1;
overflow-y: auto;
padding: 20px;
padding: 20px 0;
display: flex;
flex-direction: column;
gap: 16px;
gap: 18px;
}
.message {
.chat-bubble {
display: flex;
gap: 12px;
max-width: 85%;
gap: 10px;
max-width: 75%;
}
.message.bot { align-self: flex-start; }
.message.user { align-self: flex-end; flex-direction: row-reverse; }
.chat-bubble.bot { align-self: flex-start; }
.chat-bubble.user { align-self: flex-end; flex-direction: row-reverse; }
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
.cb-av {
width: 34px;
height: 34px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
background: #e8f5e9;
font-size: 16px;
flex-shrink: 0;
}
.message.user .avatar { background: #e3f2fd; }
.bot-av {
background: #e8e4ff;
}
.bubble {
.bot-av::before {
content: '🤖';
}
.user-av {
background: #d1fae5;
}
.user-av::before {
content: '👤';
}
.cb-body {
padding: 12px 16px;
border-radius: 16px;
border-radius: 14px;
font-size: 14px;
line-height: 1.6;
}
.message.bot .bubble {
.chat-bubble.bot .cb-body {
background: #fff;
color: #333;
color: #2d3436;
border-bottom-left-radius: 4px;
box-shadow: 0 2px 6px rgba(0,0,0,0.04);
}
.message.user .bubble {
background: #2a9d8f;
.chat-bubble.user .cb-body {
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
color: #fff;
border-bottom-right-radius: 4px;
}
.example {
opacity: 0.8;
font-size: 13px;
.message-content {
white-space: pre-wrap;
}
/* Thinking process */
@ -285,6 +257,10 @@ const selectScheme = (index) => {
.thinking-header:hover { background: #f0f0f0; }
.thinking-icon::before {
content: '🧠';
}
.thinking-toggle {
margin-left: auto;
font-size: 12px;
@ -315,10 +291,6 @@ const selectScheme = (index) => {
color: #856404;
}
.message-content {
white-space: pre-wrap;
}
/* Loading */
.thinking {
display: flex;
@ -330,7 +302,7 @@ const selectScheme = (index) => {
width: 6px;
height: 6px;
border-radius: 50%;
background: #2a9d8f;
background: #6c5ce7;
animation: pulse 1.4s infinite;
}
@ -342,156 +314,122 @@ const selectScheme = (index) => {
40% { opacity: 1; transform: scale(1); }
}
/* Input */
.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 input:disabled { background: #f5f5f5; cursor: not-allowed; }
.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);
max-height: 60vh;
overflow-y: auto;
}
.scheme-cards h3 {
margin: 0 0 16px;
font-size: 16px;
color: #1b4332;
}
.cards-grid {
.chat-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: 1fr 1fr;
gap: 12px;
padding: 16px 0;
}
.scheme-card {
border: 2px solid #e0e0e0;
.cc {
background: #fff;
border-radius: 12px;
padding: 16px;
border: 1.5px solid #eee;
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;
.cc:hover {
border-color: #6c5ce7;
}
.card-badge {
background: #f4a261;
color: #1b4332;
.cc-badge {
font-size: 11px;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
background: #fff3e0;
color: #e17055;
font-weight: 600;
}
.card-header h4 {
margin: 0;
.cc-badge.badge-b {
background: #e0f2fe;
color: #0984e3;
}
.cc h4 {
font-size: 14px;
color: #333;
margin: 6px 0 4px;
color: #2d3436;
}
.card-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
margin-bottom: 8px;
.cc-meta {
font-size: 12px;
color: #666;
color: #636e72;
}
.cc-link {
margin-top: 10px;
font-size: 12px;
color: #6c5ce7;
font-weight: 600;
}
.card-highlights {
/* Input */
.chat-input-bar {
padding: 14px 0;
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 8px;
gap: 10px;
}
.highlight-tag {
background: #e8f5e9;
color: #2a9d8f;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
.chat-input-bar input {
flex: 1;
padding: 12px 18px;
border: 1.5px solid #dfe6e9;
border-radius: 24px;
font-size: 14px;
outline: none;
transition: border 0.2s;
}
.card-route {
font-size: 11px;
color: #888;
margin-bottom: 12px;
padding: 6px;
background: #f8f9fa;
border-radius: 6px;
.chat-input-bar input:focus {
border-color: #6c5ce7;
}
.card-action {
text-align: center;
color: #2a9d8f;
font-weight: 600;
font-size: 13px;
.chat-input-bar input:disabled {
background: #f5f7fa;
cursor: not-allowed;
}
.slide-up-enter-active, .slide-up-leave-active {
transition: all 0.3s ease;
.chat-input-bar button {
width: 44px;
height: 44px;
border-radius: 50%;
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
color: #fff;
border: none;
cursor: pointer;
font-size: 16px;
transition: all 0.2s;
}
.chat-input-bar button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(108,92,231,0.35);
}
.chat-input-bar button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.slide-up-enter-from, .slide-up-leave-to {
transform: translateY(20px);
opacity: 0;
.chat-input-bar button::before {
content: '➤';
}
@media (max-width: 900px) {
.cards-grid {
@media (max-width: 768px) {
.chat-interface {
padding: 16px;
}
.chat-cards {
grid-template-columns: 1fr;
}
.chat-bubble {
max-width: 90%;
}
}
</style>

@ -1,74 +1,57 @@
<template>
<div class="custom-plan">
<div class="custom-header">
<div class="cp-header">
<h1> 自定义行程</h1>
<p>输入你的行程安排AI 帮你评估合理性并给出优化建议</p>
<p>输入你已规划的行程AI 评估合理性并给出优化建议</p>
</div>
<!-- Input form -->
<div class="custom-form">
<div class="form-group">
<div class="form-wrap">
<div class="fg">
<label>行程安排</label>
<textarea
v-model="itineraryText"
placeholder="请按格式输入你的行程安排,例如:&#10;Day 1昆明 -> 大理,上午出发,下午逛大理古城&#10;Day 2大理 -> 丽江,上午游览洱海,下午前往丽江&#10;Day 3丽江一日游游览玉龙雪山、束河古镇&#10;Day 4丽江返程&#10;&#10;也可以自由描述AI 会自动解析..."
placeholder="例如:&#10;Day 1: 昆明 -> 大理,上午出发,下午逛大理古城&#10;Day 2: 大理 -> 丽江,上午游览洱海,下午前往丽江&#10;Day 3: 丽江一日游,游览玉龙雪山、束河古镇&#10;Day 4: 丽江返程&#10;&#10;也可以自由描述AI 会自动解析..."
:disabled="isGenerating"
rows="10"
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<div class="fg">
<label>出行方式</label>
<select v-model="transport" :disabled="isGenerating">
<option value="自驾">自驾</option>
<option value="高铁">高铁</option>
<option value="飞机">飞机</option>
<option value="公共交通">公共交通</option>
<option value="步行/骑行">步行/骑行</option>
</select>
</div>
<div class="form-group">
<div class="fg">
<label>同行人员</label>
<select v-model="companions" :disabled="isGenerating">
<option value="独自">独自</option>
<option value="情侣">情侣</option>
<option value="朋友">朋友</option>
<option value="带老人">带老人</option>
<option value="带小孩">带小孩</option>
<option value="家庭出游">家庭出游</option>
<option value="情侣/夫妻">情侣/夫妻</option>
<option value="家庭(带老人小孩)">家庭带老人小孩</option>
<option value="朋友结伴">朋友结伴</option>
</select>
</div>
</div>
<button
class="generate-btn"
@click="evaluatePlan"
:disabled="!itineraryText.trim() || isGenerating"
>
<button class="btn-primary" @click="evaluatePlan" :disabled="!itineraryText.trim() || isGenerating">
{{ isGenerating ? '评估中...' : 'AI 评估行程' }}
</button>
</div>
<!-- AI Thinking process -->
<div v-if="isGenerating" class="thinking-section">
<div class="thinking-header" @click="thinkingExpanded = !thinkingExpanded">
<div class="thinking-spinner"></div>
<span class="thinking-title">AI 正在评估中...</span>
<span class="thinking-toggle">{{ thinkingExpanded ? '收起' : '展开详情' }}</span>
<div v-if="isGenerating" class="load-panel">
<div class="lp-head" @click="thinkingExpanded = !thinkingExpanded">
<div class="spin"></div><span>AI 正在评估中...</span>
<span class="lp-toggle">{{ thinkingExpanded ? '收起' : '展开' }}</span>
</div>
<transition name="expand">
<div v-if="thinkingExpanded" class="thinking-content">
<div class="thinking-text">{{ thinkingText || '正在初始化...' }}</div>
<div v-if="thinkingExpanded" class="lp-body">
<div class="lp-step" v-if="thinkingText">{{ thinkingText }}</div>
<div class="lp-step" v-else>...</div>
</div>
</transition>
</div>
<!-- Loading (fallback) -->
<div v-if="isGenerating && !thinkingText" class="loading-section">
<div class="loading-spinner"></div>
<p>{{ loadingText }}</p>
</div>
<!-- Evaluation result -->
<transition name="fade-up">
<div v-if="evaluationResult" class="result-section">
@ -119,7 +102,6 @@ const itineraryText = ref('')
const transport = ref('自驾')
const companions = ref('独自')
const isGenerating = ref(false)
const loadingText = ref('正在分析行程安排...')
const thinkingText = ref('')
const thinkingExpanded = ref(true)
const evaluationResult = ref(null)
@ -135,7 +117,6 @@ const evaluatePlan = async () => {
originalScheme.value = null
thinkingText.value = ''
thinkingExpanded.value = true
loadingText.value = '正在解析你的行程安排...'
try {
const userInput = {
@ -179,204 +160,173 @@ const useOriginalPlan = () => {
<style scoped>
.custom-plan {
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f5f5;
overflow-y: auto;
max-width: 580px;
margin: 0 auto;
padding: 24px 32px;
}
.custom-header {
.cp-header {
text-align: center;
padding: 30px 20px;
background: linear-gradient(135deg, #264653, #2a9d8f);
color: #fff;
padding: 20px 0 28px;
}
.custom-header h1 { margin: 0 0 8px; font-size: 24px; }
.custom-header p { margin: 0; opacity: 0.9; font-size: 14px; }
.cp-header h1 {
font-size: 20px;
font-weight: 700;
margin-bottom: 6px;
color: #2d3436;
}
.custom-form {
max-width: 700px;
margin: 30px auto;
padding: 24px;
.cp-header p {
color: #636e72;
font-size: 14px;
}
/* Form */
.form-wrap {
background: #fff;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
padding: 32px;
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 16px;
.fg {
margin-bottom: 18px;
}
.form-group label {
.fg label {
display: block;
font-size: 13px;
font-weight: 600;
color: #555;
color: #2d3436;
margin-bottom: 6px;
}
.form-group textarea {
padding: 12px 14px;
border: 1px solid #ddd;
border-radius: 8px;
.fg input, .fg select, .fg textarea {
width: 100%;
padding: 10px 14px;
border: 1.5px solid #dfe6e9;
border-radius: 10px;
font-size: 14px;
font-family: inherit;
outline: none;
transition: border 0.2s;
font-family: inherit;
}
.fg textarea {
min-height: 120px;
resize: vertical;
transition: border-color 0.2s;
line-height: 1.6;
}
.form-group textarea:focus {
border-color: #2a9d8f;
.fg input:focus, .fg select:focus, .fg textarea:focus {
border-color: #6c5ce7;
}
.form-group textarea:disabled {
background: #f5f5f5;
.fg input:disabled, .fg select:disabled, .fg textarea:disabled {
background: #f5f7fa;
cursor: not-allowed;
}
.form-row {
display: flex;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.form-row .form-group {
flex: 1;
}
.form-group select {
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.form-group select:focus {
border-color: #2a9d8f;
}
.form-group select:disabled {
background: #f5f5f5;
cursor: not-allowed;
}
.generate-btn {
.btn-primary {
width: 100%;
padding: 14px;
background: #2a9d8f;
padding: 13px;
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
color: #fff;
border: none;
border-radius: 10px;
font-size: 16px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
transition: all 0.2s;
}
.generate-btn:hover:not(:disabled) { background: #21867a; }
.generate-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(108,92,231,0.35);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.thinking-section {
max-width: 700px;
margin: 20px auto;
/* Loading Panel */
.load-panel {
margin-top: 20px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
border-radius: 14px;
box-shadow: 0 2px 12px rgba(0,0,0,0.05);
overflow: hidden;
}
.thinking-header {
.lp-head {
padding: 14px 20px;
background: #f8f9fa;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
gap: 10px;
font-weight: 600;
font-size: 14px;
cursor: pointer;
user-select: none;
}
.thinking-spinner {
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #2a9d8f;
.spin {
width: 18px;
height: 18px;
border: 2.5px solid #dfe6e9;
border-top-color: #6c5ce7;
border-radius: 50%;
animation: spin 1s linear infinite;
animation: spin 0.7s linear infinite;
flex-shrink: 0;
}
.thinking-title {
flex: 1;
font-size: 14px;
font-weight: 600;
color: #333;
@keyframes spin {
to { transform: rotate(360deg); }
}
.thinking-toggle {
.lp-toggle {
margin-left: auto;
font-size: 12px;
color: #999;
color: #b2bec3;
font-weight: 400;
}
.thinking-content {
border-top: 1px solid #f0f0f0;
.lp-body {
padding: 16px 20px;
max-height: 200px;
overflow-y: auto;
}
.thinking-text {
padding: 16px 20px;
.lp-step {
display: flex;
gap: 8px;
font-size: 13px;
color: #555;
line-height: 1.8;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
background: #fafafa;
color: #636e72;
margin-bottom: 8px;
}
.expand-enter-active, .expand-leave-active {
transition: all 0.3s ease;
max-height: 300px;
max-height: 200px;
overflow: hidden;
}
.expand-enter-from, .expand-leave-to {
max-height: 0;
opacity: 0;
}
.loading-section {
text-align: center;
padding: 40px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #2a9d8f;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-section p {
color: #666;
font-size: 14px;
}
/* Results */
.result-section {
max-width: 700px;
margin: 0 auto 40px;
padding: 0 20px;
margin-top: 24px;
display: flex;
flex-direction: column;
gap: 16px;
@ -392,11 +342,11 @@ const useOriginalPlan = () => {
.result-card h3 {
margin: 0 0 12px;
font-size: 16px;
color: #333;
color: #2d3436;
}
.summary-card {
border-left: 4px solid #2a9d8f;
border-left: 4px solid #00b894;
}
.summary-card p {
@ -406,7 +356,7 @@ const useOriginalPlan = () => {
}
.suggestion-card {
border-left: 4px solid #f4a261;
border-left: 4px solid #f39c12;
}
.suggestion-card ul {
@ -417,16 +367,16 @@ const useOriginalPlan = () => {
}
.itinerary-card {
border-left: 4px solid #264653;
border-left: 4px solid #6c5ce7;
}
.original-card {
border-left: 4px solid #e76f51;
border-left: 4px solid #e17055;
}
.route-preview {
font-size: 13px;
color: #666;
color: #636e72;
padding: 8px 12px;
background: #f8f9fa;
border-radius: 6px;
@ -437,30 +387,38 @@ const useOriginalPlan = () => {
display: flex;
gap: 16px;
font-size: 13px;
color: #888;
color: #b2bec3;
margin-bottom: 14px;
}
.use-btn {
width: 100%;
padding: 12px;
background: #2a9d8f;
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
color: #fff;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
transition: all 0.2s;
}
.use-btn:hover { background: #21867a; }
.use-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(108,92,231,0.35);
}
.original-btn {
background: #e76f51;
background: #fff;
color: #6c5ce7;
border: 1.5px solid #6c5ce7;
}
.original-btn:hover { background: #d35f44; }
.original-btn:hover {
background: #6c5ce7;
color: #fff;
}
.fade-up-enter-active, .fade-up-leave-active {
transition: all 0.3s ease;
@ -470,4 +428,14 @@ const useOriginalPlan = () => {
transform: translateY(20px);
opacity: 0;
}
@media (max-width: 768px) {
.custom-plan {
padding: 16px;
}
.form-row {
grid-template-columns: 1fr;
}
}
</style>

@ -227,7 +227,7 @@ watch(() => store.routeSegments, () => { updateRoute() }, { deep: true })
onMounted(() => { initMap() })
onUnmounted(() => { if (animationFrameId) cancelAnimationFrame(animationFrameId); if (map) map.remove() })
defineExpose({ animateCar })
defineExpose({ animateCar, updateRoute, clearRoute: clearRouteLines })
</script>
<style scoped>

File diff suppressed because it is too large Load Diff

@ -1,5 +1,5 @@
<template>
<div class="workbench" :class="{ 'edit-mode': store.mode === 'edit' }">
<div class="workbench">
<!-- Optimization Alert -->
<transition name="slide-down">
<div v-if="store.showOptimizationAlert" class="optimization-alert">
@ -13,60 +13,102 @@
</transition>
<!-- Top bar -->
<div class="workbench-header">
<div class="header-left">
<button class="back-btn" @click="goBack" title="返回方案预览"> 返回</button>
<h1>🚗 {{ itineraryTitle }}</h1>
<span class="subtitle">{{ itinerarySubtitle }}</span>
</div>
<div class="header-center">
<div class="stats-bar">
<span class="stat-item">总里程 <strong>{{ store.totalKm }}km</strong></span>
<span class="stat-item">驾驶 <strong>{{ store.totalDriveTime }}h</strong></span>
<span class="stat-item">天数 <strong>{{ store.travelDays }}</strong></span>
</div>
</div>
<div class="header-right">
<!-- Scheme switcher (only when quick schemes exist) -->
<div v-if="store.quickSchemes.length > 0" class="scheme-switcher-wrap">
<button class="switcher-btn" @click="schemeDropdownOpen = !schemeDropdownOpen">
方案 {{ getSchemeLabel(store.activeSchemeIndex) }}
</button>
<transition name="dropdown">
<div v-if="schemeDropdownOpen" class="scheme-dropdown">
<div class="dropdown-label">切换方案</div>
<div
<div class="wb-top">
<div class="wb-plan-selector" v-if="store.quickSchemes.length > 0">
<button
v-for="(s, i) in store.quickSchemes"
:key="'ws-' + i"
:class="['dropdown-item', { active: store.activeSchemeIndex === i }]"
:class="['wb-plan-btn', { active: store.activeSchemeIndex === i }]"
@click="switchScheme(i)"
>
<span class="item-badge">{{ getSchemeLabel(i) }}</span>
<span class="item-name">{{ s.name }}</span>
{{ getSchemeLabel(i) }}
</button>
</div>
<button class="wb-back-btn" @click="goBack"> </button>
</div>
</transition>
<!-- Main content -->
<div class="wb-wrap">
<!-- 左侧时间线 -->
<div class="wb-left">
<div class="wl-title">📋 行程时间线</div>
<div
v-for="(point, idx) in store.points"
:key="point.id"
:class="['wl-item', { active: idx === store.currentStep }]"
@click="handleClick(idx)"
>
<div class="wl-dot"></div>
<div>
<div class="wl-name">{{ point.icon }} {{ point.name }}</div>
<div class="wl-day">{{ point.day }} · {{ point.km }}</div>
</div>
<ModeToggle />
<button class="route-btn" @click="handleFetchRoutes" :disabled="store.isFetchingRoutes" title="获取真实路线">
{{ store.isFetchingRoutes ? '加载中...' : '🗺️ 真实路线' }}
</button>
<button class="export-btn" @click="handleExport" title="导出静态 HTML">📥 导出</button>
<button class="reset-btn" @click="store.resetToSample()" title="重置行程">🔄</button>
</div>
<div class="wl-nav">
<button class="prev" :disabled="store.currentStep <= 0" @click="handlePrev"> </button>
<button class="next" :disabled="store.currentStep >= store.points.length - 1" @click="handleNext"> </button>
</div>
</div>
<!-- Main content -->
<div class="workbench-body">
<div class="workbench-left">
<ReplacementPanel />
<TimelinePanel @animate="handleAnimate" @prevStep="handlePrevStep" @nextStep="handleNextStep" />
<!-- 中间详情面板 -->
<div class="wb-mid">
<template v-if="point">
<div class="wm-hero" v-if="point.heroImage">
<img :src="point.heroImage" :alt="point.name" @error="handleImageError" />
<div class="wm-ov">
<h3>{{ point.icon }} {{ point.name }}</h3>
<p>{{ point.desc }}</p>
</div>
</div>
<div class="wm-stats">
<div class="wm-stat">
<div class="ws-val">{{ point.km || '—' }}</div>
<div class="ws-lbl">行驶里程</div>
</div>
<div class="wm-stat">
<div class="ws-val">{{ point.driveTime || '—' }}</div>
<div class="ws-lbl">驾驶时间</div>
</div>
<div class="wm-stat">
<div class="ws-val">{{ point.day }}</div>
<div class="ws-lbl">第几天</div>
</div>
</div>
<div class="wm-sec" v-if="point.schedule.length">
<h4>📅 行程安排</h4>
<div class="wm-si" v-for="(s, i) in point.schedule" :key="i">
<span class="si-time">{{ s.time }}</span><span>{{ s.content }}</span>
</div>
</div>
<div class="wm-sec" v-if="point.foods && point.foods.length">
<h4>🍽 美食推荐</h4>
<div class="wm-foods">
<span v-for="(f, i) in point.foods" :key="i">{{ f }}</span>
</div>
</div>
<div class="workbench-middle">
<DetailPanel />
<div class="wm-sec" v-if="point.hotel">
<h4>🏨 住宿推荐</h4>
<div style="font-size:13px;color:#636e72">{{ point.hotel }}</div>
</div>
<div class="workbench-right">
<div class="wm-sec" v-if="point.tips">
<h4>💡 注意事项</h4>
<div class="wm-tips">{{ point.tips }}</div>
</div>
</template>
<div v-else class="empty-state">
<span class="empty-icon">🧭</span>
<p>选择一个行程节点查看详情</p>
</div>
</div>
<!-- 右侧地图 -->
<div class="wb-right">
<MapView ref="mapView" />
<div class="map-controls">
<button class="route-toggle" @click="toggleRoute" :class="{ active: showRoute }">
{{ showRoute ? '隐藏路线' : '显示路线' }}
</button>
</div>
</div>
</div>
</div>
@ -75,34 +117,13 @@
<script setup>
import { ref, computed } from 'vue'
import { useItineraryStore } from '../stores/itinerary'
import ModeToggle from './ModeToggle.vue'
import TimelinePanel from './TimelinePanel.vue'
import DetailPanel from './DetailPanel.vue'
import ReplacementPanel from './ReplacementPanel.vue'
import MapView from './MapView.vue'
import { sampleItinerary } from '../data/sampleData'
import { generateStaticHTML } from '../utils/exporter'
const store = useItineraryStore()
const mapView = ref(null)
const schemeDropdownOpen = ref(false)
const showRoute = ref(true)
// Use actual scheme data when available, fallback to mock
const itineraryTitle = computed(() => {
if (store.quickSchemes.length > 0 && store.activeSchemeIndex >= 0) {
const scheme = store.quickSchemes[store.activeSchemeIndex]
if (scheme) return scheme.name
}
return sampleItinerary.title
})
const itinerarySubtitle = computed(() => {
if (store.quickSchemes.length > 0 && store.activeSchemeIndex >= 0) {
const scheme = store.quickSchemes[store.activeSchemeIndex]
if (scheme) return scheme.route || `${scheme.days}天旅行规划`
}
return sampleItinerary.subtitle
})
const point = computed(() => store.currentPoint)
function getSchemeLabel(i) {
if (i < 0) return '?'
@ -110,130 +131,440 @@ function getSchemeLabel(i) {
}
function switchScheme(index) {
schemeDropdownOpen.value = false
store.loadSchemeByIndex(index)
}
const handleAnimate = (idx, fromStep) => { if (mapView.value) mapView.value.animateCar(idx, fromStep) }
const handlePrevStep = () => {
const handleClick = (idx) => {
const prevStep = store.currentStep
store.setCurrentStep(idx)
if (mapView.value) mapView.value.animateCar(idx, prevStep)
}
const handlePrev = () => {
const prevStep = store.currentStep
store.prevStep()
if (mapView.value) mapView.value.animateCar(store.currentStep, prevStep)
}
const handleNextStep = () => {
const handleNext = () => {
const prevStep = store.currentStep
store.nextStep()
if (mapView.value) mapView.value.animateCar(store.currentStep, prevStep)
}
const handleFetchRoutes = async () => { await store.fetchRealRoutes() }
const handleOptimize = () => { store.autoOptimizeRoute() }
const handleExport = () => {
const data = {
title: store.points[0] ? store.points[0].name.replace('(起点)', '') : '行程规划',
subtitle: itinerarySubtitle,
icon: '🚗',
points: store.points
const handleImageError = (e) => {
e.target.parentElement.style.background = 'linear-gradient(135deg, #6c5ce7, #a29bfe)'
e.target.style.display = 'none'
}
const html = generateStaticHTML(data)
const blob = new Blob([html], { type: 'text/html;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${data.title}_行程.html`
a.click()
URL.revokeObjectURL(url)
}
const handleOptimize = () => { store.autoOptimizeRoute() }
const goBack = () => {
store.setPlanningMode('quick')
store.setPhase('chat')
store.resetToModeSelection()
}
const toggleRoute = () => {
showRoute.value = !showRoute.value
if (mapView.value) {
if (showRoute.value) {
mapView.value.updateRoute()
} else {
mapView.value.clearRoute()
}
}
}
</script>
<style scoped>
.workbench { display: flex; flex-direction: column; height: 100vh; background: #f5f5f5; }
.workbench {
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f7fa;
}
/* Optimization Alert */
.optimization-alert {
position: fixed; top: 60px; left: 50%; transform: translateX(-50%);
background: #fff3cd; border: 1px solid #f4a261; border-radius: 12px;
padding: 12px 20px; z-index: 2000; display: flex; align-items: center; gap: 12px;
position: fixed;
top: 60px;
left: 50%;
transform: translateX(-50%);
background: #fff3cd;
border: 1px solid #f4a261;
border-radius: 12px;
padding: 12px 20px;
z-index: 2000;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
.alert-icon { font-size: 20px; }
.alert-msg { font-size: 13px; color: #856404; }
.alert-actions { display: flex; gap: 8px; }
.alert-btn { padding: 6px 14px; border: none; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 500; }
.alert-btn {
padding: 6px 14px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
}
.alert-btn.yes { background: #f4a261; color: #1b4332; }
.alert-btn.no { background: #e0e0e0; color: #666; }
.workbench-header {
display: flex; align-items: center; justify-content: space-between; padding: 10px 20px;
background: #1b4332; color: #fff; z-index: 10000; flex-shrink: 0;
/* Top bar */
.wb-top {
grid-column: 1/-1;
background: #fff;
padding: 12px 20px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: space-between;
margin: 12px 16px 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
.wb-plan-selector {
display: flex;
gap: 8px;
}
.wb-plan-btn {
padding: 6px 16px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
border: 1.5px solid #dfe6e9;
background: #fff;
color: #636e72;
transition: all 0.2s;
}
.wb-plan-btn.active {
background: #6c5ce7;
color: #fff;
border-color: #6c5ce7;
}
.wb-plan-btn:hover:not(.active) {
border-color: #6c5ce7;
color: #6c5ce7;
}
.wb-back-btn {
padding: 8px 16px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
border: 1.5px solid #6c5ce7;
background: #fff;
color: #6c5ce7;
transition: all 0.2s;
}
.wb-back-btn:hover {
background: #6c5ce7;
color: #fff;
}
/* Wrap layout */
.wb-wrap {
display: grid;
grid-template-columns: 220px 300px 1fr;
gap: 14px;
height: calc(100vh - 120px);
padding: 0 16px 16px;
}
/* Left sidebar */
.wb-left {
background: #2d3436;
border-radius: 12px;
padding: 16px;
overflow-y: auto;
}
.wl-title {
color: #fff;
font-size: 13px;
font-weight: 600;
margin-bottom: 14px;
}
.wl-item {
display: flex;
gap: 10px;
padding: 8px 6px;
cursor: pointer;
color: rgba(255,255,255,0.5);
transition: all 0.2s;
border-radius: 6px;
}
.wl-item:hover, .wl-item.active {
color: #fff;
background: rgba(108,92,231,0.3);
}
.wl-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(255,255,255,0.3);
margin-top: 7px;
}
.wl-item.active .wl-dot {
background: #6c5ce7;
}
.wl-name {
font-size: 13px;
}
.wl-day {
font-size: 11px;
opacity: 0.5;
}
.wl-nav {
margin-top: 16px;
display: flex;
gap: 8px;
}
.wl-nav button {
flex: 1;
padding: 8px;
border-radius: 8px;
border: none;
font-size: 12px;
cursor: pointer;
}
.wl-nav .prev {
background: rgba(255,255,255,0.1);
color: #fff;
}
.wl-nav .next {
background: #6c5ce7;
color: #fff;
}
.wl-nav button:disabled {
opacity: 0.3;
cursor: not-allowed;
}
/* Middle detail panel */
.wb-mid {
background: #fff;
border-radius: 12px;
overflow-y: auto;
}
.wm-hero {
height: 140px;
position: relative;
overflow: hidden;
}
.wm-hero img {
width: 100%;
height: 100%;
object-fit: cover;
}
.wm-ov {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 14px 16px;
background: linear-gradient(transparent, rgba(0,0,0,0.5));
color: #fff;
}
.wm-ov h3 {
font-size: 16px;
margin: 0 0 4px;
}
.wm-ov p {
font-size: 12px;
opacity: 0.8;
margin: 0;
}
.wm-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
padding: 14px;
}
.header-left h1 { margin: 0; font-size: 18px; }
.subtitle { font-size: 12px; color: rgba(255,255,255,0.6); }
.back-btn {
padding: 6px 12px; background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.3); color: #fff;
border-radius: 6px; cursor: pointer; font-size: 13px;
margin-right: 12px; transition: all 0.2s;
}
.back-btn:hover { background: rgba(255,255,255,0.2); }
.header-center { flex: 1; display: flex; justify-content: center; }
.stats-bar { display: flex; gap: 20px; }
.stat-item { font-size: 13px; color: rgba(255,255,255,0.7); }
.stat-item strong { color: #f4a261; margin-left: 4px; }
.scheme-switcher-wrap { position: relative; }
.switcher-btn {
padding: 6px 14px; background: rgba(255,255,255,0.15);
border: 1px solid rgba(255,255,255,0.3); color: #fff;
border-radius: 6px; cursor: pointer; font-size: 13px;
margin-right: 8px; transition: all 0.2s;
}
.switcher-btn:hover { background: rgba(255,255,255,0.25); }
.scheme-dropdown {
position: fixed; top: 52px; right: 240px;
background: #fff; border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.2);
min-width: 240px; padding: 10px; z-index: 100000;
border: 1px solid #e0e0e0;
}
.dropdown-label {
font-size: 11px; font-weight: 600; color: #999;
padding-bottom: 6px; border-bottom: 1px solid #f0f0f0;
.wm-stat {
text-align: center;
padding: 10px;
background: #f8f9fa;
border-radius: 10px;
}
.wm-stat .ws-val {
font-size: 15px;
font-weight: 700;
color: #6c5ce7;
}
.wm-stat .ws-lbl {
font-size: 11px;
color: #636e72;
}
.wm-sec {
padding: 14px 16px;
border-top: 1px solid #f0f0f0;
}
.wm-sec h4 {
font-size: 13px;
margin-bottom: 8px;
}
.wm-si {
display: flex;
gap: 8px;
font-size: 12px;
margin-bottom: 6px;
}
.dropdown-item {
display: flex; align-items: center; gap: 8px;
padding: 8px 10px; border-radius: 6px; cursor: pointer;
transition: background 0.15s;
}
.dropdown-item:hover { background: #f5f5f5; }
.dropdown-item.active { background: #fff3e0; }
.item-badge {
background: #e76f51; color: #fff; padding: 2px 8px;
border-radius: 4px; font-size: 11px; font-weight: 600;
}
.item-name { font-size: 13px; color: #333; }
.dropdown-enter-active, .dropdown-leave-active { transition: all 0.2s ease; }
.dropdown-enter-from, .dropdown-leave-to { opacity: 0; transform: translateY(-8px); }
.header-right { display: flex; align-items: center; gap: 8px; }
.reset-btn, .export-btn, .route-btn {
padding: 8px 12px; border: 1px solid rgba(255,255,255,0.2); color: #fff;
border-radius: 8px; cursor: pointer; font-size: 12px; transition: all 0.2s;
}
.reset-btn { background: rgba(255,255,255,0.1); }
.export-btn { background: #2a9d8f; border-color: #2a9d8f; }
.route-btn { background: rgba(42,157,143,0.3); }
.route-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.reset-btn:hover { background: rgba(255,255,255,0.2); }
.export-btn:hover { background: #21867a; }
.route-btn:hover:not(:disabled) { background: rgba(42,157,143,0.5); }
.workbench-body { flex: 1; display: flex; overflow: hidden; }
.workbench-left { width: 280px; flex-shrink: 0; position: relative; display: flex; }
.workbench-middle { width: 340px; flex-shrink: 0; border-left: 1px solid #e0e0e0; border-right: 1px solid #e0e0e0; overflow: hidden; }
.workbench-right { flex: 1; position: relative; }
.slide-down-enter-active, .slide-down-leave-active { transition: all 0.3s ease; }
.slide-down-enter-from, .slide-down-leave-to { transform: translateX(-50%) translateY(-20px); opacity: 0; }
.wm-si .si-time {
color: #6c5ce7;
font-weight: 600;
min-width: 32px;
}
.wm-foods, .wm-spots {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.wm-foods span, .wm-spots span {
font-size: 11px;
padding: 3px 8px;
border-radius: 5px;
background: #f0f0f5;
color: #636e72;
}
.wm-tips {
background: #fff8e1;
border-left: 3px solid #f39c12;
padding: 10px 12px;
border-radius: 0 8px 8px 0;
font-size: 12px;
color: #856404;
}
/* Right map */
.wb-right {
background: #dfe6e9;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #636e72;
font-size: 14px;
position: relative;
overflow: hidden;
}
.map-controls {
position: absolute;
top: 16px;
right: 16px;
z-index: 1000;
}
.route-toggle {
padding: 8px 16px;
border-radius: 8px;
border: none;
background: #fff;
color: #636e72;
font-size: 13px;
font-weight: 500;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
transition: all 0.2s;
}
.route-toggle:hover {
background: #6c5ce7;
color: #fff;
}
.route-toggle.active {
background: #6c5ce7;
color: #fff;
}
/* Empty state */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
}
.empty-icon {
font-size: 48px;
margin-bottom: 12px;
}
.empty-state p {
margin: 0;
font-size: 14px;
}
.slide-down-enter-active, .slide-down-leave-active {
transition: all 0.3s ease;
}
.slide-down-enter-from, .slide-down-leave-to {
transform: translateX(-50%) translateY(-20px);
opacity: 0;
}
@media (max-width: 1024px) {
.wb-wrap {
grid-template-columns: 180px 260px 1fr;
}
}
@media (max-width: 768px) {
.wb-wrap {
grid-template-columns: 1fr;
height: auto;
}
.wb-left {
max-height: 200px;
}
.wb-mid {
max-height: 400px;
}
.wb-right {
height: 300px;
}
}
</style>

@ -238,6 +238,10 @@ export async function chatWithAI(userMessage, onThinking = null) {
}
function parseAIResponse(content) {
if (!content || !content.trim()) {
throw new Error('AI 未返回任何内容')
}
// Remove markdown code block markers if present
let cleaned = content.trim()
@ -259,7 +263,7 @@ function parseAIResponse(content) {
throw new Error('AI 未返回有效的 JSON 数据')
}
const jsonStr = cleaned.substring(firstBrace, lastBrace + 1)
let jsonStr = cleaned.substring(firstBrace, lastBrace + 1)
try {
const parsed = JSON.parse(jsonStr)
@ -272,11 +276,28 @@ function parseAIResponse(content) {
return parsed
} catch (e) {
if (e.message.includes('缺少 schemes')) throw e
console.error('JSON parse error:', e)
console.error('Raw content:', cleaned.substring(0, 200))
// Try to fix common JSON issues
try {
// Fix unescaped newlines in string values
jsonStr = jsonStr.replace(/"([^"]*?)\n([^"]*?)"/g, '"$1\\n$2"')
// Remove trailing commas
jsonStr = jsonStr.replace(/,(\s*[}\]])/g, '$1')
const parsed = JSON.parse(jsonStr)
if (!parsed.schemes || !Array.isArray(parsed.schemes) || parsed.schemes.length === 0) {
throw new Error('AI 返回结果缺少 schemes')
}
return parsed
} catch (e2) {
console.error('JSON parse error (original):', e)
console.error('JSON parse error (after fix attempt):', e2)
console.error('Raw content (first 500 chars):', cleaned.substring(0, 500))
console.error('Raw content (last 500 chars):', cleaned.substring(Math.max(0, cleaned.length - 500)))
throw new Error('AI 返回的 JSON 格式有误,请重试')
}
}
}
// Shared streaming function
async function streamAIRequest(messages, onThinking = null) {
@ -347,11 +368,20 @@ async function streamAIRequest(messages, onThinking = null) {
}
function parseJSONFromContent(content) {
if (!content || !content.trim()) {
throw new Error('AI 未返回任何内容')
}
let cleaned = content.trim()
// Remove markdown code block markers
const codeBlockMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/)
if (codeBlockMatch) cleaned = codeBlockMatch[1].trim()
// Remove <thinking> tags
cleaned = cleaned.replace(/<thinking>[\s\S]*?<\/thinking>/g, '').trim()
// Find JSON boundaries
const firstBrace = cleaned.indexOf('{')
const lastBrace = cleaned.lastIndexOf('}')
@ -359,14 +389,30 @@ function parseJSONFromContent(content) {
throw new Error('AI 未返回有效的 JSON 数据')
}
const jsonStr = cleaned.substring(firstBrace, lastBrace + 1)
let jsonStr = cleaned.substring(firstBrace, lastBrace + 1)
try {
return JSON.parse(jsonStr)
} catch (e) {
console.error('JSON parse error:', e)
// Try to fix common JSON issues
try {
// Fix unescaped newlines in string values
jsonStr = jsonStr.replace(/"([^"]*?)\n([^"]*?)"/g, '"$1\\n$2"')
// Fix unescaped quotes within strings
jsonStr = jsonStr.replace(/:"([^"]*?)"([^,}\]])/g, ':"$1"$2')
// Remove trailing commas
jsonStr = jsonStr.replace(/,(\s*[}\]])/g, '$1')
return JSON.parse(jsonStr)
} catch (e2) {
console.error('JSON parse error (original):', e)
console.error('JSON parse error (after fix attempt):', e2)
console.error('Raw content (first 500 chars):', cleaned.substring(0, 500))
console.error('Raw content (last 500 chars):', cleaned.substring(Math.max(0, cleaned.length - 500)))
throw new Error('AI 返回的 JSON 格式有误,请重试')
}
}
}
export async function quickPlan(userInput, onThinking = null) {
const userMessage = `目的地:${userInput.destination}

@ -1,40 +1,37 @@
<template>
<div class="home-page">
<!-- Top Navigation Bar -->
<nav class="top-bar">
<div class="brand"><span class="icon"></span> 智能行程规划</div>
<button v-if="!showModeSelection" class="home-btn" @click="goHome"></button>
</nav>
<!-- Main Content -->
<div class="container">
<!-- Mode selection page -->
<div v-if="showModeSelection" class="mode-selection">
<div class="selection-header">
<div v-if="showModeSelection" class="home-section">
<div class="home-header">
<h1>智能行程规划</h1>
<p>选择一种方式开始你的旅行规划</p>
</div>
<div class="mode-cards">
<div class="mode-card chat-mode" @click="selectMode('chat')">
<div class="mode-icon">💬</div>
<h3>聊天问答式</h3>
<p>通过对话交流我逐步了解你的需求为你量身定制完美行程</p>
<div class="mode-features">
<span>多轮对话</span>
<span>逐步收集信息</span>
<span>个性化规划</span>
</div>
</div>
<div class="mode-card quick-mode" @click="selectMode('quick')">
<div class="mode-icon"></div>
<div class="mode-grid">
<div class="mode-card quick-card" @click="selectMode('quick')">
<div class="mc-icon"></div>
<h3>快速规划</h3>
<p>输入目的地和天数一键生成多个旅行方案供你选择</p>
<div class="mode-features">
<span>快速生成</span>
<span>多方案对比</span>
<span>即输即得</span>
<div class="mc-tags"><span class="mc-tag">快速生成</span><span class="mc-tag">多方案对比</span><span class="mc-tag">即输即得</span></div>
</div>
<div class="mode-card chat-card" @click="selectMode('chat')">
<div class="mc-icon"></div>
<h3>聊天问答式</h3>
<p>通过对话交流我逐步了解你的需求为你量身定制完美行程</p>
<div class="mc-tags"><span class="mc-tag">多轮对话</span><span class="mc-tag">逐步收集信息</span><span class="mc-tag">个性化规划</span></div>
</div>
<div class="mode-card custom-mode" @click="selectMode('custom')">
<div class="mode-icon"></div>
<div class="mode-card custom-card" @click="selectMode('custom')">
<div class="mc-icon"></div>
<h3>自定义行程</h3>
<p>输入你已规划的行程AI 评估合理性并给出优化建议</p>
<div class="mode-features">
<span>行程评估</span>
<span>智能建议</span>
<span>优化方案</span>
</div>
<div class="mc-tags"><span class="mc-tag">行程评估</span><span class="mc-tag">智能建议</span><span class="mc-tag">优化方案</span></div>
</div>
</div>
</div>
@ -45,10 +42,13 @@
<CustomPlanPanel v-else-if="store.phase === 'chat' && store.planningMode === 'custom'" />
<ComparisonView v-else-if="store.phase === 'comparison'" />
<Workbench v-else />
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useItineraryStore } from '../stores/itinerary'
import ChatInterface from '../components/ChatInterface.vue'
import ComparisonView from '../components/ComparisonView.vue'
@ -57,8 +57,8 @@ import QuickPlanPanel from '../components/QuickPlanPanel.vue'
import CustomPlanPanel from '../components/CustomPlanPanel.vue'
const store = useItineraryStore()
const router = useRouter()
// Show mode selection only when no planning has been started
const showModeSelection = computed(() => {
return store.phase === 'chat' && !store.planningMode
})
@ -66,111 +66,195 @@ const showModeSelection = computed(() => {
const selectMode = (mode) => {
store.setPlanningMode(mode)
}
const goHome = () => {
store.resetToModeSelection()
}
</script>
<style scoped>
.mode-selection {
.home-page {
min-height: 100vh;
background: #f5f7fa;
display: flex;
flex-direction: column;
min-height: 100vh;
background: #f5f5f5;
}
.selection-header {
text-align: center;
padding: 60px 20px 40px;
background: linear-gradient(135deg, #1b4332, #2d6a4f);
/* Top Bar */
.top-bar {
background: #fff;
padding: 12px 32px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
position: sticky;
top: 0;
z-index: 100;
}
.top-bar .brand {
display: flex;
align-items: center;
gap: 10px;
font-size: 17px;
font-weight: 700;
color: #2d3436;
}
.top-bar .brand .icon::before {
content: '';
display: inline-block;
width: 24px;
height: 24px;
background: url('/favicon.svg') no-repeat center;
background-size: contain;
}
.home-btn {
padding: 8px 16px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
border: 1.5px solid #6c5ce7;
background: #fff;
color: #6c5ce7;
transition: all 0.2s;
font-weight: 500;
}
.home-btn:hover {
background: #6c5ce7;
color: #fff;
}
.selection-header h1 {
margin: 0 0 10px;
font-size: 32px;
/* Container */
.container {
flex: 1;
}
/* Home Section */
.home-section {
max-width: 1100px;
margin: 0 auto;
padding: 24px 32px;
}
.home-header {
text-align: center;
padding: 48px 0 36px;
}
.home-header h1 {
font-size: 28px;
font-weight: 800;
color: #2d3436;
margin-bottom: 8px;
}
.selection-header p {
margin: 0;
opacity: 0.85;
font-size: 16px;
.home-header p {
color: #636e72;
font-size: 15px;
}
.mode-cards {
/* Mode Grid */
.mode-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
max-width: 1000px;
margin: -40px auto 40px;
padding: 0 20px;
gap: 20px;
margin-top: 28px;
}
.mode-card {
background: #fff;
border-radius: 16px;
padding: 30px 24px;
border-radius: 14px;
padding: 28px 22px;
cursor: pointer;
transition: all 0.3s;
transition: all 0.25s;
border: 2px solid transparent;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
}
.mode-card:hover {
transform: translateY(-6px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
transform: translateY(-3px);
box-shadow: 0 8px 24px rgba(108,92,231,0.12);
border-color: #6c5ce7;
}
.chat-mode:hover { border-color: #2a9d8f; }
.quick-mode:hover { border-color: #e76f51; }
.custom-mode:hover { border-color: #264653; }
.mc-icon {
width: 56px;
height: 56px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
margin-bottom: 14px;
}
.mode-icon {
font-size: 48px;
margin-bottom: 16px;
.quick-card .mc-icon {
background: #e8e4ff;
}
.mode-card h3 {
margin: 0 0 10px;
font-size: 20px;
color: #333;
.quick-card .mc-icon::before {
content: '⚡';
}
.mode-card > p {
margin: 0 0 16px;
font-size: 14px;
color: #666;
line-height: 1.6;
.chat-card .mc-icon {
background: #fff3e0;
}
.mode-features {
display: flex;
flex-wrap: wrap;
gap: 8px;
.chat-card .mc-icon::before {
content: '💬';
}
.mode-features span {
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
.custom-card .mc-icon {
background: #e0f7fa;
}
.chat-mode .mode-features span {
background: #e8f5e9;
color: #2a9d8f;
.custom-card .mc-icon::before {
content: '✏️';
}
.quick-mode .mode-features span {
background: #fff3e0;
color: #e76f51;
.mode-card h3 {
font-size: 17px;
font-weight: 700;
margin-bottom: 6px;
color: #2d3436;
}
.mode-card p {
font-size: 13px;
color: #636e72;
margin-bottom: 14px;
min-height: 36px;
}
.mc-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.custom-mode .mode-features span {
background: #e0f2f1;
color: #264653;
.mc-tag {
font-size: 11px;
padding: 3px 10px;
border-radius: 12px;
background: #f0f0f5;
color: #636e72;
}
@media (max-width: 768px) {
.mode-cards {
.mode-grid {
grid-template-columns: 1fr;
}
.top-bar {
padding: 12px 16px;
}
.home-section {
padding: 24px 16px;
}
}
</style>

@ -220,22 +220,23 @@ onMounted(async () => {
display: flex;
align-items: center;
padding: 12px 20px;
background: #1b4332;
color: #fff;
background: #fff;
color: #2d3436;
gap: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.back-btn {
background: rgba(255,255,255,0.1);
background: #f5f7fa;
border: none;
color: #fff;
color: #636e72;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
}
.back-btn:hover { background: rgba(255,255,255,0.2); }
.back-btn:hover { background: #e8e8e8; }
.settings-header h1 {
margin: 0;
@ -278,9 +279,9 @@ onMounted(async () => {
align-items: center;
gap: 12px;
padding: 16px;
border: 2px solid #2a9d8f;
border: 2px solid #6c5ce7;
border-radius: 10px;
background: #f0f7f4;
background: #f0f0f5;
flex: 1;
}
@ -288,7 +289,7 @@ onMounted(async () => {
.provider-info { flex: 1; }
.provider-name { display: block; font-weight: 600; color: #333; }
.provider-desc { display: block; font-size: 12px; color: #888; }
.check-mark { color: #2a9d8f; font-size: 20px; font-weight: bold; }
.check-mark { color: #6c5ce7; font-size: 20px; font-weight: bold; }
/* Input */
.input-group { display: flex; flex-direction: column; gap: 8px; }
@ -302,7 +303,7 @@ onMounted(async () => {
transition: border-color 0.2s;
}
.input-wrapper:focus-within { border-color: #2a9d8f; }
.input-wrapper:focus-within { border-color: #6c5ce7; }
.api-input, .text-input {
flex: 1;
@ -328,7 +329,7 @@ onMounted(async () => {
}
.input-hint a {
color: #2a9d8f;
color: #6c5ce7;
text-decoration: none;
}
@ -349,11 +350,11 @@ onMounted(async () => {
transition: all 0.2s;
}
.model-card:hover { border-color: #b0d4c8; }
.model-card:hover { border-color: #b8b0f0; }
.model-card.selected {
border-color: #2a9d8f;
background: #f0f7f4;
border-color: #6c5ce7;
background: #f0f0f5;
}
.model-header {
@ -374,7 +375,7 @@ onMounted(async () => {
}
.model-radio.checked {
border-color: #2a9d8f;
border-color: #6c5ce7;
}
.model-radio.checked::after {
@ -382,7 +383,7 @@ onMounted(async () => {
width: 10px;
height: 10px;
border-radius: 50%;
background: #2a9d8f;
background: #6c5ce7;
}
.model-name { font-weight: 500; color: #333; }
@ -434,7 +435,7 @@ onMounted(async () => {
width: 16px;
height: 16px;
border-radius: 50%;
background: #2a9d8f;
background: #6c5ce7;
cursor: pointer;
}
@ -442,7 +443,7 @@ onMounted(async () => {
min-width: 40px;
text-align: right;
font-weight: 600;
color: #2a9d8f;
color: #6c5ce7;
}
.item-desc { font-size: 11px; color: #aaa; }
@ -471,8 +472,8 @@ onMounted(async () => {
.test-btn:hover:not(:disabled) { background: #bbdefb; }
.test-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.save-btn { background: #2a9d8f; color: #fff; }
.save-btn:hover { background: #21867a; }
.save-btn { background: #6c5ce7; color: #fff; }
.save-btn:hover { background: #5b4cdb; }
.reset-btn { background: #f5f5f5; color: #666; }
.reset-btn:hover { background: #e0e0e0; }

Loading…
Cancel
Save