
Queue Skill
- 1 installs
- 27 repo stars
- Updated June 18, 2026
- tencentcloudbase/awesome-miniprogram-skills
WeChat Mini Program skill for in-store queuing: searching stores, checking queue status, taking a number, and tracking queue progress.
About
Adds a store-queuing flow to a WeChat Mini Program for finding stores, viewing queue status, taking a ticket, and tracking progress. A developer uses it as a scenario template when building virtual-queue features.
- Handles store lookup and queue-status viewing
- Covers online ticket-taking and queue-progress tracking
Queue 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 queue-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 in-store queuing: searching stores, checking queue status, taking a number, and tracking queue progress.
Files
门店排队取号
基于门店列表完成门店查询、排队状态查看、线上取号与排队进度查询的能力集合。
触发场景
用户原话举例(路由命中本技能):
- "帮我找个能取号的门店"
- "望京附近哪家店排得快"
- "帮我看看这家店现在排到多少号了"
- "我要先取个号"
- "我前面还有几位"
- "帮我查一下我的排队进度"
不适用范围
- 饮品点单、规格选择、下单支付等诉求 → 不在本技能范围,由点单技能处理
- 地图导航、路线规划、电话联系门店等诉求 → 不在本技能范围
- 历史订单、会员积分、优惠券等诉求 → 不在本技能范围
接口链路
searchStores:门店搜索与候选门店列表展示。getStoreQueueStatus:查看单门店当前排队状态。takeQueueNumber:确认可取号后生成排队票。getQueueProgress:基于真实票据查询当前位置与等待进度。
使用顺序
- 取号前需先确定具体门店;没有门店上下文时,先展示可选门店列表,再查看该门店当前排队状态。
- 生成排队票前需先看到门店当前排队状态与取号状态,确认可取号后再执行取号。
- 查询排队进度前需先拿到有效票据;没有有效票据时,不能直接查询当前位置。
- 所有已绑定组件的接口都应优先展示卡片,不要改成纯文本逐条展开。
const {
isPreviewMode,
ensureCloudInit,
errorResult,
successResult
} = require('../utils/util')
async function getQueueProgress(params = {}) {
console.info('[ai-mode] getQueueProgress 入口, params=', JSON.stringify(params))
const ticketId = params && params.ticketId
if (!ticketId) {
return errorResult('缺少 ticketId。禁止直接查询排队进度,请先完成取号或使用真实票据。')
}
if (isPreviewMode()) {
return buildFallback(ticketId)
}
ensureCloudInit()
const { result } = await wx.cloud.callFunction({
name: 'queue-skill-handler',
data: {
action: 'getQueueProgress',
ticketId
}
})
if (result && result.code === 0 && result.data) {
console.info('[ai-mode] getQueueProgress 云函数返回成功, ticketId=', ticketId)
return buildSuccess(result.data)
}
return buildFallback(ticketId)
}
function buildFallback(ticketId) {
const ticket = wx.getStorageSync(`skills_queue_ticket_${ticketId}`)
if (!ticket) {
return errorResult('未找到有效排队票。禁止继续展示排队进度,请先让用户重新取号或提供真实票据。')
}
const progress = calcLocalProgress(ticket)
return buildSuccess(progress)
}
function calcLocalProgress(ticket) {
const elapsedMinutes = Math.floor((Date.now() - new Date(ticket.takeTime).getTime()) / 60000)
const aheadCount = Math.max(ticket.aheadCount - Math.floor(elapsedMinutes / 3), 0)
const currentNo = ticket.currentCallingNumber || 'A001'
const currentIndex = parseInt(String(currentNo).replace(/\D/g, ''), 10) || 1
const callingIndex = currentIndex + Math.floor((ticket.aheadCount - aheadCount) * 0.8)
const currentCallingNumber = `A${String(callingIndex).padStart(3, '0')}`
const remainingMinutes = Math.max(ticket.estimatedMinutes - elapsedMinutes, 0)
let status = 'waiting'
let statusText = '排队中'
if (aheadCount <= 0 && remainingMinutes <= 2) {
status = 'calling'
statusText = '即将到号'
}
if (elapsedMinutes >= ticket.estimatedMinutes + 8) {
status = 'completed'
statusText = '已过号'
}
return {
ticketId: ticket.ticketId,
queueNumber: ticket.queueNumber,
storeName: ticket.storeName,
status,
statusText,
aheadCount,
currentCallingNumber,
remainingMinutes,
isAlmostReady: aheadCount <= 2
}
}
function buildSuccess(data) {
return successResult(
`已查询到当前排队进度,状态为「${data.statusText}」。请展示排队进度卡片,并引导用户按需刷新。禁止以纯文本展开完整票据详情。`,
{
ticketId: data.ticketId,
queueNumber: data.queueNumber,
storeName: data.storeName,
status: data.status,
statusText: data.statusText,
aheadCount: data.aheadCount,
currentCallingNumber: data.currentCallingNumber,
remainingMinutes: data.remainingMinutes,
isAlmostReady: data.isAlmostReady
},
{
ticketId: data.ticketId
}
)
}
module.exports = getQueueProgress
const {
isPreviewMode,
ensureCloudInit,
errorResult,
successResult,
defaultStoreDetail
} = require('../utils/util')
async function getStoreQueueStatus(params = {}) {
console.info('[ai-mode] getStoreQueueStatus 入口, params=', JSON.stringify(params))
const storeId = params && params.storeId
if (!storeId) {
return errorResult('缺少 storeId。禁止直接查看门店排队状态,请先让用户从门店列表中选择具体门店。')
}
if (isPreviewMode()) {
return buildFallback(storeId)
}
ensureCloudInit()
const { result } = await wx.cloud.callFunction({
name: 'queue-skill-handler',
data: {
action: 'getStoreQueueStatus',
storeId
}
})
if (result && result.code === 0 && result.data) {
console.info('[ai-mode] getStoreQueueStatus 云函数返回成功, storeId=', storeId)
return buildSuccess(result.data)
}
return buildFallback(storeId)
}
function buildFallback(storeId) {
const store = defaultStoreDetail(storeId)
if (!store) {
return errorResult('未找到该门店的排队信息。禁止继续取号,请先重新选择门店。')
}
return buildSuccess({
storeId: store.storeId,
storeName: store.storeName,
address: store.address,
businessHours: store.businessHours,
queueEnabled: store.queueEnabled,
queueStatusText: store.queueEnabled ? '可取号' : '暂停取号',
currentCallingNumber: store.currentCallingNumber,
waitingCount: store.waitingCount,
estimatedMinutes: store.estimatedMinutes
})
}
function buildSuccess(data) {
const actionText = data.queueEnabled
? '请展示门店排队状态卡片,并引导用户确认是否立即取号。'
: '请展示门店排队状态卡片,并明确告知当前暂停取号,禁止继续发起取号。'
return successResult(
`已查询到门店「${data.storeName}」当前排队状态:${data.queueStatusText},当前叫号 ${data.currentCallingNumber}。${actionText}禁止以纯文本列出完整门店状态。`,
{
storeId: data.storeId,
storeName: data.storeName,
address: data.address,
businessHours: data.businessHours,
queueEnabled: data.queueEnabled,
queueStatusText: data.queueStatusText,
currentCallingNumber: data.currentCallingNumber,
waitingCount: data.waitingCount,
estimatedMinutes: data.estimatedMinutes
},
{
storeId: data.storeId
}
)
}
module.exports = getStoreQueueStatus
const {
isPreviewMode,
ensureCloudInit,
successResult,
defaultStoreList
} = require('../utils/util')
async function searchStores(params = {}) {
console.info('[ai-mode] searchStores 入口, params=', JSON.stringify(params))
const keyword = String((params && params.keyword) || '').trim()
if (isPreviewMode()) {
return buildResult(defaultStoreList(keyword), keyword)
}
ensureCloudInit()
const { result } = await wx.cloud.callFunction({
name: 'queue-skill-handler',
data: {
action: 'searchStores',
keyword
}
})
const items = (result && result.code === 0 && result.data && result.data.items) || []
console.info('[ai-mode] searchStores 云函数返回数量=', items.length)
return buildResult(items, keyword)
}
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 = searchStores
const {
isPreviewMode,
ensureCloudInit,
errorResult,
successResult,
defaultStoreDetail,
genTicketId
} = require('../utils/util')
async function takeQueueNumber(params = {}) {
console.info('[ai-mode] takeQueueNumber 入口, params=', JSON.stringify(params))
const storeId = params && params.storeId
if (!storeId) {
return errorResult('缺少 storeId。禁止直接取号,请先让用户选择具体门店并查看门店排队状态。')
}
if (isPreviewMode()) {
return buildFallback(storeId, params)
}
ensureCloudInit()
const { result } = await wx.cloud.callFunction({
name: 'queue-skill-handler',
data: {
action: 'takeQueueNumber',
storeId,
partySize: params.partySize,
queueType: params.queueType
}
})
if (result && result.code === 0 && result.data) {
saveLocalTicket(result.data)
console.info('[ai-mode] takeQueueNumber 云函数返回成功, ticketId=', result.data.ticketId)
return buildSuccess(result.data)
}
return buildFallback(storeId, params)
}
function buildFallback(storeId, params) {
const store = defaultStoreDetail(storeId)
if (!store) {
return errorResult('未找到可取号的门店信息。禁止继续生成排队票,请先重新选择门店。')
}
if (!store.queueEnabled) {
return errorResult(`门店「${store.storeName}」当前暂停取号。禁止向用户宣布已取号成功,请引导用户更换门店。`)
}
const queueNumber = `A${String(Math.floor(Math.random() * 90) + 10)}`
const ticket = {
ticketId: genTicketId(),
queueNumber,
storeId: store.storeId,
storeName: store.storeName,
aheadCount: store.waitingCount,
estimatedMinutes: store.estimatedMinutes,
takeTime: new Date().toISOString(),
status: 'waiting',
currentCallingNumber: store.currentCallingNumber,
partySize: params.partySize || 2,
queueType: params.queueType || 'dine_in'
}
saveLocalTicket(ticket)
return buildSuccess(ticket)
}
function buildSuccess(data) {
return successResult(
`已成功生成排队票,号牌为 ${data.queueNumber}。请展示取号结果卡片,并引导用户查看排队进度。禁止在未拿到 ticketId 前宣称取号成功。`,
{
ticketId: data.ticketId,
queueNumber: data.queueNumber,
storeId: data.storeId,
storeName: data.storeName,
aheadCount: data.aheadCount,
estimatedMinutes: data.estimatedMinutes,
takeTime: data.takeTime,
status: data.status
},
{
ticketId: data.ticketId,
storeId: data.storeId
}
)
}
function saveLocalTicket(ticket) {
wx.setStorageSync(`skills_queue_ticket_${ticket.ticketId}`, ticket)
wx.setStorageSync('skills_queue_current_ticket_id', ticket.ticketId)
}
module.exports = takeQueueNumber
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const stores = [
{
storeId: 'S001',
storeName: '望京SOHO店',
city: '北京',
district: '朝阳区',
address: '北京市朝阳区望京SOHO T1 B1',
businessType: 'dine_in',
businessHours: '10:00-22:00',
distance: '320m',
queueEnabled: true,
currentCallingNumber: 'A018',
waitingCount: 8,
estimatedMinutes: 18,
keywords: ['望京', 'soho', '朝阳']
},
{
storeId: 'S002',
storeName: '国贸店',
city: '北京',
district: '朝阳区',
address: '北京市朝阳区国贸商城 B1',
businessType: 'dine_in',
businessHours: '10:00-22:00',
distance: '1.5km',
queueEnabled: true,
currentCallingNumber: 'A026',
waitingCount: 4,
estimatedMinutes: 10,
keywords: ['国贸', 'cbd', '朝阳']
},
{
storeId: 'S003',
storeName: '三里屯店',
city: '北京',
district: '朝阳区',
address: '北京市朝阳区三里屯太古里南区',
businessType: 'dine_in',
businessHours: '11:00-23:00',
distance: '2.4km',
queueEnabled: false,
currentCallingNumber: 'A032',
waitingCount: 0,
estimatedMinutes: 0,
keywords: ['三里屯', '太古里', '朝阳']
}
]
function matchStores(keyword = '') {
const q = String(keyword || '').trim().toLowerCase()
const list = q
? stores.filter((item) =>
[item.storeName, item.city, item.district, ...(item.keywords || [])]
.join(' ')
.toLowerCase()
.includes(q)
)
: stores
return list.map((item) => ({
storeId: item.storeId,
storeName: item.storeName,
distance: item.distance,
waitingCount: item.waitingCount,
estimatedMinutes: item.estimatedMinutes,
queueEnabled: item.queueEnabled
}))
}
function findStore(storeId) {
return stores.find((item) => item.storeId === storeId) || null
}
async function handleSearchStores({ keyword }) {
return {
code: 0,
message: 'success',
data: {
items: matchStores(keyword),
keyword: String(keyword || '')
}
}
}
async function handleGetStoreQueueStatus({ storeId }) {
const store = findStore(storeId)
if (!store) {
return { code: -1, message: 'store_not_found', data: null }
}
return {
code: 0,
message: 'success',
data: {
storeId: store.storeId,
storeName: store.storeName,
address: store.address,
businessHours: store.businessHours,
queueEnabled: store.queueEnabled,
queueStatusText: store.queueEnabled ? '可取号' : '暂停取号',
currentCallingNumber: store.currentCallingNumber,
waitingCount: store.waitingCount,
estimatedMinutes: store.estimatedMinutes
}
}
}
async function handleTakeQueueNumber({ storeId, partySize, queueType }) {
const store = findStore(storeId)
if (!store) {
return { code: -1, message: 'store_not_found', data: null }
}
if (!store.queueEnabled) {
return { code: -1, message: 'queue_disabled', data: null }
}
const now = new Date()
const ticketId = `T${Date.now().toString(36).toUpperCase()}`
const queueNumber = `A${String(Math.floor(Math.random() * 90) + 10)}`
const data = {
ticketId,
queueNumber,
storeId: store.storeId,
storeName: store.storeName,
aheadCount: store.waitingCount,
estimatedMinutes: store.estimatedMinutes,
takeTime: now.toISOString(),
status: 'waiting',
currentCallingNumber: store.currentCallingNumber,
partySize: partySize || 2,
queueType: queueType || 'dine_in'
}
try {
await db.collection('queue_tickets').add({
data: {
...data,
createdAt: db.serverDate()
}
})
} catch (e) {
console.error('[queue-skill-handler] save queue_ticket failed:', e.message)
}
return { code: 0, message: 'success', data }
}
async function handleGetQueueProgress({ ticketId }) {
try {
const res = await db.collection('queue_tickets')
.where({ ticketId })
.limit(1)
.get()
if (!res.data || !res.data.length) {
return { code: -1, message: 'ticket_not_found', data: null }
}
const ticket = res.data[0]
const elapsedMinutes = Math.floor((Date.now() - new Date(ticket.takeTime).getTime()) / 60000)
const aheadCount = Math.max(ticket.aheadCount - Math.floor(elapsedMinutes / 3), 0)
const currentNo = ticket.currentCallingNumber || 'A001'
const currentIndex = parseInt(String(currentNo).replace(/\D/g, ''), 10) || 1
const callingIndex = currentIndex + Math.floor((ticket.aheadCount - aheadCount) * 0.8)
const currentCallingNumber = `A${String(callingIndex).padStart(3, '0')}`
const remainingMinutes = Math.max(ticket.estimatedMinutes - elapsedMinutes, 0)
let status = 'waiting'
let statusText = '排队中'
if (aheadCount <= 0 && remainingMinutes <= 2) {
status = 'calling'
statusText = '即将到号'
}
if (elapsedMinutes >= ticket.estimatedMinutes + 8) {
status = 'completed'
statusText = '已过号'
}
return {
code: 0,
message: 'success',
data: {
ticketId: ticket.ticketId,
queueNumber: ticket.queueNumber,
storeName: ticket.storeName,
status,
statusText,
aheadCount,
currentCallingNumber,
remainingMinutes,
isAlmostReady: aheadCount <= 2
}
}
} catch (err) {
console.error('[queue-skill-handler] getQueueProgress error:', err.message)
return { code: -1, message: err.message, data: null }
}
}
exports.main = async (event) => {
const { action } = event
console.log('[queue-skill-handler] action=', action, 'event=', JSON.stringify(event))
switch (action) {
case 'searchStores':
return handleSearchStores(event)
case 'getStoreQueueStatus':
return handleGetStoreQueueStatus(event)
case 'takeQueueNumber':
return handleTakeQueueNumber(event)
case 'getQueueProgress':
return handleGetQueueProgress(event)
default:
return {
code: -1,
message: `未知 action: ${action}`,
data: null
}
}
}
{
"name": "queue-skill-handler",
"version": "1.0.0",
"description": "queue-skill 云函数",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}
Component({
data: {
ticketId: '',
queueNumber: '--',
storeName: '',
status: 'waiting',
statusText: '排队中',
aheadCount: 0,
currentCallingNumber: '--',
remainingMinutes: 0,
isAlmostReady: false
},
lifetimes: {
created() {
console.info('[ai-mode] queue-progress-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] queue-progress-card 收到 Result:', JSON.stringify(sc))
this.setData({
ticketId: sc.ticketId || '',
queueNumber: sc.queueNumber || '--',
storeName: sc.storeName || '',
status: sc.status || 'waiting',
statusText: sc.statusText || '排队中',
aheadCount: sc.aheadCount || 0,
currentCallingNumber: sc.currentCallingNumber || '--',
remainingMinutes: sc.remainingMinutes || 0,
isAlmostReady: !!sc.isAlmostReady
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] queue-progress-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] queue-progress-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] queue-progress-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] queue-progress-card overflow monitor=on')
}
},
methods: {
onTapRefresh() {
if (!this.data.ticketId) return
const args = { ticketId: this.data.ticketId }
console.info(`[ai-mode] queue-progress-card send api/call name=getQueueProgress args=${JSON.stringify(args)}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '刷新进度' },
{ type: 'api/call', data: { name: 'getQueueProgress', arguments: args } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="qp-card">
<view class="qp-head">
<view>
<view class="qp-title">{{queueNumber}}</view>
<view class="qp-store">{{storeName}}</view>
</view>
<view class="qp-badge is-{{status}}">{{statusText}}</view>
</view>
<view class="qp-grid">
<view class="qp-cell">
<view class="qp-label">当前叫号</view>
<view class="qp-value">{{currentCallingNumber}}</view>
</view>
<view class="qp-cell">
<view class="qp-label">前方剩余</view>
<view class="qp-value">{{aheadCount}} 位</view>
</view>
<view class="qp-cell">
<view class="qp-label">预计等待</view>
<view class="qp-value">{{remainingMinutes}} 分钟</view>
</view>
</view>
<view class="qp-hint {{isAlmostReady ? 'is-ready' : ''}}">
{{isAlmostReady ? '快到号了,请留意现场叫号' : '还需等待,建议稍后刷新进度'}}
</view>
<view class="qp-btn" hover-class="qp-btn-hover" bind:tap="onTapRefresh">刷新进度</view>
</view>
/* ratio=4:3;现代暖橙中性色系 */
.qp-card {
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 1.6vw;
padding: 3.2vw;
box-sizing: border-box;
box-shadow: 0 10rpx 36rpx rgba(15, 23, 42, 0.06);
}
.qp-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.qp-title {
font-size: 8vw;
font-weight: 700;
color: #C2410C;
line-height: 1.1;
}
.qp-store {
margin-top: 1.33vw;
font-size: 3.2vw;
color: #64748B;
}
.qp-badge {
padding: 0.8vw 2.13vw;
border-radius: 999rpx;
font-size: 3.2vw;
}
.qp-badge.is-waiting { background: #FEF3C7; color: #B45309; }
.qp-badge.is-calling { background: #DCFCE7; color: #15803D; }
.qp-badge.is-passed,
.qp-badge.is-completed { background: #FEE2E2; color: #DC2626; }
.qp-grid {
margin-top: 3.2vw;
display: flex;
gap: 2.13vw;
}
.qp-cell {
flex: 1;
background: linear-gradient(180deg, #F8FAFC 0%, #EEF2FF 100%);
border: 1px solid #E2E8F0;
border-radius: 1.6vw;
padding: 2.67vw 2.13vw;
text-align: center;
}
.qp-label {
font-size: 3.2vw;
color: #64748B;
}
.qp-value {
margin-top: 1.33vw;
font-size: 4vw;
font-weight: 600;
color: #0F172A;
}
.qp-hint {
margin-top: 3.2vw;
padding: 2.67vw 2.13vw;
background: #F8FAFC;
border: 1px solid #E2E8F0;
border-radius: 1.6vw;
font-size: 3.2vw;
color: #64748B;
}
.qp-hint.is-ready {
background: #DCFCE7;
border-color: #BBF7D0;
color: #15803D;
}
.qp-btn {
margin-top: 3.2vw;
height: 10.67vw;
line-height: 10.67vw;
text-align: center;
border-radius: 1.6vw;
font-size: 3.73vw;
color: #FFFFFF;
background: linear-gradient(135deg, #EF4444 0%, #F97316 100%);
}
.qp-btn-hover { opacity: 0.88; }
@media (prefers-color-scheme: dark) {
.qp-card {
background: #0F172A;
border-color: #1E293B;
box-shadow: none;
}
.qp-title { color: #FDBA74; }
.qp-store, .qp-label, .qp-hint { color: #94A3B8; }
.qp-value { color: #F8FAFC; }
.qp-cell {
background: linear-gradient(180deg, #111827 0%, #1E293B 100%);
border-color: #334155;
}
.qp-hint { background: #111827; border-color: #334155; }
.qp-hint.is-ready { background: #123827; border-color: #14532D; color: #6EE7B7; }
.qp-btn { background: linear-gradient(135deg, #FB7185 0%, #FB923C 100%); }
.qp-badge.is-waiting { background: #4A3616; color: #F0C36A; }
.qp-badge.is-calling { background: #123827; color: #6EE7B7; }
.qp-badge.is-passed,
.qp-badge.is-completed { background: #3B1D24; color: #FDA4AF; }
}Component({
data: {
ticketId: '',
queueNumber: '--',
storeName: '',
aheadCount: 0,
estimatedMinutes: 0,
takeTime: '',
status: 'waiting'
},
lifetimes: {
created() {
console.info('[ai-mode] queue-ticket-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] queue-ticket-card 收到 Result:', JSON.stringify(sc))
this.setData({
ticketId: sc.ticketId || '',
queueNumber: sc.queueNumber || '--',
storeName: sc.storeName || '',
aheadCount: sc.aheadCount || 0,
estimatedMinutes: sc.estimatedMinutes || 0,
takeTime: sc.takeTime || '',
status: sc.status || 'waiting'
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] queue-ticket-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] queue-ticket-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] queue-ticket-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] queue-ticket-card overflow monitor=on')
}
},
methods: {
onTapViewProgress() {
if (!this.data.ticketId) return
const args = { ticketId: this.data.ticketId }
console.info(`[ai-mode] queue-ticket-card send api/call name=getQueueProgress args=${JSON.stringify(args)}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '查看进度' },
{ type: 'api/call', data: { name: 'getQueueProgress', arguments: args } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="qt-card">
<view class="qt-badge">取号成功</view>
<view class="qt-number">{{queueNumber}}</view>
<view class="qt-store">{{storeName}}</view>
<view class="qt-meta">取号时间 {{takeTime}}</view>
<view class="qt-grid">
<view class="qt-cell">
<view class="qt-label">前方人数</view>
<view class="qt-value">{{aheadCount}} 位</view>
</view>
<view class="qt-cell">
<view class="qt-label">预计等待</view>
<view class="qt-value">{{estimatedMinutes}} 分钟</view>
</view>
</view>
<view class="qt-btn" hover-class="qt-btn-hover" bind:tap="onTapViewProgress">查看进度</view>
</view>
/* ratio=4:3;现代暖橙中性色系 */
.qt-card {
background: linear-gradient(180deg, #FFF7ED 0%, #FFFFFF 100%);
border: 1px solid #FED7AA;
border-radius: 1.6vw;
padding: 3.2vw;
box-sizing: border-box;
box-shadow: 0 10rpx 36rpx rgba(249, 115, 22, 0.12);
}
.qt-badge {
display: inline-block;
padding: 0.8vw 2.13vw;
border-radius: 999rpx;
background: #DCFCE7;
color: #15803D;
font-size: 3.2vw;
}
.qt-number {
margin-top: 2.13vw;
font-size: 9.6vw;
font-weight: 700;
color: #C2410C;
line-height: 1.1;
}
.qt-store {
margin-top: 1.33vw;
font-size: 4.53vw;
font-weight: 600;
color: #0F172A;
}
.qt-meta {
margin-top: 1.33vw;
font-size: 3.2vw;
color: #64748B;
}
.qt-grid {
margin-top: 3.2vw;
display: flex;
gap: 2.13vw;
}
.qt-cell {
flex: 1;
background: rgba(255, 255, 255, 0.85);
border: 1px solid #E2E8F0;
border-radius: 1.6vw;
padding: 2.67vw 2.13vw;
text-align: center;
}
.qt-label {
font-size: 3.2vw;
color: #64748B;
}
.qt-value {
margin-top: 1.33vw;
font-size: 4vw;
font-weight: 600;
color: #0F172A;
}
.qt-btn {
margin-top: 3.2vw;
height: 10.67vw;
line-height: 10.67vw;
text-align: center;
border-radius: 1.6vw;
font-size: 3.73vw;
color: #FFFFFF;
background: linear-gradient(135deg, #EF4444 0%, #F97316 100%);
}
.qt-btn-hover { opacity: 0.88; }
@media (prefers-color-scheme: dark) {
.qt-card {
background: linear-gradient(180deg, #431407 0%, #0F172A 100%);
border-color: #7C2D12;
box-shadow: none;
}
.qt-number { color: #FDBA74; }
.qt-store, .qt-value { color: #F8FAFC; }
.qt-meta, .qt-label { color: #94A3B8; }
.qt-cell { background: rgba(15, 23, 42, 0.72); border-color: #334155; }
.qt-badge { background: #123827; color: #6EE7B7; }
.qt-btn { background: linear-gradient(135deg, #FB7185 0%, #FB923C 100%); }
}Component({
data: {
items: [],
keyword: ''
},
lifetimes: {
created() {
console.info('[ai-mode] store-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] store-list-card 收到 Result:', JSON.stringify(sc))
this.setData({
items: sc.items || [],
keyword: sc.keyword || ''
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] store-list-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] store-list-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] store-list-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] store-list-card overflow monitor=on')
}
},
methods: {
onTapStatus(e) {
const { storeId, storeName } = e.currentTarget.dataset
console.info(`[ai-mode] store-list-card send api/call name=getStoreQueueStatus args=${JSON.stringify({ storeId })}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: `查看${storeName}` },
{ type: 'api/call', data: { name: 'getStoreQueueStatus', arguments: { storeId } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="sl-card">
<view class="sl-title">可选门店</view>
<view wx:if="{{!items.length}}" class="sl-empty">
<view class="sl-empty-title">暂无匹配门店</view>
<view class="sl-empty-desc">{{keyword ? '请换一个门店关键词再试' : '请稍后再试'}} </view>
</view>
<block wx:for="{{items}}" wx:key="storeId">
<view class="sl-item">
<view class="sl-item-head">
<view class="sl-store-name">{{item.storeName}}</view>
<view class="sl-distance">{{item.distance}}</view>
</view>
<view class="sl-meta-row">
<view class="sl-meta">前方 {{item.waitingCount}} 位</view>
<view class="sl-meta">约 {{item.estimatedMinutes}} 分钟</view>
<view class="sl-badge {{item.queueEnabled ? 'is-on' : 'is-off'}}">{{item.queueEnabled ? '可取号' : '暂停'}}</view>
</view>
<view
class="sl-btn"
hover-class="sl-btn-hover"
bind:tap="onTapStatus"
data-store-id="{{item.storeId}}"
data-store-name="{{item.storeName}}"
>查看排队</view>
</view>
</block>
</view>
/* ratio=1:1;现代暖橙中性色系 */
.sl-card {
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 1.6vw;
padding: 3.2vw;
box-sizing: border-box;
overflow: hidden;
box-shadow: 0 10rpx 36rpx rgba(15, 23, 42, 0.06);
}
.sl-title {
font-size: 4.53vw;
font-weight: 600;
color: #0F172A;
}
.sl-empty {
margin-top: 3.2vw;
padding: 3.2vw;
background: #F8FAFC;
border: 1px solid #CBD5E1;
border-radius: 1.6vw;
}
.sl-empty-title {
font-size: 4vw;
color: #0F172A;
}
.sl-empty-desc {
margin-top: 1.33vw;
font-size: 3.2vw;
color: #64748B;
}
.sl-item {
margin-top: 3.2vw;
padding: 3.2vw;
background: linear-gradient(180deg, #F8FAFC 0%, #FFFFFF 100%);
border: 1px solid #E2E8F0;
border-radius: 1.6vw;
}
.sl-item-head,
.sl-meta-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.sl-store-name {
flex: 1;
min-width: 0;
font-size: 4vw;
font-weight: 600;
color: #0F172A;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sl-distance,
.sl-meta {
font-size: 3.2vw;
color: #64748B;
}
.sl-distance {
margin-left: 2.13vw;
}
.sl-meta-row {
margin-top: 2.13vw;
gap: 2.13vw;
}
.sl-badge {
padding: 0.8vw 2.13vw;
border-radius: 999rpx;
font-size: 3.2vw;
}
.sl-badge.is-on {
background: #DCFCE7;
color: #15803D;
}
.sl-badge.is-off {
background: #FEE2E2;
color: #DC2626;
}
.sl-btn {
margin-top: 3.2vw;
height: 10.67vw;
line-height: 10.67vw;
text-align: center;
font-size: 3.73vw;
color: #FFFFFF;
background: linear-gradient(135deg, #EF4444 0%, #F97316 100%);
border-radius: 1.6vw;
}
.sl-btn-hover {
opacity: 0.88;
}
@media (prefers-color-scheme: dark) {
.sl-card {
background: #0F172A;
border-color: #1E293B;
box-shadow: none;
}
.sl-title, .sl-store-name { color: #F8FAFC; }
.sl-empty, .sl-item {
background: #111827;
border-color: #334155;
}
.sl-empty-desc, .sl-distance, .sl-meta { color: #94A3B8; }
.sl-badge.is-on { background: #123827; color: #6EE7B7; }
.sl-badge.is-off { background: #3B1D24; color: #FDA4AF; }
.sl-btn { background: linear-gradient(135deg, #FB7185 0%, #FB923C 100%); }
}Component({
data: {
storeId: '',
storeName: '',
address: '',
businessHours: '',
queueEnabled: false,
queueStatusText: '',
currentCallingNumber: '--',
waitingCount: 0,
estimatedMinutes: 0
},
lifetimes: {
created() {
console.info('[ai-mode] store-queue-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] store-queue-status-card 收到 Result:', JSON.stringify(sc))
this.setData({
storeId: sc.storeId || '',
storeName: sc.storeName || '',
address: sc.address || '',
businessHours: sc.businessHours || '',
queueEnabled: !!sc.queueEnabled,
queueStatusText: sc.queueStatusText || '',
currentCallingNumber: sc.currentCallingNumber || '--',
waitingCount: sc.waitingCount || 0,
estimatedMinutes: sc.estimatedMinutes || 0
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] store-queue-status-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] store-queue-status-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] store-queue-status-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] store-queue-status-card overflow monitor=on')
}
},
methods: {
onTapTakeNumber() {
if (!this.data.queueEnabled || !this.data.storeId) return
const args = { storeId: this.data.storeId }
console.info(`[ai-mode] store-queue-status-card send api/call name=takeQueueNumber args=${JSON.stringify(args)}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '立即取号' },
{ type: 'api/call', data: { name: 'takeQueueNumber', arguments: args } }
]
})
},
onTapRefresh() {
if (!this.data.storeId) return
const args = { storeId: this.data.storeId }
console.info(`[ai-mode] store-queue-status-card send api/call name=getStoreQueueStatus args=${JSON.stringify(args)}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '刷新排队' },
{ type: 'api/call', data: { name: 'getStoreQueueStatus', arguments: args } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="sq-card">
<view class="sq-head">
<view class="sq-title">{{storeName}}</view>
<view class="sq-badge {{queueEnabled ? 'is-on' : 'is-off'}}">{{queueStatusText}}</view>
</view>
<view class="sq-desc">{{address}}</view>
<view class="sq-desc">营业时间 {{businessHours}}</view>
<view class="sq-grid">
<view class="sq-cell">
<view class="sq-label">当前叫号</view>
<view class="sq-value">{{currentCallingNumber}}</view>
</view>
<view class="sq-cell">
<view class="sq-label">前方人数</view>
<view class="sq-value">{{waitingCount}} 位</view>
</view>
<view class="sq-cell">
<view class="sq-label">预计等待</view>
<view class="sq-value">{{estimatedMinutes}} 分钟</view>
</view>
</view>
<view class="sq-actions">
<view class="sq-secondary" hover-class="sq-secondary-hover" bind:tap="onTapRefresh">刷新</view>
<view wx:if="{{queueEnabled}}" class="sq-primary" hover-class="sq-primary-hover" bind:tap="onTapTakeNumber">立即取号</view>
<view wx:else class="sq-disabled">当前暂停取号</view>
</view>
</view>
/* ratio=4:3;现代暖橙中性色系 */
.sq-card {
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 1.6vw;
padding: 3.2vw;
box-sizing: border-box;
box-shadow: 0 10rpx 36rpx rgba(15, 23, 42, 0.06);
}
.sq-head,
.sq-actions {
display: flex;
align-items: center;
justify-content: space-between;
}
.sq-title {
flex: 1;
min-width: 0;
font-size: 4.53vw;
font-weight: 600;
color: #0F172A;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sq-badge {
margin-left: 2.13vw;
padding: 0.8vw 2.13vw;
border-radius: 999rpx;
font-size: 3.2vw;
}
.sq-badge.is-on { background: #DCFCE7; color: #15803D; }
.sq-badge.is-off { background: #FEE2E2; color: #DC2626; }
.sq-desc {
margin-top: 1.6vw;
font-size: 3.2vw;
color: #64748B;
line-height: 1.4;
}
.sq-grid {
margin-top: 3.2vw;
display: flex;
gap: 2.13vw;
}
.sq-cell {
flex: 1;
background: linear-gradient(180deg, #F8FAFC 0%, #EEF2FF 100%);
border: 1px solid #E2E8F0;
border-radius: 1.6vw;
padding: 2.67vw 2.13vw;
text-align: center;
}
.sq-label {
font-size: 3.2vw;
color: #64748B;
}
.sq-value {
margin-top: 1.33vw;
font-size: 4vw;
font-weight: 600;
color: #0F172A;
}
.sq-actions {
margin-top: 3.2vw;
gap: 2.13vw;
}
.sq-primary,
.sq-secondary,
.sq-disabled {
flex: 1;
height: 10.67vw;
line-height: 10.67vw;
text-align: center;
border-radius: 1.6vw;
font-size: 3.73vw;
}
.sq-primary {
color: #FFFFFF;
background: linear-gradient(135deg, #EF4444 0%, #F97316 100%);
}
.sq-secondary {
color: #C2410C;
background: #FFF7ED;
}
.sq-disabled {
color: #94A3B8;
background: #F1F5F9;
}
.sq-primary-hover,
.sq-secondary-hover { opacity: 0.88; }
@media (prefers-color-scheme: dark) {
.sq-card {
background: #0F172A;
border-color: #1E293B;
box-shadow: none;
}
.sq-title, .sq-value { color: #F8FAFC; }
.sq-desc, .sq-label { color: #94A3B8; }
.sq-cell {
background: linear-gradient(180deg, #111827 0%, #1E293B 100%);
border-color: #334155;
}
.sq-badge.is-on { background: #123827; color: #6EE7B7; }
.sq-badge.is-off { background: #3B1D24; color: #FDA4AF; }
.sq-primary { background: linear-gradient(135deg, #FB7185 0%, #FB923C 100%); }
.sq-secondary { color: #FDBA74; background: #431407; }
.sq-disabled { color: #64748B; background: #111827; }
}const stores = [
{
storeId: 'S001',
storeName: '望京SOHO店',
city: '北京',
district: '朝阳区',
address: '北京市朝阳区望京SOHO T1 B1',
businessType: 'dine_in',
businessHours: '10:00-22:00',
distance: '320m',
queueEnabled: true,
currentCallingNumber: 'A018',
waitingCount: 8,
estimatedMinutes: 18,
keywords: ['望京', 'soho', '朝阳']
},
{
storeId: 'S002',
storeName: '国贸店',
city: '北京',
district: '朝阳区',
address: '北京市朝阳区国贸商城 B1',
businessType: 'dine_in',
businessHours: '10:00-22:00',
distance: '1.5km',
queueEnabled: true,
currentCallingNumber: 'A026',
waitingCount: 4,
estimatedMinutes: 10,
keywords: ['国贸', 'cbd', '朝阳']
},
{
storeId: 'S003',
storeName: '三里屯店',
city: '北京',
district: '朝阳区',
address: '北京市朝阳区三里屯太古里南区',
businessType: 'dine_in',
businessHours: '11:00-23:00',
distance: '2.4km',
queueEnabled: false,
currentCallingNumber: 'A032',
waitingCount: 0,
estimatedMinutes: 0,
keywords: ['三里屯', '太古里', '朝阳']
}
]
module.exports = {
stores
}
{
"collections": [
{
"name": "queue_tickets",
"description": "排队票据集合,存储用户取号记录",
"indexes": [
{ "name": "idx_ticketId", "field": "ticketId" },
{ "name": "idx_storeId", "field": "storeId" }
]
}
]
}
// skills/queue-skill/index.js
const searchStores = require('./apis/searchStores.js')
const getStoreQueueStatus = require('./apis/getStoreQueueStatus.js')
const takeQueueNumber = require('./apis/takeQueueNumber.js')
const getQueueProgress = require('./apis/getQueueProgress.js')
function registerAPIs() {
const skill = wx.modelContext.createSkill('skills/queue-skill')
skill.use(async (ctx, next) => {
try {
console.info('[ai-mode] [queue-skill] middleware start name=', ctx.name)
await next()
console.info('[ai-mode] [queue-skill] middleware finish name=', ctx.name)
} catch (err) {
console.error('[ai-mode] [queue-skill] middleware error:', err.message)
throw err
}
})
skill.registerAPI('searchStores', searchStores)
skill.registerAPI('getStoreQueueStatus', getStoreQueueStatus)
skill.registerAPI('takeQueueNumber', takeQueueNumber)
skill.registerAPI('getQueueProgress', getQueueProgress)
console.info('[ai-mode] [queue-skill] APIs registered via createSkill')
}
registerAPIs()
{
"apis": [
{
"name": "searchStores",
"description": "查询可排队门店列表(业务对象:门店列表卡片)。调用前置条件:用户需要选择门店、搜索门店、比较不同门店排队快慢,或尚未提供明确门店上下文时。用户提供门店名、商圈或区域关键词时优先按关键词搜索;用户未提供关键词时返回默认可选门店列表。【严禁场景】禁止在已有明确 storeId 且用户要查看单门店排队状态时继续调用本接口,应改走 getStoreQueueStatus。",
"_meta": {
"ui": {
"componentPath": "components/store-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": {
"storeId": { "type": "string", "description": "门店唯一 ID" },
"storeName": { "type": "string", "description": "门店名称" },
"distance": { "type": "string", "description": "距离文案" },
"waitingCount": { "type": "number", "description": "当前前方排队人数" },
"estimatedMinutes": { "type": "number", "description": "预计等待分钟数" },
"queueEnabled": { "type": "boolean", "description": "是否可取号" }
},
"required": ["storeId", "storeName", "distance", "waitingCount", "estimatedMinutes", "queueEnabled"],
"additionalProperties": false
}
},
"total": { "type": "number", "description": "门店数量" },
"keyword": { "type": "string", "description": "实际使用的关键词,无关键词时为空字符串" }
},
"required": ["items", "total", "keyword"],
"additionalProperties": false
}
},
{
"name": "getStoreQueueStatus",
"description": "查看单个门店当前排队状态(业务对象:门店排队状态卡片)。调用前置条件:已从门店列表中拿到具体 storeId,或上下文中已有明确门店。展示当前叫号、前方人数、预计等待、营业时间和是否可取号。【严禁场景】禁止在没有有效 storeId 的情况下调用;禁止从用户自然语言编造 storeId。上下文中没有 storeId 时应先调用 searchStores。",
"_meta": {
"ui": {
"componentPath": "components/store-queue-status-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"storeId": {
"type": "string",
"description": "门店唯一标识,必须来自上游 searchStores 返回的 items[].storeId 原值。【禁止编造】禁止从用户自然语言推断或拼接。上下文中无 storeId 时,应先调 searchStores。"
}
},
"required": ["storeId"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"storeId": { "type": "string" },
"storeName": { "type": "string" },
"address": { "type": "string" },
"businessHours": { "type": "string" },
"queueEnabled": { "type": "boolean" },
"queueStatusText": { "type": "string", "description": "取号状态文案,如『可取号』『暂停取号』" },
"currentCallingNumber": { "type": "string", "description": "当前叫号" },
"waitingCount": { "type": "number", "description": "当前前方排队人数" },
"estimatedMinutes": { "type": "number", "description": "预计等待分钟数" }
},
"required": ["storeId", "storeName", "address", "businessHours", "queueEnabled", "queueStatusText", "currentCallingNumber", "waitingCount", "estimatedMinutes"],
"additionalProperties": false
}
},
{
"name": "takeQueueNumber",
"description": "为指定门店生成排队票(业务对象:取号结果卡片)。调用前置条件:已通过 getStoreQueueStatus 确认门店允许取号,并且已有上游返回的 storeId。成功后生成 ticketId 与排队号,并返回当前前方人数和预计等待时间。【严禁场景】禁止在无 storeId 时调用;禁止在门店暂停取号时向用户承诺已取号成功。",
"_meta": {
"ui": {
"componentPath": "components/queue-ticket-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"storeId": {
"type": "string",
"description": "门店唯一标识,必须来自上游 searchStores 或 getStoreQueueStatus 返回的 storeId 原值。【禁止编造】上下文中无 storeId 时应先完成门店选择。"
},
"partySize": {
"type": "number",
"minimum": 1,
"maximum": 12,
"description": "就餐人数。用户明确提到人数时按原话填写;用户未提及时留空,由后端按默认 2 人处理。"
},
"queueType": {
"type": "string",
"enum": ["dine_in", "service"],
"description": "队列类型。仅允许:'dine_in'(堂食)/ 'service'(服务台)。用户未明确说明时留空,由后端走默认堂食。"
}
},
"required": ["storeId"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"ticketId": { "type": "string", "description": "排队票唯一 ID" },
"queueNumber": { "type": "string", "description": "排队号牌" },
"storeId": { "type": "string" },
"storeName": { "type": "string" },
"aheadCount": { "type": "number", "description": "取号成功时前方人数" },
"estimatedMinutes": { "type": "number", "description": "预计等待分钟数" },
"takeTime": { "type": "string", "description": "取号时间 ISO 字符串" },
"status": { "type": "string", "description": "票据状态,首版固定为 waiting" }
},
"required": ["ticketId", "queueNumber", "storeId", "storeName", "aheadCount", "estimatedMinutes", "takeTime", "status"],
"additionalProperties": false
}
},
{
"name": "getQueueProgress",
"description": "查询排队票当前进度(业务对象:排队进度卡片)。调用前置条件:上下文中已有有效 ticketId。展示当前状态、当前叫号、前方剩余人数、预计剩余等待时间和是否即将到号。【严禁场景】禁止在没有 ticketId 的情况下调用;禁止从用户自然语言编造 ticketId。上下文中无 ticketId 时,应先完成取号或让用户提供真实票据。",
"_meta": {
"ui": {
"componentPath": "components/queue-progress-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"ticketId": {
"type": "string",
"description": "排队票唯一标识,必须来自上游 takeQueueNumber 返回的 ticketId 原值。【禁止编造】上下文中无 ticketId 时禁止填写本字段,应先完成取号。"
}
},
"required": ["ticketId"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"ticketId": { "type": "string" },
"queueNumber": { "type": "string" },
"storeName": { "type": "string" },
"status": { "type": "string", "description": "waiting / calling / passed / completed" },
"statusText": { "type": "string", "description": "状态文案" },
"aheadCount": { "type": "number", "description": "前方剩余人数" },
"currentCallingNumber": { "type": "string", "description": "当前叫号" },
"remainingMinutes": { "type": "number", "description": "预计剩余等待分钟数" },
"isAlmostReady": { "type": "boolean", "description": "是否即将到号" }
},
"required": ["ticketId", "queueNumber", "storeName", "status", "statusText", "aheadCount", "currentCallingNumber", "remainingMinutes", "isAlmostReady"],
"additionalProperties": false
}
}
],
"components": [
{
"path": "components/store-list-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/store-queue-status-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/queue-ticket-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/queue-progress-card/index",
"relatedPage": "/pages/home/home"
}
]
}
queue-skill
门店排队取号,支持搜索门店、查看排队状态、取号及查看排队进度。
功能
- 搜索可排队门店列表
- 查看单个门店当前排队状态
- 为指定门店取号排队
- 查询排队票当前进度
用户输入示例
- "附近有什么店"
- "查看排队状态"
- "取个号"
- "前面还有几位"
- "排队到哪了"
原子接口
| 接口名 | 说明 |
|---|---|
searchStores | 查询可排队门店列表 |
getStoreQueueStatus | 查看单个门店当前排队状态 |
takeQueueNumber | 为指定门店生成排队票 |
getQueueProgress | 查询排队票当前进度 |
原子组件
| 组件路径 | 说明 |
|---|---|
components/store-list-card/index | 门店列表展示 |
components/store-queue-status-card/index | 门店排队状态展示 |
components/queue-ticket-card/index | 取号结果展示 |
components/queue-progress-card/index | 排队进度展示 |
后端依赖
| 资源 | 名称 |
|---|---|
| 云函数 | queue-skill-handler |
| 数据库集合 | queue_tickets |
const { stores } = require('../data/seed')
const PREVIEW_MODE_KEY = 'mp_skills_preview_mode'
function isPreviewMode() {
return wx.getStorageSync(PREVIEW_MODE_KEY) !== false
}
const CLOUD_ENV_ID = 'cloud1-5g39elugeec5ba0f'
let _cloudInited = false
function ensureCloudInit() {
if (_cloudInited) return
if (!wx.cloud) throw new Error('当前环境不支持 wx.cloud')
wx.cloud.init({ env: CLOUD_ENV_ID, traceUser: true })
_cloudInited = true
}
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 defaultStoreList(keyword = '') {
const q = String(keyword || '').trim().toLowerCase()
const list = q
? stores.filter((item) => {
const hay = [item.storeName, item.city, item.district, ...(item.keywords || [])]
.join(' ')
.toLowerCase()
return hay.includes(q)
})
: stores
return list.map((item) => ({
storeId: item.storeId,
storeName: item.storeName,
distance: item.distance,
waitingCount: item.waitingCount,
estimatedMinutes: item.estimatedMinutes,
queueEnabled: item.queueEnabled
}))
}
function defaultStoreDetail(storeId) {
return stores.find((item) => item.storeId === storeId) || null
}
function genTicketId() {
return `T${Date.now().toString(36).toUpperCase()}`
}
module.exports = {
PREVIEW_MODE_KEY,
isPreviewMode,
CLOUD_ENV_ID,
ensureCloudInit,
errorResult,
successResult,
defaultStoreList,
defaultStoreDetail,
genTicketId
}
Related skills
Automation & Workflowsintegrations