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:
30
miniapp/app.js
Normal file
30
miniapp/app.js
Normal file
@@ -0,0 +1,30 @@
|
||||
App({
|
||||
globalData: {
|
||||
userInfo: null,
|
||||
location: null,
|
||||
baseUrl: 'http://localhost:8080'
|
||||
},
|
||||
|
||||
onLaunch() {
|
||||
// 不在 onLaunch 中直接调定位,等用户操作时再申请,符合新版隐私规范
|
||||
},
|
||||
|
||||
getLocation(callback) {
|
||||
const that = this;
|
||||
wx.getLocation({
|
||||
type: 'gcj02',
|
||||
success: (res) => {
|
||||
that.globalData.location = {
|
||||
latitude: res.latitude,
|
||||
longitude: res.longitude
|
||||
};
|
||||
if (callback) callback(null, that.globalData.location);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('定位失败:', err);
|
||||
// 引导用户手动选择位置或开启定位
|
||||
if (callback) callback(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
55
miniapp/app.json
Normal file
55
miniapp/app.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/takeout-result/takeout-result",
|
||||
"pages/fridge-input/fridge-input",
|
||||
"pages/recipe-list/recipe-list",
|
||||
"pages/recipe-detail/recipe-detail",
|
||||
"pages/explore-result/explore-result",
|
||||
"pages/records/records",
|
||||
"pages/mine/mine"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationBarBackgroundColor": "#E8693B",
|
||||
"navigationBarTitleText": "吃啥盲盒",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#FFFBF7"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#999999",
|
||||
"selectedColor": "#E8693B",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"borderStyle": "white",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "盲盒大厅",
|
||||
"iconPath": "static/tab-home.png",
|
||||
"selectedIconPath": "static/tab-home-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/records/records",
|
||||
"text": "记录",
|
||||
"iconPath": "static/tab-records.png",
|
||||
"selectedIconPath": "static/tab-records-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/mine/mine",
|
||||
"text": "我的",
|
||||
"iconPath": "static/tab-mine.png",
|
||||
"selectedIconPath": "static/tab-mine-active.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
"permission": {
|
||||
"scope.userLocation": {
|
||||
"desc": "需要获取你的位置来推荐附近的美食"
|
||||
}
|
||||
},
|
||||
"requiredPrivateInfos": [
|
||||
"getLocation"
|
||||
],
|
||||
"style": "v2",
|
||||
"sitemapLocation": "sitemap.json"
|
||||
}
|
||||
135
miniapp/app.wxss
Normal file
135
miniapp/app.wxss
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* ChowBox 全局设计系统 · 高级简约
|
||||
* 统一 8rpx 间距 · 3 级圆角 · 5 级字号 · 3 级阴影
|
||||
*/
|
||||
|
||||
/* ═══ 设计令牌 ═══ */
|
||||
page {
|
||||
/* 主色系 — 暖橙 */
|
||||
--color-primary: #E8693B;
|
||||
--color-primary-light: #F0976E;
|
||||
--color-primary-pale: #FFF3EE;
|
||||
--color-primary-ghost: rgba(232, 105, 59, 0.08);
|
||||
|
||||
/* 语义色 */
|
||||
--color-green: #4CAF50;
|
||||
--color-green-pale: #EDF7EE;
|
||||
--color-purple: #7C3AED;
|
||||
--color-purple-pale: #F4F0FF;
|
||||
|
||||
/* 表面色 */
|
||||
--color-bg: #FFFBF7;
|
||||
--color-surface: #FFFFFF;
|
||||
--color-hairline: #F2EDE8;
|
||||
|
||||
/* 文字色 */
|
||||
--color-text: #1A1A2E;
|
||||
--color-text-secondary: #8C8C8C;
|
||||
--color-text-muted: #B0B0B0;
|
||||
|
||||
/* 圆角 — 3 级 */
|
||||
--radius-sm: 8rpx;
|
||||
--radius-md: 16rpx;
|
||||
--radius-lg: 24rpx;
|
||||
--radius-full: 999rpx;
|
||||
|
||||
/* 阴影 — 3 级(极简克制) */
|
||||
--shadow-sm: 0 1rpx 4rpx rgba(0, 0, 0, 0.04);
|
||||
--shadow-md: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
--shadow-lg: 0 4rpx 24rpx rgba(0, 0, 0, 0.07);
|
||||
|
||||
/* 间距 — 8rpx 基数 */
|
||||
--space-xs: 8rpx;
|
||||
--space-sm: 16rpx;
|
||||
--space-md: 24rpx;
|
||||
--space-lg: 32rpx;
|
||||
--space-xl: 48rpx;
|
||||
--space-2xl: 64rpx;
|
||||
|
||||
/* 字号 — 5 级 */
|
||||
--text-caption: 22rpx;
|
||||
--text-body-sm: 24rpx;
|
||||
--text-body: 28rpx;
|
||||
--text-subtitle: 32rpx;
|
||||
--text-title: 36rpx;
|
||||
--text-headline: 40rpx;
|
||||
|
||||
/* 动画 */
|
||||
--ease-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
|
||||
/* 基础样式 */
|
||||
background-color: var(--color-bg);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", sans-serif;
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-text);
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ═══ 主按钮 ═══ */
|
||||
.btn-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
background: var(--color-primary);
|
||||
color: #FFFFFF;
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: var(--text-body);
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary::after {
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
opacity: 0.85;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-primary[disabled] {
|
||||
opacity: 0.35;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* ═══ 通用卡片 ═══ */
|
||||
.card {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ═══ 标签 ═══ */
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: var(--text-caption);
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: var(--radius-full);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-orange { color: var(--color-primary); background: var(--color-primary-pale); }
|
||||
.tag-green { color: var(--color-green); background: var(--color-green-pale); }
|
||||
.tag-purple { color: var(--color-purple); background: var(--color-purple-pale); }
|
||||
|
||||
/* ═══ 安全区 ═══ */
|
||||
.safe-bottom {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.safe-top {
|
||||
padding-top: constant(safe-area-inset-top);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
}
|
||||
105
miniapp/components/box-animation/box-animation.js
Normal file
105
miniapp/components/box-animation/box-animation.js
Normal 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: 光晕扩散 + 盒子震动 (0–400ms) ── */
|
||||
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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
3
miniapp/components/box-animation/box-animation.json
Normal file
3
miniapp/components/box-animation/box-animation.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"component": true
|
||||
}
|
||||
52
miniapp/components/box-animation/box-animation.wxml
Normal file
52
miniapp/components/box-animation/box-animation.wxml
Normal 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>
|
||||
304
miniapp/components/box-animation/box-animation.wxss
Normal file
304
miniapp/components/box-animation/box-animation.wxss
Normal file
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
* 开盒动效 · 三幕分镜
|
||||
* 参考 doc/box.md
|
||||
*
|
||||
* Act 1 (0–400ms): 光晕扩散 + 盒子震动 + 背景变暗
|
||||
* Act 2 (400–1000ms): 盒盖飞起 + 金色光芒 + 粒子溅射 + 白色闪屏
|
||||
* Act 3 (1000–1800ms): 内容弹性弹出 + 彩带纸屑
|
||||
*
|
||||
* 总时长约 1.8–2.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 · 光晕扩散 (0–400ms)
|
||||
════════════════════════ */
|
||||
|
||||
.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 · 白色闪屏 (600–1000ms)
|
||||
════════════════════════ */
|
||||
|
||||
.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 · 内容弹性弹出 (1000–1800ms)
|
||||
════════════════════════ */
|
||||
|
||||
.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); }
|
||||
}
|
||||
12
miniapp/components/ingredient-tag/ingredient-tag.js
Normal file
12
miniapp/components/ingredient-tag/ingredient-tag.js
Normal 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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
3
miniapp/components/ingredient-tag/ingredient-tag.json
Normal file
3
miniapp/components/ingredient-tag/ingredient-tag.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"component": true
|
||||
}
|
||||
4
miniapp/components/ingredient-tag/ingredient-tag.wxml
Normal file
4
miniapp/components/ingredient-tag/ingredient-tag.wxml
Normal 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>
|
||||
27
miniapp/components/ingredient-tag/ingredient-tag.wxss
Normal file
27
miniapp/components/ingredient-tag/ingredient-tag.wxss
Normal 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;
|
||||
}
|
||||
14
miniapp/components/mode-card/mode-card.js
Normal file
14
miniapp/components/mode-card/mode-card.js
Normal 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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
3
miniapp/components/mode-card/mode-card.json
Normal file
3
miniapp/components/mode-card/mode-card.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"component": true
|
||||
}
|
||||
10
miniapp/components/mode-card/mode-card.wxml
Normal file
10
miniapp/components/mode-card/mode-card.wxml
Normal 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>
|
||||
93
miniapp/components/mode-card/mode-card.wxss
Normal file
93
miniapp/components/mode-card/mode-card.wxss
Normal 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);
|
||||
}
|
||||
13
miniapp/components/result-card/result-card.js
Normal file
13
miniapp/components/result-card/result-card.js
Normal 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: [] }
|
||||
}
|
||||
});
|
||||
3
miniapp/components/result-card/result-card.json
Normal file
3
miniapp/components/result-card/result-card.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"component": true
|
||||
}
|
||||
21
miniapp/components/result-card/result-card.wxml
Normal file
21
miniapp/components/result-card/result-card.wxml
Normal 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>
|
||||
81
miniapp/components/result-card/result-card.wxss
Normal file
81
miniapp/components/result-card/result-card.wxss
Normal 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
44
miniapp/project.config.json
Normal file
44
miniapp/project.config.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"description": "吃啥盲盒 ChowBox",
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"es6": true,
|
||||
"enhance": true,
|
||||
"postcss": true,
|
||||
"preloadBackgroundData": false,
|
||||
"minified": true,
|
||||
"newFeature": true,
|
||||
"coverView": true,
|
||||
"nodeModules": false,
|
||||
"autoAudits": false,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"scopeDataCheck": false,
|
||||
"uglifyFileName": false,
|
||||
"checkInvalidKey": true,
|
||||
"checkSiteMap": true,
|
||||
"uploadWithSourceMap": true,
|
||||
"compileHotReLoad": false,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"useMultiFrameRuntime": true,
|
||||
"useApiHook": true,
|
||||
"useApiHostProcess": true,
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
}
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"libVersion": "3.15.2",
|
||||
"appid": "wxf634a69e89290cc7",
|
||||
"projectname": "chowbox",
|
||||
"condition": {},
|
||||
"editorSetting": {
|
||||
"tabIndent": "insertSpaces",
|
||||
"tabSize": 2
|
||||
}
|
||||
}
|
||||
6
miniapp/sitemap.json
Normal file
6
miniapp/sitemap.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"rules": [{
|
||||
"action": "allow",
|
||||
"page": "*"
|
||||
}]
|
||||
}
|
||||
BIN
miniapp/static/tab-home-active.png
Normal file
BIN
miniapp/static/tab-home-active.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
miniapp/static/tab-home.png
Normal file
BIN
miniapp/static/tab-home.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
miniapp/static/tab-mine-active.png
Normal file
BIN
miniapp/static/tab-mine-active.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
BIN
miniapp/static/tab-mine.png
Normal file
BIN
miniapp/static/tab-mine.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
BIN
miniapp/static/tab-records-active.png
Normal file
BIN
miniapp/static/tab-records-active.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
miniapp/static/tab-records.png
Normal file
BIN
miniapp/static/tab-records.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
88
miniapp/utils/api.js
Normal file
88
miniapp/utils/api.js
Normal file
@@ -0,0 +1,88 @@
|
||||
const app = getApp();
|
||||
|
||||
// 客户端缓存:key → { data, expireAt }
|
||||
const CACHE_PREFIX = 'api_cache_';
|
||||
const DEFAULT_TTL = 30 * 60 * 1000; // 默认 30 分钟
|
||||
|
||||
function request(method, path, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: app.globalData.baseUrl + path,
|
||||
method: method,
|
||||
data: data,
|
||||
header: { 'Content-Type': 'application/json' },
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data.code === 200) {
|
||||
resolve(res.data.data);
|
||||
} else {
|
||||
reject(res.data);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 带客户端缓存的 GET 请求
|
||||
* @param {string} path 请求路径
|
||||
* @param {object} data 请求参数
|
||||
* @param {number} ttlMs 缓存时长(毫秒),默认 30 分钟
|
||||
*/
|
||||
function getWithCache(path, data, ttlMs) {
|
||||
const cacheKey = CACHE_PREFIX + path + '_' + JSON.stringify(data || {});
|
||||
const ttl = ttlMs || DEFAULT_TTL;
|
||||
|
||||
// 读缓存
|
||||
try {
|
||||
const cached = wx.getStorageSync(cacheKey);
|
||||
if (cached && cached.expireAt > Date.now()) {
|
||||
return Promise.resolve(cached.data);
|
||||
}
|
||||
} catch (e) {
|
||||
// 读取失败,忽略
|
||||
}
|
||||
|
||||
return request('GET', path, data).then((result) => {
|
||||
// 写缓存
|
||||
try {
|
||||
wx.setStorageSync(cacheKey, {
|
||||
data: result,
|
||||
expireAt: Date.now() + ttl
|
||||
});
|
||||
} catch (e) {
|
||||
// 写入失败,忽略(存储满了等)
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除过期缓存
|
||||
*/
|
||||
function clearExpiredCache() {
|
||||
try {
|
||||
const info = wx.getStorageInfoSync();
|
||||
const keys = info.keys || [];
|
||||
const now = Date.now();
|
||||
keys.forEach((key) => {
|
||||
if (key.startsWith(CACHE_PREFIX)) {
|
||||
const cached = wx.getStorageSync(key);
|
||||
if (cached && cached.expireAt < now) {
|
||||
wx.removeStorageSync(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// 静默失败
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
get: (path, data) => request('GET', path, data),
|
||||
post: (path, data) => request('POST', path, data),
|
||||
getWithCache,
|
||||
clearExpiredCache
|
||||
};
|
||||
37
miniapp/utils/location.js
Normal file
37
miniapp/utils/location.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const app = getApp();
|
||||
|
||||
function getLocation() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cached = app.globalData.location;
|
||||
if (cached) {
|
||||
resolve(cached);
|
||||
return;
|
||||
}
|
||||
|
||||
wx.getLocation({
|
||||
type: 'gcj02',
|
||||
success: (res) => {
|
||||
const loc = { latitude: res.latitude, longitude: res.longitude };
|
||||
app.globalData.location = loc;
|
||||
resolve(loc);
|
||||
},
|
||||
fail: (err) => {
|
||||
// 常见错误:未授权、隐私协议未同意
|
||||
const msg = err.errMsg || '';
|
||||
if (msg.indexOf('auth deny') >= 0 || msg.indexOf('authorize') >= 0) {
|
||||
wx.showModal({
|
||||
title: '需要定位权限',
|
||||
content: '请在设置中允许小程序获取位置,或点击右上角手动选择位置',
|
||||
confirmText: '去设置',
|
||||
success: (res) => {
|
||||
if (res.confirm) wx.openSetting();
|
||||
}
|
||||
});
|
||||
}
|
||||
reject(new Error('定位失败'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { getLocation };
|
||||
18
miniapp/utils/storage.js
Normal file
18
miniapp/utils/storage.js
Normal file
@@ -0,0 +1,18 @@
|
||||
function get(key, defaultValue) {
|
||||
try {
|
||||
const value = wx.getStorageSync(key);
|
||||
return value !== '' ? value : defaultValue;
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
function set(key, value) {
|
||||
wx.setStorageSync(key, value);
|
||||
}
|
||||
|
||||
function remove(key) {
|
||||
wx.removeStorageSync(key);
|
||||
}
|
||||
|
||||
module.exports = { get, set, remove };
|
||||
Reference in New Issue
Block a user