
Taxi Skill
- 1 installs
- 27 repo stars
- Updated June 18, 2026
- tencentcloudbase/awesome-miniprogram-skills
WeChat Mini Program skill for ride-hailing: estimating fares, calling a taxi, checking trip status, and viewing trip history.
About
Adds a ride-hailing flow to a WeChat Mini Program covering fare estimation, taxi dispatch, trip status, and history. A developer uses it as a scenario template when building transportation features.
- Handles trip fare estimation and taxi dispatch
- Covers trip-status and history lookup from common destinations
Taxi Skill by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,983 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 taxi-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 27 |
| Last updated | June 18, 2026 |
| Repository | tencentcloudbase/awesome-miniprogram-skills ↗ |
What it does
WeChat Mini Program skill for ride-hailing: estimating fares, calling a taxi, checking trip status, and viewing trip history.
Files
出行打车
基于常用目的地完成行程预估、呼叫出租车、查看行程状态与历史行程的能力集合。
触发场景
用户原话举例(路由命中本技能):
- "帮我叫个快车去首都机场"
- "从望京SOHO到北京南站多少钱"
- "我要打车去三里屯"
- "我的车到哪了"
- "帮我查一下现在的行程"
- "看看我之前的打车记录"
- "从这到机场打车要多久"
不适用范围
- 订机票、火车票 → 不在本技能范围,由票务技能处理
- 预约顺风车、代驾、租车等 → 不在本技能范围
- 查看公交地铁路线 → 不在本技能范围
- 外卖、跑腿、快递等物流服务 → 不在本技能范围
接口链路
estimateTrip:根据出发地和目的地预估各车型价格与时长。callTaxi:确认费用后发起叫车,返回叫车状态。getTripStatus:查询当前进行中行程的状态与司机信息。getTripHistory:查看历史行程记录。
使用顺序
- 叫车前需先确定出发地和目的地;没有明确地址时,展示常用目的地列表引导用户选择。
- 发起叫车前应先完成价格预估,让用户了解各车型费用后再确认。
- 查看行程状态时,优先返回当前活跃行程;用户明确要求查看历史记录时再调 getTripHistory。
- 所有已绑定组件的接口都应优先展示卡片,不要改成纯文本逐条展开。
// skills/taxi-skill/apis/callTaxi.js
const {
isPreviewMode,
successResult,
errorResult,
genTripId,
formatTime,
carTypes
} = require('../utils/util')
async function callTaxi(params = {}) {
console.info('[ai-mode] callTaxi 入口, params=', JSON.stringify(params))
const { origin, destination, carType: carTypeId } = (params || {})
if (!origin || !destination) {
return errorResult(
'请提供出发地和目的地来叫车,例如「帮我叫个快车从望京SOHO到首都机场」。',
{
carTypes: carTypes.map(c => ({ id: c.id, name: c.name, desc: c.desc, eta: c.eta }))
}
)
}
if (isPreviewMode()) {
return buildResult(buildMockCall(origin, destination, carTypeId))
}
const { result } = await wx.cloud.callFunction({
name: 'taxi-skill-handler',
data: { action: 'callTaxi', origin, destination, carType: carTypeId }
})
if (result && result.code === 0 && result.data) {
console.info('[ai-mode] callTaxi 云函数返回成功')
return buildResult(result.data)
}
return errorResult(result?.message || '请求失败')
}
function buildMockCall(origin, destination, carTypeId) {
const carType = carTypes.find(c => c.id === (carTypeId || 'express')) || carTypes[0]
return {
tripId: genTripId(),
origin,
destination,
carType: carType.id,
carTypeName: carType.name,
price: carType.id === 'express' ? 68 : carType.id === 'premium' ? 98 : 45,
status: 'calling',
statusText: '正在为您叫车...',
callTime: formatTime(),
estimatedWait: carType.eta,
driverName: '',
plateNumber: '',
driverPhone: ''
}
}
function buildResult(data) {
return successResult(
`已为您呼叫${data.carTypeName},从「${data.origin}」到「${data.destination}」。请展示叫车状态卡片。`,
{
tripId: data.tripId,
origin: data.origin,
destination: data.destination,
carType: data.carType,
carTypeName: data.carTypeName,
price: data.price,
status: data.status,
statusText: data.statusText,
callTime: data.callTime,
estimatedWait: data.estimatedWait,
driverName: data.driverName,
plateNumber: data.plateNumber,
driverPhone: data.driverPhone
},
{ tripId: data.tripId }
)
}
module.exports = callTaxi
// skills/taxi-skill/apis/estimateTrip.js
const {
isPreviewMode,
successResult,
errorResult,
calcTripPrice,
destinations,
carTypes
} = require('../utils/util')
async function estimateTrip(params = {}) {
console.info('[ai-mode] estimateTrip 入口, params=', JSON.stringify(params))
const { origin, destination, selectedCarType } = (params || {})
const carTypeId = selectedCarType || params.carType || 'express'
if (!origin || !destination) {
return errorResult(
'请提供出发地和目的地,例如「从望京SOHO到首都机场多少钱」。',
{
destinations: destinations.map(d => ({ id: d.id, name: d.name, address: d.address, distance: d.distance })),
carTypes: carTypes.map(c => ({ id: c.id, name: c.name, desc: c.desc, eta: c.eta })),
suggestions: ['从望京SOHO到首都机场', '从三里屯到北京南站', '从中关村到望京']
}
)
}
if (isPreviewMode()) {
return buildResult(buildMockEstimate(origin, destination, carTypeId))
}
const { result } = await wx.cloud.callFunction({
name: 'taxi-skill-handler',
data: { action: 'estimateTrip', origin, destination, carType: carTypeId }
})
if (result && result.code === 0 && result.data) {
console.info('[ai-mode] estimateTrip 云函数返回成功')
return buildResult(result.data)
}
return errorResult(result?.message || '请求失败')
}
function buildMockEstimate(origin, destination, carTypeId) {
const allEstimates = carTypes.map(ct => {
const e = calcTripPrice(origin, destination, ct.id)
return { carTypeId: ct.id, carTypeName: ct.name, price: e.price, distance: e.distance, duration: e.duration, desc: ct.desc, eta: ct.eta }
})
const current = allEstimates.find(e => e.carTypeId === (carTypeId || 'express'))
const originObj = destinations.find(d => origin.includes(d.name) || d.name.includes(origin)) || { name: origin, address: '' }
const destObj = destinations.find(d => destination.includes(d.name) || d.name.includes(destination)) || { name: destination, address: '' }
return {
origin: originObj.name,
originAddress: originObj.address,
destination: destObj.name,
destinationAddress: destObj.address,
estimates: allEstimates,
selectedEstimate: current || allEstimates[0]
}
}
function buildResult(data) {
return successResult(
`已估算从「${data.origin}」到「${data.destination}」的行程费用。请展示行程预估卡片,列出各车型价格供用户选择。`,
{
origin: data.origin,
originAddress: data.originAddress,
destination: data.destination,
destinationAddress: data.destinationAddress,
estimates: data.estimates,
selectedEstimate: data.selectedEstimate
},
{ origin: data.origin, destination: data.destination }
)
}
module.exports = estimateTrip
// skills/taxi-skill/apis/getTripHistory.js
const {
isPreviewMode,
successResult,
errorResult,
historyTrips
} = require('../utils/util')
async function getTripHistory(params = {}) {
console.info('[ai-mode] getTripHistory 入口, params=', JSON.stringify(params))
if (isPreviewMode()) {
const items = historyTrips.map(t => ({
tripId: t.tripId,
origin: t.origin,
destination: t.destination,
carTypeName: t.carTypeName,
price: t.price,
status: t.status,
startTime: t.startTime,
endTime: t.endTime,
duration: t.duration,
distance: t.distance,
driverName: t.driverName,
plateNumber: t.plateNumber
}))
return buildResult(items)
}
const { result } = await wx.cloud.callFunction({
name: 'taxi-skill-handler',
data: { action: 'getTripHistory' }
})
if (result && result.code === 0 && result.data) {
console.info('[ai-mode] getTripHistory 云函数返回成功')
return buildResult(result.data.items || [])
}
return errorResult(result?.message || '请求失败')
}
function buildResult(items) {
if (items.length > 0) {
return successResult(
`您有 ${items.length} 条历史行程记录。请展示历史行程列表卡片。`,
{ items, total: items.length },
{ total: items.length }
)
}
return successResult(
'您还没有历史行程记录。',
{ items: [], total: 0 },
{ total: 0 }
)
}
module.exports = getTripHistory
// skills/taxi-skill/apis/getTripStatus.js
const {
isPreviewMode,
successResult,
errorResult,
activeTrip
} = require('../utils/util')
async function getTripStatus(params = {}) {
console.info('[ai-mode] getTripStatus 入口, params=', JSON.stringify(params))
const { tripId } = (params || {})
if (!tripId) {
if (isPreviewMode()) {
return successResult(
'当前您有一个进行中的行程。请展示行程状态卡片。',
{
trip: {
tripId: activeTrip.tripId,
origin: activeTrip.origin,
destination: activeTrip.destination,
carType: activeTrip.carType,
carTypeName: activeTrip.carTypeName,
price: activeTrip.price,
status: activeTrip.status,
statusText: activeTrip.statusText,
startTime: activeTrip.startTime,
driverName: activeTrip.driverName,
plateNumber: activeTrip.plateNumber,
driverPhone: activeTrip.driverPhone,
estimatedArrival: activeTrip.estimatedArrival,
remainingDistance: activeTrip.remainingDistance
},
hasActiveTrip: true
},
{ tripId: activeTrip.tripId }
)
}
return errorResult('请提供行程ID来查询行程状态。')
}
if (isPreviewMode()) {
if (tripId === activeTrip.tripId) {
return buildResult({
tripId: activeTrip.tripId,
origin: activeTrip.origin,
destination: activeTrip.destination,
carType: activeTrip.carType,
carTypeName: activeTrip.carTypeName,
price: activeTrip.price,
status: activeTrip.status,
statusText: activeTrip.statusText,
startTime: activeTrip.startTime,
driverName: activeTrip.driverName,
plateNumber: activeTrip.plateNumber,
driverPhone: activeTrip.driverPhone,
estimatedArrival: activeTrip.estimatedArrival,
remainingDistance: activeTrip.remainingDistance
})
}
return errorResult(
`未找到行程 ${tripId}。请确认行程 ID 是否正确。`,
{ trip: null, hasActiveTrip: false }
)
}
const { result } = await wx.cloud.callFunction({
name: 'taxi-skill-handler',
data: { action: 'getTripStatus', tripId }
})
if (result && result.code === 0 && result.data) {
console.info('[ai-mode] getTripStatus 云函数返回成功')
return buildResult(result.data)
}
return errorResult(result?.message || '请求失败')
}
function buildResult(data) {
return successResult(
`行程「${data.origin} → ${data.destination}」当前状态:${data.statusText}。请展示行程状态卡片。`,
{ trip: data, hasActiveTrip: true },
{ tripId: data.tripId, status: data.status }
)
}
module.exports = getTripStatus
// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
// 目的地种子数据(来自 seed.js)
const destinations = [
{ id: 'D001', name: '北京首都国际机场', address: '北京市朝阳区首都机场路', lat: 40.0799, lng: 116.6031, distance: '28km' },
{ id: 'D002', name: '北京南站', address: '北京市丰台区永外大街车站路', lat: 39.8650, lng: 116.3785, distance: '12km' },
{ id: 'D003', name: '三里屯太古里', address: '北京市朝阳区三里屯路19号', lat: 39.9335, lng: 116.4551, distance: '5km' },
{ id: 'D004', name: '望京SOHO', address: '北京市朝阳区望京东园四区', lat: 39.9958, lng: 116.4803, distance: '3km' },
{ id: 'D005', name: '中关村软件园', address: '北京市海淀区东北旺西路8号', lat: 40.0508, lng: 116.2989, distance: '15km' }
]
// 车型种子数据(来自 seed.js)
const carTypes = [
{ id: 'express', name: '快车', icon: '🚗', basePrice: 13, pricePerKm: 2.1, pricePerMin: 0.5, desc: '经济实惠', eta: '3分钟', color: '#1C8EFF' },
{ id: 'premium', name: '专车', icon: '🚙', basePrice: 18, pricePerKm: 3.5, pricePerMin: 0.8, desc: '舒适品质', eta: '5分钟', color: '#FF8C00' },
{ id: 'carpool', name: '拼车', icon: '🚕', basePrice: 10, pricePerKm: 1.5, pricePerMin: 0.3, desc: '绿色出行', eta: '7分钟', color: '#34C759' }
]
// 计算预估价格
function estimatePrice(distanceKm, carType, estimatedMin) {
const ct = carTypes.find(c => c.id === carType)
if (!ct) return 0
return Math.round(ct.basePrice + ct.pricePerKm * distanceKm + ct.pricePerMin * estimatedMin)
}
// 云函数入口函数
exports.main = async (event, context) => {
const { action } = event
const wxContext = cloud.getWXContext()
const openid = wxContext.OPENID
switch (action) {
case 'estimateTrip': {
const { origin, destinationId } = event
const dest = destinations.find(d => d.id === destinationId)
if (!dest) {
return { code: -1, msg: '目的地不存在' }
}
const distanceKm = parseFloat(dest.distance)
const estimates = carTypes.map(ct => ({
...ct,
estimatedPrice: estimatePrice(distanceKm, ct.id, 30),
estimatedDuration: Math.round(distanceKm * 3) + '分钟'
}))
return {
code: 0,
data: {
origin,
destination: dest,
estimates
}
}
}
case 'callTaxi': {
const { origin, destination, carType, price } = event
if (!origin || !destination || !carType || !price) {
return { code: -1, msg: '参数不完整' }
}
const tripId = 'T' + Date.now()
const ct = carTypes.find(c => c.id === carType)
const drivers = ['张师傅', '李师傅', '王师傅', '赵师傅', '刘师傅']
const plates = ['京B·12345', '京A·67890', '京C·54321', '京D·11111', '京E·22222']
const randomIdx = Math.floor(Math.random() * drivers.length)
const trip = {
tripId,
origin,
destination,
carType,
price,
status: 'en_route',
driverInfo: {
name: drivers[randomIdx],
plateNumber: plates[randomIdx],
phone: '138****8888'
},
openid,
createdAt: new Date()
}
await db.collection('trips').add({ data: trip })
return {
code: 0,
data: {
tripId,
status: 'en_route',
driverInfo: trip.driverInfo,
estimatedArrival: ct ? ct.eta : '3分钟'
}
}
}
case 'getTripStatus': {
const { tripId } = event
if (!tripId) {
return { code: -1, msg: '缺少行程ID' }
}
const res = await db.collection('trips').where({ tripId, openid }).get()
if (res.data.length === 0) {
return { code: -1, msg: '行程不存在' }
}
return { code: 0, data: res.data[0] }
}
case 'getTripHistory': {
const { page = 1, pageSize = 10 } = event
const res = await db.collection('trips')
.where({ openid })
.orderBy('createdAt', 'desc')
.skip((page - 1) * pageSize)
.limit(pageSize)
.get()
return { code: 0, data: res.data }
}
default:
return { code: -1, msg: `未知 action: ${action}` }
}
}
{
"name": "taxi-skill-handler",
"version": "1.0.0",
"description": "taxi-skill 云函数",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}
// skills/taxi-skill/components/calling-taxi-card/index.js
Component({
data: {
tripId: '',
origin: '',
destination: '',
carTypeName: '',
price: 0,
status: '',
statusText: '',
callTime: '',
estimatedWait: '',
driverName: '',
plateNumber: '',
driverPhone: '',
timerId: null,
elapsedSeconds: 0,
_formatElapsed: ''
},
lifetimes: {
created() {
console.info('[ai-mode] calling-taxi-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] calling-taxi-card 收到 Result:', JSON.stringify(sc))
this.setData({
tripId: sc.tripId || '',
origin: sc.origin || '',
destination: sc.destination || '',
carTypeName: sc.carTypeName || '',
price: sc.price || 0,
status: sc.status || '',
statusText: sc.statusText || '',
callTime: sc.callTime || '',
estimatedWait: sc.estimatedWait || '',
driverName: sc.driverName || '',
plateNumber: sc.plateNumber || '',
driverPhone: sc.driverPhone || ''
})
if (sc.status === 'calling') {
this._startTimer()
}
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] calling-taxi-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] calling-taxi-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] calling-taxi-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] calling-taxi-card overflow monitor=on')
},
detached() {
this._stopTimer()
}
},
methods: {
_startTimer() {
this._stopTimer()
const timerId = setInterval(() => {
this.setData({ elapsedSeconds: this.data.elapsedSeconds + 1 })
}, 1000)
this.setData({ timerId })
},
_stopTimer() {
if (this.data.timerId) {
clearInterval(this.data.timerId)
this.data.timerId = null
}
},
_formatElapsed(seconds) {
const m = Math.floor(seconds / 60)
const s = seconds % 60
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
},
onCancelTrip() {
console.info('[ai-mode] calling-taxi-card 取消行程')
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '取消当前叫车' },
{ type: 'api/call', data: { name: 'cancelTrip', arguments: { tripId: this.data.tripId } } }
]
})
},
onRefreshStatus() {
console.info('[ai-mode] calling-taxi-card 刷新状态')
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '查看行程状态' },
{ type: 'api/call', data: { name: 'getTripStatus', arguments: { tripId: this.data.tripId } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="ct-card">
<view wx:if="{{status === 'calling'}}" class="ct-calling">
<view class="ct-spinner-ring">
<view class="ct-spinner-inner"></view>
</view>
<view class="ct-status-text">{{statusText}}</view>
<view class="ct-timer">{{_formatElapsed(elapsedSeconds)}}</view>
<view class="ct-route">{{origin}} → {{destination}}</view>
<view class="ct-car-info">{{carTypeName}} · 约¥{{price}} · 预计{{estimatedWait}}接驾</view>
<view class="ct-actions">
<view class="ct-btn-secondary" hover-class="ct-btn-hover" bind:tap="onCancelTrip">取消叫车</view>
</view>
</view>
<view wx:elif="{{status === 'en_route' || status === 'arrived'}}" class="ct-matched">
<view class="ct-matched-icon">
<view class="ct-car-icon-bg"></view>
</view>
<view class="ct-status-text">{{statusText}}</view>
<view class="ct-driver-info">
<view class="ct-driver-row">
<text class="ct-label">司机</text>
<text class="ct-value">{{driverName}}</text>
</view>
<view class="ct-driver-row">
<text class="ct-label">车牌</text>
<text class="ct-value">{{plateNumber}}</text>
</view>
<view class="ct-driver-row">
<text class="ct-label">电话</text>
<text class="ct-value">{{driverPhone}}</text>
</view>
</view>
<view class="ct-route">{{origin}} → {{destination}}</view>
<view class="ct-actions">
<view class="ct-btn-secondary" hover-class="ct-btn-hover" bind:tap="onRefreshStatus">刷新状态</view>
</view>
</view>
</view>
/* ratio=1:1 叫车卡片
* 色源:出租车行业蓝 #1C8EFF + 淡蓝底 #F0F8FF
* 暗黑:深蓝底 #1A1A2E
*/
.ct-card {
background: #FFFFFF;
border-radius: 1.07vw;
padding: 4.27vw;
box-sizing: border-box;
overflow: hidden;
text-align: center;
}
.ct-calling {
display: flex;
flex-direction: column;
align-items: center;
}
.ct-spinner-ring {
width: 17.07vw;
height: 17.07vw;
border: 0.8vw solid #E8EDF2;
border-top-color: #1C8EFF;
border-radius: 50%;
animation: ct-spin 1s linear infinite;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 3.2vw;
}
.ct-spinner-inner {
width: 3.2vw;
height: 3.2vw;
background: #1C8EFF;
border-radius: 50%;
}
@keyframes ct-spin {
to { transform: rotate(360deg); }
}
.ct-status-text {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
margin-bottom: 1.07vw;
}
.ct-timer {
font-size: 3.47vw;
color: #1C8EFF;
font-variant-numeric: tabular-nums;
margin-bottom: 3.2vw;
}
.ct-route {
font-size: 4vw;
color: rgba(0,0,0,0.45);
margin-bottom: 1.6vw;
}
.ct-car-info {
font-size: 3.47vw;
color: rgba(0,0,0,0.45);
margin-bottom: 4.27vw;
}
.ct-matched {
display: flex;
flex-direction: column;
align-items: center;
}
.ct-matched-icon {
margin-bottom: 2.13vw;
}
.ct-car-icon-bg {
width: 12.8vw;
height: 12.8vw;
border-radius: 50%;
background: #1C8EFF;
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.92 6.01C18.72 5.42 18.16 5 17.5 5H6.5C5.84 5 5.29 5.42 5.08 6.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z'/%3E%3C/svg%3E");
mask-size: 60%;
mask-repeat: no-repeat;
mask-position: center;
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.92 6.01C18.72 5.42 18.16 5 17.5 5H6.5C5.84 5 5.29 5.42 5.08 6.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z'/%3E%3C/svg%3E");
-webkit-mask-size: 60%;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
}
.ct-driver-info {
width: 100%;
background: #F0F8FF;
border-radius: 1.07vw;
padding: 3.2vw 4.27vw;
margin: 3.2vw 0;
box-sizing: border-box;
}
.ct-driver-row {
display: flex;
justify-content: space-between;
padding: 1.07vw 0;
}
.ct-label {
font-size: 4vw;
color: rgba(0,0,0,0.45);
}
.ct-value {
font-size: 4vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
}
.ct-actions {
display: flex;
gap: 2.67vw;
width: 100%;
margin-top: 1.07vw;
}
.ct-btn-secondary {
flex: 1;
height: 10.67vw;
line-height: 10.67vw;
text-align: center;
font-size: 4vw;
font-weight: 500;
color: #1C8EFF;
background: #F0F8FF;
border-radius: 1.07vw;
}
.ct-btn-hover { opacity: 0.8; }
@media (prefers-color-scheme: dark) {
.ct-card { background: #1A1A2E; }
.ct-spinner-ring { border-color: #3A3A4E; border-top-color: #4DA6FF; }
.ct-spinner-inner { background: #4DA6FF; }
.ct-status-text { color: rgba(255,255,255,0.9); }
.ct-timer { color: #4DA6FF; }
.ct-route, .ct-car-info { color: rgba(255,255,255,0.45); }
.ct-driver-info { background: #2A2A4E; }
.ct-label { color: rgba(255,255,255,0.45); }
.ct-value { color: rgba(255,255,255,0.9); }
.ct-btn-secondary { color: #4DA6FF; background: #2A2A4E; }
}
// skills/taxi-skill/components/trip-estimate-card/index.js
Component({
data: {
origin: '',
destination: '',
estimates: [],
selectedCarType: 'express',
selectedCarName: '快车'
},
lifetimes: {
created() {
console.info('[ai-mode] trip-estimate-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-estimate-card 收到 Result:', JSON.stringify(sc))
this.setData({
origin: sc.origin || '',
destination: sc.destination || '',
estimates: sc.estimates || [],
selectedCarType: (sc.selectedEstimate && sc.selectedEstimate.carTypeId) || 'express',
selectedCarName: (sc.estimates && sc.estimates.find(e => e.carTypeId === (sc.selectedEstimate && sc.selectedEstimate.carTypeId || 'express'))?.carTypeName) || '快车'
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] trip-estimate-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] trip-estimate-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] trip-estimate-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] trip-estimate-card overflow monitor=on')
}
},
methods: {
onSelectCarType(e) {
const { carType } = e.currentTarget.dataset
const estimate = this.data.estimates.find(e => e.carTypeId === carType) || {}
const carName = estimate.carTypeName || '快车'
this.setData({ selectedCarType: carType, selectedCarName: carName })
console.info(`[ai-mode] trip-estimate-card 选择车型: ${carName}(${carType})`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: `选择${carName}` },
{ type: 'api/call', data: { name: 'estimateTrip', arguments: { selectedCarType: carType } } }
]
})
},
onCallTaxi() {
const { origin, destination, selectedCarType } = this.data
const carTypeName = (this.data.estimates.find(e => e.carTypeId === selectedCarType) || {}).carTypeName || '快车'
console.info(`[ai-mode] trip-estimate-card send api/call name=callTaxi args=${JSON.stringify({ origin, destination, carType: selectedCarType })}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: `呼叫${carTypeName},从${origin}到${destination}` },
{ type: 'api/call', data: { name: 'callTaxi', arguments: { origin, destination, carType: selectedCarType } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="te-card">
<view class="te-header">
<view class="te-route">
<view class="te-dot te-dot-origin"></view>
<view class="te-line"></view>
<view class="te-dot te-dot-dest"></view>
</view>
<view class="te-places">
<view class="te-place">{{origin}}</view>
<view class="te-arrow">→</view>
<view class="te-place te-place-dest">{{destination}}</view>
</view>
</view>
<view wx:if="{{!estimates.length}}" class="te-empty">
<view class="te-empty-title">暂无可用车型</view>
<view class="te-empty-desc">请重新选择出发地和目的地</view>
</view>
<block wx:for="{{estimates}}" wx:key="carTypeId">
<view
class="te-car-item {{selectedCarType === item.carTypeId ? 'is-selected' : ''}}"
data-car-type="{{item.carTypeId}}"
bind:tap="onSelectCarType"
>
<view class="te-car-icon" style="background: {{item.carTypeId === 'express' ? '#1C8EFF' : item.carTypeId === 'premium' ? '#FF8C00' : '#34C759'}}">
<view class="te-car-svg-{{item.carTypeId}}"></view>
</view>
<view class="te-car-info">
<view class="te-car-name">{{item.carTypeName}}</view>
<view class="te-car-desc">{{item.desc}} · {{item.eta}}接驾</view>
</view>
<view class="te-car-meta">
<view class="te-car-price">¥{{item.price}}</view>
<view class="te-car-detail">{{item.duration}} · {{item.distance}}</view>
</view>
</view>
</block>
<view class="te-btn" hover-class="te-btn-hover" bind:tap="onCallTaxi">呼叫<text>{{selectedCarName}}</text></view>
</view>
/* ratio=1:1 行程估价卡片
* 色源:出租车行业蓝 #1C8EFF + 浅蓝底 #F0F7FF
* 暗黑:浅色降明度
*/
.te-card {
background: #F0F7FF;
border-radius: 1.07vw;
padding: 3.2vw;
box-sizing: border-box;
overflow: hidden;
}
.te-header {
display: flex;
align-items: flex-start;
margin-bottom: 3.2vw;
}
.te-route {
display: flex;
flex-direction: column;
align-items: center;
width: 4vw;
margin-right: 3.2vw;
flex-shrink: 0;
}
.te-dot {
width: 2.13vw;
height: 2.13vw;
border-radius: 50%;
flex-shrink: 0;
}
.te-dot-origin { background: #1C8EFF; }
.te-dot-dest { background: #EE0A24; }
.te-line {
flex: 1;
width: 0.27vw;
min-height: 6.4vw;
background: rgba(0,0,0,0.1);
}
.te-places {
flex: 1;
min-width: 0;
}
.te-place {
font-size: 4vw;
font-weight: 500;
color: rgba(0,0,0,0.9);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.te-place-dest {
margin-top: 2.13vw;
color: rgba(0,0,0,0.6);
}
.te-arrow {
font-size: 3.2vw;
color: rgba(0,0,0,0.3);
margin: 0 1.07vw;
}
.te-empty {
padding: 6.4vw 3.2vw;
text-align: center;
background: #FFFFFF;
border-radius: 1.07vw;
}
.te-empty-title {
font-size: 4vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
}
.te-empty-desc {
font-size: 3.2vw;
color: rgba(0,0,0,0.45);
margin-top: 1.07vw;
}
.te-car-item {
display: flex;
align-items: center;
padding: 2.13vw 3.2vw;
background: #FFFFFF;
border-radius: 1.07vw;
margin-bottom: 1.6vw;
}
.te-car-item.is-selected {
border: 0.27vw solid #1C8EFF;
}
.te-car-icon {
width: 10.67vw;
height: 10.67vw;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.te-car-svg-express, .te-car-svg-premium, .te-car-svg-economy {
width: 6.4vw;
height: 6.4vw;
background: white;
mask-size: contain;
mask-repeat: no-repeat;
-webkit-mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
.te-car-svg-express {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.92 6.01C18.72 5.42 18.16 5 17.5 5H6.5C5.84 5 5.29 5.42 5.08 6.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.92 6.01C18.72 5.42 18.16 5 17.5 5H6.5C5.84 5 5.29 5.42 5.08 6.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z'/%3E%3C/svg%3E");
}
.te-car-svg-premium {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.92 6.01C18.72 5.42 18.16 5 17.5 5H6.5C5.84 5 5.29 5.42 5.08 6.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.92 6.01C18.72 5.42 18.16 5 17.5 5H6.5C5.84 5 5.29 5.42 5.08 6.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.92 6.01C18.72 5.42 18.16 5 17.5 5H6.5C5.84 5 5.29 5.42 5.08 6.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z'/%3E%3C/svg%3E");
}
.te-car-svg-economy {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.92 6.01C18.72 5.42 18.16 5 17.5 5H6.5C5.84 5 5.29 5.42 5.08 6.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.92 6.01C18.72 5.42 18.16 5 17.5 5H6.5C5.84 5 5.29 5.42 5.08 6.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z'/%3E%3C/svg%3E");
}
.te-car-info {
flex: 1;
margin-left: 2.67vw;
min-width: 0;
}
.te-car-name {
font-size: 4vw;
font-weight: 500;
color: rgba(0,0,0,0.9);
}
.te-car-desc {
font-size: 3.2vw;
color: rgba(0,0,0,0.45);
margin-top: 0.53vw;
}
.te-car-meta {
text-align: right;
flex-shrink: 0;
}
.te-car-price {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
}
.te-car-detail {
font-size: 3.2vw;
color: rgba(0,0,0,0.45);
margin-top: 0.53vw;
}
.te-btn {
margin-top: 3.2vw;
height: 10.67vw;
line-height: 10.67vw;
text-align: center;
font-size: 4vw;
font-weight: 500;
color: #FFFFFF;
background: #1C8EFF;
border-radius: 1.07vw;
}
.te-btn-hover { opacity: 0.85; }
@media (prefers-color-scheme: dark) {
.te-card { background: #1A1A2E; }
.te-place, .te-car-name, .te-car-price, .te-empty-title { color: rgba(255,255,255,0.9); }
.te-place-dest, .te-car-desc, .te-car-detail, .te-empty-desc { color: rgba(255,255,255,0.45); }
.te-line { background: rgba(255,255,255,0.12); }
.te-car-item, .te-empty { background: #2A2A3E; }
}
// skills/taxi-skill/components/trip-history-card/index.js
Component({
data: {
items: [],
total: 0
},
lifetimes: {
created() {
console.info('[ai-mode] trip-history-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-history-card 收到 Result:', JSON.stringify(sc))
this.setData({
items: sc.items || [],
total: sc.total || 0
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] trip-history-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] trip-history-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] trip-history-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] trip-history-card overflow monitor=on')
}
},
methods: {
onTripDetail(e) {
const { tripId } = e.currentTarget.dataset
const trip = this.data.items.find(t => t.tripId === tripId)
if (!trip) return
const { origin, destination, carTypeName, price, startTime, driverName, plateNumber } = trip
console.info(`[ai-mode] trip-history-card 查看行程详情 tripId=${tripId}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: `查看${origin}到${destination}的行程详情,费用¥${price},${carTypeName},司机${driverName || '无'}` }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="th-card">
<view class="th-title">历史行程</view>
<view wx:if="{{!items.length}}" class="th-empty">
<view class="th-empty-icon">
<view class="th-list-icon"><text class="th-list-icon-text">≡</text></view>
</view>
<view class="th-empty-title">暂无历史行程</view>
<view class="th-empty-desc">您还没有完成过打车行程</view>
</view>
<block wx:for="{{items}}" wx:key="tripId">
<view class="th-item {{index === 0 ? 'th-item--first' : ''}}" data-trip-id="{{item.tripId}}" bind:tap="onTripDetail">
<view class="th-item-top">
<view class="th-route-info">
<view class="th-origin">{{item.origin}}</view>
<view class="th-arrow">→</view>
<view class="th-destination">{{item.destination}}</view>
</view>
<view class="th-price">¥{{item.price}}</view>
</view>
<view class="th-item-bottom">
<view class="th-meta">{{item.startTime}}</view>
<view class="th-meta">{{item.carTypeName}}</view>
<view class="th-meta th-meta-status {{item.status}}">{{item.status === 'completed' ? '已完成' : '已取消'}}</view>
</view>
</view>
</block>
</view>
/* ratio=4:3 历史行程卡片
* 色源:出租车蓝 #1C8EFF + 浅蓝底 #F0F7FF
* 暗黑:深蓝底 #1A1A2E
*/
.th-card {
background: #F0F7FF;
border-radius: 1.07vw;
padding: 3.2vw;
box-sizing: border-box;
overflow: hidden;
}
.th-title {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
margin-bottom: 2.67vw;
}
.th-empty {
text-align: center;
padding: 6.4vw 3.2vw;
}
.th-empty-icon {
margin-bottom: 2.13vw;
}
.th-list-icon {
width: 10.67vw;
height: 10.67vw;
margin: 0 auto;
background: rgba(0,0,0,0.2);
border-radius: 1.07vw;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.th-list-icon-text {
font-size: 8vw;
color: white;
line-height: 1;
}
.th-empty-title {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
}
.th-empty-desc {
font-size: 3.47vw;
color: rgba(0,0,0,0.45);
margin-top: 1.07vw;
}
.th-item {
padding: 2.67vw 0;
border-top: 0.27vw solid rgba(0,0,0,0.06);
}
.th-item--first {
border-top: none;
}
.th-item-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.th-route-info {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.th-origin {
font-size: 4vw;
font-weight: 500;
color: rgba(0,0,0,0.9);
display: inline;
}
.th-arrow {
font-size: 3.2vw;
color: rgba(0,0,0,0.3);
margin: 0 1.07vw;
display: inline;
}
.th-destination {
font-size: 4vw;
color: rgba(0,0,0,0.6);
display: inline;
}
.th-price {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
flex-shrink: 0;
margin-left: 2.13vw;
}
.th-item-bottom {
display: flex;
gap: 2.13vw;
margin-top: 1.07vw;
}
.th-meta {
font-size: 3.2vw;
color: rgba(0,0,0,0.45);
}
.th-meta-status {
margin-left: auto;
font-weight: 500;
}
.th-meta-status.completed { color: #07C160; }
.th-meta-status.cancelled { color: #EE0A24; }
@media (prefers-color-scheme: dark) {
.th-card { background: #1A1A2E; }
.th-title, .th-origin, .th-price, .th-empty-title { color: rgba(255,255,255,0.9); }
.th-destination { color: rgba(255,255,255,0.6); }
.th-arrow { color: rgba(255,255,255,0.3); }
.th-meta { color: rgba(255,255,255,0.45); }
.th-item { border-color: rgba(255,255,255,0.08); }
.th-item--first { border-color: transparent; }
.th-list-icon { background: rgba(255,255,255,0.12); }
}
// skills/taxi-skill/components/trip-status-card/index.js
Component({
data: {
trip: null,
hasActiveTrip: false
},
lifetimes: {
created() {
console.info('[ai-mode] trip-status-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-status-card 收到 Result:', JSON.stringify(sc))
this.setData({
trip: sc.trip || null,
hasActiveTrip: sc.hasActiveTrip || false
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] trip-status-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] trip-status-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] trip-status-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] trip-status-card overflow monitor=on')
}
},
methods: {
onRefresh() {
const tripId = (this.data.trip && this.data.trip.tripId) || ''
console.info(`[ai-mode] trip-status-card send api/call name=getTripStatus args=${JSON.stringify({ tripId })}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '刷新行程状态' },
{ type: 'api/call', data: { name: 'getTripStatus', arguments: { tripId } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="ts-card">
<view wx:if="{{!hasActiveTrip || !trip}}" class="ts-empty">
<view class="ts-empty-icon">
<view class="ts-car-empty"></view>
</view>
<view class="ts-empty-title">暂无进行中的行程</view>
<view class="ts-empty-desc">需要叫车吗?告诉我出发地和目的地</view>
</view>
<view wx:else class="ts-content">
<view class="ts-header">
<view class="ts-route">
<view class="ts-dot ts-dot-origin"></view>
<view class="ts-line"></view>
<view class="ts-dot ts-dot-dest"></view>
</view>
<view class="ts-places">
<view class="ts-place">{{trip.origin}}</view>
<view class="ts-place ts-place-dest">{{trip.destination}}</view>
</view>
</view>
<view class="ts-status-bar">
<view class="ts-status-icon">
<view class="ts-car-small"></view>
</view>
<view class="ts-status-info">
<view class="ts-status-text">{{trip.statusText}}</view>
<view class="ts-driver-info">{{trip.driverName}} · {{trip.plateNumber}}</view>
</view>
</view>
<view wx:if="{{trip.estimatedArrival}}" class="ts-arrival">
<view class="ts-arrival-label">司机预计到达</view>
<view class="ts-arrival-time">{{trip.estimatedArrival}}</view>
</view>
<view wx:if="{{trip.remainingDistance}}" class="ts-distance">
距离上车点 {{trip.remainingDistance}}
</view>
<view class="ts-btn" hover-class="ts-btn-hover" bind:tap="onRefresh">刷新状态</view>
</view>
</view>
/* ratio=1:1 行程状态卡片
* 色源:出租车蓝 #1C8EFF + 浅蓝底 #F0F7FF
* 暗黑:深蓝底 #1A1A2E
*/
.ts-card {
background: #F0F7FF;
border-radius: 1.07vw;
padding: 3.2vw;
box-sizing: border-box;
overflow: hidden;
}
.ts-empty {
text-align: center;
padding: 6.4vw 3.2vw;
}
.ts-empty-icon {
margin-bottom: 2.13vw;
}
.ts-car-empty {
width: 10.67vw;
height: 10.67vw;
margin: 0 auto;
background: #1C8EFF;
border-radius: 50%;
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.92 6.01C18.72 5.42 18.16 5 17.5 5H6.5C5.84 5 5.29 5.42 5.08 6.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z'/%3E%3C/svg%3E");
mask-size: 60%;
mask-repeat: no-repeat;
mask-position: center;
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.92 6.01C18.72 5.42 18.16 5 17.5 5H6.5C5.84 5 5.29 5.42 5.08 6.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z'/%3E%3C/svg%3E");
-webkit-mask-size: 60%;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
}
.ts-empty-title {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
}
.ts-empty-desc {
font-size: 3.47vw;
color: rgba(0,0,0,0.45);
margin-top: 1.07vw;
}
.ts-header {
display: flex;
margin-bottom: 3.2vw;
}
.ts-route {
display: flex;
flex-direction: column;
align-items: center;
width: 4vw;
margin-right: 3.2vw;
flex-shrink: 0;
}
.ts-dot {
width: 2.13vw;
height: 2.13vw;
border-radius: 50%;
}
.ts-dot-origin { background: #1C8EFF; }
.ts-dot-dest { background: #EE0A24; }
.ts-line {
flex: 1;
width: 0.27vw;
min-height: 5.33vw;
background: rgba(0,0,0,0.1);
}
.ts-places {
flex: 1;
min-width: 0;
}
.ts-place {
font-size: 4vw;
font-weight: 500;
color: rgba(0,0,0,0.9);
}
.ts-place-dest {
margin-top: 1.6vw;
color: rgba(0,0,0,0.45);
}
.ts-status-bar {
display: flex;
align-items: center;
padding: 2.67vw 3.2vw;
background: #FFFFFF;
border-radius: 1.07vw;
margin-bottom: 2.13vw;
}
.ts-status-icon {
width: 8.53vw;
height: 8.53vw;
border-radius: 50%;
background: #1C8EFF;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.ts-car-small {
width: 5.33vw;
height: 5.33vw;
background: white;
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.92 6.01C18.72 5.42 18.16 5 17.5 5H6.5C5.84 5 5.29 5.42 5.08 6.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z'/%3E%3C/svg%3E");
mask-size: contain;
mask-repeat: no-repeat;
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.92 6.01C18.72 5.42 18.16 5 17.5 5H6.5C5.84 5 5.29 5.42 5.08 6.01L3 12v8c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h12v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-8l-2.08-5.99zM6.5 16c-.83 0-1.5-.67-1.5-1.5S5.67 13 6.5 13s1.5.67 1.5 1.5S7.33 16 6.5 16zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM5 11l1.5-4.5h11L19 11H5z'/%3E%3C/svg%3E");
-webkit-mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
.ts-status-info {
margin-left: 2.67vw;
flex: 1;
min-width: 0;
}
.ts-status-text {
font-size: 4vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
}
.ts-driver-info {
font-size: 3.2vw;
color: rgba(0,0,0,0.45);
margin-top: 0.53vw;
}
.ts-arrival {
text-align: center;
padding: 2.67vw;
background: #FFFFFF;
border-radius: 1.07vw;
margin-bottom: 2.13vw;
}
.ts-arrival-label {
font-size: 3.2vw;
color: rgba(0,0,0,0.45);
}
.ts-arrival-time {
font-size: 5.33vw;
font-weight: 700;
color: #1C8EFF;
margin-top: 0.53vw;
}
.ts-distance {
font-size: 3.47vw;
color: rgba(0,0,0,0.45);
text-align: center;
margin-bottom: 2.67vw;
}
.ts-btn {
height: 10.67vw;
line-height: 10.67vw;
text-align: center;
font-size: 4vw;
font-weight: 500;
color: #FFFFFF;
background: #1C8EFF;
border-radius: 1.07vw;
}
.ts-btn-hover { opacity: 0.85; }
@media (prefers-color-scheme: dark) {
.ts-card { background: #1A1A2E; }
.ts-empty-title { color: rgba(255,255,255,0.9); }
.ts-empty-desc { color: rgba(255,255,255,0.45); }
.ts-place { color: rgba(255,255,255,0.9); }
.ts-place-dest { color: rgba(255,255,255,0.45); }
.ts-line { background: rgba(255,255,255,0.12); }
.ts-status-bar, .ts-arrival { background: #2A2A3E; }
.ts-status-text { color: rgba(255,255,255,0.9); }
.ts-driver-info { color: rgba(255,255,255,0.45); }
.ts-arrival-label { color: rgba(255,255,255,0.45); }
}
// skills/taxi-skill/data/seed.js
const destinations = [
{ id: 'D001', name: '北京首都国际机场', address: '北京市朝阳区首都机场路', lat: 40.0799, lng: 116.6031, distance: '28km' },
{ id: 'D002', name: '北京南站', address: '北京市丰台区永外大街车站路', lat: 39.8650, lng: 116.3785, distance: '12km' },
{ id: 'D003', name: '三里屯太古里', address: '北京市朝阳区三里屯路19号', lat: 39.9335, lng: 116.4551, distance: '5km' },
{ id: 'D004', name: '望京SOHO', address: '北京市朝阳区望京东园四区', lat: 39.9958, lng: 116.4803, distance: '3km' },
{ id: 'D005', name: '中关村软件园', address: '北京市海淀区东北旺西路8号', lat: 40.0508, lng: 116.2989, distance: '15km' }
]
const carTypes = [
{ id: 'express', name: '快车', icon: '🚗', basePrice: 13, pricePerKm: 2.1, pricePerMin: 0.5, desc: '经济实惠', eta: '3分钟', color: '#1C8EFF' },
{ id: 'premium', name: '专车', icon: '🚙', basePrice: 18, pricePerKm: 3.5, pricePerMin: 0.8, desc: '舒适品质', eta: '5分钟', color: '#FF8C00' },
{ id: 'carpool', name: '拼车', icon: '🚕', basePrice: 10, pricePerKm: 1.5, pricePerMin: 0.3, desc: '绿色出行', eta: '7分钟', color: '#34C759' }
]
const historyTrips = [
{
tripId: 'H001',
origin: '望京SOHO',
destination: '北京首都国际机场',
carType: 'express',
carTypeName: '快车',
price: 68,
status: 'completed',
startTime: '2026-06-07 14:30',
endTime: '2026-06-07 15:10',
duration: '40分钟',
distance: '28km',
driverName: '张师傅',
plateNumber: '京B·12345'
},
{
tripId: 'H002',
origin: '中关村软件园',
destination: '北京南站',
carType: 'premium',
carTypeName: '专车',
price: 56,
status: 'completed',
startTime: '2026-06-06 09:00',
endTime: '2026-06-06 09:40',
duration: '40分钟',
distance: '15km',
driverName: '李师傅',
plateNumber: '京A·67890'
},
{
tripId: 'H003',
origin: '三里屯太古里',
destination: '望京SOHO',
carType: 'carpool',
carTypeName: '拼车',
price: 22,
status: 'cancelled',
startTime: '2026-06-05 20:15',
endTime: '',
duration: '',
distance: '5km',
driverName: '',
plateNumber: ''
}
]
const activeTrip = {
tripId: 'A001',
origin: '望京SOHO',
destination: '北京首都国际机场',
carType: 'express',
carTypeName: '快车',
price: 68,
status: 'en_route',
statusText: '司机已接单,正在赶来',
startTime: '2026-06-08 10:00',
driverName: '王师傅',
plateNumber: '京C·54321',
driverPhone: '138****8888',
driverLat: 39.9910,
driverLng: 116.4760,
pickupLat: 39.9958,
pickupLng: 116.4803,
estimatedArrival: '3分钟',
remainingDistance: '0.8km'
}
module.exports = {
destinations,
carTypes,
historyTrips,
activeTrip
}
{
"collections": [
{
"name": "trips",
"description": "出行行程集合",
"fields": [
{ "name": "tripId", "type": "string", "description": "行程ID" },
{ "name": "origin", "type": "string", "description": "出发地" },
{ "name": "destination", "type": "string", "description": "目的地" },
{ "name": "carType", "type": "string", "description": "车型" },
{ "name": "price", "type": "number", "description": "价格" },
{ "name": "status", "type": "string", "description": "行程状态" },
{ "name": "driverInfo", "type": "object", "description": "司机信息" },
{ "name": "openid", "type": "string", "description": "用户openid" },
{ "name": "createdAt", "type": "date", "description": "创建时间" }
],
"indexes": [
{ "field": "openid", "unique": false }
]
}
]
}
// skills/taxi-skill/index.js
const estimateTrip = require('./apis/estimateTrip')
const callTaxi = require('./apis/callTaxi')
const getTripStatus = require('./apis/getTripStatus')
const getTripHistory = require('./apis/getTripHistory')
function registerAPIs() {
const skill = wx.modelContext.createSkill('skills/taxi-skill')
skill.use(async (ctx, next) => {
try {
console.info('[ai-mode] [taxi-skill] middleware start name=', ctx.name)
await next()
console.info('[ai-mode] [taxi-skill] middleware finish name=', ctx.name)
} catch (err) {
console.error('[ai-mode] [taxi-skill] middleware error:', err.message)
throw err
}
})
skill.registerAPI('estimateTrip', estimateTrip)
skill.registerAPI('callTaxi', callTaxi)
skill.registerAPI('getTripStatus', getTripStatus)
skill.registerAPI('getTripHistory', getTripHistory)
console.info('[ai-mode] [taxi-skill] APIs registered via createSkill')
}
registerAPIs()
{
"apis": [
{
"name": "estimateTrip",
"description": "预估行程价格与时长(业务对象:行程预估卡片)。调用前置条件:用户已明确出发地和目的地,或用户问『打车到某地多少钱』。返回各车型(快车/专车/拼车)的预估价格、距离和时长。【严禁场景】禁止在无出发地或目的地时调用;禁止在已有进行中行程时调用来覆盖当前行程。",
"_meta": {
"ui": {
"componentPath": "components/trip-estimate-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"origin": {
"type": "string",
"description": "出发地名称。取值来源:用户原话中的地址或地名(如『望京SOHO』『三里屯』『当前定位』)。【禁止编造】用户未明确给出时可留空,返回常用目的地列表引导用户选择。"
},
"destination": {
"type": "string",
"description": "目的地名称。取值来源:用户原话中的地址或地名(如『首都机场』『北京南站』)。【禁止编造】用户未明确给出时可留空,返回常用目的地列表引导用户选择。"
},
"carType": {
"type": "string",
"enum": ["express", "premium", "carpool"],
"description": "车型 ID。用户明确提到车型时使用(如『快车』→ express、『专车』→ premium、『拼车』→ carpool)。用户未提及时留空,返回所有车型的预估价格。"
}
},
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"origin": { "type": "string", "description": "出发地名称" },
"originAddress": { "type": "string", "description": "出发地详细地址" },
"destination": { "type": "string", "description": "目的地名称" },
"destinationAddress": { "type": "string", "description": "目的地详细地址" },
"estimates": {
"type": "array",
"description": "各车型预估列表",
"items": {
"type": "object",
"properties": {
"carTypeId": { "type": "string", "enum": ["express", "premium", "carpool"] },
"carTypeName": { "type": "string" },
"price": { "type": "number", "description": "预估价格(元)" },
"distance": { "type": "string", "description": "预估距离" },
"duration": { "type": "string", "description": "预估时长" },
"desc": { "type": "string", "description": "车型描述" },
"eta": { "type": "string", "description": "预计接驾时间" }
},
"required": ["carTypeId", "carTypeName", "price", "distance", "duration", "desc", "eta"],
"additionalProperties": false
}
},
"selectedEstimate": {
"type": "object",
"description": "当前选中车型的预估详情",
"properties": {
"carTypeId": { "type": "string" },
"carTypeName": { "type": "string" },
"price": { "type": "number" },
"distance": { "type": "string" },
"duration": { "type": "string" },
"desc": { "type": "string" },
"eta": { "type": "string" }
},
"required": ["carTypeId", "carTypeName", "price", "distance", "duration", "desc", "eta"],
"additionalProperties": false
}
},
"required": ["origin", "destination", "estimates", "selectedEstimate"],
"additionalProperties": false
}
},
{
"name": "callTaxi",
"description": "呼叫出租车(业务对象:叫车状态卡片)。调用前置条件:用户已明确出发地和目的地,并选择或确认车型。发起叫车请求,返回叫车状态和预计等待时间。【严禁场景】禁止在无出发地或目的地时调用;禁止在已有进行中行程时重复叫车;禁止未确认费用就调用。",
"_meta": {
"ui": {
"componentPath": "components/calling-taxi-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"origin": {
"type": "string",
"description": "出发地名称,必须来自上游 estimateTrip 返回的 origin 原值或用户明确确认的出发地。【禁止编造】无出发地时禁止填写。"
},
"destination": {
"type": "string",
"description": "目的地名称,必须来自上游 estimateTrip 返回的 destination 原值或用户明确确认的目的地。【禁止编造】无目的地时禁止填写。"
},
"carType": {
"type": "string",
"enum": ["express", "premium", "carpool"],
"description": "车型 ID,来自上游 estimateTrip 返回的 estimates[].carTypeId。用户未明确指定时默认走快车(express)。"
}
},
"required": ["origin", "destination"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"tripId": { "type": "string", "description": "行程唯一 ID" },
"origin": { "type": "string" },
"destination": { "type": "string" },
"carType": { "type": "string" },
"carTypeName": { "type": "string" },
"price": { "type": "number", "description": "预估价格" },
"status": { "type": "string", "description": "calling(叫车中)/ en_route(已接单)/ arrived(已到达)/ in_trip(行程中)/ completed(已完成)/ cancelled(已取消)" },
"statusText": { "type": "string", "description": "状态文案" },
"callTime": { "type": "string", "description": "叫车时间 ISO 字符串" },
"estimatedWait": { "type": "string", "description": "预计等待时间" },
"driverName": { "type": "string", "description": "司机姓名,接单后填充" },
"plateNumber": { "type": "string", "description": "车牌号,接单后填充" },
"driverPhone": { "type": "string", "description": "司机电话,接单后填充" }
},
"required": ["tripId", "origin", "destination", "carType", "carTypeName", "price", "status", "statusText", "callTime", "estimatedWait"],
"additionalProperties": false
}
},
{
"name": "getTripStatus",
"description": "查看行程状态(业务对象:行程状态卡片)。调用前置条件:已有进行中的行程(tripId),或用户想查看当前是否有进行中的行程。返回行程实时状态、司机信息、预计到达时间等。【严禁场景】禁止在没有有效 tripId 时传入 tripId 参数;用户单纯问『我的车到哪了』时可不传 tripId,返回当前活跃行程状态。",
"_meta": {
"ui": {
"componentPath": "components/trip-status-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"tripId": {
"type": "string",
"description": "行程唯一标识。取值来源:上游 callTaxi 返回的 tripId 原值。留空时返回当前用户的活跃行程状态。"
}
},
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"trip": {
"type": "object",
"properties": {
"tripId": { "type": "string" },
"origin": { "type": "string" },
"destination": { "type": "string" },
"carType": { "type": "string" },
"carTypeName": { "type": "string" },
"price": { "type": "number" },
"status": { "type": "string" },
"statusText": { "type": "string" },
"startTime": { "type": "string" },
"driverName": { "type": "string" },
"plateNumber": { "type": "string" },
"driverPhone": { "type": "string" },
"estimatedArrival": { "type": "string", "description": "预计到达上车点时间" },
"remainingDistance": { "type": "string", "description": "司机距您还有多远" }
},
"required": ["tripId", "origin", "destination", "carType", "carTypeName", "status", "statusText"],
"additionalProperties": false
},
"hasActiveTrip": { "type": "boolean" }
},
"required": ["trip", "hasActiveTrip"],
"additionalProperties": false
}
},
{
"name": "getTripHistory",
"description": "查看历史行程(业务对象:历史行程列表卡片)。调用前置条件:用户想查看历史打车记录、费用明细。返回历史行程列表,包含时间、路线、费用、司机信息等。【严禁场景】禁止在用户询问当前行程状态时调用此接口替代 getTripStatus。",
"_meta": {
"ui": {
"componentPath": "components/trip-history-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": {
"tripId": { "type": "string" },
"origin": { "type": "string" },
"destination": { "type": "string" },
"carTypeName": { "type": "string" },
"price": { "type": "number" },
"status": { "type": "string", "description": "completed / cancelled" },
"startTime": { "type": "string" },
"endTime": { "type": "string" },
"duration": { "type": "string" },
"distance": { "type": "string" },
"driverName": { "type": "string" },
"plateNumber": { "type": "string" }
},
"required": ["tripId", "origin", "destination", "carTypeName", "price", "status", "startTime"],
"additionalProperties": false
}
},
"total": { "type": "number" }
},
"required": ["items", "total"],
"additionalProperties": false
}
}
],
"components": [
{
"path": "components/trip-estimate-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/calling-taxi-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/trip-status-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/trip-history-card/index",
"relatedPage": "/pages/home/home"
}
]
}
taxi-skill
出行打车,支持预估行程、呼叫出租车、查看行程状态及历史记录。
功能
- 预估各车型(快车/专车/拼车)价格与时长
- 发起叫车请求
- 实时查看行程状态与司机信息
- 查看历史行程记录
用户输入示例
- "打个车"
- "我要去机场"
- "从国贸到三里屯多少钱"
- "叫个快车"
- "车到哪了"
- "看看我的行程记录"
原子接口
| 接口名 | 说明 |
|---|---|
estimateTrip | 预估行程价格与时长 |
callTaxi | 呼叫出租车 |
getTripStatus | 查看行程状态 |
getTripHistory | 查看历史行程 |
原子组件
| 组件路径 | 说明 |
|---|---|
components/trip-estimate-card/index | 行程预估卡片 |
components/calling-taxi-card/index | 叫车状态卡片 |
components/trip-status-card/index | 行程状态卡片 |
components/trip-history-card/index | 历史行程列表卡片 |
后端依赖
| 资源 | 名称 |
|---|---|
| 云函数 | taxi-skill-handler |
| 数据库集合 | trips |
// skills/taxi-skill/utils/util.js
const { destinations, carTypes, historyTrips, activeTrip } = 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 calcTripPrice(origin, destination, carTypeId) {
const carType = carTypes.find(c => c.id === carTypeId) || carTypes[0]
const distance = Math.floor(Math.random() * 20 + 3)
const duration = Math.floor(Math.random() * 30 + 10)
const total = Math.round(carType.basePrice + carType.pricePerKm * distance + carType.pricePerMin * duration)
return { price: total, distance: distance + 'km', duration: duration + '分钟', carType }
}
function genTripId() {
return `T${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substring(2, 5).toUpperCase()}`
}
function formatTime(date) {
const d = date || new Date()
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
module.exports = {
isPreviewMode,
errorResult,
successResult,
calcTripPrice,
genTripId,
formatTime,
destinations,
carTypes,
historyTrips,
activeTrip
}
Related skills
Automation & Workflowsintegrations