forked from zhouruizhe/gmTouringMiniApp
Initial commit: gmTouringMiniApp project
This commit is contained in:
@@ -0,0 +1,525 @@
|
||||
<script setup lang="ts">
|
||||
import type { PoiResolved } from '@/domain/poi'
|
||||
import TravelPoiCard from '@/components/travel/TravelPoiCard.vue'
|
||||
import { getPoiRepository } from '@/data/poi'
|
||||
import { loadPlan } from '@/services/travel-assistant'
|
||||
import { useMapStore } from '@/stores'
|
||||
|
||||
interface AssistantPrompt {
|
||||
title: string
|
||||
description: string
|
||||
themes: string[]
|
||||
interests: string[]
|
||||
extra: string
|
||||
}
|
||||
|
||||
const mapStore = useMapStore()
|
||||
const featuredPois = ref<PoiResolved[]>([])
|
||||
const hasSavedPlan = ref(false)
|
||||
|
||||
const prompts: AssistantPrompt[] = [
|
||||
{
|
||||
title: '亲子半日轻松游',
|
||||
description: '公园、田园与科普点位,控制步行强度',
|
||||
themes: ['亲子'],
|
||||
interests: ['自然风光', '生态科普'],
|
||||
extra: '安排适合亲子的轻松半日路线',
|
||||
},
|
||||
{
|
||||
title: '雨天室内研学',
|
||||
description: '优先文化场馆与科普体验,避开户外点位',
|
||||
themes: ['研学'],
|
||||
interests: ['文化场馆', '生态科普'],
|
||||
extra: '下雨,只安排室内项目',
|
||||
},
|
||||
{
|
||||
title: '城市摄影漫游',
|
||||
description: '串联绿道、公园和人文地标',
|
||||
themes: ['朋友'],
|
||||
interests: ['摄影', '自然风光'],
|
||||
extra: '优先推荐适合拍照和慢慢散步的地点',
|
||||
},
|
||||
]
|
||||
|
||||
function loadAssistantHome() {
|
||||
const repository = getPoiRepository()
|
||||
featuredPois.value = repository.getPublishedPois()
|
||||
.slice()
|
||||
.sort((left, right) => right.recommendationIndex - left.recommendationIndex || left.name.localeCompare(right.name, 'zh-CN'))
|
||||
.slice(0, 3)
|
||||
hasSavedPlan.value = Boolean(loadPlan())
|
||||
}
|
||||
|
||||
function openPlanner(prompt?: AssistantPrompt) {
|
||||
if (prompt) {
|
||||
uni.setStorageSync('guangming:planner-preset', {
|
||||
themes: prompt.themes,
|
||||
interests: prompt.interests,
|
||||
extraRequirements: prompt.extra,
|
||||
})
|
||||
}
|
||||
uni.switchTab({ url: '/pages/planner/index' })
|
||||
}
|
||||
|
||||
function openItinerary() {
|
||||
uni.navigateTo({ url: '/pages/itinerary/index' })
|
||||
}
|
||||
|
||||
function openDetail(poiId: string) {
|
||||
uni.navigateTo({ url: `/pages/poi/detail?poiId=${encodeURIComponent(poiId)}` })
|
||||
}
|
||||
|
||||
function focusOnMap(poiId: string) {
|
||||
const poi = getPoiRepository().getPoiById(poiId)
|
||||
if (!poi)
|
||||
return
|
||||
|
||||
mapStore.setCategory(null)
|
||||
mapStore.selectPoi(poi.id)
|
||||
mapStore.updateViewport({ longitude: poi.longitude, latitude: poi.latitude, scale: 14 })
|
||||
uni.switchTab({ url: '/pages/map/index' })
|
||||
}
|
||||
|
||||
onShow(loadAssistantHome)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="assistant-page">
|
||||
<view class="assistant-hero">
|
||||
<view class="assistant-hero__glow assistant-hero__glow--one" />
|
||||
<view class="assistant-hero__glow assistant-hero__glow--two" />
|
||||
<view class="assistant-hero__top">
|
||||
<view>
|
||||
<text class="assistant-hero__eyebrow">GUANGMING AI GUIDE</text>
|
||||
<text class="assistant-hero__title">光明文旅 AI 助手</text>
|
||||
</view>
|
||||
<view class="assistant-hero__status">
|
||||
<view class="assistant-hero__status-dot" />
|
||||
POC 可用
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="assistant-bot" aria-label="AI 助手形象">
|
||||
<view class="assistant-bot__antenna" />
|
||||
<view class="assistant-bot__head">
|
||||
<view class="assistant-bot__face">
|
||||
<view class="assistant-bot__eye" />
|
||||
<view class="assistant-bot__mouth" />
|
||||
<view class="assistant-bot__eye" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="assistant-bot__body">AI</view>
|
||||
</view>
|
||||
|
||||
<view class="assistant-hero__speech">
|
||||
<text class="assistant-hero__hello">你好,我是你的光明向导</text>
|
||||
<text class="assistant-hero__copy">告诉我同行人、兴趣和节奏,我会基于已审核的本地点位编排一条可展示、可调整的游览顺序。</text>
|
||||
</view>
|
||||
|
||||
<button class="assistant-hero__action" @click="openPlanner()">
|
||||
<text>定制我的光明路线</text>
|
||||
<text>→</text>
|
||||
</button>
|
||||
<button v-if="hasSavedPlan" class="assistant-hero__secondary" @click="openItinerary">
|
||||
继续查看上次路线
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="assistant-content">
|
||||
<view class="assistant-section">
|
||||
<view class="assistant-section__heading">
|
||||
<view>
|
||||
<text class="assistant-section__kicker">QUICK START</text>
|
||||
<text class="assistant-section__title">试试这样问</text>
|
||||
</view>
|
||||
<text class="assistant-section__count">03</text>
|
||||
</view>
|
||||
<view class="prompt-list">
|
||||
<button v-for="(prompt, index) in prompts" :key="prompt.title" class="prompt-card" @click="openPlanner(prompt)">
|
||||
<text class="prompt-card__index">0{{ index + 1 }}</text>
|
||||
<view class="prompt-card__copy">
|
||||
<text class="prompt-card__title">{{ prompt.title }}</text>
|
||||
<text class="prompt-card__description">{{ prompt.description }}</text>
|
||||
</view>
|
||||
<text class="prompt-card__arrow">›</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="assistant-section">
|
||||
<view class="assistant-section__heading">
|
||||
<view>
|
||||
<text class="assistant-section__kicker">CURATED PLACES</text>
|
||||
<text class="assistant-section__title">从这些点位开始</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="featured-list">
|
||||
<TravelPoiCard
|
||||
v-for="poi in featuredPois"
|
||||
:key="poi.id"
|
||||
:poi="poi"
|
||||
@detail="openDetail"
|
||||
@map="focusOnMap"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="assistant-notice">
|
||||
<text class="assistant-notice__title">POC 能力边界</text>
|
||||
<text>未配置在线 AI 服务时,助手会使用本地 POI 和确定性规则生成路线;当前路线为推荐游览顺序,不提供实时路况或逐段导航。</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
layout: map
|
||||
style:
|
||||
navigationBarTitleText: AI 文旅助手
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
navigationBarTextStyle: black
|
||||
backgroundColor: '#f4f8f6'
|
||||
</route>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.assistant-page {
|
||||
min-height: 100vh;
|
||||
color: #17221d;
|
||||
background: #f4f8f6;
|
||||
}
|
||||
|
||||
.assistant-hero {
|
||||
position: relative;
|
||||
padding: 48rpx 36rpx 54rpx;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
background: linear-gradient(145deg, #123d35 0%, #08724d 62%, #07573d 100%);
|
||||
}
|
||||
|
||||
.assistant-hero__glow {
|
||||
position: absolute;
|
||||
border: 1rpx solid rgb(220 255 234 / 22%);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.assistant-hero__glow--one {
|
||||
top: -180rpx;
|
||||
right: -200rpx;
|
||||
width: 620rpx;
|
||||
height: 620rpx;
|
||||
}
|
||||
|
||||
.assistant-hero__glow--two {
|
||||
top: 170rpx;
|
||||
right: 38rpx;
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
background: rgb(184 245 207 / 12%);
|
||||
}
|
||||
|
||||
.assistant-hero__top {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.assistant-hero__eyebrow,
|
||||
.assistant-section__kicker {
|
||||
display: block;
|
||||
font-size: 18rpx;
|
||||
line-height: 28rpx;
|
||||
color: #9fe0bd;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.assistant-hero__title {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
font-size: 44rpx;
|
||||
font-weight: 800;
|
||||
line-height: 60rpx;
|
||||
}
|
||||
|
||||
.assistant-hero__status {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 9rpx;
|
||||
align-items: center;
|
||||
padding: 10rpx 15rpx;
|
||||
font-size: 18rpx;
|
||||
color: #dff9e9;
|
||||
background: rgb(255 255 255 / 11%);
|
||||
border: 1rpx solid rgb(255 255 255 / 24%);
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.assistant-hero__status-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
background: #8df0b3;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 5rpx rgb(141 240 179 / 12%);
|
||||
}
|
||||
|
||||
.assistant-bot {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 150rpx;
|
||||
height: 175rpx;
|
||||
margin: 42rpx auto 0;
|
||||
}
|
||||
|
||||
.assistant-bot__antenna {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 72rpx;
|
||||
width: 6rpx;
|
||||
height: 32rpx;
|
||||
background: #9fe0bd;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.assistant-bot__antenna::before {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
left: -7rpx;
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
content: "";
|
||||
background: #dff9e9;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.assistant-bot__head {
|
||||
position: absolute;
|
||||
top: 28rpx;
|
||||
left: 12rpx;
|
||||
box-sizing: border-box;
|
||||
width: 126rpx;
|
||||
height: 96rpx;
|
||||
padding: 16rpx;
|
||||
background: #f1fbf6;
|
||||
border: 4rpx solid #9fe0bd;
|
||||
border-radius: 44rpx;
|
||||
box-shadow: 0 12rpx 28rpx rgb(0 44 30 / 18%);
|
||||
}
|
||||
|
||||
.assistant-bot__face {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #092f2b;
|
||||
border-radius: 26rpx;
|
||||
}
|
||||
|
||||
.assistant-bot__eye {
|
||||
width: 13rpx;
|
||||
height: 29rpx;
|
||||
background: #20e2b7;
|
||||
border-radius: 999rpx;
|
||||
box-shadow: 0 0 10rpx rgb(32 226 183 / 60%);
|
||||
}
|
||||
|
||||
.assistant-bot__mouth {
|
||||
width: 20rpx;
|
||||
height: 5rpx;
|
||||
margin-top: 18rpx;
|
||||
background: #20e2b7;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.assistant-bot__body {
|
||||
position: absolute;
|
||||
top: 122rpx;
|
||||
left: 40rpx;
|
||||
width: 70rpx;
|
||||
height: 48rpx;
|
||||
font-size: 17rpx;
|
||||
font-weight: 800;
|
||||
line-height: 48rpx;
|
||||
color: #087a4b;
|
||||
text-align: center;
|
||||
background: #e5f7ee;
|
||||
border: 4rpx solid #9fe0bd;
|
||||
border-radius: 26rpx 26rpx 12rpx 12rpx;
|
||||
}
|
||||
|
||||
.assistant-hero__speech {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 28rpx;
|
||||
margin-top: 24rpx;
|
||||
background: rgb(255 255 255 / 11%);
|
||||
border: 1rpx solid rgb(255 255 255 / 22%);
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.assistant-hero__hello {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
line-height: 42rpx;
|
||||
}
|
||||
|
||||
.assistant-hero__copy {
|
||||
margin-top: 12rpx;
|
||||
font-size: 23rpx;
|
||||
line-height: 38rpx;
|
||||
color: rgb(239 255 246 / 76%);
|
||||
}
|
||||
|
||||
.assistant-hero__action,
|
||||
.assistant-hero__secondary {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 92rpx;
|
||||
margin: 28rpx 0 0;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
line-height: 92rpx;
|
||||
border: 0;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.assistant-hero__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 30rpx;
|
||||
color: #0d5038;
|
||||
background: #c9f1da;
|
||||
box-shadow: 0 14rpx 32rpx rgb(0 42 28 / 20%);
|
||||
}
|
||||
|
||||
.assistant-hero__secondary {
|
||||
height: 72rpx;
|
||||
margin-top: 16rpx;
|
||||
line-height: 72rpx;
|
||||
color: #e9fff2;
|
||||
background: transparent;
|
||||
border: 1rpx solid rgb(255 255 255 / 30%);
|
||||
}
|
||||
|
||||
.assistant-hero__action::after,
|
||||
.assistant-hero__secondary::after,
|
||||
.prompt-card::after {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.assistant-content {
|
||||
padding: 8rpx 28rpx calc(48rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.assistant-section {
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.assistant-section__heading {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.assistant-section__kicker {
|
||||
color: #16805c;
|
||||
}
|
||||
|
||||
.assistant-section__title {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
line-height: 50rpx;
|
||||
}
|
||||
|
||||
.assistant-section__count {
|
||||
font-family: Georgia, serif;
|
||||
font-size: 48rpx;
|
||||
font-style: italic;
|
||||
color: rgb(23 56 47 / 18%);
|
||||
}
|
||||
|
||||
.prompt-list,
|
||||
.featured-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.prompt-card {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 132rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
background: #fff;
|
||||
border: 1rpx solid #dce6e1;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 10rpx 28rpx rgb(20 78 53 / 5%);
|
||||
}
|
||||
|
||||
.prompt-card__index {
|
||||
flex: none;
|
||||
font-family: Georgia, serif;
|
||||
font-size: 27rpx;
|
||||
color: #16805c;
|
||||
}
|
||||
|
||||
.prompt-card__copy {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.prompt-card__title {
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
line-height: 38rpx;
|
||||
color: #18201d;
|
||||
}
|
||||
|
||||
.prompt-card__description {
|
||||
margin-top: 5rpx;
|
||||
font-size: 21rpx;
|
||||
line-height: 32rpx;
|
||||
color: #718078;
|
||||
}
|
||||
|
||||
.prompt-card__arrow {
|
||||
flex: none;
|
||||
font-size: 42rpx;
|
||||
color: #16805c;
|
||||
}
|
||||
|
||||
.assistant-notice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 26rpx;
|
||||
margin-top: 46rpx;
|
||||
font-size: 21rpx;
|
||||
line-height: 34rpx;
|
||||
color: #637069;
|
||||
background: #eef6f2;
|
||||
border: 1rpx solid #d6e7df;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.assistant-notice__title {
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #0e6247;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user