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,105 @@
Component({
properties: {
show: { type: Boolean, value: false },
boxType: { type: String, value: 'takeout' },
dataReady: { type: Boolean, value: false }
},
data: {
act: 0,
boxEmoji: '🎁',
sparkEmoji: '✨',
confettiColors: [],
_pendingAct3: false
},
observers: {
'show': function(val) {
if (val) {
this.setData({ act: 0, _pendingAct3: false });
this.startAct1();
} else {
this.reset();
}
},
'dataReady': function(val) {
if (val && this.data._pendingAct3) {
this.playAct3();
}
},
'boxType': function(val) {
const config = this.getTypeConfig(val);
this.setData({
boxEmoji: config.boxEmoji,
sparkEmoji: config.sparkEmoji,
confettiColors: config.confettiColors
});
}
},
methods: {
getTypeConfig(type) {
const map = {
takeout: {
boxEmoji: '🛵',
sparkEmoji: '🛵',
confettiColors: ['#E8693B', '#FF8A5C', '#FFD54F', '#FFAB40',
'#FFCC80', '#E8693B', '#FF8A5C', '#FFD54F', '#FFAB40']
},
fridge: {
boxEmoji: '🥬',
sparkEmoji: '🥬',
confettiColors: ['#4CAF50', '#66BB6A', '#A5D6A7', '#81C784',
'#C8E6C9', '#4CAF50', '#66BB6A', '#A5D6A7', '#81C784']
},
explore: {
boxEmoji: '🍜',
sparkEmoji: '📍',
confettiColors: ['#7C3AED', '#9C27B0', '#CE93D8', '#BA68C8',
'#E1BEE7', '#7C3AED', '#9C27B0', '#CE93D8', '#BA68C8']
}
};
return map[type] || map.takeout;
},
/* ── Act 1: 光晕扩散 + 盒子震动 (0400ms) ── */
startAct1() {
wx.vibrateShort({ type: 'light' });
setTimeout(() => {
this.setData({ act: 1 });
}, 50);
// Act 2: 盒盖飞起 + 金光 + 白屏 (400ms)
setTimeout(() => {
wx.vibrateShort({ type: 'medium' });
this.setData({ act: 2 });
// 标记等待数据
this.setData({ _pendingAct3: true });
// 如果数据已就绪,立即进入 Act 3
if (this.data.dataReady) {
this.playAct3();
}
}, 400);
},
/* ── Act 3: 内容弹出 + 纸屑(数据就绪后触发) ── */
playAct3() {
this.setData({ _pendingAct3: false });
wx.vibrateLong();
setTimeout(() => {
this.setData({ act: 3 });
}, 150);
// 通知父页面动画完成
setTimeout(() => {
this.triggerEvent('done');
}, 2200);
},
reset() {
this.setData({ act: 0, _pendingAct3: false });
}
}
});

View File

@@ -0,0 +1,3 @@
{
"component": true
}

View File

@@ -0,0 +1,52 @@
<view class="animation-overlay box-{{boxType}} {{show ? 'visible' : ''}}" wx:if="{{show}}">
<!-- ═══ Act 1: 光晕扩散 + 背景变暗 ═══ -->
<view class="bg-dimmer {{act >= 1 ? 'active' : ''}}"></view>
<view class="act1-glow-ring {{act >= 1 ? 'active' : ''}}">
<view class="glow-inner"></view>
</view>
<!-- ═══ 盲盒主体 + 盒盖 ═══ -->
<view class="box-stage {{act >= 1 ? 'active' : ''}}">
<!-- 盒盖Act2 飞起) -->
<view class="box-lid {{act >= 2 ? 'active' : ''}}">
<view class="lid-top"></view>
</view>
<!-- 金色光芒Act2 从盒缝透出) -->
<view class="golden-light {{act >= 2 ? 'active' : ''}}"></view>
<!-- 盒体 -->
<view class="box-body {{act >= 2 ? 'compressed' : ''}}">
<text class="box-emoji">{{boxEmoji}}</text>
</view>
<!-- 粒子Act2 溅射) -->
<view class="spark-particles {{act >= 2 ? 'active' : ''}}">
<view class="spark s1">{{sparkEmoji}}</view>
<view class="spark s2">{{sparkEmoji}}</view>
<view class="spark s3">{{sparkEmoji}}</view>
<view class="spark s4">{{sparkEmoji}}</view>
</view>
</view>
<!-- ═══ Act 2: 白色闪屏过渡 ═══ -->
<view class="act2-mask {{act >= 2 ? 'active' : ''}}"></view>
<!-- ═══ Act 3: 内容承载层(弹性弹出) ═══ -->
<view class="act3-content {{act >= 3 ? 'active' : ''}}">
<view class="content-inner">
<slot></slot>
</view>
</view>
<!-- ═══ 彩带纸屑Act3 收尾) ═══ -->
<view class="confetti-stage {{act >= 3 ? 'active' : ''}}">
<view class="confetti c1" wx:for="{{confettiColors}}" wx:key="*this"
style="--c:{{item}}; --x:{{10 + index * 22}}%; --d:{{0.8 + index * 0.15}}s;">
</view>
</view>
</view>

View File

@@ -0,0 +1,304 @@
/*
* 开盒动效 · 三幕分镜
* 参考 doc/box.md
*
* Act 1 (0400ms): 光晕扩散 + 盒子震动 + 背景变暗
* Act 2 (4001000ms): 盒盖飞起 + 金色光芒 + 粒子溅射 + 白色闪屏
* Act 3 (10001800ms): 内容弹性弹出 + 彩带纸屑
*
* 总时长约 1.82.0s
*/
/* ═══ 覆盖层 ═══ */
.animation-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.animation-overlay.visible {
pointer-events: auto;
}
/* ═══ 背景变暗 ═══ */
.bg-dimmer {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.55);
opacity: 0;
transition: opacity 0.4s ease-out;
}
.bg-dimmer.active {
opacity: 1;
}
/* ════════════════════════
Act 1 · 光晕扩散 (0400ms)
════════════════════════ */
.act1-glow-ring {
position: absolute;
width: 320rpx;
height: 320rpx;
border-radius: 50%;
opacity: 0;
transform: scale(0.15);
}
.act1-glow-ring.active {
animation: glowExpand 0.5s ease-out forwards;
}
.glow-inner {
width: 100%;
height: 100%;
border-radius: 50%;
}
/* 模式颜色 */
.box-takeout .glow-inner {
background: radial-gradient(circle, #E8693B 0%, rgba(232,105,59,0.4) 35%, transparent 70%);
}
.box-fridge .glow-inner {
background: radial-gradient(circle, #4CAF50 0%, rgba(76,175,80,0.4) 35%, transparent 70%);
}
.box-explore .glow-inner {
background: radial-gradient(circle, #7C3AED 0%, rgba(124,58,237,0.4) 35%, transparent 70%);
}
@keyframes glowExpand {
0% { opacity: 0.9; transform: scale(0.15); }
50% { opacity: 0.6; transform: scale(1.5); }
100% { opacity: 0; transform: scale(3.2); }
}
/* ════════════════════════
Act 1 · 盒子震动
════════════════════════ */
.box-stage {
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
opacity: 0;
transform: scale(0.85);
transition: opacity 0.15s ease, transform 0.15s ease;
}
.box-stage.active {
opacity: 1;
transform: scale(1);
}
/* ── 盒盖 ── */
.box-lid {
position: relative;
z-index: 3;
margin-bottom: -12rpx;
transition: transform 0.35s ease-in, opacity 0.35s ease-in;
transition-delay: 0.05s;
}
.lid-top {
width: 120rpx;
height: 40rpx;
background: linear-gradient(180deg, #F5A623 0%, #E8961A 100%);
border-radius: 20rpx 20rpx 4rpx 4rpx;
box-shadow: 0 4rpx 16rpx rgba(200, 120, 20, 0.4);
}
.box-lid.active {
transform: translateY(-80rpx) rotate(-8deg);
opacity: 0;
}
/* ── 金色光芒 ── */
.golden-light {
position: absolute;
z-index: 1;
width: 200rpx;
height: 200rpx;
border-radius: 50%;
opacity: 0;
transform: scale(0.3);
}
.box-takeout .golden-light {
background: radial-gradient(circle, #FFD54F 0%, rgba(255, 180, 50, 0.6) 30%, transparent 65%);
}
.box-fridge .golden-light {
background: radial-gradient(circle, #A5D6A7 0%, rgba(76, 175, 80, 0.5) 30%, transparent 65%);
}
.box-explore .golden-light {
background: radial-gradient(circle, #CE93D8 0%, rgba(124, 58, 237, 0.5) 30%, transparent 65%);
}
.golden-light.active {
animation: goldenBurst 0.6s ease-out forwards;
}
@keyframes goldenBurst {
0% { opacity: 0; transform: scale(0.3); }
30% { opacity: 1; transform: scale(1.8); }
100% { opacity: 0; transform: scale(3.5); }
}
/* ── 盒体Act2 压缩反弹) ── */
.box-body {
position: relative;
z-index: 2;
transition: transform 0.25s ease-out;
}
.box-body.compressed {
animation: bodyCompress 0.3s ease-out;
}
.box-emoji {
font-size: 160rpx;
display: block;
}
@keyframes bodyCompress {
0% { transform: scaleY(1); }
40% { transform: scaleY(0.85); }
100% { transform: scaleY(1); }
}
/* ── 粒子溅射Act2 ── */
.spark-particles {
position: absolute;
z-index: 0;
top: 50%; left: 50%;
width: 0; height: 0;
pointer-events: none;
}
.spark-particles.active .spark {
animation: sparkBurst 0.7s ease-out forwards;
}
.spark {
position: absolute;
font-size: 32rpx;
opacity: 0;
}
.spark.s1 { animation-delay: 0s !important; }
.spark.s2 { animation-delay: 0.06s !important; }
.spark.s3 { animation-delay: 0.12s !important; }
.spark.s4 { animation-delay: 0.18s !important; }
@keyframes sparkBurst {
0% { opacity: 1; transform: translate(0, 0) scale(0.5); }
100% { opacity: 0; transform: translate(var(--sx, 60rpx), var(--sy, -80rpx)) scale(1.2); }
}
.box-takeout .spark { --sx: 70rpx; --sy: -90rpx; }
.box-takeout .spark.s2 { --sx: -60rpx; --sy: -70rpx; }
.box-takeout .spark.s3 { --sx: 50rpx; --sy: -100rpx; }
.box-takeout .spark.s4 { --sx: -80rpx; --sy: -80rpx; }
.box-fridge .spark { --sx: 70rpx; --sy: -90rpx; }
.box-fridge .spark.s2 { --sx: -60rpx; --sy: -70rpx; }
.box-fridge .spark.s3 { --sx: 50rpx; --sy: -100rpx; }
.box-fridge .spark.s4 { --sx: -80rpx; --sy: -80rpx; }
.box-explore .spark { --sx: 70rpx; --sy: -90rpx; }
.box-explore .spark.s2 { --sx: -60rpx; --sy: -70rpx; }
.box-explore .spark.s3 { --sx: 50rpx; --sy: -100rpx; }
.box-explore .spark.s4 { --sx: -80rpx; --sy: -80rpx; }
/* ════════════════════════
Act 2 · 白色闪屏 (6001000ms)
════════════════════════ */
.act2-mask {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: #FFFFFF;
opacity: 0;
pointer-events: none;
}
.act2-mask.active {
animation: maskFlash 0.5s ease-in-out forwards;
animation-delay: 0.2s;
}
@keyframes maskFlash {
0% { opacity: 0; }
45% { opacity: 0.92; }
100% { opacity: 0; }
}
/* ════════════════════════
Act 3 · 内容弹性弹出 (10001800ms)
════════════════════════ */
.act3-content {
position: relative;
z-index: 20;
opacity: 0;
transform: scale(0.85) translateY(30rpx);
}
.act3-content.active {
animation: contentReveal 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
.content-inner {
display: flex;
flex-direction: column;
align-items: center;
}
@keyframes contentReveal {
0% { opacity: 0; transform: scale(0.85) translateY(30rpx); }
100% { opacity: 1; transform: scale(1) translateY(0); }
}
/* ════════════════════════
Act 3 · 彩带纸屑 (1600ms+)
════════════════════════ */
.confetti-stage {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
overflow: hidden;
}
.confetti-stage.active .confetti {
animation: confettiDrop 1.4s ease-in forwards;
}
.confetti {
position: absolute;
top: -20rpx;
left: var(--x, 20%);
width: 14rpx;
height: 14rpx;
border-radius: 3rpx;
background: var(--c, #E8693B);
opacity: 0;
animation-delay: var(--d, 0s);
}
@keyframes confettiDrop {
0% { opacity: 1; transform: translateY(0) rotate(0deg) scale(1); }
30% { opacity: 0.9; transform: translateY(280rpx) rotate(180deg) scale(0.7); }
60% { opacity: 0.5; transform: translateY(600rpx) rotate(400deg) scale(0.4); }
100% { opacity: 0; transform: translateY(1000rpx) rotate(720deg) scale(0.1); }
}

View File

@@ -0,0 +1,12 @@
Component({
properties: {
name: { type: String, value: '' },
icon: { type: String, value: '' },
selected: { type: Boolean, value: false }
},
methods: {
onTap() {
this.triggerEvent('toggle', { name: this.data.name, selected: !this.data.selected });
}
}
});

View File

@@ -0,0 +1,3 @@
{
"component": true
}

View File

@@ -0,0 +1,4 @@
<view class="ingredient-tag {{selected ? 'selected' : ''}}" bind:tap="onTap">
<text>{{icon}} {{name}}</text>
<text class="check" wx:if="{{selected}}">✓</text>
</view>

View File

@@ -0,0 +1,27 @@
.ingredient-tag {
display: inline-flex;
align-items: center;
padding: 8rpx var(--space-md);
border-radius: var(--radius-full);
background: #F5F2EE;
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
margin: 6rpx;
transition: all 0.2s var(--ease-out);
}
.ingredient-tag:active {
transform: scale(0.95);
}
.ingredient-tag.selected {
background: var(--color-green-pale);
color: var(--color-green);
font-weight: 600;
}
.check {
margin-left: 6rpx;
font-size: var(--text-caption);
font-weight: 700;
}

View File

@@ -0,0 +1,14 @@
Component({
properties: {
type: { type: String, value: 'takeout' },
icon: { type: String, value: '' },
title: { type: String, value: '' },
desc: { type: String, value: '' },
actionText: { type: String, value: '开盒' }
},
methods: {
onTap() {
this.triggerEvent('open', { type: this.data.type });
}
}
});

View File

@@ -0,0 +1,3 @@
{
"component": true
}

View File

@@ -0,0 +1,10 @@
<view class="mode-card {{type}}" bind:tap="onTap">
<view class="card-icon-wrap">
<text class="card-icon">{{icon}}</text>
</view>
<view class="card-body">
<view class="card-title">{{title}}</view>
<view class="card-desc">{{desc}}</view>
</view>
<view class="card-action">{{actionText}}</view>
</view>

View File

@@ -0,0 +1,93 @@
/*
* 模式卡片
* 水平布局:图标区 | 文字区 | 行动按钮
* 三种模式各用色条区分(无伪元素装饰)
*/
.mode-card {
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);
}
.mode-card:active {
transform: scale(0.985);
}
/* ── 图标区 ── */
.card-icon-wrap {
width: 88rpx;
height: 88rpx;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-right: var(--space-md);
}
.card-icon {
font-size: 44rpx;
line-height: 1;
}
/* ── 文字区 ── */
.card-body {
flex: 1;
min-width: 0;
}
.card-title {
font-size: var(--text-body);
font-weight: 600;
color: var(--color-text);
margin-bottom: 4rpx;
}
.card-desc {
font-size: var(--text-body-sm);
color: var(--color-text-muted);
}
/* ── 行动按钮 ── */
.card-action {
flex-shrink: 0;
font-size: var(--text-body-sm);
font-weight: 500;
padding: var(--space-xs) var(--space-md);
border-radius: var(--radius-full);
white-space: nowrap;
}
/* ═══ 各模式专属色 ═══ */
/* 外卖 — 橙 */
.mode-card.takeout .card-icon-wrap {
background: var(--color-primary-pale);
}
.mode-card.takeout .card-action {
color: var(--color-primary);
background: var(--color-primary-pale);
}
/* 冰箱 — 绿 */
.mode-card.fridge .card-icon-wrap {
background: var(--color-green-pale);
}
.mode-card.fridge .card-action {
color: var(--color-green);
background: var(--color-green-pale);
}
/* 探店 — 紫 */
.mode-card.explore .card-icon-wrap {
background: var(--color-purple-pale);
}
.mode-card.explore .card-action {
color: var(--color-purple);
background: var(--color-purple-pale);
}

View File

@@ -0,0 +1,13 @@
Component({
properties: {
imageUrl: { type: String, value: '' },
name: { type: String, value: '' },
rating: { type: Number, value: 0 },
sales: { type: String, value: '' },
avgPrice: { type: Number, value: 0 },
distance: { type: String, value: '' },
extra: { type: String, value: '' },
recommendReason: { type: String, value: '' },
signatureDishes: { type: Array, value: [] }
}
});

View File

@@ -0,0 +1,3 @@
{
"component": true
}

View File

@@ -0,0 +1,21 @@
<view class="result-card">
<image class="result-image" src="{{imageUrl}}" mode="aspectFill" wx:if="{{imageUrl}}"></image>
<view class="result-body">
<view class="result-rating" wx:if="{{rating}}">
<text class="star">⭐</text>
<text>{{rating}}</text>
<text wx:if="{{sales}}"> 月销{{sales}}</text>
</view>
<view class="result-name">{{name}}</view>
<view class="result-info" wx:if="{{avgPrice || distance}}">
<text wx:if="{{avgPrice}}">¥{{avgPrice}}/人</text>
<text wx:if="{{avgPrice && distance}}"> · </text>
<text wx:if="{{distance}}">{{distance}}</text>
</view>
<view class="result-extra" wx:if="{{extra}}">{{extra}}</view>
<view class="result-recommend" wx:if="{{recommendReason}}">"{{recommendReason}}"</view>
<view class="result-dishes" wx:if="{{signatureDishes && signatureDishes.length}}">
<text class="tag tag-orange" wx:for="{{signatureDishes}}" wx:key="*this">{{item}}</text>
</view>
</view>
</view>

View File

@@ -0,0 +1,81 @@
/*
* 结果卡片 — 外卖/探店共用
*/
.result-card {
width: 100%;
background: var(--color-surface);
border-radius: var(--radius-md);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
/* ── 图片 ── */
.result-image {
width: 100%;
height: 340rpx;
background: #F3F0EC;
}
/* ── 内容 ── */
.result-body {
padding: var(--space-md);
}
/* 评分 */
.result-rating {
display: inline-flex;
align-items: center;
font-size: var(--text-body-sm);
font-weight: 600;
color: #E8A040;
margin-bottom: 8rpx;
}
.result-rating .star {
margin-right: 4rpx;
}
/* 名称 */
.result-name {
font-size: var(--text-headline);
font-weight: 700;
color: var(--color-text);
margin-bottom: 8rpx;
}
/* 信息 */
.result-info {
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
margin-bottom: 12rpx;
}
/* 配送 */
.result-extra {
font-size: var(--text-body-sm);
color: var(--color-text-muted);
margin-bottom: 12rpx;
padding: var(--space-xs) var(--space-sm);
background: #FAFAF8;
border-radius: var(--radius-sm);
}
/* 推荐语 */
.result-recommend {
font-size: var(--text-body-sm);
color: var(--color-primary);
margin-bottom: 16rpx;
}
/* 标签 */
.result-dishes {
display: flex;
flex-wrap: wrap;
}
.result-dishes .tag {
font-size: var(--text-caption);
margin-right: 8rpx;
margin-bottom: 8rpx;
}