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,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);
}