
Party Skill
- 1 installs
- 27 repo stars
- Updated June 18, 2026
- tencentcloudbase/awesome-miniprogram-skills
WeChat Mini Program skill for organizing gatherings: creating a party, recommending venues, inviting friends, and viewing party details.
About
Adds a party-planning flow to a WeChat Mini Program for creating events, getting venue recommendations, inviting friends, and viewing details. A developer uses it as a scenario template when building social event features.
- Handles party creation and venue recommendations
- Covers friend invitations and party-detail views
Party 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 party-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 organizing gatherings: creating a party, recommending venues, inviting friends, and viewing party details.
Files
聚会安排
创建聚会活动、获取聚会场地推荐、邀请朋友参加以及查看聚会详情的能力集合。
触发场景
用户原话举例(路由命中本技能):
- "帮我组织一个聚会"
- "周末想搞个生日趴"
- "有什么适合聚会的地方推荐"
- "推荐几个轰趴馆"
- "帮我邀请朋友参加聚会"
- "看看我的聚会详情"
- "创建个周末聚餐活动"
不适用范围
- 门店排队取号、线上点单、下单支付等诉求 → 不在本技能范围,由排队/点单技能处理
- 地图导航、路线规划、打车出行等诉求 → 不在本技能范围
- 历史订单、会员积分、优惠券等诉求 → 不在本技能范围
接口链路
createParty:创建聚会活动,填写聚会名称、日期、时间等基本信息。getRecommendations:获取聚会场地推荐(餐厅/轰趴馆/KTV/户外等)。inviteFriends:选择朋友并发起邀请。getPartyDetails:查看聚会完整详情(包含成员状态)。
使用顺序
- 创建聚会前需先确定聚会名称、日期和时间;缺少必要信息时引导用户补充。
- 获取推荐前用户应没有明确场地;已有明确场地时不应再调用推荐。
- 邀请朋友前需先有已创建的聚会;没有 partyId 时先创建聚会。
- 查看聚会详情前需有有效 partyId;没有时先创建或选择聚会。
- 所有已绑定组件的接口都应优先展示卡片,不要改成纯文本逐条展开。
const {
isPreviewMode,
successResult,
errorResult,
genPartyId,
genInviteCode,
addParty
} = require('../utils/util')
async function createParty(params = {}) {
console.info('[ai-mode] createParty 入口, params=', JSON.stringify(params))
const { title, date, time, location, type, description } = params || {}
const theme = title || ''
if (isPreviewMode()) {
return buildResult(buildDefaultParty(theme, date, time, location, type, description))
}
const { result } = await wx.cloud.callFunction({
name: 'party-skill-handler',
data: {
action: 'createParty',
theme: theme,
date: date || '',
time: time || '',
location: location || '',
type: type || '',
description: description || ''
}
})
if (result && result.code === 0 && result.data) {
console.info('[ai-mode] createParty 云函数返回成功')
return buildResult(result.data)
}
return errorResult(result?.message || '请求失败')
}
function buildDefaultParty(theme, date, time, location, type, description) {
const now = new Date().toISOString()
const party = {
partyId: genPartyId(),
theme: theme || '新聚会',
date: date || '',
time: time || '',
location: location || '',
type: type || '',
description: description || '',
guestCount: 0,
status: 'planning',
createTime: now,
inviteCode: genInviteCode()
}
addParty(party)
return party
}
function buildResult(data) {
const { partyId, theme, status, createTime, inviteCode } = data
const themeText = data.theme || '新聚会'
if (partyId) {
return successResult(
`已成功创建聚会「${themeText}」。请展示聚会创建成功卡片,包含活动详情和邀请码。引导用户下一步可以邀请好友或查看推荐场所。`,
data,
{ partyId }
)
}
return errorResult(
'创建聚会失败,请稍后重试。',
null,
{ error: 'create_failed' }
)
}
module.exports = createParty
const {
isPreviewMode,
successResult,
errorResult,
getPartyById
} = require('../utils/util')
async function getPartyDetails(params = {}) {
console.info('[ai-mode] getPartyDetails 入口, params=', JSON.stringify(params))
const partyId = String((params && params.partyId) || '').trim()
if (!partyId) {
return errorResult(
'缺少聚会活动 ID,请先创建或选择一个聚会。',
null,
{ error: 'missing_partyId' }
)
}
if (isPreviewMode()) {
return buildResult(getPartyById(partyId))
}
const { result } = await wx.cloud.callFunction({
name: 'party-skill-handler',
data: {
action: 'getPartyDetails',
partyId
}
})
if (result && result.code === 0 && result.data) {
console.info('[ai-mode] getPartyDetails 云函数返回成功')
return buildResult(result.data)
}
return errorResult(result?.message || '请求失败')
}
function buildResult(data) {
if (!data) {
return errorResult(
'未找到该聚会详情,请确认聚会 ID 是否正确。',
null,
{ error: 'party_not_found' }
)
}
const { theme, statusText } = data
return successResult(
`已找到聚会「${theme || '未命名'}」(${statusText || '未知'})。请展示聚会详情卡片,包含活动信息和好友状态。`,
data,
{ partyId: data.partyId }
)
}
module.exports = getPartyDetails
const {
isPreviewMode,
successResult,
errorResult,
filterRecommendations
} = require('../utils/util')
async function getRecommendations(params = {}) {
console.info('[ai-mode] getRecommendations 入口, params=', JSON.stringify(params))
const type = String((params && params.type) || '').trim()
const keyword = String((params && params.keyword) || '').trim()
if (isPreviewMode()) {
return buildResult(filterRecommendations(type, keyword), type, keyword)
}
const { result } = await wx.cloud.callFunction({
name: 'party-skill-handler',
data: {
action: 'getRecommendations',
type,
keyword
}
})
if (result && result.code === 0 && result.data) {
const items = result.data.items || []
console.info('[ai-mode] getRecommendations 云函数返回数量=', items.length)
return buildResult(items, type, keyword)
}
return errorResult(result?.message || '请求失败')
}
function buildResult(items, type, keyword) {
const total = items.length
const typeTextMap = { restaurant: '餐厅', party_house: '轰趴馆', ktv: 'KTV', outdoor: '户外' }
const typeLabel = typeTextMap[type] || ''
if (total > 0) {
const prefix = typeLabel ? `已找到 ${total} 个${typeLabel}推荐` : `已找到 ${total} 个聚会场所推荐`
return successResult(
`${prefix}。请展示推荐列表卡片,让用户从卡片中选择一个场所。禁止以纯文本列出推荐详情。`,
{ items, total, type, keyword },
{ type, keyword }
)
}
if (typeLabel) {
return successResult(
keyword
? `未找到与「${keyword}」相关的${typeLabel}推荐。请展示空列表卡片,并引导用户换一个关键词或类型。`
: `当前没有${typeLabel}推荐。请展示空列表卡片,引导用户选择其他类型。`,
{ items: [], total: 0, type, keyword },
{ type, keyword }
)
}
return successResult(
keyword
? `未找到与「${keyword}」相关的聚会场所。请展示空列表卡片,引导用户换一个关键词。`
: '当前没有可推荐的聚会场所。请稍后再试。',
{ items: [], total: 0, type, keyword },
{ type, keyword }
)
}
module.exports = getRecommendations
const {
isPreviewMode,
successResult,
errorResult,
getFriendList,
getPartyById
} = require('../utils/util')
async function inviteFriends(params = {}) {
console.info('[ai-mode] inviteFriends 入口, params=', JSON.stringify(params))
const { partyId, friendIds, keyword } = params || {}
if (!partyId) {
return errorResult(
'缺少聚会活动信息,请先创建聚会后再邀请好友。',
null,
{ error: 'missing_partyId' }
)
}
if (isPreviewMode()) {
return buildResult(buildDefaultInviteData(partyId, friendIds, keyword), partyId)
}
const { result } = await wx.cloud.callFunction({
name: 'party-skill-handler',
data: {
action: 'inviteFriends',
partyId,
friendIds: friendIds || []
}
})
if (result && result.code === 0 && result.data) {
console.info('[ai-mode] inviteFriends 云函数返回成功')
return buildResult(result.data, partyId)
}
return errorResult(result?.message || '请求失败')
}
function buildDefaultInviteData(partyId, friendIds, keyword) {
const party = getPartyById(partyId)
let allFriends = getFriendList()
// Filter by keyword if provided
if (keyword && keyword.trim()) {
const kw = keyword.trim().toLowerCase()
allFriends = allFriends.filter((f) => f.name.toLowerCase().includes(kw))
}
// If friendIds provided, mark selected ones
if (friendIds && friendIds.length > 0) {
const invited = friendIds.map((fid) => {
const friend = allFriends.find((f) => f.friendId === fid)
return friend || { friendId: fid, name: '未知', avatar: '', status: 'pending' }
})
return {
partyId,
friends: allFriends.map((f) => ({
...f,
status: friendIds.includes(f.friendId) ? 'pending' : 'uninvited'
})),
invitedCount: friendIds.length,
acceptedCount: 0
}
}
// First call: show friend list
return {
partyId,
friends: allFriends,
invitedCount: 0,
acceptedCount: 0
}
}
function buildResult(data, partyId) {
const { friends: friendList, invitedCount } = data
if (invitedCount > 0) {
return successResult(
`已成功邀请 ${invitedCount} 位好友参加聚会。请展示邀请结果卡片,包含好友回复状态。`,
data,
{ partyId }
)
}
// Show friend selection list
if (friendList && friendList.length > 0) {
return successResult(
'请展示好友列表卡片,让用户选择要邀请的好友。点击好友可发送邀请。',
data,
{ partyId }
)
}
return errorResult(
'没有可邀请的好友列表。',
null,
{ error: 'no_friends' }
)
}
module.exports = inviteFriends
// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
// 推荐场所种子数据(来自 seed.js)
const recommendations = [
{
id: 'R001',
name: '花园餐厅·望京店',
type: 'restaurant',
typeText: '餐厅',
rating: 4.7,
priceLevel: '中档',
address: '北京市朝阳区望京SOHO T2 3F',
distance: '320m',
capacity: '4-20人',
tags: ['精致料理', '包间', '适合聚会'],
keywords: ['望京', '餐厅', '聚餐', '包间']
},
{
id: 'R002',
name: '老北京涮肉馆',
type: 'restaurant',
typeText: '餐厅',
rating: 4.5,
priceLevel: '平价',
address: '北京市东城区东四北大街128号',
distance: '2.1km',
capacity: '2-12人',
tags: ['火锅', '老字号', '热闹'],
keywords: ['东城', '火锅', '涮肉', '聚餐']
},
{
id: 'R003',
name: '轰趴馆·欢乐空间',
type: 'party_house',
typeText: '轰趴馆',
rating: 4.8,
priceLevel: '中高档',
address: '北京市朝阳区建国路88号SOHO现代城B1',
distance: '1.5km',
capacity: '8-30人',
tags: ['KTV', '桌游', '台球', '剧本杀', '适合团建'],
keywords: ['轰趴', '团建', '桌游', '剧本杀']
},
{
id: 'R004',
name: '唱响KTV·三里屯店',
type: 'ktv',
typeText: 'KTV',
rating: 4.4,
priceLevel: '中档',
address: '北京市朝阳区三里屯太古里南区B1',
distance: '2.4km',
capacity: '2-20人',
tags: ['豪华包间', '海量曲库', '酒水畅饮'],
keywords: ['KTV', '唱歌', '三里屯', '娱乐']
},
{
id: 'R005',
name: '阳光露营基地',
type: 'outdoor',
typeText: '户外',
rating: 4.6,
priceLevel: '中档',
address: '北京市怀柔区雁栖湖路18号',
distance: '35km',
capacity: '10-50人',
tags: ['烧烤', '露营', '篝火', '亲近自然'],
keywords: ['户外', '露营', '烧烤', '怀柔', '雁栖湖']
},
{
id: 'R006',
name: '日料·樱花亭',
type: 'restaurant',
typeText: '餐厅',
rating: 4.3,
priceLevel: '高档',
address: '北京市朝阳区国贸商城北区4F',
distance: '1.8km',
capacity: '2-8人',
tags: ['日料', '刺身', '私密包间', '约会'],
keywords: ['日料', '国贸', '精致', '包间']
}
]
// 好友种子数据(来自 seed.js)
const friends = [
{ friendId: 'F001', name: '小明', avatar: '', phone: '138****1234' },
{ friendId: 'F002', name: '小红', avatar: '', phone: '139****5678' },
{ friendId: 'F003', name: '大伟', avatar: '', phone: '137****9012' },
{ friendId: 'F004', name: '莉莉', avatar: '', phone: '136****3456' },
{ friendId: 'F005', name: '阿强', avatar: '', phone: '135****7890' }
]
// 云函数入口函数
exports.main = async (event, context) => {
const { action } = event
const wxContext = cloud.getWXContext()
const openid = wxContext.OPENID
switch (action) {
case 'createParty': {
const { title, date, location, invitees } = event
if (!title || !date || !location) {
return { code: -1, msg: '参数不完整' }
}
const partyId = 'P' + Date.now()
const party = {
partyId,
title,
date,
location,
invitees: invitees || [],
status: 'planning',
openid,
createdAt: new Date()
}
await db.collection('parties').add({ data: party })
return { code: 0, data: { partyId, status: 'planning' } }
}
case 'getRecommendations': {
const { keyword, type } = event
let results = recommendations
if (keyword) {
const kw = keyword.toLowerCase()
results = results.filter(r =>
r.name.includes(kw) ||
r.keywords.some(k => k.includes(kw)) ||
r.tags.some(t => t.includes(kw))
)
}
if (type) {
results = results.filter(r => r.type === type)
}
return { code: 0, data: results }
}
case 'inviteFriends': {
const { partyId, friendIds } = event
if (!partyId || !friendIds || !friendIds.length) {
return { code: -1, msg: '参数不完整' }
}
const invitedList = friends.filter(f => friendIds.includes(f.friendId)).map(f => ({
friendId: f.friendId,
name: f.name,
avatar: f.avatar,
status: 'pending',
statusText: '待回复'
}))
// 更新聚会邀请人列表
const partyRes = await db.collection('parties').where({ partyId, openid }).get()
if (partyRes.data.length === 0) {
return { code: -1, msg: '聚会不存在' }
}
const existingInvitees = partyRes.data[0].invitees || []
const mergedInvitees = [...existingInvitees]
invitedList.forEach(inv => {
if (!mergedInvitees.find(e => e.friendId === inv.friendId)) {
mergedInvitees.push(inv)
}
})
await db.collection('parties').where({ partyId, openid }).update({
data: { invitees: mergedInvitees }
})
return { code: 0, data: { partyId, invitees: mergedInvitees } }
}
case 'getPartyDetails': {
const { partyId } = event
if (!partyId) {
return { code: -1, msg: '缺少聚会ID' }
}
const res = await db.collection('parties').where({ partyId, openid }).get()
if (res.data.length === 0) {
return { code: -1, msg: '聚会不存在' }
}
return { code: 0, data: res.data[0] }
}
default:
return { code: -1, msg: `未知 action: ${action}` }
}
}
{
"name": "party-skill-handler",
"version": "1.0.0",
"description": "party-skill 云函数",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}
Component({
data: {
partyId: '',
friends: [],
invitedCount: 0,
acceptedCount: 0,
selectedIds: [],
isSent: false
},
lifetimes: {
created() {
console.info('[ai-mode] invite-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] invite-card 收到 Result:', JSON.stringify(sc))
this.setData({
partyId: sc.partyId || '',
friends: sc.friends || [],
invitedCount: sc.invitedCount || 0,
acceptedCount: sc.acceptedCount || 0,
isSent: (sc.invitedCount || 0) > 0
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] invite-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] invite-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] invite-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] invite-card overflow monitor=on')
}
},
methods: {
onToggleFriend(e) {
const { friendid } = e.currentTarget.dataset
let selected = [...this.data.selectedIds]
const idx = selected.indexOf(friendid)
if (idx > -1) {
selected.splice(idx, 1)
} else {
selected.push(friendid)
}
this.setData({ selectedIds: selected })
},
onSendInvite() {
const { partyId, selectedIds } = this.data
if (!partyId || selectedIds.length === 0) return
console.info(`[ai-mode] invite-card send api/call name=inviteFriends args=${JSON.stringify({ partyId, friendIds: selectedIds })}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: `邀请 ${selectedIds.length} 位好友` },
{ type: 'api/call', data: { name: 'inviteFriends', arguments: { partyId, friendIds: selectedIds } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="ic-card">
<view wx:if="{{!isSent}}" class="ic-select">
<view class="ic-title">邀请好友</view>
<view class="ic-hint">选择要邀请的好友</view>
<view wx:if="{{!friends.length}}" class="ic-empty">
<view class="ic-empty-text">暂无好友列表</view>
</view>
<block wx:for="{{friends}}" wx:key="friendId">
<view
class="ic-friend {{selectedIds.indexOf(item.friendId) > -1 ? 'is-selected' : ''}}"
data-friendid="{{item.friendId}}"
bind:tap="onToggleFriend"
>
<view class="ic-avatar">{{item.name.slice(0, 1)}}</view>
<view class="ic-friend-info">
<text class="ic-friend-name">{{item.name}}</text>
<text class="ic-friend-status">{{item.statusText || '待邀请'}}</text>
</view>
<view class="ic-check {{selectedIds.indexOf(item.friendId) > -1 ? 'is-checked' : ''}}">
<view class="ic-check-inner" wx:if="{{selectedIds.indexOf(item.friendId) > -1}}"></view>
</view>
</view>
</block>
<view
class="ic-btn {{selectedIds.length === 0 ? 'is-disabled' : ''}}"
hover-class="ic-btn-hover"
bind:tap="onSendInvite"
>发送邀请({{selectedIds.length}})</view>
</view>
<view wx:else class="ic-result">
<view class="ic-result-mark"><view class="ic-result-mark-inner"></view></view>
<view class="ic-result-title">邀请已发送</view>
<view class="ic-result-desc">已邀请 {{invitedCount}} 位好友,{{acceptedCount}} 人已接受</view>
<view class="ic-friend-list">
<view class="ic-friend" wx:for="{{friends}}" wx:key="friendId" wx:if="{{item.status !== 'uninvited'}}">
<view class="ic-avatar">{{item.name.slice(0, 1)}}</view>
<view class="ic-friend-info">
<text class="ic-friend-name">{{item.name}}</text>
<text class="ic-friend-status is-{{item.status}}">{{item.statusText}}</text>
</view>
</view>
</view>
</view>
</view>
/* ratio=4:3 邀请好友卡片
* 色源:app.json navigationBarBackgroundColor #5C3A21 + window.backgroundColor #FFF8F0
* 暗黑:浅色降明度
*/
.ic-card {
background: #FFF8F0;
border-radius: 1.07vw;
padding: 3.2vw;
box-sizing: border-box;
overflow: hidden;
}
.ic-title {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
text-align: center;
}
.ic-hint {
font-size: 3.47vw;
color: rgba(0,0,0,0.45);
text-align: center;
margin-top: 1.07vw;
margin-bottom: 3.2vw;
}
.ic-empty {
padding: 6.4vw;
text-align: center;
background: #FFFFFF;
border-radius: 1.07vw;
}
.ic-empty-text {
font-size: 4vw;
color: rgba(0,0,0,0.45);
}
.ic-friend {
display: flex;
align-items: center;
padding: 2.67vw 3.2vw;
background: #FFFFFF;
border-radius: 1.07vw;
margin-bottom: 2.13vw;
}
.ic-friend.is-selected {
background: #F5F0EB;
}
.ic-avatar {
width: 9.6vw;
height: 9.6vw;
line-height: 9.6vw;
text-align: center;
font-size: 3.73vw;
font-weight: 500;
color: #FFFFFF;
background: #5C3A21;
border-radius: 50%;
flex-shrink: 0;
}
.ic-friend-info {
flex: 1;
margin-left: 2.67vw;
min-width: 0;
}
.ic-friend-name {
display: block;
font-size: 4vw;
font-weight: 500;
color: rgba(0,0,0,0.9);
}
.ic-friend-status {
display: block;
font-size: 3.2vw;
color: rgba(0,0,0,0.45);
margin-top: 0.53vw;
}
.ic-friend-status.is-accepted { color: #07C160; }
.ic-friend-status.is-declined { color: #EE0A24; }
.ic-friend-status.is-organizer { color: #5C3A21; }
.ic-check {
width: 5.33vw;
height: 5.33vw;
background: #F5F0EB;
border-radius: 50%;
flex-shrink: 0;
position: relative;
}
.ic-check.is-checked {
background: #5C3A21;
display: flex;
align-items: center;
justify-content: center;
}
.ic-check-inner {
width: 2.13vw;
height: 1.07vw;
border-left: 0.4vw solid white;
border-bottom: 0.4vw solid white;
transform: rotate(-45deg);
margin-bottom: 0.27vw;
}
.ic-btn {
margin-top: 3.2vw;
height: 10.67vw;
line-height: 10.67vw;
text-align: center;
font-size: 4vw;
font-weight: 500;
color: #FFFFFF;
background: #5C3A21;
border-radius: 1.07vw;
}
.ic-btn.is-disabled {
opacity: 0.35;
}
.ic-btn-hover {
opacity: 0.85;
}
.ic-result {
text-align: center;
}
.ic-result-mark {
width: 12.8vw;
height: 12.8vw;
margin: 2.13vw auto 2.13vw;
background: #07C160;
border-radius: 50%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.ic-result-mark-inner {
width: 4.27vw;
height: 2.13vw;
border-left: 0.53vw solid white;
border-bottom: 0.53vw solid white;
transform: rotate(-45deg);
margin-bottom: 0.53vw;
}
.ic-result-title {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
}
.ic-result-desc {
font-size: 3.47vw;
color: rgba(0,0,0,0.45);
margin-top: 1.07vw;
margin-bottom: 4.27vw;
}
.ic-friend-list {
text-align: left;
}
@media (prefers-color-scheme: dark) {
.ic-card { background: #2A1E16; }
.ic-title, .ic-result-title { color: rgba(255,255,255,0.9); }
.ic-hint, .ic-result-desc, .ic-empty-text { color: rgba(255,255,255,0.45); }
.ic-empty { background: #3C2E24; }
.ic-friend { background: #3C2E24; }
.ic-friend.is-selected { background: rgba(92,58,33,0.4); }
.ic-friend-name { color: rgba(255,255,255,0.9); }
.ic-friend-status { color: rgba(255,255,255,0.45); }
.ic-friend-status.is-accepted { color: #30D158; }
.ic-friend-status.is-declined { color: #FF453A; }
.ic-friend-status.is-organizer { color: #E8A87C; }
.ic-check { background: rgba(255,255,255,0.12); }
.ic-check.is-checked { background: #8B5E3C; }
}
Component({
data: {
partyId: '',
theme: '',
date: '',
time: '',
location: '',
guestCount: 0,
status: '',
createTime: '',
inviteCode: '',
isCreated: false
},
lifetimes: {
created() {
console.info('[ai-mode] party-create-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] party-create-card 收到 Result:', JSON.stringify(sc))
if (sc.partyId) {
this.setData({
partyId: sc.partyId,
theme: sc.theme || '',
date: sc.date || '',
time: sc.time || '',
location: sc.location || '',
guestCount: sc.guestCount || 0,
status: sc.status || '',
createTime: sc.createTime || '',
inviteCode: sc.inviteCode || '',
isCreated: true
})
}
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] party-create-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] party-create-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] party-create-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] party-create-card overflow monitor=on')
}
},
methods: {
onEditField(e) {
const field = e.currentTarget.dataset.field
const fieldLabels = {
theme: '活动主题',
date: '日期',
time: '时间',
location: '地点',
guestCount: '预计人数'
}
const currentValue = this.data[field] || '未填写'
const label = fieldLabels[field] || field
console.info(`[ai-mode] party-create-card 编辑字段 ${label},当前值:${currentValue}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: `修改聚会信息:${label}(当前:${currentValue})` },
{ type: 'api/call', data: { name: 'editPartyField', arguments: { field, label, currentValue } } }
]
})
},
onCreate() {
const { theme, date, time, location, guestCount } = this.data
const args = {}
if (theme) args.title = theme
if (date) args.date = date
if (time) args.time = time
if (location) args.location = location
console.info(`[ai-mode] party-create-card send api/call name=createParty args=${JSON.stringify(args)}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: theme ? `创建聚会:${theme}` : '创建聚会' },
{ type: 'api/call', data: { name: 'createParty', arguments: args } }
]
})
},
onGetRecommendations() {
console.info('[ai-mode] party-create-card send api/call name=getRecommendations')
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '看看有什么推荐场所' },
{ type: 'api/call', data: { name: 'getRecommendations', arguments: {} } }
]
})
},
onInviteFriends() {
const { partyId } = this.data
if (!partyId) return
console.info(`[ai-mode] party-create-card send api/call name=inviteFriends args=${JSON.stringify({ partyId })}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '邀请好友' },
{ type: 'api/call', data: { name: 'inviteFriends', arguments: { partyId } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="pc-card">
<view wx:if="{{!isCreated}}" class="pc-form">
<view class="pc-title">创建聚会</view>
<view class="pc-field" bind:tap="onEditField" data-field="theme">
<text class="pc-label">活动主题</text>
<text class="pc-value {{theme ? '' : 'pc-placeholder'}}">{{theme || '例如:生日派对、同学聚会'}}</text>
</view>
<view class="pc-field" bind:tap="onEditField" data-field="date">
<text class="pc-label">日期</text>
<text class="pc-value {{date ? '' : 'pc-placeholder'}}">{{date || '例如:本周六、6月15日'}}</text>
</view>
<view class="pc-field" bind:tap="onEditField" data-field="time">
<text class="pc-label">时间</text>
<text class="pc-value {{time ? '' : 'pc-placeholder'}}">{{time || '例如:晚上7点、下午3点'}}</text>
</view>
<view class="pc-field" bind:tap="onEditField" data-field="location">
<text class="pc-label">地点</text>
<text class="pc-value {{location ? '' : 'pc-placeholder'}}">{{location || '例如:望京、国贸'}}</text>
</view>
<view class="pc-field" bind:tap="onEditField" data-field="guestCount">
<text class="pc-label">预计人数</text>
<text class="pc-value {{guestCount ? '' : 'pc-placeholder'}}">{{guestCount || '例如:8'}}</text>
</view>
<view class="pc-btn" hover-class="pc-btn-hover" bind:tap="onCreate">创建聚会</view>
</view>
<view wx:else class="pc-success">
<view class="pc-success-mark"><view class="pc-success-mark-inner"></view></view>
<view class="pc-success-title">聚会创建成功!</view>
<view class="pc-detail-card">
<view class="pc-detail-row">
<text class="pc-detail-label">主题</text>
<text class="pc-detail-value">{{theme}}</text>
</view>
<view class="pc-detail-row" wx:if="{{date}}">
<text class="pc-detail-label">日期</text>
<text class="pc-detail-value">{{date}}</text>
</view>
<view class="pc-detail-row" wx:if="{{time}}">
<text class="pc-detail-label">时间</text>
<text class="pc-detail-value">{{time}}</text>
</view>
<view class="pc-detail-row" wx:if="{{location}}">
<text class="pc-detail-label">地点</text>
<text class="pc-detail-value">{{location}}</text>
</view>
<view class="pc-detail-row" wx:if="{{guestCount}}">
<text class="pc-detail-label">人数</text>
<text class="pc-detail-value">{{guestCount}} 人</text>
</view>
<view class="pc-detail-row">
<text class="pc-detail-label">邀请码</text>
<text class="pc-detail-value pc-code">{{inviteCode}}</text>
</view>
</view>
<view class="pc-actions">
<view class="pc-btn pc-btn-secondary" hover-class="pc-btn-hover" bind:tap="onGetRecommendations">推荐场所</view>
<view class="pc-btn" hover-class="pc-btn-hover" bind:tap="onInviteFriends">邀请好友</view>
</view>
</view>
</view>
/* ratio=1:1 聚会创建卡片
* 色源:app.json navigationBarBackgroundColor #5C3A21 + window.backgroundColor #FFF8F0
* 暗黑:浅色降明度
*/
.pc-card {
background: #FFF8F0;
border-radius: 1.07vw;
padding: 3.2vw;
box-sizing: border-box;
overflow: hidden;
}
.pc-title {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
text-align: center;
margin-bottom: 4.27vw;
}
.pc-field {
margin-bottom: 3.2vw;
}
.pc-label {
display: block;
font-size: 3.47vw;
color: rgba(0,0,0,0.45);
margin-bottom: 1.07vw;
}
.pc-value {
display: block;
width: 100%;
height: 10.67vw;
line-height: 10.67vw;
padding: 0 3.2vw;
font-size: 4vw;
color: rgba(0,0,0,0.9);
background: #FFFFFF;
border-radius: 1.07vw;
box-sizing: border-box;
}
.pc-placeholder {
color: rgba(0,0,0,0.3);
}
.pc-btn {
margin-top: 4.27vw;
height: 10.67vw;
line-height: 10.67vw;
text-align: center;
font-size: 4vw;
font-weight: 500;
color: #FFFFFF;
background: #5C3A21;
border-radius: 1.07vw;
}
.pc-btn-secondary {
background: #FFFFFF;
color: #5C3A21;
}
.pc-btn-hover {
opacity: 0.85;
}
.pc-success {
text-align: center;
}
.pc-success-mark {
width: 12.8vw;
height: 12.8vw;
margin: 2.13vw auto 2.13vw;
background: #07C160;
border-radius: 50%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.pc-success-mark-inner {
width: 4.27vw;
height: 2.13vw;
border-left: 0.53vw solid white;
border-bottom: 0.53vw solid white;
transform: rotate(-45deg);
margin-bottom: 0.53vw;
}
.pc-success-title {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
margin-bottom: 4.27vw;
}
.pc-detail-card {
background: #FFFFFF;
border-radius: 1.07vw;
padding: 3.2vw;
margin-bottom: 4.27vw;
text-align: left;
}
.pc-detail-row {
display: flex;
align-items: center;
padding: 2.13vw 0;
}
.pc-detail-label {
width: 16vw;
font-size: 3.47vw;
color: rgba(0,0,0,0.45);
flex-shrink: 0;
}
.pc-detail-value {
flex: 1;
font-size: 4vw;
color: rgba(0,0,0,0.9);
min-width: 0;
}
.pc-code {
font-family: monospace;
color: #5C3A21;
font-weight: 600;
}
.pc-actions {
display: flex;
gap: 3.2vw;
}
.pc-actions .pc-btn {
flex: 1;
margin-top: 0;
}
@media (prefers-color-scheme: dark) {
.pc-card { background: #2A1E16; }
.pc-title, .pc-success-title { color: rgba(255,255,255,0.9); }
.pc-label { color: rgba(255,255,255,0.45); }
.pc-value { background: #3C2E24; color: rgba(255,255,255,0.9); }
.pc-placeholder { color: rgba(255,255,255,0.3); }
.pc-detail-card { background: #3C2E24; }
.pc-detail-label { color: rgba(255,255,255,0.45); }
.pc-detail-value { color: rgba(255,255,255,0.9); }
.pc-code { color: #E8A87C; }
.pc-btn-secondary { background: #3C2E24; color: #E8A87C; }
}
Component({
data: {
partyId: '',
theme: '',
date: '',
time: '',
location: '',
guestCount: 0,
status: '',
statusText: '',
inviteCode: '',
createTime: '',
recommendation: null,
friends: []
},
lifetimes: {
created() {
console.info('[ai-mode] party-detail-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] party-detail-card 收到 Result:', JSON.stringify(sc))
this.setData({
partyId: sc.partyId || '',
theme: sc.theme || '',
date: sc.date || '',
time: sc.time || '',
location: sc.location || '',
guestCount: sc.guestCount || 0,
status: sc.status || '',
statusText: sc.statusText || '',
inviteCode: sc.inviteCode || '',
createTime: sc.createTime || '',
recommendation: sc.recommendation || null,
friends: sc.friends || []
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] party-detail-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] party-detail-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] party-detail-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] party-detail-card overflow monitor=on')
}
},
methods: {
onInvite() {
const { partyId } = this.data
if (!partyId) return
console.info(`[ai-mode] party-detail-card send api/call name=inviteFriends args=${JSON.stringify({ partyId })}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: '邀请好友' },
{ type: 'api/call', data: { name: 'inviteFriends', arguments: { partyId } } }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="pdc-card">
<view class="pdc-banner">
<view class="pdc-theme">{{theme}}</view>
<view class="pdc-status">{{statusText}}</view>
</view>
<view class="pdc-section">
<view class="pdc-section-title">活动信息</view>
<view class="pdc-row" wx:if="{{date}}">
<view class="pdc-icon-row">
<view class="pdc-icon-calendar"></view>
<text class="pdc-label">日期</text>
</view>
<text class="pdc-value">{{date}}</text>
</view>
<view class="pdc-row" wx:if="{{time}}">
<view class="pdc-icon-row">
<view class="pdc-icon-clock"></view>
<text class="pdc-label">时间</text>
</view>
<text class="pdc-value">{{time}}</text>
</view>
<view class="pdc-row" wx:if="{{location}}">
<view class="pdc-icon-row">
<view class="pdc-icon-location"></view>
<text class="pdc-label">地点</text>
</view>
<text class="pdc-value">{{location}}</text>
</view>
<view class="pdc-row" wx:if="{{guestCount}}">
<view class="pdc-icon-row">
<view class="pdc-icon-people"></view>
<text class="pdc-label">人数</text>
</view>
<text class="pdc-value">{{guestCount}} 人</text>
</view>
<view class="pdc-row">
<view class="pdc-icon-row">
<view class="pdc-icon-key"></view>
<text class="pdc-label">邀请码</text>
</view>
<text class="pdc-value pdc-code">{{inviteCode}}</text>
</view>
</view>
<view class="pdc-section" wx:if="{{recommendation}}">
<view class="pdc-section-title">选定场所</view>
<view class="pdc-place">
<view class="pdc-place-name">{{recommendation.name}}</view>
<view class="pdc-place-meta">{{recommendation.typeText}} · {{recommendation.address}}</view>
</view>
</view>
<view class="pdc-section" wx:if="{{friends.length}}">
<view class="pdc-section-title">好友动态</view>
<view class="pdc-friend" wx:for="{{friends}}" wx:key="friendId">
<view class="pdc-avatar">{{item.name.slice(0, 1)}}</view>
<view class="pdc-friend-info">
<text class="pdc-friend-name">{{item.name}}</text>
</view>
<text class="pdc-friend-status is-{{item.status}}">{{item.statusText}}</text>
</view>
</view>
<view class="pdc-btn" hover-class="pdc-btn-hover" bind:tap="onInvite">邀请好友</view>
</view>
/* ratio=4:3 聚会详情卡片
* 色源:app.json navigationBarBackgroundColor #5C3A21 + window.backgroundColor #FFF8F0
* 暗黑:浅色降明度
*/
.pdc-card {
background: #FFF8F0;
border-radius: 1.07vw;
padding: 3.2vw;
box-sizing: border-box;
overflow: hidden;
}
.pdc-banner {
text-align: center;
padding: 4.27vw 0;
background: #F5F0EB;
border-radius: 1.07vw;
margin-bottom: 3.2vw;
}
.pdc-theme {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
}
.pdc-status {
display: inline-block;
margin-top: 1.6vw;
padding: 0.53vw 3.2vw;
font-size: 3.2vw;
color: #FFFFFF;
background: #5C3A21;
border-radius: 1.07vw;
}
.pdc-section {
margin-bottom: 3.2vw;
}
.pdc-section-title {
font-size: 3.73vw;
font-weight: 600;
color: rgba(0,0,0,0.45);
margin-bottom: 2.13vw;
padding-left: 1.07vw;
}
.pdc-row {
display: flex;
align-items: center;
padding: 2.13vw 3.2vw;
background: #FFFFFF;
border-radius: 1.07vw;
margin-bottom: 1.07vw;
}
.pdc-icon-row {
display: flex;
align-items: center;
gap: 1.6vw;
width: 21.33vw;
flex-shrink: 0;
}
.pdc-icon-calendar, .pdc-icon-clock, .pdc-icon-location, .pdc-icon-people, .pdc-icon-key {
width: 3.73vw;
height: 3.73vw;
background: #5C3A21;
mask-size: contain;
mask-repeat: no-repeat;
-webkit-mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
.pdc-icon-calendar { mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2' y='4' width='16' height='14' rx='2' fill='%235C3A21'/%3E%3Cpath d='M2 8h16M6 2v3M14 2v3' stroke='%235C3A21' stroke-width='1.5'/%3E%3C/svg%3E"); -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2' y='4' width='16' height='14' rx='2' fill='%235C3A21'/%3E%3Cpath d='M2 8h16M6 2v3M14 2v3' stroke='%235C3A21' stroke-width='1.5'/%3E%3C/svg%3E"); }
.pdc-icon-clock { mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='10' cy='10' r='8' fill='%235C3A21'/%3E%3Cpath d='M10 6v5l4 2' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E"); -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='10' cy='10' r='8' fill='%235C3A21'/%3E%3Cpath d='M10 6v5l4 2' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E"); }
.pdc-icon-location { mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 1C7.2 1 5 3.2 5 6c0 3.8 5 9 5 9s5-5.2 5-9c0-2.8-2.2-5-5-5z' fill='%235C3A21'/%3E%3Ccircle cx='10' cy='6' r='2' fill='white'/%3E%3C/svg%3E"); -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 1C7.2 1 5 3.2 5 6c0 3.8 5 9 5 9s5-5.2 5-9c0-2.8-2.2-5-5-5z' fill='%235C3A21'/%3E%3Ccircle cx='10' cy='6' r='2' fill='white'/%3E%3C/svg%3E"); }
.pdc-icon-people { mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='7' cy='6' r='3' fill='%235C3A21'/%3E%3Ccircle cx='14' cy='6' r='2.5' fill='%235C3A21'/%3E%3Cpath d='M1 17c0-3.3 2.7-6 6-6s6 2.7 6 6M11 17c0-2.2 1.8-4 4-4s4 1.8 4 4' fill='none' stroke='%235C3A21' stroke-width='1.5'/%3E%3C/svg%3E"); -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='7' cy='6' r='3' fill='%235C3A21'/%3E%3Ccircle cx='14' cy='6' r='2.5' fill='%235C3A21'/%3E%3Cpath d='M1 17c0-3.3 2.7-6 6-6s6 2.7 6 6M11 17c0-2.2 1.8-4 4-4s4 1.8 4 4' fill='none' stroke='%235C3A21' stroke-width='1.5'/%3E%3C/svg%3E"); }
.pdc-icon-key { mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='8' cy='12' r='5' fill='none' stroke='%235C3A21' stroke-width='2'/%3E%3Cpath d='M11 9l6-6M15 5l2-2' stroke='%235C3A21' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='8' cy='12' r='5' fill='none' stroke='%235C3A21' stroke-width='2'/%3E%3Cpath d='M11 9l6-6M15 5l2-2' stroke='%235C3A21' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E"); }
.pdc-label {
font-size: 3.73vw;
color: rgba(0,0,0,0.45);
}
.pdc-value {
flex: 1;
font-size: 4vw;
color: rgba(0,0,0,0.9);
min-width: 0;
text-align: right;
}
.pdc-code {
font-family: monospace;
color: #5C3A21;
font-weight: 600;
letter-spacing: 0.27vw;
}
.pdc-place {
padding: 3.2vw;
background: #FFFFFF;
border-radius: 1.07vw;
}
.pdc-place-name {
font-size: 4vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
}
.pdc-place-meta {
font-size: 3.2vw;
color: rgba(0,0,0,0.45);
margin-top: 1.07vw;
}
.pdc-friend {
display: flex;
align-items: center;
padding: 2.13vw 3.2vw;
background: #FFFFFF;
border-radius: 1.07vw;
margin-bottom: 1.07vw;
}
.pdc-avatar {
width: 8.53vw;
height: 8.53vw;
line-height: 8.53vw;
text-align: center;
font-size: 3.47vw;
font-weight: 500;
color: #FFFFFF;
background: #5C3A21;
border-radius: 50%;
flex-shrink: 0;
}
.pdc-friend-info {
flex: 1;
margin-left: 2.67vw;
min-width: 0;
}
.pdc-friend-name {
font-size: 3.73vw;
color: rgba(0,0,0,0.9);
}
.pdc-friend-status {
font-size: 3.2vw;
font-weight: 500;
}
.pdc-friend-status.is-organizer { color: #5C3A21; }
.pdc-friend-status.is-accepted { color: #07C160; }
.pdc-friend-status.is-pending { color: rgba(0,0,0,0.45); }
.pdc-friend-status.is-declined { color: #EE0A24; }
.pdc-btn {
margin-top: 3.2vw;
height: 10.67vw;
line-height: 10.67vw;
text-align: center;
font-size: 4vw;
font-weight: 500;
color: #FFFFFF;
background: #5C3A21;
border-radius: 1.07vw;
}
.pdc-btn-hover {
opacity: 0.85;
}
@media (prefers-color-scheme: dark) {
.pdc-card { background: #2A1E16; }
.pdc-banner { background: rgba(92,58,33,0.3); }
.pdc-theme { color: rgba(255,255,255,0.9); }
.pdc-status { background: #8B5E3C; }
.pdc-section-title { color: rgba(255,255,255,0.45); }
.pdc-row { background: #3C2E24; }
.pdc-label { color: rgba(255,255,255,0.45); }
.pdc-value { color: rgba(255,255,255,0.9); }
.pdc-code { color: #E8A87C; }
.pdc-icon-calendar, .pdc-icon-clock, .pdc-icon-location, .pdc-icon-people, .pdc-icon-key { background: #E8A87C; }
.pdc-place { background: #3C2E24; }
.pdc-place-name { color: rgba(255,255,255,0.9); }
.pdc-place-meta { color: rgba(255,255,255,0.45); }
.pdc-friend { background: #3C2E24; }
.pdc-friend-name { color: rgba(255,255,255,0.9); }
.pdc-friend-status.is-pending { color: rgba(255,255,255,0.45); }
.pdc-avatar { background: #8B5E3C; }
}
Component({
data: {
items: [],
total: 0,
type: '',
keyword: '',
activeFilter: '',
filterOptions: [
{ value: '', label: '全部' },
{ value: 'restaurant', label: '餐厅' },
{ value: 'party_house', label: '轰趴馆' },
{ value: 'ktv', label: 'KTV' },
{ value: 'outdoor', label: '户外' }
]
},
lifetimes: {
created() {
console.info('[ai-mode] recommend-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] recommend-card 收到 Result:', JSON.stringify(sc))
this.setData({
items: sc.items || [],
total: sc.total || 0,
type: sc.type || '',
keyword: sc.keyword || '',
activeFilter: sc.type || ''
})
})
const viewCtx = wx.modelContext.getViewContext(this)
try {
const { width, minHeight, maxHeight } = viewCtx.getDimensions()
console.info(`[ai-mode] recommend-card dimensions width=${width} minHeight=${minHeight} maxHeight=${maxHeight}`)
} catch (e) {
console.info('[ai-mode] recommend-card getDimensions skipped:', e.message)
}
viewCtx.on(NotificationType.Overflow, (data) => {
const overflowed = !!(data && data.overflowHeight > 0)
console.info(`[ai-mode] recommend-card overflow overflowed=${overflowed} data=${JSON.stringify(data)}`)
})
console.info('[ai-mode] recommend-card overflow monitor=on')
}
},
methods: {
onFilterChange(e) {
const type = e.currentTarget.dataset.value
this.setData({ activeFilter: type })
console.info(`[ai-mode] recommend-card send api/call name=getRecommendations args=${JSON.stringify({ type })}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: type ? `查看${type}类推荐` : '查看全部推荐' },
{ type: 'api/call', data: { name: 'getRecommendations', arguments: { type } } }
]
})
},
onSelect(e) {
const { id, name } = e.currentTarget.dataset
console.info(`[ai-mode] recommend-card select item id=${id} name=${name}`)
wx.modelContext.getContext(this).sendFollowUpMessage({
content: [
{ type: 'text', text: `选择${name}` }
]
})
}
}
})
{
"component": true,
"usingComponents": {}
}
<view class="rc-card">
<view class="rc-header">
<text class="rc-title">聚会推荐</text>
</view>
<view class="rc-filters">
<view
class="rc-filter {{activeFilter === item.value ? 'is-active' : ''}}"
wx:for="{{filterOptions}}"
wx:key="value"
data-value="{{item.value}}"
bind:tap="onFilterChange"
>{{item.label}}</view>
</view>
<view wx:if="{{!items.length}}" class="rc-empty">
<view class="rc-empty-title">暂无推荐</view>
<view class="rc-empty-desc">{{keyword ? '换一个关键词试试' : '请选择其他类型'}}</view>
</view>
<scroll-view class="rc-list" scroll-x="{{true}}">
<view class="rc-list-inner">
<view class="rc-item" wx:for="{{items}}" wx:key="id" data-id="{{item.id}}" data-name="{{item.name}}" bind:tap="onSelect">
<view class="rc-item-top">
<view class="rc-item-name">{{item.name}}</view>
<view class="rc-item-type">{{item.typeText}}</view>
</view>
<view class="rc-item-meta">
<view class="rc-rating">
<view class="rc-star"></view>
<text>{{item.rating}}</text>
</view>
<text class="rc-price">{{item.priceLevel}}</text>
<text wx:if="{{item.distance}}" class="rc-distance">{{item.distance}}</text>
<text class="rc-capacity">{{item.capacity}}</text>
</view>
<view class="rc-address">{{item.address}}</view>
<view class="rc-tags">
<text class="rc-tag" wx:for="{{item.tags}}" wx:key="*this">{{item}}</text>
</view>
</view>
</view>
</scroll-view>
</view>
/* ratio=1:1 聚会推荐列表卡片
* 色源:app.json navigationBarBackgroundColor #5C3A21 + window.backgroundColor #FFF8F0
* 暗黑:源项目无 darkmode → 浅色降明度
*/
.rc-card {
background: #FFF8F0;
border-radius: 1.07vw;
padding: 3.2vw;
box-sizing: border-box;
overflow: hidden;
}
.rc-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2.13vw;
}
.rc-title {
font-size: 4.53vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
}
.rc-filters {
display: flex;
gap: 2.13vw;
margin-bottom: 2.13vw;
overflow-x: auto;
white-space: nowrap;
}
.rc-filter {
flex-shrink: 0;
padding: 1.07vw 2.67vw;
font-size: 3.2vw;
color: rgba(0,0,0,0.45);
background: #FFFFFF;
border-radius: 1.07vw;
}
.rc-filter.is-active {
color: #FFFFFF;
background: #5C3A21;
}
.rc-empty {
padding: 6.4vw 4.27vw;
text-align: center;
background: #FFFFFF;
border-radius: 1.07vw;
}
.rc-empty-title {
font-size: 4vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
}
.rc-empty-desc {
margin-top: 1.07vw;
font-size: 3.2vw;
color: rgba(0,0,0,0.3);
}
.rc-list {
overflow: hidden;
}
.rc-list-inner {
display: flex;
flex-direction: column;
}
.rc-item {
margin-top: 2.13vw;
padding: 3.2vw;
background: #FFFFFF;
border-radius: 1.07vw;
}
.rc-item-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.rc-item-name {
flex: 1;
min-width: 0;
font-size: 4vw;
font-weight: 600;
color: rgba(0,0,0,0.9);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rc-item-type {
font-size: 3.2vw;
color: #5C3A21;
background: #FFF8F0;
padding: 0.53vw 2.13vw;
border-radius: 1.07vw;
margin-left: 2.13vw;
flex-shrink: 0;
}
.rc-item-meta {
display: flex;
align-items: center;
gap: 2.13vw;
margin-top: 2.13vw;
}
.rc-rating {
display: flex;
align-items: center;
gap: 0.53vw;
font-size: 3.2vw;
color: rgba(0,0,0,0.45);
}
.rc-star {
width: 3.2vw;
height: 3.2vw;
background: #E8A87C;
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}
.rc-price, .rc-distance, .rc-capacity {
font-size: 3.2vw;
color: rgba(0,0,0,0.45);
}
.rc-address {
font-size: 3.2vw;
color: rgba(0,0,0,0.3);
margin-top: 1.07vw;
}
.rc-tags {
display: flex;
flex-wrap: wrap;
gap: 1.07vw;
margin-top: 2.13vw;
}
.rc-tag {
font-size: 2.93vw;
color: rgba(0,0,0,0.45);
background: #F5F0EB;
padding: 0.53vw 2.13vw;
border-radius: 1.07vw;
}
@media (prefers-color-scheme: dark) {
.rc-card { background: #2A1E16; }
.rc-title { color: rgba(255,255,255,0.9); }
.rc-filter {
background: #3C2E24;
color: rgba(255,255,255,0.45);
}
.rc-filter.is-active { background: #8B5E3C; color: rgba(255,255,255,0.9); }
.rc-empty { background: #3C2E24; }
.rc-empty-title { color: rgba(255,255,255,0.9); }
.rc-empty-desc { color: rgba(255,255,255,0.3); }
.rc-item { background: #3C2E24; }
.rc-item-name { color: rgba(255,255,255,0.9); }
.rc-item-type { color: #E8A87C; background: rgba(92,58,33,0.4); }
.rc-rating, .rc-price, .rc-distance, .rc-capacity { color: rgba(255,255,255,0.45); }
.rc-address { color: rgba(255,255,255,0.3); }
.rc-tag { color: rgba(255,255,255,0.45); background: rgba(255,255,255,0.08); }
}
const recommendations = [
{
id: 'R001',
name: '花园餐厅·望京店',
type: 'restaurant',
typeText: '餐厅',
rating: 4.7,
priceLevel: '中档',
address: '北京市朝阳区望京SOHO T2 3F',
distance: '320m',
capacity: '4-20人',
tags: ['精致料理', '包间', '适合聚会'],
imageUrl: '',
keywords: ['望京', '餐厅', '聚餐', '包间']
},
{
id: 'R002',
name: '老北京涮肉馆',
type: 'restaurant',
typeText: '餐厅',
rating: 4.5,
priceLevel: '平价',
address: '北京市东城区东四北大街128号',
distance: '2.1km',
capacity: '2-12人',
tags: ['火锅', '老字号', '热闹'],
imageUrl: '',
keywords: ['东城', '火锅', '涮肉', '聚餐']
},
{
id: 'R003',
name: '轰趴馆·欢乐空间',
type: 'party_house',
typeText: '轰趴馆',
rating: 4.8,
priceLevel: '中高档',
address: '北京市朝阳区建国路88号SOHO现代城B1',
distance: '1.5km',
capacity: '8-30人',
tags: ['KTV', '桌游', '台球', '剧本杀', '适合团建'],
imageUrl: '',
keywords: ['轰趴', '团建', '桌游', '剧本杀']
},
{
id: 'R004',
name: '唱响KTV·三里屯店',
type: 'ktv',
typeText: 'KTV',
rating: 4.4,
priceLevel: '中档',
address: '北京市朝阳区三里屯太古里南区B1',
distance: '2.4km',
capacity: '2-20人',
tags: ['豪华包间', '海量曲库', '酒水畅饮'],
imageUrl: '',
keywords: ['KTV', '唱歌', '三里屯', '娱乐']
},
{
id: 'R005',
name: '阳光露营基地',
type: 'outdoor',
typeText: '户外',
rating: 4.6,
priceLevel: '中档',
address: '北京市怀柔区雁栖湖路18号',
distance: '35km',
capacity: '10-50人',
tags: ['烧烤', '露营', '篝火', '亲近自然'],
imageUrl: '',
keywords: ['户外', '露营', '烧烤', '怀柔', '雁栖湖']
},
{
id: 'R006',
name: '日料·樱花亭',
type: 'restaurant',
typeText: '餐厅',
rating: 4.3,
priceLevel: '高档',
address: '北京市朝阳区国贸商城北区4F',
distance: '1.8km',
capacity: '2-8人',
tags: ['日料', '刺身', '私密包间', '约会'],
imageUrl: '',
keywords: ['日料', '国贸', '精致', '包间']
}
]
const friends = [
{
friendId: 'F001',
name: '小明',
avatar: '',
phone: '138****1234'
},
{
friendId: 'F002',
name: '小红',
avatar: '',
phone: '139****5678'
},
{
friendId: 'F003',
name: '大伟',
avatar: '',
phone: '137****9012'
},
{
friendId: 'F004',
name: '莉莉',
avatar: '',
phone: '136****3456'
},
{
friendId: 'F005',
name: '阿强',
avatar: '',
phone: '135****7890'
}
]
const parties = [
{
partyId: 'P001',
theme: '小明生日派对',
date: '2026-06-15',
time: '18:00',
location: '望京',
guestCount: 8,
status: 'planning',
statusText: '筹备中',
inviteCode: 'PARTY-A1B2',
createTime: '2026-06-01T10:00:00.000Z',
recommendationId: 'R001',
recommendation: {
id: 'R001',
name: '花园餐厅·望京店',
typeText: '餐厅',
address: '北京市朝阳区望京SOHO T2 3F'
},
friends: [
{ friendId: 'F001', name: '小明', avatar: '', status: 'organizer', statusText: '组织者' },
{ friendId: 'F002', name: '小红', avatar: '', status: 'accepted', statusText: '已接受' },
{ friendId: 'F003', name: '大伟', avatar: '', status: 'accepted', statusText: '已接受' },
{ friendId: 'F004', name: '莉莉', avatar: '', status: 'pending', statusText: '待回复' },
{ friendId: 'F005', name: '阿强', avatar: '', status: 'pending', statusText: '待回复' }
]
},
{
partyId: 'P002',
theme: '部门团建聚餐',
date: '2026-06-20',
time: '11:30',
location: '国贸',
guestCount: 12,
status: 'planning',
statusText: '筹备中',
inviteCode: 'PARTY-C3D4',
createTime: '2026-06-05T14:30:00.000Z',
recommendationId: 'R003',
recommendation: {
id: 'R003',
name: '轰趴馆·欢乐空间',
typeText: '轰趴馆',
address: '北京市朝阳区建国路88号SOHO现代城B1'
},
friends: [
{ friendId: 'F003', name: '大伟', avatar: '', status: 'organizer', statusText: '组织者' },
{ friendId: 'F001', name: '小明', avatar: '', status: 'accepted', statusText: '已接受' },
{ friendId: 'F005', name: '阿强', avatar: '', status: 'accepted', statusText: '已接受' },
{ friendId: 'F002', name: '小红', avatar: '', status: 'pending', statusText: '待回复' },
{ friendId: 'F004', name: '莉莉', avatar: '', status: 'declined', statusText: '已拒绝' }
]
}
]
module.exports = {
recommendations,
friends,
parties
}
{
"collections": [
{
"name": "parties",
"description": "聚会活动集合",
"fields": [
{ "name": "partyId", "type": "string", "description": "聚会ID" },
{ "name": "title", "type": "string", "description": "聚会标题/主题" },
{ "name": "date", "type": "string", "description": "聚会日期" },
{ "name": "location", "type": "string", "description": "聚会地点" },
{ "name": "invitees", "type": "array", "description": "受邀人列表" },
{ "name": "status", "type": "string", "description": "聚会状态" },
{ "name": "openid", "type": "string", "description": "用户openid" },
{ "name": "createdAt", "type": "date", "description": "创建时间" }
],
"indexes": [
{ "field": "openid", "unique": false }
]
}
]
}
// skills/party-skill/index.js
// 注册所有原子接口
const createParty = require('./apis/createParty.js')
const getRecommendations = require('./apis/getRecommendations.js')
const inviteFriends = require('./apis/inviteFriends.js')
const getPartyDetails = require('./apis/getPartyDetails.js')
function registerAPIs() {
const skill = wx.modelContext.createSkill('skills/party-skill')
skill.use(async (ctx, next) => {
try {
console.info('[ai-mode] [party-skill] middleware start name=', ctx.name)
await next()
console.info('[ai-mode] [party-skill] middleware finish name=', ctx.name)
} catch (err) {
console.error('[ai-mode] [party-skill] middleware error:', err.message)
throw err
}
})
skill.registerAPI('createParty', createParty)
skill.registerAPI('getRecommendations', getRecommendations)
skill.registerAPI('inviteFriends', inviteFriends)
skill.registerAPI('getPartyDetails', getPartyDetails)
console.info('[ai-mode] [party-skill] APIs registered via createSkill')
}
registerAPIs()
module.exports = {}
{
"apis": [
{
"name": "createParty",
"description": "创建聚会活动(业务对象:聚会创建卡片)。调用前置条件:用户表达了举办聚会的意图,需收集聚会名称、日期、时间等基本信息。用户提供的信息不完整时,可以引导用户补充或使用默认值。成功后返回聚会详情对象。【严禁场景】禁止在无 title/date/time 时创建聚会;禁止编造用户未提供的聚会信息。",
"_meta": {
"ui": {
"componentPath": "components/party-create-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "聚会名称。取值来源:用户原话中的活动主题(如『生日趴』『桌游局』)。用户未明确说明时可引导用户提供。"
},
"date": {
"type": "string",
"description": "聚会日期,格式 YYYY-MM-DD。取值来源:用户原话中的日期信息或通过上下文推断。"
},
"time": {
"type": "string",
"description": "聚会时间,格式 HH:mm。取值来源:用户原话中的时间信息。"
},
"location": {
"type": "string",
"description": "聚会地点名称。取值来源:用户指定或来自上游 getRecommendations 返回的推荐场地。可选字段。"
},
"type": {
"type": "string",
"description": "聚会类型,如生日聚会、朋友聚会、团建等。可选字段,默认『朋友聚会』。"
},
"description": {
"type": "string",
"description": "聚会描述或备注。可选字段。"
}
},
"required": ["title", "date", "time"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"party": {
"type": "object",
"description": "创建的聚会详情",
"properties": {
"partyId": { "type": "string", "description": "聚会唯一 ID" },
"title": { "type": "string", "description": "聚会名称" },
"date": { "type": "string", "description": "聚会日期" },
"time": { "type": "string", "description": "聚会时间" },
"location": { "type": "string", "description": "聚会地点" },
"type": { "type": "string", "description": "聚会类型" },
"hostName": { "type": "string", "description": "发起人" },
"guests": { "type": "array", "description": "受邀成员列表" },
"status": { "type": "string", "description": "聚会状态:planning / confirmed / ongoing / completed" },
"description": { "type": "string", "description": "聚会描述" },
"createdAt": { "type": "string", "description": "创建时间 ISO 字符串" }
},
"required": ["partyId", "title", "date", "time", "status"],
"additionalProperties": false
}
},
"required": ["party"],
"additionalProperties": false
}
},
{
"name": "getRecommendations",
"description": "获取聚会推荐(餐厅/场地/娱乐场所等)(业务对象:推荐列表卡片)。调用前置条件:用户需要聚会场地推荐、或对聚会地点没有明确想法时。可按关键词搜索或按类型筛选。成功后返回推荐列表。【严禁场景】禁止在用户已有明确聚会地点时调用;禁止编造推荐数据。",
"_meta": {
"ui": {
"componentPath": "components/recommend-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": "搜索关键词,如类型(餐厅/轰趴馆/KTV/户外)、菜系(日式/川菜)、地名(望京/国贸)。用户未提供关键词时返回全部推荐列表。"
},
"type": {
"type": "string",
"description": "类型筛选:餐厅 / 轰趴馆 / KTV / 户外 / 娱乐。可选字段。"
}
},
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"items": {
"type": "array",
"description": "推荐列表",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string", "description": "场地名称" },
"type": { "type": "string", "description": "场地类型" },
"tags": { "type": "array", "items": { "type": "string" }, "description": "标签列表" },
"rating": { "type": "number", "description": "评分" },
"priceLevel": { "type": "string", "description": "价格等级" },
"address": { "type": "string" },
"distance": { "type": "string", "description": "距离文案" },
"capacity": { "type": "string", "description": "容纳人数" },
"description": { "type": "string" },
"recommendReason": { "type": "string", "description": "推荐理由" }
},
"required": ["id", "name", "type", "rating"],
"additionalProperties": false
}
},
"total": { "type": "number" },
"keyword": { "type": "string" },
"type": { "type": "string" }
},
"required": ["items", "total", "keyword", "type"],
"additionalProperties": false
}
},
{
"name": "inviteFriends",
"description": "邀请朋友参加聚会(业务对象:邀请卡片)。调用前置条件:已有有效 partyId(来自 createParty 或 getPartyDetails 返回)。展示可邀请的朋友列表,用户可以勾选后发送邀请。【严禁场景】禁止在没有 partyId 时调用;禁止编造 partyId。上下文中无 partyId 时应先创建聚会。",
"_meta": {
"ui": {
"componentPath": "components/invite-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"partyId": {
"type": "string",
"description": "聚会唯一标识,必须来自上游 createParty 或 getPartyDetails 返回的 partyId 原值。【禁止编造】上下文中无 partyId 时应先创建聚会。"
},
"keyword": {
"type": "string",
"description": "搜索朋友关键词。用户可输入朋友姓名搜索。可选字段。"
}
},
"required": ["partyId"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"friends": {
"type": "array",
"description": "可邀请的朋友列表",
"items": {
"type": "object",
"properties": {
"id": { "type": "string", "description": "好友唯一 ID" },
"name": { "type": "string", "description": "好友名称" },
"status": { "type": "string", "description": "available / busy" },
"mutualFriends": { "type": "number", "description": "共同好友数" }
},
"required": ["id", "name", "status"],
"additionalProperties": false
}
},
"total": { "type": "number" },
"partyId": { "type": "string" }
},
"required": ["friends", "total", "partyId"],
"additionalProperties": false
}
},
{
"name": "getPartyDetails",
"description": "查看聚会详情(业务对象:聚会详情卡片)。调用前置条件:已有有效 partyId。展示聚会的完整信息,包括基本信息、地点、参与成员及状态等。【严禁场景】禁止在没有 partyId 时调用;禁止编造 partyId。上下文中无 partyId 时应先创建聚会。",
"_meta": {
"ui": {
"componentPath": "components/party-detail-card/index"
}
},
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"partyId": {
"type": "string",
"description": "聚会唯一标识,必须来自上游 createParty 返回的 partyId 原值。【禁止编造】上下文中无 partyId 时禁止填写本字段,应先创建聚会。"
}
},
"required": ["partyId"],
"additionalProperties": false
},
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"party": {
"type": "object",
"description": "聚会详情",
"properties": {
"partyId": { "type": "string" },
"title": { "type": "string" },
"date": { "type": "string" },
"time": { "type": "string" },
"location": { "type": "string" },
"address": { "type": "string" },
"type": { "type": "string" },
"hostName": { "type": "string" },
"guests": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"status": { "type": "string", "description": "host / confirmed / pending" }
},
"required": ["id", "name", "status"],
"additionalProperties": false
}
},
"status": { "type": "string" },
"description": { "type": "string" }
},
"required": ["partyId", "title", "date", "time", "status"],
"additionalProperties": false
}
},
"required": ["party"],
"additionalProperties": false
}
}
],
"components": [
{
"path": "components/party-create-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/recommend-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/invite-card/index",
"relatedPage": "/pages/home/home"
},
{
"path": "components/party-detail-card/index",
"relatedPage": "/pages/home/home"
}
]
}
party-skill
聚会安排,支持创建聚会、获取场地推荐、邀请朋友及查看聚会详情。
功能
- 创建聚会活动(名称/日期/时间/地点)
- 获取聚会场地推荐(餐厅/轰趴馆/KTV/户外)
- 邀请朋友参加聚会
- 查看聚会详情与成员状态
用户输入示例
- "周末搞个聚会"
- "推荐几个聚会场所"
- "邀请朋友来玩"
- "看看聚会详情"
- "创建个生日派对"
原子接口
| 接口名 | 说明 |
|---|---|
createParty | 创建聚会活动 |
getRecommendations | 获取聚会推荐(餐厅/场地/娱乐场所) |
inviteFriends | 邀请朋友参加聚会 |
getPartyDetails | 查看聚会详情 |
原子组件
| 组件路径 | 说明 |
|---|---|
components/party-create-card/index | 聚会创建表单 |
components/recommend-card/index | 聚会推荐列表 |
components/invite-card/index | 邀请朋友界面 |
components/party-detail-card/index | 聚会详情展示 |
后端依赖
| 资源 | 名称 |
|---|---|
| 云函数 | party-skill-handler |
| 数据库集合 | parties |
const { recommendations, friends, parties: seedParties } = require('../data/seed')
const _dynamicParties = [...seedParties]
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 filterRecommendations(type = '', keyword = '') {
const q = String(keyword || '').trim().toLowerCase()
const t = String(type || '').trim()
let list = [...recommendations]
if (t) {
list = list.filter((item) => item.type === t)
}
if (q) {
list = list.filter((item) => {
const hay = [item.name, item.address, item.typeText, ...(item.keywords || [])]
.join(' ')
.toLowerCase()
return hay.includes(q)
})
}
return list
}
function getRecommendationById(id) {
return recommendations.find((item) => item.id === id) || null
}
function getFriendList() {
return friends.map((item) => ({
friendId: item.friendId,
name: item.name,
avatar: item.avatar,
status: 'pending'
}))
}
function getPartyById(partyId) {
return _dynamicParties.find((item) => item.partyId === partyId) || null
}
function addParty(party) {
_dynamicParties.push(party)
}
function genPartyId() {
return `P${Date.now().toString(36).toUpperCase()}`
}
function genInviteCode() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
let code = 'PARTY-'
for (let i = 0; i < 4; i++) {
code += chars[Math.floor(Math.random() * chars.length)]
}
return code
}
module.exports = {
isPreviewMode,
errorResult,
successResult,
filterRecommendations,
getRecommendationById,
getFriendList,
getPartyById,
addParty,
genPartyId,
genInviteCode
}