
Travel Skill
- 2 installs
- 27 repo stars
- Updated June 18, 2026
- tencentcloudbase/awesome-miniprogram-skills
WeChat Mini Program skill for travel planning: destination search, itinerary planning with transport and lodging, weather, and travel tips.
About
Adds a travel-planning flow to a WeChat Mini Program covering destination search, itineraries, weather, and tips. A developer uses it as a scenario template when building trip-planning features.
- Covers destination search and transport-plus-lodging itineraries
- Includes weather queries and travel tips
Travel Skill by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,839 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tencentcloudbase/awesome-miniprogram-skills --skill travel-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 27 |
| Last updated | June 18, 2026 |
| Repository | tencentcloudbase/awesome-miniprogram-skills ↗ |
What it does
WeChat Mini Program skill for travel planning: destination search, itinerary planning with transport and lodging, weather, and travel tips.
Files
旅行规划
基于热门旅行目的地,为用户提供目的地搜索、行程规划(交通+住宿)、天气查询和旅行贴士的一站式旅行规划能力集合。
触发场景
用户原话举例(路由命中本技能):
- "帮我推荐几个旅行目的地"
- "我想去三亚玩,帮我规划一下行程"
- "丽江最近天气怎么样"
- "去成都怎么去最划算"
- "大理有哪些好的酒店"
- "有什么旅行建议吗"
- "帮我查一下杭州的出行攻略"
不适用范围
- 机票酒店预订下单、支付等诉求 → 不在本技能范围,由预订系统处理
- 签证办理、护照申请等诉求 → 不在本技能范围
- 旅游攻略撰写、游记分享等诉求 → 不在本技能范围
接口链路
searchDestinations:热门旅行目的地搜索与列表展示。planTrip:查看指定目的地的交通方案与住宿推荐。getWeatherInfo:查询指定目的地当前天气情况。getTravelTips:获取通用旅行贴士建议列表。
使用顺序
- 规划行程前需先确定目的地;没有目的地上下文时,先展示可选目的地列表。
- 查看行程规划(交通+住宿)前需先选定具体目的地(destId)。
- 查询天气前需先选定具体目的地(destId)。
- 旅行贴士为通用内容,可在任意时刻调用。
- 所有已绑定组件的接口都应优先展示卡片,不要改成纯文本逐条展开。
// skills/travel-skill/apis/getTravelTips.js
const {
isPreviewMode,
successResult,
errorResult,
defaultTips
} = require('../utils/util')
async function getTravelTips(params = {}) {
console.info('[ai-mode] getTravelTips 入口, params=', JSON.stringify(params))
if (isPreviewMode()) {
console.info('[ai-mode] getTravelTips 预览模式')
return buildResult(defaultTips())
}
const { result } = await wx.cloud.callFunction({
name: 'travel-skill-handler',
data: { action: 'getTravelTips' }
})
const items = (result && result.code === 0 && result.data && result.data.items) || []
console.info('[ai-mode] getTravelTips 云函数返回数量=', items.length)
if (items.length) {
return buildResult(items)
}
return errorResult(result?.message || '获取旅行贴士失败')
}
function buildResult(items) {
if (items && items.length > 0) {
return successResult(
`为你准备了 ${items.length} 条实用旅行贴士。请展示贴士卡片,让用户浏览查看。`,
{ items },
{}
)
}
return successResult(
'暂无旅行贴士。请稍后再试。',
{ items: [] },
{}
)
}
module.exports = getTravelTips
// skills/travel-skill/apis/getWeatherInfo.js
const {
isPreviewMode,
successResult,
errorResult,
defaultWeather,
defaultDestDetail
} = require('../utils/util')
async function getWeatherInfo(params = {}) {
console.info('[ai-mode] getWeatherInfo 入口, params=', JSON.stringify(params))
const destId = params && params.destId ? String(params.destId).trim() : ''
if (!destId) {
return successResult(
'缺少目的地信息,请先选择一个目的地。',
{ weather: null, destName: '' },
{ destId: '' }
)
}
if (isPreviewMode()) {
console.info('[ai-mode] getWeatherInfo 预览模式')
const weather = defaultWeather(destId)
const dest = defaultDestDetail(destId)
if (!weather) {
return successResult(
'暂未获取到该目的地的天气信息。',
{ weather: null, destName: dest ? dest.name : '' },
{ destId }
)
}
return buildResult({ weather, destName: dest ? dest.name : '' }, destId)
}
const { result } = await wx.cloud.callFunction({
name: 'travel-skill-handler',
data: { action: 'getWeatherInfo', destId }
})
if (result && result.code === 0 && result.data) {
return buildResult(result.data, destId)
}
return errorResult(result?.message || '查询天气失败')
}
function buildResult(data, destId) {
return successResult(
data.weather
? `「${data.destName}」当前天气:${data.weather.icon} ${data.weather.temp}°C,${data.weather.condition}。${data.weather.suggestion}`
: '暂未获取到天气信息。',
{ weather: data.weather || null, destName: data.destName || '' },
{ destId }
)
}
module.exports = getWeatherInfo
// skills/travel-skill/apis/planTrip.js
const {
isPreviewMode,
successResult,
errorResult,
defaultDestDetail
} = require('../utils/util')
async function planTrip(params = {}) {
console.info('[ai-mode] planTrip 入口, params=', JSON.stringify(params))
const destId = params && params.destId ? String(params.destId).trim() : ''
if (!destId) {
return successResult(
'缺少目的地信息,请先选择一个目的地。',
{ dest: null, transport: [], hotels: [] },
{ destId: '' }
)
}
if (isPreviewMode()) {
console.info('[ai-mode] planTrip 预览模式')
const dest = defaultDestDetail(destId)
if (!dest) {
return successResult(
'未找到该目的地的信息。请返回列表重新选择。',
{ dest: null, transport: [], hotels: [] },
{ destId }
)
}
return buildResult(
{ dest: mapDestBrief(dest), transport: dest.transport, hotels: dest.hotels },
destId
)
}
const { result } = await wx.cloud.callFunction({
name: 'travel-skill-handler',
data: { action: 'planTrip', destId }
})
if (result && result.code === 0 && result.data) {
return buildResult(result.data, destId)
}
return errorResult(result?.message || '规划行程失败')
}
function mapDestBrief(dest) {
return {
destId: dest.destId,
name: dest.name,
cover: dest.cover,
rating: dest.rating,
description: dest.description,
bestSeason: dest.bestSeason,
bestSeasonDesc: dest.bestSeasonDesc,
tags: dest.tags
}
}
function buildResult(data, destId) {
return successResult(
`已获取「${data.dest ? data.dest.name : ''}」的行程规划方案,包含交通与住宿推荐。请展示行程规划卡片。`,
{
dest: data.dest || null,
transport: data.transport || [],
hotels: data.hotels || []
},
{ destId }
)
}
module.exports = planTrip
// skills/travel-skill/apis/searchDestinations.js
const {
isPreviewMode,
successResult,
errorResult,
defaultDestinations
} = require('../utils/util')
async function searchDestinations(params = {}) {
console.info('[ai-mode] searchDestinations 入口, params=', JSON.stringify(params))
const keyword = String((params && params.keyword) || '').trim()
if (isPreviewMode()) {
console.info('[ai-mode] searchDestinations 预览模式')
return buildResult(defaultDestinations(keyword), keyword)
}
const { result } = await wx.cloud.callFunction({
name: 'travel-skill-handler',
data: { action: 'searchDestinations', keyword }
})
if (result && result.code === 0 && result.data && result.data.items) {
console.info('[ai-mode] searchDestinations 云函数返回数量=', result.data.items.length)
return buildResult(result.data.items, keyword)
}
return errorResult(result?.message || '搜索目的地失败')
}
function buildResult(items, keyword) {
const total = items.length
if (total > 0) {
return successResult(
`已找到 ${total} 个热门旅行目的地。请展示目的地列表卡片,让用户从卡片中选择一个查看详情并规划行程。禁止以纯文本列出目的地详情。`,
{ items, total, keyword },
{ keyword }
)
}
return successResult(
keyword
? `未找到与「${keyword}」相关的目的地。请展示空列表卡片,并引导用户换一个关键词搜索。`
: '当前没有可展示的目的地。请展示空列表卡片,并引导用户稍后再试。',
{ items: [], total: 0, keyword },
{ keyword }
)
}
module.exports = searchDestinations
// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
// 目的地种子数据(来自 seed.js)
const destinations = [
{
destId: 'D001',
name: '三亚',
nameEn: 'Sanya',
cover: 'https://picsum.photos/seed/sanya/400/300',
rating: 4.8,
description: '中国最美海滨城市,热带天堂,阳光、沙滩、椰林与碧海蓝天。',
bestSeason: '10月-次年4月',
bestSeasonDesc: '气候宜人,避寒胜地',
tags: ['海滨', '度假', '潜水'],
transport: [
{ type: 'flight', label: '直飞航班', from: '北京', to: '三亚', duration: '3h50m', price: 680, carrier: '海南航空' },
{ type: 'flight', label: '直飞航班', from: '上海', to: '三亚', duration: '3h10m', price: 520, carrier: '东方航空' },
{ type: 'train', label: '动车', from: '海口', to: '三亚', duration: '1h30m', price: 108, carrier: '铁路12306' }
],
hotels: [
{ name: '三亚海棠湾艾迪逊酒店', stars: 5, rating: 4.9, price: 1288, district: '海棠湾', features: ['私人沙滩', '无边泳池'] },
{ name: '三亚亚龙湾万豪度假酒店', stars: 5, rating: 4.7, price: 899, district: '亚龙湾', features: ['海景房', '亲子'] },
{ name: '三亚湾海居铂尔曼酒店', stars: 4, rating: 4.5, price: 499, district: '三亚湾', features: ['性价比高', '近市区'] }
]
},
{
destId: 'D002',
name: '丽江',
nameEn: 'Lijiang',
cover: 'https://picsum.photos/seed/lijiang/400/300',
rating: 4.7,
description: '世界文化遗产古城,雪山脚下的小桥流水人家,感受纳西族风情。',
bestSeason: '3月-5月、9月-11月',
bestSeasonDesc: '春暖花开或秋高气爽',
tags: ['古城', '人文', '雪山'],
transport: [
{ type: 'flight', label: '直飞航班', from: '北京', to: '丽江', duration: '4h', price: 880, carrier: '中国国航' },
{ type: 'flight', label: '直飞航班', from: '上海', to: '丽江', duration: '3h40m', price: 750, carrier: '吉祥航空' },
{ type: 'train', label: '动车', from: '昆明', to: '丽江', duration: '3h', price: 220, carrier: '铁路12306' }
],
hotels: [
{ name: '丽江悦榕庄', stars: 5, rating: 4.8, price: 1580, district: '束河古镇', features: ['雪山景观', 'SPA'] },
{ name: '丽江古城英迪格酒店', stars: 5, rating: 4.6, price: 780, district: '大研古城', features: ['纳西风格', '古城内'] },
{ name: '丽江花间堂客栈', stars: 4, rating: 4.5, price: 380, district: '大研古城', features: ['特色民宿', '庭院'] }
]
},
{
destId: 'D003',
name: '成都',
nameEn: 'Chengdu',
cover: 'https://picsum.photos/seed/chengdu/400/300',
rating: 4.9,
description: '天府之国,美食之都,熊猫的故乡,体验慢生活与麻辣鲜香。',
bestSeason: '3月-6月、9月-11月',
bestSeasonDesc: '气温舒适,美食四季皆宜',
tags: ['美食', '熊猫', '休闲'],
transport: [
{ type: 'flight', label: '直飞航班', from: '北京', to: '成都', duration: '3h', price: 620, carrier: '四川航空' },
{ type: 'flight', label: '直飞航班', from: '上海', to: '成都', duration: '3h15m', price: 580, carrier: '东方航空' },
{ type: 'train', label: '高铁', from: '重庆', to: '成都', duration: '1h', price: 150, carrier: '铁路12306' }
],
hotels: [
{ name: '成都博舍酒店', stars: 5, rating: 4.9, price: 1480, district: '太古里', features: ['设计感', '太古里核心'] },
{ name: '成都群光君悦酒店', stars: 5, rating: 4.7, price: 920, district: '春熙路', features: ['高空景观', '购物便利'] },
{ name: '成都春熙路亚朵酒店', stars: 4, rating: 4.6, price: 480, district: '春熙路', features: ['性价比', '舒适'] }
]
},
{
destId: 'D004',
name: '杭州',
nameEn: 'Hangzhou',
cover: 'https://picsum.photos/seed/hangzhou/400/300',
rating: 4.8,
description: '上有天堂下有苏杭,西湖美景天下闻名,江南水乡的诗意画卷。',
bestSeason: '3月-5月、9月-11月',
bestSeasonDesc: '烟雨江南最美时节',
tags: ['西湖', '江南', '文化'],
transport: [
{ type: 'flight', label: '直飞航班', from: '北京', to: '杭州', duration: '2h15m', price: 560, carrier: '中国国航' },
{ type: 'flight', label: '直飞航班', from: '广州', to: '杭州', duration: '2h', price: 480, carrier: '南方航空' },
{ type: 'train', label: '高铁', from: '上海', to: '杭州', duration: '45m', price: 73, carrier: '铁路12306' }
],
hotels: [
{ name: '杭州西子湖四季酒店', stars: 5, rating: 4.9, price: 1880, district: '西湖', features: ['西湖景观', '园林'] },
{ name: '杭州柏悦酒店', stars: 5, rating: 4.8, price: 1180, district: '钱江新城', features: ['高空大堂', '江景'] },
{ name: '杭州全季酒店西湖店', stars: 4, rating: 4.5, price: 420, district: '西湖', features: ['位置佳', '简约'] }
]
},
{
destId: 'D005',
name: '大理',
nameEn: 'Dali',
cover: 'https://picsum.photos/seed/dali/400/300',
rating: 4.7,
description: '风花雪月,苍山洱海,白族文化的发源地,文艺青年的诗和远方。',
bestSeason: '3月-5月、9月-11月',
bestSeasonDesc: '洱海风光最美',
tags: ['洱海', '文艺', '古镇'],
transport: [
{ type: 'flight', label: '直飞航班', from: '北京', to: '大理', duration: '3h50m', price: 920, carrier: '东方航空' },
{ type: 'flight', label: '直飞航班', from: '成都', to: '大理', duration: '1h30m', price: 380, carrier: '四川航空' },
{ type: 'train', label: '动车', from: '昆明', to: '大理', duration: '2h', price: 145, carrier: '铁路12306' }
],
hotels: [
{ name: '大理海纳尔云墅酒店', stars: 5, rating: 4.8, price: 1680, district: '洱海边', features: ['洱海全景', '无边泳池'] },
{ name: '大理古城一号院', stars: 4, rating: 4.6, price: 680, district: '大理古城', features: ['庭院', '白族建筑'] },
{ name: '大理双廊海景客栈', stars: 3, rating: 4.4, price: 320, district: '双廊', features: ['海景房', '文艺'] }
]
},
{
destId: 'D006',
name: '厦门',
nameEn: 'Xiamen',
cover: 'https://picsum.photos/seed/xiamen/400/300',
rating: 4.6,
description: '海上花园,鼓浪屿的琴声悠扬,闽南风情与文艺小清新的完美融合。',
bestSeason: '3月-5月、10月-12月',
bestSeasonDesc: '避暑避寒皆宜',
tags: ['海岛', '文艺', '美食'],
transport: [
{ type: 'flight', label: '直飞航班', from: '北京', to: '厦门', duration: '3h', price: 620, carrier: '厦门航空' },
{ type: 'flight', label: '直飞航班', from: '上海', to: '厦门', duration: '1h50m', price: 480, carrier: '东方航空' },
{ type: 'train', label: '高铁', from: '福州', to: '厦门', duration: '1h30m', price: 95, carrier: '铁路12306' }
],
hotels: [
{ name: '厦门华尔道夫酒店', stars: 5, rating: 4.9, price: 1380, district: '思明区', features: ['奢华', '城市景观'] },
{ name: '厦门鼓浪屿林氏府酒店', stars: 4, rating: 4.6, price: 650, district: '鼓浪屿', features: ['百年别墅', '历史'] },
{ name: '厦门曾厝垵民宿', stars: 3, rating: 4.3, price: 280, district: '曾厝垵', features: ['文艺', '海边'] }
]
}
]
// 天气模拟数据(来自 seed.js)
const weatherData = {
D001: { temp: 30, condition: '晴', humidity: 70, wind: '3级', icon: '☀️', suggestion: '适宜海边活动,注意防晒' },
D002: { temp: 18, condition: '多云', humidity: 55, wind: '2级', icon: '⛅', suggestion: '早晚温差大,建议携带外套' },
D003: { temp: 24, condition: '阴', humidity: 65, wind: '2级', icon: '☁️', suggestion: '适宜户外活动,推荐火锅' },
D004: { temp: 22, condition: '小雨', humidity: 80, wind: '3级', icon: '🌦️', suggestion: '建议携带雨具,雨中西湖别样美' },
D005: { temp: 20, condition: '晴', humidity: 50, wind: '4级', icon: '☀️', suggestion: '洱海边风大,建议带防风外套' },
D006: { temp: 26, condition: '多云', humidity: 72, wind: '3级', icon: '⛅', suggestion: '适宜环岛路骑行' }
}
// 旅行贴士(来自 seed.js)
const travelTips = [
{
id: 'T01',
category: '行前准备',
icon: '🎒',
title: '提前预订省更多',
content: '建议提前2-4周预订机票和酒店,可节省20%-30%的费用。关注航司会员日和酒店促销活动。',
priority: 1
},
{
id: 'T02',
category: '交通出行',
icon: '🚗',
title: '租车 vs 打车',
content: '家庭出行推荐租车,日均约200元起;2人以内建议打车或网约车,比租车更划算。',
priority: 2
},
{
id: 'T03',
category: '住宿选择',
icon: '🏨',
title: '住宿地段建议',
content: '选择交通便利的市区或景区附近住宿,节省通勤时间。多看住客评价,重点关注"位置"和"卫生"评分。',
priority: 3
},
{
id: 'T04',
category: '美食推荐',
icon: '🍜',
title: '避开景区餐饮',
content: '景区内餐饮通常溢价30%-50%,步行10分钟到居民区就能找到更地道实惠的美食。',
priority: 4
},
{
id: 'T05',
category: '安全提醒',
icon: '🔒',
title: '旅行保险建议',
content: '国内出行建议购买旅游意外险(10元/天起),包含医疗、财产损失等保障,花小钱买安心。',
priority: 5
}
]
// 云函数入口函数
exports.main = async (event, context) => {
const { action } = event
const wxContext = cloud.getWXContext()
const openid = wxContext.OPENID
switch (action) {
case 'searchDestinations': {
const { keyword, tag } = event
let results = destinations
if (keyword) {
const kw = keyword.toLowerCase()
results = results.filter(d =>
d.name.includes(kw) ||
d.nameEn.toLowerCase().includes(kw) ||
d.tags.some(t => t.includes(kw)) ||
d.description.includes(kw)
)
}
if (tag) {
results = results.filter(d => d.tags.includes(tag))
}
return { code: 0, data: { items: results } }
}
case 'getWeatherInfo': {
const { destId } = event
if (!destId) {
return { code: -1, msg: '缺少目的地ID' }
}
const weather = weatherData[destId]
if (!weather) {
return { code: -1, msg: '未找到该目的地的天气信息' }
}
return { code: 0, data: weather }
}
case 'planTrip': {
const { destId, departureDate, returnDate, budget, transport, hotel } = event
if (!destId || !departureDate || !returnDate) {
return { code: -1, msg: '参数不完整' }
}
const dest = destinations.find(d => d.destId === destId)
if (!dest) {
return { code: -1, msg: '目的地不存在' }
}
const planId = 'TP' + Date.now()
const plan = {
planId,
destination: dest.name,
departureDate,
returnDate,
budget: budget || 0,
transport: transport || null,
hotel: hotel || null,
status: 'planned',
openid,
createdAt: new Date()
}
await db.collection('travel_plans').add({ data: plan })
return { code: 0, data: { planId, status: 'planned', destination: dest.name } }
}
case 'getTravelTips': {
const { category } = event
let tips = travelTips
if (category) {
tips = tips.filter(t => t.category === category)
}
return { code: 0, data: tips }
}
default:
return { code: -1, msg: `未知 action: ${action}` }
}
}
{
"name": "travel-skill-handler",
"version": "1.0.0",
"description": "travel-skill 云函数",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}
// skills/travel-skill/components/destination-list-card/index.js
Component({
data: {
items: [],
keyword: ''
},
lifetimes: {
created() {
console.info('[ai-mode] destination-list-card created')
const { NotificationType } = wx.modelContext
const modelCtx = wx.modelContext.getContext(this)
modelCtx.on(NotificationType.Result, (data) => {
const sc = (data && data.result && data.result.structuredContent) || {}
console.info('[ai-mode] destination-list-card 收到 Result:', JSON.stringify(sc))
this.setData({
items: sc.items || [],
keyword: sc.keyword || ''
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] destination-list-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] destination-list-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] destination-list-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] destination-list-card overflow monitor=on')
}
},
methods: {
onTapPlan(e) {
const { destId, name } = e.currentTarget.dataset
console.info(`[ai-mode] destination-list-card send api/call name=planTrip args=${JSON.stringify({ destId })}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: `规划${name}行程` },
{ type: 'api/call', data: { name: 'planTrip', arguments: { destId } } }
]
})
},
onTapWeather(e) {
const { destId, name } = e.currentTarget.dataset
console.info(`[ai-mode] destination-list-card send api/call name=getWeatherInfo args=${JSON.stringify({ destId })}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: `${name}天气` },
{ type: 'api/call', data: { name: 'getWeatherInfo', arguments: { destId } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="dl-card">
<view class="dl-header">
<view class="dl-title">热门目的地</view>
<view wx:if="{{keyword}}" class="dl-keyword">「{{keyword}}」</view>
</view>
<view wx:if="{{!items.length}}" class="dl-empty">
<view class="dl-empty-title">暂无匹配目的地</view>
<view class="dl-empty-desc">{{keyword ? '请换个关键词试试' : '请稍后再试'}}</view>
</view>
<scroll-view class="dl-scroll" scroll-y scroll-x="{{true}}" enhanced show-scrollbar="{{false}}" upper-threshold="0">
<block wx:for="{{items}}" wx:key="destId">
<view class="dl-item" data-dest-id="{{item.destId}}">
<image class="dl-cover" src="{{item.cover}}" mode="aspectFill" />
<view class="dl-info">
<view class="dl-name-row">
<text class="dl-name">{{item.name}}</text>
<text class="dl-en-name">{{item.nameEn}}</text>
<view class="dl-rating">
<text class="dl-star">★</text>
<text class="dl-score">{{item.rating}}</text>
</view>
</view>
<view class="dl-desc">{{item.description}}</view>
<view class="dl-meta-row">
<view class="dl-season">
<text class="dl-label">最佳季节</text>
<text class="dl-value">{{item.bestSeason}}</text>
</view>
<view class="dl-season-desc">{{item.bestSeasonDesc}}</view>
</view>
<view class="dl-tags">
<text wx:for="{{item.tags}}" wx:key="this" class="dl-tag">{{item}}</text>
</view>
<view class="dl-actions">
<view
class="dl-btn dl-btn-plan"
hover-class="dl-btn-hover"
bind:tap="onTapPlan"
data-dest-id="{{item.destId}}"
data-name="{{item.name}}"
>规划行程</view>
<view
class="dl-btn dl-btn-weather"
hover-class="dl-btn-hover"
bind:tap="onTapWeather"
data-dest-id="{{item.destId}}"
data-name="{{item.name}}"
>查天气</view>
</view>
</view>
</view>
</block>
</scroll-view>
</view>
/* ratio=1:1;蓝紫渐变探索感 */
/* 强调色:#667EEA(蓝紫)配 #F5F0FF(淡紫底) */
.dl-card {
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 16px;
padding: 16px;
box-sizing: border-box;
overflow: hidden;
box-shadow: 0 10rpx 36rpx rgba(102, 126, 234, 0.08);
}
.dl-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.dl-title {
font-size: 17px;
font-weight: 600;
color: rgba(0,0,0,0.85);
}
.dl-keyword {
font-size: 13px;
color: #667EEA;
background: #F5F0FF;
padding: 2px 10px;
border-radius: 999rpx;
}
.dl-scroll {
max-height: 520px;
}
.dl-empty {
margin-top: 16px;
padding: 24px 16px;
background: #F5F0FF;
border: 1px dashed #C4B5FD;
border-radius: 16px;
text-align: center;
}
.dl-empty-title {
font-size: 15px;
color: rgba(0,0,0,0.85);
}
.dl-empty-desc {
margin-top: 6px;
font-size: 13px;
color: rgba(0,0,0,0.50);
}
.dl-item {
display: flex;
margin-top: 12px;
padding: 12px;
background: linear-gradient(180deg, #F5F0FF 0%, #FFFFFF 100%);
border: 1px solid #E2E8F0;
border-radius: 16px;
gap: 12px;
}
.dl-cover {
width: 120px;
height: 120px;
border-radius: 12px;
flex-shrink: 0;
background: #E2E8F0;
}
.dl-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.dl-name-row {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.dl-name {
font-size: 17px;
font-weight: 600;
color: rgba(0,0,0,0.85);
}
.dl-en-name {
font-size: 13px;
color: rgba(0,0,0,0.30);
}
.dl-rating {
display: flex;
align-items: center;
margin-left: auto;
}
.dl-star {
color: #F59E0B;
font-size: 13px;
}
.dl-score {
font-size: 13px;
font-weight: 600;
color: #F59E0B;
margin-left: 2px;
}
.dl-desc {
font-size: 13px;
color: rgba(0,0,0,0.50);
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.dl-meta-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.dl-season {
display: flex;
align-items: center;
gap: 4px;
}
.dl-label {
font-size: 12px;
color: rgba(0,0,0,0.30);
}
.dl-value {
font-size: 12px;
font-weight: 500;
color: #667EEA;
}
.dl-season-desc {
font-size: 12px;
color: rgba(0,0,0,0.50);
}
.dl-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.dl-tag {
font-size: 11px;
color: #667EEA;
background: #F5F0FF;
padding: 2px 8px;
border-radius: 999rpx;
}
.dl-actions {
display: flex;
gap: 8px;
margin-top: 4px;
}
.dl-btn {
flex: 1;
height: 36px;
line-height: 36px;
text-align: center;
font-size: 14px;
border-radius: 12px;
}
.dl-btn-plan {
color: #FFFFFF;
background: linear-gradient(135deg, #764BA2, #667EEA);
}
.dl-btn-weather {
color: #667EEA;
background: #F5F0FF;
border: 1px solid #C4B5FD;
}
.dl-btn-hover {
opacity: 0.85;
}
/* 暗黑模式:深紫灰底 #1A1A2E */
@media (prefers-color-scheme: dark) {
.dl-card {
background: #1A1A2E;
border-color: #2D2D4A;
box-shadow: none;
}
.dl-title { color: rgba(255,255,255,0.90); }
.dl-keyword { background: #2D2D4A; color: #A78BFA; }
.dl-empty { background: #1A1A2E; border-color: #2D2D4A; }
.dl-empty-title { color: rgba(255,255,255,0.85); }
.dl-empty-desc { color: rgba(255,255,255,0.45); }
.dl-item {
background: linear-gradient(180deg, #1E1E3A 0%, #1A1A2E 100%);
border-color: #2D2D4A;
}
.dl-name { color: rgba(255,255,255,0.90); }
.dl-en-name { color: rgba(255,255,255,0.30); }
.dl-desc { color: rgba(255,255,255,0.50); }
.dl-label { color: rgba(255,255,255,0.30); }
.dl-value { color: #A78BFA; }
.dl-season-desc { color: rgba(255,255,255,0.45); }
.dl-tag { background: #2D2D4A; color: #A78BFA; }
.dl-btn-weather {
background: #2D2D4A;
color: #A78BFA;
border-color: #3D3D5A;
}
}
// skills/travel-skill/components/tips-card/index.js
Component({
data: {
items: [],
expandedId: ''
},
lifetimes: {
created() {
console.info('[ai-mode] tips-card created')
const { NotificationType } = wx.modelContext
const modelCtx = wx.modelContext.getContext(this)
modelCtx.on(NotificationType.Result, (data) => {
const sc = (data && data.result && data.result.structuredContent) || {}
console.info('[ai-mode] tips-card 收到 Result:', JSON.stringify(sc))
this.setData({ items: sc.items || [] })
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] tips-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] tips-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] tips-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] tips-card overflow monitor=on')
}
},
methods: {
onToggle(e) {
const { id } = e.currentTarget.dataset
this.setData({
expandedId: this.data.expandedId === id ? '' : id
})
},
onTapSearch(e) {
console.info('[ai-mode] tips-card send api/call name=searchDestinations')
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '开始规划旅行' },
{ type: 'api/call', data: { name: 'searchDestinations', arguments: { keyword: '' } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="tc-card">
<view class="tc-header">
<view class="tc-title">旅行贴士</view>
<view class="tc-subtitle">出行前必看的实用建议</view>
</view>
<view wx:if="{{!items.length}}" class="tc-empty">
<view class="tc-empty-text">暂无旅行贴士</view>
</view>
<view wx:else class="tc-list">
<view
wx:for="{{items}}"
wx:key="id"
class="tc-item {{expandedId === item.id ? 'is-expanded' : ''}}"
data-id="{{item.id}}"
bind:tap="onToggle"
>
<view class="tc-item-head">
<text class="tc-item-icon">{{item.icon}}</text>
<view class="tc-item-info">
<view class="tc-item-category">{{item.category}}</view>
<view class="tc-item-title">{{item.title}}</view>
</view>
<view class="tc-item-arrow">{{expandedId === item.id ? '▼' : '▶'}}</view>
</view>
<view wx:if="{{expandedId === item.id}}" class="tc-item-body">
<text class="tc-item-content">{{item.content}}</text>
</view>
</view>
</view>
<view class="tc-footer">
<view
class="tc-btn"
hover-class="tc-btn-hover"
bind:tap="onTapSearch"
>开始规划旅行</view>
</view>
</view>
/* ratio=4:3;蓝紫渐变探索感 */
/* 强调色:#667EEA(蓝紫)配 #F5F0FF(淡紫底) */
.tc-card {
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 10rpx 36rpx rgba(102, 126, 234, 0.08);
}
.tc-header {
padding: 16px 16px 12px;
border-bottom: 1px solid #F0F0F5;
}
.tc-title {
font-size: 17px;
font-weight: 600;
color: rgba(0,0,0,0.85);
}
.tc-subtitle {
font-size: 13px;
color: rgba(0,0,0,0.50);
margin-top: 2px;
}
.tc-empty {
padding: 32px 16px;
text-align: center;
}
.tc-empty-text {
font-size: 15px;
color: rgba(0,0,0,0.50);
}
.tc-list {
padding: 4px 0;
}
.tc-item {
padding: 12px 16px;
border-bottom: 1px solid #F0F0F5;
}
.tc-item-head {
display: flex;
align-items: center;
gap: 12px;
}
.tc-item-icon {
font-size: 24px;
width: 32px;
text-align: center;
flex-shrink: 0;
}
.tc-item-info {
flex: 1;
min-width: 0;
}
.tc-item-category {
font-size: 12px;
color: #667EEA;
font-weight: 500;
}
.tc-item-title {
font-size: 15px;
font-weight: 500;
color: rgba(0,0,0,0.85);
margin-top: 2px;
}
.tc-item-arrow {
font-size: 12px;
color: rgba(0,0,0,0.30);
flex-shrink: 0;
}
.tc-item-body {
margin-top: 10px;
padding: 12px;
background: #F5F0FF;
border-radius: 12px;
}
.tc-item-content {
font-size: 14px;
color: rgba(0,0,0,0.65);
line-height: 1.6;
}
.tc-footer {
padding: 12px 16px;
border-top: 1px solid #E2E8F0;
background: #F5F0FF;
}
.tc-btn {
height: 40px;
line-height: 40px;
text-align: center;
font-size: 15px;
font-weight: 600;
color: #FFFFFF;
background: linear-gradient(135deg, #764BA2, #667EEA);
border-radius: 12px;
}
.tc-btn-hover {
opacity: 0.85;
}
/* 暗黑模式 */
@media (prefers-color-scheme: dark) {
.tc-card {
background: #1A1A2E;
border-color: #2D2D4A;
box-shadow: none;
}
.tc-header { border-color: #2D2D4A; }
.tc-title { color: rgba(255,255,255,0.90); }
.tc-subtitle { color: rgba(255,255,255,0.45); }
.tc-empty-text { color: rgba(255,255,255,0.45); }
.tc-item { border-color: #2D2D4A; }
.tc-item-title { color: rgba(255,255,255,0.90); }
.tc-item-category { color: #A78BFA; }
.tc-item-arrow { color: rgba(255,255,255,0.30); }
.tc-item-body { background: #1E1E3A; }
.tc-item-content { color: rgba(255,255,255,0.65); }
.tc-footer {
background: #1E1E3A;
border-color: #2D2D4A;
}
}
// skills/travel-skill/components/trip-plan-card/index.js
Component({
data: {
dest: null,
transport: [],
hotels: [],
activeTab: 'transport'
},
lifetimes: {
created() {
console.info('[ai-mode] trip-plan-card created')
const { NotificationType } = wx.modelContext
const modelCtx = wx.modelContext.getContext(this)
modelCtx.on(NotificationType.Result, (data) => {
const sc = (data && data.result && data.result.structuredContent) || {}
console.info('[ai-mode] trip-plan-card 收到 Result:', JSON.stringify(sc))
this.setData({
dest: sc.dest || null,
transport: sc.transport || [],
hotels: sc.hotels || []
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] trip-plan-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] trip-plan-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] trip-plan-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] trip-plan-card overflow monitor=on')
}
},
methods: {
onSwitchTab(e) {
const tab = e.currentTarget.dataset.tab
this.setData({ activeTab: tab })
},
onTapWeather(e) {
const { destId, name } = e.currentTarget.dataset
console.info(`[ai-mode] trip-plan-card send api/call name=getWeatherInfo args=${JSON.stringify({ destId })}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: `${name}天气` },
{ type: 'api/call', data: { name: 'getWeatherInfo', arguments: { destId } } }
]
})
},
onTapBack(e) {
const { keyword } = e.currentTarget.dataset
console.info('[ai-mode] trip-plan-card send api/call name=searchDestinations')
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '返回目的地列表' },
{ type: 'api/call', data: { name: 'searchDestinations', arguments: { keyword: keyword || '' } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="tp-card">
<view wx:if="{{!dest}}" class="tp-empty">
<view class="tp-empty-title">暂未选择目的地</view>
<view class="tp-empty-desc">请先搜索并选择一个目的地</view>
</view>
<block wx:else>
<view class="tp-hero">
<image class="tp-cover" src="{{dest.cover}}" mode="aspectFill" />
<view class="tp-hero-overlay">
<view class="tp-hero-name">{{dest.name}}</view>
<view class="tp-hero-en">{{dest.nameEn}}</view>
<view class="tp-hero-rating">★ {{dest.rating}}</view>
<view class="tp-hero-season">{{dest.bestSeasonDesc}}</view>
</view>
</view>
<view class="tp-desc">{{dest.description}}</view>
<view class="tp-tabs">
<view
class="tp-tab {{activeTab === 'transport' ? 'is-active' : ''}}"
data-tab="transport"
bind:tap="onSwitchTab"
>
交通方案
<view wx:if="{{activeTab === 'transport'}}" class="tp-tab-active-bar"></view>
</view>
<view
class="tp-tab {{activeTab === 'hotels' ? 'is-active' : ''}}"
data-tab="hotels"
bind:tap="onSwitchTab"
>
推荐住宿
<view wx:if="{{activeTab === 'hotels'}}" class="tp-tab-active-bar"></view>
</view>
</view>
<!-- 交通方案 -->
<view wx:if="{{activeTab === 'transport'}}" class="tp-section">
<view wx:for="{{transport}}" wx:key="type" class="tp-item">
<view class="tp-item-icon">
<text wx:if="{{item.type === 'flight'}}">✈️</text>
<text wx:elif="{{item.type === 'train'}}">🚄</text>
<text wx:else>🚌</text>
</view>
<view class="tp-item-body">
<view class="tp-item-label">{{item.label}}</view>
<view class="tp-item-route">{{item.from}} → {{item.to}}</view>
<view class="tp-item-carrier">{{item.carrier}}</view>
</view>
<view class="tp-item-right">
<view class="tp-item-duration">{{item.duration}}</view>
<view class="tp-item-price">¥{{item.price}}起</view>
</view>
</view>
</view>
<!-- 推荐住宿 -->
<view wx:elif="{{activeTab === 'hotels'}}" class="tp-section">
<view wx:for="{{hotels}}" wx:key="name" class="tp-item">
<view class="tp-item-icon">🏨</view>
<view class="tp-item-body">
<view class="tp-item-label">
<text>{{item.name}}</text>
<text wx:if="{{item.stars >= 5}}" class="tp-star-tag">奢华</text>
</view>
<view class="tp-item-stars">
<text wx:for="{{item.stars}}" wx:key="*">★</text>
<text class="tp-hotel-rating">{{item.rating}}分</text>
</view>
<view class="tp-item-features">
<text wx:for="{{item.features}}" wx:key="this" class="tp-feature-tag">{{item}}</text>
</view>
<view wx:if="{{item.district}}" class="tp-item-district">{{item.district}}</view>
</view>
<view class="tp-item-right">
<view class="tp-item-price">¥{{item.price}}</view>
<view class="tp-item-unit">/晚起</view>
</view>
</view>
</view>
<view class="tp-footer-actions">
<view
class="tp-btn tp-btn-weather"
hover-class="tp-btn-hover"
bind:tap="onTapWeather"
data-dest-id="{{dest.destId}}"
data-name="{{dest.name}}"
>🌤 查天气</view>
<view
class="tp-btn tp-btn-back"
hover-class="tp-btn-hover"
bind:tap="onTapBack"
data-keyword=""
>← 返回列表</view>
</view>
</block>
</view>
/* ratio=4:3;蓝紫渐变探索感 */
/* 强调色:#667EEA(蓝紫)配 #F5F0FF(淡紫底) */
.tp-card {
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 10rpx 36rpx rgba(102, 126, 234, 0.08);
}
.tp-empty {
padding: 48px 16px;
text-align: center;
}
.tp-empty-title {
font-size: 17px;
font-weight: 600;
color: rgba(0,0,0,0.85);
}
.tp-empty-desc {
margin-top: 8px;
font-size: 15px;
color: rgba(0,0,0,0.50);
}
.tp-hero {
position: relative;
height: 180px;
overflow: hidden;
}
.tp-cover {
width: 100%;
height: 100%;
}
.tp-hero-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
background: linear-gradient(transparent, rgba(26, 26, 46, 0.85));
}
.tp-hero-name {
font-size: 20px;
font-weight: 700;
color: #FFFFFF;
}
.tp-hero-en {
font-size: 13px;
color: rgba(255,255,255,0.70);
}
.tp-hero-rating {
font-size: 13px;
color: #FCD34D;
margin-top: 4px;
}
.tp-hero-season {
font-size: 12px;
color: rgba(255,255,255,0.80);
margin-top: 2px;
}
.tp-desc {
padding: 12px 16px;
font-size: 15px;
color: rgba(0,0,0,0.50);
line-height: 1.5;
background: #F5F0FF;
border-bottom: 1px solid #E2E8F0;
}
.tp-tabs {
display: flex;
border-bottom: 1px solid #E2E8F0;
}
.tp-tab {
flex: 1;
height: 44px;
line-height: 44px;
text-align: center;
font-size: 15px;
font-weight: 500;
color: rgba(0,0,0,0.50);
position: relative;
}
.tp-tab.is-active {
color: #667EEA;
font-weight: 600;
position: relative;
}
.tp-tab-active-bar {
position: absolute;
bottom: 0;
left: 20%;
right: 20%;
height: 3px;
background: linear-gradient(135deg, #764BA2, #667EEA);
border-radius: 3px 3px 0 0;
}
.tp-section {
padding: 8px 16px 16px;
}
.tp-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid #F0F0F5;
}
.tp-item-icon {
font-size: 24px;
width: 36px;
text-align: center;
flex-shrink: 0;
}
.tp-item-body {
flex: 1;
min-width: 0;
}
.tp-item-label {
font-size: 15px;
font-weight: 600;
color: rgba(0,0,0,0.85);
display: flex;
align-items: center;
gap: 6px;
}
.tp-star-tag {
font-size: 11px;
color: #667EEA;
background: #F5F0FF;
padding: 1px 6px;
border-radius: 4px;
}
.tp-item-route,
.tp-item-carrier {
font-size: 13px;
color: rgba(0,0,0,0.50);
margin-top: 2px;
}
.tp-item-stars {
font-size: 13px;
color: #F59E0B;
margin-top: 2px;
display: flex;
align-items: center;
gap: 4px;
}
.tp-hotel-rating {
font-size: 12px;
color: rgba(0,0,0,0.50);
}
.tp-item-features {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-top: 4px;
}
.tp-feature-tag {
font-size: 11px;
color: #667EEA;
background: #F5F0FF;
padding: 1px 8px;
border-radius: 4px;
}
.tp-item-district {
font-size: 12px;
color: rgba(0,0,0,0.30);
margin-top: 4px;
}
.tp-item-right {
text-align: right;
flex-shrink: 0;
}
.tp-item-duration {
font-size: 13px;
font-weight: 500;
color: #667EEA;
}
.tp-item-price {
font-size: 17px;
font-weight: 700;
color: #EF4444;
}
.tp-item-unit {
font-size: 11px;
color: rgba(0,0,0,0.30);
}
.tp-footer-actions {
display: flex;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid #E2E8F0;
background: #F5F0FF;
}
.tp-btn {
flex: 1;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 15px;
border-radius: 12px;
}
.tp-btn-weather {
color: #FFFFFF;
background: linear-gradient(135deg, #764BA2, #667EEA);
}
.tp-btn-back {
color: #667EEA;
background: #FFFFFF;
border: 1px solid #C4B5FD;
}
.tp-btn-hover {
opacity: 0.85;
}
/* 暗黑模式 */
@media (prefers-color-scheme: dark) {
.tp-card {
background: #1A1A2E;
border-color: #2D2D4A;
box-shadow: none;
}
.tp-empty-title { color: rgba(255,255,255,0.90); }
.tp-empty-desc { color: rgba(255,255,255,0.45); }
.tp-desc {
background: #1E1E3A;
color: rgba(255,255,255,0.50);
border-color: #2D2D4A;
}
.tp-tabs { border-color: #2D2D4A; }
.tp-tab { color: rgba(255,255,255,0.45); }
.tp-tab.is-active { color: #A78BFA; }
.tp-item { border-color: #2D2D4A; }
.tp-item-label { color: rgba(255,255,255,0.90); }
.tp-item-route,
.tp-item-carrier { color: rgba(255,255,255,0.45); }
.tp-item-district { color: rgba(255,255,255,0.30); }
.tp-hotel-rating { color: rgba(255,255,255,0.45); }
.tp-star-tag { background: #2D2D4A; color: #A78BFA; }
.tp-feature-tag { background: #2D2D4A; color: #A78BFA; }
.tp-item-unit { color: rgba(255,255,255,0.30); }
.tp-footer-actions {
background: #1E1E3A;
border-color: #2D2D4A;
}
.tp-btn-back {
background: #1A1A2E;
color: #A78BFA;
border-color: #3D3D5A;
}
}
// skills/travel-skill/components/weather-card/index.js
Component({
data: {
weather: null,
destName: ''
},
lifetimes: {
created() {
console.info('[ai-mode] weather-card created')
const { NotificationType } = wx.modelContext
const modelCtx = wx.modelContext.getContext(this)
modelCtx.on(NotificationType.Result, (data) => {
const sc = (data && data.result && data.result.structuredContent) || {}
console.info('[ai-mode] weather-card 收到 Result:', JSON.stringify(sc))
this.setData({
weather: sc.weather || null,
destName: sc.destName || ''
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] weather-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] weather-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] weather-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] weather-card overflow monitor=on')
}
},
methods: {
onTapPlan(e) {
const { destId, name } = e.currentTarget.dataset
console.info(`[ai-mode] weather-card send api/call name=planTrip args=${JSON.stringify({ destId })}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: `规划${name}行程` },
{ type: 'api/call', data: { name: 'planTrip', arguments: { destId } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="wc-card">
<view wx:if="{{!weather}}" class="wc-empty">
<view class="wc-empty-text">暂无天气数据</view>
</view>
<block wx:else>
<view class="wc-row">
<view class="wc-dest">{{destName}}</view>
<view class="wc-icon">{{weather.icon}}</view>
<view class="wc-temp">{{weather.temp}}°C</view>
<view class="wc-condition">{{weather.condition}}</view>
<view class="wc-detail">
<text class="wc-detail-item">湿度 {{weather.humidity}}%</text>
<text class="wc-detail-item">风力 {{weather.wind}}</text>
</view>
<view class="wc-suggestion">{{weather.suggestion}}</view>
</view>
</block>
</view>
/* ratio=4:1;蓝紫渐变探索感 */
/* 强调色:#667EEA(蓝紫)配 #F5F0FF(淡紫底) */
.wc-card {
background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
border-radius: 16px;
padding: 12px 16px;
box-sizing: border-box;
}
.wc-empty {
text-align: center;
padding: 12px;
}
.wc-empty-text {
font-size: 15px;
color: rgba(255,255,255,0.70);
}
.wc-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.wc-dest {
font-size: 15px;
font-weight: 600;
color: #FFFFFF;
}
.wc-icon {
font-size: 28px;
}
.wc-temp {
font-size: 28px;
font-weight: 700;
color: #FFFFFF;
letter-spacing: -1px;
}
.wc-condition {
font-size: 15px;
color: rgba(255,255,255,0.85);
}
.wc-detail {
display: flex;
gap: 8px;
font-size: 12px;
color: rgba(255,255,255,0.70);
}
.wc-detail-item {
background: rgba(255,255,255,0.15);
padding: 2px 8px;
border-radius: 999rpx;
}
.wc-suggestion {
width: 100%;
font-size: 13px;
color: rgba(255,255,255,0.80);
margin-top: 4px;
}
/* 暗黑模式 */
@media (prefers-color-scheme: dark) {
.wc-card {
background: linear-gradient(135deg, #4C3F7A 0%, #2D2D5A 100%);
}
}
// skills/travel-skill/data/seed.js
// 6 个热门目的地种子数据
const destinations = [
{
destId: 'D001',
name: '三亚',
nameEn: 'Sanya',
cover: 'https://picsum.photos/seed/sanya/400/300',
rating: 4.8,
description: '中国最美海滨城市,热带天堂,阳光、沙滩、椰林与碧海蓝天。',
bestSeason: '10月-次年4月',
bestSeasonDesc: '气候宜人,避寒胜地',
tags: ['海滨', '度假', '潜水'],
transport: [
{ type: 'flight', label: '直飞航班', from: '北京', to: '三亚', duration: '3h50m', price: 680, carrier: '海南航空' },
{ type: 'flight', label: '直飞航班', from: '上海', to: '三亚', duration: '3h10m', price: 520, carrier: '东方航空' },
{ type: 'train', label: '动车', from: '海口', to: '三亚', duration: '1h30m', price: 108, carrier: '铁路12306' }
],
hotels: [
{ name: '三亚海棠湾艾迪逊酒店', stars: 5, rating: 4.9, price: 1288, district: '海棠湾', features: ['私人沙滩', '无边泳池'] },
{ name: '三亚亚龙湾万豪度假酒店', stars: 5, rating: 4.7, price: 899, district: '亚龙湾', features: ['海景房', '亲子'] },
{ name: '三亚湾海居铂尔曼酒店', stars: 4, rating: 4.5, price: 499, district: '三亚湾', features: ['性价比高', '近市区'] }
]
},
{
destId: 'D002',
name: '丽江',
nameEn: 'Lijiang',
cover: 'https://picsum.photos/seed/lijiang/400/300',
rating: 4.7,
description: '世界文化遗产古城,雪山脚下的小桥流水人家,感受纳西族风情。',
bestSeason: '3月-5月、9月-11月',
bestSeasonDesc: '春暖花开或秋高气爽',
tags: ['古城', '人文', '雪山'],
transport: [
{ type: 'flight', label: '直飞航班', from: '北京', to: '丽江', duration: '4h', price: 880, carrier: '中国国航' },
{ type: 'flight', label: '直飞航班', from: '上海', to: '丽江', duration: '3h40m', price: 750, carrier: '吉祥航空' },
{ type: 'train', label: '动车', from: '昆明', to: '丽江', duration: '3h', price: 220, carrier: '铁路12306' }
],
hotels: [
{ name: '丽江悦榕庄', stars: 5, rating: 4.8, price: 1580, district: '束河古镇', features: ['雪山景观', 'SPA'] },
{ name: '丽江古城英迪格酒店', stars: 5, rating: 4.6, price: 780, district: '大研古城', features: ['纳西风格', '古城内'] },
{ name: '丽江花间堂客栈', stars: 4, rating: 4.5, price: 380, district: '大研古城', features: ['特色民宿', '庭院'] }
]
},
{
destId: 'D003',
name: '成都',
nameEn: 'Chengdu',
cover: 'https://picsum.photos/seed/chengdu/400/300',
rating: 4.9,
description: '天府之国,美食之都,熊猫的故乡,体验慢生活与麻辣鲜香。',
bestSeason: '3月-6月、9月-11月',
bestSeasonDesc: '气温舒适,美食四季皆宜',
tags: ['美食', '熊猫', '休闲'],
transport: [
{ type: 'flight', label: '直飞航班', from: '北京', to: '成都', duration: '3h', price: 620, carrier: '四川航空' },
{ type: 'flight', label: '直飞航班', from: '上海', to: '成都', duration: '3h15m', price: 580, carrier: '东方航空' },
{ type: 'train', label: '高铁', from: '重庆', to: '成都', duration: '1h', price: 150, carrier: '铁路12306' }
],
hotels: [
{ name: '成都博舍酒店', stars: 5, rating: 4.9, price: 1480, district: '太古里', features: ['设计感', '太古里核心'] },
{ name: '成都群光君悦酒店', stars: 5, rating: 4.7, price: 920, district: '春熙路', features: ['高空景观', '购物便利'] },
{ name: '成都春熙路亚朵酒店', stars: 4, rating: 4.6, price: 480, district: '春熙路', features: ['性价比', '舒适'] }
]
},
{
destId: 'D004',
name: '杭州',
nameEn: 'Hangzhou',
cover: 'https://picsum.photos/seed/hangzhou/400/300',
rating: 4.8,
description: '上有天堂下有苏杭,西湖美景天下闻名,江南水乡的诗意画卷。',
bestSeason: '3月-5月、9月-11月',
bestSeasonDesc: '烟雨江南最美时节',
tags: ['西湖', '江南', '文化'],
transport: [
{ type: 'flight', label: '直飞航班', from: '北京', to: '杭州', duration: '2h15m', price: 560, carrier: '中国国航' },
{ type: 'flight', label: '直飞航班', from: '广州', to: '杭州', duration: '2h', price: 480, carrier: '南方航空' },
{ type: 'train', label: '高铁', from: '上海', to: '杭州', duration: '45m', price: 73, carrier: '铁路12306' }
],
hotels: [
{ name: '杭州西子湖四季酒店', stars: 5, rating: 4.9, price: 1880, district: '西湖', features: ['西湖景观', '园林'] },
{ name: '杭州柏悦酒店', stars: 5, rating: 4.8, price: 1180, district: '钱江新城', features: ['高空大堂', '江景'] },
{ name: '杭州全季酒店西湖店', stars: 4, rating: 4.5, price: 420, district: '西湖', features: ['位置佳', '简约'] }
]
},
{
destId: 'D005',
name: '大理',
nameEn: 'Dali',
cover: 'https://picsum.photos/seed/dali/400/300',
rating: 4.7,
description: '风花雪月,苍山洱海,白族文化的发源地,文艺青年的诗和远方。',
bestSeason: '3月-5月、9月-11月',
bestSeasonDesc: '洱海风光最美',
tags: ['洱海', '文艺', '古镇'],
transport: [
{ type: 'flight', label: '直飞航班', from: '北京', to: '大理', duration: '3h50m', price: 920, carrier: '东方航空' },
{ type: 'flight', label: '直飞航班', from: '成都', to: '大理', duration: '1h30m', price: 380, carrier: '四川航空' },
{ type: 'train', label: '动车', from: '昆明', to: '大理', duration: '2h', price: 145, carrier: '铁路12306' }
],
hotels: [
{ name: '大理海纳尔云墅酒店', stars: 5, rating: 4.8, price: 1680, district: '洱海边', features: ['洱海全景', '无边泳池'] },
{ name: '大理古城一号院', stars: 4, rating: 4.6, price: 680, district: '大理古城', features: ['庭院', '白族建筑'] },
{ name: '大理双廊海景客栈', stars: 3, rating: 4.4, price: 320, district: '双廊', features: ['海景房', '文艺'] }
]
},
{
destId: 'D006',
name: '厦门',
nameEn: 'Xiamen',
cover: 'https://picsum.photos/seed/xiamen/400/300',
rating: 4.6,
description: '海上花园,鼓浪屿的琴声悠扬,闽南风情与文艺小清新的完美融合。',
bestSeason: '3月-5月、10月-12月',
bestSeasonDesc: '避暑避寒皆宜',
tags: ['海岛', '文艺', '美食'],
transport: [
{ type: 'flight', label: '直飞航班', from: '北京', to: '厦门', duration: '3h', price: 620, carrier: '厦门航空' },
{ type: 'flight', label: '直飞航班', from: '上海', to: '厦门', duration: '1h50m', price: 480, carrier: '东方航空' },
{ type: 'train', label: '高铁', from: '福州', to: '厦门', duration: '1h30m', price: 95, carrier: '铁路12306' }
],
hotels: [
{ name: '厦门华尔道夫酒店', stars: 5, rating: 4.9, price: 1380, district: '思明区', features: ['奢华', '城市景观'] },
{ name: '厦门鼓浪屿林氏府酒店', stars: 4, rating: 4.6, price: 650, district: '鼓浪屿', features: ['百年别墅', '历史'] },
{ name: '厦门曾厝垵民宿', stars: 3, rating: 4.3, price: 280, district: '曾厝垵', features: ['文艺', '海边'] }
]
}
]
// 天气模拟数据
const weatherData = {
D001: { temp: 30, condition: '晴', humidity: 70, wind: '3级', icon: '☀️', suggestion: '适宜海边活动,注意防晒' },
D002: { temp: 18, condition: '多云', humidity: 55, wind: '2级', icon: '⛅', suggestion: '早晚温差大,建议携带外套' },
D003: { temp: 24, condition: '阴', humidity: 65, wind: '2级', icon: '☁️', suggestion: '适宜户外活动,推荐火锅' },
D004: { temp: 22, condition: '小雨', humidity: 80, wind: '3级', icon: '🌦️', suggestion: '建议携带雨具,雨中西湖别样美' },
D005: { temp: 20, condition: '晴', humidity: 50, wind: '4级', icon: '☀️', suggestion: '洱海边风大,建议带防风外套' },
D006: { temp: 26, condition: '多云', humidity: 72, wind: '3级', icon: '⛅', suggestion: '适宜环岛路骑行' }
}
// 旅行贴士
const travelTips = [
{
id: 'T01',
category: '行前准备',
icon: '🎒',
title: '提前预订省更多',
content: '建议提前2-4周预订机票和酒店,可节省20%-30%的费用。关注航司会员日和酒店促销活动。',
priority: 1
},
{
id: 'T02',
category: '交通出行',
icon: '🚗',
title: '租车 vs 打车',
content: '家庭出行推荐租车,日均约200元起;2人以内建议打车或网约车,比租车更划算。',
priority: 2
},
{
id: 'T03',
category: '住宿选择',
icon: '🏨',
title: '住宿地段建议',
content: '选择交通便利的市区或景区附近住宿,节省通勤时间。多看住客评价,重点关注"位置"和"卫生"评分。',
priority: 3
},
{
id: 'T04',
category: '美食推荐',
icon: '🍜',
title: '避开景区餐饮',
content: '景区内餐饮通常溢价30%-50%,步行10分钟到居民区就能找到更地道实惠的美食。',
priority: 4
},
{
id: 'T05',
category: '安全提醒',
icon: '🔒',
title: '旅行保险建议',
content: '国内出行建议购买旅游意外险(10元/天起),包含医疗、财产损失等保障,花小钱买安心。',
priority: 5
}
]
module.exports = {
destinations,
weatherData,
travelTips
}
{
"collections": [
{
"name": "travel_plans",
"description": "旅行规划集合",
"fields": [
{ "name": "planId", "type": "string", "description": "旅行计划ID" },
{ "name": "destination", "type": "string", "description": "目的地" },
{ "name": "departureDate", "type": "string", "description": "出发日期" },
{ "name": "returnDate", "type": "string", "description": "返回日期" },
{ "name": "budget", "type": "number", "description": "预算" },
{ "name": "transport", "type": "object", "description": "交通信息" },
{ "name": "hotel", "type": "object", "description": "酒店信息" },
{ "name": "status", "type": "string", "description": "计划状态" },
{ "name": "openid", "type": "string", "description": "用户openid" },
{ "name": "createdAt", "type": "date", "description": "创建时间" }
],
"indexes": [
{ "field": "openid", "unique": false }
]
}
]
}
// skills/travel-skill/index.js
const searchDestinations = require('./apis/searchDestinations.js')
const planTrip = require('./apis/planTrip.js')
const getWeatherInfo = require('./apis/getWeatherInfo.js')
const getTravelTips = require('./apis/getTravelTips.js')
function registerAPIs() {
const skill = wx.modelContext.createSkill('skills/travel-skill')
skill.use(async (ctx, next) => {
try {
console.info('[ai-mode] [travel-skill] middleware start name=', ctx.name)
await next()
console.info('[ai-mode] [travel-skill] middleware finish name=', ctx.name)
} catch (err) {
console.error('[ai-mode] [travel-skill] middleware error:', err.message)
throw err
}
})
skill.registerAPI('searchDestinations', searchDestinations)
skill.registerAPI('planTrip', planTrip)
skill.registerAPI('getWeatherInfo', getWeatherInfo)
skill.registerAPI('getTravelTips', getTravelTips)
console.info('[ai-mode] [travel-skill] APIs registered via createSkill')
}
registerAPIs()
module.exports = { registerAPIs }
{
"apis": [
{
"name": "searchDestinations",
"description": "搜索热门旅行目的地(业务对象:目的地列表卡片)。调用前置条件:用户想要查找旅行目的地、浏览可选目的地、或尚未提供明确目的地时。用户提供目的地名称、关键词时优先按关键词搜索;用户未提供关键词时返回默认热门目的地列表。【严禁场景】禁止在已有明确 destId 且用户要查看行程规划时继续调用本接口,应改走 planTrip。",
"_meta": {
"ui": {
"componentPath": "components/destination-list-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": "目的地搜索关键词。取值来源:用户原话中的地名、城市名(如『三亚』『丽江』『成都』)。【禁止编造】用户未明确给出关键词时可留空,返回默认热门目的地列表。"
}
},
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"items": {
"type": "array",
"description": "热门目的地列表",
"items": {
"type": "object",
"properties": {
"destId": { "type": "string", "description": "目的地唯一 ID" },
"name": { "type": "string", "description": "目的地名称" },
"nameEn": { "type": "string", "description": "目的地英文名称" },
"cover": { "type": "string", "description": "封面图片 URL" },
"rating": { "type": "number", "description": "评分" },
"description": { "type": "string", "description": "简介描述" },
"bestSeason": { "type": "string", "description": "最佳旅行季节" },
"bestSeasonDesc": { "type": "string", "description": "最佳季节说明" },
"tags": {
"type": "array",
"items": { "type": "string" },
"description": "目的地标签"
}
},
"required": ["destId", "name", "cover", "rating", "description", "bestSeason", "tags"],
"additionalProperties": false
}
},
"total": { "type": "number", "description": "目的地数量" },
"keyword": { "type": "string", "description": "实际使用的关键词,无关键词时为空字符串" }
},
"required": ["items", "total", "keyword"],
"additionalProperties": false
}
},
{
"name": "planTrip",
"description": "查看指定目的地的行程规划方案(业务对象:行程规划卡片)。包含交通方案(航班/火车)和住宿推荐。调用前置条件:已从 searchDestinations 返回结果中拿到具体 destId,或上下文中已有明确目的地。展示目的地详情、交通选项和酒店推荐。【严禁场景】禁止在没有有效 destId 的情况下调用;禁止从用户自然语言编造 destId。上下文中没有 destId 时应先调用 searchDestinations。",
"_meta": {
"ui": {
"componentPath": "components/trip-plan-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"destId": {
"type": "string",
"description": "目的地唯一标识,必须来自上游 searchDestinations 返回的 items[].destId 原值。【禁止编造】禁止从用户自然语言推断或拼接。上下文中无 destId 时,应先调 searchDestinations。"
}
},
"required": ["destId"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"dest": {
"type": "object",
"description": "目的地基本信息",
"properties": {
"destId": { "type": "string" },
"name": { "type": "string" },
"cover": { "type": "string" },
"rating": { "type": "number" },
"description": { "type": "string" },
"bestSeason": { "type": "string" },
"bestSeasonDesc": { "type": "string" },
"tags": { "type": "array", "items": { "type": "string" } }
},
"required": ["destId", "name", "cover", "rating", "description", "bestSeason", "tags"],
"additionalProperties": false
},
"transport": {
"type": "array",
"description": "交通方案列表",
"items": {
"type": "object",
"properties": {
"type": { "type": "string", "description": "交通类型:flight/train" },
"label": { "type": "string", "description": "方案标签" },
"from": { "type": "string", "description": "出发地" },
"to": { "type": "string", "description": "目的地" },
"duration": { "type": "string", "description": "行程时长" },
"price": { "type": "number", "description": "参考价格" },
"carrier": { "type": "string", "description": "承运方" }
},
"required": ["type", "label", "from", "to", "duration", "price", "carrier"],
"additionalProperties": false
}
},
"hotels": {
"type": "array",
"description": "推荐酒店列表",
"items": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "酒店名称" },
"stars": { "type": "number", "description": "星级" },
"rating": { "type": "number", "description": "评分" },
"price": { "type": "number", "description": "每晚参考价格" },
"district": { "type": "string", "description": "所在区域" },
"features": { "type": "array", "items": { "type": "string" }, "description": "特色标签" }
},
"required": ["name", "stars", "rating", "price", "district", "features"],
"additionalProperties": false
}
}
},
"required": ["dest", "transport", "hotels"],
"additionalProperties": false
}
},
{
"name": "getWeatherInfo",
"description": "查询指定目的地的当前天气情况(业务对象:天气工具条卡片)。调用前置条件:上下文中已有明确目的地 destId。展示温度、天气状况、湿度、风力和出行建议。【严禁场景】禁止在没有有效 destId 的情况下调用;禁止从用户自然语言编造 destId。上下文中无 destId 时应先调用 searchDestinations 或 planTrip。",
"_meta": {
"ui": {
"componentPath": "components/weather-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"destId": {
"type": "string",
"description": "目的地唯一标识,必须来自上游接口返回的 destId 原值。【禁止编造】上下文中无 destId 时禁止填写本字段。"
}
},
"required": ["destId"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"weather": {
"type": "object",
"properties": {
"temp": { "type": "number", "description": "当前温度(摄氏度)" },
"condition": { "type": "string", "description": "天气状况" },
"humidity": { "type": "number", "description": "湿度百分比" },
"wind": { "type": "string", "description": "风力等级" },
"icon": { "type": "string", "description": "天气图标 emoji" },
"suggestion": { "type": "string", "description": "出行建议" }
},
"required": ["temp", "condition", "humidity", "wind", "icon", "suggestion"],
"additionalProperties": false
},
"destName": { "type": "string", "description": "目的地名称" }
},
"required": ["weather", "destName"],
"additionalProperties": false
}
},
{
"name": "getTravelTips",
"description": "获取通用旅行贴士建议列表(业务对象:旅行贴士卡片)。调用前置条件:无,可在任意时刻调用。返回按优先级排序的旅行贴士列表,包含行前准备、交通出行、住宿选择、美食推荐和安全提醒等类别。每条贴士可展开查看详情。【严禁场景】禁止在用户明确需要目的地行程规划时调用本接口代替 searchDestinations 或 planTrip。",
"_meta": {
"ui": {
"componentPath": "components/tips-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {},
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"items": {
"type": "array",
"description": "旅行贴士列表",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"category": { "type": "string", "description": "贴士类别" },
"icon": { "type": "string", "description": "类别图标 emoji" },
"title": { "type": "string", "description": "贴士标题" },
"content": { "type": "string", "description": "贴士详细内容" },
"priority": { "type": "number", "description": "优先级(1最高)" }
},
"required": ["id", "category", "icon", "title", "content", "priority"],
"additionalProperties": false
}
}
},
"required": ["items"],
"additionalProperties": false
}
}
],
"components": [
{
"path": "components/destination-list-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/trip-plan-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/weather-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/tips-card/index",
"relatedPage": "/pages/home/home"
}
]
}
travel-skill
旅行规划,支持搜索目的地、查看行程方案、查询天气及获取旅行贴士。
功能
- 搜索热门旅行目的地
- 查看目的地的交通方案与酒店推荐
- 查询目的地当前天气情况
- 获取通用旅行贴士建议
用户输入示例
- "想去旅行"
- "推荐几个旅游目的地"
- "三亚有什么好玩的"
- "查一下丽江的天气"
- "规划一下大理的行程"
- "旅游注意事项"
原子接口
| 接口名 | 说明 |
|---|---|
searchDestinations | 搜索热门旅行目的地 |
planTrip | 查看指定目的地的行程规划方案(交通+住宿) |
getWeatherInfo | 查询指定目的地当前天气 |
getTravelTips | 获取通用旅行贴士建议列表 |
原子组件
| 组件路径 | 说明 |
|---|---|
components/destination-list-card/index | 目的地列表展示 |
components/trip-plan-card/index | 行程规划方案展示 |
components/weather-card/index | 天气信息展示 |
components/tips-card/index | 旅行贴士列表 |
后端依赖
| 资源 | 名称 |
|---|---|
| 云函数 | travel-skill-handler |
| 数据库集合 | travel_plans |
// skills/travel-skill/utils/util.js
const { destinations, weatherData, travelTips } = require('../data/seed')
const PREVIEW_MODE_KEY = 'mp_skills_preview_mode'
function isPreviewMode() {
return wx.getStorageSync(PREVIEW_MODE_KEY) !== false
}
function errorResult(msg, structuredContent, meta) {
const result = { isError: true, content: [{ type: 'text', text: msg }] }
if (structuredContent !== undefined) result.structuredContent = structuredContent
if (meta !== undefined) result._meta = meta
return result
}
function successResult(msg, structuredContent, meta) {
const result = { isError: false, content: [{ type: 'text', text: msg }] }
if (structuredContent !== undefined) result.structuredContent = structuredContent
if (meta !== undefined) result._meta = meta
return result
}
function defaultDestinations(keyword) {
const q = String(keyword || '').trim().toLowerCase()
if (!q) return destinations.map(mapDestItem)
return destinations
.filter((d) => {
const hay = [d.name, d.nameEn, d.description, ...(d.tags || [])].join(' ').toLowerCase()
return hay.includes(q)
})
.map(mapDestItem)
}
function mapDestItem(d) {
return {
destId: d.destId,
name: d.name,
nameEn: d.nameEn,
cover: d.cover,
rating: d.rating,
description: d.description,
bestSeason: d.bestSeason,
bestSeasonDesc: d.bestSeasonDesc,
tags: d.tags
}
}
function defaultDestDetail(destId) {
return destinations.find((d) => d.destId === destId) || null
}
function defaultWeather(destId) {
return weatherData[destId] || null
}
function defaultTips() {
return travelTips
}
module.exports = {
isPreviewMode,
errorResult,
successResult,
defaultDestinations,
defaultDestDetail,
defaultWeather,
defaultTips
}