
Hospital Skill
- 1 installs
- 27 repo stars
- Updated June 18, 2026
- tencentcloudbase/awesome-miniprogram-skills
WeChat Mini Program skill for hospital appointment booking: searching hospitals and departments, selecting time slots, booking, and viewing records.
About
Adds a hospital-registration flow to a WeChat Mini Program for searching hospitals and departments, picking slots, booking, and viewing records. A developer uses it as a scenario template when building medical appointment features.
- Handles hospital and department search plus time-slot selection
- Covers appointment booking and registration-record lookup
Hospital 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 hospital-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 hospital appointment booking: searching hospitals and departments, selecting time slots, booking, and viewing records.
Files
医院挂号
基于医院列表完成医院搜索、科室与时段选择、预约挂号与挂号记录查询的能力集合。
触发场景
用户原话举例(路由命中本技能):
- "帮我挂个号"
- "附近有哪些医院"
- "我想挂呼吸内科的号"
- "北京大学第一医院有哪些科室"
- "帮我预约一下"
- "查看我的挂号记录"
- "我之前的挂号记录"
不适用范围
- 在线问诊、图文咨询、药品配送等 → 不在本技能范围,由问诊/药房技能处理
- 医疗费用报销、医保查询等 → 不在本技能范围
- 急诊急救、120 呼叫等紧急医疗诉求 → 不在本技能范围,请引导用户拨打 120
接口链路
searchHospitals:医院搜索与候选医院列表展示,支持关键词搜索。getAvailableSlots:查看指定科室的可挂号时段列表。bookAppointment:确认时段后执行预约挂号。getMyAppointments:查询当前用户的挂号记录列表。
使用顺序
- 挂号前需先确定医院和科室;没有医院上下文时,先展示可选医院列表,用户选择医院后再展示科室列表。
- 选择科室后,展示该科室可预约时段列表,用户选择具体时段后进行预约确认。
- 预约确认后展示挂号结果卡片,包含就诊信息。
- 查询挂号记录无需前置条件,直接返回历史记录。
- 所有已绑定组件的接口都应优先展示卡片,不要改成纯文本逐条展开。
// skills/hospital-skill/apis/bookAppointment.js
const {
isPreviewMode,
successResult,
errorResult,
genAppointmentId
} = require('../utils/util')
async function bookAppointment(params = {}) {
console.info('[ai-mode] bookAppointment 入口, params=', JSON.stringify(params))
const { hospitalId, deptId, slotId, patientName, patientPhone, hospitalName, deptName, doctorName, doctorTitle, date, time, price } = (params || {})
if (!hospitalId || !slotId) {
return successResult(
'缺少预约信息,请先选择可预约时段。',
{ appointment: null, success: false },
{ hospitalId, deptId }
)
}
if (isPreviewMode()) {
return buildResult(buildMockAppointment(params))
}
const { result } = await wx.cloud.callFunction({
name: 'hospital-skill-handler',
data: { action: 'bookAppointment', ...params }
})
if (result && result.code === 0 && result.data && result.data.appointment) {
console.info('[ai-mode] bookAppointment 云函数成功')
return buildResult(result.data.appointment)
}
return errorResult(result?.message || '请求失败')
}
function buildMockAppointment(params) {
const { hospitalId, hospitalName, deptName, doctorName, doctorTitle, date, time, patientName, patientPhone, price } = params || {}
return {
appointmentId: genAppointmentId(),
hospitalId: hospitalId || '',
hospitalName: hospitalName || '',
deptName: deptName || '',
doctorName: doctorName || '',
doctorTitle: doctorTitle || '',
date: date || '',
time: time || '',
patientName: patientName || '',
patientPhone: patientPhone || '',
price: price || 0,
status: 'confirmed',
statusText: '已确认',
createTime: new Date().toISOString()
}
}
function buildResult(appointment) {
return successResult(
`挂号成功!${appointment.hospitalName} ${appointment.deptName} - ${appointment.doctorName} ${appointment.doctorTitle}\n就诊时间:${appointment.date} ${appointment.time}\n请展示挂号结果卡片。`,
{ appointment, success: true }
)
}
module.exports = bookAppointment
// skills/hospital-skill/apis/getAvailableSlots.js
const {
isPreviewMode,
successResult,
errorResult,
defaultSlotsForDept
} = require('../utils/util')
async function getAvailableSlots(params = {}) {
console.info('[ai-mode] getAvailableSlots 入口, params=', JSON.stringify(params))
const hospitalId = (params && params.hospitalId) || ''
const deptId = (params && params.deptId) || ''
if (!hospitalId || !deptId) {
return successResult(
'缺少医院或科室信息,请先选择医院和科室。',
{ items: [], hospitalId, deptId },
{ hospitalId, deptId }
)
}
if (isPreviewMode()) {
return buildResult(defaultSlotsForDept(hospitalId, deptId), hospitalId, deptId)
}
const { result } = await wx.cloud.callFunction({
name: 'hospital-skill-handler',
data: { action: 'getAvailableSlots', hospitalId, deptId }
})
if (result && result.code === 0 && result.data) {
const items = result.data.items || []
console.info('[ai-mode] getAvailableSlots 云函数返回数量=', items.length)
return buildResult(items, hospitalId, deptId)
}
return errorResult(result?.message || '请求失败')
}
function buildResult(items, hospitalId, deptId) {
const total = items.length
if (total > 0) {
return successResult(
`找到 ${total} 个可预约时段,请展示时段选择卡片让用户选择。`,
{ items, total, hospitalId, deptId },
{ hospitalId, deptId }
)
}
return successResult(
'该科室当前没有可用时段。请展示空列表卡片,引导用户选择其他日期或科室。',
{ items: [], total: 0, hospitalId, deptId },
{ hospitalId, deptId }
)
}
module.exports = getAvailableSlots
// skills/hospital-skill/apis/getMyAppointments.js
const { appointments } = require('../data/seed')
const {
isPreviewMode,
successResult,
errorResult
} = require('../utils/util')
async function getMyAppointments(params = {}) {
console.info('[ai-mode] getMyAppointments 入口, params=', JSON.stringify(params))
if (isPreviewMode()) {
return buildResult(appointments)
}
const { result } = await wx.cloud.callFunction({
name: 'hospital-skill-handler',
data: { action: 'getMyAppointments' }
})
if (result && result.code === 0 && result.data) {
const items = result.data.items || []
console.info('[ai-mode] getMyAppointments 云函数返回数量=', items.length)
return buildResult(items)
}
return errorResult(result?.message || '请求失败')
}
function buildResult(items) {
const total = items.length
if (total > 0) {
return successResult(
`共 ${total} 条挂号记录,请展示挂号记录列表卡片。`,
{ items, total }
)
}
return successResult(
'当前没有挂号记录。请展示空列表卡片。',
{ items: [], total: 0 }
)
}
module.exports = getMyAppointments
// skills/hospital-skill/apis/searchHospitals.js
const {
isPreviewMode,
successResult,
errorResult,
defaultHospitalList
} = require('../utils/util')
async function searchHospitals(params = {}) {
console.info('[ai-mode] searchHospitals 入口, params=', JSON.stringify(params))
const keyword = String((params && params.keyword) || '').trim()
if (isPreviewMode()) {
return buildResult(defaultHospitalList(keyword), keyword)
}
const { result } = await wx.cloud.callFunction({
name: 'hospital-skill-handler',
data: { action: 'searchHospitals', keyword }
})
if (result && result.code === 0 && result.data) {
const items = result.data.items || []
console.info('[ai-mode] searchHospitals 云函数返回数量=', 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 = searchHospitals
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
// 种子数据(内嵌,不依赖外部 require)
const hospitals = [
{
hospitalId: 'H001',
hospitalName: '北京大学第一医院',
level: '三甲',
address: '北京市西城区西什库大街8号',
phone: '010-83572211',
rating: 4.8,
tags: ['综合', '重点'],
departments: [
{
deptId: 'D001',
deptName: '呼吸内科',
desc: '呼吸系统疾病诊治',
slots: [
{ slotId: 'S00101', date: '2026-06-09', time: '08:30-09:00', doctor: '王建国', title: '主任医师', available: 3, price: 100 },
{ slotId: 'S00102', date: '2026-06-09', time: '09:00-09:30', doctor: '王建国', title: '主任医师', available: 2, price: 100 },
{ slotId: 'S00103', date: '2026-06-09', time: '10:00-10:30', doctor: '李明辉', title: '副主任医师', available: 5, price: 60 },
{ slotId: 'S00104', date: '2026-06-10', time: '08:30-09:00', doctor: '王建国', title: '主任医师', available: 1, price: 100 },
{ slotId: 'S00105', date: '2026-06-10', time: '14:00-14:30', doctor: '李明辉', title: '副主任医师', available: 4, price: 60 },
{ slotId: 'S00106', date: '2026-06-11', time: '09:00-09:30', doctor: '张丽华', title: '主治医师', available: 6, price: 30 }
]
},
{
deptId: 'D002',
deptName: '消化内科',
desc: '消化系统疾病诊治',
slots: [
{ slotId: 'S00201', date: '2026-06-09', time: '09:00-09:30', doctor: '赵伟', title: '主任医师', available: 2, price: 100 },
{ slotId: 'S00202', date: '2026-06-09', time: '10:30-11:00', doctor: '赵伟', title: '主任医师', available: 3, price: 100 },
{ slotId: 'S00203', date: '2026-06-10', time: '08:00-08:30', doctor: '陈敏', title: '副主任医师', available: 1, price: 60 },
{ slotId: 'S00204', date: '2026-06-10', time: '14:00-14:30', doctor: '陈敏', title: '副主任医师', available: 5, price: 60 },
{ slotId: 'S00205', date: '2026-06-11', time: '09:00-09:30', doctor: '孙悦', title: '主治医师', available: 4, price: 30 }
]
},
{
deptId: 'D003',
deptName: '心血管内科',
desc: '心血管疾病诊治',
slots: [
{ slotId: 'S00301', date: '2026-06-09', time: '08:00-08:30', doctor: '刘强', title: '主任医师', available: 1, price: 100 },
{ slotId: 'S00302', date: '2026-06-10', time: '09:30-10:00', doctor: '刘强', title: '主任医师', available: 2, price: 100 },
{ slotId: 'S00303', date: '2026-06-10', time: '15:00-15:30', doctor: '周婷', title: '主治医师', available: 3, price: 30 }
]
}
]
},
{
hospitalId: 'H002',
hospitalName: '北京协和医院',
level: '三甲',
address: '北京市东城区帅府园1号',
phone: '010-69156114',
rating: 4.9,
tags: ['综合', '重点', '疑难'],
departments: [
{
deptId: 'D004',
deptName: '内分泌科',
desc: '内分泌与代谢疾病',
slots: [
{ slotId: 'S00401', date: '2026-06-09', time: '08:00-08:30', doctor: '林芳', title: '主任医师', available: 1, price: 200 },
{ slotId: 'S00402', date: '2026-06-09', time: '09:00-09:30', doctor: '林芳', title: '主任医师', available: 0, price: 200 },
{ slotId: 'S00403', date: '2026-06-10', time: '08:30-09:00', doctor: '林芳', title: '主任医师', available: 2, price: 200 },
{ slotId: 'S00404', date: '2026-06-10', time: '10:00-10:30', doctor: '郭磊', title: '副主任医师', available: 3, price: 100 },
{ slotId: 'S00405', date: '2026-06-11', time: '14:00-14:30', doctor: '郭磊', title: '副主任医师', available: 5, price: 100 }
]
},
{
deptId: 'D005',
deptName: '风湿免疫科',
desc: '风湿免疫性疾病',
slots: [
{ slotId: 'S00501', date: '2026-06-09', time: '08:30-09:00', doctor: '吴敏', title: '主任医师', available: 1, price: 200 },
{ slotId: 'S00502', date: '2026-06-10', time: '09:00-09:30', doctor: '吴敏', title: '主任医师', available: 2, price: 200 },
{ slotId: 'S00503', date: '2026-06-11', time: '08:00-08:30', doctor: '何琳', title: '主治医师', available: 4, price: 50 }
]
}
]
},
{
hospitalId: 'H003',
hospitalName: '北京朝阳医院',
level: '三甲',
address: '北京市朝阳区工体南路8号',
phone: '010-85231000',
rating: 4.6,
tags: ['综合', '急诊'],
departments: [
{
deptId: 'D006',
deptName: '呼吸内科',
desc: '呼吸系统疾病诊治',
slots: [
{ slotId: 'S00601', date: '2026-06-09', time: '08:00-08:30', doctor: '杨波', title: '副主任医师', available: 4, price: 60 },
{ slotId: 'S00602', date: '2026-06-09', time: '14:00-14:30', doctor: '杨波', title: '副主任医师', available: 6, price: 60 },
{ slotId: 'S00603', date: '2026-06-10', time: '09:00-09:30', doctor: '杨波', title: '副主任医师', available: 3, price: 60 },
{ slotId: 'S00604', date: '2026-06-11', time: '08:30-09:00', doctor: '许磊', title: '主治医师', available: 5, price: 30 }
]
},
{
deptId: 'D007',
deptName: '皮肤科',
desc: '皮肤疾病诊治',
slots: [
{ slotId: 'S00701', date: '2026-06-09', time: '09:00-09:30', doctor: '郑丽', title: '主任医师', available: 2, price: 100 },
{ slotId: 'S00702', date: '2026-06-10', time: '08:00-08:30', doctor: '郑丽', title: '主任医师', available: 3, price: 100 },
{ slotId: 'S00703', date: '2026-06-10', time: '14:30-15:00', doctor: '王倩', title: '主治医师', available: 4, price: 30 }
]
}
]
},
{
hospitalId: 'H004',
hospitalName: '北京友谊医院',
level: '三乙',
address: '北京市西城区永安路95号',
phone: '010-63016616',
rating: 4.5,
tags: ['综合'],
departments: [
{
deptId: 'D008',
deptName: '消化内科',
desc: '消化系统疾病诊治',
slots: [
{ slotId: 'S00801', date: '2026-06-09', time: '08:30-09:00', doctor: '马超', title: '副主任医师', available: 3, price: 50 },
{ slotId: 'S00802', date: '2026-06-09', time: '10:00-10:30', doctor: '马超', title: '副主任医师', available: 4, price: 50 },
{ slotId: 'S00803', date: '2026-06-10', time: '09:00-09:30', doctor: '马超', title: '副主任医师', available: 2, price: 50 },
{ slotId: 'S00804', date: '2026-06-11', time: '08:00-08:30', doctor: '宋婷', title: '主治医师', available: 6, price: 20 }
]
},
{
deptId: 'D009',
deptName: '骨科',
desc: '骨骼关节疾病',
slots: [
{ slotId: 'S00901', date: '2026-06-09', time: '09:00-09:30', doctor: '黄刚', title: '主任医师', available: 1, price: 80 },
{ slotId: 'S00902', date: '2026-06-10', time: '08:00-08:30', doctor: '黄刚', title: '主任医师', available: 2, price: 80 },
{ slotId: 'S00903', date: '2026-06-10', time: '15:00-15:30', doctor: '黄刚', title: '主任医师', available: 3, price: 80 }
]
}
]
},
{
hospitalId: 'H005',
hospitalName: '北京海淀医院',
level: '二甲',
address: '北京市海淀区中关村大街29号',
phone: '010-62583042',
rating: 4.3,
tags: ['综合', '社区'],
departments: [
{
deptId: 'D010',
deptName: '普通内科',
desc: '常见内科疾病',
slots: [
{ slotId: 'S01001', date: '2026-06-09', time: '08:00-08:30', doctor: '刘洋', title: '副主任医师', available: 5, price: 30 },
{ slotId: 'S01002', date: '2026-06-09', time: '09:30-10:00', doctor: '刘洋', title: '副主任医师', available: 8, price: 30 },
{ slotId: 'S01003', date: '2026-06-10', time: '08:00-08:30', doctor: '刘洋', title: '副主任医师', available: 6, price: 30 },
{ slotId: 'S01004', date: '2026-06-11', time: '09:00-09:30', doctor: '李丽', title: '主治医师', available: 10, price: 15 }
]
},
{
deptId: 'D011',
deptName: '儿科',
desc: '儿童疾病诊治',
slots: [
{ slotId: 'S01101', date: '2026-06-09', time: '08:30-09:00', doctor: '赵雪', title: '副主任医师', available: 4, price: 30 },
{ slotId: 'S01102', date: '2026-06-10', time: '09:00-09:30', doctor: '赵雪', title: '副主任医师', available: 3, price: 30 },
{ slotId: 'S01103', date: '2026-06-11', time: '08:00-08:30', doctor: '孙明', title: '主治医师', available: 7, price: 15 }
]
}
]
}
]
function flattenAllSlots() {
const result = []
hospitals.forEach((h) => {
h.departments.forEach((dept) => {
dept.slots.forEach((slot) => {
result.push({
...slot,
hospitalId: h.hospitalId,
hospitalName: h.hospitalName,
hospitalLevel: h.level,
deptId: dept.deptId,
deptName: dept.deptName
})
})
})
})
return result
}
async function handleSearchHospitals({ keyword }) {
const q = String(keyword || '').trim().toLowerCase()
const filtered = q
? hospitals.filter((h) =>
[h.hospitalName, h.level, h.address, ...h.tags]
.join(' ')
.toLowerCase()
.includes(q)
)
: hospitals
const items = filtered.map((h) => ({
hospitalId: h.hospitalId,
hospitalName: h.hospitalName,
level: h.level,
address: h.address,
rating: h.rating,
tags: h.tags,
departments: h.departments.map((d) => ({ deptId: d.deptId, deptName: d.deptName, desc: d.desc }))
}))
return {
code: 0,
message: 'success',
data: { items, keyword: q }
}
}
async function handleGetAvailableSlots({ hospitalId, deptId }) {
if (!hospitalId) {
return { code: -1, message: 'hospitalId 不能为空', data: null }
}
const hospital = hospitals.find((h) => h.hospitalId === hospitalId)
if (!hospital) {
return { code: -1, message: 'hospital_not_found', data: null }
}
let departments = hospital.departments
if (deptId) {
departments = departments.filter((d) => d.deptId === deptId)
}
const slots = []
departments.forEach((dept) => {
dept.slots.forEach((slot) => {
slots.push({
...slot,
hospitalId: hospital.hospitalId,
hospitalName: hospital.hospitalName,
deptId: dept.deptId,
deptName: dept.deptName
})
})
})
return {
code: 0,
message: 'success',
data: { hospitalId: hospital.hospitalId, hospitalName: hospital.hospitalName, slots }
}
}
async function handleBookAppointment({ slotId, patientName, patientPhone, openid }) {
if (!slotId) {
return { code: -1, message: 'slotId 不能为空', data: null }
}
if (!patientName) {
return { code: -1, message: 'patientName 不能为空', data: null }
}
const allSlots = flattenAllSlots()
const slot = allSlots.find((s) => s.slotId === slotId)
if (!slot) {
return { code: -1, message: 'slot_not_found', data: null }
}
if (slot.available <= 0) {
return { code: -1, message: 'slot_unavailable', data: null }
}
const appointmentId = `A${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).slice(2, 6).toUpperCase()}`
const now = new Date()
const data = {
appointmentId,
hospitalId: slot.hospitalId,
hospitalName: slot.hospitalName,
departmentId: slot.deptId,
departmentName: slot.deptName,
doctorName: slot.doctor,
slotDate: slot.date,
slotTime: slot.time,
patientName,
patientPhone: patientPhone || '',
status: 'confirmed',
openid: openid || '',
createdAt: db.serverDate()
}
try {
await db.collection('hospital_appointments').add({ data })
} catch (e) {
console.error('[hospital-skill-handler] save appointment failed:', e.message)
}
return {
code: 0,
message: 'success',
data: {
appointmentId: data.appointmentId,
hospitalName: data.hospitalName,
departmentName: data.departmentName,
doctorName: data.doctorName,
slotDate: data.slotDate,
slotTime: data.slotTime,
patientName: data.patientName,
status: data.status
}
}
}
async function handleGetMyAppointments({ openid }) {
if (!openid) {
return { code: -1, message: 'openid 不能为空', data: null }
}
try {
const res = await db.collection('hospital_appointments')
.where({ openid })
.orderBy('createdAt', 'desc')
.get()
return {
code: 0,
message: 'success',
data: { items: res.data || [] }
}
} catch (err) {
console.error('[hospital-skill-handler] getMyAppointments error:', err.message)
return { code: -1, message: err.message, data: null }
}
}
exports.main = async (event) => {
const { action } = event
console.log('[hospital-skill-handler] action=', action, 'event=', JSON.stringify(event))
switch (action) {
case 'searchHospitals':
return handleSearchHospitals(event)
case 'getAvailableSlots':
return handleGetAvailableSlots(event)
case 'bookAppointment':
return handleBookAppointment(event)
case 'getMyAppointments':
return handleGetMyAppointments(event)
default:
return {
code: -1,
message: `未知 action: ${action}`,
data: null
}
}
}
{
"name": "hospital-skill-handler",
"version": "1.0.0",
"description": "hospital-skill 云函数",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}
Component({
data: {
items: []
},
lifetimes: {
created() {
console.info('[ai-mode] appointment-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] appointment-list-card 收到 Result:', JSON.stringify(sc))
this.setData({
items: sc.items || []
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] appointment-list-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] appointment-list-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] appointment-list-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] appointment-list-card overflow monitor=on')
}
},
methods: {
// 纯展示组件,无上行 tap 交互
}
})
{
"component": true,
"usingComponents": {}
}
<view class="al-card">
<view class="al-title">我的挂号记录</view>
<view wx:if="{{!items.length}}" class="al-empty">
<view class="al-empty-title">暂无挂号记录</view>
<view class="al-empty-desc">您还没有任何挂号记录</view>
</view>
<block wx:for="{{items}}" wx:key="appointmentId">
<view class="al-item">
<view class="al-item-top">
<view class="al-hospital">{{item.hospitalName}}</view>
<view class="al-status {{item.status}}">{{item.statusText}}</view>
</view>
<view class="al-row">
<text class="al-label">科室</text>
<text class="al-val">{{item.deptName}}</text>
</view>
<view class="al-row">
<text class="al-label">医生</text>
<text class="al-val">{{item.doctorName}} {{item.doctorTitle}}</text>
</view>
<view class="al-row">
<text class="al-label">就诊时间</text>
<text class="al-val">{{item.date}} {{item.time}}</text>
</view>
<view class="al-row">
<text class="al-label">患者</text>
<text class="al-val">{{item.patientName}}</text>
</view>
<view class="al-row">
<text class="al-label">费用</text>
<text class="al-val al-price">¥{{item.price}}</text>
</view>
</view>
</block>
</view>
/* ratio=1:1;蓝调医疗健康风格 */
/* 色源:强调色 #007AFF / 淡蓝底 #F0F8FF / 成功绿 #34C759 */
.al-card {
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 16px;
padding: 16px;
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);
}
.al-title {
font-size: 17px;
font-weight: 600;
color: rgba(0,0,0,0.85);
margin-bottom: 12px;
}
.al-empty {
padding: 16px;
background: #F0F8FF;
border: 1px solid #B3D9FF;
border-radius: 12px;
}
.al-empty-title {
font-size: 15px;
color: rgba(0,0,0,0.85);
}
.al-empty-desc {
margin-top: 8px;
font-size: 13px;
color: rgba(0,0,0,0.50);
}
.al-item {
margin-top: 12px;
padding: 16px;
background: #F0F8FF;
border: 1px solid #D0E8FF;
border-radius: 12px;
}
.al-item:first-of-type {
margin-top: 0;
}
.al-item-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.al-hospital {
font-size: 15px;
font-weight: 600;
color: rgba(0,0,0,0.85);
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.al-status {
font-size: 12px;
padding: 2px 8px;
border-radius: 999px;
flex-shrink: 0;
}
.al-status.confirmed {
background: #DCFCE7;
color: #15803D;
}
.al-status.completed {
background: #E6F2FF;
color: #007AFF;
}
.al-status.cancelled {
background: #FEE2E2;
color: #DC2626;
}
.al-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
}
.al-label {
font-size: 13px;
color: rgba(0,0,0,0.50);
flex-shrink: 0;
}
.al-val {
font-size: 13px;
color: rgba(0,0,0,0.85);
text-align: right;
flex: 1;
margin-left: 12px;
}
.al-price {
color: #007AFF;
font-weight: 600;
}
@media (prefers-color-scheme: dark) {
.al-card {
background: #1C1C1E;
border-color: #2C2C2E;
box-shadow: none;
}
.al-title { color: #FFFFFF; }
.al-empty { background: #1C1C1E; border-color: #3A3A3C; }
.al-empty-title { color: #FFFFFF; }
.al-empty-desc { color: #8E8E93; }
.al-item {
background: #2C2C2E;
border-color: #3A3A3C;
}
.al-hospital { color: #FFFFFF; }
.al-status.confirmed { background: #123827; color: #6EE7B7; }
.al-status.completed { background: #1A3A5C; color: #64B5F6; }
.al-status.cancelled { background: #3B1D24; color: #FDA4AF; }
.al-label { color: #8E8E93; }
.al-val { color: #FFFFFF; }
.al-price { color: #64B5F6; }
}
Component({
data: {
appointment: null
},
lifetimes: {
created() {
console.info('[ai-mode] booking-result-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] booking-result-card 收到 Result:', JSON.stringify(sc))
this.setData({
appointment: sc.appointment || null
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] booking-result-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] booking-result-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] booking-result-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] booking-result-card overflow monitor=on')
}
},
methods: {
onTapViewRecords() {
console.info('[ai-mode] booking-result-card send api/call name=getMyAppointments')
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '查看我的挂号记录' },
{ type: 'api/call', data: { name: 'getMyAppointments', arguments: {} } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="br-card">
<view class="br-success-icon">✓</view>
<view class="br-success-text">挂号成功</view>
<view class="br-info">
<view class="br-row {{index > 0 ? 'br-row-bordered' : ''}}">
<text class="br-label">医院</text>
<text class="br-value">{{appointment.hospitalName}}</text>
</view>
<view class="br-row {{index > 0 ? 'br-row-bordered' : ''}}">
<text class="br-label">科室</text>
<text class="br-value">{{appointment.deptName}}</text>
</view>
<view class="br-row {{index > 0 ? 'br-row-bordered' : ''}}">
<text class="br-label">医生</text>
<text class="br-value">{{appointment.doctorName}} {{appointment.doctorTitle}}</text>
</view>
<view class="br-row {{index > 0 ? 'br-row-bordered' : ''}}">
<text class="br-label">时间</text>
<text class="br-value">{{appointment.date}} {{appointment.time}}</text>
</view>
<view class="br-row {{index > 0 ? 'br-row-bordered' : ''}}">
<text class="br-label">患者</text>
<text class="br-value">{{appointment.patientName || '待填写'}}</text>
</view>
<view class="br-row {{index > 0 ? 'br-row-bordered' : ''}}">
<text class="br-label">费用</text>
<text class="br-value br-price">¥{{appointment.price}}</text>
</view>
</view>
<view class="br-tip">请按时就诊,可提前 15 分钟到院取号</view>
<view
class="br-btn"
hover-class="br-btn-hover"
bind:tap="onTapViewRecords"
>查看我的挂号</view>
</view>
/* ratio=1:1;蓝调医疗健康风格 */
/* 色源:强调色 #007AFF / 成功绿 #34C759 / 淡蓝底 #F0F8FF / 按钮渐变 #409CFF → #007AFF */
.br-card {
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 16px;
padding: 16px;
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);
display: flex;
flex-direction: column;
align-items: center;
}
.br-success-icon {
width: 48px;
height: 48px;
line-height: 48px;
text-align: center;
font-size: 24px;
color: #FFFFFF;
background: #34C759;
border-radius: 50%;
margin-bottom: 8px;
}
.br-success-text {
font-size: 17px;
font-weight: 600;
color: rgba(0,0,0,0.85);
margin-bottom: 16px;
}
.br-info {
width: 100%;
background: #F0F8FF;
border: 1px solid #D0E8FF;
border-radius: 12px;
padding: 12px 16px;
box-sizing: border-box;
}
.br-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.br-row-bordered {
border-top: 1px solid #D0E8FF;
}
.br-label {
font-size: 13px;
color: rgba(0,0,0,0.50);
flex-shrink: 0;
}
.br-value {
font-size: 15px;
color: rgba(0,0,0,0.85);
text-align: right;
flex: 1;
margin-left: 12px;
}
.br-price {
color: #007AFF;
font-weight: 600;
}
.br-tip {
width: 100%;
margin-top: 12px;
padding: 10px 12px;
background: #FFF3CD;
border: 1px solid #FFE69C;
border-radius: 8px;
font-size: 13px;
color: #856404;
text-align: center;
box-sizing: border-box;
}
.br-btn {
margin-top: 16px;
width: 100%;
height: 44px;
line-height: 44px;
text-align: center;
font-size: 15px;
font-weight: 500;
color: #FFFFFF;
background: linear-gradient(135deg, #409CFF, #007AFF);
border-radius: 12px;
box-sizing: border-box;
}
.br-btn-hover {
opacity: 0.85;
}
@media (prefers-color-scheme: dark) {
.br-card {
background: #1C1C1E;
border-color: #2C2C2E;
box-shadow: none;
}
.br-success-icon { background: #30D158; }
.br-success-text { color: #FFFFFF; }
.br-info {
background: #2C2C2E;
border-color: #3A3A3C;
}
.br-row-bordered { border-color: #3A3A3C; }
.br-label { color: #8E8E93; }
.br-value { color: #FFFFFF; }
.br-price { color: #64B5F6; }
.br-tip {
background: #3A2E1A;
border-color: #5C4820;
color: #FFD60A;
}
.br-btn { background: linear-gradient(135deg, #409CFF, #007AFF); }
}
Component({
data: {
items: [],
keyword: ''
},
lifetimes: {
created() {
console.info('[ai-mode] hospital-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] hospital-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] hospital-list-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] hospital-list-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] hospital-list-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] hospital-list-card overflow monitor=on')
}
},
methods: {
onTapDept(e) {
const { hospitalId, hospitalName, deptId, deptName } = e.currentTarget.dataset
console.info(`[ai-mode] hospital-list-card send api/call name=getAvailableSlots args=${JSON.stringify({ hospitalId, deptId })}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: `查看${hospitalName} ${deptName}的挂号时段` },
{ type: 'api/call', data: { name: 'getAvailableSlots', arguments: { hospitalId, deptId } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="hl-card">
<view class="hl-title">医院列表</view>
<view wx:if="{{!items.length}}" class="hl-empty">
<view class="hl-empty-title">暂无匹配医院</view>
<view class="hl-empty-desc">{{keyword ? '请换一个关键词再试' : '请稍后再试'}}</view>
</view>
<block wx:for="{{items}}" wx:key="hospitalId">
<view class="hl-hospital">
<view class="hl-hospital-top">
<view class="hl-hospital-left">
<view class="hl-name-row">
<text class="hl-name">{{item.hospitalName}}</text>
<text class="hl-level">{{item.level}}</text>
</view>
<view class="hl-rating">评分 {{item.rating}}</view>
</view>
<view class="hl-tags">
<text wx:for="{{item.tags}}" wx:key="tag" class="hl-tag">{{item}}</text>
</view>
</view>
<view class="hl-addr">{{item.address}}</view>
<view class="hl-dept-list">
<view
wx:for="{{item.departments}}"
wx:key="deptId"
class="hl-dept-item {{index === 0 ? 'hl-dept-item-first' : ''}}"
hover-class="hl-dept-hover"
bind:tap="onTapDept"
data-hospital-id="{{item.hospitalId}}"
data-hospital-name="{{item.hospitalName}}"
data-dept-id="{{item.deptId}}"
data-dept-name="{{item.deptName}}"
>
<text class="hl-dept-name">{{item.deptName}}</text>
<text class="hl-dept-desc">{{item.desc}}</text>
<text class="hl-dept-arrow">›</text>
</view>
</view>
</view>
</block>
</view>
/* ratio=1:1;蓝调医疗健康风格 */
/* 色源:强调色 #007AFF / 淡蓝底 #F0F8FF / 卡片白 #FFFFFF */
.hl-card {
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 16px;
padding: 16px;
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);
}
.hl-title {
font-size: 17px;
font-weight: 600;
color: rgba(0,0,0,0.85);
margin-bottom: 12px;
}
.hl-empty {
padding: 16px;
background: #F0F8FF;
border: 1px solid #B3D9FF;
border-radius: 12px;
}
.hl-empty-title {
font-size: 15px;
color: rgba(0,0,0,0.85);
}
.hl-empty-desc {
margin-top: 8px;
font-size: 13px;
color: rgba(0,0,0,0.50);
}
.hl-hospital {
margin-top: 12px;
padding: 16px;
background: #F0F8FF;
border: 1px solid #D0E8FF;
border-radius: 12px;
}
.hl-hospital-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.hl-hospital-left {
flex: 1;
min-width: 0;
}
.hl-name-row {
display: flex;
align-items: center;
gap: 8px;
}
.hl-name {
font-size: 15px;
font-weight: 600;
color: rgba(0,0,0,0.85);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hl-level {
font-size: 11px;
color: #FFFFFF;
background: #007AFF;
padding: 2px 6px;
border-radius: 4px;
flex-shrink: 0;
}
.hl-rating {
margin-top: 4px;
font-size: 13px;
color: rgba(0,0,0,0.50);
}
.hl-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
flex-shrink: 0;
margin-left: 8px;
}
.hl-tag {
font-size: 11px;
color: #007AFF;
background: #E6F2FF;
padding: 2px 8px;
border-radius: 999px;
}
.hl-addr {
margin-top: 8px;
font-size: 13px;
color: rgba(0,0,0,0.50);
}
.hl-dept-list {
margin-top: 12px;
}
.hl-dept-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 8px;
margin-top: 8px;
}
.hl-dept-item-first {
margin-top: 0;
}
.hl-dept-hover {
opacity: 0.7;
}
.hl-dept-name {
font-size: 15px;
font-weight: 500;
color: rgba(0,0,0,0.85);
flex-shrink: 0;
}
.hl-dept-desc {
flex: 1;
font-size: 13px;
color: rgba(0,0,0,0.50);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hl-dept-arrow {
font-size: 18px;
color: rgba(0,0,0,0.30);
flex-shrink: 0;
}
@media (prefers-color-scheme: dark) {
.hl-card {
background: #1C1C1E;
border-color: #2C2C2E;
box-shadow: none;
}
.hl-title { color: #FFFFFF; }
.hl-empty { background: #1C1C1E; border-color: #3A3A3C; }
.hl-empty-title { color: #FFFFFF; }
.hl-empty-desc { color: #8E8E93; }
.hl-hospital {
background: #2C2C2E;
border-color: #3A3A3C;
}
.hl-name { color: #FFFFFF; }
.hl-level { background: #007AFF; }
.hl-rating, .hl-addr { color: #8E8E93; }
.hl-tag { background: #1A3A5C; color: #64B5F6; }
.hl-dept-item {
background: #1C1C1E;
border-color: #3A3A3C;
}
.hl-dept-name { color: #FFFFFF; }
.hl-dept-desc { color: #8E8E93; }
.hl-dept-arrow { color: #636366; }
}
Component({
data: {
items: [],
hospitalId: '',
deptId: ''
},
lifetimes: {
created() {
console.info('[ai-mode] slot-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] slot-list-card 收到 Result:', JSON.stringify(sc))
this.setData({
items: sc.items || [],
hospitalId: sc.hospitalId || '',
deptId: sc.deptId || ''
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] slot-list-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] slot-list-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] slot-list-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] slot-list-card overflow monitor=on')
}
},
methods: {
onTapBook(e) {
const ds = e.currentTarget.dataset
if (ds.available <= 0) return
console.info(`[ai-mode] slot-list-card send api/call name=bookAppointment args=${JSON.stringify({
hospitalId: ds.hospitalId,
deptId: ds.deptId,
slotId: ds.slotId,
hospitalName: ds.hospitalName,
deptName: ds.deptName,
doctorName: ds.doctorName,
doctorTitle: ds.doctorTitle,
date: ds.date,
time: ds.time,
price: ds.price
})}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: `预约${ds.hospitalName} ${ds.deptName} ${ds.doctorName} ${ds.date} ${ds.time}` },
{ type: 'api/call', data: {
name: 'bookAppointment',
arguments: {
hospitalId: ds.hospitalId,
deptId: ds.deptId,
slotId: ds.slotId,
hospitalName: ds.hospitalName,
deptName: ds.deptName,
doctorName: ds.doctorName,
doctorTitle: ds.doctorTitle,
date: ds.date,
time: ds.time,
price: ds.price
}
}}
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="sl-card">
<view wx:if="{{items.length}}" class="sl-header">
<text class="sl-hospital">{{items[0].hospitalName}}</text>
<text class="sl-dept">{{items[0].deptName}}</text>
</view>
<view wx:if="{{!items.length}}" class="sl-empty">
<view class="sl-empty-title">暂无可用时段</view>
<view class="sl-empty-desc">该科室当前没有可预约的时段,请选择其他日期或科室</view>
</view>
<view wx:for="{{items}}" wx:key="slotId" class="sl-item {{index === 0 ? 'sl-item-first' : ''}}">
<view class="sl-item-left">
<text class="sl-date">{{item.date}}</text>
<text class="sl-time">{{item.time}}</text>
</view>
<view class="sl-item-center">
<text class="sl-doctor">{{item.doctor}}</text>
<text class="sl-title">{{item.title}}</text>
</view>
<view class="sl-item-right">
<text class="sl-price">¥{{item.price}}</text>
<text class="sl-remain {{item.available <= 2 ? 'is-low' : ''}}">{{item.available > 0 ? '余' + item.available : '已满'}}</text>
</view>
<view
class="sl-btn"
hover-class="sl-btn-hover"
bind:tap="onTapBook"
data-slot-id="{{item.slotId}}"
data-hospital-id="{{item.hospitalId || hospitalId}}"
data-dept-id="{{item.deptId || deptId}}"
data-hospital-name="{{item.hospitalName}}"
data-dept-name="{{item.deptName}}"
data-doctor-name="{{item.doctor}}"
data-doctor-title="{{item.title}}"
data-date="{{item.date}}"
data-time="{{item.time}}"
data-price="{{item.price}}"
data-available="{{item.available}}"
>预约</view>
</view>
</view>
/* ratio=4:3;蓝调医疗健康风格 */
/* 色源:强调色 #007AFF / 淡蓝底 #F0F8FF / 按钮渐变 #409CFF → #007AFF */
.sl-card {
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 16px;
padding: 16px;
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);
}
.sl-header {
margin-bottom: 12px;
display: flex;
flex-direction: column;
gap: 4px;
}
.sl-hospital {
font-size: 17px;
font-weight: 600;
color: rgba(0,0,0,0.85);
}
.sl-dept {
font-size: 13px;
color: rgba(0,0,0,0.50);
}
.sl-empty {
padding: 16px;
background: #F0F8FF;
border: 1px solid #B3D9FF;
border-radius: 12px;
}
.sl-empty-title {
font-size: 15px;
color: rgba(0,0,0,0.85);
}
.sl-empty-desc {
margin-top: 8px;
font-size: 13px;
color: rgba(0,0,0,0.50);
}
.sl-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #F0F8FF;
border: 1px solid #D0E8FF;
border-radius: 12px;
margin-top: 8px;
}
.sl-item-first {
margin-top: 0;
}
.sl-item-left {
display: flex;
flex-direction: column;
align-items: center;
min-width: 72px;
flex-shrink: 0;
}
.sl-date {
font-size: 13px;
color: rgba(0,0,0,0.50);
}
.sl-time {
font-size: 15px;
font-weight: 600;
color: rgba(0,0,0,0.85);
margin-top: 2px;
}
.sl-item-center {
flex: 1;
min-width: 0;
}
.sl-doctor {
font-size: 15px;
font-weight: 500;
color: rgba(0,0,0,0.85);
}
.sl-title {
font-size: 13px;
color: rgba(0,0,0,0.50);
margin-top: 2px;
}
.sl-item-right {
display: flex;
flex-direction: column;
align-items: flex-end;
flex-shrink: 0;
}
.sl-price {
font-size: 15px;
font-weight: 600;
color: #007AFF;
}
.sl-remain {
font-size: 12px;
color: rgba(0,0,0,0.50);
margin-top: 2px;
}
.sl-remain.is-low {
color: #FF3B30;
font-weight: 500;
}
.sl-btn {
flex-shrink: 0;
padding: 8px 16px;
font-size: 13px;
font-weight: 500;
color: #FFFFFF;
background: linear-gradient(135deg, #409CFF, #007AFF);
border-radius: 12px;
text-align: center;
min-width: 52px;
}
.sl-btn-hover {
opacity: 0.85;
}
@media (prefers-color-scheme: dark) {
.sl-card {
background: #1C1C1E;
border-color: #2C2C2E;
box-shadow: none;
}
.sl-hospital { color: #FFFFFF; }
.sl-dept { color: #8E8E93; }
.sl-empty { background: #1C1C1E; border-color: #3A3A3C; }
.sl-empty-title { color: #FFFFFF; }
.sl-empty-desc { color: #8E8E93; }
.sl-item {
background: #2C2C2E;
border-color: #3A3A3C;
}
.sl-date { color: #8E8E93; }
.sl-time { color: #FFFFFF; }
.sl-doctor { color: #FFFFFF; }
.sl-title { color: #8E8E93; }
.sl-price { color: #64B5F6; }
.sl-remain { color: #8E8E93; }
.sl-remain.is-low { color: #FF453A; }
.sl-btn { background: linear-gradient(135deg, #409CFF, #007AFF); }
}
// skills/hospital-skill/data/seed.js
// 模拟医院挂号种子数据
const hospitals = [
{
hospitalId: 'H001',
hospitalName: '北京大学第一医院',
level: '三甲',
address: '北京市西城区西什库大街8号',
phone: '010-83572211',
rating: 4.8,
tags: ['综合', '重点'],
departments: [
{
deptId: 'D001',
deptName: '呼吸内科',
desc: '呼吸系统疾病诊治',
slots: [
{ slotId: 'S00101', date: '2026-06-09', time: '08:30-09:00', doctor: '王建国', title: '主任医师', available: 3, price: 100 },
{ slotId: 'S00102', date: '2026-06-09', time: '09:00-09:30', doctor: '王建国', title: '主任医师', available: 2, price: 100 },
{ slotId: 'S00103', date: '2026-06-09', time: '10:00-10:30', doctor: '李明辉', title: '副主任医师', available: 5, price: 60 },
{ slotId: 'S00104', date: '2026-06-10', time: '08:30-09:00', doctor: '王建国', title: '主任医师', available: 1, price: 100 },
{ slotId: 'S00105', date: '2026-06-10', time: '14:00-14:30', doctor: '李明辉', title: '副主任医师', available: 4, price: 60 },
{ slotId: 'S00106', date: '2026-06-11', time: '09:00-09:30', doctor: '张丽华', title: '主治医师', available: 6, price: 30 }
]
},
{
deptId: 'D002',
deptName: '消化内科',
desc: '消化系统疾病诊治',
slots: [
{ slotId: 'S00201', date: '2026-06-09', time: '09:00-09:30', doctor: '赵伟', title: '主任医师', available: 2, price: 100 },
{ slotId: 'S00202', date: '2026-06-09', time: '10:30-11:00', doctor: '赵伟', title: '主任医师', available: 3, price: 100 },
{ slotId: 'S00203', date: '2026-06-10', time: '08:00-08:30', doctor: '陈敏', title: '副主任医师', available: 1, price: 60 },
{ slotId: 'S00204', date: '2026-06-10', time: '14:00-14:30', doctor: '陈敏', title: '副主任医师', available: 5, price: 60 },
{ slotId: 'S00205', date: '2026-06-11', time: '09:00-09:30', doctor: '孙悦', title: '主治医师', available: 4, price: 30 }
]
},
{
deptId: 'D003',
deptName: '心血管内科',
desc: '心血管疾病诊治',
slots: [
{ slotId: 'S00301', date: '2026-06-09', time: '08:00-08:30', doctor: '刘强', title: '主任医师', available: 1, price: 100 },
{ slotId: 'S00302', date: '2026-06-10', time: '09:30-10:00', doctor: '刘强', title: '主任医师', available: 2, price: 100 },
{ slotId: 'S00303', date: '2026-06-10', time: '15:00-15:30', doctor: '周婷', title: '主治医师', available: 3, price: 30 }
]
}
]
},
{
hospitalId: 'H002',
hospitalName: '北京协和医院',
level: '三甲',
address: '北京市东城区帅府园1号',
phone: '010-69156114',
rating: 4.9,
tags: ['综合', '重点', '疑难'],
departments: [
{
deptId: 'D004',
deptName: '内分泌科',
desc: '内分泌与代谢疾病',
slots: [
{ slotId: 'S00401', date: '2026-06-09', time: '08:00-08:30', doctor: '林芳', title: '主任医师', available: 1, price: 200 },
{ slotId: 'S00402', date: '2026-06-09', time: '09:00-09:30', doctor: '林芳', title: '主任医师', available: 0, price: 200 },
{ slotId: 'S00403', date: '2026-06-10', time: '08:30-09:00', doctor: '林芳', title: '主任医师', available: 2, price: 200 },
{ slotId: 'S00404', date: '2026-06-10', time: '10:00-10:30', doctor: '郭磊', title: '副主任医师', available: 3, price: 100 },
{ slotId: 'S00405', date: '2026-06-11', time: '14:00-14:30', doctor: '郭磊', title: '副主任医师', available: 5, price: 100 }
]
},
{
deptId: 'D005',
deptName: '风湿免疫科',
desc: '风湿免疫性疾病',
slots: [
{ slotId: 'S00501', date: '2026-06-09', time: '08:30-09:00', doctor: '吴敏', title: '主任医师', available: 1, price: 200 },
{ slotId: 'S00502', date: '2026-06-10', time: '09:00-09:30', doctor: '吴敏', title: '主任医师', available: 2, price: 200 },
{ slotId: 'S00503', date: '2026-06-11', time: '08:00-08:30', doctor: '何琳', title: '主治医师', available: 4, price: 50 }
]
}
]
},
{
hospitalId: 'H003',
hospitalName: '北京朝阳医院',
level: '三甲',
address: '北京市朝阳区工体南路8号',
phone: '010-85231000',
rating: 4.6,
tags: ['综合', '急诊'],
departments: [
{
deptId: 'D006',
deptName: '呼吸内科',
desc: '呼吸系统疾病诊治',
slots: [
{ slotId: 'S00601', date: '2026-06-09', time: '08:00-08:30', doctor: '杨波', title: '副主任医师', available: 4, price: 60 },
{ slotId: 'S00602', date: '2026-06-09', time: '14:00-14:30', doctor: '杨波', title: '副主任医师', available: 6, price: 60 },
{ slotId: 'S00603', date: '2026-06-10', time: '09:00-09:30', doctor: '杨波', title: '副主任医师', available: 3, price: 60 },
{ slotId: 'S00604', date: '2026-06-11', time: '08:30-09:00', doctor: '许磊', title: '主治医师', available: 5, price: 30 }
]
},
{
deptId: 'D007',
deptName: '皮肤科',
desc: '皮肤疾病诊治',
slots: [
{ slotId: 'S00701', date: '2026-06-09', time: '09:00-09:30', doctor: '郑丽', title: '主任医师', available: 2, price: 100 },
{ slotId: 'S00702', date: '2026-06-10', time: '08:00-08:30', doctor: '郑丽', title: '主任医师', available: 3, price: 100 },
{ slotId: 'S00703', date: '2026-06-10', time: '14:30-15:00', doctor: '王倩', title: '主治医师', available: 4, price: 30 }
]
}
]
},
{
hospitalId: 'H004',
hospitalName: '北京友谊医院',
level: '三乙',
address: '北京市西城区永安路95号',
phone: '010-63016616',
rating: 4.5,
tags: ['综合'],
departments: [
{
deptId: 'D008',
deptName: '消化内科',
desc: '消化系统疾病诊治',
slots: [
{ slotId: 'S00801', date: '2026-06-09', time: '08:30-09:00', doctor: '马超', title: '副主任医师', available: 3, price: 50 },
{ slotId: 'S00802', date: '2026-06-09', time: '10:00-10:30', doctor: '马超', title: '副主任医师', available: 4, price: 50 },
{ slotId: 'S00803', date: '2026-06-10', time: '09:00-09:30', doctor: '马超', title: '副主任医师', available: 2, price: 50 },
{ slotId: 'S00804', date: '2026-06-11', time: '08:00-08:30', doctor: '宋婷', title: '主治医师', available: 6, price: 20 }
]
},
{
deptId: 'D009',
deptName: '骨科',
desc: '骨骼关节疾病',
slots: [
{ slotId: 'S00901', date: '2026-06-09', time: '09:00-09:30', doctor: '黄刚', title: '主任医师', available: 1, price: 80 },
{ slotId: 'S00902', date: '2026-06-10', time: '08:00-08:30', doctor: '黄刚', title: '主任医师', available: 2, price: 80 },
{ slotId: 'S00903', date: '2026-06-10', time: '15:00-15:30', doctor: '黄刚', title: '主任医师', available: 3, price: 80 }
]
}
]
},
{
hospitalId: 'H005',
hospitalName: '北京海淀医院',
level: '二甲',
address: '北京市海淀区中关村大街29号',
phone: '010-62583042',
rating: 4.3,
tags: ['综合', '社区'],
departments: [
{
deptId: 'D010',
deptName: '普通内科',
desc: '常见内科疾病',
slots: [
{ slotId: 'S01001', date: '2026-06-09', time: '08:00-08:30', doctor: '刘洋', title: '副主任医师', available: 5, price: 30 },
{ slotId: 'S01002', date: '2026-06-09', time: '09:30-10:00', doctor: '刘洋', title: '副主任医师', available: 8, price: 30 },
{ slotId: 'S01003', date: '2026-06-10', time: '08:00-08:30', doctor: '刘洋', title: '副主任医师', available: 6, price: 30 },
{ slotId: 'S01004', date: '2026-06-11', time: '09:00-09:30', doctor: '李丽', title: '主治医师', available: 10, price: 15 }
]
},
{
deptId: 'D011',
deptName: '儿科',
desc: '儿童疾病诊治',
slots: [
{ slotId: 'S01101', date: '2026-06-09', time: '08:30-09:00', doctor: '赵雪', title: '副主任医师', available: 4, price: 30 },
{ slotId: 'S01102', date: '2026-06-10', time: '09:00-09:30', doctor: '赵雪', title: '副主任医师', available: 3, price: 30 },
{ slotId: 'S01103', date: '2026-06-11', time: '08:00-08:30', doctor: '孙明', title: '主治医师', available: 7, price: 15 }
]
}
]
}
]
// 模拟挂号记录
const appointments = [
{
appointmentId: 'A00001',
hospitalId: 'H001',
hospitalName: '北京大学第一医院',
deptName: '呼吸内科',
doctorName: '王建国',
doctorTitle: '主任医师',
date: '2026-06-09',
time: '08:30-09:00',
patientName: '张三',
patientPhone: '138****1234',
price: 100,
status: 'confirmed',
statusText: '已确认',
createTime: '2026-06-08T10:00:00.000Z'
},
{
appointmentId: 'A00002',
hospitalId: 'H002',
hospitalName: '北京协和医院',
deptName: '内分泌科',
doctorName: '林芳',
doctorTitle: '主任医师',
date: '2026-06-10',
time: '08:30-09:00',
patientName: '张三',
patientPhone: '138****1234',
price: 200,
status: 'confirmed',
statusText: '已确认',
createTime: '2026-06-07T14:30:00.000Z'
},
{
appointmentId: 'A00003',
hospitalId: 'H005',
hospitalName: '北京海淀医院',
deptName: '普通内科',
doctorName: '刘洋',
doctorTitle: '副主任医师',
date: '2026-06-09',
time: '09:30-10:00',
patientName: '张三',
patientPhone: '138****1234',
price: 30,
status: 'completed',
statusText: '已完成',
createTime: '2026-06-05T09:00:00.000Z'
}
]
module.exports = { hospitals, appointments }
{
"collections": [
{
"name": "hospital_appointments",
"description": "医院挂号预约集合,存储用户挂号记录",
"indexes": [
{ "name": "idx_openid", "field": "openid" },
{ "name": "idx_hospitalId", "field": "hospitalId" }
]
}
]
}
// skills/hospital-skill/index.js
const searchHospitals = require('./apis/searchHospitals.js')
const getAvailableSlots = require('./apis/getAvailableSlots.js')
const bookAppointment = require('./apis/bookAppointment.js')
const getMyAppointments = require('./apis/getMyAppointments.js')
function registerAPIs() {
const skill = wx.modelContext.createSkill('skills/hospital-skill')
skill.use(async (ctx, next) => {
try {
console.info('[ai-mode] [hospital-skill] middleware start name=', ctx.name)
await next()
console.info('[ai-mode] [hospital-skill] middleware finish name=', ctx.name)
} catch (err) {
console.error('[ai-mode] [hospital-skill] middleware error:', err.message)
throw err
}
})
skill.registerAPI('searchHospitals', searchHospitals)
skill.registerAPI('getAvailableSlots', getAvailableSlots)
skill.registerAPI('bookAppointment', bookAppointment)
skill.registerAPI('getMyAppointments', getMyAppointments)
console.info('[ai-mode] [hospital-skill] APIs registered via createSkill')
}
registerAPIs()
module.exports = { registerAPIs }
{
"apis": [
{
"name": "searchHospitals",
"description": "查询医院列表(业务对象:医院列表卡片)。调用前置条件:用户需要选择医院、搜索医院、或尚未提供明确医院上下文时。用户提供医院名、科室名或区域关键词时优先按关键词搜索;用户未提供关键词时返回默认医院列表。【严禁场景】禁止在已有明确 hospitalId 且用户已选择医院时继续调用本接口,应改走 getAvailableSlots 展示科室时段。",
"_meta": {
"ui": {
"componentPath": "components/hospital-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": {
"hospitalId": { "type": "string", "description": "医院唯一 ID" },
"hospitalName": { "type": "string", "description": "医院名称" },
"level": { "type": "string", "description": "医院等级(三甲/三乙/二甲等)" },
"address": { "type": "string", "description": "医院地址" },
"rating": { "type": "number", "description": "评分" },
"tags": { "type": "array", "items": { "type": "string" }, "description": "标签" },
"phone": { "type": "string", "description": "联系电话" },
"departments": {
"type": "array",
"description": "科室列表",
"items": {
"type": "object",
"properties": {
"deptId": { "type": "string", "description": "科室唯一 ID" },
"deptName": { "type": "string", "description": "科室名称" },
"desc": { "type": "string", "description": "科室简介" }
},
"required": ["deptId", "deptName"],
"additionalProperties": false
}
}
},
"required": ["hospitalId", "hospitalName", "level", "address", "rating", "tags", "phone", "departments"],
"additionalProperties": false
}
},
"total": { "type": "number", "description": "医院数量" },
"keyword": { "type": "string", "description": "实际使用的关键词,无关键词时为空字符串" }
},
"required": ["items", "total", "keyword"],
"additionalProperties": false
}
},
{
"name": "getAvailableSlots",
"description": "查看指定科室的可挂号时段(业务对象:时段选择卡片)。调用前置条件:已从医院列表中选择医院和科室,即已有 hospitalId 和 deptId。展示可选日期、时间段、医生姓名、职称、剩余号源和价格。【严禁场景】禁止在没有有效 hospitalId 和 deptId 的情况下调用。上下文中缺少科室信息时应先调用 searchHospitals。",
"_meta": {
"ui": {
"componentPath": "components/slot-list-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"hospitalId": {
"type": "string",
"description": "医院唯一标识,必须来自上游 searchHospitals 返回的 items[].hospitalId 原值。【禁止编造】"
},
"deptId": {
"type": "string",
"description": "科室唯一标识,必须来自上游 searchHospitals 返回的 items[].departments[].deptId 原值。【禁止编造】"
}
},
"required": ["hospitalId", "deptId"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"items": {
"type": "array",
"description": "可用时段列表",
"items": {
"type": "object",
"properties": {
"slotId": { "type": "string", "description": "时段唯一 ID" },
"date": { "type": "string", "description": "日期 YYYY-MM-DD" },
"time": { "type": "string", "description": "时间段 HH:MM-HH:MM" },
"doctor": { "type": "string", "description": "医生姓名" },
"title": { "type": "string", "description": "医生职称" },
"available": { "type": "number", "description": "剩余号源数" },
"price": { "type": "number", "description": "挂号费(元)" },
"hospitalName": { "type": "string", "description": "医院名称" },
"deptName": { "type": "string", "description": "科室名称" }
},
"required": ["slotId", "date", "time", "doctor", "title", "available", "price", "hospitalName", "deptName"],
"additionalProperties": false
}
},
"total": { "type": "number", "description": "时段数量" },
"hospitalId": { "type": "string" },
"deptId": { "type": "string" }
},
"required": ["items", "total", "hospitalId", "deptId"],
"additionalProperties": false
}
},
{
"name": "bookAppointment",
"description": "为指定时段执行预约挂号(业务对象:挂号结果卡片)。调用前置条件:已通过 getAvailableSlots 选择具体时段,即已有 slotId 和完整预约上下文。成功后生成 appointmentId 并返回就诊详情。【严禁场景】禁止在无 slotId 时调用;禁止在号源为 0 时向用户承诺预约成功。",
"_meta": {
"ui": {
"componentPath": "components/booking-result-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"hospitalId": {
"type": "string",
"description": "医院 ID,来自上游"
},
"deptId": {
"type": "string",
"description": "科室 ID,来自上游"
},
"slotId": {
"type": "string",
"description": "时段唯一标识,必须来自上游 getAvailableSlots 返回的 items[].slotId 原值。【禁止编造】"
},
"hospitalName": {
"type": "string",
"description": "医院名称"
},
"deptName": {
"type": "string",
"description": "科室名称"
},
"doctorName": {
"type": "string",
"description": "医生姓名"
},
"doctorTitle": {
"type": "string",
"description": "医生职称"
},
"date": {
"type": "string",
"description": "就诊日期 YYYY-MM-DD"
},
"time": {
"type": "string",
"description": "就诊时间段"
},
"price": {
"type": "number",
"description": "挂号费"
},
"patientName": {
"type": "string",
"description": "患者姓名。用户未提供时询问用户。"
},
"patientPhone": {
"type": "string",
"description": "患者联系电话。用户未提供时询问用户。"
}
},
"required": ["hospitalId", "slotId"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"appointment": {
"type": "object",
"description": "挂号结果",
"properties": {
"appointmentId": { "type": "string", "description": "预约唯一 ID" },
"hospitalId": { "type": "string" },
"hospitalName": { "type": "string" },
"deptName": { "type": "string" },
"doctorName": { "type": "string" },
"doctorTitle": { "type": "string" },
"date": { "type": "string" },
"time": { "type": "string" },
"patientName": { "type": "string" },
"patientPhone": { "type": "string" },
"price": { "type": "number" },
"status": { "type": "string", "description": "预约状态,首版固定为 confirmed" },
"statusText": { "type": "string", "description": "状态文案" },
"createTime": { "type": "string", "description": "预约创建时间 ISO 字符串" }
},
"required": ["appointmentId", "hospitalId", "hospitalName", "deptName", "doctorName", "date", "time", "status", "statusText", "createTime"],
"additionalProperties": false
},
"success": { "type": "boolean" }
},
"required": ["appointment", "success"],
"additionalProperties": false
}
},
{
"name": "getMyAppointments",
"description": "查询当前用户的挂号记录(业务对象:挂号记录列表卡片)。调用前置条件:用户想查看自己的挂号记录时调用。展示所有历史预约,包含医院、科室、医生、时间、状态等。【严禁场景】禁止在用户没有提出查看记录时主动调用。",
"_meta": {
"ui": {
"componentPath": "components/appointment-list-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {},
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"items": {
"type": "array",
"description": "挂号记录列表",
"items": {
"type": "object",
"properties": {
"appointmentId": { "type": "string" },
"hospitalId": { "type": "string" },
"hospitalName": { "type": "string" },
"deptName": { "type": "string" },
"doctorName": { "type": "string" },
"doctorTitle": { "type": "string" },
"date": { "type": "string" },
"time": { "type": "string" },
"patientName": { "type": "string" },
"patientPhone": { "type": "string" },
"price": { "type": "number" },
"status": { "type": "string", "description": "confirmed / completed / cancelled" },
"statusText": { "type": "string" },
"createTime": { "type": "string" }
},
"required": ["appointmentId", "hospitalId", "hospitalName", "deptName", "doctorName", "date", "time", "status", "statusText", "createTime"],
"additionalProperties": false
}
},
"total": { "type": "number" }
},
"required": ["items", "total"],
"additionalProperties": false
}
}
],
"components": [
{
"path": "components/hospital-list-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/slot-list-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/booking-result-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/appointment-list-card/index",
"relatedPage": "/pages/home/home"
}
]
}
hospital-skill
医院挂号,支持搜索医院、查看科室时段、预约挂号及查看记录。
功能
- 按关键词搜索医院及科室
- 查看指定科室的可挂号时段与医生信息
- 选择时段完成预约挂号
- 查看历史挂号记录
用户输入示例
- "帮我挂个号"
- "预约看病"
- "附近有哪些医院"
- "挂呼吸科的号"
- "看看我的挂号记录"
- "预约明天上午的号"
原子接口
| 接口名 | 说明 |
|---|---|
searchHospitals | 查询医院列表(含科室信息) |
getAvailableSlots | 查看指定科室的可挂号时段 |
bookAppointment | 为指定时段执行预约挂号 |
getMyAppointments | 查询当前用户的挂号记录 |
原子组件
| 组件路径 | 说明 |
|---|---|
components/hospital-list-card/index | 医院列表展示 |
components/slot-list-card/index | 科室时段选择 |
components/booking-result-card/index | 挂号结果展示 |
components/appointment-list-card/index | 挂号记录列表 |
后端依赖
| 资源 | 名称 |
|---|---|
| 云函数 | hospital-skill-handler |
| 数据库集合 | hospital_appointments |
// skills/hospital-skill/utils/util.js
const { hospitals } = require('../data/seed')
const PREVIEW_MODE_KEY = 'mp_skills_preview_mode'
function isPreviewMode() {
return wx.getStorageSync(PREVIEW_MODE_KEY) !== false
}
function errorResult(msg, structuredContent, meta) {
const result = { isError: true, content: [{ type: 'text', text: msg }] }
if (structuredContent !== undefined) result.structuredContent = structuredContent
if (meta !== undefined) result._meta = meta
return result
}
function successResult(msg, structuredContent, meta) {
const result = { isError: false, content: [{ type: 'text', text: msg }] }
if (structuredContent !== undefined) result.structuredContent = structuredContent
if (meta !== undefined) result._meta = meta
return result
}
/**
* 根据关键词搜索医院(本地降级)
*/
function defaultHospitalList(keyword) {
const q = String(keyword || '').trim().toLowerCase()
const list = q
? hospitals.filter((h) => {
const hay = [h.hospitalName, ...h.tags, h.level, h.address]
.join(' ')
.toLowerCase()
return hay.includes(q)
})
: hospitals
return list.map((h) => ({
hospitalId: h.hospitalId,
hospitalName: h.hospitalName,
level: h.level,
address: h.address,
rating: h.rating,
tags: h.tags,
phone: h.phone,
departments: h.departments.map((d) => ({
deptId: d.deptId,
deptName: d.deptName,
desc: d.desc
}))
}))
}
/**
* 根据医院ID获取完整医院信息
*/
function defaultHospitalDetail(hospitalId) {
return hospitals.find((h) => h.hospitalId === hospitalId) || null
}
/**
* 根据科室ID获取可用时段
*/
function defaultSlotsForDept(hospitalId, deptId) {
const hospital = hospitals.find((h) => h.hospitalId === hospitalId)
if (!hospital) return []
const dept = hospital.departments.find((d) => d.deptId === deptId)
if (!dept) return []
return dept.slots.map((s) => ({
...s,
hospitalName: hospital.hospitalName,
deptName: dept.deptName
}))
}
/**
* 生成预约ID
*/
function genAppointmentId() {
return `A${Date.now().toString(36).toUpperCase()}${String(Math.random()).slice(2, 6)}`
}
module.exports = {
isPreviewMode,
errorResult,
successResult,
defaultHospitalList,
defaultHospitalDetail,
defaultSlotsForDept,
genAppointmentId
}