forked from zhouruizhe/gmTouringMiniApp
Initial commit: gmTouringMiniApp project
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||
CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||
</script>
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+13773
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "uni-preset-vue",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev:custom": "uni -p",
|
||||
"dev:h5": "uni",
|
||||
"dev:h5:ssr": "uni --ssr",
|
||||
"dev:mp-alipay": "uni -p mp-alipay",
|
||||
"dev:mp-baidu": "uni -p mp-baidu",
|
||||
"dev:mp-jd": "uni -p mp-jd",
|
||||
"dev:mp-kuaishou": "uni -p mp-kuaishou",
|
||||
"dev:mp-lark": "uni -p mp-lark",
|
||||
"dev:mp-qq": "uni -p mp-qq",
|
||||
"dev:mp-toutiao": "uni -p mp-toutiao",
|
||||
"dev:mp-harmony": "uni -p mp-harmony",
|
||||
"dev:mp-weixin": "uni -p mp-weixin",
|
||||
"dev:mp-xhs": "uni -p mp-xhs",
|
||||
"dev:quickapp-webview": "uni -p quickapp-webview",
|
||||
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
|
||||
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
|
||||
"build:custom": "uni build -p",
|
||||
"build:h5": "uni build",
|
||||
"build:h5:ssr": "uni build --ssr",
|
||||
"build:mp-alipay": "uni build -p mp-alipay",
|
||||
"build:mp-baidu": "uni build -p mp-baidu",
|
||||
"build:mp-jd": "uni build -p mp-jd",
|
||||
"build:mp-kuaishou": "uni build -p mp-kuaishou",
|
||||
"build:mp-lark": "uni build -p mp-lark",
|
||||
"build:mp-qq": "uni build -p mp-qq",
|
||||
"build:mp-toutiao": "uni build -p mp-toutiao",
|
||||
"build:mp-harmony": "uni build -p mp-harmony",
|
||||
"build:mp-weixin": "uni build -p mp-weixin",
|
||||
"build:mp-xhs": "uni build -p mp-xhs",
|
||||
"build:quickapp-webview": "uni build -p quickapp-webview",
|
||||
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
|
||||
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-app-harmony": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-app-plus": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-components": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-h5": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-mp-alipay": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-mp-baidu": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-mp-harmony": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-mp-jd": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-mp-kuaishou": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-mp-lark": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-mp-qq": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-mp-toutiao": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-mp-xhs": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-quickapp-webview": "3.0.0-5010520260709002",
|
||||
"vue": "^3.4.21",
|
||||
"vue-i18n": "^9.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/types": "^3.4.8",
|
||||
"@dcloudio/uni-automator": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-5010520260709002",
|
||||
"@dcloudio/uni-stacktracey": "3.0.0-5010520260709002",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-5010520260709002",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"@vue/runtime-core": "^3.4.21",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "5.2.8",
|
||||
"vue-tsc": "^1.0.24"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"setting": {
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"minified": true,
|
||||
"uglifyFileName": false,
|
||||
"enhance": true,
|
||||
"packNpmRelationList": [],
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
},
|
||||
"useCompilerPlugins": false,
|
||||
"minifyWXML": true
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"simulatorPluginLibVersion": {},
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"appid": "wx2a49d023ec320d0b",
|
||||
"editorSetting": {}
|
||||
}
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
/// <reference types='@dcloudio/types' />
|
||||
import 'vue'
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
type Hooks = App.AppInstance & Page.PageInstance;
|
||||
|
||||
interface ComponentCustomOptions extends Hooks {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
|
||||
onLaunch(() => {
|
||||
console.log("App Launch");
|
||||
});
|
||||
onShow(() => {
|
||||
console.log("App Show");
|
||||
});
|
||||
onHide(() => {
|
||||
console.log("App Hide");
|
||||
});
|
||||
</script>
|
||||
<style></style>
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { APIError, PlanResponse, TravelPreferences } from '@/types/travel'
|
||||
|
||||
// H5 dev uses the Vite /api proxy; mini-program builds should inject an HTTPS base URL.
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''
|
||||
|
||||
const request = <T>(options: UniApp.RequestOptions): Promise<T> =>
|
||||
new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
...options,
|
||||
success: (response) => {
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
resolve(response.data as T)
|
||||
return
|
||||
}
|
||||
const error = response.data as APIError
|
||||
reject(new Error(error?.detail || `请求失败(${response.statusCode})`))
|
||||
},
|
||||
fail: () => {
|
||||
reject(new Error('无法连接行程服务,请检查后端是否已启动'))
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
export const createPlan = (preferences: TravelPreferences) =>
|
||||
request<PlanResponse>({
|
||||
url: `${API_BASE_URL}/api/v1/plans`,
|
||||
method: 'POST',
|
||||
data: preferences,
|
||||
header: { 'content-type': 'application/json' },
|
||||
timeout: 50000,
|
||||
})
|
||||
|
||||
export const adjustPlan = (conversationId: string, message: string) =>
|
||||
request<PlanResponse>({
|
||||
url: `${API_BASE_URL}/api/v1/conversations/${conversationId}/messages`,
|
||||
method: 'POST',
|
||||
data: { message },
|
||||
header: { 'content-type': 'application/json' },
|
||||
timeout: 50000,
|
||||
})
|
||||
|
||||
export const PLAN_STORAGE_KEY = 'guangming:last-plan'
|
||||
export const PREFERENCES_STORAGE_KEY = 'guangming:last-preferences'
|
||||
|
||||
export const savePlan = (plan: PlanResponse) => {
|
||||
uni.setStorageSync(PLAN_STORAGE_KEY, plan)
|
||||
}
|
||||
|
||||
export const loadPlan = (): PlanResponse | null => {
|
||||
const value = uni.getStorageSync(PLAN_STORAGE_KEY)
|
||||
return value && typeof value === 'object' ? (value as PlanResponse) : null
|
||||
}
|
||||
|
||||
export const savePreferences = (preferences: TravelPreferences) => {
|
||||
uni.setStorageSync(PREFERENCES_STORAGE_KEY, preferences)
|
||||
}
|
||||
|
||||
export const loadPreferences = (): TravelPreferences | null => {
|
||||
const value = uni.getStorageSync(PREFERENCES_STORAGE_KEY)
|
||||
return value && typeof value === 'object' ? (value as TravelPreferences) : null
|
||||
}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createSSRApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App);
|
||||
return {
|
||||
app,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name" : "",
|
||||
"appid" : "",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
"compilerVersion" : 3,
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
/* 模块配置 */
|
||||
"modules" : {},
|
||||
/* 应用发布信息 */
|
||||
"distribute" : {
|
||||
/* android打包配置 */
|
||||
"android" : {
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "wx2a49d023ec320d0b",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"uniStatistics": {
|
||||
"enable": false
|
||||
},
|
||||
"vueVersion" : "3"
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "逐光 · 光明区"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/planner/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "定制光明行程"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/itinerary/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "我的光明行程"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "逐光文旅",
|
||||
"navigationBarBackgroundColor": "#1D392C",
|
||||
"backgroundColor": "#F1EEE5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="hero">
|
||||
<view class="orb orb-one" />
|
||||
<view class="orb orb-two" />
|
||||
<view class="grid" />
|
||||
|
||||
<view class="topbar">
|
||||
<view class="brand">
|
||||
<text class="brand-cn">逐 光</text>
|
||||
<text class="brand-en">GUANGMING FIELD GUIDE</text>
|
||||
</view>
|
||||
<view class="district-pill">深圳 · 光明区</view>
|
||||
</view>
|
||||
|
||||
<view class="hero-copy">
|
||||
<text class="eyebrow">SCIENCE × NATURE × CITY</text>
|
||||
<text class="hero-title">在光明,<br />遇见城市的<br /><text class="accent">另一种呼吸</text></text>
|
||||
<text class="hero-body">从未来科技馆到都市田园,一天之内切换科学、自然与生活。</text>
|
||||
<view class="hero-action" @click="makePlan">
|
||||
<text>定制我的光明行程</text>
|
||||
<text class="arrow">↗</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="coordinate">
|
||||
<text>22.748° N</text>
|
||||
<view class="coordinate-line" />
|
||||
<text>113.935° E</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="content">
|
||||
<view class="intro">
|
||||
<text class="section-kicker">WHY GUANGMING</text>
|
||||
<text class="intro-title">一座把未来种进田野的城区</text>
|
||||
<text class="intro-body">这里有科学城的银色飞船,也有四季变化的田园花景。沿着绿道慢慢走,城市的锋芒与自然的松弛恰好相遇。</text>
|
||||
<view class="fact-row">
|
||||
<view class="fact">
|
||||
<text class="fact-value">½—1</text>
|
||||
<text class="fact-label">推荐游玩天数</text>
|
||||
</view>
|
||||
<view class="fact">
|
||||
<text class="fact-value">15</text>
|
||||
<text class="fact-label">POC精选地点</text>
|
||||
</view>
|
||||
<view class="fact">
|
||||
<text class="fact-value">AI</text>
|
||||
<text class="fact-label">按偏好编排行程</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-head">
|
||||
<view>
|
||||
<text class="section-kicker">THREE WAYS TO SEE</text>
|
||||
<text class="section-title">三种光明</text>
|
||||
</view>
|
||||
<text class="index-mark">03</text>
|
||||
</view>
|
||||
|
||||
<view class="story-list">
|
||||
<view v-for="(story, index) in stories" :key="story.title" class="story" :class="`story-${index + 1}`">
|
||||
<view class="story-art">
|
||||
<view class="story-shape" />
|
||||
<text class="story-number">0{{ index + 1 }}</text>
|
||||
</view>
|
||||
<view class="story-copy">
|
||||
<text class="story-tag">{{ story.tag }}</text>
|
||||
<text class="story-title">{{ story.title }}</text>
|
||||
<text class="story-desc">{{ story.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="planner-card" @click="makePlan">
|
||||
<view class="planner-top">
|
||||
<text class="planner-label">AI TRIP MAKER</text>
|
||||
<text class="spark">✦</text>
|
||||
</view>
|
||||
<text class="planner-title">不照搬攻略,<br />只生成适合你的光明。</text>
|
||||
<view class="choice-preview">
|
||||
<text>亲子</text><text>半日</text><text>轻松</text><text>自然</text>
|
||||
</view>
|
||||
<view class="planner-button">
|
||||
<text>开始选择偏好</text>
|
||||
<text>→</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="source-note">地点资料基于光明区政府及场馆公开信息整理,动态开放与预约信息请出行前确认。</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const stories = [
|
||||
{ tag: 'FUTURE / 未来', title: '科学城的银色飞船', desc: '走进深圳科学技术馆,在互动展项里触摸数字文明。' },
|
||||
{ tag: 'FIELD / 田野', title: '都市里的四季田园', desc: '在欢乐田园等风吹过稻浪,把时间交还给自然。' },
|
||||
{ tag: 'GREEN / 漫游', title: '桥与绿道之间', desc: '沿虹桥与山林步道散步,看城市向绿色深处延伸。' },
|
||||
]
|
||||
|
||||
const makePlan = () => {
|
||||
uni.navigateTo({ url: '/pages/planner/index' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page { background: #efeede; }
|
||||
.page { min-height: 100vh; color: #17382f; background: #efeede; }
|
||||
.hero { position: relative; height: 1180rpx; overflow: hidden; color: #f5f1da; background: linear-gradient(145deg, #123d35 0%, #0b2825 62%, #071d1c 100%); }
|
||||
.grid { position: absolute; inset: 0; opacity: .15; background-image: linear-gradient(rgba(255,255,255,.22) 1rpx, transparent 1rpx), linear-gradient(90deg, rgba(255,255,255,.22) 1rpx, transparent 1rpx); background-size: 86rpx 86rpx; transform: perspective(500rpx) rotateX(62deg) scale(1.5); transform-origin: bottom; }
|
||||
.orb { position: absolute; border-radius: 50%; filter: blur(2rpx); }
|
||||
.orb-one { width: 620rpx; height: 620rpx; right: -240rpx; top: 130rpx; border: 1rpx solid rgba(208,255,101,.5); box-shadow: 0 0 120rpx rgba(174,235,55,.13) inset; }
|
||||
.orb-two { width: 190rpx; height: 190rpx; right: 58rpx; top: 330rpx; background: #c5ef55; box-shadow: 0 0 100rpx rgba(197,239,85,.35); }
|
||||
.topbar { position: relative; z-index: 2; display: flex; justify-content: space-between; align-items: flex-start; padding: calc(34rpx + env(safe-area-inset-top)) 38rpx 0; }
|
||||
.brand { display: flex; flex-direction: column; gap: 4rpx; }
|
||||
.brand-cn { font-family: 'STSong','Songti SC',serif; font-size: 30rpx; font-weight: 700; letter-spacing: 14rpx; }
|
||||
.brand-en { font-size: 13rpx; letter-spacing: 3rpx; opacity: .6; }
|
||||
.district-pill { flex-shrink: 0; padding: 11rpx 15rpx; border: 1rpx solid rgba(255,255,255,.3); border-radius: 999rpx; font-size: 18rpx; letter-spacing: 1rpx; }
|
||||
.hero-copy { position: absolute; z-index: 2; left: 42rpx; right: 42rpx; bottom: 130rpx; }
|
||||
.eyebrow,.section-kicker { display: block; color: #a5c83d; font-size: 18rpx; letter-spacing: 5rpx; }
|
||||
.hero-title { display: block; margin-top: 32rpx; font-family: 'STSong','Songti SC',serif; font-size: 78rpx; line-height: 1.2; font-weight: 700; letter-spacing: 2rpx; }
|
||||
.accent { color: #c8ed5d; font-style: italic; }
|
||||
.hero-body { display: block; width: 560rpx; margin-top: 28rpx; color: rgba(245,241,218,.7); font-size: 27rpx; line-height: 1.8; }
|
||||
.hero-action { display: flex; justify-content: space-between; align-items: center; width: 570rpx; margin-top: 40rpx; padding: 27rpx 30rpx; box-sizing: border-box; color: #102f29; background: #c8ed5d; font-size: 27rpx; font-weight: 700; }
|
||||
.arrow { font-size: 38rpx; }
|
||||
.coordinate { position: absolute; z-index: 2; right: -84rpx; bottom: 325rpx; display: flex; align-items: center; gap: 14rpx; color: rgba(255,255,255,.42); font-size: 15rpx; letter-spacing: 2rpx; transform: rotate(90deg); }
|
||||
.coordinate-line { width: 80rpx; height: 1rpx; background: rgba(255,255,255,.3); }
|
||||
.content { padding: 0 36rpx 100rpx; background: radial-gradient(circle at 90% 10%, rgba(200,237,93,.16), transparent 340rpx), #efeede; }
|
||||
.intro { padding: 84rpx 6rpx 78rpx; border-bottom: 1rpx solid rgba(23,56,47,.16); }
|
||||
.intro-title,.section-title { display: block; font-family: 'STSong','Songti SC',serif; font-weight: 700; }
|
||||
.intro-title { margin-top: 22rpx; font-size: 51rpx; line-height: 1.35; }
|
||||
.intro-body { display: block; margin-top: 25rpx; color: #4f635b; font-family: 'STSong','Songti SC',serif; font-size: 28rpx; line-height: 1.95; }
|
||||
.fact-row { display: flex; margin-top: 48rpx; }
|
||||
.fact { flex: 1; display: flex; flex-direction: column; gap: 9rpx; }
|
||||
.fact-value { font-family: Georgia,serif; font-size: 37rpx; color: #17382f; }
|
||||
.fact-label { font-size: 19rpx; color: #718078; }
|
||||
.section-head { display: flex; justify-content: space-between; align-items: flex-end; padding: 78rpx 6rpx 38rpx; }
|
||||
.section-title { margin-top: 12rpx; font-size: 54rpx; }
|
||||
.index-mark { font-family: Georgia,serif; font-style: italic; font-size: 68rpx; color: rgba(23,56,47,.17); }
|
||||
.story-list { display: flex; flex-direction: column; gap: 24rpx; }
|
||||
.story { display: flex; min-height: 260rpx; background: #f7f4e8; box-shadow: 0 18rpx 50rpx rgba(19,52,44,.07); }
|
||||
.story-art { position: relative; width: 250rpx; overflow: hidden; background: #173d35; }
|
||||
.story-shape { position: absolute; width: 200rpx; height: 200rpx; border-radius: 50%; right: -70rpx; bottom: -54rpx; background: #c7eb5d; }
|
||||
.story-2 .story-art { background: #ca6b3d; }
|
||||
.story-2 .story-shape { width: 250rpx; height: 110rpx; border-radius: 50% 50% 0 0; right: -35rpx; bottom: 0; background: #f2c85d; }
|
||||
.story-3 .story-art { background: #244f48; }
|
||||
.story-3 .story-shape { width: 280rpx; height: 50rpx; right: -20rpx; bottom: 76rpx; border-radius: 99rpx; background: #df593f; transform: rotate(-26deg); }
|
||||
.story-number { position: absolute; left: 22rpx; top: 20rpx; color: rgba(255,255,255,.75); font-family: Georgia,serif; font-size: 23rpx; }
|
||||
.story-copy { flex: 1; display: flex; flex-direction: column; justify-content: center; padding: 28rpx; }
|
||||
.story-tag { color: #b05e39; font-size: 17rpx; letter-spacing: 3rpx; }
|
||||
.story-title { margin-top: 13rpx; font-family: 'STSong','Songti SC',serif; font-size: 33rpx; font-weight: 700; }
|
||||
.story-desc { margin-top: 13rpx; color: #68776f; font-size: 22rpx; line-height: 1.65; }
|
||||
.planner-card { margin-top: 90rpx; padding: 40rpx; color: #f3f0dc; background: #15372f; box-shadow: 18rpx 18rpx 0 #c8ed5d; }
|
||||
.planner-top { display: flex; align-items: center; justify-content: space-between; }
|
||||
.planner-label { color: #c8ed5d; font-size: 18rpx; letter-spacing: 4rpx; }
|
||||
.spark { color: #c8ed5d; font-size: 45rpx; }
|
||||
.planner-title { display: block; margin-top: 30rpx; font-family: 'STSong','Songti SC',serif; font-size: 47rpx; line-height: 1.45; font-weight: 700; }
|
||||
.choice-preview { display: flex; flex-wrap: wrap; gap: 12rpx; margin-top: 34rpx; }
|
||||
.choice-preview text { padding: 12rpx 18rpx; border: 1rpx solid rgba(255,255,255,.28); border-radius: 999rpx; font-size: 20rpx; }
|
||||
.planner-button { display: flex; justify-content: space-between; margin-top: 36rpx; padding-top: 28rpx; border-top: 1rpx solid rgba(255,255,255,.2); color: #c8ed5d; font-size: 25rpx; }
|
||||
.source-note { display: block; margin: 72rpx 16rpx 0; color: #87928b; font-size: 19rpx; line-height: 1.7; text-align: center; }
|
||||
|
||||
/* AI travel assistant visual system */
|
||||
page,.page { background: #f4f8f6; }
|
||||
.page { color: #17221d; font-family: 'PingFang SC','Microsoft YaHei',sans-serif; }
|
||||
.hero { height: 1020rpx; color: #17221d; background: radial-gradient(circle at 90% 28%, rgba(8,156,92,.14), transparent 360rpx), linear-gradient(180deg,#fff 0%,#f3faf6 100%); }
|
||||
.grid { opacity: .28; background-image: linear-gradient(rgba(7,142,85,.1) 1rpx, transparent 1rpx),linear-gradient(90deg,rgba(7,142,85,.1) 1rpx,transparent 1rpx); }
|
||||
.orb-one { border-color: rgba(7,156,92,.22); box-shadow: 0 0 120rpx rgba(7,156,92,.08) inset; }
|
||||
.orb-two { background: #0a9b5d; box-shadow: 0 0 80rpx rgba(7,156,92,.22); }
|
||||
.topbar { padding-top: calc(28rpx + env(safe-area-inset-top)); }
|
||||
.brand-cn { color: #087b4a; }
|
||||
.brand-en { color: #73827a; opacity: 1; }
|
||||
.district-pill { color: #087b4a; border-color: #b9d9c9; background: rgba(255,255,255,.75); }
|
||||
.hero-copy { bottom: 100rpx; }
|
||||
.eyebrow,.section-kicker { color: #078e55; }
|
||||
.hero-title { font-size: 72rpx; }
|
||||
.accent { color: #078e55; font-style: normal; }
|
||||
.hero-body { color: #68766f; }
|
||||
.hero-action { width: 590rpx; border-radius: 20rpx; color: #fff; background: linear-gradient(100deg,#079c5c,#067d4a); box-shadow: 0 14rpx 32rpx rgba(3,129,75,.24); }
|
||||
.coordinate { color: rgba(7,96,57,.4); }
|
||||
.coordinate-line { background: rgba(7,96,57,.24); }
|
||||
.content { background: #f4f8f6; }
|
||||
.intro { margin-top: 28rpx; padding: 58rpx 34rpx; border: 2rpx solid #dce9e2; border-radius: 30rpx; background: #fff; box-shadow: 0 18rpx 48rpx rgba(19,75,50,.07); }
|
||||
.intro-title,.section-title,.story-title,.planner-title { font-family: 'PingFang SC','Microsoft YaHei',sans-serif; }
|
||||
.intro-body { color: #617068; font-family: 'PingFang SC','Microsoft YaHei',sans-serif; font-size: 25rpx; }
|
||||
.story { border: 2rpx solid #e0e9e4; border-radius: 24rpx; overflow: hidden; background: #fff; box-shadow: 0 12rpx 34rpx rgba(19,75,50,.06); }
|
||||
.story-art { background: #087c4b; }
|
||||
.story-shape { background: #8fe0b8; }
|
||||
.story-2 .story-art { background: #0b9560; }
|
||||
.story-2 .story-shape { background: #b6ead0; }
|
||||
.story-3 .story-art { background: #076b48; }
|
||||
.story-3 .story-shape { background: #62cea0; }
|
||||
.story-tag { color: #078e55; }
|
||||
.planner-card { border-radius: 30rpx; background: linear-gradient(145deg,#089b5d,#067645); box-shadow: 0 20rpx 50rpx rgba(3,114,66,.22); }
|
||||
.planner-label,.spark,.planner-button { color: #d9ffeb; }
|
||||
.choice-preview text { background: rgba(255,255,255,.08); }
|
||||
</style>
|
||||
@@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<view v-if="plan" class="page">
|
||||
<view class="nav">
|
||||
<view class="back" @click="goBack">‹</view>
|
||||
<text class="nav-title">推荐路线</text>
|
||||
<text class="replan" @click="goPlanner">重新规划</text>
|
||||
</view>
|
||||
|
||||
<view class="content">
|
||||
<view class="route-card">
|
||||
<view class="recommend"><text class="recommend-icon">✦</text>为你推荐以下路线</view>
|
||||
<text class="route-title">{{ plan.itinerary.title }}</text>
|
||||
<view class="tags">
|
||||
<text v-for="tag in routeTags" :key="tag">{{ tag }}</text>
|
||||
</view>
|
||||
<view class="route-meta">
|
||||
<view><text class="meta-label">总时长</text><text class="meta-value">{{ formatMinutes(plan.itinerary.totalMinutes) }}</text></view>
|
||||
<view><text class="meta-label">行程</text><text class="meta-value">{{ plan.itinerary.items.length }} 个地点</text></view>
|
||||
</view>
|
||||
<text class="route-summary">{{ plan.itinerary.summary }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="plan.changeSummary" class="change-note"><text>✓ 已更新</text>{{ plan.changeSummary }}</view>
|
||||
|
||||
<view class="map-card">
|
||||
<view class="map-grid" />
|
||||
<view class="park park-one" />
|
||||
<view class="park park-two" />
|
||||
<view class="water" />
|
||||
<view class="road road-one" />
|
||||
<view class="road road-two" />
|
||||
<view class="road road-three" />
|
||||
<view class="route-line route-one" />
|
||||
<view class="route-line route-two" />
|
||||
<view class="route-line route-three" />
|
||||
<view
|
||||
v-for="(item, index) in mapItems"
|
||||
:key="item.placeId"
|
||||
class="map-stop"
|
||||
:class="`map-stop-${index + 1}`"
|
||||
>
|
||||
<view class="map-pin">{{ index + 1 }}</view>
|
||||
<text class="map-label">{{ shortName(item.placeName) }}</text>
|
||||
</view>
|
||||
<view class="map-badge"><text>路线示意</text><text>非实时地图</text></view>
|
||||
</view>
|
||||
|
||||
<view class="list-card">
|
||||
<view
|
||||
v-for="(item, index) in plan.itinerary.items"
|
||||
:key="`${item.placeId}-${index}`"
|
||||
class="place-row"
|
||||
>
|
||||
<view class="place-art" :class="`art-${index % 4}`">
|
||||
<view class="sun" />
|
||||
<view class="hill hill-back" />
|
||||
<view class="hill hill-front" />
|
||||
<view class="building"><view /><view /><view /></view>
|
||||
<text>{{ String(index + 1).padStart(2, '0') }}</text>
|
||||
</view>
|
||||
<view class="place-copy">
|
||||
<view class="place-head">
|
||||
<view class="place-number">{{ index + 1 }}</view>
|
||||
<text class="place-name">{{ item.placeName }}</text>
|
||||
</view>
|
||||
<view class="place-tags">
|
||||
<text>{{ index === 0 ? '首站' : '推荐' }}</text>
|
||||
<text>{{ preferenceTag(index) }}</text>
|
||||
</view>
|
||||
<text class="activity">{{ item.activity }}</text>
|
||||
<view class="place-foot">
|
||||
<text>{{ item.startTime }}–{{ item.endTime }}</text>
|
||||
<text v-if="index">{{ transferText(item.transferFromPreviousMinutes) }}</text>
|
||||
</view>
|
||||
<view class="reason"><text>✦</text>{{ item.reason }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="cost-card">
|
||||
<view class="cost-icon">¥</view>
|
||||
<view><text>费用提示</text><text>{{ plan.itinerary.estimatedCostText }}</text></view>
|
||||
</view>
|
||||
|
||||
<view class="adjust-card">
|
||||
<text class="section-kicker">AI 路线微调</text>
|
||||
<text class="section-title">还想怎么调整?</text>
|
||||
<view class="quick-list">
|
||||
<view v-for="prompt in quickPrompts" :key="prompt" class="quick" @click="sendAdjustment(prompt)">
|
||||
<text>{{ prompt }}</text><text>›</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="composer">
|
||||
<input
|
||||
v-model="message"
|
||||
:disabled="adjusting"
|
||||
maxlength="500"
|
||||
placeholder="例如:第二个地点换成室内项目"
|
||||
placeholder-class="placeholder"
|
||||
confirm-type="send"
|
||||
@confirm="sendAdjustment()"
|
||||
/>
|
||||
<button :disabled="adjusting || !message.trim()" @click="sendAdjustment()">{{ adjusting ? '…' : '发送' }}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-card">
|
||||
<view class="detail-section">
|
||||
<text class="section-kicker">出行提醒</text>
|
||||
<text v-for="note in plan.itinerary.notes" :key="note" class="note">· {{ note }}</text>
|
||||
</view>
|
||||
<view class="detail-section sources">
|
||||
<text class="section-kicker">资料来源</text>
|
||||
<view v-for="source in plan.itinerary.sources" :key="source.placeId" class="source" @click="copySource(source.sourceUrl)">
|
||||
<view><text>{{ source.sourceName }}</text><text>{{ source.updatedAt }} 更新</text></view><text>复制链接</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-bar">
|
||||
<button class="save-button" @click="saveRoute"><text>☆</text>{{ saved ? '已保存' : '保存路线' }}</button>
|
||||
<button class="accept-button" @click="acceptRoute"><text>➤</text>采纳这条路线</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="empty">
|
||||
<view class="empty-icon">⌁</view>
|
||||
<text>还没有生成行程</text>
|
||||
<button @click="goPlanner">返回选择偏好</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { computed, ref } from 'vue'
|
||||
import { adjustPlan, loadPlan, loadPreferences, savePlan } from '@/api/travelAssistant'
|
||||
import type { PlanResponse, TravelPreferences } from '@/types/travel'
|
||||
|
||||
const plan = ref<PlanResponse | null>(null)
|
||||
const preferences = ref<TravelPreferences | null>(null)
|
||||
const message = ref('')
|
||||
const adjusting = ref(false)
|
||||
const saved = ref(false)
|
||||
const quickPrompts = ['节奏慢一点', '换一个室内地点', '增加亲子体验']
|
||||
|
||||
const paceLabels = { relaxed: '轻松休闲', moderate: '节奏适中', compact: '紧凑高效' }
|
||||
const durationLabels = { half_day: '半日游', full_day: '一日游' }
|
||||
|
||||
const routeTags = computed(() => {
|
||||
const value = preferences.value
|
||||
if (!value) return ['AI 优选', '光明文旅']
|
||||
return [
|
||||
durationLabels[value.duration],
|
||||
value.themes[0],
|
||||
paceLabels[value.pace],
|
||||
value.interests[0],
|
||||
].filter(Boolean)
|
||||
})
|
||||
const mapItems = computed(() => plan.value?.itinerary.items.slice(0, 4) || [])
|
||||
|
||||
onLoad(() => {
|
||||
plan.value = loadPlan()
|
||||
preferences.value = loadPreferences()
|
||||
})
|
||||
|
||||
const formatMinutes = (minutes: number) => {
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const rest = minutes % 60
|
||||
return rest ? `${hours}小时${rest}分` : `${hours}小时`
|
||||
}
|
||||
const transferText = (minutes: number | null) => minutes === null ? '交通待确认' : `约 ${minutes} 分钟`
|
||||
const shortName = (name: string) => name.length > 8 ? `${name.slice(0, 8)}…` : name
|
||||
const preferenceTag = (index: number) => {
|
||||
const interests = preferences.value?.interests || []
|
||||
return interests[index % Math.max(interests.length, 1)] || '光明精选'
|
||||
}
|
||||
const goBack = () => uni.navigateBack()
|
||||
const goPlanner = () => uni.redirectTo({ url: '/pages/planner/index' })
|
||||
const copySource = (url: string) => {
|
||||
uni.setClipboardData({ data: url, success: () => uni.showToast({ title: '来源链接已复制', icon: 'none' }) })
|
||||
}
|
||||
const saveRoute = () => {
|
||||
if (!plan.value) return
|
||||
savePlan(plan.value)
|
||||
saved.value = true
|
||||
uni.showToast({ title: '路线已保存', icon: 'success' })
|
||||
}
|
||||
const acceptRoute = () => {
|
||||
saveRoute()
|
||||
uni.showModal({
|
||||
title: '路线已采纳',
|
||||
content: '行程已保存到本机。真实地图导航将在下一阶段接入。',
|
||||
showCancel: false,
|
||||
})
|
||||
}
|
||||
const sendAdjustment = async (quick?: string) => {
|
||||
if (!plan.value || adjusting.value) return
|
||||
const content = (quick || message.value).trim()
|
||||
if (!content) return
|
||||
adjusting.value = true
|
||||
try {
|
||||
const response = await adjustPlan(plan.value.conversationId, content)
|
||||
plan.value = response
|
||||
savePlan(response)
|
||||
saved.value = false
|
||||
message.value = ''
|
||||
uni.pageScrollTo({ scrollTop: 0, duration: 300 })
|
||||
} catch (error) {
|
||||
uni.showModal({ title: '调整未完成', content: error instanceof Error ? error.message : '上一次行程已为你保留', showCancel: false })
|
||||
} finally {
|
||||
adjusting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page { background: #f4f8f6; }
|
||||
.page { min-height: 100vh; padding-bottom: calc(150rpx + env(safe-area-inset-bottom)); color: #17221d; background: radial-gradient(circle at 8% 8%,rgba(9,151,89,.06),transparent 340rpx),#f4f8f6; font-family: 'PingFang SC','Microsoft YaHei',sans-serif; }
|
||||
.nav { position: sticky; z-index: 30; top: 0; height: 100rpx; display: flex; align-items: center; justify-content: space-between; padding: calc(24rpx + env(safe-area-inset-top)) 28rpx 0; border-bottom: 1rpx solid #e7eeea; background: rgba(255,255,255,.96); }
|
||||
.back { width: 76rpx; color: #14201a; font-family: Georgia,serif; font-size: 59rpx; font-weight: 300; }
|
||||
.nav-title { position: absolute; left: 180rpx; right: 180rpx; text-align: center; font-size: 32rpx; font-weight: 800; }
|
||||
.replan { color: #087d4d; font-size: 23rpx; font-weight: 700; }
|
||||
.content { padding: 24rpx 20rpx 60rpx; }
|
||||
.route-card,.list-card,.adjust-card,.detail-card { border: 2rpx solid #d9e7e0; border-radius: 26rpx; background: #fff; box-shadow: 0 14rpx 40rpx rgba(20,78,53,.07); }
|
||||
.route-card { padding: 28rpx; border-radius: 26rpx 26rpx 0 0; }
|
||||
.recommend { display: flex; align-items: center; gap: 10rpx; color: #0a9258; font-size: 21rpx; font-weight: 700; }
|
||||
.recommend-icon { width: 34rpx; height: 34rpx; display: flex; align-items: center; justify-content: center; border-radius: 50%; color: #087b4b; background: #c9f0dc; }
|
||||
.route-title { display: block; margin-top: 19rpx; font-size: 38rpx; line-height: 1.35; font-weight: 800; letter-spacing: 1rpx; }
|
||||
.tags,.place-tags { display: flex; flex-wrap: wrap; gap: 10rpx; }
|
||||
.tags { margin-top: 17rpx; }
|
||||
.tags text,.place-tags text { padding: 8rpx 14rpx; border-radius: 8rpx; color: #27825a; background: #eaf7f0; font-size: 18rpx; }
|
||||
.route-meta { display: flex; gap: 48rpx; margin-top: 24rpx; }
|
||||
.route-meta > view { display: flex; align-items: baseline; gap: 10rpx; }
|
||||
.meta-label { color: #829089; font-size: 19rpx; }
|
||||
.meta-value { color: #18231e; font-size: 29rpx; font-weight: 800; }
|
||||
.route-summary { display: block; margin-top: 15rpx; color: #718078; font-size: 20rpx; line-height: 1.7; }
|
||||
.change-note { margin-top: 18rpx; padding: 20rpx 22rpx; border: 1rpx solid #cce6d8; border-radius: 15rpx; color: #527061; background: #eaf7f0; font-size: 20rpx; }
|
||||
.change-note text { margin-right: 12rpx; color: #087d4d; font-weight: 800; }
|
||||
.map-card { position: relative; height: 430rpx; overflow: hidden; border-right: 2rpx solid #d9e7e0; border-bottom: 2rpx solid #d9e7e0; border-left: 2rpx solid #d9e7e0; background: #edf4ed; }
|
||||
.map-grid { position: absolute; inset: 0; opacity: .8; background-image: linear-gradient(23deg,transparent 46%,#d7ded7 47%,#fff 49%,#d7ded7 51%,transparent 53%),linear-gradient(115deg,transparent 45%,#dae1da 46%,#fff 49%,#dae1da 52%,transparent 53%); background-size: 180rpx 150rpx,230rpx 190rpx; }
|
||||
.park { position: absolute; border-radius: 46% 54% 50% 50%; background: rgba(151,206,146,.33); }
|
||||
.park-one { left: 38rpx; top: 48rpx; width: 190rpx; height: 110rpx; transform: rotate(-14deg); }
|
||||
.park-two { right: 44rpx; bottom: 34rpx; width: 230rpx; height: 140rpx; transform: rotate(17deg); }
|
||||
.water { position: absolute; left: -50rpx; top: 232rpx; width: 820rpx; height: 22rpx; border-radius: 50%; background: #b9dfec; transform: rotate(-10deg); }
|
||||
.road { position: absolute; height: 15rpx; border: 3rpx solid #fff; border-radius: 999rpx; background: #d4d9d5; }
|
||||
.road-one { left: -30rpx; top: 92rpx; width: 520rpx; transform: rotate(12deg); }
|
||||
.road-two { right: -40rpx; top: 200rpx; width: 500rpx; transform: rotate(-18deg); }
|
||||
.road-three { left: 110rpx; bottom: 70rpx; width: 530rpx; transform: rotate(8deg); }
|
||||
.route-line { position: absolute; z-index: 3; height: 10rpx; border-radius: 99rpx; background: #1686df; box-shadow: 0 0 0 4rpx rgba(255,255,255,.8); transform-origin: left center; }
|
||||
.route-one { left: 148rpx; top: 112rpx; width: 210rpx; transform: rotate(22deg); }
|
||||
.route-two { left: 339rpx; top: 188rpx; width: 190rpx; transform: rotate(35deg); }
|
||||
.route-three { left: 493rpx; top: 294rpx; width: 150rpx; transform: rotate(-30deg); }
|
||||
.map-stop { position: absolute; z-index: 5; display: flex; align-items: center; gap: 8rpx; }
|
||||
.map-stop-1 { left: 98rpx; top: 73rpx; }
|
||||
.map-stop-2 { left: 315rpx; top: 156rpx; }
|
||||
.map-stop-3 { left: 492rpx; top: 272rpx; }
|
||||
.map-stop-4 { right: 38rpx; bottom: 64rpx; flex-direction: row-reverse; }
|
||||
.map-pin { width: 50rpx; height: 50rpx; display: flex; align-items: center; justify-content: center; border: 5rpx solid #fff; border-radius: 50% 50% 50% 8rpx; color: #fff; background: #078b52; box-shadow: 0 5rpx 14rpx rgba(6,93,56,.28); font-size: 21rpx; font-weight: 800; transform: rotate(-45deg); }
|
||||
.map-pin::first-letter { transform: rotate(45deg); }
|
||||
.map-label { max-width: 190rpx; padding: 8rpx 12rpx; border-radius: 8rpx; color: #28342e; background: rgba(255,255,255,.92); box-shadow: 0 4rpx 12rpx rgba(40,75,57,.12); font-size: 17rpx; font-weight: 700; }
|
||||
.map-badge { position: absolute; left: 18rpx; bottom: 16rpx; z-index: 7; display: flex; flex-direction: column; gap: 3rpx; padding: 10rpx 14rpx; border-radius: 10rpx; color: #315543; background: rgba(255,255,255,.9); font-size: 16rpx; }
|
||||
.map-badge text:last-child { color: #8a9690; font-size: 13rpx; }
|
||||
.list-card { margin-top: 18rpx; overflow: hidden; }
|
||||
.place-row { display: flex; gap: 20rpx; padding: 24rpx; border-bottom: 1rpx solid #e8eeeb; }
|
||||
.place-row:last-child { border-bottom: 0; }
|
||||
.place-art { position: relative; flex-shrink: 0; width: 190rpx; height: 150rpx; overflow: hidden; border-radius: 16rpx; background: linear-gradient(#b8e5f2 0 48%,#a8d58c 49%); }
|
||||
.place-art.art-1 { background: linear-gradient(#cce8f2 0 49%,#99c7a0 50%); }
|
||||
.place-art.art-2 { background: linear-gradient(#c1e2f4 0 48%,#8ec69a 49%); }
|
||||
.place-art.art-3 { background: linear-gradient(#f4d8b4 0 48%,#c18a55 49%); }
|
||||
.place-art > text { position: absolute; right: 9rpx; top: 7rpx; color: rgba(255,255,255,.88); font-family: Georgia,serif; font-size: 19rpx; font-weight: 700; }
|
||||
.sun { position: absolute; right: 25rpx; top: 21rpx; width: 30rpx; height: 30rpx; border-radius: 50%; background: #fff3a2; }
|
||||
.hill { position: absolute; border-radius: 50% 50% 0 0; }
|
||||
.hill-back { left: -28rpx; bottom: 38rpx; width: 150rpx; height: 58rpx; background: #74b986; transform: rotate(12deg); }
|
||||
.hill-front { right: -34rpx; bottom: 22rpx; width: 160rpx; height: 70rpx; background: #4b9b68; transform: rotate(-7deg); }
|
||||
.building { position: absolute; z-index: 2; left: 65rpx; bottom: 22rpx; width: 65rpx; height: 58rpx; display: flex; justify-content: space-around; padding: 13rpx 7rpx 0; box-sizing: border-box; background: #f0eee4; box-shadow: 0 4rpx 8rpx rgba(0,0,0,.18); }
|
||||
.building::before { content: ''; position: absolute; left: -8rpx; top: -20rpx; border-right: 40rpx solid transparent; border-bottom: 22rpx solid #557d67; border-left: 40rpx solid transparent; }
|
||||
.building view { width: 8rpx; height: 34rpx; background: #759682; }
|
||||
.place-copy { min-width: 0; flex: 1; }
|
||||
.place-head { display: flex; align-items: flex-start; gap: 10rpx; }
|
||||
.place-number { flex-shrink: 0; width: 37rpx; height: 37rpx; display: flex; align-items: center; justify-content: center; border-radius: 50%; color: #fff; background: #177fce; font-size: 18rpx; font-weight: 800; }
|
||||
.place-name { flex: 1; padding-top: 2rpx; font-size: 27rpx; line-height: 1.35; font-weight: 800; }
|
||||
.place-tags { margin: 12rpx 0 10rpx 47rpx; }
|
||||
.place-tags text { padding: 5rpx 10rpx; font-size: 15rpx; }
|
||||
.activity { display: block; margin-left: 47rpx; color: #68766f; font-size: 20rpx; line-height: 1.55; }
|
||||
.place-foot { display: flex; justify-content: space-between; margin: 12rpx 0 0 47rpx; color: #858f8a; font-size: 17rpx; }
|
||||
.reason { display: flex; gap: 8rpx; margin: 14rpx 0 0 47rpx; padding: 12rpx; border-radius: 9rpx; color: #426351; background: #f0f8f4; font-size: 17rpx; line-height: 1.45; }
|
||||
.reason text { color: #07935a; }
|
||||
.cost-card { display: flex; gap: 18rpx; margin-top: 18rpx; padding: 24rpx; border-radius: 20rpx; color: #fff; background: linear-gradient(135deg,#0a965b,#077647); box-shadow: 0 12rpx 28rpx rgba(5,118,69,.17); }
|
||||
.cost-icon { flex-shrink: 0; width: 52rpx; height: 52rpx; display: flex; align-items: center; justify-content: center; border: 1rpx solid rgba(255,255,255,.5); border-radius: 50%; font-size: 23rpx; }
|
||||
.cost-card > view:last-child { display: flex; flex-direction: column; gap: 7rpx; font-size: 18rpx; line-height: 1.5; }
|
||||
.cost-card > view:last-child text:first-child { font-size: 22rpx; font-weight: 800; }
|
||||
.adjust-card,.detail-card { margin-top: 18rpx; padding: 28rpx; }
|
||||
.section-kicker { display: block; color: #078e55; font-size: 18rpx; font-weight: 800; letter-spacing: 2rpx; }
|
||||
.section-title { display: block; margin-top: 8rpx; font-size: 31rpx; font-weight: 800; }
|
||||
.quick-list { display: flex; flex-direction: column; margin-top: 20rpx; border: 1rpx solid #e1eae5; border-radius: 14rpx; }
|
||||
.quick { display: flex; justify-content: space-between; padding: 21rpx 20rpx; border-bottom: 1rpx solid #e8eeeb; font-size: 21rpx; }
|
||||
.quick:last-child { border-bottom: 0; }
|
||||
.quick text:last-child { color: #078e55; font-size: 30rpx; line-height: 20rpx; }
|
||||
.composer { display: flex; margin-top: 18rpx; overflow: hidden; border: 2rpx solid #dce7e1; border-radius: 14rpx; }
|
||||
.composer input { flex: 1; height: 82rpx; padding: 0 20rpx; font-size: 20rpx; }
|
||||
.placeholder { color: #a5afaa; }
|
||||
.composer button { width: 112rpx; height: 82rpx; margin: 0; border-radius: 0; color: #fff; background: #078e55; font-size: 20rpx; line-height: 82rpx; }
|
||||
.composer button::after { border: 0; }
|
||||
.composer button[disabled] { opacity: .55; }
|
||||
.detail-section + .detail-section { margin-top: 30rpx; padding-top: 26rpx; border-top: 1rpx solid #e5ece8; }
|
||||
.note { display: block; margin-top: 13rpx; color: #68766f; font-size: 19rpx; line-height: 1.6; }
|
||||
.source { display: flex; align-items: center; justify-content: space-between; gap: 18rpx; padding: 18rpx 0; border-bottom: 1rpx solid #e8eeeb; color: #078e55; font-size: 17rpx; }
|
||||
.source view { flex: 1; display: flex; flex-direction: column; gap: 5rpx; color: #34473d; }
|
||||
.source view text:last-child { color: #919b96; font-size: 15rpx; }
|
||||
.bottom-bar { position: fixed; z-index: 40; right: 0; bottom: 0; left: 0; display: flex; gap: 18rpx; padding: 20rpx 24rpx calc(20rpx + env(safe-area-inset-bottom)); border-top: 1rpx solid #dfe8e3; background: rgba(255,255,255,.96); box-shadow: 0 -10rpx 30rpx rgba(26,70,49,.07); }
|
||||
.bottom-bar button { height: 88rpx; display: flex; align-items: center; justify-content: center; gap: 10rpx; margin: 0; border-radius: 18rpx; font-size: 23rpx; font-weight: 800; }
|
||||
.bottom-bar button::after { border: 0; }
|
||||
.save-button { width: 230rpx; color: #087d4d; border: 2rpx solid #11915b; background: #fff; }
|
||||
.save-button text { font-size: 33rpx; font-weight: 400; }
|
||||
.accept-button { flex: 1; color: #fff; background: linear-gradient(100deg,#079c5c,#067d4a); box-shadow: 0 10rpx 22rpx rgba(3,129,75,.2); }
|
||||
.accept-button text { font-size: 26rpx; transform: rotate(-26deg); }
|
||||
.empty { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 28rpx; color: #26372e; background: #f4f8f6; font-size: 31rpx; font-weight: 700; }
|
||||
.empty-icon { width: 100rpx; height: 100rpx; display: flex; align-items: center; justify-content: center; border-radius: 50%; color: #078e55; background: #e4f5ec; font-size: 56rpx; }
|
||||
.empty button { padding: 0 34rpx; border-radius: 16rpx; color: #fff; background: #078e55; font-size: 21rpx; }
|
||||
</style>
|
||||
@@ -0,0 +1,302 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="nav">
|
||||
<view class="back" @click="goBack">‹</view>
|
||||
<text class="nav-title">AI 智能规划</text>
|
||||
<view class="nav-space" />
|
||||
</view>
|
||||
|
||||
<view class="intro">
|
||||
<view class="bot" aria-label="光明文旅 AI 助手">
|
||||
<view class="antenna"><view class="antenna-dot" /></view>
|
||||
<view class="bot-ear left" />
|
||||
<view class="bot-ear right" />
|
||||
<view class="bot-head">
|
||||
<view class="bot-face">
|
||||
<view class="bot-eye" />
|
||||
<view class="bot-mouth" />
|
||||
<view class="bot-eye" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="bot-body"><text>AI</text></view>
|
||||
</view>
|
||||
<view class="speech">
|
||||
<view class="speech-tail" />
|
||||
<text class="hello">Hi,我是光明文旅 AI 助手</text>
|
||||
<text class="intro-copy">告诉我你的偏好和需求,<br />我将为你生成专属路线</text>
|
||||
<view class="online"><view class="online-dot" /> REAL AI · 在线规划</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<view class="card-title">
|
||||
<view>
|
||||
<text class="title">告诉我你的偏好</text>
|
||||
<text class="title-sub">已选内容会成为 AI 规划依据</text>
|
||||
</view>
|
||||
<view class="progress">1/1</view>
|
||||
</view>
|
||||
|
||||
<view class="section first">
|
||||
<view class="section-head">
|
||||
<text class="label">出行主题</text><text class="hint">多选</text>
|
||||
</view>
|
||||
<view class="option-grid four">
|
||||
<view v-for="item in themeOptions" :key="item" class="option" :class="{ active: form.themes.includes(item) }" @click="toggleTheme(item)">
|
||||
<text v-if="form.themes.includes(item)" class="check">✓</text>{{ item }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-head"><text class="label">游玩时长</text></view>
|
||||
<view class="option-grid three">
|
||||
<view class="option" :class="{ active: form.duration === 'half_day' }" @click="form.duration = 'half_day'">
|
||||
<text class="option-main">半日游</text><text class="option-sub">约 4 小时</text>
|
||||
</view>
|
||||
<view class="option" :class="{ active: form.duration === 'full_day' }" @click="form.duration = 'full_day'">
|
||||
<text class="option-main">一日游</text><text class="option-sub">约 8 小时</text>
|
||||
</view>
|
||||
<view class="option disabled"><text class="option-main">两日游</text><text class="option-sub">敬请期待</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-head"><text class="label">节奏偏好</text></view>
|
||||
<view class="option-grid three">
|
||||
<view v-for="item in paceOptions" :key="item.value" class="option" :class="{ active: form.pace === item.value }" @click="form.pace = item.value">
|
||||
<text class="option-main">{{ item.label }}</text><text class="option-sub">{{ item.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-head">
|
||||
<text class="label">特色偏好</text><text class="hint">多选</text>
|
||||
</view>
|
||||
<view class="option-grid three interest-grid">
|
||||
<view v-for="item in interestOptions" :key="item" class="option" :class="{ active: form.interests.includes(item) }" @click="toggleInterest(item)">
|
||||
<text v-if="form.interests.includes(item)" class="check">✓</text>{{ item }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-head"><text class="label">出行方式</text></view>
|
||||
<view class="option-grid three">
|
||||
<view v-for="item in transportOptions" :key="item.value" class="option" :class="{ active: form.transport === item.value }" @click="form.transport = item.value">{{ item.label }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-head"><text class="label">预算偏好</text></view>
|
||||
<view class="option-grid three">
|
||||
<view v-for="item in budgetOptions" :key="item.value" class="option" :class="{ active: form.budgetLevel === item.value }" @click="form.budgetLevel = item.value">{{ item.label }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section people-section">
|
||||
<view class="section-head">
|
||||
<text class="label">同行人数</text><text class="hint">儿童年龄会影响推荐</text>
|
||||
</view>
|
||||
<view class="people-grid">
|
||||
<view class="counter-row">
|
||||
<view><text class="person-label">成人</text><text class="person-note">18 岁以上</text></view>
|
||||
<view class="counter"><button @click="changeAdults(-1)">−</button><text>{{ form.adults }}</text><button @click="changeAdults(1)">+</button></view>
|
||||
</view>
|
||||
<view class="counter-row">
|
||||
<view><text class="person-label">儿童</text><text class="person-note">0–17 岁</text></view>
|
||||
<view class="counter"><button @click="changeChildren(-1)">−</button><text>{{ form.children }}</text><button @click="changeChildren(1)">+</button></view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="form.children" class="ages">
|
||||
<view v-for="(_, index) in form.childAges" :key="index" class="age-row">
|
||||
<text>儿童 {{ index + 1 }} 年龄</text>
|
||||
<slider :value="form.childAges[index]" min="0" max="17" activeColor="#07945b" backgroundColor="#dce8e2" block-color="#07945b" block-size="18" @change="setAge(index, $event)" />
|
||||
<text class="age-value">{{ form.childAges[index] }} 岁</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section last">
|
||||
<view class="section-head">
|
||||
<text class="label">补充说明</text><text class="hint">选填</text>
|
||||
</view>
|
||||
<view class="textarea-wrap">
|
||||
<textarea v-model="form.extraRequirements" class="textarea" maxlength="500" placeholder="如:想带小朋友,喜欢公园和美食,不想爬山…" placeholder-class="placeholder" />
|
||||
<text class="count">{{ form.extraRequirements.length }}/500</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="submit" :disabled="submitting" @click="submit">
|
||||
<text class="submit-spark">✦</text>
|
||||
<text>{{ submitting ? 'AI 正在编排行程…' : '开始智能规划' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="footer-note">
|
||||
<view class="line" /><view class="footer-dot" />
|
||||
<text>AI 懂你所想 · 规划你所需</text>
|
||||
<view class="footer-dot" /><view class="line" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { createPlan, savePlan, savePreferences } from '@/api/travelAssistant'
|
||||
import type { BudgetLevel, Interest, Pace, Theme, Transport, TravelPreferences } from '@/types/travel'
|
||||
|
||||
const themeOptions: Theme[] = ['亲子', '情侣', '朋友', '银发', '研学']
|
||||
const interestOptions: Interest[] = ['自然风光', '文化场馆', '生态科普', '美食', '摄影']
|
||||
const paceOptions: { label: string; desc: string; value: Pace }[] = [
|
||||
{ label: '轻松', desc: '慢慢走', value: 'relaxed' },
|
||||
{ label: '适中', desc: '刚刚好', value: 'moderate' },
|
||||
{ label: '紧凑', desc: '多看看', value: 'compact' },
|
||||
]
|
||||
const transportOptions: { label: string; value: Transport }[] = [
|
||||
{ label: '公共交通', value: 'public_transport' },
|
||||
{ label: '自驾', value: 'driving' },
|
||||
{ label: '步行', value: 'walking' },
|
||||
]
|
||||
const budgetOptions: { label: string; value: BudgetLevel }[] = [
|
||||
{ label: '经济', value: 'economy' },
|
||||
{ label: '适中', value: 'standard' },
|
||||
{ label: '品质', value: 'quality' },
|
||||
]
|
||||
|
||||
const form = reactive<TravelPreferences>({
|
||||
destination: '深圳市光明区',
|
||||
themes: ['亲子'],
|
||||
duration: 'half_day',
|
||||
pace: 'relaxed',
|
||||
interests: ['自然风光'],
|
||||
adults: 2,
|
||||
children: 0,
|
||||
childAges: [],
|
||||
transport: 'public_transport',
|
||||
budgetLevel: 'standard',
|
||||
extraRequirements: '',
|
||||
})
|
||||
const submitting = ref(false)
|
||||
|
||||
const toggleTheme = (item: Theme) => {
|
||||
const index = form.themes.indexOf(item)
|
||||
index >= 0 ? form.themes.splice(index, 1) : form.themes.push(item)
|
||||
}
|
||||
const toggleInterest = (item: Interest) => {
|
||||
const index = form.interests.indexOf(item)
|
||||
index >= 0 ? form.interests.splice(index, 1) : form.interests.push(item)
|
||||
}
|
||||
const changeAdults = (delta: number) => {
|
||||
form.adults = Math.min(20, Math.max(0, form.adults + delta))
|
||||
}
|
||||
const changeChildren = (delta: number) => {
|
||||
const next = Math.min(20, Math.max(0, form.children + delta))
|
||||
while (form.childAges.length < next) form.childAges.push(6)
|
||||
form.childAges.splice(next)
|
||||
form.children = next
|
||||
}
|
||||
const setAge = (index: number, event: { detail: { value: number } }) => {
|
||||
form.childAges[index] = event.detail.value
|
||||
}
|
||||
const goBack = () => uni.navigateBack()
|
||||
|
||||
const submit = async () => {
|
||||
if (!form.themes.length && !form.interests.length) {
|
||||
uni.showToast({ title: '请至少选择一项主题或偏好', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (form.adults + form.children < 1) {
|
||||
uni.showToast({ title: '出行总人数至少为 1', icon: 'none' })
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
const preferences = { ...form, themes: [...form.themes], interests: [...form.interests], childAges: [...form.childAges] }
|
||||
const response = await createPlan(preferences)
|
||||
savePreferences(preferences)
|
||||
savePlan(response)
|
||||
uni.navigateTo({ url: '/pages/itinerary/index' })
|
||||
} catch (error) {
|
||||
uni.showModal({ title: '暂时无法生成', content: error instanceof Error ? error.message : '请稍后再试', showCancel: false })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page { background: #f4f8f6; }
|
||||
.page { min-height: 100vh; padding-bottom: calc(60rpx + env(safe-area-inset-bottom)); color: #17221d; background: radial-gradient(circle at 12% 12%, rgba(20,166,105,.08), transparent 330rpx), #f4f8f6; font-family: 'PingFang SC','Microsoft YaHei',sans-serif; }
|
||||
.nav { position: relative; display: flex; align-items: center; justify-content: space-between; height: 100rpx; padding: calc(24rpx + env(safe-area-inset-top)) 28rpx 0; background: rgba(255,255,255,.92); border-bottom: 1rpx solid #e9efec; }
|
||||
.back,.nav-space { width: 72rpx; height: 72rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.back { color: #101a15; font-family: Georgia,serif; font-size: 58rpx; font-weight: 300; }
|
||||
.nav-title { font-size: 32rpx; font-weight: 700; letter-spacing: 1rpx; }
|
||||
.intro { display: flex; align-items: flex-end; gap: 20rpx; padding: 42rpx 28rpx 34rpx; }
|
||||
.bot { position: relative; flex-shrink: 0; width: 150rpx; height: 170rpx; }
|
||||
.antenna { position: absolute; z-index: 1; left: 73rpx; top: 0; width: 4rpx; height: 30rpx; background: #81d2ad; }
|
||||
.antenna-dot { position: absolute; left: -8rpx; top: -4rpx; width: 20rpx; height: 20rpx; border: 3rpx solid #b5e4ce; border-radius: 50%; background: #fff; box-shadow: 0 3rpx 10rpx rgba(0,123,76,.18); }
|
||||
.bot-head { position: absolute; z-index: 3; left: 16rpx; top: 28rpx; width: 118rpx; height: 92rpx; border: 4rpx solid #93d8b8; border-radius: 46rpx 46rpx 38rpx 38rpx; background: linear-gradient(145deg,#fff,#edf8f3); box-shadow: 0 12rpx 24rpx rgba(5,112,68,.15); }
|
||||
.bot-face { position: absolute; inset: 17rpx 14rpx; display: flex; align-items: center; justify-content: space-around; border-radius: 28rpx; background: #092f2b; box-shadow: inset 0 0 16rpx rgba(0,246,190,.18); }
|
||||
.bot-eye { width: 13rpx; height: 29rpx; border-radius: 999rpx; background: #18e4bd; box-shadow: 0 0 10rpx rgba(24,228,189,.7); }
|
||||
.bot-mouth { width: 20rpx; height: 5rpx; margin-top: 18rpx; border-radius: 99rpx; background: #18e4bd; }
|
||||
.bot-ear { position: absolute; z-index: 2; top: 58rpx; width: 20rpx; height: 46rpx; border: 3rpx solid #8ad5b1; background: #e7f7ef; }
|
||||
.bot-ear.left { left: 3rpx; border-radius: 18rpx 4rpx 4rpx 18rpx; }
|
||||
.bot-ear.right { right: 3rpx; border-radius: 4rpx 18rpx 18rpx 4rpx; }
|
||||
.bot-body { position: absolute; left: 37rpx; top: 117rpx; width: 76rpx; height: 48rpx; display: flex; justify-content: center; padding-top: 14rpx; box-sizing: border-box; border: 4rpx solid #9bdcbe; border-radius: 30rpx 30rpx 12rpx 12rpx; color: #06905a; background: #f2fbf7; font-size: 14rpx; font-weight: 800; letter-spacing: 2rpx; }
|
||||
.speech { position: relative; flex: 1; min-height: 150rpx; display: flex; flex-direction: column; justify-content: center; padding: 26rpx 28rpx; box-sizing: border-box; border: 2rpx solid #d4e8df; border-radius: 30rpx; background: rgba(255,255,255,.92); box-shadow: 0 12rpx 32rpx rgba(20,80,54,.07); }
|
||||
.speech-tail { position: absolute; left: -16rpx; bottom: 34rpx; width: 28rpx; height: 28rpx; border-left: 2rpx solid #d4e8df; border-bottom: 2rpx solid #d4e8df; background: #fff; transform: rotate(45deg); }
|
||||
.hello { color: #087846; font-size: 29rpx; font-weight: 800; }
|
||||
.intro-copy { margin-top: 10rpx; color: #64716b; font-size: 21rpx; line-height: 1.65; }
|
||||
.online { display: flex; align-items: center; margin-top: 12rpx; color: #7a8b83; font-size: 15rpx; letter-spacing: 1rpx; }
|
||||
.online-dot { width: 11rpx; height: 11rpx; margin-right: 8rpx; border-radius: 50%; background: #08a865; box-shadow: 0 0 0 5rpx rgba(8,168,101,.12); }
|
||||
.form-card { margin: 0 20rpx; padding: 0 28rpx 30rpx; border: 2rpx solid #d9e6e0; border-radius: 32rpx; background: rgba(255,255,255,.96); box-shadow: 0 20rpx 60rpx rgba(19,75,50,.08); }
|
||||
.card-title { display: flex; align-items: center; justify-content: space-between; margin: 0 -28rpx; padding: 29rpx 28rpx; border-bottom: 1rpx solid #e9efec; }
|
||||
.title { display: block; font-size: 31rpx; font-weight: 800; }
|
||||
.title-sub { display: block; margin-top: 6rpx; color: #8b9892; font-size: 18rpx; }
|
||||
.progress { padding: 8rpx 16rpx; border-radius: 999rpx; color: #07814d; background: #e9f7f0; font-size: 17rpx; font-weight: 700; }
|
||||
.section { padding: 34rpx 0; border-bottom: 1rpx solid #edf1ef; }
|
||||
.section.first { padding-top: 30rpx; }
|
||||
.section.last { border-bottom: 0; }
|
||||
.section-head { display: flex; align-items: center; gap: 12rpx; margin-bottom: 20rpx; }
|
||||
.label { font-size: 25rpx; font-weight: 700; }
|
||||
.hint { color: #9aa49f; font-size: 19rpx; }
|
||||
.hint::before { content: '· '; }
|
||||
.option-grid { display: grid; gap: 16rpx; }
|
||||
.option-grid.four { grid-template-columns: repeat(4,1fr); }
|
||||
.option-grid.three { grid-template-columns: repeat(3,1fr); }
|
||||
.option { min-height: 70rpx; display: flex; align-items: center; justify-content: center; gap: 7rpx; padding: 12rpx 10rpx; box-sizing: border-box; border: 2rpx solid #e0e6e3; border-radius: 14rpx; color: #454f4a; background: #fff; font-size: 23rpx; transition: transform .18s ease, background .18s ease; }
|
||||
.option:active { transform: scale(.97); }
|
||||
.option.active { color: #fff; border-color: #078e55; background: linear-gradient(145deg,#079c5c,#087d4d); box-shadow: 0 8rpx 18rpx rgba(5,142,84,.2); font-weight: 700; }
|
||||
.option.disabled { color: #b7bfbb; background: #f6f8f7; }
|
||||
.check { font-size: 18rpx; }
|
||||
.option-main,.option-sub { display: block; text-align: center; }
|
||||
.option-main { font-size: 23rpx; }
|
||||
.option-sub { margin-left: 6rpx; color: #9aa49f; font-size: 16rpx; }
|
||||
.option.active .option-sub { color: rgba(255,255,255,.75); }
|
||||
.interest-grid .option { font-size: 21rpx; }
|
||||
.people-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16rpx; }
|
||||
.counter-row { min-height: 112rpx; display: flex; flex-direction: column; gap: 18rpx; padding: 20rpx; box-sizing: border-box; border: 2rpx solid #e4eae7; border-radius: 16rpx; }
|
||||
.person-label,.person-note { display: block; }
|
||||
.person-label { font-size: 23rpx; font-weight: 700; }
|
||||
.person-note { margin-top: 3rpx; color: #9aa49f; font-size: 16rpx; }
|
||||
.counter { display: flex; align-items: center; justify-content: space-between; }
|
||||
.counter button { width: 52rpx; height: 52rpx; margin: 0; padding: 0; line-height: 50rpx; border: 0; border-radius: 12rpx; color: #087d4d; background: #eaf7f0; font-size: 28rpx; }
|
||||
.counter button::after { border: 0; }
|
||||
.counter text { min-width: 36rpx; text-align: center; color: #16231d; font-size: 27rpx; font-weight: 800; }
|
||||
.ages { margin-top: 18rpx; padding: 18rpx; border-radius: 14rpx; background: #f4f9f6; }
|
||||
.age-row { display: grid; grid-template-columns: 150rpx 1fr 68rpx; align-items: center; font-size: 18rpx; }
|
||||
.age-value { text-align: right; color: #07814d; font-weight: 700; }
|
||||
.textarea-wrap { position: relative; }
|
||||
.textarea { width: 100%; height: 180rpx; padding: 22rpx 24rpx 50rpx; box-sizing: border-box; border: 2rpx solid #e0e6e3; border-radius: 16rpx; color: #29342f; background: #fbfcfc; font-size: 22rpx; line-height: 1.7; }
|
||||
.placeholder { color: #aeb7b2; }
|
||||
.count { position: absolute; right: 18rpx; bottom: 16rpx; color: #a1aaa5; font-size: 17rpx; }
|
||||
.submit { width: 100%; height: 94rpx; display: flex; align-items: center; justify-content: center; gap: 17rpx; margin-top: 30rpx; border: 0; border-radius: 20rpx; color: #fff; background: linear-gradient(100deg,#079c5c,#067d4a); box-shadow: 0 13rpx 28rpx rgba(3,129,75,.25); font-size: 28rpx; font-weight: 800; letter-spacing: 1rpx; }
|
||||
.submit::after { border: 0; }
|
||||
.submit[disabled] { opacity: .7; }
|
||||
.submit-spark { font-size: 31rpx; }
|
||||
.footer-note { display: flex; align-items: center; justify-content: center; gap: 13rpx; padding: 46rpx 28rpx calc(10rpx + env(safe-area-inset-bottom)); color: #176744; font-family: 'STSong','Songti SC',serif; font-size: 22rpx; letter-spacing: 3rpx; }
|
||||
.line { width: 50rpx; height: 1rpx; background: #9cc8b3; }
|
||||
.footer-dot { width: 10rpx; height: 10rpx; border-radius: 50%; background: #8cbda6; }
|
||||
</style>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export {}
|
||||
|
||||
declare module "vue" {
|
||||
type Hooks = App.AppInstance & Page.PageInstance;
|
||||
interface ComponentCustomOptions extends Hooks {}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 537 KiB |
@@ -0,0 +1,59 @@
|
||||
export type Theme = '亲子' | '情侣' | '朋友' | '银发' | '研学'
|
||||
export type Interest = '自然风光' | '文化场馆' | '生态科普' | '美食' | '摄影'
|
||||
export type Duration = 'half_day' | 'full_day'
|
||||
export type Pace = 'relaxed' | 'moderate' | 'compact'
|
||||
export type Transport = 'walking' | 'driving' | 'public_transport'
|
||||
export type BudgetLevel = 'economy' | 'standard' | 'quality'
|
||||
|
||||
export interface TravelPreferences {
|
||||
destination: '深圳市光明区'
|
||||
themes: Theme[]
|
||||
duration: Duration
|
||||
pace: Pace
|
||||
interests: Interest[]
|
||||
adults: number
|
||||
children: number
|
||||
childAges: number[]
|
||||
transport: Transport
|
||||
budgetLevel: BudgetLevel
|
||||
extraRequirements: string
|
||||
}
|
||||
|
||||
export interface ItineraryItem {
|
||||
startTime: string
|
||||
endTime: string
|
||||
placeId: string
|
||||
placeName: string
|
||||
activity: string
|
||||
reason: string
|
||||
transferFromPreviousMinutes: number | null
|
||||
tips: string[]
|
||||
}
|
||||
|
||||
export interface ItinerarySource {
|
||||
placeId: string
|
||||
sourceName: string
|
||||
sourceUrl: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface Itinerary {
|
||||
title: string
|
||||
summary: string
|
||||
totalMinutes: number
|
||||
estimatedCostText: string
|
||||
items: ItineraryItem[]
|
||||
notes: string[]
|
||||
sources: ItinerarySource[]
|
||||
}
|
||||
|
||||
export interface PlanResponse {
|
||||
conversationId: string
|
||||
assistantMessage: string
|
||||
itinerary: Itinerary
|
||||
changeSummary?: string | null
|
||||
}
|
||||
|
||||
export interface APIError {
|
||||
detail?: string
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
|
||||
/* 颜色变量 */
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color: #333; // 基本色
|
||||
$uni-text-color-inverse: #fff; // 反色
|
||||
$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable: #c0c0c0;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color: #fff;
|
||||
$uni-bg-color-grey: #f8f8f8;
|
||||
$uni-bg-color-hover: #f1f1f1; // 点击状态颜色
|
||||
$uni-bg-color-mask: rgba(0, 0, 0, 0.4); // 遮罩颜色
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color: #c8c7cc;
|
||||
|
||||
/* 尺寸变量 */
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm: 12px;
|
||||
$uni-font-size-base: 14px;
|
||||
$uni-font-size-lg: 16;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm: 20px;
|
||||
$uni-img-size-base: 26px;
|
||||
$uni-img-size-lg: 40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
$uni-border-radius-base: 3px;
|
||||
$uni-border-radius-lg: 6px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 水平间距 */
|
||||
$uni-spacing-row-sm: 5px;
|
||||
$uni-spacing-row-base: 10px;
|
||||
$uni-spacing-row-lg: 15px;
|
||||
|
||||
/* 垂直间距 */
|
||||
$uni-spacing-col-sm: 4px;
|
||||
$uni-spacing-col-base: 8px;
|
||||
$uni-spacing-col-lg: 12px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2c405a; // 文章标题颜色
|
||||
$uni-font-size-title: 20px;
|
||||
$uni-color-subtitle: #555; // 二级标题颜色
|
||||
$uni-font-size-subtitle: 18px;
|
||||
$uni-color-paragraph: #3f536e; // 文章段落颜色
|
||||
$uni-font-size-paragraph: 15px;
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"lib": ["esnext", "dom"],
|
||||
"types": ["@dcloudio/types"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from "vite";
|
||||
import uni from "@dcloudio/vite-plugin-uni";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [uni()],
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://127.0.0.1:8000",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user