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:
79
miniapp/pages/explore-result/explore-result.js
Normal file
79
miniapp/pages/explore-result/explore-result.js
Normal 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);
|
||||
}
|
||||
});
|
||||
7
miniapp/pages/explore-result/explore-result.json
Normal file
7
miniapp/pages/explore-result/explore-result.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"result-card": "/components/result-card/result-card",
|
||||
"box-animation": "/components/box-animation/box-animation"
|
||||
},
|
||||
"navigationBarTitleText": "探店盲盒"
|
||||
}
|
||||
37
miniapp/pages/explore-result/explore-result.wxml
Normal file
37
miniapp/pages/explore-result/explore-result.wxml
Normal 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>
|
||||
92
miniapp/pages/explore-result/explore-result.wxss
Normal file
92
miniapp/pages/explore-result/explore-result.wxss
Normal 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;
|
||||
}
|
||||
165
miniapp/pages/fridge-input/fridge-input.js
Normal file
165
miniapp/pages/fridge-input/fridge-input.js
Normal 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 });
|
||||
}
|
||||
});
|
||||
6
miniapp/pages/fridge-input/fridge-input.json
Normal file
6
miniapp/pages/fridge-input/fridge-input.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"ingredient-tag": "/components/ingredient-tag/ingredient-tag"
|
||||
},
|
||||
"navigationBarTitleText": "冰箱盲盒"
|
||||
}
|
||||
66
miniapp/pages/fridge-input/fridge-input.wxml
Normal file
66
miniapp/pages/fridge-input/fridge-input.wxml
Normal 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>
|
||||
206
miniapp/pages/fridge-input/fridge-input.wxss
Normal file
206
miniapp/pages/fridge-input/fridge-input.wxss
Normal 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;
|
||||
}
|
||||
89
miniapp/pages/index/index.js
Normal file
89
miniapp/pages/index/index.js
Normal 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 });
|
||||
}
|
||||
});
|
||||
6
miniapp/pages/index/index.json
Normal file
6
miniapp/pages/index/index.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"mode-card": "/components/mode-card/mode-card"
|
||||
},
|
||||
"navigationBarTitleText": "吃啥盲盒"
|
||||
}
|
||||
41
miniapp/pages/index/index.wxml
Normal file
41
miniapp/pages/index/index.wxml
Normal 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>
|
||||
85
miniapp/pages/index/index.wxss
Normal file
85
miniapp/pages/index/index.wxss
Normal 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;
|
||||
}
|
||||
45
miniapp/pages/mine/mine.js
Normal file
45
miniapp/pages/mine/mine.js
Normal 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', []);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
3
miniapp/pages/mine/mine.json
Normal file
3
miniapp/pages/mine/mine.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "我的"
|
||||
}
|
||||
48
miniapp/pages/mine/mine.wxml
Normal file
48
miniapp/pages/mine/mine.wxml
Normal 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">¥ 人均<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">¥¥ 人均>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>
|
||||
158
miniapp/pages/mine/mine.wxss
Normal file
158
miniapp/pages/mine/mine.wxss
Normal 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);
|
||||
}
|
||||
96
miniapp/pages/recipe-detail/recipe-detail.js
Normal file
96
miniapp/pages/recipe-detail/recipe-detail.js
Normal 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' });
|
||||
}
|
||||
}
|
||||
});
|
||||
3
miniapp/pages/recipe-detail/recipe-detail.json
Normal file
3
miniapp/pages/recipe-detail/recipe-detail.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "菜谱详情"
|
||||
}
|
||||
43
miniapp/pages/recipe-detail/recipe-detail.wxml
Normal file
43
miniapp/pages/recipe-detail/recipe-detail.wxml
Normal 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>
|
||||
196
miniapp/pages/recipe-detail/recipe-detail.wxss
Normal file
196
miniapp/pages/recipe-detail/recipe-detail.wxss
Normal 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);
|
||||
}
|
||||
82
miniapp/pages/recipe-list/recipe-list.js
Normal file
82
miniapp/pages/recipe-list/recipe-list.js
Normal 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);
|
||||
}
|
||||
});
|
||||
6
miniapp/pages/recipe-list/recipe-list.json
Normal file
6
miniapp/pages/recipe-list/recipe-list.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"box-animation": "/components/box-animation/box-animation"
|
||||
},
|
||||
"navigationBarTitleText": "菜谱推荐"
|
||||
}
|
||||
42
miniapp/pages/recipe-list/recipe-list.wxml
Normal file
42
miniapp/pages/recipe-list/recipe-list.wxml
Normal 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>
|
||||
133
miniapp/pages/recipe-list/recipe-list.wxss
Normal file
133
miniapp/pages/recipe-list/recipe-list.wxss
Normal 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;
|
||||
}
|
||||
8
miniapp/pages/records/records.js
Normal file
8
miniapp/pages/records/records.js
Normal file
@@ -0,0 +1,8 @@
|
||||
Page({
|
||||
data: { items: [] },
|
||||
onShow() { this.loadRecords(); },
|
||||
loadRecords() {
|
||||
const history = wx.getStorageSync('box_history') || [];
|
||||
this.setData({ items: history.reverse() });
|
||||
}
|
||||
});
|
||||
3
miniapp/pages/records/records.json
Normal file
3
miniapp/pages/records/records.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "开盒记录"
|
||||
}
|
||||
18
miniapp/pages/records/records.wxml
Normal file
18
miniapp/pages/records/records.wxml
Normal 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>
|
||||
81
miniapp/pages/records/records.wxss
Normal file
81
miniapp/pages/records/records.wxss
Normal 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;
|
||||
}
|
||||
138
miniapp/pages/takeout-result/takeout-result.js
Normal file
138
miniapp/pages/takeout-result/takeout-result.js
Normal 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);
|
||||
}
|
||||
});
|
||||
7
miniapp/pages/takeout-result/takeout-result.json
Normal file
7
miniapp/pages/takeout-result/takeout-result.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"result-card": "/components/result-card/result-card",
|
||||
"box-animation": "/components/box-animation/box-animation"
|
||||
},
|
||||
"navigationBarTitleText": "外卖盲盒"
|
||||
}
|
||||
41
miniapp/pages/takeout-result/takeout-result.wxml
Normal file
41
miniapp/pages/takeout-result/takeout-result.wxml
Normal 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>
|
||||
93
miniapp/pages/takeout-result/takeout-result.wxss
Normal file
93
miniapp/pages/takeout-result/takeout-result.wxss
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user