
Order Skill
- 1 installs
- 27 repo stars
- Updated June 18, 2026
- tencentcloudbase/awesome-miniprogram-skills
WeChat Mini Program skill for food delivery: searching restaurants, browsing menus, placing paid orders, and tracking delivery status.
About
Adds a food-delivery flow to a WeChat Mini Program covering restaurant search, menu browsing, checkout, and delivery tracking. A developer uses it as a scenario template when building takeout ordering features.
- Handles restaurant and cuisine search plus menu browsing
- Covers online ordering, payment, and delivery-status tracking
Order Skill by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,980 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tencentcloudbase/awesome-miniprogram-skills --skill order-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 food delivery: searching restaurants, browsing menus, placing paid orders, and tracking delivery status.
Files
外卖点餐
基于餐厅列表完成餐厅搜索、菜单浏览、线上下单与配送状态查询的能力集合。
触发场景
用户原话举例(路由命中本技能):
- "帮我点个外卖"
- "附近有什么好吃的餐厅"
- "我想吃汉堡/川菜/日料"
- "看看这家店有什么菜"
- "我要下单"
- "我的外卖到哪了"
- "帮我查一下订单配送状态"
不适用范围
- 门店排队取号、排队进度查询 → 不在本技能范围,由排队取号技能处理
- 饮品点单、规格选择等 → 不在本技能范围,由饮品点单技能处理
- 待办事项、提醒设置等 → 不在本技能范围,由待办技能处理
- 地图导航、路线规划、电话联系餐厅等 → 不在本技能范围
前置条件
- 用户需在可用配送范围内
- 查询配送状态前需先完成下单拿到订单号
接口链路
searchRestaurants:餐厅搜索与候选餐厅列表展示。getMenuItems:查看指定餐厅的菜单与菜品列表。placeOrder:选择菜品后提交订单(需配送地址和联系电话)。getOrderStatus:基于真实订单号查询配送进度与骑手信息。
使用顺序
- 点餐前需先确定具体餐厅;没有餐厅上下文时,先展示附近餐厅列表,再查看该餐厅菜单。
- 下单前需先查看菜单并选择菜品;确认购物车、配送地址和联系电话后再执行下单。
- 查询配送状态前需先拿到有效订单号;没有有效订单时,不能直接查询配送进度。
- 所有已绑定组件的接口都应优先展示卡片,不要改成纯文本逐条展开。
// skills/order-skill/apis/getMenuItems.js — 查看餐厅菜单与菜品
const {
isPreviewMode,
successResult,
errorResult,
defaultRestaurantDetail
} = require('../utils/util')
async function getMenuItems(params = {}) {
console.info('[ai-mode] getMenuItems 入口, params=', JSON.stringify(params))
const restaurantId = params && params.restaurantId
if (!restaurantId) {
return errorResult('缺少 restaurantId 参数,请先通过 searchRestaurants 选择餐厅。禁止编造 restaurantId。')
}
if (isPreviewMode()) {
const restaurant = defaultRestaurantDetail(restaurantId)
if (!restaurant) {
return errorResult(`未找到 ID 为「${restaurantId}」的餐厅。请返回餐厅列表重新选择。`)
}
return buildResult(restaurant, restaurant.menu || [])
}
const { result } = await wx.cloud.callFunction({
name: 'order-skill-handler',
data: { action: 'getMenuItems', restaurantId }
})
if (result && result.code === 0 && result.data) {
const { restaurant, menu } = result.data
console.info('[ai-mode] getMenuItems 云函数返回, 菜品数=', (menu || []).length)
return buildResult(restaurant, menu)
}
return errorResult(result?.message || '请求失败')
}
function buildResult(restaurant, menu) {
if (!restaurant) {
return errorResult('未找到餐厅信息。')
}
const total = (menu || []).length
if (total > 0) {
return successResult(
`${restaurant.name} 共有 ${total} 道菜品。请展示菜单卡片,让用户选择想点的菜品并加入购物车。禁止以纯文本逐条列出菜品。`,
{
restaurant: {
restaurantId: restaurant.restaurantId,
name: restaurant.name,
rating: restaurant.rating,
monthlySales: restaurant.monthlySales,
deliveryFee: restaurant.deliveryFee,
estimatedMinutes: restaurant.estimatedMinutes,
minOrder: restaurant.minOrder,
tags: restaurant.tags,
status: restaurant.status
},
items: menu,
total
},
{ restaurantId: restaurant.restaurantId }
)
}
return successResult(
`${restaurant.name} 暂无菜品数据。`,
{ restaurant: { restaurantId: restaurant.restaurantId, name: restaurant.name }, items: [], total: 0 },
{ restaurantId: restaurant.restaurantId }
)
}
module.exports = getMenuItems
// skills/order-skill/apis/getOrderStatus.js — 查询订单配送状态
const {
isPreviewMode,
successResult,
errorResult,
defaultOrderDetail
} = require('../utils/util')
async function getOrderStatus(params = {}) {
console.info('[ai-mode] getOrderStatus 入口, params=', JSON.stringify(params))
const orderId = params && params.orderId
if (!orderId) {
return errorResult('缺少 orderId 参数,请先完成下单或提供真实订单号。禁止编造 orderId。')
}
if (isPreviewMode()) {
const order = defaultOrderDetail(orderId)
if (!order) {
return errorResult(`未找到订单「${orderId}」。请确认订单号是否正确。`)
}
return buildResult(order)
}
const { result } = await wx.cloud.callFunction({
name: 'order-skill-handler',
data: { action: 'getOrderStatus', orderId }
})
if (result && result.code === 0 && result.data) {
console.info('[ai-mode] getOrderStatus 云函数返回, status=', result.data.status)
return buildResult(result.data)
}
return errorResult(result?.message || '请求失败')
}
function buildResult(order) {
return successResult(
`订单 ${order.orderId} 当前状态:${order.statusText}。请展示订单配送状态卡片。`,
{ order },
{ orderId: order.orderId }
)
}
module.exports = getOrderStatus
// skills/order-skill/apis/placeOrder.js — 下单(选菜品+地址+支付)
const {
isPreviewMode,
successResult,
errorResult,
defaultRestaurantDetail,
genOrderId,
addOrder
} = require('../utils/util')
async function placeOrder(params = {}) {
console.info('[ai-mode] placeOrder 入口, params=', JSON.stringify(params))
const { restaurantId, items, deliveryAddress, contactPhone, deliveryNote } = (params || {})
if (!restaurantId) {
return errorResult('缺少 restaurantId 参数,请先通过 searchRestaurants 选择餐厅。禁止编造 restaurantId。')
}
if (!items || !Array.isArray(items) || items.length === 0) {
return errorResult('缺少 items 参数,请先通过 getMenuItems 选择菜品并加入购物车。')
}
if (!deliveryAddress) {
return errorResult('缺少配送地址,请输入配送地址。')
}
if (!contactPhone) {
return errorResult('缺少联系电话,请输入联系电话。')
}
if (isPreviewMode()) {
return buildMockResult(restaurantId, items, deliveryAddress, contactPhone, deliveryNote)
}
const { result } = await wx.cloud.callFunction({
name: 'order-skill-handler',
data: { action: 'placeOrder', restaurantId, items, deliveryAddress, contactPhone, deliveryNote }
})
if (result && result.code === 0 && result.data) {
console.info('[ai-mode] placeOrder 云函数下单成功, orderId=', result.data.orderId)
return buildResult(result.data, restaurantId)
}
return errorResult(result?.message || '请求失败')
}
function buildMockResult(restaurantId, items, deliveryAddress, contactPhone, deliveryNote) {
const restaurant = defaultRestaurantDetail(restaurantId)
if (!restaurant) {
return errorResult(`未找到 ID 为「${restaurantId}」的餐厅。`)
}
const now = new Date()
const orderData = {
orderId: genOrderId(),
restaurantId,
restaurantName: restaurant.name,
items: items.map((item) => ({
itemId: item.itemId,
name: item.name || '未知菜品',
price: item.price || 0,
quantity: item.quantity || 1
})),
totalAmount: items.reduce((sum, item) => sum + (item.price || 0) * (item.quantity || 1), 0),
deliveryFee: restaurant.deliveryFee,
deliveryAddress,
contactPhone,
status: 'pending',
statusText: '商家接单中',
riderName: '',
riderPhone: '',
estimatedArrival: `约${restaurant.estimatedMinutes}分钟`,
orderTime: now.toLocaleString('zh-CN', { hour12: false }),
deliveryNote: deliveryNote || ''
}
addOrder(orderData)
return buildResult(orderData, restaurantId)
}
function buildResult(orderData, restaurantId) {
return successResult(
`订单已提交!${orderData.restaurantName} 正在准备中,预计 ${orderData.estimatedArrival} 送达。请展示订单确认卡片,包含菜品清单、金额、配送信息。`,
{ order: orderData },
{ restaurantId }
)
}
module.exports = placeOrder
// skills/order-skill/apis/searchRestaurants.js — 搜索附近餐厅
const {
isPreviewMode,
successResult,
errorResult,
defaultRestaurantList
} = require('../utils/util')
async function searchRestaurants(params = {}) {
console.info('[ai-mode] searchRestaurants 入口, params=', JSON.stringify(params))
const keyword = String((params && params.keyword) || '').trim()
if (isPreviewMode()) {
return buildResult(defaultRestaurantList(keyword), keyword)
}
const { result } = await wx.cloud.callFunction({
name: 'order-skill-handler',
data: { action: 'searchRestaurants', keyword }
})
if (result && result.code === 0 && result.data) {
const items = result.data.items || []
console.info('[ai-mode] searchRestaurants 云函数返回数量=', items.length)
return buildResult(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 = searchRestaurants
// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
// 餐厅种子数据(来自 seed.js)
const restaurants = [
{
restaurantId: 'R001',
name: '麦香基·望京店',
rating: 4.6,
monthlySales: 3280,
distance: '680m',
deliveryFee: 3,
estimatedMinutes: 25,
tags: ['汉堡', '炸鸡', '快餐'],
keywords: ['望京', '快餐', '汉堡'],
minOrder: 20,
status: 'open',
menu: [
{ itemId: 'M001', name: '香辣鸡腿堡套餐', price: 32.9, image: 'https://picsum.photos/seed/m001/400/300', description: '香辣鸡腿堡+薯条+可乐', monthlySales: 1850 },
{ itemId: 'M002', name: '经典牛肉堡', price: 26.9, image: 'https://picsum.photos/seed/m002/400/300', description: '100%澳洲牛肉饼配新鲜蔬菜', monthlySales: 1220 },
{ itemId: 'M003', name: '脆皮炸鸡桶(6块)', price: 39.9, image: 'https://picsum.photos/seed/m003/400/300', description: '外酥里嫩,秘制腌料', monthlySales: 980 },
{ itemId: 'M004', name: '黄金薯条(大)', price: 12.9, image: 'https://picsum.photos/seed/m004/400/300', description: '现炸酥脆,撒上海盐', monthlySales: 2100 },
{ itemId: 'M005', name: '冰镇可乐(大)', price: 8.0, image: 'https://picsum.photos/seed/m005/400/300', description: '畅爽怡神', monthlySales: 2600 }
]
},
{
restaurantId: 'R002',
name: '川味轩·国贸店',
rating: 4.8,
monthlySales: 2150,
distance: '1.2km',
deliveryFee: 4,
estimatedMinutes: 30,
tags: ['川菜', '麻辣', '中餐'],
keywords: ['国贸', '川菜', '麻辣', '中餐'],
minOrder: 25,
status: 'open',
menu: [
{ itemId: 'M101', name: '水煮鱼', price: 58.0, image: 'https://picsum.photos/seed/m101/400/300', description: '活鱼现杀,麻辣鲜香', monthlySales: 760 },
{ itemId: 'M102', name: '麻婆豆腐', price: 22.0, image: 'https://picsum.photos/seed/m102/400/300', description: '正宗川味,入口即化', monthlySales: 1430 },
{ itemId: 'M103', name: '宫保鸡丁', price: 36.0, image: 'https://picsum.photos/seed/m103/400/300', description: '花生与鸡丁的经典搭配', monthlySales: 1150 },
{ itemId: 'M104', name: '酸辣土豆丝', price: 16.0, image: 'https://picsum.photos/seed/m104/400/300', description: '家常酸辣味', monthlySales: 980 },
{ itemId: 'M105', name: '米饭', price: 3.0, image: 'https://picsum.photos/seed/m105/400/300', description: '东北大米', monthlySales: 3000 }
]
},
{
restaurantId: 'R003',
name: '寿司之魂·三里屯店',
rating: 4.7,
monthlySales: 1680,
distance: '2.0km',
deliveryFee: 5,
estimatedMinutes: 35,
tags: ['日料', '寿司', '刺身'],
keywords: ['三里屯', '日料', '寿司'],
minOrder: 30,
status: 'open',
menu: [
{ itemId: 'M201', name: '三文鱼刺身(8片)', price: 68.0, image: 'https://picsum.photos/seed/m201/400/300', description: '挪威进口三文鱼,当日空运', monthlySales: 620 },
{ itemId: 'M202', name: '鳗鱼手握(2个)', price: 28.0, image: 'https://picsum.photos/seed/m202/400/300', description: '蒲烧鳗鱼配醋饭', monthlySales: 880 },
{ itemId: 'M203', name: '加州卷(8个)', price: 42.0, image: 'https://picsum.photos/seed/m203/400/300', description: '蟹棒+牛油果+黄瓜', monthlySales: 750 },
{ itemId: 'M204', name: '味噌汤', price: 12.0, image: 'https://picsum.photos/seed/m204/400/300', description: '日式传统味噌', monthlySales: 1100 }
]
},
{
restaurantId: 'R004',
name: '兰州拉面·中关村店',
rating: 4.5,
monthlySales: 4520,
distance: '350m',
deliveryFee: 2,
estimatedMinutes: 20,
tags: ['面食', '西北', '中餐'],
keywords: ['中关村', '拉面', '面食'],
minOrder: 15,
status: 'open',
menu: [
{ itemId: 'M301', name: '招牌牛肉拉面', price: 22.0, image: 'https://picsum.photos/seed/m301/400/300', description: '手工拉面,牛骨浓汤', monthlySales: 3200 },
{ itemId: 'M302', name: '牛肉板面', price: 24.0, image: 'https://picsum.photos/seed/m302/400/300', description: '宽面配卤牛肉', monthlySales: 1450 },
{ itemId: 'M303', name: '凉皮', price: 12.0, image: 'https://picsum.photos/seed/m303/400/300', description: '陕西风味凉皮', monthlySales: 980 },
{ itemId: 'M304', name: '肉夹馍', price: 10.0, image: 'https://picsum.photos/seed/m304/400/300', description: '腊汁肉夹白吉馍', monthlySales: 2100 },
{ itemId: 'M305', name: '酸梅汤', price: 6.0, image: 'https://picsum.photos/seed/m305/400/300', description: '冰镇解暑', monthlySales: 1800 },
{ itemId: 'M306', name: '卤蛋', price: 2.0, image: 'https://picsum.photos/seed/m306/400/300', description: '五香卤蛋', monthlySales: 2500 }
]
},
{
restaurantId: 'R005',
name: '必胜乐·五道口店',
rating: 4.4,
monthlySales: 2860,
distance: '800m',
deliveryFee: 3,
estimatedMinutes: 28,
tags: ['披萨', '西餐', '意面'],
keywords: ['五道口', '披萨', '西餐'],
minOrder: 28,
status: 'open',
menu: [
{ itemId: 'M401', name: '超级至尊披萨(9寸)', price: 59.0, image: 'https://picsum.photos/seed/m401/400/300', description: '培根+香肠+青椒+蘑菇', monthlySales: 920 },
{ itemId: 'M402', name: '奶油蘑菇意面', price: 36.0, image: 'https://picsum.photos/seed/m402/400/300', description: '浓郁奶油酱汁', monthlySales: 680 },
{ itemId: 'M403', name: 'BBQ烤鸡翅(6只)', price: 28.0, image: 'https://picsum.photos/seed/m403/400/300', description: '秘制BBQ酱烤制', monthlySales: 1100 },
{ itemId: 'M404', name: '凯撒沙拉', price: 22.0, image: 'https://picsum.photos/seed/m404/400/300', description: '新鲜罗马生菜配凯撒酱', monthlySales: 450 },
{ itemId: 'M405', name: '柠檬红茶', price: 10.0, image: 'https://picsum.photos/seed/m405/400/300', description: '冰爽柠檬红茶', monthlySales: 1500 }
]
},
{
restaurantId: 'R006',
name: '粥公粥婆·西二旗店',
rating: 4.3,
monthlySales: 1980,
distance: '1.5km',
deliveryFee: 2,
estimatedMinutes: 22,
tags: ['粥', '早餐', '养生'],
keywords: ['西二旗', '粥', '早餐'],
minOrder: 10,
status: 'open',
menu: [
{ itemId: 'M501', name: '皮蛋瘦肉粥', price: 15.0, image: 'https://picsum.photos/seed/m501/400/300', description: '慢火熬制,绵密鲜香', monthlySales: 1600 },
{ itemId: 'M502', name: '鲜虾粥', price: 28.0, image: 'https://picsum.photos/seed/m502/400/300', description: '鲜活大虾现煮', monthlySales: 780 },
{ itemId: 'M503', name: '小笼包(8只)', price: 18.0, image: 'https://picsum.photos/seed/m503/400/300', description: '鲜肉小笼,汤汁饱满', monthlySales: 1200 },
{ itemId: 'M504', name: '油条', price: 3.0, image: 'https://picsum.photos/seed/m504/400/300', description: '现炸酥脆', monthlySales: 2200 },
{ itemId: 'M505', name: '豆浆', price: 5.0, image: 'https://picsum.photos/seed/m505/400/300', description: '现磨浓豆浆', monthlySales: 1800 }
]
},
{
restaurantId: 'R007',
name: '沙县小吃·知春路店',
rating: 4.2,
monthlySales: 3650,
distance: '200m',
deliveryFee: 1,
estimatedMinutes: 15,
tags: ['小吃', '简餐', '中式'],
keywords: ['知春路', '小吃', '简餐'],
minOrder: 8,
status: 'open',
menu: [
{ itemId: 'M601', name: '蒸饺(10只)', price: 10.0, image: 'https://picsum.photos/seed/m601/400/300', description: '手工现包蒸饺', monthlySales: 2800 },
{ itemId: 'M602', name: '鸡腿饭', price: 16.0, image: 'https://picsum.photos/seed/m602/400/300', description: '卤鸡腿+时蔬+米饭', monthlySales: 2100 },
{ itemId: 'M603', name: '扁肉(小份)', price: 8.0, image: 'https://picsum.photos/seed/m603/400/300', description: '沙县特色扁肉', monthlySales: 1800 },
{ itemId: 'M604', name: '拌面', price: 6.0, image: 'https://picsum.photos/seed/m604/400/300', description: '花生酱拌面', monthlySales: 3200 },
{ itemId: 'M605', name: '炖罐(排骨)', price: 12.0, image: 'https://picsum.photos/seed/m605/400/300', description: '隔水炖排骨汤', monthlySales: 950 }
]
}
]
// 云函数入口函数
exports.main = async (event, context) => {
const { action } = event
const wxContext = cloud.getWXContext()
const openid = wxContext.OPENID
switch (action) {
case 'searchRestaurants': {
const { keyword } = event
let results = restaurants
if (keyword) {
const kw = keyword.toLowerCase()
results = restaurants.filter(r =>
r.name.includes(kw) ||
r.keywords.some(k => k.includes(kw)) ||
r.tags.some(t => t.includes(kw))
)
}
return { code: 0, data: results }
}
case 'getMenuItems': {
const { restaurantId } = event
const restaurant = restaurants.find(r => r.restaurantId === restaurantId)
if (!restaurant) {
return { code: -1, msg: '餐厅不存在' }
}
return { code: 0, data: { restaurant, menu: restaurant.menu } }
}
case 'placeOrder': {
const { restaurantId, items, deliveryAddress, contactPhone } = event
if (!restaurantId || !items || !deliveryAddress) {
return { code: -1, msg: '参数不完整' }
}
const totalPrice = items.reduce((sum, item) => sum + (item.price || 0) * (item.quantity || 1), 0)
const orderId = 'OD' + Date.now()
const order = {
orderId,
restaurantId,
items,
totalPrice,
status: 'pending',
address: deliveryAddress,
openid,
createdAt: new Date()
}
await db.collection('orders').add({ data: order })
return { code: 0, data: { orderId, status: 'pending' } }
}
case 'getOrderStatus': {
const { orderId } = event
if (!orderId) {
return { code: -1, msg: '缺少订单ID' }
}
const res = await db.collection('orders').where({ orderId, openid }).get()
if (res.data.length === 0) {
return { code: -1, msg: '订单不存在' }
}
return { code: 0, data: res.data[0] }
}
default:
return { code: -1, msg: `未知 action: ${action}` }
}
}
{
"name": "order-skill-handler",
"version": "1.0.0",
"description": "order-skill 云函数",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}
// skills/order-skill/components/menu-list-card/index.js
Component({
data: {
restaurant: {},
items: [],
visibleItems: [],
omittedCount: 0,
cart: [],
cartCount: 0,
cartTotal: 0
},
lifetimes: {
created() {
console.info('[ai-mode] menu-list-card created')
const { NotificationType } = wx.modelContext
const modelCtx = wx.modelContext.getContext(this)
const viewCtx = wx.modelContext.getViewContext(this)
modelCtx.on(NotificationType.Result, (data) => {
const sc = (data && data.result && data.result.structuredContent) || {}
const items = sc.items || []
console.info('[ai-mode] menu-list-card 收到 Result, items=', items.length)
// 1:1 容器约 100vw 高,每个紧凑项约 13.33vw,最多显示 4 项
const maxVisible = 4
const visibleItems = items.slice(0, maxVisible)
const omittedCount = Math.max(items.length - maxVisible, 0)
this.setData({
restaurant: sc.restaurant || {},
items,
visibleItems,
omittedCount,
cart: [],
cartCount: 0,
cartTotal: 0
})
console.info(`[ai-mode] menu-list-card setData total=${items.length} visible=${visibleItems.length} omitted=${omittedCount}`)
})
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] menu-list-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] menu-list-card overflow monitor=on')
}
},
methods: {
onTapItem(e) {
const item = e.currentTarget.dataset.item
const restaurantId = this.data.restaurant.restaurantId || ''
console.info(`[ai-mode] menu-list-card send api/call name=placeOrder args=${JSON.stringify({restaurantId, items: [item]})}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: `选择 ${item.name}, 下单` },
{ type: 'api/call', data: { name: 'placeOrder', arguments: { restaurantId, items: [item] } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="ml-card">
<view class="ml-header">
<text class="ml-rest-name">{{restaurant.name}}</text>
<text class="ml-rest-meta">{{restaurant.rating}}分 · 月售{{restaurant.monthlySales}}</text>
</view>
<view wx:if="{{!items.length}}" class="ml-empty">
<text class="ml-empty-title">暂无菜单</text>
</view>
<view class="ml-list" wx:else>
<view class="ml-item {{index === 0 ? 'ml-item--first' : ''}}" wx:for="{{visibleItems}}" wx:key="itemId" data-item="{{item}}" bind:tap="onTapItem">
<image class="ml-item-img" src="{{item.image}}" mode="aspectFill" wx:if="{{item.image}}" />
<view class="ml-info">
<text class="ml-item-name">{{item.name}}</text>
<text class="ml-item-desc" wx:if="{{item.description}}">{{item.description}}</text>
<view class="ml-item-bottom">
<text class="ml-item-price">¥{{item.price}}</text>
<text class="ml-item-sales" wx:if="{{item.monthlySales}}">月售{{item.monthlySales}}</text>
</view>
</view>
</view>
<view class="ml-omitted" wx:if="{{omittedCount > 0}}">还有 {{omittedCount}} 道菜品未展示</view>
</view>
</view>
/* ratio=1:1 菜单列表卡片,紧凑布局
* 色源:project app.json style (order-skill uses warm orange)
*/
.ml-card {
background: #FFFFFF;
border-radius: 1.07vw;
padding: 3.2vw;
box-sizing: border-box;
overflow: hidden;
}
.ml-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2.4vw;
}
.ml-rest-name {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
.ml-rest-meta {
font-size: 3.2vw;
color: rgba(0,0,0,0.45);
flex-shrink: 0;
margin-left: 2.4vw;
}
.ml-list {
display: flex;
flex-direction: column;
}
.ml-item {
display: flex;
align-items: center;
padding: 1.6vw 0;
border-top: 1px solid rgba(0,0,0,0.06);
}
.ml-item--first {
border-top: none;
}
.ml-item-img {
width: 10.67vw;
height: 10.67vw;
border-radius: 1.07vw;
background: #F5F5F5;
flex-shrink: 0;
margin-right: 2.13vw;
}
.ml-info {
flex: 1;
min-width: 0;
}
.ml-item-name {
font-size: 3.73vw;
font-weight: 500;
color: rgba(0,0,0,0.9);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ml-item-desc {
font-size: 3.2vw;
color: rgba(0,0,0,0.3);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 0.53vw;
}
.ml-item-bottom {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 0.53vw;
}
.ml-item-price {
font-size: 3.73vw;
font-weight: 600;
color: #FF6B35;
}
.ml-item-sales {
font-size: 2.93vw;
color: rgba(0,0,0,0.45);
}
.ml-omitted {
text-align: center;
font-size: 3.2vw;
color: rgba(0,0,0,0.45);
padding: 2.13vw 0 0.53vw;
}
@media (prefers-color-scheme: dark) {
.ml-card { background: #1C1C1E; }
.ml-rest-name { color: rgba(255,255,255,0.9); }
.ml-rest-meta { color: rgba(255,255,255,0.45); }
.ml-item { border-color: rgba(255,255,255,0.08); }
.ml-item--first { border-color: transparent; }
.ml-item-name { color: rgba(255,255,255,0.9); }
.ml-item-desc { color: rgba(255,255,255,0.3); }
.ml-item-price { color: #FF9F6E; }
.ml-item-sales { color: rgba(255,255,255,0.45); }
.ml-omitted { color: rgba(255,255,255,0.45); }
}
// skills/order-skill/components/order-confirm-card/index.js
Component({
data: {
order: {},
visibleItems: [],
omittedCount: 0
},
lifetimes: {
created() {
console.info('[ai-mode] order-confirm-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) || {}
const order = sc.order || {}
const items = order.items || []
const maxVisible = 3
const omittedCount = Math.max(items.length - maxVisible, 0)
order.items = items.slice(0, maxVisible)
order._omittedCount = omittedCount
console.info('[ai-mode] order-confirm-card 收到 Result, orderId=', order.orderId, 'items=', items.length, 'visible=', items.length)
this.setData({ order })
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const dimensions = viewCtx.getDimensions()
console.info(`[ai-mode] order-confirm-card dimensions width=${dimensions.width} minHeight=${dimensions.minHeight} maxHeight=${dimensions.maxHeight}`)
} catch (e) {
console.info('[ai-mode] order-confirm-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] order-confirm-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] order-confirm-card overflow monitor=on')
}
},
methods: {
onTapTrack(e) {
const { orderId } = e.currentTarget.dataset
console.info(`[ai-mode] order-confirm-card send api/call name=getOrderStatus args=${JSON.stringify({ orderId })}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '查看配送状态' },
{ type: 'api/call', data: { name: 'getOrderStatus', arguments: { orderId } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="oc-card">
<view class="oc-title">订单确认</view>
<view wx:if="{{order.statusText}}" class="oc-status-badge">{{order.statusText}}</view>
<view class="oc-rest-name">{{order.restaurantName}}</view>
<view class="oc-items">
<view class="oc-item" wx:for="{{order.items}}" wx:key="itemId">
<text class="oc-item-name">{{item.name}}</text>
<text class="oc-item-qty">x{{item.quantity}}</text>
<text class="oc-item-price">¥{{(item.price * item.quantity).toFixed(1)}}</text>
</view>
</view>
<view wx:if="{{order._omittedCount > 0}}" class="oc-omitted">还有 {{order._omittedCount}} 件商品</view>
<view class="oc-divider"></view>
<view class="oc-summary">
<text>商品小计</text>
<text>¥{{(order.totalAmount).toFixed(1)}}</text>
</view>
<view class="oc-summary">
<text>配送费</text>
<text>¥{{(order.deliveryFee || 0).toFixed(1)}}</text>
</view>
<view class="oc-total-row">
<text>合计</text>
<text class="oc-total-amount">¥{{((order.totalAmount || 0) + (order.deliveryFee || 0)).toFixed(1)}}</text>
</view>
<view class="oc-delivery-info">
<view class="oc-delivery-row">
<text class="oc-delivery-label">配送地址</text>
<text>{{order.deliveryAddress}}</text>
</view>
<view class="oc-delivery-row">
<text class="oc-delivery-label">联系电话</text>
<text>{{order.contactPhone}}</text>
</view>
<view wx:if="{{order.riderName}}" class="oc-delivery-row">
<text class="oc-delivery-label">配送骑手</text>
<text>{{order.riderName}} {{order.riderPhone}}</text>
</view>
<view class="oc-delivery-row oc-delivery-row-last">
<text class="oc-delivery-label">预计送达</text>
<text>{{order.estimatedArrival}}</text>
</view>
</view>
<view wx:if="{{order.deliveryNote}}" class="oc-note">备注:{{order.deliveryNote}}</view>
<view class="oc-note">下单时间:{{order.orderTime}}</view>
<view class="oc-btn-row">
<view
class="oc-btn-secondary"
hover-class="oc-btn-secondary-hover"
bind:tap="onTapTrack"
data-order-id="{{order.orderId}}"
>查看配送</view>
</view>
</view>
/* ratio=1:1;美团/饿了么暖橙风格
强调色 #FF6B35 | 浅橙底 #FFF5F0
按钮渐变 linear-gradient(135deg, #FF8A50, #FF6B35)
大圆角 16px→3.2vw / 12px→2.4vw
柔和阴影 0 2px 8px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06)
字体:标题 17px→4.53vw 600 / 正文 15px→4vw / 注释 13px→3.47vw
文字色阶:主文 rgba(0,0,0,0.85) / 次要 rgba(0,0,0,0.50) / 辅助 rgba(0,0,0,0.30)
卡片内边距 16px→3.2vw,元素间距 12px→2.4vw */
.oc-card {
background: #FFFFFF;
border-radius: 3.2vw;
padding: 3.2vw;
box-sizing: border-box;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06);
}
.oc-title {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.85);
margin-bottom: 2.4vw;
}
.oc-status-badge {
display: inline-block;
padding: 0.8vw 2.13vw;
background: #FFF5F0;
border-radius: 999rpx;
font-size: 3.2vw;
color: #FF6B35;
margin-bottom: 2.4vw;
}
.oc-rest-name {
font-size: 4vw;
font-weight: 500;
color: rgba(0,0,0,0.85);
margin-bottom: 2.4vw;
}
.oc-items {
margin-bottom: 2.4vw;
}
.oc-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.6vw 0;
font-size: 3.73vw;
color: rgba(0,0,0,0.85);
}
.oc-item-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.oc-item-qty {
margin: 0 2.13vw;
color: rgba(0,0,0,0.50);
font-size: 3.47vw;
}
.oc-item-price {
flex-shrink: 0;
color: rgba(0,0,0,0.85);
}
.oc-divider {
height: 1px;
background: rgba(0,0,0,0.06);
margin: 2.4vw 0;
}
.oc-summary {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.6vw;
font-size: 3.47vw;
color: rgba(0,0,0,0.50);
}
.oc-total-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2.4vw;
font-size: 4vw;
font-weight: 600;
color: rgba(0,0,0,0.85);
}
.oc-total-amount {
color: #FF6B35;
font-size: 4.53vw;
}
.oc-delivery-info {
background: #FFF5F0;
border-radius: 2.4vw;
padding: 2.4vw;
margin-bottom: 2.4vw;
}
.oc-delivery-row {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 3.47vw;
color: rgba(0,0,0,0.85);
margin-bottom: 1.07vw;
}
.oc-delivery-row-last {
margin-bottom: 0;
}
.oc-delivery-label {
color: rgba(0,0,0,0.50);
}
.oc-note {
margin-top: 2.4vw;
font-size: 3.2vw;
color: rgba(0,0,0,0.30);
line-height: 1.5;
}
.oc-btn-row {
margin-top: 2.4vw;
display: flex;
gap: 2.4vw;
}
.oc-btn {
flex: 1;
height: 10.67vw;
line-height: 10.67vw;
text-align: center;
font-size: 3.73vw;
font-weight: 500;
color: #FFFFFF;
background: linear-gradient(135deg, #FF8A50, #FF6B35);
border-radius: 2.4vw;
}
.oc-btn-hover {
opacity: 0.88;
}
.oc-btn-secondary {
flex: 1;
height: 10.67vw;
line-height: 10.67vw;
text-align: center;
font-size: 3.73vw;
font-weight: 500;
color: #FF6B35;
background: #FFF5F0;
border-radius: 2.4vw;
}
.oc-btn-secondary-hover {
opacity: 0.8;
}
@media (prefers-color-scheme: dark) {
.oc-card {
background: #1C1C1E;
box-shadow: none;
}
.oc-title, .oc-rest-name, .oc-item, .oc-item-price, .oc-total-row { color: #F5F5F7; }
.oc-item-qty, .oc-summary, .oc-delivery-label { color: rgba(245,245,247,0.50); }
.oc-status-badge { background: #2C2C2E; color: #FF9F6E; }
.oc-divider { background: rgba(245,245,247,0.10); }
.oc-total-amount { color: #FF9F6E; }
.oc-delivery-info { background: #2C2C2E; }
.oc-delivery-row { color: #F5F5F7; }
.oc-note { color: rgba(245,245,247,0.30); }
.oc-btn-secondary { background: #2C2C2E; color: #FF9F6E; }
}
// skills/order-skill/components/order-status-card/index.js
Component({
data: {
order: {}
},
lifetimes: {
created() {
console.info('[ai-mode] order-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) || {}
const order = sc.order || {}
console.info('[ai-mode] order-status-card 收到 Result, orderId=', order.orderId, 'status=', order.status)
this.setData({ order })
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] order-status-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] order-status-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] order-status-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] order-status-card overflow monitor=on')
}
},
methods: {
onTapRefresh(e) {
const { orderId } = e.currentTarget.dataset
console.info(`[ai-mode] order-status-card send api/call name=getOrderStatus args=${JSON.stringify({ orderId })}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '刷新配送状态' },
{ type: 'api/call', data: { name: 'getOrderStatus', arguments: { orderId } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="os-card">
<view class="os-header">
<view class="os-title">配送状态</view>
<view class="os-badge os-badge-{{order.status}}">{{order.statusText}}</view>
</view>
<view class="os-rest-name">{{order.restaurantName}}</view>
<!-- 进度条 -->
<view class="os-progress">
<view class="os-step">
<view class="os-step-dot {{order.status === 'completed' || order.status === 'delivering' ? 'active' : 'current'}}"></view>
<view class="os-step-label {{order.status === 'completed' || order.status === 'delivering' ? 'active' : ''}}">已下单</view>
</view>
<view class="os-step">
<view class="os-step-line {{order.status === 'completed' || order.status === 'delivering' ? 'active' : ''}}"></view>
<view class="os-step-dot {{order.status === 'delivering' ? 'current' : (order.status === 'completed' ? 'active' : '')}}"></view>
<view class="os-step-label {{order.status === 'delivering' || order.status === 'completed' ? 'active' : ''}}">配送中</view>
</view>
<view class="os-step">
<view class="os-step-line {{order.status === 'completed' ? 'active' : ''}}"></view>
<view class="os-step-dot {{order.status === 'completed' ? 'active' : ''}}"></view>
<view class="os-step-label {{order.status === 'completed' ? 'active' : ''}}">已送达</view>
</view>
</view>
<view class="os-order-id">订单号:{{order.orderId}}</view>
<view class="os-info">
<view class="os-info-row">
<text class="os-info-label">预计送达</text>
<text>{{order.estimatedArrival}}</text>
</view>
<view wx:if="{{order.riderName}}" class="os-info-row">
<text class="os-info-label">配送骑手</text>
<text>{{order.riderName}} {{order.riderPhone}}</text>
</view>
<view class="os-info-row os-info-row-last">
<text class="os-info-label">配送地址</text>
<text>{{order.deliveryAddress}}</text>
</view>
</view>
<view class="os-btn-row">
<view
class="os-btn"
hover-class="os-btn-hover"
bind:tap="onTapRefresh"
data-order-id="{{order.orderId}}"
>刷新状态</view>
</view>
</view>
/* ratio=4:1;美团/饿了么暖橙风格
强调色 #FF6B35 | 浅橙底 #FFF5F0
按钮渐变 linear-gradient(135deg, #FF8A50, #FF6B35)
大圆角 16px→3.2vw / 12px→2.4vw
柔和阴影 0 2px 8px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06)
字体:标题 17px→4.53vw 600 / 正文 15px→4vw / 注释 13px→3.47vw
文字色阶:主文 rgba(0,0,0,0.85) / 次要 rgba(0,0,0,0.50) / 辅助 rgba(0,0,0,0.30)
卡片内边距 16px→3.2vw,元素间距 12px→2.4vw */
.os-card {
background: #FFFFFF;
border-radius: 3.2vw;
padding: 3.2vw;
box-sizing: border-box;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06);
}
.os-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2.4vw;
}
.os-title {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.85);
}
.os-badge {
padding: 0.8vw 2.13vw;
background: #FFF5F0;
border-radius: 999rpx;
font-size: 3.2vw;
color: #FF6B35;
font-weight: 500;
}
.os-badge-delivering {
background: #FFF5F0;
color: #FF6B35;
}
.os-badge-completed {
background: #E8F5E9;
color: #2E7D32;
}
.os-badge-pending {
background: #FFF8E1;
color: #F57F17;
}
.os-rest-name {
font-size: 4vw;
font-weight: 500;
color: rgba(0,0,0,0.85);
margin-bottom: 2.4vw;
}
.os-progress {
display: flex;
align-items: center;
margin-bottom: 2.4vw;
}
.os-step {
flex: 1;
text-align: center;
position: relative;
}
.os-step-dot {
width: 3.2vw;
height: 3.2vw;
border-radius: 50%;
margin: 0 auto 1.07vw;
background: #D1D5DB;
}
.os-step-dot.active {
background: linear-gradient(135deg, #FF8A50, #FF6B35);
}
.os-step-dot.current {
background: #FF6B35;
box-shadow: 0 0 0 0.53vw rgba(255,107,53,0.2);
}
.os-step-label {
font-size: 3.2vw;
color: rgba(0,0,0,0.30);
}
.os-step-label.active {
color: rgba(0,0,0,0.85);
font-weight: 500;
}
.os-step-line {
position: absolute;
top: 1.33vw;
left: 50%;
width: 100%;
height: 0.53vw;
background: #D1D5DB;
}
.os-step-line.active {
background: linear-gradient(90deg, #FF8A50, #FF6B35);
}
.os-info {
background: #FFF5F0;
border-radius: 2.4vw;
padding: 2.4vw;
margin-bottom: 2.4vw;
}
.os-info-row {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 3.47vw;
color: rgba(0,0,0,0.85);
margin-bottom: 1.07vw;
}
.os-info-row-last {
margin-bottom: 0;
}
.os-info-label {
color: rgba(0,0,0,0.50);
}
.os-order-id {
font-size: 3.2vw;
color: rgba(0,0,0,0.30);
margin-bottom: 1.6vw;
}
.os-btn-row {
display: flex;
gap: 2.4vw;
}
.os-btn {
flex: 1;
height: 10.67vw;
line-height: 10.67vw;
text-align: center;
font-size: 3.73vw;
font-weight: 500;
color: #FFFFFF;
background: linear-gradient(135deg, #FF8A50, #FF6B35);
border-radius: 2.4vw;
}
.os-btn-hover {
opacity: 0.88;
}
@media (prefers-color-scheme: dark) {
.os-card {
background: #1C1C1E;
box-shadow: none;
}
.os-title, .os-rest-name { color: #F5F5F7; }
.os-badge { background: #2C2C2E; color: #FF9F6E; }
.os-badge-completed { background: #1B3D1B; color: #6EE7B7; }
.os-badge-pending { background: #3D3B1B; color: #FFD54F; }
.os-step-dot { background: #3A3A3C; }
.os-step-dot.active { background: linear-gradient(135deg, #FF8A50, #FF6B35); }
.os-step-dot.current { background: #FF6B35; }
.os-step-label { color: rgba(245,245,247,0.30); }
.os-step-label.active { color: #F5F5F7; }
.os-step-line { background: #3A3A3C; }
.os-step-line.active { background: linear-gradient(90deg, #FF8A50, #FF6B35); }
.os-info { background: #2C2C2E; }
.os-info-row { color: #F5F5F7; }
.os-info-label { color: rgba(245,245,247,0.50); }
.os-order-id { color: rgba(245,245,247,0.30); }
}
// skills/order-skill/components/restaurant-list-card/index.js
Component({
data: {
items: [],
keyword: ''
},
lifetimes: {
created() {
console.info('[ai-mode] restaurant-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] restaurant-list-card 收到 Result, items=', (sc.items || []).length)
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] restaurant-list-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] restaurant-list-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] restaurant-list-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] restaurant-list-card overflow monitor=on')
}
},
methods: {
onTapMenu(e) {
const { restaurantId, restaurantName } = e.currentTarget.dataset
console.info(`[ai-mode] restaurant-list-card send api/call name=getMenuItems args=${JSON.stringify({ restaurantId })}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: `查看${restaurantName}菜单` },
{ type: 'api/call', data: { name: 'getMenuItems', arguments: { restaurantId } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="rl-card">
<view class="rl-title">附近餐厅</view>
<view wx:if="{{!items.length}}" class="rl-empty">
<view class="rl-empty-title">{{keyword ? '未找到相关餐厅' : '暂无可用餐厅'}}</view>
<view class="rl-empty-desc">{{keyword ? '请换一个关键词搜索' : '请稍后再试'}}</view>
</view>
<block wx:for="{{items}}" wx:key="restaurantId">
<view
class="rl-item"
hover-class="rl-item-active"
bind:tap="onTapMenu"
data-restaurant-id="{{item.restaurantId}}"
data-restaurant-name="{{item.name}}"
>
<view class="rl-item-head">
<view class="rl-name">{{item.name}}</view>
<view class="rl-tags">
<view wx:for="{{item.tags.slice(0, 2)}}" wx:key="*this" class="rl-tag">{{item}}</view>
</view>
</view>
<view class="rl-meta-row">
<text class="rl-rating">★ {{item.rating}}</text>
<text class="rl-sales">月售 {{item.monthlySales}}</text>
<text class="rl-distance">{{item.distance}}</text>
</view>
<view class="rl-info-row">
<text class="rl-fee">配送费 ¥{{item.deliveryFee}}</text>
<text class="rl-time">约 {{item.estimatedMinutes}} 分钟</text>
<text wx:if="{{item.minOrder}}" class="rl-fee">满 ¥{{item.minOrder}} 起送</text>
</view>
<view class="rl-btn" hover-class="rl-btn-hover">查看菜单</view>
</view>
</block>
</view>
/* ratio=1:1;美团/饿了么暖橙风格
强调色 #FF6B35 | 浅橙底 #FFF5F0
按钮渐变 linear-gradient(135deg, #FF8A50, #FF6B35)
大圆角 16px→3.2vw / 12px→2.4vw
柔和阴影 0 2px 8px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06)
字体:标题 17px→4.53vw 600 / 正文 15px→4vw / 注释 13px→3.47vw
文字色阶:主文 rgba(0,0,0,0.85) / 次要 rgba(0,0,0,0.50) / 辅助 rgba(0,0,0,0.30)
卡片内边距 16px→3.2vw,元素间距 12px→2.4vw */
.rl-card {
background: #FFFFFF;
border-radius: 3.2vw;
padding: 3.2vw;
box-sizing: border-box;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06);
}
.rl-title {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.85);
margin-bottom: 2.4vw;
}
.rl-empty {
padding: 6.4vw 3.2vw;
text-align: center;
background: #FFF5F0;
border-radius: 2.4vw;
}
.rl-empty-title {
font-size: 4vw;
color: rgba(0,0,0,0.85);
margin-bottom: 1.33vw;
}
.rl-empty-desc {
font-size: 3.47vw;
color: rgba(0,0,0,0.50);
}
.rl-item {
margin-top: 2.4vw;
padding: 3.2vw;
background: #FFF5F0;
border-radius: 2.4vw;
cursor: pointer;
}
.rl-item-active {
opacity: 0.85;
}
.rl-item-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.6vw;
}
.rl-name {
flex: 1;
min-width: 0;
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.85);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rl-tags {
display: flex;
gap: 1.33vw;
flex-shrink: 0;
}
.rl-tag {
padding: 0.53vw 1.6vw;
background: #FFE4D6;
border-radius: 999rpx;
font-size: 3.2vw;
color: #FF6B35;
}
.rl-meta-row {
display: flex;
align-items: center;
gap: 2.4vw;
margin-bottom: 1.6vw;
font-size: 3.47vw;
color: rgba(0,0,0,0.50);
}
.rl-rating {
color: #FF6B35;
font-weight: 600;
}
.rl-sales {
color: rgba(0,0,0,0.50);
}
.rl-distance {
color: rgba(0,0,0,0.50);
}
.rl-info-row {
display: flex;
align-items: center;
gap: 2.4vw;
font-size: 3.47vw;
color: rgba(0,0,0,0.50);
}
.rl-fee {
color: rgba(0,0,0,0.50);
}
.rl-time {
color: rgba(0,0,0,0.50);
}
.rl-btn {
margin-top: 2.4vw;
height: 10.67vw;
line-height: 10.67vw;
text-align: center;
font-size: 3.73vw;
font-weight: 500;
color: #FFFFFF;
background: linear-gradient(135deg, #FF8A50, #FF6B35);
border-radius: 2.4vw;
}
.rl-btn-hover {
opacity: 0.88;
}
@media (prefers-color-scheme: dark) {
.rl-card {
background: #1C1C1E;
box-shadow: none;
}
.rl-title { color: #F5F5F7; }
.rl-empty { background: #2C2C2E; }
.rl-empty-title { color: #F5F5F7; }
.rl-empty-desc { color: rgba(245,245,247,0.50); }
.rl-item { background: #2C2C2E; }
.rl-name { color: #F5F5F7; }
.rl-tag { background: #3A3A3C; color: #FF9F6E; }
.rl-meta-row, .rl-rating, .rl-sales, .rl-distance, .rl-info-row, .rl-fee, .rl-time { color: rgba(245,245,247,0.50); }
.rl-btn { background: linear-gradient(135deg, #FF8A50, #FF6B35); }
}
// skills/order-skill/data/seed.js — mock 种子数据
const restaurants = [
{
restaurantId: 'R001',
name: '麦香基·望京店',
rating: 4.6,
monthlySales: 3280,
distance: '680m',
deliveryFee: 3,
estimatedMinutes: 25,
tags: ['汉堡', '炸鸡', '快餐'],
keywords: ['望京', '快餐', '汉堡'],
minOrder: 20,
status: 'open',
menu: [
{ itemId: 'M001', name: '香辣鸡腿堡套餐', price: 32.9, image: 'https://picsum.photos/seed/m001/400/300', description: '香辣鸡腿堡+薯条+可乐', monthlySales: 1850 },
{ itemId: 'M002', name: '经典牛肉堡', price: 26.9, image: 'https://picsum.photos/seed/m002/400/300', description: '100%澳洲牛肉饼配新鲜蔬菜', monthlySales: 1220 },
{ itemId: 'M003', name: '脆皮炸鸡桶(6块)', price: 39.9, image: 'https://picsum.photos/seed/m003/400/300', description: '外酥里嫩,秘制腌料', monthlySales: 980 },
{ itemId: 'M004', name: '黄金薯条(大)', price: 12.9, image: 'https://picsum.photos/seed/m004/400/300', description: '现炸酥脆,撒上海盐', monthlySales: 2100 },
{ itemId: 'M005', name: '冰镇可乐(大)', price: 8.0, image: 'https://picsum.photos/seed/m005/400/300', description: '畅爽怡神', monthlySales: 2600 }
]
},
{
restaurantId: 'R002',
name: '川味轩·国贸店',
rating: 4.8,
monthlySales: 2150,
distance: '1.2km',
deliveryFee: 4,
estimatedMinutes: 30,
tags: ['川菜', '麻辣', '中餐'],
keywords: ['国贸', '川菜', '麻辣', '中餐'],
minOrder: 25,
status: 'open',
menu: [
{ itemId: 'M101', name: '水煮鱼', price: 58.0, image: 'https://picsum.photos/seed/m101/400/300', description: '活鱼现杀,麻辣鲜香', monthlySales: 760 },
{ itemId: 'M102', name: '麻婆豆腐', price: 22.0, image: 'https://picsum.photos/seed/m102/400/300', description: '正宗川味,入口即化', monthlySales: 1430 },
{ itemId: 'M103', name: '宫保鸡丁', price: 36.0, image: 'https://picsum.photos/seed/m103/400/300', description: '花生与鸡丁的经典搭配', monthlySales: 1150 },
{ itemId: 'M104', name: '酸辣土豆丝', price: 16.0, image: 'https://picsum.photos/seed/m104/400/300', description: '家常酸辣味', monthlySales: 980 },
{ itemId: 'M105', name: '米饭', price: 3.0, image: 'https://picsum.photos/seed/m105/400/300', description: '东北大米', monthlySales: 3000 }
]
},
{
restaurantId: 'R003',
name: '寿司之魂·三里屯店',
rating: 4.7,
monthlySales: 1680,
distance: '2.0km',
deliveryFee: 5,
estimatedMinutes: 35,
tags: ['日料', '寿司', '刺身'],
keywords: ['三里屯', '日料', '寿司'],
minOrder: 30,
status: 'open',
menu: [
{ itemId: 'M201', name: '三文鱼刺身(8片)', price: 68.0, image: 'https://picsum.photos/seed/m201/400/300', description: '挪威进口三文鱼,当日空运', monthlySales: 620 },
{ itemId: 'M202', name: '鳗鱼手握(2个)', price: 28.0, image: 'https://picsum.photos/seed/m202/400/300', description: '蒲烧鳗鱼配醋饭', monthlySales: 880 },
{ itemId: 'M203', name: '加州卷(8个)', price: 42.0, image: 'https://picsum.photos/seed/m203/400/300', description: '蟹棒+牛油果+黄瓜', monthlySales: 750 },
{ itemId: 'M204', name: '味噌汤', price: 12.0, image: 'https://picsum.photos/seed/m204/400/300', description: '日式传统味噌', monthlySales: 1100 }
]
},
{
restaurantId: 'R004',
name: '兰州拉面·中关村店',
rating: 4.5,
monthlySales: 4520,
distance: '350m',
deliveryFee: 2,
estimatedMinutes: 20,
tags: ['面食', '西北', '中餐'],
keywords: ['中关村', '拉面', '面食'],
minOrder: 15,
status: 'open',
menu: [
{ itemId: 'M301', name: '招牌牛肉拉面', price: 22.0, image: 'https://picsum.photos/seed/m301/400/300', description: '手工拉面,牛骨浓汤', monthlySales: 3200 },
{ itemId: 'M302', name: '牛肉板面', price: 24.0, image: 'https://picsum.photos/seed/m302/400/300', description: '宽面配卤牛肉', monthlySales: 1450 },
{ itemId: 'M303', name: '凉皮', price: 12.0, image: 'https://picsum.photos/seed/m303/400/300', description: '陕西风味凉皮', monthlySales: 980 },
{ itemId: 'M304', name: '肉夹馍', price: 10.0, image: 'https://picsum.photos/seed/m304/400/300', description: '腊汁肉夹白吉馍', monthlySales: 2100 },
{ itemId: 'M305', name: '酸梅汤', price: 6.0, image: 'https://picsum.photos/seed/m305/400/300', description: '冰镇解暑', monthlySales: 1800 },
{ itemId: 'M306', name: '卤蛋', price: 2.0, image: 'https://picsum.photos/seed/m306/400/300', description: '五香卤蛋', monthlySales: 2500 }
]
},
{
restaurantId: 'R005',
name: '必胜乐·五道口店',
rating: 4.4,
monthlySales: 2860,
distance: '800m',
deliveryFee: 3,
estimatedMinutes: 28,
tags: ['披萨', '西餐', '意面'],
keywords: ['五道口', '披萨', '西餐'],
minOrder: 28,
status: 'open',
menu: [
{ itemId: 'M401', name: '超级至尊披萨(9寸)', price: 59.0, image: 'https://picsum.photos/seed/m401/400/300', description: '培根+香肠+青椒+蘑菇', monthlySales: 920 },
{ itemId: 'M402', name: '奶油蘑菇意面', price: 36.0, image: 'https://picsum.photos/seed/m402/400/300', description: '浓郁奶油酱汁', monthlySales: 680 },
{ itemId: 'M403', name: 'BBQ烤鸡翅(6只)', price: 28.0, image: 'https://picsum.photos/seed/m403/400/300', description: '秘制BBQ酱烤制', monthlySales: 1100 },
{ itemId: 'M404', name: '凯撒沙拉', price: 22.0, image: 'https://picsum.photos/seed/m404/400/300', description: '新鲜罗马生菜配凯撒酱', monthlySales: 450 },
{ itemId: 'M405', name: '柠檬红茶', price: 10.0, image: 'https://picsum.photos/seed/m405/400/300', description: '冰爽柠檬红茶', monthlySales: 1500 }
]
},
{
restaurantId: 'R006',
name: '粥公粥婆·西二旗店',
rating: 4.3,
monthlySales: 1980,
distance: '1.5km',
deliveryFee: 2,
estimatedMinutes: 22,
tags: ['粥', '早餐', '养生'],
keywords: ['西二旗', '粥', '早餐'],
minOrder: 10,
status: 'open',
menu: [
{ itemId: 'M501', name: '皮蛋瘦肉粥', price: 15.0, image: 'https://picsum.photos/seed/m501/400/300', description: '慢火熬制,绵密鲜香', monthlySales: 1600 },
{ itemId: 'M502', name: '鲜虾粥', price: 28.0, image: 'https://picsum.photos/seed/m502/400/300', description: '鲜活大虾现煮', monthlySales: 780 },
{ itemId: 'M503', name: '小笼包(8只)', price: 18.0, image: 'https://picsum.photos/seed/m503/400/300', description: '鲜肉小笼,汤汁饱满', monthlySales: 1200 },
{ itemId: 'M504', name: '油条', price: 3.0, image: 'https://picsum.photos/seed/m504/400/300', description: '现炸酥脆', monthlySales: 2200 },
{ itemId: 'M505', name: '豆浆', price: 5.0, image: 'https://picsum.photos/seed/m505/400/300', description: '现磨浓豆浆', monthlySales: 1800 }
]
},
{
restaurantId: 'R007',
name: '沙县小吃·知春路店',
rating: 4.2,
monthlySales: 3650,
distance: '200m',
deliveryFee: 1,
estimatedMinutes: 15,
tags: ['小吃', '简餐', '中式'],
keywords: ['知春路', '小吃', '简餐'],
minOrder: 8,
status: 'open',
menu: [
{ itemId: 'M601', name: '蒸饺(10只)', price: 10.0, image: 'https://picsum.photos/seed/m601/400/300', description: '手工现包蒸饺', monthlySales: 2800 },
{ itemId: 'M602', name: '鸡腿饭', price: 16.0, image: 'https://picsum.photos/seed/m602/400/300', description: '卤鸡腿+时蔬+米饭', monthlySales: 2100 },
{ itemId: 'M603', name: '扁肉(小份)', price: 8.0, image: 'https://picsum.photos/seed/m603/400/300', description: '沙县特色扁肉', monthlySales: 1800 },
{ itemId: 'M604', name: '拌面', price: 6.0, image: 'https://picsum.photos/seed/m604/400/300', description: '花生酱拌面', monthlySales: 3200 },
{ itemId: 'M605', name: '炖罐(排骨)', price: 12.0, image: 'https://picsum.photos/seed/m605/400/300', description: '隔水炖排骨汤', monthlySales: 950 }
]
}
]
const orders = [
{
orderId: 'OD20250101001',
restaurantId: 'R001',
restaurantName: '麦香基·望京店',
items: [
{ itemId: 'M001', name: '香辣鸡腿堡套餐', price: 32.9, quantity: 1 },
{ itemId: 'M004', name: '黄金薯条(大)', price: 12.9, quantity: 1 }
],
totalAmount: 45.8,
deliveryFee: 3,
deliveryAddress: '北京市朝阳区望京SOHO T2 15层',
contactPhone: '138****1234',
status: 'delivering',
statusText: '配送中',
riderName: '张师傅',
riderPhone: '139****5678',
estimatedArrival: '约15分钟后送达',
orderTime: '2026-06-08 11:32',
deliveryNote: ''
},
{
orderId: 'OD20250101002',
restaurantId: 'R004',
restaurantName: '兰州拉面·中关村店',
items: [
{ itemId: 'M301', name: '招牌牛肉拉面', price: 22.0, quantity: 2 },
{ itemId: 'M304', name: '肉夹馍', price: 10.0, quantity: 1 }
],
totalAmount: 54.0,
deliveryFee: 2,
deliveryAddress: '北京市海淀区中关村大街1号',
contactPhone: '138****5678',
status: 'pending',
statusText: '商家接单中',
riderName: '',
riderPhone: '',
estimatedArrival: '约20分钟',
orderTime: '2026-06-08 12:05',
deliveryNote: '不要辣'
},
{
orderId: 'OD20250101003',
restaurantId: 'R007',
restaurantName: '沙县小吃·知春路店',
items: [
{ itemId: 'M601', name: '蒸饺(10只)', price: 10.0, quantity: 1 },
{ itemId: 'M603', name: '扁肉(小份)', price: 8.0, quantity: 1 },
{ itemId: 'M604', name: '拌面', price: 6.0, quantity: 1 }
],
totalAmount: 24.0,
deliveryFee: 1,
deliveryAddress: '北京市海淀区知春路113号',
contactPhone: '138****9012',
status: 'completed',
statusText: '已送达',
riderName: '李师傅',
riderPhone: '139****0000',
estimatedArrival: '已送达',
orderTime: '2026-06-08 08:15',
deliveryNote: ''
}
]
module.exports = { restaurants, orders }
{
"collections": [
{
"name": "orders",
"description": "外卖订单集合",
"fields": [
{ "name": "orderId", "type": "string", "description": "订单ID" },
{ "name": "restaurantId", "type": "string", "description": "餐厅ID" },
{ "name": "restaurantName", "type": "string", "description": "餐厅名称" },
{ "name": "items", "type": "array", "description": "订单商品列表" },
{ "name": "totalPrice", "type": "number", "description": "总价" },
{ "name": "status", "type": "string", "description": "订单状态" },
{ "name": "address", "type": "string", "description": "配送地址" },
{ "name": "openid", "type": "string", "description": "用户openid" },
{ "name": "createdAt", "type": "date", "description": "创建时间" }
],
"indexes": [
{ "field": "openid", "unique": false },
{ "field": "status", "unique": false }
]
}
]
}
// skills/order-skill/index.js — 外卖点餐 Skill 注册入口
const searchRestaurants = require('./apis/searchRestaurants.js')
const getMenuItems = require('./apis/getMenuItems.js')
const placeOrder = require('./apis/placeOrder.js')
const getOrderStatus = require('./apis/getOrderStatus.js')
function registerAPIs() {
const skill = wx.modelContext.createSkill('skills/order-skill')
skill.use(async (ctx, next) => {
try {
console.info('[ai-mode] [order-skill] middleware start name=', ctx.name)
await next()
console.info('[ai-mode] [order-skill] middleware finish name=', ctx.name)
} catch (err) {
console.error('[ai-mode] [order-skill] middleware error:', err.message)
throw err
}
})
skill.registerAPI('searchRestaurants', searchRestaurants)
skill.registerAPI('getMenuItems', getMenuItems)
skill.registerAPI('placeOrder', placeOrder)
skill.registerAPI('getOrderStatus', getOrderStatus)
console.info('[ai-mode] [order-skill] APIs registered via createSkill')
}
registerAPIs()
{
"apis": [
{
"name": "searchRestaurants",
"description": "搜索附近餐厅列表(业务对象:餐厅列表卡片)。调用前置条件:用户需要点餐、搜索餐厅、比较不同餐厅时。用户提供餐厅名、品类或区域关键词时优先按关键词搜索;用户未提供关键词时返回默认附近餐厅列表。【严禁场景】禁止在已有明确 restaurantId 且用户要查看单餐厅菜单时继续调用本接口,应改走 getMenuItems。",
"_meta": {
"ui": {
"componentPath": "components/restaurant-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": {
"restaurantId": { "type": "string", "description": "餐厅唯一 ID" },
"name": { "type": "string", "description": "餐厅名称" },
"rating": { "type": "number", "description": "评分" },
"monthlySales": { "type": "number", "description": "月销量" },
"distance": { "type": "string", "description": "距离文案" },
"deliveryFee": { "type": "number", "description": "配送费" },
"estimatedMinutes": { "type": "number", "description": "预计送达分钟数" },
"tags": { "type": "array", "items": { "type": "string" }, "description": "品类标签" },
"minOrder": { "type": "number", "description": "起送价" },
"status": { "type": "string", "description": "营业状态 open/closed" }
},
"required": ["restaurantId", "name", "rating", "monthlySales", "distance", "deliveryFee", "estimatedMinutes", "tags", "minOrder", "status"],
"additionalProperties": false
}
},
"total": { "type": "number", "description": "餐厅数量" },
"keyword": { "type": "string", "description": "实际使用的关键词,无关键词时为空字符串" }
},
"required": ["items", "total", "keyword"],
"additionalProperties": false
}
},
{
"name": "getMenuItems",
"description": "查看指定餐厅的菜单与菜品列表(业务对象:菜单列表卡片)。调用前置条件:已从餐厅列表中拿到具体 restaurantId,或上下文中已有明确餐厅。展示该餐厅的菜品名称、价格、描述、月销量。【严禁场景】禁止在没有有效 restaurantId 的情况下调用;禁止从用户自然语言编造 restaurantId。上下文中没有 restaurantId 时应先调用 searchRestaurants。",
"_meta": {
"ui": {
"componentPath": "components/menu-list-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"restaurantId": {
"type": "string",
"description": "餐厅唯一标识,必须来自上游 searchRestaurants 返回的 items[].restaurantId 原值。【禁止编造】禁止从用户自然语言推断或拼接。上下文中无 restaurantId 时,应先调 searchRestaurants。"
}
},
"required": ["restaurantId"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"restaurant": {
"type": "object",
"description": "餐厅基本信息",
"properties": {
"restaurantId": { "type": "string" },
"name": { "type": "string" },
"rating": { "type": "number" },
"monthlySales": { "type": "number" },
"deliveryFee": { "type": "number" },
"estimatedMinutes": { "type": "number" },
"minOrder": { "type": "number" },
"tags": { "type": "array", "items": { "type": "string" } },
"status": { "type": "string" }
},
"required": ["restaurantId", "name", "rating", "monthlySales", "deliveryFee", "estimatedMinutes", "minOrder", "tags", "status"],
"additionalProperties": false
},
"items": {
"type": "array",
"description": "菜品列表",
"items": {
"type": "object",
"properties": {
"itemId": { "type": "string", "description": "菜品唯一 ID" },
"name": { "type": "string", "description": "菜品名称" },
"price": { "type": "number", "description": "单价" },
"image": { "type": "string", "description": "图片 URL" },
"description": { "type": "string", "description": "菜品描述" },
"monthlySales": { "type": "number", "description": "月销量" }
},
"required": ["itemId", "name", "price", "image", "description", "monthlySales"],
"additionalProperties": false
}
},
"total": { "type": "number", "description": "菜品数量" }
},
"required": ["restaurant", "items", "total"],
"additionalProperties": false
}
},
{
"name": "placeOrder",
"description": "下单操作(业务对象:订单确认卡片)。调用前置条件:已通过 getMenuItems 选择菜品并确认购物车内容,且有配送地址和联系电话。系统自动计算总金额并提交订单。【严禁场景】禁止在无 restaurantId 或购物车为空时调用。缺少配送地址或联系电话时应引导用户补充。",
"_meta": {
"ui": {
"componentPath": "components/order-confirm-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"restaurantId": {
"type": "string",
"description": "餐厅唯一标识,必须来自上游 searchRestaurants 或 getMenuItems 返回的 restaurantId 原值。【禁止编造】"
},
"items": {
"type": "array",
"description": "购买的菜品列表",
"items": {
"type": "object",
"properties": {
"itemId": { "type": "string", "description": "菜品 ID" },
"name": { "type": "string", "description": "菜品名称" },
"price": { "type": "number", "description": "单价" },
"quantity": { "type": "number", "minimum": 1, "description": "数量" }
},
"required": ["itemId", "name", "price", "quantity"],
"additionalProperties": false
},
"minItems": 1
},
"deliveryAddress": {
"type": "string",
"description": "配送地址,用户提供或系统已有。缺少时需向用户确认。"
},
"contactPhone": {
"type": "string",
"description": "联系电话,用户提供或系统已有。缺少时需向用户确认。"
},
"deliveryNote": {
"type": "string",
"description": "配送备注,如口味要求。用户未提供时留空。"
}
},
"required": ["restaurantId", "items", "deliveryAddress", "contactPhone"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"order": {
"type": "object",
"description": "订单信息",
"properties": {
"orderId": { "type": "string", "description": "订单唯一 ID" },
"restaurantId": { "type": "string" },
"restaurantName": { "type": "string" },
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"itemId": { "type": "string" },
"name": { "type": "string" },
"price": { "type": "number" },
"quantity": { "type": "number" }
},
"required": ["itemId", "name", "price", "quantity"],
"additionalProperties": false
}
},
"totalAmount": { "type": "number", "description": "商品总金额" },
"deliveryFee": { "type": "number", "description": "配送费" },
"deliveryAddress": { "type": "string" },
"contactPhone": { "type": "string" },
"status": { "type": "string", "description": "订单状态 pending/delivering/completed" },
"statusText": { "type": "string", "description": "状态文案" },
"riderName": { "type": "string", "description": "骑手姓名" },
"riderPhone": { "type": "string", "description": "骑手电话" },
"estimatedArrival": { "type": "string", "description": "预计送达文案" },
"orderTime": { "type": "string", "description": "下单时间" },
"deliveryNote": { "type": "string", "description": "备注" }
},
"required": ["orderId", "restaurantId", "restaurantName", "items", "totalAmount", "deliveryFee", "deliveryAddress", "contactPhone", "status", "statusText", "estimatedArrival", "orderTime"],
"additionalProperties": false
}
},
"required": ["order"],
"additionalProperties": false
}
},
{
"name": "getOrderStatus",
"description": "查询订单当前配送状态(业务对象:配送状态卡片)。调用前置条件:上下文中已有有效 orderId。展示当前状态、进度条、骑手信息、预计送达时间。【严禁场景】禁止在没有 orderId 的情况下调用;禁止从用户自然语言编造 orderId。上下文中无 orderId 时,应先完成下单或让用户提供真实订单号。",
"_meta": {
"ui": {
"componentPath": "components/order-status-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"orderId": {
"type": "string",
"description": "订单唯一标识,必须来自上游 placeOrder 返回的 orderId 原值。【禁止编造】上下文中无 orderId 时禁止填写本字段,应先完成下单。"
}
},
"required": ["orderId"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"order": {
"type": "object",
"description": "订单配送状态信息",
"properties": {
"orderId": { "type": "string" },
"restaurantId": { "type": "string" },
"restaurantName": { "type": "string" },
"status": { "type": "string", "description": "pending/delivering/completed" },
"statusText": { "type": "string", "description": "状态文案" },
"riderName": { "type": "string" },
"riderPhone": { "type": "string" },
"estimatedArrival": { "type": "string" },
"deliveryAddress": { "type": "string" },
"orderTime": { "type": "string" }
},
"required": ["orderId", "restaurantId", "restaurantName", "status", "statusText", "estimatedArrival", "deliveryAddress", "orderTime"],
"additionalProperties": false
}
},
"required": ["order"],
"additionalProperties": false
}
}
],
"components": [
{
"path": "components/restaurant-list-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/menu-list-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/order-confirm-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/order-status-card/index",
"relatedPage": "/pages/home/home"
}
]
}
order-skill
外卖点餐,支持搜索餐厅、查看菜单、下单及查看配送状态。
功能
- 按关键词搜索附近餐厅
- 查看餐厅菜单与菜品详情
- 选择菜品并下单
- 实时查看订单配送状态与骑手信息
用户输入示例
- "附近有什么好吃的"
- "点个外卖"
- "我要点餐"
- "看看麦当劳有什么"
- "帮我下单"
- "外卖到哪了"
原子接口
| 接口名 | 说明 |
|---|---|
searchRestaurants | 搜索附近餐厅列表 |
getMenuItems | 查看指定餐厅的菜单与菜品列表 |
placeOrder | 提交订单(含菜品、地址、联系电话) |
getOrderStatus | 查询订单当前配送状态 |
原子组件
| 组件路径 | 说明 |
|---|---|
components/restaurant-list-card/index | 餐厅列表展示 |
components/menu-list-card/index | 菜单与菜品列表 |
components/order-confirm-card/index | 订单确认与下单 |
components/order-status-card/index | 配送状态与骑手信息 |
后端依赖
| 资源 | 名称 |
|---|---|
| 云函数 | order-skill-handler |
| 数据库集合 | orders |
// skills/order-skill/utils/util.js — 工具函数
const { restaurants, orders: seedOrders } = require('../data/seed')
const PREVIEW_MODE_KEY = 'mp_skills_preview_mode'
function isPreviewMode() {
return wx.getStorageSync(PREVIEW_MODE_KEY) !== false
}
// 运行时动态订单池,placeOrder 生成的订单会追加到这里
const _dynamicOrders = [...seedOrders]
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 defaultRestaurantList(keyword) {
const q = String(keyword || '').trim().toLowerCase()
const list = q
? restaurants.filter((r) => {
const hay = [r.name, ...(r.tags || []), ...(r.keywords || [])].join(' ').toLowerCase()
return hay.includes(q)
})
: restaurants
return list.map((r) => ({
restaurantId: r.restaurantId,
name: r.name,
rating: r.rating,
monthlySales: r.monthlySales,
distance: r.distance,
deliveryFee: r.deliveryFee,
estimatedMinutes: r.estimatedMinutes,
tags: r.tags,
minOrder: r.minOrder,
status: r.status
}))
}
function defaultRestaurantDetail(restaurantId) {
return restaurants.find((r) => r.restaurantId === restaurantId) || null
}
function defaultOrderList() {
return _dynamicOrders.map((o) => ({ ...o }))
}
function defaultOrderDetail(orderId) {
return _dynamicOrders.find((o) => o.orderId === orderId) || null
}
function addOrder(order) {
_dynamicOrders.push(order)
}
function genOrderId() {
const now = new Date()
const date = now.toISOString().slice(0, 10).replace(/-/g, '')
const rand = Math.random().toString(36).substring(2, 8).toUpperCase()
return `OD${date}${rand}`
}
module.exports = {
isPreviewMode,
errorResult,
successResult,
defaultRestaurantList,
defaultRestaurantDetail,
defaultOrderList,
defaultOrderDetail,
addOrder,
genOrderId
}
Related skills
Automation & Workflowsintegrations