fix: 修复 VoiceController Map.of 兼容性 + ExploreController 参数不匹配

- VoiceController: Map.of() -> Collections.singletonMap() 兼容 Java 8
- ExploreController: 补齐 takeoutService.roll() 缺失的 taste/priceRange/allergies 参数

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
王鹏
2026-05-08 20:02:27 +08:00
commit 802b4ba229
98 changed files with 5761 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
const app = getApp();
const api = require('../../utils/api');
const loc = require('../../utils/location');
Page({
data: {
showAnimation: false,
dataReady: false,
result: null,
error: '',
distanceText: ''
},
onLoad() {
this.roll();
},
roll() {
this.setData({
showAnimation: true,
dataReady: false,
error: '',
result: null
});
loc.getLocation().then((pos) => {
return api.post('/api/explore/roll', {
latitude: pos.latitude,
longitude: pos.longitude,
openid: 'anonymous'
});
}).then((data) => {
this.setData({
result: data,
distanceText: ((data.distance || 0) / 1000).toFixed(1) + 'km',
dataReady: true
});
this.saveRecord(data);
}).catch((err) => {
this.setData({
showAnimation: false,
error: err.message || '附近暂无推荐好店,换片区域试试?'
});
});
},
onAnimationDone() {
this.setData({ showAnimation: false });
},
navigate() {
const shop = this.data.result;
if (!shop) return;
wx.openLocation({
latitude: shop.latitude || app.globalData.location.latitude,
longitude: shop.longitude || app.globalData.location.longitude,
name: shop.name,
address: shop.address,
scale: 16
});
},
retry() {
this.roll();
},
saveRecord(data) {
const history = wx.getStorageSync('box_history') || [];
history.push({
id: Date.now().toString(),
icon: '🍱',
name: data.name,
time: new Date().toLocaleString(),
typeName: '探店盲盒'
});
wx.setStorageSync('box_history', history);
}
});

View File

@@ -0,0 +1,7 @@
{
"usingComponents": {
"result-card": "/components/result-card/result-card",
"box-animation": "/components/box-animation/box-animation"
},
"navigationBarTitleText": "探店盲盒"
}

View File

@@ -0,0 +1,37 @@
<view class="explore-page">
<view class="empty" wx:if="{{error}}">
<view class="empty-icon">😿</view>
<view>{{error}}</view>
<button class="btn-primary" style="margin-top:32rpx" bind:tap="retry">换片区域试试</button>
</view>
<view class="result-area" wx:if="{{!showAnimation && result}}">
<view class="result-title">🎊 附近发现一家宝藏店</view>
<result-card
imageUrl="{{result.imageUrl}}"
name="{{result.name}}"
rating="{{result.rating}}"
avgPrice="{{result.avgPrice}}"
distance="{{distanceText}}"
recommendReason="{{result.recommendReason}}"
signatureDishes="{{result.signatureDishes}}"
/>
<view class="actions">
<button class="btn-primary btn-main" bind:tap="navigate">导航过去</button>
<view class="btn-retry" bind:tap="retry">再开一个</view>
</view>
</view>
<box-animation
show="{{showAnimation}}"
boxType="explore"
dataReady="{{dataReady}}"
bind:done="onAnimationDone"
>
<view class="animation-result-preview" wx:if="{{result}}">
<view class="preview-emoji">🍜</view>
<view class="preview-text">{{result.name}}</view>
<view class="preview-sub" wx:if="{{result.rating}}">⭐ {{result.rating}}</view>
</view>
</box-animation>
</view>

View File

@@ -0,0 +1,92 @@
/*
* 探店结果页
*/
.explore-page {
padding: var(--space-md);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
.result-area {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.result-title {
font-size: var(--text-subtitle);
font-weight: 600;
color: var(--color-text);
margin: var(--space-md) 0 var(--space-md);
text-align: center;
}
.actions {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-top: var(--space-lg);
}
.btn-main {
width: 80%;
}
.btn-retry {
margin-top: var(--space-md);
font-size: var(--text-body-sm);
color: var(--color-text-muted);
padding: var(--space-sm);
}
.btn-retry:active {
color: var(--color-purple);
}
/* ── 错误 ── */
.empty {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 200rpx;
text-align: center;
font-size: var(--text-body);
color: var(--color-text-secondary);
}
.empty-icon {
font-size: 80rpx;
margin-bottom: var(--space-md);
opacity: 0.6;
}
/* ── 动效预览 ── */
.animation-result-preview {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx;
}
.preview-emoji {
font-size: 64rpx;
margin-bottom: 12rpx;
}
.preview-text {
font-size: var(--text-subtitle);
font-weight: 700;
color: #FFFFFF;
text-align: center;
}
.preview-sub {
font-size: var(--text-body-sm);
color: rgba(255, 255, 255, 0.8);
margin-top: 8rpx;
}

View File

@@ -0,0 +1,165 @@
const storage = require('../../utils/storage');
const app = getApp();
const DEFAULT_STAPLES = ['油', '盐', '酱油', '醋', '料酒', '生抽', '蚝油', '葱', '姜', '蒜'];
Page({
data: {
inputValue: '',
ingredients: [],
selected: {},
staples: [],
voiceRecording: false,
showStapleEditor: false,
stapleEditValue: ''
},
onLoad() {
const saved = storage.get('custom_staples', null);
this.setData({ staples: saved || [...DEFAULT_STAPLES] });
},
onInput(e) {
this.setData({ inputValue: e.detail.value });
},
addIngredient() {
const name = this.data.inputValue.trim();
if (!name) return;
this.add(name);
this.setData({ inputValue: '' });
},
onToggle(e) {
const { name, selected } = e.detail;
if (selected) {
this.add(name);
} else {
this.remove(name);
}
},
add(name) {
if (this.data.ingredients.includes(name)) return;
const ingredients = [...this.data.ingredients, name];
const selected = { ...this.data.selected, [name]: true };
this.setData({ ingredients, selected });
},
remove(name) {
const ingredients = this.data.ingredients.filter(i => i !== name);
const selected = { ...this.data.selected, [name]: false };
this.setData({ ingredients, selected });
},
/* ── 语音输入 ── */
voiceInput() {
const recorder = wx.getRecorderManager();
if (this.data.voiceRecording) {
// 松手停止
recorder.stop();
this.setData({ voiceRecording: false });
wx.hideLoading();
return;
}
this.setData({ voiceRecording: true });
wx.showLoading({ title: '正在听…', mask: true });
recorder.start({
duration: 10000,
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 48000,
format: 'mp3'
});
recorder.onStop((res) => {
this.setData({ voiceRecording: false });
wx.hideLoading();
// 上传录音到后端识别
wx.uploadFile({
url: app.globalData.baseUrl + '/api/voice/recognize',
filePath: res.tempFilePath,
name: 'audio',
header: { 'Content-Type': 'multipart/form-data' },
success: (uploadRes) => {
try {
const data = JSON.parse(uploadRes.data);
if (data.code === 200 && data.data && data.data.text) {
const text = data.data.text.trim();
if (text) {
// 按逗号/空格/顿号分割多个食材
const names = text.split(/[,,、\s]+/);
names.forEach(n => this.add(n));
wx.showToast({ title: '已识别 ' + names.length + ' 种食材', icon: 'success' });
return;
}
}
wx.showToast({ title: '未识别到食材,请手动输入', icon: 'none' });
} catch (e) {
wx.showToast({ title: '识别失败,请手动输入', icon: 'none' });
}
},
fail: () => {
wx.showToast({ title: '语音服务暂不可用', icon: 'none' });
}
});
});
recorder.onError(() => {
this.setData({ voiceRecording: false });
wx.hideLoading();
wx.showToast({ title: '录音失败,请重试', icon: 'none' });
});
},
/* ── 自定义常备调料 ── */
editStaples() {
this.setData({ showStapleEditor: true, stapleEditValue: '' });
},
onStapleInput(e) {
this.setData({ stapleEditValue: e.detail.value });
},
addStaple() {
const name = this.data.stapleEditValue.trim();
if (!name) return;
if (this.data.staples.includes(name)) {
wx.showToast({ title: '已存在', icon: 'none' });
return;
}
const staples = [...this.data.staples, name];
this.setData({ staples, stapleEditValue: '' });
storage.set('custom_staples', staples);
},
removeStaple(e) {
const name = e.currentTarget.dataset.name;
const staples = this.data.staples.filter(s => s !== name);
this.setData({ staples });
storage.set('custom_staples', staples);
},
closeStapleEditor() {
this.setData({ showStapleEditor: false });
},
/* ── 开盒 ── */
openBox() {
if (this.data.ingredients.length === 0) {
wx.showToast({ title: '请先输入食材', icon: 'none' });
return;
}
// 将用户自定义的常备调料一并传入后端
const payload = {
ingredients: this.data.ingredients,
staples: this.data.staples
};
const params = encodeURIComponent(JSON.stringify(payload));
wx.navigateTo({ url: '/pages/recipe-list/recipe-list?payload=' + params });
}
});

View File

@@ -0,0 +1,6 @@
{
"usingComponents": {
"ingredient-tag": "/components/ingredient-tag/ingredient-tag"
},
"navigationBarTitleText": "冰箱盲盒"
}

View File

@@ -0,0 +1,66 @@
<view class="fridge-page">
<view class="title">冰箱里有什么?</view>
<view class="input-area">
<input class="ingredient-input"
placeholder="输入食材名称"
value="{{inputValue}}"
bind:input="onInput"
bind:confirm="addIngredient"
/>
<view class="voice-btn {{voiceRecording ? 'recording' : ''}}" bind:tap="voiceInput">
<text>{{voiceRecording ? '🔴' : '🎤'}}</text>
</view>
</view>
<view class="section">
<view class="section-title">常用食材</view>
<view class="tag-list">
<ingredient-tag icon="🥚" name="鸡蛋" selected="{{selected['鸡蛋']}}" bind:toggle="onToggle" />
<ingredient-tag icon="🍅" name="西红柿" selected="{{selected['西红柿']}}" bind:toggle="onToggle" />
<ingredient-tag icon="🍗" name="鸡胸肉" selected="{{selected['鸡胸肉']}}" bind:toggle="onToggle" />
<ingredient-tag icon="🧅" name="洋葱" selected="{{selected['洋葱']}}" bind:toggle="onToggle" />
<ingredient-tag icon="🥔" name="土豆" selected="{{selected['土豆']}}" bind:toggle="onToggle" />
<ingredient-tag icon="🌶️" name="青椒" selected="{{selected['青椒']}}" bind:toggle="onToggle" />
<ingredient-tag icon="🥦" name="胡萝卜" selected="{{selected['胡萝卜']}}" bind:toggle="onToggle" />
<ingredient-tag icon="🥬" name="西兰花" selected="{{selected['西兰花']}}" bind:toggle="onToggle" />
<ingredient-tag icon="🥕" name="生菜" selected="{{selected['生菜']}}" bind:toggle="onToggle" />
<ingredient-tag icon="🥬" name="豆腐" selected="{{selected['豆腐']}}" bind:toggle="onToggle" />
<ingredient-tag icon="🥩" name="猪肉" selected="{{selected['猪肉']}}" bind:toggle="onToggle" />
<ingredient-tag icon="🐥" name="鸡腿" selected="{{selected['鸡腿']}}" bind:toggle="onToggle" />
</view>
</view>
<view class="section">
<view class="section-title">
我的常备调料
<text class="edit-link" bind:tap="editStaples">编辑</text>
</view>
<view class="tag-list staples">
<text class="staple-tag" wx:for="{{staples}}" wx:key="*this">{{item}}</text>
</view>
</view>
<button class="btn-primary btn-large" bind:tap="openBox" disabled="{{ingredients.length === 0}}">
开盒做饭!
</button>
<!-- 常备调料编辑弹窗 -->
<view class="modal-mask" wx:if="{{showStapleEditor}}" bind:tap="closeStapleEditor"></view>
<view class="modal-sheet {{showStapleEditor ? 'show' : ''}}">
<view class="sheet-header">
<text class="sheet-title">编辑常备调料</text>
<text class="sheet-close" bind:tap="closeStapleEditor">完成</text>
</view>
<view class="sheet-input-row">
<input class="sheet-input" placeholder="调料名称" value="{{stapleEditValue}}" bind:input="onStapleInput" bind:confirm="addStaple" />
<view class="btn-add" bind:tap="addStaple">添加</view>
</view>
<view class="sheet-tags">
<view class="sheet-tag" wx:for="{{staples}}" wx:key="*this">
<text>{{item}}</text>
<text class="tag-remove" bind:tap="removeStaple" data-name="{{item}}">✕</text>
</view>
</view>
</view>
</view>

View File

@@ -0,0 +1,206 @@
/*
* 冰箱盲盒 · 食材输入
*/
.fridge-page {
padding: var(--space-lg);
min-height: 100vh;
}
/* ── 标题 ── */
.title {
font-size: var(--text-headline);
font-weight: 700;
margin-bottom: var(--space-md);
}
/* ── 输入区 ── */
.input-area {
display: flex;
align-items: center;
margin-bottom: var(--space-lg);
}
.ingredient-input {
flex: 1;
height: 88rpx;
background: var(--color-surface);
border-radius: var(--radius-lg);
padding: 0 var(--space-md);
font-size: var(--text-body);
box-shadow: var(--shadow-sm);
margin-right: var(--space-sm);
}
.voice-btn {
width: 88rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-primary);
border-radius: var(--radius-lg);
font-size: 36rpx;
transition: background 0.15s ease;
}
.voice-btn:active {
opacity: 0.8;
}
.voice-btn.recording {
background: #E53935;
animation: voicePulse 0.6s ease-in-out infinite alternate;
}
@keyframes voicePulse {
from { transform: scale(1); }
to { transform: scale(1.08); }
}
/* ── 分区 ── */
.section {
margin-bottom: var(--space-lg);
}
.section-title {
font-size: var(--text-body-sm);
font-weight: 600;
color: var(--color-text-secondary);
margin-bottom: var(--space-sm);
display: flex;
align-items: center;
}
.tag-list {
display: flex;
flex-wrap: wrap;
}
/* ── 常备调料 ── */
.staple-tag {
display: inline-flex;
padding: 8rpx var(--space-md);
border-radius: var(--radius-full);
background: #F5F2EE;
font-size: var(--text-body-sm);
color: var(--color-text-muted);
margin: 4rpx 8rpx;
}
.edit-link {
font-size: var(--text-body-sm);
color: var(--color-primary);
font-weight: 500;
margin-left: 8rpx;
}
/* ── 开盒按钮 ── */
.btn-large {
width: 100%;
height: 100rpx;
font-size: var(--text-body);
margin-top: var(--space-xl);
}
/* ── 调料编辑弹窗 ── */
.modal-mask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 100;
}
.modal-sheet {
position: fixed;
left: 0; right: 0; bottom: 0;
background: var(--color-surface);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
padding: var(--space-lg);
padding-bottom: calc(var(--space-lg) + constant(safe-area-inset-bottom));
padding-bottom: calc(var(--space-lg) + env(safe-area-inset-bottom));
z-index: 101;
transform: translateY(100%);
transition: transform 0.3s ease;
}
.modal-sheet.show {
transform: translateY(0);
}
.sheet-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-md);
}
.sheet-title {
font-size: var(--text-subtitle);
font-weight: 700;
}
.sheet-close {
font-size: var(--text-body);
color: var(--color-primary);
font-weight: 600;
}
.sheet-input-row {
display: flex;
align-items: center;
margin-bottom: var(--space-md);
}
.sheet-input {
flex: 1;
height: 80rpx;
background: #F7F5F2;
border-radius: var(--radius-md);
padding: 0 var(--space-md);
font-size: var(--text-body);
margin-right: var(--space-sm);
}
.btn-add {
width: 120rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-primary);
color: #FFFFFF;
border-radius: var(--radius-md);
font-size: var(--text-body-sm);
font-weight: 600;
}
.btn-add:active {
opacity: 0.8;
}
.sheet-tags {
display: flex;
flex-wrap: wrap;
max-height: 360rpx;
overflow-y: auto;
}
.sheet-tag {
display: flex;
align-items: center;
padding: 8rpx var(--space-md);
background: #F7F5F2;
border-radius: var(--radius-full);
font-size: var(--text-body-sm);
color: var(--color-text);
margin-right: 12rpx;
margin-bottom: 12rpx;
}
.tag-remove {
margin-left: 8rpx;
font-size: var(--text-caption);
color: var(--color-text-muted);
padding: 2rpx;
}

View File

@@ -0,0 +1,89 @@
const app = getApp();
const tips = [
'今天宜尝试酸辣口味',
'唯有美食与爱不可辜负',
'厨房里的秘密,全在一勺之间',
'一日三餐,每一餐都值得认真对待',
'今天适合来点甜的犒劳自己',
'好心情从一顿好饭开始',
'美食是治愈一切的力量'
];
Page({
data: {
greeting: '',
locationText: '点击获取位置',
dailyTip: ''
},
onShow() {
this.updateGreeting();
this.updateLocation();
this.setData({ dailyTip: tips[Math.floor(Math.random() * tips.length)] });
},
updateGreeting() {
const h = new Date().getHours();
let g = '晚上好';
if (h < 9) g = '早上好';
else if (h < 12) g = '上午好';
else if (h < 14) g = '中午好';
else if (h < 18) g = '下午好';
this.setData({ greeting: g });
},
updateLocation() {
const loc = app.globalData.location;
if (loc) {
this.setData({
locationText: `已定位 (${loc.latitude.toFixed(2)}, ${loc.longitude.toFixed(2)})`
});
} else {
// 尝试获取定位
this.requestLocation();
}
},
requestLocation() {
app.getLocation((err, loc) => {
if (loc) {
this.setData({
locationText: `已定位 (${loc.latitude.toFixed(2)}, ${loc.longitude.toFixed(2)})`
});
} else {
this.setData({ locationText: '点击选择位置' });
}
});
},
chooseLocation() {
wx.chooseLocation({
success: (res) => {
app.globalData.location = {
latitude: res.latitude,
longitude: res.longitude
};
this.setData({
locationText: `已定位 (${res.latitude.toFixed(2)}, ${res.longitude.toFixed(2)})`
});
}
});
},
onOpenBox(e) {
// 开盒前检查定位
if (!app.globalData.location) {
wx.showToast({ title: '请先获取位置', icon: 'none' });
this.requestLocation();
return;
}
const type = e.detail.type;
const routes = {
takeout: '/pages/takeout-result/takeout-result',
fridge: '/pages/fridge-input/fridge-input',
explore: '/pages/explore-result/explore-result'
};
wx.navigateTo({ url: routes[type] || routes.takeout });
}
});

View File

@@ -0,0 +1,6 @@
{
"usingComponents": {
"mode-card": "/components/mode-card/mode-card"
},
"navigationBarTitleText": "吃啥盲盒"
}

View File

@@ -0,0 +1,41 @@
<view class="home">
<view class="header">
<view class="greeting">{{greeting}}</view>
<view class="location-badge" bind:tap="chooseLocation">{{locationText}}</view>
</view>
<view class="hero">
<view class="hero-emoji">🎁</view>
<view class="hero-title">今天吃啥?</view>
<view class="hero-subtitle">开个盲盒</view>
</view>
<view class="modes">
<mode-card
type="takeout"
icon="🛵"
title="外卖盲盒"
desc="随机高质量外卖"
actionText="开个外卖盒"
bind:open="onOpenBox"
/>
<mode-card
type="fridge"
icon="🥬"
title="冰箱盲盒"
desc="有啥食材做啥菜"
actionText="开个冰箱盒"
bind:open="onOpenBox"
/>
<mode-card
type="explore"
icon="🍱"
title="探店盲盒"
desc="发现隐藏好店"
actionText="开个附近盒"
bind:open="onOpenBox"
/>
</view>
<view class="daily-tip">{{dailyTip}}</view>
</view>

View File

@@ -0,0 +1,85 @@
/*
* 首页 · 盲盒大厅
* 空间节奏32 → 48 → 48 → 64
*/
.home {
padding: var(--space-lg) var(--space-lg);
min-height: 100vh;
}
/* ── 顶部 ── */
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-xl);
}
.greeting {
font-size: var(--text-body);
font-weight: 500;
color: var(--color-text-secondary);
}
.location-badge {
display: flex;
align-items: center;
font-size: var(--text-body-sm);
color: var(--color-primary);
padding: var(--space-xs) var(--space-sm);
background: var(--color-primary-pale);
border-radius: var(--radius-full);
}
.location-badge:active {
opacity: 0.7;
}
/* ── 主视觉 ── */
.hero {
text-align: center;
margin-bottom: var(--space-xl);
}
.hero-emoji {
display: inline-block;
font-size: 88rpx;
margin-bottom: var(--space-sm);
animation: hero-float 3s ease-in-out infinite;
}
@keyframes hero-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10rpx); }
}
.hero-title {
font-size: 44rpx;
font-weight: 700;
color: var(--color-text);
letter-spacing: 2rpx;
margin-bottom: var(--space-xs);
}
.hero-subtitle {
font-size: var(--text-subtitle);
color: var(--color-primary);
font-weight: 500;
}
/* ── 卡片列表 ── */
.modes {
display: flex;
flex-direction: column;
gap: var(--space-md);
margin-bottom: var(--space-xl);
}
/* ── 每日一言 ── */
.daily-tip {
text-align: center;
font-size: var(--text-body-sm);
color: var(--color-text-muted);
padding: var(--space-lg) 0;
}

View File

@@ -0,0 +1,45 @@
const storage = require('../../utils/storage');
Page({
data: {
avatarUrl: '',
nickname: '',
prefs: { taste: '都可以', priceRange: 'all', allergies: '' },
shoppingList: []
},
onShow() {
const prefs = storage.get('user_prefs', { taste: '都可以', priceRange: 'all', allergies: '' });
const shoppingList = storage.get('shopping_list', []);
this.setData({ prefs, shoppingList });
},
setPref(e) {
const { key, val } = e.currentTarget.dataset;
const prefs = { ...this.data.prefs, [key]: val };
this.setData({ prefs });
storage.set('user_prefs', prefs);
},
setAllergies(e) {
const prefs = { ...this.data.prefs, allergies: e.detail.value };
this.setData({ prefs });
storage.set('user_prefs', prefs);
},
toggleItem(e) {
const idx = e.currentTarget.dataset.index;
const list = this.data.shoppingList;
list[idx].checked = !list[idx].checked;
this.setData({ shoppingList: list });
storage.set('shopping_list', list);
},
clearShopping() {
wx.showModal({
title: '清空清单',
content: '确定清空所有购物清单吗?',
success: (res) => {
if (res.confirm) {
this.setData({ shoppingList: [] });
storage.set('shopping_list', []);
}
}
});
}
});

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "我的"
}

View File

@@ -0,0 +1,48 @@
<view class="mine-page">
<view class="profile">
<image class="avatar" src="{{avatarUrl}}" mode="aspectFill" wx:if="{{avatarUrl}}"></image>
<view class="avatar-placeholder" wx:else>👤</view>
<view class="nickname">{{nickname || '点击登录'}}</view>
</view>
<view class="card-section">
<view class="card-title">口味偏好</view>
<view class="tag-row">
<view class="pref-tag {{prefs.taste === '辣' ? 'active' : ''}}" data-key="taste" data-val="辣" bind:tap="setPref">🌶️ 辣</view>
<view class="pref-tag {{prefs.taste === '清淡' ? 'active' : ''}}" data-key="taste" data-val="清淡" bind:tap="setPref">🌱 清淡</view>
<view class="pref-tag {{prefs.taste === '酸甜' ? 'active' : ''}}" data-key="taste" data-val="酸甜" bind:tap="setPref">🍋 酸甜</view>
<view class="pref-tag {{prefs.taste === '都可以' ? 'active' : ''}}" data-key="taste" data-val="都可以" bind:tap="setPref">😋 都可以</view>
</view>
</view>
<view class="card-section">
<view class="card-title">价格区间(外卖)</view>
<view class="tag-row">
<view class="pref-tag {{prefs.priceRange === 'low' ? 'active' : ''}}" data-key="priceRange" data-val="low" bind:tap="setPref">¥ 人均&lt;30</view>
<view class="pref-tag {{prefs.priceRange === 'medium' ? 'active' : ''}}" data-key="priceRange" data-val="medium" bind:tap="setPref">¥ 人均30-80</view>
<view class="pref-tag {{prefs.priceRange === 'high' ? 'active' : ''}}" data-key="priceRange" data-val="high" bind:tap="setPref">¥¥ 人均&gt;80</view>
<view class="pref-tag {{prefs.priceRange === 'all' ? 'active' : ''}}" data-key="priceRange" data-val="all" bind:tap="setPref">都行</view>
</view>
</view>
<view class="card-section">
<view class="card-title">忌口/过敏</view>
<input class="allergy-input" placeholder="如:海鲜、花生、牛奶(逗号分隔)" value="{{prefs.allergies}}" bind:blur="setAllergies" />
</view>
<view class="card-section" wx:if="{{shoppingList.length}}">
<view class="card-title">
购物清单 ({{shoppingList.length}}项)
<text class="clear-btn" bind:tap="clearShopping">清空</text>
</view>
<view class="shop-item {{item.checked ? 'checked' : ''}}" wx:for="{{shoppingList}}" wx:key="name" bind:tap="toggleItem" data-index="{{index}}">
<view class="shop-check">{{item.checked ? '✅' : '⬜'}}</view>
<view class="shop-info">
<view class="shop-name">{{item.name}} <text class="shop-amount">{{item.amount}}</text></view>
<view class="shop-from">来自:{{item.from}}</view>
</view>
</view>
</view>
<view class="about">吃啥盲盒 ChowBox v1.0.0</view>
</view>

View File

@@ -0,0 +1,158 @@
/*
* 我的
*/
.mine-page {
padding: var(--space-xl) var(--space-lg);
min-height: 100vh;
}
/* ── 个人资料 ── */
.profile {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: var(--space-xl);
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: #F0EDE8;
}
.avatar-placeholder {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: #F5F2EE;
display: flex;
align-items: center;
justify-content: center;
font-size: 56rpx;
}
.nickname {
font-size: var(--text-subtitle);
font-weight: 600;
color: var(--color-text);
margin-top: var(--space-sm);
}
/* ── 卡片 ── */
.card-section {
padding: var(--space-md);
margin-bottom: var(--space-sm);
background: var(--color-surface);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.card-title {
font-size: var(--text-body);
font-weight: 600;
color: var(--color-text);
margin-bottom: var(--space-sm);
}
/* ── 偏好标签 ── */
.tag-row {
display: flex;
flex-wrap: wrap;
}
.pref-tag {
padding: 8rpx var(--space-md);
border-radius: var(--radius-full);
background: #F5F2EE;
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
margin-right: 12rpx;
margin-bottom: 12rpx;
transition: all 0.2s var(--ease-out);
}
.pref-tag:active {
transform: scale(0.95);
}
.pref-tag.active {
background: var(--color-primary-pale);
color: var(--color-primary);
font-weight: 600;
}
/* ── 过敏原输入 ── */
.allergy-input {
width: 100%;
font-size: var(--text-body-sm);
padding: 12rpx 0;
color: var(--color-text);
border-bottom: 1rpx solid var(--color-hairline);
}
/* ── 清空按钮 ── */
.clear-btn {
font-size: var(--text-body-sm);
color: var(--color-primary);
float: right;
font-weight: 500;
}
/* ── 购物清单 ── */
.shop-item {
display: flex;
align-items: center;
padding: 14rpx 0;
border-bottom: 1rpx solid var(--color-hairline);
}
.shop-item:last-child {
border-bottom: none;
}
.shop-item:active {
opacity: 0.6;
}
.shop-item.checked .shop-name {
text-decoration: line-through;
color: var(--color-text-muted);
}
.shop-check {
font-size: 32rpx;
margin-right: var(--space-sm);
}
.shop-info {
flex: 1;
min-width: 0;
}
.shop-name {
font-size: var(--text-body);
font-weight: 500;
color: var(--color-text);
}
.shop-amount {
font-size: var(--text-body-sm);
color: var(--color-text-muted);
}
.shop-from {
font-size: var(--text-caption);
color: var(--color-text-muted);
margin-top: 2rpx;
}
/* ── 版本 ── */
.about {
text-align: center;
font-size: var(--text-body-sm);
color: var(--color-text-muted);
margin-top: var(--space-2xl);
padding-bottom: var(--space-xl);
}

View File

@@ -0,0 +1,96 @@
const api = require('../../utils/api');
const storage = require('../../utils/storage');
Page({
data: { loading: true, recipe: null, missingIngredients: [] },
onLoad(options) {
if (options.id) {
api.get('/api/recipe/' + options.id).then(data => {
this.setData({ loading: false, recipe: data });
}).catch(() => {
this.setData({ loading: false });
wx.showToast({ title: '加载失败', icon: 'none' });
});
}
if (options.missing) {
try {
this.setData({ missingIngredients: JSON.parse(decodeURIComponent(options.missing)) });
} catch (e) {
// ignore
}
}
// 启用分享
wx.showShareMenu({
withShareTicket: false,
menus: ['shareAppMessage', 'shareTimeline']
});
},
onShareAppMessage() {
const recipe = this.data.recipe;
if (!recipe) return {};
return {
title: '我用冰箱剩菜做出了「' + recipe.name + '」!你也来试试?',
path: '/pages/recipe-detail/recipe-detail?id=' + recipe.id,
imageUrl: recipe.imageUrl || ''
};
},
onShareTimeline() {
const recipe = this.data.recipe;
if (!recipe) return {};
return {
title: recipe.name + ' · 冰箱盲盒开出的好菜',
query: 'id=' + recipe.id,
imageUrl: recipe.imageUrl || ''
};
},
startTimer() {
wx.showToast({ title: '计时器功能开发中', icon: 'none' });
},
addToShopping() {
const recipe = this.data.recipe;
if (!recipe || !recipe.ingredients) return;
const raw = storage.get('shopping_list', []);
const existing = Array.isArray(raw) ? raw : [];
const names = new Set(existing.map(i => i.name));
// 只添加"缺少的食材";若无缺失信息则提示而非加全部
const missing = this.data.missingIngredients;
if (!Array.isArray(missing) || missing.length === 0) {
wx.showToast({ title: '该菜谱食材已齐全', icon: 'none' });
return;
}
const targetNames = new Set(missing);
let added = 0;
recipe.ingredients.forEach(ing => {
if (!targetNames.has(ing.ingredientName)) return;
if (names.has(ing.ingredientName)) return;
existing.push({
name: ing.ingredientName,
amount: ing.amount || '',
from: recipe.name,
checked: false
});
names.add(ing.ingredientName);
added++;
});
storage.set('shopping_list', existing);
if (added === 0) {
wx.showToast({ title: '缺少食材已在清单中', icon: 'none' });
} else {
wx.showToast({ title: '已添加 ' + added + ' 项缺少食材', icon: 'success' });
}
}
});

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "菜谱详情"
}

View File

@@ -0,0 +1,43 @@
<view class="recipe-detail" wx:if="{{recipe}}">
<image class="hero-img" src="{{recipe.imageUrl}}" mode="aspectFill" wx:if="{{recipe.imageUrl}}"></image>
<view class="hero-placeholder" wx:else>🍲</view>
<view class="detail-header">
<view class="detail-name">{{recipe.name}}</view>
<view class="detail-meta">
<text>⭐ 难度{{recipe.difficulty}}</text>
<text>⏱️ {{recipe.cookTime}}分钟</text>
<text>{{recipe.category}}</text>
</view>
</view>
<view class="section">
<view class="section-title">用料</view>
<view class="ingredient-list">
<view class="ing-item {{item.isStaple ? 'staple' : ''}}" wx:for="{{recipe.ingredients}}" wx:key="id">
<text>{{item.ingredientName}}</text>
<text class="amount">{{item.amount}}</text>
</view>
</view>
</view>
<view class="section">
<view class="section-title">步骤</view>
<view class="step-list">
<view class="step-item" wx:for="{{recipe.steps}}" wx:key="id">
<view class="step-num">{{item.stepOrder}}</view>
<view class="step-body">
<view class="step-text">{{item.content}}</view>
<image class="step-img" src="{{item.imageUrl}}" mode="widthFix" wx:if="{{item.imageUrl}}"></image>
</view>
</view>
</view>
</view>
<view class="bottom-bar">
<button class="btn-primary btn-small" bind:tap="startTimer">计时器</button>
<view class="btn-outline" bind:tap="addToShopping">加入购物清单</view>
</view>
</view>
<view class="center" wx:if="{{loading}}">加载中…</view>

View File

@@ -0,0 +1,196 @@
/*
* 菜谱详情
*/
.recipe-detail {
padding-bottom: 140rpx;
}
/* ── 主图 ── */
.hero-img {
width: 100%;
height: 400rpx;
background: #F2EFEB;
}
.hero-placeholder {
width: 100%;
height: 300rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 100rpx;
background: linear-gradient(180deg, #FFF8F2, #FFF0E4);
}
/* ── 头部 ── */
.detail-header {
padding: var(--space-md) var(--space-lg) var(--space-sm);
}
.detail-name {
font-size: var(--text-headline);
font-weight: 700;
color: var(--color-text);
margin-bottom: 8rpx;
}
.detail-meta {
display: flex;
align-items: center;
gap: var(--space-sm);
font-size: var(--text-body-sm);
color: var(--color-text-muted);
}
.detail-meta text {
display: inline-flex;
align-items: center;
padding: 4rpx 14rpx;
background: #F5F2EE;
border-radius: var(--radius-full);
}
/* ── 分区 ── */
.section {
padding: 0 var(--space-lg);
margin-bottom: var(--space-lg);
}
.section-title {
font-size: var(--text-body);
font-weight: 700;
color: var(--color-text);
margin-bottom: var(--space-sm);
}
/* ── 用料 ── */
.ingredient-list {
display: flex;
flex-wrap: wrap;
}
.ing-item {
display: flex;
align-items: center;
padding: 8rpx var(--space-md);
background: #F7F5F2;
border-radius: var(--radius-full);
font-size: var(--text-body-sm);
color: var(--color-text);
margin-right: 12rpx;
margin-bottom: 12rpx;
}
.ing-item.staple {
opacity: 0.4;
}
.amount {
color: var(--color-text-muted);
margin-left: 8rpx;
font-size: var(--text-caption);
}
/* ── 步骤 ── */
.step-list {
display: flex;
flex-direction: column;
}
.step-item {
display: flex;
align-items: flex-start;
margin-bottom: var(--space-sm);
}
.step-item:last-child {
margin-bottom: 0;
}
.step-num {
width: 48rpx;
height: 48rpx;
background: var(--color-primary);
color: #FFFFFF;
border-radius: var(--radius-full);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-body-sm);
font-weight: 700;
flex-shrink: 0;
margin-right: var(--space-sm);
}
.step-body {
flex: 1;
min-width: 0;
}
.step-text {
font-size: var(--text-body);
line-height: 1.7;
color: var(--color-text);
padding-top: 6rpx;
}
.step-img {
width: 100%;
border-radius: var(--radius-md);
margin-top: var(--space-sm);
background: #F2EFEB;
}
/* ── 底部栏 ── */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
padding: var(--space-sm) var(--space-lg);
padding-bottom: calc(var(--space-sm) + constant(safe-area-inset-bottom));
padding-bottom: calc(var(--space-sm) + env(safe-area-inset-bottom));
background: var(--color-surface);
box-shadow: 0 -1rpx 8rpx rgba(0, 0, 0, 0.04);
z-index: 10;
}
.btn-small {
flex: 1;
height: 80rpx;
font-size: var(--text-body-sm);
margin-right: var(--space-sm);
}
.btn-small:last-child {
margin-right: 0;
}
.btn-outline {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 80rpx;
border: 1rpx solid var(--color-hairline);
color: var(--color-text);
background: var(--color-surface);
border-radius: var(--radius-lg);
font-size: var(--text-body-sm);
font-weight: 500;
transition: background 0.15s ease;
}
.btn-outline:active {
background: #FAFAF8;
}
/* ── 加载 ── */
.center {
padding: 160rpx 0;
text-align: center;
color: var(--color-text-muted);
font-size: var(--text-body);
}

View File

@@ -0,0 +1,82 @@
const api = require('../../utils/api');
Page({
data: {
showAnimation: false,
dataReady: false,
loading: false,
results: [],
ingredients: []
},
onLoad(options) {
if (options.payload) {
try {
const payload = JSON.parse(decodeURIComponent(options.payload));
const ingredients = payload.ingredients || [];
const staples = payload.staples || [];
this.setData({ ingredients });
this.match(ingredients, staples);
} catch (e) {
wx.showToast({ title: '参数错误', icon: 'none' });
}
}
},
match(ingredients, staples) {
this.setData({
showAnimation: true,
dataReady: false,
results: []
});
api.post('/api/fridge/match', { ingredients: ingredients, staples: staples })
.then((data) => {
this.setData({ results: data, dataReady: true });
if (data.length > 0) {
const history = wx.getStorageSync('box_history') || [];
history.push({
id: Date.now().toString(),
icon: '🥬',
name: data[0].recipe.name + ' (' + data[0].matchRate + '%匹配)',
time: new Date().toLocaleString(),
typeName: '冰箱盲盒'
});
wx.setStorageSync('box_history', history);
}
})
.catch(() => {
this.setData({
showAnimation: false,
results: [],
loading: false
});
wx.showToast({ title: '匹配失败,请重试', icon: 'none' });
});
},
onAnimationDone() {
this.setData({ showAnimation: false });
},
goDetail(e) {
const id = e.currentTarget.dataset.id;
let missing = e.currentTarget.dataset.missing;
// dataset 值可能是字符串,需要解析
if (typeof missing === 'string') {
try { missing = JSON.parse(missing); } catch (e) { missing = []; }
}
if (!Array.isArray(missing)) missing = [];
let url = '/pages/recipe-detail/recipe-detail?id=' + id;
if (missing.length > 0) {
url += '&missing=' + encodeURIComponent(JSON.stringify(missing));
}
wx.navigateTo({ url: url });
},
reshuffle() {
this.match(this.data.ingredients);
}
});

View File

@@ -0,0 +1,6 @@
{
"usingComponents": {
"box-animation": "/components/box-animation/box-animation"
},
"navigationBarTitleText": "菜谱推荐"
}

View File

@@ -0,0 +1,42 @@
<view class="recipe-list-page">
<box-animation
show="{{showAnimation}}"
boxType="fridge"
dataReady="{{dataReady}}"
bind:done="onAnimationDone"
>
<view class="animation-result-preview" wx:if="{{results.length > 0}}">
<view class="preview-emoji">🥬</view>
<view class="preview-text">{{results[0].recipe.name}}</view>
<view class="preview-sub">匹配度 {{results[0].matchRate}}%</view>
</view>
</box-animation>
<view class="list-header" wx:if="{{!showAnimation && results.length}}">
为你找到 {{results.length}} 个菜谱
</view>
<view class="recipe-cards" wx:if="{{!showAnimation}}">
<view class="recipe-card card" wx:for="{{results}}" wx:key="recipe.id" bind:tap="goDetail" data-id="{{item.recipe.id}}" data-missing="{{item.missingIngredients}}">
<view class="card-left">
<view class="recipe-name">{{item.recipe.name}}</view>
<view class="recipe-meta">
<text wx:if="{{item.recipe.difficulty}}">⭐{{item.recipe.difficulty}}</text>
<text wx:if="{{item.recipe.cookTime}}"> ⏱️{{item.recipe.cookTime}}分钟</text>
</view>
<view class="match-rate">
<view class="rate-bar">
<view class="rate-fill" style="width:{{item.matchRate}}%"></view>
</view>
<text>{{item.matchRate}}%</text>
</view>
<view class="missing" wx:if="{{item.missingIngredients.length}}">
缺:<text wx:for="{{item.missingIngredients}}" wx:key="*this">{{item}} </text>
</view>
</view>
</view>
</view>
<view class="center" wx:if="{{!showAnimation && results.length === 0}}">没有匹配的菜谱,试试换个食材组合</view>
<view class="btn-retry" wx:if="{{!showAnimation && results.length}}" bind:tap="reshuffle">换一批菜谱</view>
</view>

View File

@@ -0,0 +1,133 @@
/*
* 菜谱推荐列表
*/
.recipe-list-page {
padding: var(--space-md) var(--space-lg);
min-height: 100vh;
}
/* ── 页头 ── */
.list-header {
font-size: var(--text-body);
font-weight: 600;
color: var(--color-text);
margin-bottom: var(--space-md);
}
/* ── 卡片 ── */
.recipe-cards {
display: flex;
flex-direction: column;
gap: var(--space-sm);
margin-bottom: var(--space-md);
}
.recipe-card {
padding: var(--space-md);
background: var(--color-surface);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
transition: transform 0.15s ease;
}
.recipe-card:active {
transform: scale(0.985);
}
/* ── 内容 ── */
.recipe-name {
font-size: var(--text-subtitle);
font-weight: 700;
color: var(--color-text);
margin-bottom: 4rpx;
}
.recipe-meta {
font-size: var(--text-body-sm);
color: var(--color-text-muted);
margin-bottom: var(--space-sm);
}
/* ── 匹配度条 ── */
.match-rate {
display: flex;
align-items: center;
margin-bottom: 8rpx;
font-size: var(--text-body-sm);
font-weight: 600;
color: var(--color-text);
}
.rate-bar {
flex: 1;
height: 6rpx;
background: #F0EDE8;
border-radius: 3rpx;
overflow: hidden;
margin-right: 12rpx;
}
.rate-fill {
height: 100%;
background: var(--color-green);
border-radius: 3rpx;
transition: width 0.5s ease;
}
/* ── 缺少食材 ── */
.missing {
font-size: var(--text-caption);
color: #E8A040;
margin-top: 8rpx;
}
/* ── 空态 ── */
.center {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
font-size: var(--text-body);
color: var(--color-text-muted);
}
/* ── 换一批 ── */
.btn-retry {
text-align: center;
font-size: var(--text-body);
font-weight: 500;
color: var(--color-primary);
padding: var(--space-md);
}
.btn-retry:active {
opacity: 0.6;
}
/* ── 动效内预览 ── */
.animation-result-preview {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx;
}
.preview-emoji {
font-size: 64rpx;
margin-bottom: 12rpx;
}
.preview-text {
font-size: var(--text-subtitle);
font-weight: 700;
color: #FFFFFF;
text-align: center;
}
.preview-sub {
font-size: var(--text-body-sm);
color: rgba(255, 255, 255, 0.8);
margin-top: 8rpx;
}

View File

@@ -0,0 +1,8 @@
Page({
data: { items: [] },
onShow() { this.loadRecords(); },
loadRecords() {
const history = wx.getStorageSync('box_history') || [];
this.setData({ items: history.reverse() });
}
});

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "开盒记录"
}

View File

@@ -0,0 +1,18 @@
<view class="records-page">
<view class="empty" wx:if="{{items.length === 0}}">
<view class="empty-icon">📜</view>
<view>还没有开盒记录</view>
<view class="sub">去首页开个盲盒吧</view>
</view>
<view class="record-list" wx:else>
<view class="record-item" wx:for="{{items}}" wx:key="id">
<view class="record-icon">{{item.icon}}</view>
<view class="record-info">
<view class="record-name">{{item.name}}</view>
<view class="record-time">{{item.time}}</view>
</view>
<view class="record-type tag tag-orange">{{item.typeName}}</view>
</view>
</view>
</view>

View File

@@ -0,0 +1,81 @@
/*
* 开盒记录
*/
.records-page {
padding: var(--space-md) var(--space-lg);
min-height: 100vh;
}
/* ── 空态 ── */
.empty {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 220rpx;
font-size: var(--text-body);
color: var(--color-text-muted);
}
.empty-icon {
font-size: 80rpx;
margin-bottom: var(--space-md);
opacity: 0.5;
}
.sub {
font-size: var(--text-body-sm);
color: var(--color-text-muted);
margin-top: 8rpx;
}
/* ── 列表 ── */
.record-list {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.record-item {
display: flex;
align-items: center;
padding: var(--space-md);
background: var(--color-surface);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
transition: transform 0.15s var(--ease-out);
}
.record-item:active {
transform: scale(0.985);
}
.record-icon {
font-size: 44rpx;
margin-right: var(--space-sm);
opacity: 0.8;
}
.record-info {
flex: 1;
min-width: 0;
}
.record-name {
font-size: var(--text-body);
font-weight: 600;
color: var(--color-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.record-time {
font-size: var(--text-caption);
color: var(--color-text-muted);
margin-top: 4rpx;
}
.record-type {
flex-shrink: 0;
}

View File

@@ -0,0 +1,138 @@
const app = getApp();
const api = require('../../utils/api');
const loc = require('../../utils/location');
const storage = require('../../utils/storage');
Page({
data: {
showAnimation: false,
dataReady: false,
result: null,
error: '',
distanceText: '',
deliveryInfo: ''
},
onLoad() {
this.roll();
},
roll() {
this.setData({
showAnimation: true,
dataReady: false,
error: '',
result: null
});
const prefs = storage.get('user_prefs', {});
loc.getLocation().then((pos) => {
return api.post('/api/takeout/roll', {
latitude: pos.latitude,
longitude: pos.longitude,
openid: 'anonymous',
taste: prefs.taste || '都可以',
priceRange: prefs.priceRange || 'all',
allergies: prefs.allergies || ''
});
}).then((data) => {
const distKm = ((data.distance || 0) / 1000).toFixed(1);
const deliveryTime = data.deliveryTime || '';
const minOrder = data.minOrder || '';
const deliveryFee = data.deliveryFee ? ('配送费' + data.deliveryFee) : '';
const parts = [deliveryTime, minOrder, deliveryFee].filter(Boolean);
this.setData({
result: data,
distanceText: distKm + 'km',
deliveryInfo: parts.join(' · '),
dataReady: true
});
}).catch((err) => {
this.setData({
showAnimation: false,
error: err.message || '附近喵星人占领了,换片区域试试?'
});
});
},
onAnimationDone() {
this.setData({ showAnimation: false });
},
goOrder() {
const shop = this.data.result;
if (!shop) return;
const shopName = shop.name || '';
const shopAddress = shop.address || '';
const lat = app.globalData.location ? app.globalData.location.latitude : 0;
const lng = app.globalData.location ? app.globalData.location.longitude : 0;
wx.showActionSheet({
itemList: ['美团外卖', '饿了么', '查看地图位置'],
success: (res) => {
if (res.tapIndex === 0) {
this.openMeituan(shopName);
} else if (res.tapIndex === 1) {
this.openEleme(shopName);
} else if (res.tapIndex === 2) {
wx.openLocation({
latitude: shop.latitude || lat,
longitude: shop.longitude || lng,
name: shopName,
address: shopAddress,
scale: 16
});
}
},
fail: () => {
this.openMeituan(shopName);
}
});
},
openMeituan(name) {
wx.navigateToMiniProgram({
appId: 'wxde8ac0a21135c07d',
path: '',
extraData: { query: name },
success: () => {
this.saveRecord(this.data.result);
},
fail: () => {
wx.showToast({ title: '请安装美团外卖 APP', icon: 'none', duration: 2000 });
}
});
},
openEleme(name) {
wx.navigateToMiniProgram({
appId: 'wxece3a9a4c82f58c9',
path: '',
extraData: { query: name },
success: () => {
this.saveRecord(this.data.result);
},
fail: () => {
wx.showToast({ title: '请安装饿了么 APP', icon: 'none', duration: 2000 });
}
});
},
retry() {
this.roll();
},
saveRecord(data) {
const history = wx.getStorageSync('box_history') || [];
history.push({
id: Date.now().toString(),
icon: '🛵',
name: data.name,
time: new Date().toLocaleString(),
typeName: '外卖盲盒'
});
wx.setStorageSync('box_history', history);
}
});

View File

@@ -0,0 +1,7 @@
{
"usingComponents": {
"result-card": "/components/result-card/result-card",
"box-animation": "/components/box-animation/box-animation"
},
"navigationBarTitleText": "外卖盲盒"
}

View File

@@ -0,0 +1,41 @@
<view class="takeout-page">
<!-- 错误态 -->
<view class="empty" wx:if="{{error}}">
<view class="empty-icon">😿</view>
<view>{{error}}</view>
<button class="btn-primary" style="margin-top:32rpx" bind:tap="retry">换个区域试试</button>
</view>
<!-- 结果区(动效结束后展示) -->
<view class="result-area" wx:if="{{!showAnimation && result}}">
<view class="result-title">🎉 你抽到的外卖是…</view>
<result-card
imageUrl="{{result.imageUrl}}"
name="{{result.name}}"
rating="{{result.rating}}"
avgPrice="{{result.avgPrice}}"
distance="{{distanceText}}"
extra="{{deliveryInfo}}"
recommendReason="{{result.recommendReason}}"
signatureDishes="{{result.signatureDishes}}"
/>
<view class="actions">
<button class="btn-primary btn-main" bind:tap="goOrder">去下单</button>
<view class="btn-retry" bind:tap="retry">再开一个</view>
</view>
</view>
<!-- 开盒动效覆盖层 -->
<box-animation
show="{{showAnimation}}"
boxType="takeout"
dataReady="{{dataReady}}"
bind:done="onAnimationDone"
>
<view class="animation-result-preview" wx:if="{{result}}">
<view class="preview-emoji">🛵</view>
<view class="preview-text">{{result.name}}</view>
<view class="preview-sub" wx:if="{{result.rating}}">⭐ {{result.rating}}</view>
</view>
</box-animation>
</view>

View File

@@ -0,0 +1,93 @@
/*
* 外卖结果页
* 流程:动效覆盖层(加载+揭晓)→ 结果展示
*/
.takeout-page {
padding: var(--space-md);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
/* ── 结果区(动效结束后) ── */
.result-area {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.result-title {
font-size: var(--text-subtitle);
font-weight: 600;
color: var(--color-text);
margin: var(--space-md) 0 var(--space-md);
text-align: center;
}
.actions {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-top: var(--space-lg);
}
.btn-main {
width: 80%;
}
.btn-retry {
margin-top: var(--space-md);
font-size: var(--text-body-sm);
color: var(--color-text-muted);
padding: var(--space-sm);
}
.btn-retry:active {
color: var(--color-primary);
}
/* ── 错误态 ── */
.empty {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 200rpx;
font-size: var(--text-body);
color: var(--color-text-secondary);
}
.empty-icon {
font-size: 80rpx;
margin-bottom: var(--space-md);
opacity: 0.6;
}
/* ── 动效内预览Act 3 slot 内容) ── */
.animation-result-preview {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx;
}
.preview-emoji {
font-size: 64rpx;
margin-bottom: 12rpx;
}
.preview-text {
font-size: var(--text-subtitle);
font-weight: 700;
color: #FFFFFF;
text-align: center;
}
.preview-sub {
font-size: var(--text-body-sm);
color: rgba(255, 255, 255, 0.8);
margin-top: 8rpx;
}