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:
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# IDE
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# Build output
|
||||
target/
|
||||
build/
|
||||
dist/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# WeChat
|
||||
miniapp/project.private.config.json
|
||||
108
CLAUDE.md
Normal file
108
CLAUDE.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project: ChowBox (吃啥盲盒)
|
||||
|
||||
WeChat Mini Program for solving "what to eat" decisions. Three modes:
|
||||
- **Takeout Box (外卖盲盒)**: Random nearby quality delivery recommendation, jump to Meituan/Ele.me to order.
|
||||
- **Fridge Box (冰箱盲盒)**: Input ingredients, get recipe matches ranked by match %, highlight missing items.
|
||||
- **Explore Box (探店盲盒)**: Random nearby dine-in restaurant discovery, navigate via map apps.
|
||||
|
||||
**Status**: Pre-development planning. Zero code written. Docs in `doc/` are the source of truth.
|
||||
|
||||
### Tech Stack
|
||||
- **Frontend**: Native WeChat Mini Program (WXML + WXSS + JavaScript), WeChat Developer Tools
|
||||
- **Backend**: Java 8 + Spring Boot 2.x (classic MVC)
|
||||
- **Database**: MySQL 5.7+ (primary) + Redis (cache)
|
||||
- **External APIs**: Amap Web API (POI search, primary), Tencent Location Service (backup), WeChat Open APIs (navigation, subscribe messages)
|
||||
|
||||
### Design Tokens
|
||||
- Primary: `#FF6A3D` (orange), Background: `#FFFBF4` (warm white), Text: `#333`, Accent: `#4CAF50`
|
||||
- Card border-radius: 16px, Button border-radius: 24px
|
||||
- Font: WeChat default Chinese font, bold titles at 16-20px
|
||||
|
||||
### Architecture
|
||||
- Bottom tab navigation: Home (盲盒大厅), Records (记录), My (我的)
|
||||
- Data flow: user location → backend → map POI API (cached by geo-grid + time) → filtering/weighting → weighted random selection → result
|
||||
- POI cache: 1h client-side, 6h server-side for hot zones
|
||||
- Box opening animation: 1.8-2.2s, 3-act (shake → lid open with glow → card reveal), Lottie + CSS keyframes, degrade to CSS-only on low-end devices (see `doc/box.md`)
|
||||
|
||||
### Key Docs
|
||||
- `doc/plan.md` — Full product plan v1.0 (features, architecture, roadmap, budget)
|
||||
- `doc/box.md` — Opening animation storyboard and dev parameters
|
||||
- `doc/ui.md` — ASCII wireframes for all 6 core pages
|
||||
|
||||
### Roadmap
|
||||
- **Phase 1 (MVP, 4-6 weeks)**: Scaffold mini program + Spring Boot backend, MySQL schema, Amap POI integration for takeout box, 100 recipes, basic box animation
|
||||
- **Phase 2 (3-4 weeks)**: Explore box, voice ingredient input, user feedback weighting, 500+ recipes
|
||||
- **Phase 3 (2-3 weeks)**: User preferences, share cards, subscribe messages, perf optimization
|
||||
- **Phase 4**: Testing, WeChat review submission, iteration
|
||||
|
||||
No build/lint/test commands exist yet.
|
||||
|
||||
---
|
||||
|
||||
## Behavioral Guidelines
|
||||
|
||||
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||
|
||||
## 1. Think Before Coding
|
||||
|
||||
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||
|
||||
Before implementing:
|
||||
- State your assumptions explicitly. If uncertain, ask.
|
||||
- If multiple interpretations exist, present them - don't pick silently.
|
||||
- If a simpler approach exists, say so. Push back when warranted.
|
||||
- If something is unclear, stop. Name what's confusing. Ask.
|
||||
|
||||
## 2. Simplicity First
|
||||
|
||||
**Minimum code that solves the problem. Nothing speculative.**
|
||||
|
||||
- No features beyond what was asked.
|
||||
- No abstractions for single-use code.
|
||||
- No "flexibility" or "configurability" that wasn't requested.
|
||||
- No error handling for impossible scenarios.
|
||||
- If you write 200 lines and it could be 50, rewrite it.
|
||||
|
||||
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
||||
|
||||
## 3. Surgical Changes
|
||||
|
||||
**Touch only what you must. Clean up only your own mess.**
|
||||
|
||||
When editing existing code:
|
||||
- Don't "improve" adjacent code, comments, or formatting.
|
||||
- Don't refactor things that aren't broken.
|
||||
- Match existing style, even if you'd do it differently.
|
||||
- If you notice unrelated dead code, mention it - don't delete it.
|
||||
|
||||
When your changes create orphans:
|
||||
- Remove imports/variables/functions that YOUR changes made unused.
|
||||
- Don't remove pre-existing dead code unless asked.
|
||||
|
||||
The test: Every changed line should trace directly to the user's request.
|
||||
|
||||
## 4. Goal-Driven Execution
|
||||
|
||||
**Define success criteria. Loop until verified.**
|
||||
|
||||
Transform tasks into verifiable goals:
|
||||
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
||||
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
||||
- "Refactor X" → "Ensure tests pass before and after"
|
||||
|
||||
For multi-step tasks, state a brief plan:
|
||||
```
|
||||
1. [Step] → verify: [check]
|
||||
2. [Step] → verify: [check]
|
||||
3. [Step] → verify: [check]
|
||||
```
|
||||
|
||||
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
||||
|
||||
---
|
||||
|
||||
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
|
||||
69
backend/pom.xml
Normal file
69
backend/pom.xml
Normal file
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.7.18</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.chowbox</groupId>
|
||||
<artifactId>chowbox-backend</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>3.5.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>8.0.33</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
13
backend/src/main/java/com/chowbox/ChowBoxApplication.java
Normal file
13
backend/src/main/java/com/chowbox/ChowBoxApplication.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.chowbox;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.chowbox.mapper")
|
||||
public class ChowBoxApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ChowBoxApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.chowbox.config;
|
||||
|
||||
import com.chowbox.model.ApiResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ApiResponse<?> handleException(Exception e) {
|
||||
log.error("Unhandled exception: {}", e.getMessage(), e);
|
||||
return ApiResponse.fail(500, "服务器出了点小差,请稍后再试");
|
||||
}
|
||||
}
|
||||
21
backend/src/main/java/com/chowbox/config/RedisConfig.java
Normal file
21
backend/src/main/java/com/chowbox/config/RedisConfig.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.chowbox.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(factory);
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
|
||||
return template;
|
||||
}
|
||||
}
|
||||
16
backend/src/main/java/com/chowbox/config/WebConfig.java
Normal file
16
backend/src/main/java/com/chowbox/config/WebConfig.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.chowbox.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOrigins("*")
|
||||
.allowedMethods("GET", "POST", "OPTIONS")
|
||||
.allowedHeaders("*");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.chowbox.controller;
|
||||
|
||||
import com.chowbox.model.ApiResponse;
|
||||
import com.chowbox.model.Restaurant;
|
||||
import com.chowbox.service.TakeoutService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/explore")
|
||||
public class ExploreController {
|
||||
|
||||
@Autowired
|
||||
private TakeoutService takeoutService;
|
||||
|
||||
@PostMapping("/roll")
|
||||
public ApiResponse<Restaurant> roll(@RequestBody Map<String, Object> body) {
|
||||
double lat = Double.parseDouble(String.valueOf(body.get("latitude")));
|
||||
double lng = Double.parseDouble(String.valueOf(body.get("longitude")));
|
||||
String openid = (String) body.getOrDefault("openid", "anonymous");
|
||||
|
||||
// Phase 2: 差异化过滤条件(堂食场景)。Phase 1 复用外卖逻辑
|
||||
Restaurant result = takeoutService.roll(lat, lng, openid, "都可以", "all", "");
|
||||
if (result == null) {
|
||||
return ApiResponse.fail(404, "附近暂无推荐好店,换片区域试试?");
|
||||
}
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.chowbox.controller;
|
||||
|
||||
import com.chowbox.model.ApiResponse;
|
||||
import com.chowbox.model.FridgeMatchResult;
|
||||
import com.chowbox.service.FridgeService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/fridge")
|
||||
public class FridgeController {
|
||||
|
||||
@Autowired
|
||||
private FridgeService fridgeService;
|
||||
|
||||
@PostMapping("/match")
|
||||
public ApiResponse<List<FridgeMatchResult>> match(@RequestBody Map<String, Object> body) {
|
||||
Object ingObj = body.get("ingredients");
|
||||
log.info("Fridge match request: ingredients type={}, value={}", ingObj != null ? ingObj.getClass().getSimpleName() : "null", ingObj);
|
||||
|
||||
List<String> ingredients = new ArrayList<>();
|
||||
if (ingObj instanceof List) {
|
||||
for (Object item : (List<?>) ingObj) {
|
||||
if (item != null) ingredients.add(item.toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (ingredients.isEmpty()) {
|
||||
return ApiResponse.fail(400, "请输入至少一种食材");
|
||||
}
|
||||
|
||||
// 接收前端自定义常备调料
|
||||
List<String> staples = new ArrayList<>();
|
||||
Object stapleObj = body.get("staples");
|
||||
if (stapleObj instanceof List) {
|
||||
for (Object item : (List<?>) stapleObj) {
|
||||
if (item != null) staples.add(item.toString());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Fridge match: parsed ingredients={}, staples={}", ingredients, staples);
|
||||
List<FridgeMatchResult> results = fridgeService.matchRecipes(ingredients, staples);
|
||||
log.info("Fridge match: {} results", results.size());
|
||||
return ApiResponse.ok(results);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.chowbox.controller;
|
||||
|
||||
import com.chowbox.model.ApiResponse;
|
||||
import com.chowbox.model.Recipe;
|
||||
import com.chowbox.service.FridgeService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/recipe")
|
||||
public class RecipeController {
|
||||
|
||||
@Autowired
|
||||
private FridgeService fridgeService;
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<Recipe> detail(@PathVariable Long id) {
|
||||
Recipe recipe = fridgeService.getRecipeDetail(id);
|
||||
if (recipe == null) {
|
||||
return ApiResponse.fail(404, "菜谱不存在");
|
||||
}
|
||||
return ApiResponse.ok(recipe);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.chowbox.controller;
|
||||
|
||||
import com.chowbox.model.ApiResponse;
|
||||
import com.chowbox.model.Restaurant;
|
||||
import com.chowbox.service.TakeoutService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/takeout")
|
||||
public class TakeoutController {
|
||||
|
||||
@Autowired
|
||||
private TakeoutService takeoutService;
|
||||
|
||||
@PostMapping("/roll")
|
||||
public ApiResponse<Restaurant> roll(@RequestBody Map<String, Object> body) {
|
||||
double lat = Double.parseDouble(String.valueOf(body.get("latitude")));
|
||||
double lng = Double.parseDouble(String.valueOf(body.get("longitude")));
|
||||
String openid = (String) body.getOrDefault("openid", "anonymous");
|
||||
String taste = (String) body.getOrDefault("taste", "都可以");
|
||||
String priceRange = (String) body.getOrDefault("priceRange", "all");
|
||||
String allergies = (String) body.getOrDefault("allergies", "");
|
||||
|
||||
Restaurant result = takeoutService.roll(lat, lng, openid, taste, priceRange, allergies);
|
||||
if (result == null) {
|
||||
return ApiResponse.fail(404, "附近暂无合适的商家,换个区域试试吧");
|
||||
}
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.chowbox.controller;
|
||||
|
||||
import com.chowbox.model.ApiResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 语音识别接口(STT stub)。
|
||||
* 当前返回空文本,后续可接入阿里云/腾讯云 ASR 服务。
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/voice")
|
||||
public class VoiceController {
|
||||
|
||||
@PostMapping("/recognize")
|
||||
public ApiResponse<Map<String, String>> recognize(@RequestParam("audio") MultipartFile audio) {
|
||||
log.info("Voice recognize: file={}, size={}bytes", audio.getOriginalFilename(), audio.getSize());
|
||||
|
||||
// TODO: 接入 ASR 服务,对音频做语音识别
|
||||
// 目前 stub 返回空结果,前端会 fallback 提示用户手动输入
|
||||
|
||||
return ApiResponse.ok(Collections.singletonMap("text", ""));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.chowbox.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.chowbox.model.RecipeIngredient;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface RecipeIngredientMapper extends BaseMapper<RecipeIngredient> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.chowbox.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.chowbox.model.Recipe;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface RecipeMapper extends BaseMapper<Recipe> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.chowbox.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.chowbox.model.RecipeStep;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface RecipeStepMapper extends BaseMapper<RecipeStep> {
|
||||
}
|
||||
25
backend/src/main/java/com/chowbox/model/ApiResponse.java
Normal file
25
backend/src/main/java/com/chowbox/model/ApiResponse.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.chowbox.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ApiResponse<T> {
|
||||
private int code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public static <T> ApiResponse<T> ok(T data) {
|
||||
ApiResponse<T> r = new ApiResponse<>();
|
||||
r.code = 200;
|
||||
r.message = "success";
|
||||
r.data = data;
|
||||
return r;
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> fail(int code, String message) {
|
||||
ApiResponse<T> r = new ApiResponse<>();
|
||||
r.code = code;
|
||||
r.message = message;
|
||||
return r;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.chowbox.model;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class FridgeMatchResult {
|
||||
private Recipe recipe;
|
||||
private double matchRate; // 匹配度 0-100
|
||||
private List<String> missingIngredients; // 缺少的食材
|
||||
}
|
||||
25
backend/src/main/java/com/chowbox/model/Recipe.java
Normal file
25
backend/src/main/java/com/chowbox/model/Recipe.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.chowbox.model;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@TableName("recipes")
|
||||
public class Recipe {
|
||||
private Long id;
|
||||
private String name;
|
||||
private Integer difficulty;
|
||||
private Integer cookTime;
|
||||
private String imageUrl;
|
||||
private String category;
|
||||
private String season;
|
||||
private String mealTime;
|
||||
|
||||
@TableField(exist = false)
|
||||
private List<RecipeIngredient> ingredients;
|
||||
|
||||
@TableField(exist = false)
|
||||
private List<RecipeStep> steps;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.chowbox.model;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("recipe_ingredients")
|
||||
public class RecipeIngredient {
|
||||
private Long id;
|
||||
private Long recipeId;
|
||||
private String ingredientName;
|
||||
private String amount;
|
||||
private Integer isStaple;
|
||||
}
|
||||
14
backend/src/main/java/com/chowbox/model/RecipeStep.java
Normal file
14
backend/src/main/java/com/chowbox/model/RecipeStep.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.chowbox.model;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("recipe_steps")
|
||||
public class RecipeStep {
|
||||
private Long id;
|
||||
private Long recipeId;
|
||||
private Integer stepOrder;
|
||||
private String content;
|
||||
private String imageUrl;
|
||||
}
|
||||
24
backend/src/main/java/com/chowbox/model/Restaurant.java
Normal file
24
backend/src/main/java/com/chowbox/model/Restaurant.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.chowbox.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class Restaurant {
|
||||
private String id;
|
||||
private String name;
|
||||
private String address;
|
||||
private Double rating;
|
||||
private Integer monthlySales;
|
||||
private Double avgPrice;
|
||||
private Double distance;
|
||||
private Double longitude;
|
||||
private Double latitude;
|
||||
private String deliveryTime;
|
||||
private String deliveryFee;
|
||||
private String minOrder;
|
||||
private String phone;
|
||||
private String imageUrl;
|
||||
private String category;
|
||||
private String recommendReason;
|
||||
private String[] signatureDishes;
|
||||
}
|
||||
197
backend/src/main/java/com/chowbox/service/AmapService.java
Normal file
197
backend/src/main/java/com/chowbox/service/AmapService.java
Normal file
@@ -0,0 +1,197 @@
|
||||
package com.chowbox.service;
|
||||
|
||||
import com.chowbox.model.Restaurant;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AmapService {
|
||||
|
||||
private static final String AMAP_AROUND_URL = "https://restapi.amap.com/v3/place/around";
|
||||
|
||||
@Value("${amap.api-key}")
|
||||
private String apiKey;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<Restaurant> searchNearbyPOI(double lat, double lng, int radius) {
|
||||
String gridKey = String.format("poi:%.3f:%.3f:restaurant",
|
||||
Math.floor(lat * 10) / 10, Math.floor(lng * 10) / 10);
|
||||
|
||||
List<Restaurant> cached = (List<Restaurant>) redisTemplate.opsForValue().get(gridKey);
|
||||
if (cached != null && !cached.isEmpty()) {
|
||||
log.info("Amap cache hit: key={}, size={}", gridKey, cached.size());
|
||||
return cached;
|
||||
}
|
||||
|
||||
String url = String.format(
|
||||
"%s?key=%s&location=%f,%f&radius=%d&types=050000&offset=50&extensions=all",
|
||||
AMAP_AROUND_URL, apiKey, lng, lat, radius);
|
||||
|
||||
log.info("Amap API call: location={},{} radius={}", lat, lng, radius);
|
||||
|
||||
try {
|
||||
Map<String, Object> response = restTemplate.getForObject(url, Map.class);
|
||||
List<Restaurant> results = parseResponse(response, lat, lng);
|
||||
log.info("Amap parsed: {} restaurants", results.size());
|
||||
if (!results.isEmpty()) {
|
||||
redisTemplate.opsForValue().set(gridKey, results, 1, TimeUnit.HOURS);
|
||||
}
|
||||
return results;
|
||||
} catch (Exception e) {
|
||||
log.error("Amap API error: {}", e.getMessage(), e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Restaurant> parseResponse(Map<String, Object> response, double centerLat, double centerLng) {
|
||||
if (response == null) {
|
||||
log.warn("Amap response is null");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String status = String.valueOf(response.get("status"));
|
||||
if (!"1".equals(status)) {
|
||||
log.warn("Amap response status={}, info={}", status, response.get("info"));
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Object poisObj = response.get("pois");
|
||||
if (poisObj == null) {
|
||||
log.warn("Amap response has no 'pois' key");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Map<String, Object>> pois = (List<Map<String, Object>>) poisObj;
|
||||
log.info("Amap POIs count: {}", pois.size());
|
||||
|
||||
return pois.stream().map(poi -> parseRestaurant(poi, centerLat, centerLng))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Restaurant parseRestaurant(Map<String, Object> poi, double centerLat, double centerLng) {
|
||||
Restaurant r = new Restaurant();
|
||||
r.setId(String.valueOf(poi.get("id")));
|
||||
r.setName(String.valueOf(poi.get("name")));
|
||||
r.setAddress(String.valueOf(poi.get("address")));
|
||||
|
||||
// 坐标 - 用于后续地图导航
|
||||
try {
|
||||
String[] locParts = String.valueOf(poi.get("location")).split(",");
|
||||
if (locParts.length == 2) {
|
||||
r.setLongitude(Double.parseDouble(locParts[0]));
|
||||
r.setLatitude(Double.parseDouble(locParts[1]));
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
r.setLongitude(centerLng);
|
||||
r.setLatitude(centerLat);
|
||||
}
|
||||
|
||||
// 距离
|
||||
Object distObj = poi.get("distance");
|
||||
if (distObj instanceof String) r.setDistance(Double.parseDouble((String) distObj));
|
||||
else if (distObj instanceof Number) r.setDistance(((Number) distObj).doubleValue());
|
||||
|
||||
// biz_ext: 评分、人均
|
||||
Object bizExtObj = poi.get("biz_ext");
|
||||
if (bizExtObj instanceof Map) {
|
||||
Map<String, Object> bizExt = (Map<String, Object>) bizExtObj;
|
||||
try { r.setRating(Double.parseDouble(String.valueOf(bizExt.get("rating")))); } catch (Exception ignored) {}
|
||||
try { r.setAvgPrice(Double.parseDouble(String.valueOf(bizExt.get("cost")))); } catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// 分类
|
||||
r.setCategory(detectCategory(poi));
|
||||
|
||||
// 标签 → 用作招牌菜来源
|
||||
Object tagObj = poi.get("tag");
|
||||
if (tagObj instanceof String && !((String) tagObj).isEmpty()) {
|
||||
r.setSignatureDishes(extractTags((String) tagObj));
|
||||
} else if (tagObj instanceof List && !((List<?>) tagObj).isEmpty()) {
|
||||
List<String> tags = ((List<?>) tagObj).stream()
|
||||
.map(String::valueOf).collect(Collectors.toList());
|
||||
r.setSignatureDishes(tags.toArray(new String[0]));
|
||||
}
|
||||
|
||||
// 电话
|
||||
Object telObj = poi.get("tel");
|
||||
if (telObj instanceof String) r.setPhone((String) telObj);
|
||||
else if (telObj instanceof List) {
|
||||
r.setPhone(String.join(";", ((List<?>) telObj).stream()
|
||||
.map(String::valueOf).collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
// 照片
|
||||
Object photosObj = poi.get("photos");
|
||||
if (photosObj instanceof List) {
|
||||
List<?> photos = (List<?>) photosObj;
|
||||
if (!photos.isEmpty()) {
|
||||
Object firstPhoto = photos.get(0);
|
||||
if (firstPhoto instanceof Map) {
|
||||
Map<String, Object> photoMap = (Map<String, Object>) firstPhoto;
|
||||
Object url = photoMap.get("url");
|
||||
if (url != null) {
|
||||
r.setImageUrl(url.toString());
|
||||
if (url.toString().startsWith("http://")) {
|
||||
r.setImageUrl(url.toString().replace("http://", "https://"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 type 和 tag 字段推断可读的分类名
|
||||
*/
|
||||
private String detectCategory(Map<String, Object> poi) {
|
||||
// type 格式如 "050100|中餐厅|050100"
|
||||
String type = String.valueOf(poi.getOrDefault("type", ""));
|
||||
if (type.contains("川菜")) return "川菜";
|
||||
if (type.contains("粤菜")) return "粤菜";
|
||||
if (type.contains("湘菜")) return "湘菜";
|
||||
if (type.contains("火锅")) return "火锅";
|
||||
if (type.contains("烧烤")) return "烧烤";
|
||||
if (type.contains("日料") || type.contains("日本")) return "日料";
|
||||
if (type.contains("韩餐") || type.contains("韩国")) return "韩餐";
|
||||
if (type.contains("西餐") || type.contains("西式")) return "西餐";
|
||||
if (type.contains("快餐")) return "快餐";
|
||||
if (type.contains("小吃")) return "小吃";
|
||||
if (type.contains("咖啡")) return "咖啡";
|
||||
if (type.contains("甜品")) return "甜品";
|
||||
if (type.contains("面") || type.contains("粉")) return "面馆";
|
||||
|
||||
String tag = String.valueOf(poi.getOrDefault("tag", ""));
|
||||
if (!tag.isEmpty() && !"null".equals(tag)) {
|
||||
return tag;
|
||||
}
|
||||
return "餐饮";
|
||||
}
|
||||
|
||||
/**
|
||||
* 从标签字符串中提取关键词作为招牌菜
|
||||
*/
|
||||
private String[] extractTags(String tagStr) {
|
||||
if (tagStr == null || tagStr.isEmpty()) return null;
|
||||
return Arrays.stream(tagStr.split("[;,,;\\s]+"))
|
||||
.map(String::trim)
|
||||
.filter(t -> !t.isEmpty())
|
||||
.limit(4)
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
}
|
||||
183
backend/src/main/java/com/chowbox/service/FridgeService.java
Normal file
183
backend/src/main/java/com/chowbox/service/FridgeService.java
Normal file
@@ -0,0 +1,183 @@
|
||||
package com.chowbox.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.chowbox.mapper.RecipeIngredientMapper;
|
||||
import com.chowbox.mapper.RecipeMapper;
|
||||
import com.chowbox.mapper.RecipeStepMapper;
|
||||
import com.chowbox.model.FridgeMatchResult;
|
||||
import com.chowbox.model.Recipe;
|
||||
import com.chowbox.model.RecipeIngredient;
|
||||
import com.chowbox.model.RecipeStep;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class FridgeService {
|
||||
|
||||
@Autowired
|
||||
private RecipeMapper recipeMapper;
|
||||
|
||||
@Autowired
|
||||
private RecipeIngredientMapper ingredientMapper;
|
||||
|
||||
@Autowired
|
||||
private RecipeStepMapper stepMapper;
|
||||
|
||||
private static final Set<String> DEFAULT_STAPLES = new HashSet<>(Arrays.asList(
|
||||
"油", "盐", "酱油", "生抽", "老抽", "醋", "料酒", "蚝油",
|
||||
"糖", "白糖", "淀粉", "香油", "姜", "蒜", "葱", "干辣椒", "花椒"
|
||||
));
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
public List<FridgeMatchResult> matchRecipes(List<String> userIngredients, List<String> customStaples) {
|
||||
Set<String> userSet = new HashSet<>();
|
||||
for (String ing : userIngredients) {
|
||||
userSet.add(ing.trim());
|
||||
}
|
||||
|
||||
// 合并系统默认 + 用户自定义常备调料
|
||||
Set<String> allStaples = new HashSet<>(DEFAULT_STAPLES);
|
||||
if (customStaples != null) {
|
||||
for (String s : customStaples) {
|
||||
allStaples.add(s.trim());
|
||||
}
|
||||
}
|
||||
userSet.addAll(allStaples);
|
||||
|
||||
List<Recipe> allRecipes = recipeMapper.selectList(null);
|
||||
log.info("FridgeService: total recipes in DB = {}", allRecipes.size());
|
||||
List<FridgeMatchResult> results = new ArrayList<>();
|
||||
|
||||
int currentHour = LocalTime.now().getHour();
|
||||
int currentMonth = LocalDate.now().getMonthValue();
|
||||
String mealTime = getMealTime(currentHour);
|
||||
String season = getSeason(currentMonth);
|
||||
log.info("FridgeService: current mealTime={}, season={}", mealTime, season);
|
||||
|
||||
for (Recipe recipe : allRecipes) {
|
||||
List<RecipeIngredient> recipeIngredients = ingredientMapper.selectList(
|
||||
new QueryWrapper<RecipeIngredient>().eq("recipe_id", recipe.getId())
|
||||
);
|
||||
|
||||
List<String> recipeIngNames = recipeIngredients.stream()
|
||||
.map(ri -> ri.getIngredientName().trim())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 计算交集
|
||||
long matchCount = recipeIngNames.stream()
|
||||
.filter(userSet::contains)
|
||||
.count();
|
||||
|
||||
double matchRate = recipeIngNames.isEmpty() ? 0 :
|
||||
(double) matchCount / recipeIngNames.size() * 100;
|
||||
|
||||
// 找出缺失的食材(非调料类)
|
||||
List<String> missing = recipeIngNames.stream()
|
||||
.filter(name -> !userSet.contains(name) && !allStaples.contains(name))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 时段/季节加权:匹配当前时段或季节 +8%
|
||||
double timeBoost = 0;
|
||||
if (mealTime.equals(recipe.getMealTime())) timeBoost += 8;
|
||||
if (season.equals(recipe.getSeason())) timeBoost += 8;
|
||||
|
||||
FridgeMatchResult result = new FridgeMatchResult();
|
||||
result.setRecipe(recipe);
|
||||
result.setMatchRate(Math.round(matchRate + timeBoost));
|
||||
result.setMissingIngredients(missing);
|
||||
results.add(result);
|
||||
}
|
||||
|
||||
// 按匹配度降序排列,取 Top10 候选池,加权随机选 3-5 个
|
||||
List<FridgeMatchResult> pool = results.stream()
|
||||
.sorted((a, b) -> Double.compare(b.getMatchRate(), a.getMatchRate()))
|
||||
.limit(10)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return weightedRandomPick(pool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从候选池加权随机选 3-5 个。
|
||||
* 权重 = matchRate²,保证高分菜更有机会但不乏惊喜。
|
||||
*/
|
||||
private List<FridgeMatchResult> weightedRandomPick(List<FridgeMatchResult> pool) {
|
||||
if (pool.size() <= 3) return pool;
|
||||
|
||||
int pickCount = 3 + RANDOM.nextInt(Math.min(3, pool.size() - 2)); // 3-5
|
||||
|
||||
List<FridgeMatchResult> shuffled = new ArrayList<>(pool);
|
||||
Collections.shuffle(shuffled, RANDOM);
|
||||
|
||||
// 加权随机
|
||||
List<FridgeMatchResult> picked = new ArrayList<>();
|
||||
List<FridgeMatchResult> remaining = new ArrayList<>(shuffled);
|
||||
|
||||
for (int i = 0; i < pickCount && !remaining.isEmpty(); i++) {
|
||||
double totalWeight = remaining.stream()
|
||||
.mapToDouble(r -> Math.pow(r.getMatchRate(), 2))
|
||||
.sum();
|
||||
|
||||
if (totalWeight <= 0) {
|
||||
int idx = RANDOM.nextInt(remaining.size());
|
||||
picked.add(remaining.remove(idx));
|
||||
continue;
|
||||
}
|
||||
|
||||
double dart = RANDOM.nextDouble() * totalWeight;
|
||||
double cumulative = 0;
|
||||
int selectedIdx = 0;
|
||||
for (int j = 0; j < remaining.size(); j++) {
|
||||
cumulative += Math.pow(remaining.get(j).getMatchRate(), 2);
|
||||
if (dart <= cumulative) {
|
||||
selectedIdx = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
picked.add(remaining.remove(selectedIdx));
|
||||
}
|
||||
|
||||
// 匹配度高的排前面
|
||||
picked.sort((a, b) -> Double.compare(b.getMatchRate(), a.getMatchRate()));
|
||||
return picked;
|
||||
}
|
||||
|
||||
private String getMealTime(int hour) {
|
||||
if (hour >= 5 && hour < 10) return "早餐";
|
||||
if (hour >= 10 && hour < 14) return "午餐";
|
||||
if (hour >= 14 && hour < 17) return "下午茶";
|
||||
if (hour >= 17 && hour < 21) return "晚餐";
|
||||
return "夜宵";
|
||||
}
|
||||
|
||||
private String getSeason(int month) {
|
||||
if (month >= 3 && month <= 5) return "春季";
|
||||
if (month >= 6 && month <= 8) return "夏季";
|
||||
if (month >= 9 && month <= 11) return "秋季";
|
||||
return "冬季";
|
||||
}
|
||||
|
||||
public Recipe getRecipeDetail(Long recipeId) {
|
||||
Recipe recipe = recipeMapper.selectById(recipeId);
|
||||
if (recipe == null) return null;
|
||||
|
||||
List<RecipeIngredient> ingredients = ingredientMapper.selectList(
|
||||
new QueryWrapper<RecipeIngredient>().eq("recipe_id", recipeId)
|
||||
);
|
||||
List<RecipeStep> steps = stepMapper.selectList(
|
||||
new QueryWrapper<RecipeStep>().eq("recipe_id", recipeId).orderByAsc("step_order")
|
||||
);
|
||||
|
||||
recipe.setIngredients(ingredients);
|
||||
recipe.setSteps(steps);
|
||||
return recipe;
|
||||
}
|
||||
}
|
||||
320
backend/src/main/java/com/chowbox/service/TakeoutService.java
Normal file
320
backend/src/main/java/com/chowbox/service/TakeoutService.java
Normal file
@@ -0,0 +1,320 @@
|
||||
package com.chowbox.service;
|
||||
|
||||
import com.chowbox.model.Restaurant;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class TakeoutService {
|
||||
|
||||
@Autowired
|
||||
private AmapService amapService;
|
||||
|
||||
private final Map<String, Set<String>> recentlyShown = new HashMap<>();
|
||||
|
||||
// 保质期较短的缓存,避免同一会话内重复展示
|
||||
private static final int MAX_SHOWN_HISTORY = 20;
|
||||
|
||||
public Restaurant roll(double lat, double lng, String openid,
|
||||
String taste, String priceRange, String allergies) {
|
||||
List<Restaurant> all = amapService.searchNearbyPOI(lat, lng, 3000);
|
||||
|
||||
log.info("Takeout roll: total POIs={}", all.size());
|
||||
if (!all.isEmpty()) {
|
||||
Restaurant first = all.get(0);
|
||||
log.info("Takeout sample: name={}, rating={}, category={}, tags={}",
|
||||
first.getName(), first.getRating(), first.getCategory(),
|
||||
first.getSignatureDishes() != null ? String.join(",", first.getSignatureDishes()) : "null");
|
||||
}
|
||||
|
||||
if (all.isEmpty()) return null;
|
||||
|
||||
// ── 高质量池过滤(分层降级) ──
|
||||
List<Restaurant> pool = filterByRating(all, 4.3);
|
||||
if (pool.size() < 3) {
|
||||
log.info("Takeout pool >=4.3: {}, expanding to >=4.0", pool.size());
|
||||
pool = filterByRating(all, 4.0);
|
||||
}
|
||||
if (pool.size() < 3) {
|
||||
log.info("Takeout pool >=4.0: {}, expanding to >=3.5", pool.size());
|
||||
pool = filterByRating(all, 3.5);
|
||||
}
|
||||
if (pool.isEmpty()) {
|
||||
log.info("Takeout fallback: using all POIs");
|
||||
pool = new ArrayList<>(all);
|
||||
}
|
||||
|
||||
// ── 去除最近已展示的商家 ──
|
||||
Set<String> shown = recentlyShown.getOrDefault(openid, new LinkedHashSet<>());
|
||||
List<Restaurant> candidates = pool.stream()
|
||||
.filter(r -> !shown.contains(r.getId()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (candidates.isEmpty()) {
|
||||
// 全部展示过了,清空历史重来
|
||||
recentlyShown.remove(openid);
|
||||
candidates = new ArrayList<>(pool);
|
||||
}
|
||||
|
||||
// ── 用户偏好过滤 ──
|
||||
List<Restaurant> prefFiltered = filterByPreferences(candidates, taste, priceRange, allergies);
|
||||
if (!prefFiltered.isEmpty()) {
|
||||
candidates = prefFiltered;
|
||||
} else {
|
||||
log.info("Takeout pref filter empty, fallback to all candidates");
|
||||
}
|
||||
|
||||
// ── 加权随机(评分权重 + 距离惩罚) ──
|
||||
Restaurant selected = weightedRandom(candidates);
|
||||
|
||||
// 记录已展示
|
||||
Set<String> userShown = recentlyShown.computeIfAbsent(openid,
|
||||
k -> new LinkedHashSet<>());
|
||||
userShown.add(selected.getId());
|
||||
// 限制历史大小
|
||||
if (userShown.size() > MAX_SHOWN_HISTORY) {
|
||||
Iterator<String> it = userShown.iterator();
|
||||
it.next();
|
||||
it.remove();
|
||||
}
|
||||
|
||||
// ── 补充合理默认值 ──
|
||||
enrichRestaurant(selected);
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
private List<Restaurant> filterByRating(List<Restaurant> list, double minRating) {
|
||||
return list.stream()
|
||||
.filter(r -> r.getRating() != null && r.getRating() >= minRating)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户偏好过滤餐厅列表,三层过滤:价格 → 口味 → 忌口
|
||||
*/
|
||||
private List<Restaurant> filterByPreferences(List<Restaurant> list,
|
||||
String taste, String priceRange, String allergies) {
|
||||
List<Restaurant> result = new ArrayList<>(list);
|
||||
|
||||
// 1. 价格区间过滤
|
||||
if (priceRange != null && !"all".equals(priceRange)) {
|
||||
result = filterByPriceRange(result, priceRange);
|
||||
}
|
||||
|
||||
// 2. 口味偏好过滤
|
||||
if (taste != null && !"都可以".equals(taste)) {
|
||||
result = filterByTaste(result, taste);
|
||||
}
|
||||
|
||||
// 3. 忌口过滤
|
||||
if (allergies != null && !allergies.isEmpty()) {
|
||||
result = filterByAllergies(result, allergies);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<Restaurant> filterByPriceRange(List<Restaurant> list, String priceRange) {
|
||||
return list.stream().filter(r -> {
|
||||
Double price = r.getAvgPrice();
|
||||
if (price == null) return true; // 无价格数据的保留
|
||||
switch (priceRange) {
|
||||
case "low": return price < 30;
|
||||
case "medium": return price >= 30 && price <= 80;
|
||||
case "high": return price > 80;
|
||||
default: return true;
|
||||
}
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<Restaurant> filterByTaste(List<Restaurant> list, String taste) {
|
||||
return list.stream().filter(r -> {
|
||||
String cat = r.getCategory() != null ? r.getCategory() : "";
|
||||
String name = r.getName() != null ? r.getName() : "";
|
||||
switch (taste) {
|
||||
case "辣":
|
||||
return containsAny(cat, "川菜", "湘菜", "火锅", "烧烤", "麻辣", "香锅")
|
||||
|| name.contains("火锅") || name.contains("烧烤") || name.contains("麻辣");
|
||||
case "清淡":
|
||||
return containsAny(cat, "粤菜", "日料", "面馆", "粥粉面", "蒸菜", "轻食")
|
||||
|| name.contains("粥") || name.contains("面") || name.contains("粉");
|
||||
case "酸甜":
|
||||
return containsAny(cat, "甜品", "咖啡", "饮品", "东南亚", "糖水")
|
||||
|| name.contains("甜品") || name.contains("咖啡") || name.contains("茶");
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<Restaurant> filterByAllergies(List<Restaurant> list, String allergies) {
|
||||
String[] items = allergies.split("[,,]");
|
||||
return list.stream().filter(r -> {
|
||||
String cat = r.getCategory() != null ? r.getCategory() : "";
|
||||
String name = r.getName() != null ? r.getName() : "";
|
||||
String[] dishes = r.getSignatureDishes() != null ? r.getSignatureDishes() : new String[0];
|
||||
String combined = cat + name + String.join("", dishes);
|
||||
for (String item : items) {
|
||||
String kw = item.trim();
|
||||
if (!kw.isEmpty() && combined.contains(kw)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private boolean containsAny(String text, String... keywords) {
|
||||
for (String kw : keywords) {
|
||||
if (text.contains(kw)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Restaurant weightedRandom(List<Restaurant> candidates) {
|
||||
// 综合权重 = 评分² × e^(-distance/2000)
|
||||
double totalWeight = 0;
|
||||
double[] weights = new double[candidates.size()];
|
||||
for (int i = 0; i < candidates.size(); i++) {
|
||||
Restaurant r = candidates.get(i);
|
||||
double rating = r.getRating() != null ? r.getRating() : 3.5;
|
||||
double dist = r.getDistance() != null ? r.getDistance() : 1500;
|
||||
// 评分权重为主,距离越近权重越高
|
||||
weights[i] = rating * rating * Math.exp(-dist / 2000.0);
|
||||
totalWeight += weights[i];
|
||||
}
|
||||
|
||||
double random = Math.random() * totalWeight;
|
||||
double cumulative = 0;
|
||||
for (int i = 0; i < candidates.size(); i++) {
|
||||
cumulative += weights[i];
|
||||
if (random <= cumulative) {
|
||||
return candidates.get(i);
|
||||
}
|
||||
}
|
||||
return candidates.get(candidates.size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用合理数据填充缺失字段,不再硬编码假数据
|
||||
*/
|
||||
private void enrichRestaurant(Restaurant r) {
|
||||
Double rating = r.getRating();
|
||||
Double avgPrice = r.getAvgPrice();
|
||||
Double distance = r.getDistance();
|
||||
|
||||
// ── 推荐语:基于真实数据生成 ──
|
||||
if (r.getRecommendReason() == null || r.getRecommendReason().isEmpty()) {
|
||||
r.setRecommendReason(buildRecommendReason(rating, avgPrice, r.getCategory()));
|
||||
}
|
||||
|
||||
// ── 招牌菜:如果 POI 没有 tag 数据 → 从菜系推断 ──
|
||||
if (r.getSignatureDishes() == null || r.getSignatureDishes().length == 0) {
|
||||
r.setSignatureDishes(guessSignatureDishes(r.getCategory(), r.getName()));
|
||||
}
|
||||
|
||||
// ── 配送/起送信息:基于品类和人均估算 ──
|
||||
if (r.getDeliveryTime() == null) {
|
||||
r.setDeliveryTime(estimateDeliveryTime(distance));
|
||||
}
|
||||
if (r.getMinOrder() == null) {
|
||||
r.setMinOrder(estimateMinOrder(avgPrice, r.getCategory()));
|
||||
}
|
||||
if (r.getDeliveryFee() == null) {
|
||||
r.setDeliveryFee(estimateDeliveryFee(distance));
|
||||
}
|
||||
}
|
||||
|
||||
private String buildRecommendReason(Double rating, Double avgPrice, String category) {
|
||||
List<String> points = new ArrayList<>();
|
||||
|
||||
if (rating != null && rating >= 4.5) {
|
||||
points.add("评分" + rating + ",口碑极佳");
|
||||
} else if (rating != null && rating >= 4.0) {
|
||||
points.add("评分" + rating + ",广受好评");
|
||||
} else if (rating != null) {
|
||||
points.add("评分" + rating);
|
||||
}
|
||||
|
||||
if (avgPrice != null && avgPrice <= 30) {
|
||||
points.add("人均¥" + avgPrice.intValue() + ",超高性价比");
|
||||
} else if (avgPrice != null && avgPrice <= 60) {
|
||||
points.add("人均¥" + avgPrice.intValue());
|
||||
}
|
||||
|
||||
if (category != null && !"餐饮".equals(category)) {
|
||||
points.add("热门" + category);
|
||||
}
|
||||
|
||||
if (points.isEmpty()) {
|
||||
points.add("附近人气商家");
|
||||
}
|
||||
|
||||
return String.join(" · ", points);
|
||||
}
|
||||
|
||||
private String[] guessSignatureDishes(String category, String name) {
|
||||
if (category == null) category = "";
|
||||
if (name == null) name = "";
|
||||
|
||||
List<String> guesses = new ArrayList<>();
|
||||
|
||||
// 从店名提取线索
|
||||
if (name.contains("火锅")) guesses.add("鸳鸯锅底");
|
||||
if (name.contains("烧烤")) guesses.add("羊肉串");
|
||||
if (name.contains("面") || category.equals("面馆")) guesses.add("招牌汤面");
|
||||
if (name.contains("饺子") || name.contains("水饺")) guesses.add("手工水饺");
|
||||
if (name.contains("米线") || name.contains("米粉")) guesses.add("过桥米线");
|
||||
if (name.contains("汉堡") || name.contains("炸鸡")) guesses.add("招牌汉堡");
|
||||
|
||||
// 从菜系推断
|
||||
if (category.contains("川菜")) { guesses.add("水煮鱼"); guesses.add("麻婆豆腐"); }
|
||||
if (category.contains("粤菜")) { guesses.add("白切鸡"); guesses.add("叉烧"); }
|
||||
if (category.contains("湘菜")) { guesses.add("剁椒鱼头"); guesses.add("小炒肉"); }
|
||||
if (category.contains("日料")) { guesses.add("三文鱼刺身"); guesses.add("豚骨拉面"); }
|
||||
if (category.contains("韩餐")) { guesses.add("石锅拌饭"); guesses.add("韩式炸鸡"); }
|
||||
if (category.contains("火锅")) { guesses.add("毛肚"); guesses.add("虾滑"); }
|
||||
if (category.contains("烧烤")) { guesses.add("烤串套餐"); guesses.add("烤茄子"); }
|
||||
if (category.contains("快餐")) { guesses.add("超值套餐"); }
|
||||
if (category.contains("甜品") || category.contains("咖啡")) { guesses.add("招牌拿铁"); guesses.add("提拉米苏"); }
|
||||
|
||||
// 不够3个用通用项补齐
|
||||
if (guesses.size() < 3) guesses.add("招牌套餐");
|
||||
|
||||
return guesses.subList(0, Math.min(guesses.size(), 4)).toArray(new String[0]);
|
||||
}
|
||||
|
||||
private String estimateDeliveryTime(Double distanceMeters) {
|
||||
if (distanceMeters == null) return "约30分钟";
|
||||
double km = distanceMeters / 1000.0;
|
||||
if (km <= 1) return "约25分钟";
|
||||
if (km <= 2) return "约30分钟";
|
||||
if (km <= 3) return "约35分钟";
|
||||
return "约40分钟";
|
||||
}
|
||||
|
||||
private String estimateMinOrder(Double avgPrice, String category) {
|
||||
// 快餐/小吃类起送低,正餐类高
|
||||
if (category != null && (category.contains("快餐") || category.contains("小吃"))) {
|
||||
return "15元起送";
|
||||
}
|
||||
if (avgPrice != null && avgPrice >= 80) {
|
||||
return "30元起送";
|
||||
}
|
||||
return "20元起送";
|
||||
}
|
||||
|
||||
private String estimateDeliveryFee(Double distanceMeters) {
|
||||
if (distanceMeters == null) return "约3元";
|
||||
double km = distanceMeters / 1000.0;
|
||||
if (km <= 1.5) return "免配送费";
|
||||
if (km <= 2.5) return "约3元";
|
||||
return "约5元";
|
||||
}
|
||||
}
|
||||
24
backend/src/main/resources/application.yml
Normal file
24
backend/src/main/resources/application.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/chowbox?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password:
|
||||
timeout: 3000ms
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: auto
|
||||
|
||||
amap:
|
||||
api-key: e35f0d06a77ab57ba1e6806120b46c2a
|
||||
382
backend/src/main/resources/db/data.sql
Normal file
382
backend/src/main/resources/db/data.sql
Normal file
@@ -0,0 +1,382 @@
|
||||
-- ChowBox 菜谱种子数据(100道)
|
||||
USE chowbox;
|
||||
|
||||
-- ============================================================
|
||||
-- 家常快手菜(20道)- 难度低、时间短
|
||||
-- ============================================================
|
||||
INSERT INTO recipes (id, name, difficulty, cook_time, category, season, meal_time) VALUES
|
||||
(1, '西红柿炒鸡蛋', 1, 10, '家常菜', 'all', 'all'),
|
||||
(2, '酸辣土豆丝', 1, 10, '家常菜', 'all', 'lunch,dinner'),
|
||||
(3, '青椒肉丝', 1, 15, '家常菜', 'all', 'lunch,dinner'),
|
||||
(4, '蒜蓉西兰花', 1, 8, '家常菜', 'all', 'lunch,dinner'),
|
||||
(5, '糖醋里脊', 2, 25, '家常菜', 'all', 'lunch,dinner'),
|
||||
(6, '干煸四季豆', 1, 12, '家常菜', 'all', 'lunch,dinner'),
|
||||
(7, '可乐鸡翅', 1, 20, '家常菜', 'all', 'lunch,dinner'),
|
||||
(8, '韭黄炒蛋', 1, 8, '家常菜', 'all', 'all'),
|
||||
(9, '红烧豆腐', 1, 15, '家常菜', 'all', 'lunch,dinner'),
|
||||
(10, '蚝油生菜', 1, 5, '家常菜', 'all', 'lunch,dinner'),
|
||||
(11, '孜然土豆片', 1, 12, '家常菜', 'all', 'lunch,dinner'),
|
||||
(12, '洋葱炒肉', 1, 10, '家常菜', 'all', 'lunch,dinner'),
|
||||
(13, '香菇油菜', 1, 10, '家常菜', 'all', 'lunch,dinner'),
|
||||
(14, '家常豆腐', 2, 18, '家常菜', 'all', 'lunch,dinner'),
|
||||
(15, '雪菜肉丝', 1, 12, '家常菜', 'all', 'lunch,dinner'),
|
||||
(16, '素炒豆芽', 1, 6, '家常菜', 'all', 'lunch,dinner'),
|
||||
(17, '芹菜香干', 1, 10, '家常菜', 'all', 'lunch,dinner'),
|
||||
(18, '番茄菜花', 1, 10, '家常菜', 'all', 'lunch,dinner'),
|
||||
(19, '肉末茄子', 2, 20, '家常菜', 'all', 'lunch,dinner'),
|
||||
(20, '葱爆羊肉', 2, 15, '家常菜', 'winter', 'lunch,dinner');
|
||||
|
||||
-- ============================================================
|
||||
-- 川菜(15道)
|
||||
-- ============================================================
|
||||
INSERT INTO recipes (id, name, difficulty, cook_time, category, season, meal_time) VALUES
|
||||
(21, '麻婆豆腐', 2, 15, '川菜', 'all', 'lunch,dinner'),
|
||||
(22, '回锅肉', 2, 25, '川菜', 'all', 'lunch,dinner'),
|
||||
(23, '宫保鸡丁', 2, 20, '川菜', 'all', 'lunch,dinner'),
|
||||
(24, '水煮肉片', 3, 30, '川菜', 'winter', 'lunch,dinner'),
|
||||
(25, '鱼香肉丝', 2, 20, '川菜', 'all', 'lunch,dinner'),
|
||||
(26, '辣子鸡丁', 3, 30, '川菜', 'all', 'lunch,dinner'),
|
||||
(27, '夫妻肺片', 2, 20, '川菜', 'summer', 'lunch,dinner'),
|
||||
(28, '口水鸡', 2, 30, '川菜', 'summer', 'lunch,dinner'),
|
||||
(29, '干煸牛肉丝', 2, 20, '川菜', 'all', 'lunch,dinner'),
|
||||
(30, '蒜泥白肉', 1, 20, '川菜', 'summer', 'lunch,dinner'),
|
||||
(31, '酸菜鱼', 3, 35, '川菜', 'winter', 'lunch,dinner'),
|
||||
(32, '毛血旺', 3, 30, '川菜', 'winter', 'lunch,dinner'),
|
||||
(33, '麻辣香锅', 2, 25, '川菜', 'all', 'lunch,dinner'),
|
||||
(34, '泡椒凤爪', 2, 40, '川菜', 'summer', 'all'),
|
||||
(35, '担担面', 2, 25, '川菜', 'all', 'lunch,dinner');
|
||||
|
||||
-- ============================================================
|
||||
-- 粤菜(15道)
|
||||
-- ============================================================
|
||||
INSERT INTO recipes (id, name, difficulty, cook_time, category, season, meal_time) VALUES
|
||||
(36, '白切鸡', 3, 40, '粤菜', 'all', 'lunch,dinner'),
|
||||
(37, '清蒸鲈鱼', 2, 20, '粤菜', 'all', 'lunch,dinner'),
|
||||
(38, '白灼虾', 1, 10, '粤菜', 'all', 'lunch,dinner'),
|
||||
(39, '广式叉烧', 4, 60, '粤菜', 'all', 'lunch,dinner'),
|
||||
(40, '菠萝咕咾肉', 2, 25, '粤菜', 'summer', 'lunch,dinner'),
|
||||
(41, '干炒牛河', 3, 15, '粤菜', 'all', 'lunch,dinner'),
|
||||
(42, '蒜蓉粉丝蒸扇贝', 2, 15, '粤菜', 'all', 'dinner'),
|
||||
(43, '豉汁蒸排骨', 2, 25, '粤菜', 'all', 'lunch,dinner'),
|
||||
(44, '煲仔饭', 3, 35, '粤菜', 'winter', 'lunch,dinner'),
|
||||
(45, '冬瓜排骨汤', 1, 50, '粤菜', 'summer', 'dinner'),
|
||||
(46, '蚝油牛肉', 2, 15, '粤菜', 'all', 'lunch,dinner'),
|
||||
(47, '虾饺', 4, 40, '粤菜', 'all', 'breakfast,lunch'),
|
||||
(48, '肠粉', 2, 20, '粤菜', 'all', 'breakfast'),
|
||||
(49, '叉烧包', 3, 45, '粤菜', 'all', 'breakfast'),
|
||||
(50, '蛋挞', 2, 30, '粤菜', 'all', 'all');
|
||||
|
||||
-- ============================================================
|
||||
-- 湘菜(8道)
|
||||
-- ============================================================
|
||||
INSERT INTO recipes (id, name, difficulty, cook_time, category, season, meal_time) VALUES
|
||||
(51, '剁椒鱼头', 3, 30, '湘菜', 'winter', 'lunch,dinner'),
|
||||
(52, '农家小炒肉', 2, 15, '湘菜', 'all', 'lunch,dinner'),
|
||||
(53, '酸豆角肉末', 1, 12, '湘菜', 'summer', 'lunch,dinner'),
|
||||
(54, '腊味合蒸', 2, 30, '湘菜', 'winter', 'lunch,dinner'),
|
||||
(55, '干锅花菜', 2, 18, '湘菜', 'all', 'lunch,dinner'),
|
||||
(56, '口味虾', 3, 35, '湘菜', 'summer', 'dinner'),
|
||||
(57, '擂辣椒皮蛋', 1, 10, '湘菜', 'summer', 'all'),
|
||||
(58, '湘西外婆菜', 2, 15, '湘菜', 'all', 'lunch,dinner');
|
||||
|
||||
-- ============================================================
|
||||
-- 东北菜(8道)
|
||||
-- ============================================================
|
||||
INSERT INTO recipes (id, name, difficulty, cook_time, category, season, meal_time) VALUES
|
||||
(59, '锅包肉', 3, 30, '东北菜', 'all', 'lunch,dinner'),
|
||||
(60, '地三鲜', 2, 20, '东北菜', 'all', 'lunch,dinner'),
|
||||
(61, '猪肉炖粉条', 2, 40, '东北菜', 'winter', 'lunch,dinner'),
|
||||
(62, '东北乱炖', 1, 35, '东北菜', 'winter', 'lunch,dinner'),
|
||||
(63, '京酱肉丝', 2, 20, '东北菜', 'all', 'lunch,dinner'),
|
||||
(64, '拔丝地瓜', 3, 25, '东北菜', 'winter', 'dinner'),
|
||||
(65, '酸菜白肉', 2, 45, '东北菜', 'winter', 'lunch,dinner'),
|
||||
(66, '溜肉段', 2, 20, '东北菜', 'all', 'lunch,dinner');
|
||||
|
||||
-- ============================================================
|
||||
-- 早餐类(12道)
|
||||
-- ============================================================
|
||||
INSERT INTO recipes (id, name, difficulty, cook_time, category, season, meal_time) VALUES
|
||||
(67, '鸡蛋灌饼', 2, 15, '早餐', 'all', 'breakfast'),
|
||||
(68, '皮蛋瘦肉粥', 1, 30, '早餐', 'all', 'breakfast'),
|
||||
(69, '葱花鸡蛋饼', 1, 8, '早餐', 'all', 'breakfast'),
|
||||
(70, '小米南瓜粥', 1, 25, '早餐', 'autumn,winter', 'breakfast'),
|
||||
(71, '煎饺', 2, 15, '早餐', 'all', 'breakfast'),
|
||||
(72, '三明治', 1, 5, '早餐', 'all', 'breakfast'),
|
||||
(73, '豆浆油条', 3, 40, '早餐', 'all', 'breakfast'),
|
||||
(74, '馄饨', 3, 30, '早餐', 'all', 'breakfast,lunch'),
|
||||
(75, '肉夹馍', 3, 60, '早餐', 'all', 'all'),
|
||||
(76, '糯米鸡', 2, 40, '早餐', 'all', 'breakfast'),
|
||||
(77, '煎饼果子', 2, 15, '早餐', 'all', 'breakfast'),
|
||||
(78, '豆沙包', 2, 40, '早餐', 'all', 'breakfast');
|
||||
|
||||
-- ============================================================
|
||||
-- 汤羹类(8道)
|
||||
-- ============================================================
|
||||
INSERT INTO recipes (id, name, difficulty, cook_time, category, season, meal_time) VALUES
|
||||
(79, '番茄蛋花汤', 1, 10, '汤羹', 'all', 'lunch,dinner'),
|
||||
(80, '紫菜蛋花汤', 1, 5, '汤羹', 'all', 'lunch,dinner'),
|
||||
(81, '玉米排骨汤', 1, 60, '汤羹', 'all', 'dinner'),
|
||||
(82, '西湖牛肉羹', 2, 25, '汤羹', 'winter', 'dinner'),
|
||||
(83, '酸辣汤', 2, 15, '汤羹', 'winter', 'lunch,dinner'),
|
||||
(84, '鲫鱼豆腐汤', 2, 30, '汤羹', 'all', 'dinner'),
|
||||
(85, '莲藕排骨汤', 1, 60, '汤羹', 'autumn,winter', 'dinner'),
|
||||
(86, '银耳莲子羹', 1, 40, '汤羹', 'summer,autumn', 'all');
|
||||
|
||||
-- ============================================================
|
||||
-- 凉菜类(8道)
|
||||
-- ============================================================
|
||||
INSERT INTO recipes (id, name, difficulty, cook_time, category, season, meal_time) VALUES
|
||||
(87, '凉拌黄瓜', 1, 5, '凉菜', 'summer', 'lunch,dinner'),
|
||||
(88, '皮蛋豆腐', 1, 5, '凉菜', 'summer', 'lunch,dinner'),
|
||||
(89, '凉拌木耳', 1, 8, '凉菜', 'summer', 'lunch,dinner'),
|
||||
(90, '凉拌三丝', 1, 10, '凉菜', 'summer', 'lunch,dinner'),
|
||||
(91, '糖拌番茄', 1, 3, '凉菜', 'summer', 'all'),
|
||||
(92, '凉拌鸡丝', 1, 15, '凉菜', 'summer', 'lunch,dinner'),
|
||||
(93, '拍黄瓜', 1, 3, '凉菜', 'summer', 'lunch,dinner'),
|
||||
(94, '凉拌海带丝', 1, 8, '凉菜', 'summer', 'lunch,dinner');
|
||||
|
||||
-- ============================================================
|
||||
-- 主食类(6道)
|
||||
-- ============================================================
|
||||
INSERT INTO recipes (id, name, difficulty, cook_time, category, season, meal_time) VALUES
|
||||
(95, '蛋炒饭', 1, 8, '主食', 'all', 'all'),
|
||||
(96, '番茄鸡蛋面', 1, 12, '主食', 'all', 'all'),
|
||||
(97, '炸酱面', 2, 25, '主食', 'all', 'lunch,dinner'),
|
||||
(98, '葱油拌面', 1, 10, '主食', 'all', 'all'),
|
||||
(99, '炒米粉', 1, 10, '主食', 'all', 'all'),
|
||||
(100, '阳春面', 1, 8, '主食', 'all', 'breakfast,lunch');
|
||||
|
||||
-- ============================================================
|
||||
-- 菜谱食材数据
|
||||
-- ============================================================
|
||||
-- 家常快手菜 (1-20)
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
(1, '西红柿', '2个', 1),(1, '鸡蛋', '3个', 1),(1, '葱', '1根', 0),(1, '糖', '少许', 0),(1, '盐', '适量', 0),
|
||||
(2, '土豆', '2个', 1),(2, '干辣椒', '3个', 0),(2, '花椒', '少许', 0),(2, '醋', '1勺', 0),(2, '盐', '适量', 0),
|
||||
(3, '猪肉', '150克', 1),(3, '青椒', '2个', 1),(3, '姜', '2片', 0),(3, '蒜', '2瓣', 0),(3, '生抽', '1勺', 0),(3, '料酒', '1勺', 0),(3, '盐', '适量', 0),(3, '淀粉', '少许', 0),
|
||||
(4, '西兰花', '1颗', 1),(4, '蒜', '5瓣', 0),(4, '盐', '适量', 0),(4, '蚝油', '1勺', 0),
|
||||
(5, '猪里脊', '200克', 1),(5, '鸡蛋', '1个', 0),(5, '淀粉', '适量', 0),(5, '番茄酱', '3勺', 0),(5, '糖', '2勺', 0),(5, '醋', '1勺', 0),(5, '盐', '适量', 0),
|
||||
(6, '四季豆', '300克', 1),(6, '干辣椒', '5个', 0),(6, '花椒', '少许', 0),(6, '蒜', '3瓣', 0),(6, '盐', '适量', 0),
|
||||
(7, '鸡翅', '10个', 1),(7, '可乐', '1罐', 0),(7, '姜', '3片', 0),(7, '生抽', '2勺', 0),(7, '料酒', '1勺', 0),(7, '盐', '适量', 0),
|
||||
(8, '韭黄', '200克', 1),(8, '鸡蛋', '3个', 1),(8, '盐', '适量', 0),
|
||||
(9, '豆腐', '1块', 1),(9, '蒜', '3瓣', 0),(9, '生抽', '2勺', 0),(9, '蚝油', '1勺', 0),(9, '糖', '少许', 0),(9, '盐', '适量', 0),
|
||||
(10, '生菜', '300克', 1),(10, '蒜', '5瓣', 0),(10, '蚝油', '2勺', 0),(10, '盐', '适量', 0),
|
||||
(11, '土豆', '2个', 1),(11, '孜然粉', '1勺', 0),(11, '辣椒粉', '适量', 0),(11, '盐', '适量', 0),
|
||||
(12, '洋葱', '1个', 1),(12, '猪肉', '150克', 1),(12, '生抽', '1勺', 0),(12, '料酒', '1勺', 0),(12, '盐', '适量', 0),
|
||||
(13, '香菇', '8朵', 1),(13, '油菜', '200克', 1),(13, '蒜', '3瓣', 0),(13, '蚝油', '1勺', 0),(13, '盐', '适量', 0),
|
||||
(14, '豆腐', '1块', 1),(14, '青椒', '1个', 0),(14, '木耳', '适量', 0),(14, '生抽', '2勺', 0),(14, '蚝油', '1勺', 0),(14, '盐', '适量', 0),
|
||||
(15, '雪菜', '100克', 1),(15, '猪肉', '150克', 1),(15, '姜', '2片', 0),(15, '料酒', '1勺', 0),(15, '盐', '适量', 0),
|
||||
(16, '豆芽', '300克', 1),(16, '蒜', '3瓣', 0),(16, '醋', '1勺', 0),(16, '干辣椒', '3个', 0),(16, '盐', '适量', 0),
|
||||
(17, '芹菜', '200克', 1),(17, '香干', '150克', 1),(17, '蒜', '2瓣', 0),(17, '生抽', '1勺', 0),(17, '盐', '适量', 0),
|
||||
(18, '番茄', '2个', 1),(18, '菜花', '300克', 1),(18, '蒜', '3瓣', 0),(18, '盐', '适量', 0),(18, '糖', '少许', 0),
|
||||
(19, '茄子', '2根', 1),(19, '猪肉末', '100克', 1),(19, '蒜', '5瓣', 0),(19, '姜', '2片', 0),(19, '生抽', '2勺', 0),(19, '蚝油', '1勺', 0),(19, '糖', '少许', 0),(19, '盐', '适量', 0),
|
||||
(20, '羊肉', '200克', 1),(20, '大葱', '2根', 1),(20, '姜', '2片', 0),(20, '料酒', '1勺', 0),(20, '生抽', '1勺', 0),(20, '盐', '适量', 0),(20, '孜然粉', '少许', 0);
|
||||
|
||||
-- 川菜 (21-35)
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
(21, '豆腐', '1块', 1),(21, '猪肉末', '80克', 0),(21, '豆瓣酱', '1勺', 0),(21, '花椒粉', '少许', 0),(21, '姜', '2片', 0),(21, '蒜', '3瓣', 0),(21, '生抽', '1勺', 0),
|
||||
(22, '五花肉', '300克', 1),(22, '蒜苗', '3根', 1),(22, '豆瓣酱', '1勺', 0),(22, '姜', '2片', 0),(22, '蒜', '3瓣', 0),(22, '生抽', '1勺', 0),(22, '料酒', '1勺', 0),
|
||||
(23, '鸡胸肉', '300克', 1),(23, '花生米', '50克', 1),(23, '干辣椒', '5个', 0),(23, '花椒', '少许', 0),(23, '姜', '2片', 0),(23, '蒜', '3瓣', 0),(23, '生抽', '1勺', 0),(23, '醋', '1勺', 0),(23, '糖', '1勺', 0),(23, '料酒', '1勺', 0),(23, '淀粉', '少许', 0),
|
||||
(24, '猪肉', '200克', 1),(24, '白菜', '200克', 1),(24, '豆芽', '100克', 0),(24, '干辣椒', '10个', 0),(24, '花椒', '1勺', 0),(24, '豆瓣酱', '1勺', 0),(24, '姜', '3片', 0),(24, '蒜', '5瓣', 0),
|
||||
(25, '猪肉', '200克', 1),(25, '木耳', '适量', 0),(25, '胡萝卜', '半根', 0),(25, '青椒', '1个', 0),(25, '豆瓣酱', '1勺', 0),(25, '姜', '2片', 0),(25, '蒜', '3瓣', 0),(25, '生抽', '1勺', 0),(25, '醋', '1勺', 0),(25, '糖', '1勺', 0),(25, '淀粉', '少许', 0),
|
||||
(26, '鸡腿肉', '300克', 1),(26, '干辣椒', '15个', 0),(26, '花椒', '1勺', 0),(26, '姜', '3片', 0),(26, '蒜', '5瓣', 0),(26, '料酒', '1勺', 0),(26, '生抽', '1勺', 0),(26, '淀粉', '少许', 0),(26, '盐', '适量', 0),
|
||||
(27, '牛腱子', '300克', 1),(27, '牛肚', '150克', 1),(27, '花生米', '50克', 0),(27, '香菜', '适量', 0),(27, '辣椒油', '2勺', 0),(27, '花椒粉', '少许', 0),(27, '蒜', '5瓣', 0),(27, '生抽', '2勺', 0),(27, '醋', '1勺', 0),(27, '糖', '少许', 0),
|
||||
(28, '鸡腿', '2只', 1),(28, '姜', '5片', 0),(28, '葱', '2根', 0),(28, '辣椒油', '2勺', 0),(28, '花椒粉', '少许', 0),(28, '生抽', '2勺', 0),(28, '醋', '1勺', 0),(28, '糖', '少许', 0),(28, '蒜', '5瓣', 0),(28, '花生碎', '适量', 0),
|
||||
(29, '牛肉', '200克', 1),(29, '芹菜', '100克', 1),(29, '干辣椒', '5个', 0),(29, '姜', '2片', 0),(29, '蒜', '3瓣', 0),(29, '生抽', '1勺', 0),(29, '料酒', '1勺', 0),(29, '盐', '适量', 0),
|
||||
(30, '五花肉', '300克', 1),(30, '蒜', '10瓣', 0),(30, '姜', '3片', 0),(30, '生抽', '2勺', 0),(30, '醋', '1勺', 0),(30, '辣椒油', '2勺', 0),(30, '糖', '少许', 0),(30, '料酒', '1勺', 0),
|
||||
(31, '草鱼', '1条', 1),(31, '酸菜', '200克', 1),(31, '干辣椒', '5个', 0),(31, '花椒', '少许', 0),(31, '姜', '5片', 0),(31, '蒜', '5瓣', 0),(31, '料酒', '2勺', 0),(31, '淀粉', '适量', 0),(31, '盐', '适量', 0),
|
||||
(32, '鸭血', '200克', 1),(32, '毛肚', '150克', 1),(32, '午餐肉', '100克', 1),(32, '豆芽', '100克', 0),(32, '干辣椒', '10个', 0),(32, '花椒', '1勺', 0),(32, '豆瓣酱', '1勺', 0),(32, '姜', '5片', 0),(32, '蒜', '5瓣', 0),
|
||||
(33, '虾', '150克', 1),(33, '藕', '100克', 1),(33, '土豆', '1个', 1),(33, '花菜', '100克', 0),(33, '干辣椒', '10个', 0),(33, '花椒', '1勺', 0),(33, '豆瓣酱', '1勺', 0),(33, '蒜', '5瓣', 0),(33, '姜', '3片', 0),(33, '生抽', '1勺', 0),
|
||||
(34, '鸡爪', '500克', 1),(34, '泡椒', '10个', 0),(34, '姜', '5片', 0),(34, '蒜', '5瓣', 0),(34, '料酒', '2勺', 0),(34, '白醋', '2勺', 0),(34, '盐', '适量', 0),
|
||||
(35, '面条', '200克', 1),(35, '猪肉末', '100克', 0),(35, '芝麻酱', '2勺', 0),(35, '辣椒油', '1勺', 0),(35, '花椒粉', '少许', 0),(35, '生抽', '1勺', 0),(35, '醋', '少许', 0),(35, '蒜', '3瓣', 0),(35, '葱', '1根', 0);
|
||||
|
||||
-- 粤菜 (36-50)
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
(36, '鸡', '1只', 1),(36, '姜', '5片', 0),(36, '葱', '3根', 0),(36, '料酒', '2勺', 0),(36, '盐', '适量', 0),
|
||||
(37, '鲈鱼', '1条', 1),(37, '姜', '5片', 0),(37, '葱', '2根', 0),(37, '蒸鱼豉油', '2勺', 0),(37, '料酒', '1勺', 0),(37, '盐', '适量', 0),
|
||||
(38, '虾', '500克', 1),(38, '姜', '3片', 0),(38, '料酒', '1勺', 0),(38, '盐', '适量', 0),
|
||||
(39, '梅花肉', '500克', 1),(39, '叉烧酱', '3勺', 0),(39, '蜂蜜', '2勺', 0),(39, '生抽', '2勺', 0),(39, '料酒', '1勺', 0),(39, '姜', '3片', 0),(39, '蒜', '3瓣', 0),
|
||||
(40, '猪里脊', '200克', 1),(40, '菠萝', '150克', 1),(40, '青椒', '1个', 0),(40, '番茄酱', '3勺', 0),(40, '糖', '2勺', 0),(40, '醋', '1勺', 0),(40, '淀粉', '适量', 0),(40, '盐', '适量', 0),(40, '鸡蛋', '1个', 0),
|
||||
(41, '河粉', '300克', 1),(41, '牛肉', '150克', 1),(41, '豆芽', '100克', 0),(41, '葱', '2根', 0),(41, '生抽', '2勺', 0),(41, '老抽', '1勺', 0),(41, '料酒', '1勺', 0),(41, '淀粉', '少许', 0),
|
||||
(42, '扇贝', '6个', 1),(42, '粉丝', '1把', 0),(42, '蒜', '10瓣', 0),(42, '生抽', '1勺', 0),(42, '蚝油', '1勺', 0),(42, '盐', '适量', 0),
|
||||
(43, '排骨', '300克', 1),(43, '豆豉', '1勺', 0),(43, '蒜', '5瓣', 0),(43, '姜', '2片', 0),(43, '生抽', '1勺', 0),(43, '料酒', '1勺', 0),(43, '淀粉', '少许', 0),(43, '糖', '少许', 0),
|
||||
(44, '大米', '200克', 1),(44, '腊肠', '1根', 1),(44, '排骨', '150克', 1),(44, '油菜', '3棵', 0),(44, '生抽', '2勺', 0),(44, '蚝油', '1勺', 0),(44, '姜', '2片', 0),(44, '盐', '适量', 0),
|
||||
(45, '排骨', '300克', 1),(45, '冬瓜', '500克', 1),(45, '姜', '3片', 0),(45, '盐', '适量', 0),(45, '料酒', '1勺', 0),
|
||||
(46, '牛肉', '200克', 1),(46, '青椒', '1个', 0),(46, '蚝油', '2勺', 0),(46, '姜', '2片', 0),(46, '蒜', '3瓣', 0),(46, '生抽', '1勺', 0),(46, '料酒', '1勺', 0),(46, '淀粉', '少许', 0),
|
||||
(47, '虾仁', '200克', 1),(47, '笋', '50克', 1),(47, '澄粉', '150克', 1),(47, '淀粉', '50克', 0),(47, '猪油', '少许', 0),(47, '盐', '适量', 0),(47, '糖', '少许', 0),
|
||||
(48, '大米', '200克', 1),(48, '虾仁', '50克', 1),(48, '牛肉', '50克', 1),(48, '鸡蛋', '1个', 0),(48, '生抽', '1勺', 0),(48, '盐', '适量', 0),
|
||||
(49, '面粉', '250克', 1),(49, '叉烧肉', '150克', 1),(49, '酵母', '3克', 0),(49, '糖', '30克', 0),(49, '蚝油', '1勺', 0),(49, '生抽', '1勺', 0),
|
||||
(50, '鸡蛋', '2个', 1),(50, '牛奶', '100毫升', 1),(50, '糖', '30克', 0),(50, '面粉', '80克', 0),(50, '黄油', '10克', 0);
|
||||
|
||||
-- 湘菜 (51-58)
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
(51, '鱼头', '1个', 1),(51, '剁椒', '100克', 0),(51, '姜', '5片', 0),(51, '蒜', '5瓣', 0),(51, '葱', '2根', 0),(51, '料酒', '2勺', 0),(51, '蒸鱼豉油', '2勺', 0),(51, '盐', '适量', 0),
|
||||
(52, '五花肉', '200克', 1),(52, '青椒', '5个', 1),(52, '蒜', '5瓣', 0),(52, '姜', '2片', 0),(52, '生抽', '1勺', 0),(52, '盐', '适量', 0),
|
||||
(53, '酸豆角', '200克', 1),(53, '猪肉末', '150克', 1),(53, '干辣椒', '3个', 0),(53, '蒜', '3瓣', 0),(53, '姜', '2片', 0),(53, '生抽', '1勺', 0),(53, '盐', '适量', 0),
|
||||
(54, '腊肉', '200克', 1),(54, '腊鱼', '150克', 1),(54, '姜', '3片', 0),(54, '蒜', '3瓣', 0),(54, '干辣椒', '3个', 0),(54, '料酒', '1勺', 0),
|
||||
(55, '花菜', '300克', 1),(55, '五花肉', '80克', 0),(55, '干辣椒', '5个', 0),(55, '蒜', '5瓣', 0),(55, '生抽', '1勺', 0),(55, '盐', '适量', 0),
|
||||
(56, '小龙虾', '1000克', 1),(56, '干辣椒', '15个', 0),(56, '花椒', '1勺', 0),(56, '豆瓣酱', '1勺', 0),(56, '姜', '5片', 0),(56, '蒜', '10瓣', 0),(56, '生抽', '2勺', 0),(56, '料酒', '2勺', 0),(56, '糖', '少许', 0),(56, '盐', '适量', 0),
|
||||
(57, '青椒', '4个', 1),(57, '皮蛋', '2个', 1),(57, '蒜', '5瓣', 0),(57, '醋', '1勺', 0),(57, '生抽', '1勺', 0),(57, '盐', '适量', 0),
|
||||
(58, '猪肉末', '150克', 1),(58, '萝卜干', '50克', 1),(58, '干辣椒', '3个', 0),(58, '蒜', '3瓣', 0),(58, '生抽', '1勺', 0),(58, '盐', '适量', 0);
|
||||
|
||||
-- 东北菜 (59-66)
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
(59, '猪里脊', '250克', 1),(59, '淀粉', '适量', 0),(59, '鸡蛋', '1个', 0),(59, '胡萝卜', '半根', 0),(59, '姜', '3片', 0),(59, '糖', '3勺', 0),(59, '白醋', '3勺', 0),(59, '盐', '适量', 0),
|
||||
(60, '土豆', '1个', 1),(60, '茄子', '1根', 1),(60, '青椒', '1个', 1),(60, '蒜', '5瓣', 0),(60, '生抽', '2勺', 0),(60, '糖', '少许', 0),(60, '盐', '适量', 0),(60, '淀粉', '少许', 0),
|
||||
(61, '五花肉', '200克', 1),(61, '粉条', '100克', 1),(61, '白菜', '200克', 0),(61, '姜', '3片', 0),(61, '蒜', '3瓣', 0),(61, '生抽', '2勺', 0),(61, '老抽', '1勺', 0),(61, '料酒', '1勺', 0),(61, '盐', '适量', 0),
|
||||
(62, '排骨', '200克', 1),(62, '土豆', '1个', 1),(62, '豆角', '200克', 1),(62, '玉米', '1根', 0),(62, '姜', '3片', 0),(62, '生抽', '2勺', 0),(62, '盐', '适量', 0),
|
||||
(63, '猪里脊', '200克', 1),(63, '大葱', '2根', 1),(63, '豆腐皮', '适量', 0),(63, '姜', '2片', 0),(63, '甜面酱', '2勺', 0),(63, '糖', '少许', 0),(63, '生抽', '1勺', 0),(63, '淀粉', '少许', 0),
|
||||
(64, '地瓜', '1个', 1),(64, '糖', '150克', 0),(64, '油', '适量', 0),
|
||||
(65, '五花肉', '300克', 1),(65, '酸菜', '200克', 1),(65, '姜', '3片', 0),(65, '蒜', '3瓣', 0),(65, '料酒', '1勺', 0),(65, '盐', '适量', 0),
|
||||
(66, '猪里脊', '200克', 1),(66, '青椒', '1个', 0),(66, '胡萝卜', '半根', 0),(66, '淀粉', '适量', 0),(66, '姜', '2片', 0),(66, '蒜', '3瓣', 0),(66, '生抽', '2勺', 0),(66, '糖', '1勺', 0),(66, '醋', '1勺', 0),(66, '盐', '适量', 0);
|
||||
|
||||
-- 早餐类 (67-78)
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
(67, '面粉', '200克', 1),(67, '鸡蛋', '2个', 1),(67, '葱', '2根', 0),(67, '盐', '适量', 0),
|
||||
(68, '大米', '100克', 1),(68, '皮蛋', '2个', 1),(68, '猪肉末', '50克', 0),(68, '姜', '2片', 0),(68, '盐', '适量', 0),
|
||||
(69, '鸡蛋', '3个', 1),(69, '面粉', '100克', 1),(69, '葱', '2根', 0),(69, '盐', '适量', 0),
|
||||
(70, '小米', '100克', 1),(70, '南瓜', '200克', 1),(70, '糖', '少许', 0),
|
||||
(71, '饺子', '15个', 1),(71, '油', '适量', 0),(71, '醋', '蘸料', 0),
|
||||
(72, '吐司', '4片', 1),(72, '鸡蛋', '1个', 1),(72, '火腿', '2片', 1),(72, '生菜', '2片', 0),(72, '番茄', '1个', 0),(72, '沙拉酱', '适量', 0),
|
||||
(73, '面粉', '200克', 1),(73, '黄豆', '100克', 1),(73, '酵母', '2克', 0),(73, '泡打粉', '1克', 0),(73, '盐', '适量', 0),(73, '糖', '适量', 0),
|
||||
(74, '猪肉末', '200克', 1),(74, '馄饨皮', '30张', 1),(74, '虾皮', '适量', 0),(74, '紫菜', '适量', 0),(74, '姜', '2片', 0),(74, '生抽', '1勺', 0),(74, '料酒', '1勺', 0),(74, '盐', '适量', 0),(74, '香油', '少许', 0),
|
||||
(75, '五花肉', '500克', 1),(75, '面粉', '300克', 1),(75, '酵母', '3克', 0),(75, '姜', '3片', 0),(75, '生抽', '2勺', 0),(75, '老抽', '1勺', 0),(75, '料酒', '1勺', 0),(75, '盐', '适量', 0),
|
||||
(76, '糯米', '200克', 1),(76, '鸡腿肉', '150克', 1),(76, '干香菇', '5朵', 0),(76, '生抽', '1勺', 0),(76, '蚝油', '1勺', 0),(76, '盐', '适量', 0),
|
||||
(77, '面粉', '150克', 1),(77, '鸡蛋', '2个', 1),(77, '薄脆', '2片', 0),(77, '生菜', '2片', 0),(77, '葱', '1根', 0),(77, '甜面酱', '适量', 0),(77, '辣椒酱', '适量', 0),(77, '盐', '适量', 0),
|
||||
(78, '面粉', '200克', 1),(78, '红豆沙', '150克', 1),(78, '酵母', '2克', 0),(78, '糖', '20克', 0);
|
||||
|
||||
-- 汤羹类 (79-86)
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
(79, '番茄', '2个', 1),(79, '鸡蛋', '2个', 1),(79, '盐', '适量', 0),(79, '香油', '少许', 0),
|
||||
(80, '紫菜', '适量', 1),(80, '鸡蛋', '2个', 1),(80, '虾皮', '少许', 0),(80, '盐', '适量', 0),(80, '香油', '少许', 0),
|
||||
(81, '排骨', '300克', 1),(81, '玉米', '2根', 1),(81, '胡萝卜', '1根', 0),(81, '姜', '3片', 0),(81, '盐', '适量', 0),(81, '料酒', '1勺', 0),
|
||||
(82, '牛肉末', '100克', 1),(82, '豆腐', '1块', 0),(82, '鸡蛋清', '1个', 0),(82, '香菜', '适量', 0),(82, '姜', '2片', 0),(82, '淀粉', '适量', 0),(82, '盐', '适量', 0),(82, '香油', '少许', 0),
|
||||
(83, '豆腐', '1块', 1),(83, '木耳', '适量', 0),(83, '鸡蛋', '1个', 0),(83, '醋', '2勺', 0),(83, '白胡椒粉', '1勺', 0),(83, '淀粉', '适量', 0),(83, '盐', '适量', 0),(83, '香油', '少许', 0),
|
||||
(84, '鲫鱼', '1条', 1),(84, '豆腐', '1块', 1),(84, '姜', '5片', 0),(84, '葱', '2根', 0),(84, '料酒', '1勺', 0),(84, '盐', '适量', 0),
|
||||
(85, '排骨', '300克', 1),(85, '莲藕', '1节', 1),(85, '姜', '3片', 0),(85, '盐', '适量', 0),(85, '料酒', '1勺', 0),
|
||||
(86, '银耳', '1朵', 1),(86, '莲子', '50克', 1),(86, '冰糖', '适量', 0);
|
||||
|
||||
-- 凉菜类 (87-94)
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
(87, '黄瓜', '2根', 1),(87, '蒜', '5瓣', 0),(87, '醋', '2勺', 0),(87, '生抽', '1勺', 0),(87, '盐', '适量', 0),(87, '香油', '少许', 0),
|
||||
(88, '皮蛋', '2个', 1),(88, '豆腐', '1块', 1),(88, '蒜', '3瓣', 0),(88, '生抽', '1勺', 0),(88, '醋', '1勺', 0),(88, '香油', '少许', 0),(88, '盐', '适量', 0),
|
||||
(89, '木耳', '适量', 1),(89, '蒜', '5瓣', 0),(89, '醋', '1勺', 0),(89, '生抽', '1勺', 0),(89, '辣椒油', '适量', 0),(89, '盐', '适量', 0),(89, '香油', '少许', 0),
|
||||
(90, '黄瓜', '1根', 1),(90, '胡萝卜', '半根', 1),(90, '粉丝', '1把', 1),(90, '蒜', '5瓣', 0),(90, '醋', '2勺', 0),(90, '生抽', '1勺', 0),(90, '盐', '适量', 0),(90, '香油', '少许', 0),
|
||||
(91, '番茄', '2个', 1),(91, '糖', '2勺', 0),
|
||||
(92, '鸡胸肉', '200克', 1),(92, '黄瓜', '半根', 0),(92, '蒜', '5瓣', 0),(92, '醋', '1勺', 0),(92, '生抽', '1勺', 0),(92, '辣椒油', '适量', 0),(92, '盐', '适量', 0),(92, '香油', '少许', 0),
|
||||
(93, '黄瓜', '2根', 1),(93, '蒜', '5瓣', 0),(93, '醋', '2勺', 0),(93, '生抽', '1勺', 0),(93, '盐', '适量', 0),(93, '辣椒油', '适量', 0),
|
||||
(94, '海带丝', '200克', 1),(94, '蒜', '5瓣', 0),(94, '醋', '1勺', 0),(94, '生抽', '1勺', 0),(94, '辣椒油', '适量', 0),(94, '盐', '适量', 0),(94, '香油', '少许', 0);
|
||||
|
||||
-- 主食类 (95-100)
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
(95, '米饭', '300克', 1),(95, '鸡蛋', '2个', 1),(95, '葱', '1根', 0),(95, '盐', '适量', 0),
|
||||
(96, '面条', '200克', 1),(96, '番茄', '2个', 1),(96, '鸡蛋', '2个', 1),(96, '葱', '1根', 0),(96, '盐', '适量', 0),(96, '生抽', '1勺', 0),
|
||||
(97, '面条', '200克', 1),(97, '猪肉末', '150克', 1),(97, '黄豆酱', '1勺', 0),(97, '甜面酱', '1勺', 0),(97, '黄瓜', '半根', 0),(97, '姜', '2片', 0),(97, '蒜', '3瓣', 0),(97, '料酒', '1勺', 0),
|
||||
(98, '面条', '200克', 1),(98, '葱', '5根', 1),(98, '生抽', '2勺', 0),(98, '老抽', '1勺', 0),(98, '糖', '少许', 0),
|
||||
(99, '米粉', '200克', 1),(99, '鸡蛋', '2个', 1),(99, '豆芽', '100克', 0),(99, '生抽', '1勺', 0),(99, '盐', '适量', 0),
|
||||
(100, '面条', '200克', 1),(100, '葱', '2根', 0),(100, '生抽', '1勺', 0),(100, '香油', '少许', 0),(100, '盐', '适量', 0);
|
||||
|
||||
-- ============================================================
|
||||
-- 菜谱步骤数据(只列关键步骤,每道菜3-4步)
|
||||
-- ============================================================
|
||||
INSERT INTO recipe_steps (recipe_id, step_order, content) VALUES
|
||||
(1,1,'西红柿洗净切块,鸡蛋打散加少许盐搅匀'),(1,2,'热锅凉油,倒入蛋液划散盛出'),(1,3,'锅留底油爆香葱,下西红柿翻炒出汁'),(1,4,'倒入鸡蛋,加糖和盐调味,翻炒均匀出锅'),
|
||||
(2,1,'土豆去皮切细丝,冷水浸泡去淀粉后沥干'),(2,2,'热锅凉油爆香干辣椒和花椒'),(2,3,'下土豆丝大火翻炒,加醋和盐调味'),(2,4,'炒至土豆丝断生即可出锅'),
|
||||
(3,1,'猪肉切丝加料酒、生抽、淀粉腌制10分钟'),(3,2,'青椒切丝,姜蒜切末'),(3,3,'热油滑炒肉丝至变色盛出'),(3,4,'爆香姜蒜下青椒翻炒,倒回肉丝加调料炒匀'),
|
||||
(4,1,'西兰花掰小朵焯水1分钟捞出'),(4,2,'蒜切末热油爆香'),(4,3,'下西兰花大火快炒,加蚝油和盐调味'),
|
||||
(5,1,'里脊肉切条加蛋液、盐、淀粉腌制'),(5,2,'六成油温炸至金黄捞出,再复炸至酥脆'),(5,3,'番茄酱、糖、醋、水调成糖醋汁'),(5,4,'锅留底油倒入糖醋汁熬至浓稠,倒肉条裹匀'),
|
||||
(6,1,'四季豆去筋掰段焯水沥干'),(6,2,'热油爆香干辣椒、花椒和蒜末'),(6,3,'下四季豆大火煸炒至表面起皱'),(6,4,'加盐调味炒匀出锅'),
|
||||
(7,1,'鸡翅表面划刀焯水去血沫'),(7,2,'热油煎鸡翅至两面金黄'),(7,3,'加姜片、料酒、生抽翻炒'),(7,4,'倒入可乐没过鸡翅,中火炖至汤汁浓稠'),
|
||||
(8,1,'韭黄洗净切段,鸡蛋打散'),(8,2,'热油炒鸡蛋至凝固盛出'),(8,3,'锅中炒韭黄至变软,倒回鸡蛋加盐炒匀'),
|
||||
(9,1,'豆腐切块焯水去豆腥味'),(9,2,'热油爆香蒜末加生抽蚝油'),(9,3,'加少量水放入豆腐中火炖5分钟'),(9,4,'加糖和盐调味大火收汁出锅'),
|
||||
(10,1,'生菜洗净掰开焯水5秒捞出摆盘'),(10,2,'蒜切末热油爆香加蚝油略炒'),(10,3,'浇在生菜上即可'),
|
||||
(11,1,'土豆去皮切片,热油煎至两面金黄'),(11,2,'撒孜然粉、辣椒粉和盐'),(11,3,'翻匀出锅'),
|
||||
(12,1,'洋葱切丝,猪肉切片加料酒生抽腌制'),(12,2,'热油炒肉片至变色盛出'),(12,3,'炒洋葱至变软倒回肉片加盐炒匀'),
|
||||
(13,1,'香菇泡发切片,油菜焯水备用'),(13,2,'热油爆香蒜末下香菇翻炒'),(13,3,'加蚝油和盐调味'),(13,4,'放入油菜翻匀出锅'),
|
||||
(14,1,'豆腐切块煎至两面金黄'),(14,2,'青椒和木耳炒香'),(14,3,'加生抽蚝油和少量水放入豆腐炖3分钟'),(14,4,'加盐调味收汁出锅'),
|
||||
(15,1,'猪肉切丝加料酒腌制,雪菜切碎'),(15,2,'热油下姜片炒肉丝至变色'),(15,3,'加雪菜翻炒加盐调味出锅'),
|
||||
(16,1,'豆芽洗净沥干'),(16,2,'热油爆香蒜末和干辣椒'),(16,3,'下豆芽大火快炒加醋和盐,断生即出锅'),
|
||||
(17,1,'芹菜切段焯水,香干切片'),(17,2,'热油爆香蒜末下香干煸炒'),(17,3,'加芹菜翻炒加生抽和盐调味'),
|
||||
(18,1,'番茄切块,菜花掰小朵焯水'),(18,2,'热油下番茄翻炒出汁'),(18,3,'加菜花翻炒加盐和糖调味'),
|
||||
(19,1,'茄子切滚刀块加盐腌制挤水'),(19,2,'热油煸炒茄条至软盛出'),(19,3,'爆香姜蒜加肉末炒至变色'),(19,4,'加生抽蚝油倒回茄子翻炒收汁'),
|
||||
(20,1,'羊肉切片加料酒生抽腌制'),(20,2,'大葱斜刀切片'),(20,3,'热油爆香姜片下羊肉大火快炒'),(20,4,'羊肉变色后加葱片、孜然粉和盐快速翻炒出锅'),
|
||||
(21,1,'豆腐切块焯水备用'),(21,2,'热油炒肉末至变色加豆瓣酱炒出红油'),(21,3,'加姜蒜末和适量水放入豆腐中火炖5分钟'),(21,4,'撒花椒粉出锅'),
|
||||
(22,1,'五花肉冷水下锅加姜料酒煮至八成熟切片'),(22,2,'热锅少油煸炒肉片至卷曲出油'),(22,3,'加豆瓣酱姜蒜炒出红油'),(22,4,'加蒜苗段和生抽翻炒至蒜苗断生'),
|
||||
(23,1,'鸡胸肉切丁加料酒淀粉腌制,花生米油炸备用'),(23,2,'调酱汁:生抽+醋+糖+淀粉+水'),(23,3,'热油爆香干辣椒花椒姜蒜下鸡丁滑炒'),(23,4,'倒酱汁翻炒至浓稠加花生米翻匀'),
|
||||
(24,1,'猪肉切片加淀粉料酒腌制'),(24,2,'热油爆香豆瓣酱姜蒜干辣椒花椒'),(24,3,'加水煮沸下白菜豆芽煮熟捞出铺碗底'),(24,4,'肉片下锅滑熟连汤倒入碗中,表面撒干辣椒花椒淋热油'),
|
||||
(25,1,'猪肉切丝加料酒淀粉腌制'),(25,2,'调鱼香汁:生抽+醋+糖+淀粉+水'),(25,3,'木耳胡萝卜青椒切丝,热油爆香豆瓣酱姜蒜'),(25,4,'下肉丝滑炒加配菜倒鱼香汁炒匀'),
|
||||
(26,1,'鸡腿肉切丁加料酒生抽淀粉腌制'),(26,2,'干辣椒剪段,热油炸鸡丁至金黄捞出'),(26,3,'锅留底油爆香干辣椒花椒姜蒜'),(26,4,'倒回鸡丁快速翻炒加盐出锅'),
|
||||
(27,1,'牛腱子和牛肚加姜料酒卤煮至熟烂切片'),(27,2,'调汁:辣椒油+花椒粉+蒜泥+生抽+醋+糖'),(27,3,'浇在肉片上撒花生碎和香菜'),
|
||||
(28,1,'鸡腿加姜葱料酒煮熟捞出冰水过凉'),(28,2,'斩块摆盘'),(28,3,'调汁:辣椒油+花椒粉+蒜泥+生抽+醋+糖'),(28,4,'浇汁撒花生碎和葱花'),
|
||||
(29,1,'牛肉切丝加料酒生抽腌制'),(29,2,'芹菜切段,干辣椒剪段'),(29,3,'热油爆香干辣椒姜蒜下牛肉丝煸炒'),(29,4,'加芹菜翻炒加盐出锅'),
|
||||
(30,1,'五花肉加姜片料酒冷水煮熟切片摆盘'),(30,2,'调汁:蒜泥+生抽+醋+辣椒油+糖'),(30,3,'浇在肉片上即可'),
|
||||
(31,1,'草鱼片加料酒盐淀粉腌制,酸菜切丝'),(31,2,'热油炒酸菜加姜蒜干辣椒花椒'),(31,3,'加水煮沸下鱼片滑熟'),(31,4,'表面撒干辣椒花椒淋热油'),
|
||||
(32,1,'鸭血毛肚午餐肉切片焯水'),(32,2,'热油爆香豆瓣酱姜蒜干辣椒花椒'),(32,3,'加水煮沸下所有食材煮5分钟'),(32,4,'撒蒜末淋热油'),
|
||||
(33,1,'虾去虾线藕土豆切片焯水'),(33,2,'热油爆香豆瓣酱姜蒜干辣椒花椒'),(33,3,'下所有食材大火翻炒加生抽'),(33,4,'炒至入味撒芝麻出锅'),
|
||||
(34,1,'鸡爪剪指甲对半切开焯水'),(34,2,'加姜蒜料酒煮熟捞出'),(34,3,'泡椒连水加白醋和盐调成泡汁'),(34,4,'鸡爪浸入泡汁冷藏4小时以上'),
|
||||
(35,1,'面条煮熟过凉水沥干'),(35,2,'芝麻酱调稀加生抽醋蒜泥辣椒油花椒粉'),(35,3,'猪肉末炒香加调料做臊子'),(35,4,'面条拌酱撒臊子和葱花'),
|
||||
(36,1,'整鸡处理干净加姜葱料酒冷水煮开'),(36,2,'转小火浸煮20分钟至熟'),(36,3,'捞出冰水浸泡使皮爽滑'),(36,4,'斩块摆盘配姜葱油蘸料'),
|
||||
(37,1,'鲈鱼处理干净两侧划刀抹盐和料酒'),(37,2,'鱼身铺姜片葱段'),(37,3,'水开上锅大火蒸8-10分钟'),(37,4,'倒掉蒸汁淋蒸鱼豉油,热油浇在鱼身上'),
|
||||
(38,1,'虾洗净剪去虾须挑虾线'),(38,2,'水加姜片料酒烧开下虾'),(38,3,'煮至虾变红弯曲即可捞出摆盘'),
|
||||
(39,1,'梅花肉加叉烧酱生抽料酒姜蒜腌制4小时'),(39,2,'烤箱200度烤15分钟取出刷蜂蜜'),(39,3,'翻面再烤15分钟至表面焦香'),(39,4,'切片装盘'),
|
||||
(40,1,'里脊肉切丁加蛋液盐淀粉腌,菠萝青椒切块'),(40,2,'六成油温炸肉至金黄捞出'),(40,3,'番茄酱糖醋水调汁熬浓'),(40,4,'倒肉和菠萝青椒翻炒裹匀'),
|
||||
(41,1,'牛肉切片加生抽料酒淀粉腌制'),(41,2,'河粉抖散热油炒至微焦盛出'),(41,3,'热油爆炒牛肉至变色加豆芽'),(41,4,'倒回河粉加生抽老抽大火翻炒均匀撒葱'),
|
||||
(42,1,'粉丝温水泡软扇贝洗净'),(42,2,'蒜切末热油炸至金黄加生抽蚝油'),(42,3,'扇贝上铺粉丝和蒜蓉'),(42,4,'水开上锅大火蒸5分钟'),
|
||||
(43,1,'排骨斩小块加所有调料腌制30分钟'),(43,2,'码入盘中'),(43,3,'水开上锅大火蒸15-20分钟'),
|
||||
(44,1,'大米泡30分钟排骨腌制腊肠切片'),(44,2,'砂锅抹油放米加水大火煮开'),(44,3,'水快干时铺排骨腊肠转小火焖15分钟'),(44,4,'沿锅边淋油续焖3分钟加焯水油菜淋生抽蚝油'),
|
||||
(45,1,'排骨焯水去血沫'),(45,2,'冬瓜去皮切大块'),(45,3,'排骨加姜片料酒足量水大火煮开转小火炖40分钟'),(45,4,'加冬瓜续炖15分钟加盐调味'),
|
||||
(46,1,'牛肉切片加料酒生抽淀粉腌制'),(46,2,'青椒切块,热油滑牛肉至变色盛出'),(46,3,'爆香姜蒜下青椒翻炒'),(46,4,'倒回牛肉加蚝油翻炒均匀出锅'),
|
||||
(47,1,'虾仁切粒加调料拌匀做馅'),(47,2,'澄粉加淀粉开水烫面揉匀'),(47,3,'擀皮包入虾馅捏成饺子形'),(47,4,'水开上锅大火蒸8分钟'),
|
||||
(48,1,'大米提前泡好磨成米浆'),(48,2,'平盘刷油倒薄薄一层米浆'),(48,3,'撒虾仁或牛肉碎上锅蒸2分钟'),(48,4,'刮起装盘淋生抽'),
|
||||
(49,1,'面粉加酵母糖温水和面发酵至2倍大'),(49,2,'叉烧肉切小丁加蚝油生抽调馅'),(49,3,'面团分剂子包入叉烧馅'),(49,4,'二次发酵后上锅大火蒸12分钟'),
|
||||
(50,1,'鸡蛋打散加糖牛奶搅匀过筛'),(50,2,'倒入蛋挞皮八分满'),(50,3,'烤箱200度烤15-20分钟至表面微焦'),
|
||||
(51,1,'鱼头处理干净抹盐料酒腌制'),(51,2,'盘中铺姜葱放鱼头'),(51,3,'铺满剁椒上锅大火蒸10-12分钟'),(51,4,'倒掉蒸汁淋蒸鱼豉油,热油浇在剁椒上'),
|
||||
(52,1,'五花肉切薄片青椒斜刀切圈'),(52,2,'热锅少油煸五花肉至出油卷曲'),(52,3,'加姜蒜爆香加青椒大火翻炒'),(52,4,'加生抽和盐炒至青椒断生'),
|
||||
(53,1,'酸豆角切粒猪肉剁末'),(53,2,'热油炒肉末至变色加姜蒜干辣椒'),(53,3,'加酸豆角翻炒加生抽和盐'),
|
||||
(54,1,'腊肉腊鱼温水洗净切片'),(54,2,'码入盘中加姜蒜干辣椒和料酒'),(54,3,'上锅大火蒸20分钟'),
|
||||
(55,1,'花菜掰小朵焯水,五花肉切薄片'),(55,2,'热锅少油煸五花肉出油'),(55,3,'加干辣椒和蒜爆香下花菜翻炒'),(55,4,'加生抽和盐翻炒至花菜入味'),
|
||||
(56,1,'小龙虾刷净去虾线焯水'),(56,2,'热油爆香豆瓣酱姜蒜干辣椒花椒'),(56,3,'下小龙虾翻炒加生抽料酒糖和适量水'),(56,4,'中火焖煮15分钟收汁'),
|
||||
(57,1,'青椒直接放火上烤至表皮焦黑'),(57,2,'去皮撕成条'),(57,3,'皮蛋剥壳切瓣摆盘'),(57,4,'加蒜泥生抽醋盐拌匀'),
|
||||
(58,1,'猪肉剁末萝卜干泡发切碎'),(58,2,'热油炒肉末至变色加姜蒜干辣椒'),(58,3,'加萝卜干翻炒加生抽和盐'),
|
||||
(59,1,'里脊肉切厚片用刀背拍松加盐料酒腌制'),(59,2,'淀粉加水调糊肉片裹糊六成油温炸至金黄'),(59,3,'复炸至酥脆捞出'),(59,4,'糖醋水调汁熬浓倒肉片快速翻匀'),
|
||||
(60,1,'土豆茄子青椒切滚刀块'),(60,2,'分别油炸:土豆炸金黄茄子炸软青椒过油'),(60,3,'爆香蒜末加生抽糖盐和少许水'),(60,4,'倒入所有食材翻炒均匀勾薄芡'),
|
||||
(61,1,'五花肉切块焯水粉条泡软'),(61,2,'热锅少油煸五花肉至出油加姜蒜生抽老抽料酒'),(61,3,'加足量水大火煮开转小火炖30分钟'),(61,4,'加粉条和白菜续炖10分钟加盐调味'),
|
||||
(62,1,'排骨焯水土豆切块豆角掰段玉米切段'),(62,2,'热油爆香姜片下排骨翻炒'),(62,3,'加生抽和足量水大火煮沸转小火炖20分钟'),(62,4,'加土豆豆角玉米续炖20分钟加盐'),
|
||||
(63,1,'里脊肉切丝加生抽料酒淀粉腌制'),(63,2,'热油滑炒肉丝至变色盛出'),(63,3,'锅中炒甜面酱加糖和少许水'),(63,4,'倒回肉丝翻炒裹匀酱汁配葱丝和豆腐皮'),
|
||||
(64,1,'地瓜去皮切滚刀块'),(64,2,'六成油温炸地瓜至金黄熟透捞出'),(64,3,'另起锅加糖和少量水小火熬至琥珀色'),(64,4,'迅速倒入地瓜块翻匀拉丝装盘'),
|
||||
(65,1,'五花肉切大块焯水酸菜切丝'),(65,2,'热锅少油煸五花肉至出油加姜蒜料酒'),(65,3,'加足量水大火煮开转小火炖30分钟'),(65,4,'加酸菜续炖20分钟加盐'),
|
||||
(66,1,'里脊肉切条加料酒盐腌制,淀粉加水调糊'),(66,2,'肉条裹糊六成油温炸至金黄捞出复炸'),(66,3,'调汁:生抽+糖+醋+淀粉+水'),(66,4,'爆香姜蒜下青椒胡萝卜倒汁熬浓加肉条翻匀'),
|
||||
(67,1,'面粉加水和少许盐揉成光滑面团醒30分钟'),(67,2,'擀成薄饼刷油撒葱花'),(67,3,'卷起盘成圆饼再擀薄'),(67,4,'平底锅刷油中小火烙至两面金黄打入鸡蛋摊匀'),
|
||||
(68,1,'大米泡30分钟加水大火煮开转小火'),(68,2,'皮蛋切丁猪肉末加料酒腌制'),(68,3,'粥熬至米粒开花加肉末皮蛋姜丝搅匀'),(68,4,'续煮5分钟加盐调味'),
|
||||
(69,1,'鸡蛋打散加面粉盐和水搅成面糊'),(69,2,'平底锅刷油倒入面糊摊平'),(69,3,'中小火煎至底面金黄翻面'),(69,4,'煎至两面金黄撒葱花出锅切块'),
|
||||
(70,1,'小米洗净南瓜去皮切小块'),(70,2,'加水大火煮开转小火'),(70,3,'熬25分钟至小米和南瓜软烂'),(70,4,'根据口味加少许糖'),
|
||||
(71,1,'饺子提前包好或冷冻饺子'),(71,2,'平底锅刷油摆入饺子'),(71,3,'加没过饺子三分之一的水加盖中火煎'),(71,4,'水干后改小火煎至底部金黄酥脆'),
|
||||
(72,1,'吐司去边鸡蛋打散煎成蛋皮'),(72,2,'吐司上依次铺生菜火腿番茄蛋皮'),(72,3,'挤沙拉酱盖上另一片吐司'),(72,4,'对角切开成三角形'),
|
||||
(73,1,'黄豆泡发打豆浆过滤煮沸'),(73,2,'面粉加酵母泡打粉盐糖和面发酵'),(73,3,'面团分剂子擀成长条入油锅炸至金黄蓬松'),(73,4,'配豆浆食用'),
|
||||
(74,1,'猪肉末加生抽料酒姜末盐和少许水调馅'),(74,2,'馄饨皮包入馅料捏紧'),(74,3,'水开下馄饨煮至浮起'),(74,4,'碗底放虾皮紫菜盐香油加汤盛入馄饨'),
|
||||
(75,1,'五花肉切大块焯水加生抽老抽料酒姜炖烂'),(75,2,'面粉加酵母温水和面发酵至2倍大'),(75,3,'面团分剂子擀饼烙至两面微黄'),(75,4,'肉剁碎夹入饼中浇少许肉汁'),
|
||||
(76,1,'糯米泡4小时鸡腿肉切丁加生抽蚝油腌制'),(76,2,'香菇泡发切丁热油爆香'),(76,3,'加鸡肉炒变色倒入糯米翻炒'),(76,4,'加盐调味包入荷叶上锅蒸40分钟'),
|
||||
(77,1,'面粉加盐和水调成面糊醒20分钟'),(77,2,'平底锅抹薄薄一层面糊打入鸡蛋摊匀撒葱'),(77,3,'翻面刷甜面酱和辣椒酱'),(77,4,'放薄脆和生菜卷起即可'),
|
||||
(78,1,'面粉加酵母糖温水和面发酵至2倍大'),(78,2,'面团分剂子擀皮包入红豆沙'),(78,3,'收口朝下二次发酵15分钟'),(78,4,'上锅大火蒸12分钟焖3分钟开盖'),
|
||||
(79,1,'番茄切块鸡蛋打散'),(79,2,'热油下番茄翻炒出汁加适量水煮开'),(79,3,'淋入蛋液搅成蛋花'),(79,4,'加盐和香油调味出锅'),
|
||||
(80,1,'紫菜撕碎鸡蛋打散'),(80,2,'水烧开下紫菜和虾皮'),(80,3,'淋入蛋液搅成蛋花'),(80,4,'加盐和香油出锅'),
|
||||
(81,1,'排骨焯水玉米切段胡萝卜切滚刀块'),(81,2,'排骨加姜片料酒足量水大火煮开转小火炖40分钟'),(81,3,'加玉米和胡萝卜续炖20分钟'),(81,4,'加盐调味出锅'),
|
||||
(82,1,'牛肉末加蛋清淀粉少许盐拌匀'),(82,2,'豆腐切小丁焯水'),(82,3,'水加姜片煮开下牛肉滑散'),(82,4,'加豆腐煮3分钟勾薄芡加盐香油撒香菜'),
|
||||
(83,1,'豆腐切丝木耳切丝鸡蛋打散'),(83,2,'水加姜片煮开下豆腐木耳'),(83,3,'加醋白胡椒粉和盐调味'),(83,4,'淋蛋液勾芡加香油出锅'),
|
||||
(84,1,'鲫鱼处理干净两面划刀抹盐料酒'),(84,2,'热油煎鲫鱼至两面微黄'),(84,3,'加姜片和足量开水大火煮10分钟至汤变白'),(84,4,'加豆腐块续煮5分钟加盐撒葱'),
|
||||
(85,1,'排骨焯水莲藕去皮切滚刀块'),(85,2,'排骨加姜片料酒足量水大火煮开转小火炖40分钟'),(85,3,'加莲藕续炖20分钟'),(85,4,'加盐调味出锅'),
|
||||
(86,1,'银耳泡发撕小朵莲子去芯泡发'),(86,2,'银耳和莲子放入锅中加足量水'),(86,3,'大火煮开转小火熬40分钟至银耳出胶'),(86,4,'加冰糖搅拌融化即可'),
|
||||
(87,1,'黄瓜洗净用刀拍裂切段'),(87,2,'蒜切末加生抽醋盐调汁'),(87,3,'浇在黄瓜上拌匀淋香油'),
|
||||
(88,1,'皮蛋剥壳切瓣豆腐切块'),(88,2,'豆腐垫底皮蛋摆上面'),(88,3,'调汁:蒜末+生抽+醋+香油+盐'),(88,4,'浇汁即可'),
|
||||
(89,1,'木耳泡发洗净焯水2分钟过凉'),(89,2,'调汁:蒜末+生抽+醋+辣椒油+盐+香油'),(89,3,'浇在木耳上拌匀'),
|
||||
(90,1,'黄瓜胡萝卜切丝粉丝泡软焯水'),(90,2,'调汁:蒜末+生抽+醋+盐+香油'),(90,3,'所有食材混合浇汁拌匀'),
|
||||
(91,1,'番茄洗净切块'),(91,2,'撒白糖拌匀即可'),
|
||||
(92,1,'鸡胸肉加姜片料酒煮熟撕成丝'),(92,2,'黄瓜切丝'),(92,3,'调汁:蒜末+生抽+醋+辣椒油+盐+香油'),(92,4,'鸡丝和黄瓜混合浇汁拌匀'),
|
||||
(93,1,'黄瓜洗净用刀拍裂切段'),(93,2,'蒜切末加生抽醋盐辣椒油调汁'),(93,3,'浇在黄瓜上拌匀'),
|
||||
(94,1,'海带丝焯水过凉'),(94,2,'调汁:蒜末+生抽+醋+辣椒油+盐+香油'),(94,3,'浇在海带丝上拌匀'),
|
||||
(95,1,'鸡蛋打散,葱切末'),(95,2,'热油炒鸡蛋划散盛出'),(95,3,'锅中放米饭炒散加鸡蛋翻炒'),(95,4,'加盐和葱末翻炒均匀出锅'),
|
||||
(96,1,'番茄切块鸡蛋打散'),(96,2,'热油炒鸡蛋盛出,炒番茄出汁加水煮开'),(96,3,'下面条煮熟'),(96,4,'倒回鸡蛋加盐生抽调味撒葱花'),
|
||||
(97,1,'猪肉剁末,热油炒肉末至变色加姜蒜'),(97,2,'加黄豆酱和甜面酱翻炒加料酒和少许水熬煮'),(97,3,'面条煮熟过凉沥干盛碗'),(97,4,'码上肉酱和黄瓜丝拌匀'),
|
||||
(98,1,'葱切段热油炸至金黄焦香得葱油'),(98,2,'面碗底加生抽老抽糖'),(98,3,'面条煮熟捞入碗中'),(98,4,'淋上葱油拌匀'),
|
||||
(99,1,'米粉泡软沥干鸡蛋打散'),(99,2,'热油炒鸡蛋盛出,炒豆芽'),(99,3,'下米粉翻炒加生抽和盐'),(99,4,'倒回鸡蛋翻炒均匀出锅'),
|
||||
(100,1,'面条煮熟捞入碗中'),(100,2,'加生抽盐香油调味'),(100,3,'撒上葱花浇上面汤即可');
|
||||
49
backend/src/main/resources/db/data_ingredients.sql
Normal file
49
backend/src/main/resources/db/data_ingredients.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
-- ============================================================
|
||||
-- 100道菜谱食材数据
|
||||
-- ============================================================
|
||||
USE chowbox;
|
||||
|
||||
-- 家常快手菜食材
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
-- 1. 西红柿炒鸡蛋
|
||||
(1, '西红柿', '2个', 1),(1, '鸡蛋', '3个', 1),(1, '葱', '1根', 0),(1, '盐', '适量', 0),(1, '糖', '少许', 0),(1, '油', '适量', 0),
|
||||
-- 2. 酸辣土豆丝
|
||||
(2, '土豆', '2个', 1),(2, '干辣椒', '3个', 0),(2, '花椒', '少许', 0),(2, '醋', '1勺', 0),(2, '盐', '适量', 0),(2, '葱', '1根', 0),(2, '油', '适量', 0),
|
||||
-- 3. 青椒肉丝
|
||||
(3, '猪里脊', '200克', 1),(3, '青椒', '3个', 1),(3, '姜', '3片', 0),(3, '蒜', '2瓣', 0),(3, '生抽', '1勺', 0),(3, '料酒', '1勺', 0),(3, '淀粉', '1勺', 0),(3, '盐', '适量', 0),(3, '油', '适量', 0),
|
||||
-- 4. 蒜蓉西兰花
|
||||
(4, '西兰花', '1颗', 1),(4, '蒜', '5瓣', 0),(4, '盐', '适量', 0),(4, '蚝油', '1勺', 0),(4, '油', '适量', 0),
|
||||
-- 5. 糖醋里脊
|
||||
(5, '猪里脊', '300克', 1),(5, '鸡蛋', '1个', 0),(5, '面粉', '50克', 0),(5, '番茄酱', '2勺', 0),(5, '白糖', '2勺', 0),(5, '白醋', '1勺', 0),(5, '盐', '适量', 0),(5, '油', '适量', 0),
|
||||
-- 6. 干煸四季豆
|
||||
(6, '四季豆', '300克', 1),(6, '猪肉末', '50克', 0),(6, '干辣椒', '3个', 0),(6, '花椒', '少许', 0),(6, '蒜', '3瓣', 0),(6, '生抽', '1勺', 0),(6, '盐', '适量', 0),(6, '油', '适量', 0),
|
||||
-- 7. 可乐鸡翅
|
||||
(7, '鸡翅中', '8个', 1),(7, '可乐', '1罐', 0),(7, '生抽', '2勺', 0),(7, '老抽', '半勺', 0),(7, '姜', '3片', 0),(7, '料酒', '1勺', 0),(7, '盐', '少许', 0),(7, '油', '适量', 0),
|
||||
-- 8. 韭黄炒蛋
|
||||
(8, '韭黄', '200克', 1),(8, '鸡蛋', '3个', 1),(8, '盐', '适量', 0),(8, '油', '适量', 0),
|
||||
-- 9. 红烧豆腐
|
||||
(9, '老豆腐', '1块', 1),(9, '葱', '2根', 0),(9, '姜', '3片', 0),(9, '蒜', '2瓣', 0),(9, '生抽', '2勺', 0),(9, '老抽', '半勺', 0),(9, '豆瓣酱', '1勺', 0),(9, '糖', '少许', 0),(9, '淀粉', '1勺', 0),(9, '油', '适量', 0),
|
||||
-- 10. 蚝油生菜
|
||||
(10, '生菜', '1颗', 1),(10, '蒜', '3瓣', 0),(10, '蚝油', '2勺', 0),(10, '生抽', '1勺', 0),(10, '油', '适量', 0);
|
||||
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
-- 11. 孜然土豆片
|
||||
(11, '土豆', '2个', 1),(11, '孜然粉', '1勺', 0),(11, '辣椒粉', '半勺', 0),(11, '盐', '适量', 0),(11, '葱', '1根', 0),(11, '油', '适量', 0),
|
||||
-- 12. 洋葱炒肉
|
||||
(12, '洋葱', '1个', 1),(12, '猪肉片', '150克', 1),(12, '生抽', '1勺', 0),(12, '料酒', '1勺', 0),(12, '盐', '适量', 0),(12, '油', '适量', 0),
|
||||
-- 13. 香菇油菜
|
||||
(13, '油菜', '5棵', 1),(13, '香菇', '6朵', 1),(13, '蒜', '3瓣', 0),(13, '蚝油', '1勺', 0),(13, '盐', '适量', 0),(13, '油', '适量', 0),
|
||||
-- 14. 家常豆腐
|
||||
(14, '老豆腐', '1块', 1),(14, '木耳', '10朵', 0),(14, '青椒', '1个', 0),(14, '胡萝卜', '半根', 0),(14, '豆瓣酱', '1勺', 0),(14, '生抽', '1勺', 0),(14, '糖', '少许', 0),(14, '油', '适量', 0),
|
||||
-- 15. 雪菜肉丝
|
||||
(15, '雪里蕻', '200克', 1),(15, '猪肉丝', '100克', 1),(15, '干辣椒', '2个', 0),(15, '生抽', '1勺', 0),(15, '料酒', '1勺', 0),(15, '盐', '适量', 0),(15, '油', '适量', 0),
|
||||
-- 16. 素炒豆芽
|
||||
(16, '绿豆芽', '300克', 1),(16, '葱', '1根', 0),(16, '盐', '适量', 0),(16, '醋', '少许', 0),(16, '油', '适量', 0),
|
||||
-- 17. 芹菜香干
|
||||
(17, '芹菜', '200克', 1),(17, '豆干', '3块', 1),(17, '生抽', '1勺', 0),(17, '盐', '适量', 0),(17, '油', '适量', 0),
|
||||
-- 18. 番茄菜花
|
||||
(18, '菜花', '半颗', 1),(18, '西红柿', '1个', 1),(18, '番茄酱', '1勺', 0),(18, '盐', '适量', 0),(18, '糖', '少许', 0),(18, '油', '适量', 0),
|
||||
-- 19. 肉末茄子
|
||||
(19, '长茄子', '2根', 1),(19, '猪肉末', '100克', 1),(19, '蒜', '3瓣', 0),(19, '姜', '2片', 0),(19, '生抽', '2勺', 0),(19, '老抽', '半勺', 0),(19, '郫县豆瓣酱', '1勺', 0),(19, '糖', '少许', 0),(19, '料酒', '1勺', 0),(19, '油', '适量', 0),
|
||||
-- 20. 葱爆羊肉
|
||||
(20, '羊肉片', '250克', 1),(20, '大葱', '2根', 1),(20, '生抽', '2勺', 0),(20, '料酒', '1勺', 0),(20, '孜然粉', '半勺', 0),(20, '盐', '适量', 0),(20, '油', '适量', 0);
|
||||
46
backend/src/main/resources/db/data_ingredients_2.sql
Normal file
46
backend/src/main/resources/db/data_ingredients_2.sql
Normal file
@@ -0,0 +1,46 @@
|
||||
-- 川菜 + 粤菜食材数据
|
||||
USE chowbox;
|
||||
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
-- 21. 麻婆豆腐
|
||||
(21, '嫩豆腐', '1块', 1),(21, '猪肉末', '80克', 0),(21, '郫县豆瓣酱', '1.5勺', 0),(21, '花椒粉', '半勺', 0),(21, '辣椒面', '半勺', 0),(21, '豆豉', '少许', 0),(21, '蒜', '2瓣', 0),(21, '淀粉', '1勺', 0),(21, '葱', '1根', 0),(21, '油', '适量', 0),
|
||||
-- 22. 回锅肉
|
||||
(22, '五花肉', '300克', 1),(22, '蒜苗', '3根', 1),(22, '郫县豆瓣酱', '1勺', 0),(22, '生抽', '1勺', 0),(22, '姜', '3片', 0),(22, '蒜', '2瓣', 0),(22, '料酒', '1勺', 0),(22, '糖', '少许', 0),(22, '油', '适量', 0),
|
||||
-- 23. 宫保鸡丁
|
||||
(23, '鸡胸肉', '250克', 1),(23, '花生米', '50克', 0),(23, '黄瓜', '半根', 0),(23, '干辣椒', '5个', 0),(23, '花椒', '少许', 0),(23, '生抽', '1勺', 0),(23, '醋', '1勺', 0),(23, '糖', '1勺', 0),(23, '淀粉', '1勺', 0),(23, '蒜', '2瓣', 0),(23, '油', '适量', 0),
|
||||
-- 24. 水煮肉片
|
||||
(24, '猪里脊', '250克', 1),(24, '豆芽', '100克', 0),(24, '白菜叶', '3片', 0),(24, '郫县豆瓣酱', '2勺', 0),(24, '干辣椒', '10个', 0),(24, '花椒', '适量', 0),(24, '蒜', '5瓣', 0),(24, '料酒', '1勺', 0),(24, '淀粉', '1勺', 0),(24, '油', '适量', 0),
|
||||
-- 25. 鱼香肉丝
|
||||
(25, '猪里脊', '200克', 1),(25, '木耳', '10朵', 0),(25, '胡萝卜', '半根', 0),(25, '青椒', '1个', 0),(25, '泡椒', '4个', 0),(25, '蒜', '3瓣', 0),(25, '生抽', '1勺', 0),(25, '醋', '1勺', 0),(25, '糖', '1勺', 0),(25, '淀粉', '1勺', 0),(25, '郫县豆瓣酱', '半勺', 0),(25, '油', '适量', 0),
|
||||
-- 26. 辣子鸡丁
|
||||
(26, '鸡腿肉', '300克', 1),(26, '干辣椒', '20个', 0),(26, '花椒', '适量', 0),(26, '姜', '5片', 0),(26, '蒜', '3瓣', 0),(26, '料酒', '1勺', 0),(26, '生抽', '1勺', 0),(26, '白芝麻', '少许', 0),(26, '油', '适量', 0),
|
||||
-- 27. 夫妻肺片
|
||||
(27, '牛腱子', '150克', 1),(27, '牛肚', '150克', 1),(27, '花生碎', '30克', 0),(27, '香菜', '2根', 0),(27, '红油', '3勺', 0),(27, '花椒粉', '半勺', 0),(27, '生抽', '2勺', 0),(27, '醋', '1勺', 0),
|
||||
-- 28. 口水鸡
|
||||
(28, '鸡腿', '2个', 1),(28, '花生碎', '30克', 0),(28, '芝麻酱', '1勺', 0),(28, '红油', '2勺', 0),(28, '花椒粉', '半勺', 0),(28, '生抽', '2勺', 0),(28, '醋', '1勺', 0),(28, '蒜', '3瓣', 0),(28, '葱', '1根', 0),(28, '香菜', '1根', 0),
|
||||
-- 29. 干煸牛肉丝
|
||||
(29, '牛肉', '250克', 1),(29, '芹菜', '100克', 0),(29, '干辣椒', '5个', 0),(29, '花椒', '少许', 0),(29, '姜', '3片', 0),(29, '郫县豆瓣酱', '1勺', 0),(29, '生抽', '1勺', 0),(29, '料酒', '1勺', 0),(29, '油', '适量', 0),
|
||||
-- 30. 蒜泥白肉
|
||||
(30, '五花肉', '250克', 1),(30, '黄瓜', '1根', 0),(30, '蒜', '6瓣', 0),(30, '生抽', '2勺', 0),(30, '红油', '1勺', 0),(30, '醋', '半勺', 0),(30, '糖', '少许', 0);
|
||||
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
-- 31. 酸菜鱼
|
||||
(31, '草鱼片', '300克', 1),(31, '酸菜', '200克', 1),(31, '泡椒', '5个', 0),(31, '干辣椒', '5个', 0),(31, '花椒', '少许', 0),(31, '姜', '5片', 0),(31, '蒜', '3瓣', 0),(31, '料酒', '1勺', 0),(31, '淀粉', '1勺', 0),(31, '盐', '适量', 0),(31, '油', '适量', 0),
|
||||
-- 32. 毛血旺
|
||||
(32, '鸭血', '200克', 1),(32, '午餐肉', '100克', 1),(32, '毛肚', '100克', 0),(32, '豆芽', '100克', 0),(32, '郫县豆瓣酱', '2勺', 0),(32, '干辣椒', '10个', 0),(32, '花椒', '适量', 0),(32, '蒜', '3瓣', 0),(32, '料酒', '1勺', 0),(32, '油', '适量', 0),
|
||||
-- 33. 麻辣香锅
|
||||
(33, '虾', '10只', 1),(33, '土豆', '1个', 0),(33, '藕', '半节', 0),(33, '西兰花', '半颗', 0),(33, '香菇', '5朵', 0),(33, '火锅底料', '50克', 0),(33, '干辣椒', '8个', 0),(33, '花椒', '适量', 0),(33, '蒜', '3瓣', 0),(33, '油', '适量', 0),
|
||||
-- 34. 泡椒凤爪
|
||||
(34, '鸡爪', '500克', 1),(34, '泡椒', '200克', 0),(34, '白醋', '2勺', 0),(34, '盐', '适量', 0),(34, '料酒', '1勺', 0),(34, '姜', '5片', 0),
|
||||
-- 35. 担担面
|
||||
(35, '面条', '200克', 1),(35, '猪肉末', '80克', 0),(35, '芽菜', '30克', 0),(35, '花生碎', '20克', 0),(35, '芝麻酱', '1勺', 0),(35, '红油', '2勺', 0),(35, '生抽', '1勺', 0),(35, '醋', '少许', 0),(35, '葱', '1根', 0),(35, '油', '适量', 0),
|
||||
-- 36. 白切鸡
|
||||
(36, '三黄鸡', '1只', 1),(36, '姜', '5片', 0),(36, '葱', '3根', 0),(36, '料酒', '2勺', 0),(36, '生抽', '3勺', 0),(36, '花生油', '2勺', 0),(36, '蒜', '3瓣', 0),
|
||||
-- 37. 清蒸鲈鱼
|
||||
(37, '鲈鱼', '1条', 1),(37, '姜', '5片', 0),(37, '葱', '2根', 0),(37, '蒸鱼豉油', '2勺', 0),(37, '料酒', '1勺', 0),(37, '油', '适量', 0),
|
||||
-- 38. 白灼虾
|
||||
(38, '基围虾', '300克', 1),(38, '姜', '3片', 0),(38, '料酒', '1勺', 0),(38, '生抽', '2勺', 0),(38, '姜末', '少许', 0),(38, '香油', '少许', 0),
|
||||
-- 39. 广式叉烧
|
||||
(39, '猪梅花肉', '500克', 1),(39, '叉烧酱', '4勺', 0),(39, '蜂蜜', '2勺', 0),(39, '生抽', '1勺', 0),(39, '老抽', '半勺', 0),
|
||||
-- 40. 菠萝咕咾肉
|
||||
(40, '猪里脊', '250克', 1),(40, '菠萝', '半个', 1),(40, '青椒', '1个', 0),(40, '红椒', '1个', 0),(40, '番茄酱', '3勺', 0),(40, '白醋', '1勺', 0),(40, '糖', '2勺', 0),(40, '淀粉', '适量', 0),(40, '油', '适量', 0);
|
||||
44
backend/src/main/resources/db/data_ingredients_3.sql
Normal file
44
backend/src/main/resources/db/data_ingredients_3.sql
Normal file
@@ -0,0 +1,44 @@
|
||||
-- 粤菜 + 湘菜 + 东北菜 + 早餐 + 汤羹 + 凉菜 + 主食 食材数据
|
||||
USE chowbox;
|
||||
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
-- 41. 干炒牛河
|
||||
(41, '河粉', '300克', 1),(41, '牛肉片', '150克', 1),(41, '豆芽', '50克', 0),(41, '韭黄', '30克', 0),(41, '生抽', '2勺', 0),(41, '老抽', '半勺', 0),(41, '油', '适量', 0),
|
||||
-- 42. 蒜蓉粉丝蒸扇贝
|
||||
(42, '扇贝', '6只', 1),(42, '粉丝', '1把', 0),(42, '蒜', '6瓣', 0),(42, '蒸鱼豉油', '1勺', 0),(42, '葱', '1根', 0),(42, '油', '适量', 0),
|
||||
-- 43. 豉汁蒸排骨
|
||||
(43, '排骨', '300克', 1),(43, '豆豉', '1勺', 0),(43, '蒜', '3瓣', 0),(43, '生抽', '1勺', 0),(43, '料酒', '1勺', 0),(43, '淀粉', '1勺', 0),(43, '糖', '少许', 0),
|
||||
-- 44. 煲仔饭
|
||||
(44, '大米', '200克', 1),(44, '腊肠', '1根', 0),(44, '腊肉', '50克', 0),(44, '油菜', '3棵', 0),(44, '生抽', '1勺', 0),(44, '油', '少许', 0),
|
||||
-- 45. 冬瓜排骨汤
|
||||
(45, '排骨', '300克', 1),(45, '冬瓜', '300克', 1),(45, '姜', '3片', 0),(45, '盐', '适量', 0),
|
||||
-- 46. 蚝油牛肉
|
||||
(46, '牛肉', '250克', 1),(46, '青椒', '1个', 0),(46, '洋葱', '半个', 0),(46, '蚝油', '2勺', 0),(46, '生抽', '1勺', 0),(46, '料酒', '1勺', 0),(46, '淀粉', '1勺', 0),(46, '油', '适量', 0),
|
||||
-- 47. 虾饺
|
||||
(47, '虾仁', '200克', 1),(47, '猪肥膘', '30克', 0),(47, '澄面', '150克', 0),(47, '生粉', '50克', 0),(47, '竹笋', '30克', 0),(47, '盐', '适量', 0),
|
||||
-- 48. 肠粉
|
||||
(48, '粘米粉', '150克', 1),(48, '鸡蛋', '1个', 0),(48, '猪肉末', '50克', 0),(48, '生菜', '2片', 0),(48, '生抽', '2勺', 0),(48, '油', '少许', 0),
|
||||
-- 49. 叉烧包
|
||||
(49, '中筋面粉', '300克', 1),(49, '叉烧肉', '200克', 1),(49, '酵母', '3克', 0),(49, '糖', '20克', 0),(49, '蚝油', '1勺', 0),
|
||||
-- 50. 蛋挞
|
||||
(50, '蛋挞皮', '8个', 1),(50, '鸡蛋', '2个', 1),(50, '淡奶油', '100毫升', 0),(50, '牛奶', '100毫升', 0),(50, '糖', '30克', 0),
|
||||
-- 51. 剁椒鱼头
|
||||
(51, '胖头鱼头', '1个', 1),(51, '剁椒', '100克', 0),(51, '姜', '5片', 0),(51, '蒜', '3瓣', 0),(51, '料酒', '2勺', 0),(51, '蒸鱼豉油', '1勺', 0),(51, '葱', '1根', 0),(51, '油', '适量', 0),
|
||||
-- 52. 农家小炒肉
|
||||
(52, '五花肉', '200克', 1),(52, '青椒', '4个', 1),(52, '蒜', '3瓣', 0),(52, '生抽', '1勺', 0),(52, '老抽', '少许', 0),(52, '豆豉', '半勺', 0),(52, '油', '适量', 0),
|
||||
-- 53. 酸豆角肉末
|
||||
(53, '酸豆角', '200克', 1),(53, '猪肉末', '100克', 1),(53, '干辣椒', '3个', 0),(53, '蒜', '2瓣', 0),(53, '生抽', '1勺', 0),(53, '油', '适量', 0),
|
||||
-- 54. 腊味合蒸
|
||||
(54, '腊肉', '150克', 1),(54, '腊肠', '1根', 1),(54, '腊鱼', '100克', 0),(54, '豆豉', '1勺', 0),(54, '干辣椒', '2个', 0),(54, '料酒', '1勺', 0),
|
||||
-- 55. 干锅花菜
|
||||
(55, '花菜', '半颗', 1),(55, '五花肉片', '80克', 0),(55, '干辣椒', '5个', 0),(55, '蒜', '3瓣', 0),(55, '生抽', '1勺', 0),(55, '郫县豆瓣酱', '半勺', 0),(55, '油', '适量', 0),
|
||||
-- 56. 口味虾
|
||||
(56, '小龙虾', '500克', 1),(56, '干辣椒', '10个', 0),(56, '花椒', '适量', 0),(56, '蒜', '5瓣', 0),(56, '姜', '5片', 0),(56, '郫县豆瓣酱', '1勺', 0),(56, '啤酒', '半瓶', 0),(56, '紫苏', '少许', 0),(56, '油', '适量', 0),
|
||||
-- 57. 擂辣椒皮蛋
|
||||
(57, '青椒', '4个', 1),(57, '皮蛋', '2个', 1),(57, '蒜', '3瓣', 0),(57, '醋', '1勺', 0),(57, '生抽', '1勺', 0),(57, '香油', '少许', 0),
|
||||
-- 58. 湘西外婆菜
|
||||
(58, '酸菜', '200克', 1),(58, '猪肉末', '80克', 0),(58, '干辣椒', '3个', 0),(58, '蒜', '2瓣', 0),(58, '生抽', '1勺', 0),(58, '油', '适量', 0),
|
||||
-- 59. 锅包肉
|
||||
(59, '猪里脊', '300克', 1),(59, '胡萝卜', '半根', 0),(59, '大葱', '1段', 0),(59, '姜', '3片', 0),(59, '白醋', '3勺', 0),(59, '白糖', '3勺', 0),(59, '淀粉', '100克', 0),(59, '油', '适量', 0),
|
||||
-- 60. 地三鲜
|
||||
(60, '茄子', '1根', 1),(60, '土豆', '1个', 1),(60, '青椒', '2个', 1),(60, '蒜', '3瓣', 0),(60, '生抽', '2勺', 0),(60, '老抽', '半勺', 0),(60, '糖', '少许', 0),(60, '盐', '适量', 0),(60, '油', '适量', 0);
|
||||
43
backend/src/main/resources/db/data_ingredients_4.sql
Normal file
43
backend/src/main/resources/db/data_ingredients_4.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
-- 61-100 剩余食材
|
||||
USE chowbox;
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
-- 61. 猪肉炖粉条
|
||||
(61, '五花肉', '250克', 1),(61, '粉条', '100克', 1),(61, '白菜', '3片', 0),(61, '生抽', '2勺', 0),(61, '老抽', '半勺', 0),(61, '八角', '1个', 0),(61, '姜', '3片', 0),(61, '油', '适量', 0),
|
||||
-- 62. 东北乱炖
|
||||
(62, '排骨', '200克', 1),(62, '土豆', '2个', 0),(62, '豆角', '100克', 0),(62, '茄子', '1根', 0),(62, '玉米', '1根', 0),(62, '生抽', '2勺', 0),(62, '老抽', '半勺', 0),(62, '盐', '适量', 0),(62, '油', '适量', 0),
|
||||
-- 63. 京酱肉丝
|
||||
(63, '猪里脊', '250克', 1),(63, '大葱', '2根', 0),(63, '豆腐皮', '3张', 0),(63, '甜面酱', '2勺', 0),(63, '生抽', '1勺', 0),(63, '糖', '少许', 0),(63, '料酒', '1勺', 0),(63, '淀粉', '1勺', 0),(63, '油', '适量', 0),
|
||||
-- 64. 拔丝地瓜
|
||||
(64, '红薯', '2个', 1),(64, '白糖', '100克', 0),(64, '油', '适量', 0),
|
||||
-- 65. 酸菜白肉
|
||||
(65, '五花肉', '300克', 1),(65, '东北酸菜', '200克', 1),(65, '粉条', '50克', 0),(65, '姜', '3片', 0),(65, '八角', '1个', 0),(65, '盐', '适量', 0),
|
||||
-- 66. 溜肉段
|
||||
(66, '猪瘦肉', '250克', 1),(66, '青椒', '1个', 0),(66, '胡萝卜', '半根', 0),(66, '生抽', '2勺', 0),(66, '醋', '1勺', 0),(66, '糖', '1勺', 0),(66, '淀粉', '适量', 0),(66, '油', '适量', 0),
|
||||
-- 67. 鸡蛋灌饼
|
||||
(67, '面粉', '200克', 1),(67, '鸡蛋', '2个', 1),(67, '生菜', '2片', 0),(67, '甜面酱', '1勺', 0),(67, '油', '适量', 0),
|
||||
-- 68. 皮蛋瘦肉粥
|
||||
(68, '大米', '100克', 1),(68, '皮蛋', '1个', 0),(68, '瘦肉丝', '50克', 0),(68, '姜', '2片', 0),(68, '盐', '适量', 0),
|
||||
-- 69. 葱花鸡蛋饼
|
||||
(69, '面粉', '150克', 1),(69, '鸡蛋', '2个', 1),(69, '葱', '2根', 0),(69, '盐', '适量', 0),(69, '油', '适量', 0),
|
||||
-- 70. 小米南瓜粥
|
||||
(70, '小米', '100克', 1),(70, '南瓜', '200克', 1),(70, '水', '适量', 0),
|
||||
-- 71. 煎饺
|
||||
(71, '饺子皮', '20张', 1),(71, '猪肉馅', '200克', 1),(71, '白菜', '100克', 0),(71, '生抽', '1勺', 0),(71, '料酒', '1勺', 0),(71, '盐', '适量', 0),(71, '油', '适量', 0),
|
||||
-- 72. 三明治
|
||||
(72, '面包片', '2片', 1),(72, '鸡蛋', '1个', 0),(72, '火腿', '2片', 0),(72, '生菜', '1片', 0),(72, '沙拉酱', '适量', 0),
|
||||
-- 73. 豆浆油条
|
||||
(73, '黄豆', '150克', 1),(73, '面粉', '300克', 1),(73, '酵母', '3克', 0),(73, '泡打粉', '3克', 0),(73, '油', '适量', 0),
|
||||
-- 74. 馄饨
|
||||
(74, '馄饨皮', '30张', 1),(74, '猪肉馅', '200克', 1),(74, '虾仁', '50克', 0),(74, '紫菜', '少许', 0),(74, '虾皮', '少许', 0),(74, '葱', '1根', 0),(74, '生抽', '1勺', 0),(74, '盐', '适量', 0),
|
||||
-- 75. 肉夹馍
|
||||
(75, '面粉', '300克', 1),(75, '五花肉', '400克', 1),(75, '酵母', '3克', 0),(75, '生抽', '3勺', 0),(75, '老抽', '1勺', 0),(75, '八角', '2个', 0),(75, '桂皮', '1片', 0),(75, '姜', '3片', 0),
|
||||
-- 76. 糯米鸡
|
||||
(76, '糯米', '300克', 1),(76, '鸡腿肉', '200克', 1),(76, '香菇', '5朵', 0),(76, '腊肠', '半根', 0),(76, '生抽', '2勺', 0),(76, '蚝油', '1勺', 0),(76, '荷叶', '2张', 0),
|
||||
-- 77. 煎饼果子
|
||||
(77, '面粉', '100克', 1),(77, '绿豆面', '50克', 0),(77, '鸡蛋', '1个', 0),(77, '薄脆', '1片', 0),(77, '甜面酱', '1勺', 0),(77, '辣酱', '适量', 0),(77, '葱', '少许', 0),(77, '油', '少许', 0),
|
||||
-- 78. 豆沙包
|
||||
(78, '面粉', '300克', 1),(78, '红豆', '200克', 1),(78, '酵母', '3克', 0),(78, '白糖', '50克', 0),
|
||||
-- 79. 番茄蛋花汤
|
||||
(79, '西红柿', '2个', 1),(79, '鸡蛋', '2个', 1),(79, '盐', '适量', 0),(79, '葱', '少许', 0),(79, '香油', '几滴', 0),
|
||||
-- 80. 紫菜蛋花汤
|
||||
(80, '紫菜', '1片', 1),(80, '鸡蛋', '1个', 1),(80, '盐', '适量', 0),(80, '香油', '几滴', 0),(80, '虾皮', '少许', 0);
|
||||
23
backend/src/main/resources/db/data_ingredients_5.sql
Normal file
23
backend/src/main/resources/db/data_ingredients_5.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
-- 81-100 + 所有步骤
|
||||
USE chowbox;
|
||||
INSERT INTO recipe_ingredients (recipe_id, ingredient_name, amount, is_staple) VALUES
|
||||
(81, '排骨', '300克', 1),(81, '玉米', '1根', 1),(81, '姜', '3片', 0),(81, '盐', '适量', 0),
|
||||
(82, '牛肉末', '100克', 1),(82, '嫩豆腐', '半块', 0),(82, '鸡蛋清', '1个', 0),(82, '香菜', '少许', 0),(82, '盐', '适量', 0),(82, '白胡椒粉', '少许', 0),(82, '淀粉', '1勺', 0),
|
||||
(83, '豆腐', '半块', 0),(83, '木耳', '5朵', 0),(83, '鸡蛋', '1个', 0),(83, '醋', '2勺', 0),(83, '白胡椒粉', '1勺', 0),(83, '生抽', '1勺', 0),(83, '淀粉', '1勺', 0),(83, '盐', '适量', 0),(83, '香油', '少许', 0),
|
||||
(84, '鲫鱼', '1条', 1),(84, '嫩豆腐', '1块', 1),(84, '姜', '3片', 0),(84, '葱', '1根', 0),(84, '料酒', '1勺', 0),(84, '盐', '适量', 0),(84, '油', '适量', 0),
|
||||
(85, '排骨', '300克', 1),(85, '莲藕', '1节', 1),(85, '姜', '3片', 0),(85, '红枣', '3颗', 0),(85, '盐', '适量', 0),
|
||||
(86, '银耳', '1朵', 1),(86, '莲子', '30克', 0),(86, '红枣', '5颗', 0),(86, '冰糖', '适量', 0),
|
||||
(87, '黄瓜', '2根', 1),(87, '蒜', '3瓣', 0),(87, '醋', '1勺', 0),(87, '生抽', '1勺', 0),(87, '盐', '适量', 0),(87, '香油', '少许', 0),(87, '辣椒油', '适量', 0),
|
||||
(88, '内酯豆腐', '1盒', 1),(88, '皮蛋', '1个', 0),(88, '生抽', '2勺', 0),(88, '醋', '1勺', 0),(88, '盐', '少许', 0),(88, '香油', '少许', 0),(88, '葱', '1根', 0),
|
||||
(89, '木耳', '20朵', 1),(89, '蒜', '3瓣', 0),(89, '醋', '1勺', 0),(89, '生抽', '1勺', 0),(89, '盐', '适量', 0),(89, '辣椒油', '适量', 0),(89, '香油', '少许', 0),
|
||||
(90, '粉丝', '1把', 1),(90, '黄瓜', '1根', 0),(90, '胡萝卜', '半根', 0),(90, '生抽', '1勺', 0),(90, '醋', '1勺', 0),(90, '蒜', '2瓣', 0),(90, '盐', '适量', 0),(90, '辣椒油', '适量', 0),(90, '香油', '少许', 0),
|
||||
(91, '番茄', '2个', 1),(91, '白糖', '2勺', 0),
|
||||
(92, '鸡胸肉', '1块', 1),(92, '黄瓜', '半根', 0),(92, '生抽', '1勺', 0),(92, '醋', '1勺', 0),(92, '蒜', '2瓣', 0),(92, '辣椒油', '1勺', 0),(92, '芝麻酱', '半勺', 0),(92, '盐', '适量', 0),
|
||||
(93, '黄瓜', '2根', 1),(93, '蒜', '3瓣', 0),(93, '醋', '1勺', 0),(93, '生抽', '半勺', 0),(93, '盐', '适量', 0),(93, '辣椒油', '适量', 0),(93, '香油', '少许', 0),
|
||||
(94, '海带丝', '200克', 1),(94, '蒜', '2瓣', 0),(94, '醋', '1勺', 0),(94, '生抽', '1勺', 0),(94, '盐', '适量', 0),(94, '辣椒油', '适量', 0),(94, '香油', '少许', 0),
|
||||
(95, '米饭', '1碗', 1),(95, '鸡蛋', '2个', 1),(95, '葱', '1根', 0),(95, '盐', '适量', 0),(95, '油', '适量', 0),
|
||||
(96, '面条', '150克', 1),(96, '番茄', '1个', 1),(96, '鸡蛋', '1个', 1),(96, '盐', '适量', 0),(96, '葱', '少许', 0),(96, '油', '适量', 0),
|
||||
(97, '面条', '200克', 1),(97, '猪肉末', '100克', 1),(97, '甜面酱', '2勺', 0),(97, '黄豆酱', '1勺', 0),(97, '黄瓜', '1根', 0),(97, '葱', '1根', 0),(97, '油', '适量', 0),
|
||||
(98, '面条', '150克', 1),(98, '葱', '4根', 0),(98, '生抽', '2勺', 0),(98, '老抽', '半勺', 0),(98, '糖', '半勺', 0),(98, '油', '适量', 0),
|
||||
(99, '米粉', '200克', 1),(99, '鸡蛋', '1个', 0),(99, '豆芽', '50克', 0),(99, '火腿', '1根', 0),(99, '生抽', '1勺', 0),(99, '油', '适量', 0),
|
||||
(100, '面条', '100克', 1),(100, '酱油', '1勺', 0),(100, '猪油', '半勺', 0),(100, '葱', '1根', 0),(100, '盐', '适量', 0);
|
||||
124
backend/src/main/resources/db/data_steps.sql
Normal file
124
backend/src/main/resources/db/data_steps.sql
Normal file
@@ -0,0 +1,124 @@
|
||||
USE chowbox;
|
||||
-- ============================================================
|
||||
-- 100道菜谱步骤数据
|
||||
-- ============================================================
|
||||
INSERT INTO recipe_steps (recipe_id, step_order, content) VALUES
|
||||
-- 家常快手菜
|
||||
(1,1,'鸡蛋打散加少许盐搅匀;西红柿切块备用'),(1,2,'热锅倒油,倒入蛋液炒熟盛出'),(1,3,'锅中再倒少许油,下西红柿炒出汁,加少许糖'),(1,4,'倒回鸡蛋翻炒均匀,加盐调味,撒葱花出锅'),
|
||||
(2,1,'土豆削皮切细丝,放凉水中浸泡去淀粉'),(2,2,'热锅倒油,下花椒、干辣椒爆香'),(2,3,'大火下土豆丝快速翻炒,加盐、醋'),(2,4,'翻炒2分钟至断生,撒葱花出锅'),
|
||||
(3,1,'猪里脊切丝,加料酒、淀粉、生抽腌制10分钟'),(3,2,'青椒切丝,姜蒜切末'),(3,3,'热锅倒油,下肉丝滑炒至变色盛出'),(3,4,'锅中下姜蒜爆香,下青椒翻炒,倒回肉丝,加盐调味出锅'),
|
||||
(4,1,'西兰花掰小朵,焯水1分钟捞出'),(4,2,'蒜切末'),(4,3,'热锅倒油,下蒜末爆香'),(4,4,'下西兰花大火翻炒,加蚝油、盐调味出锅'),
|
||||
(5,1,'猪里脊切条,加盐、料酒、蛋液腌制'),(5,2,'裹上面粉,油温六成热下锅炸至金黄'),(5,3,'调糖醋汁:番茄酱+糖+白醋+水'),(5,4,'锅中留底油,倒入糖醋汁煮沸,下里脊翻炒裹汁出锅'),
|
||||
(6,1,'四季豆去筋掰段,焯水后沥干'),(6,2,'热锅多倒油,下四季豆煸至表面起皱盛出'),(6,3,'锅中留底油,下肉末、干辣椒、花椒、蒜炒香'),(6,4,'倒回四季豆,加生抽、盐翻炒均匀出锅'),
|
||||
(7,1,'鸡翅洗净划两刀,冷水下锅焯水去血沫'),(7,2,'热锅倒少许油,煎鸡翅至两面金黄'),(7,3,'倒入可乐没过鸡翅,加生抽、老抽、姜、料酒'),(7,4,'中火炖15分钟至汤汁收浓,加少许盐出锅'),
|
||||
(8,1,'韭黄洗净切段,鸡蛋打散加盐'),(8,2,'热锅倒油,倒入蛋液炒熟盛出'),(8,3,'锅中再倒油,大火下韭黄翻炒1分钟'),(8,4,'倒回鸡蛋炒匀,加盐出锅'),
|
||||
(9,1,'豆腐切厚片,两面煎至金黄盛出'),(9,2,'锅中下葱姜蒜、豆瓣酱炒出红油'),(9,3,'加入生抽、老抽、糖和半碗水,放入豆腐'),(9,4,'中小火炖5分钟入味,水淀粉勾芡,撒葱花出锅'),
|
||||
(10,1,'生菜洗净掰散,蒜切末'),(10,2,'锅中烧水,水开焯生菜10秒捞出摆盘'),(10,3,'小碗调汁:蚝油+生抽+少许水搅匀'),(10,4,'热锅少油爆香蒜末,倒入料汁煮开,淋在生菜上'),
|
||||
-- 11-20
|
||||
(11,1,'土豆削皮切厚片,焯水1分钟捞出沥干'),(11,2,'热锅多倒油,下土豆片煎至两面金黄'),(11,3,'撒入孜然粉、辣椒粉、盐翻炒均匀'),(11,4,'撒葱花出锅'),
|
||||
(12,1,'洋葱切丝,猪肉切片加料酒、生抽腌制'),(12,2,'热锅倒油,下肉片炒至变色盛出'),(12,3,'锅中下洋葱大火翻炒至半透明'),(12,4,'倒回肉片,加盐调味出锅'),
|
||||
(13,1,'油菜洗净对半切,香菇去蒂切片'),(13,2,'热锅倒油,下蒜末爆香'),(13,3,'下香菇炒软,再下油菜大火翻炒'),(13,4,'加蚝油、盐调味出锅'),
|
||||
(14,1,'豆腐切三角片,煎至两面金黄盛出'),(14,2,'木耳泡发洗净,青椒、胡萝卜切片'),(14,3,'锅中下豆瓣酱炒出红油,加生抽和糖'),(14,4,'放入所有食材翻炒,加少许水焖2分钟出锅'),
|
||||
(15,1,'雪里蕻洗净切段,猪肉切丝加料酒腌制'),(15,2,'热锅倒油,下肉丝煸炒至变色'),(15,3,'下干辣椒和雪里蕻大火翻炒'),(15,4,'加生抽、盐调味出锅'),
|
||||
(16,1,'豆芽洗净沥干,葱切段'),(16,2,'热锅倒油,大火下豆芽快炒'),(16,3,'加盐、少许醋提鲜'),(16,4,'翻炒30秒撒葱段出锅'),
|
||||
(17,1,'芹菜去叶切段焯水,豆干切条'),(17,2,'热锅倒油,下豆干煎至微黄'),(17,3,'下芹菜大火翻炒'),(17,4,'加生抽、盐调味出锅'),
|
||||
(18,1,'菜花掰小朵焯水,西红柿切块'),(18,2,'热锅倒油,下西红柿炒出汁'),(18,3,'加入番茄酱和少许糖'),(18,4,'下菜花翻炒均匀,加盐出锅'),
|
||||
(19,1,'茄子切条,撒盐腌制10分钟后挤去水分'),(19,2,'热锅多倒油,下茄子煎软盛出'),(19,3,'锅中下肉末炒香,加姜蒜豆瓣酱炒出红油'),(19,4,'加生抽老抽糖料酒,倒回茄子翻炒,焖2分钟出锅'),
|
||||
(20,1,'羊肉片加生抽料酒腌制,大葱切斜段'),(20,2,'热锅多倒油,大火下羊肉快速滑炒至变色盛出'),(20,3,'锅中留油下大葱炒香'),(20,4,'倒回羊肉,加孜然粉、盐翻炒均匀出锅');
|
||||
|
||||
INSERT INTO recipe_steps (recipe_id, step_order, content) VALUES
|
||||
-- 川菜 21-30
|
||||
(21,1,'豆腐切小块焯水'),(21,2,'锅中倒油,下肉末炒香,加豆瓣酱炒出红油'),(21,3,'加水和豆腐煮3分钟,加花椒粉、辣椒面'),(21,4,'水淀粉勾芡,撒葱花出锅'),
|
||||
(22,1,'五花肉整块冷水下锅,加姜、料酒煮20分钟至断生'),(22,2,'捞出切薄片,蒜苗切段'),(22,3,'热锅少油,下肉片煸至卷曲出油'),(22,4,'加豆瓣酱、生抽、糖,下蒜苗翻炒出锅'),
|
||||
(23,1,'鸡胸肉切丁,加料酒、淀粉、盐腌制'),(23,2,'黄瓜切丁,调酱汁:生抽+醋+糖+淀粉+水'),(23,3,'小火炒香花生米盛出'),(23,4,'锅中爆香干辣椒花椒蒜,下鸡丁炒变色,加黄瓜丁和酱汁翻炒,最后加花生米出锅'),
|
||||
(24,1,'猪里脊切薄片,加料酒、淀粉、盐腌制'),(24,2,'豆芽和白菜叶焯水铺碗底'),(24,3,'锅中炒香豆瓣酱,加水煮开,下肉片滑熟'),(24,4,'连汤倒入碗中,撒上蒜末、干辣椒、花椒,浇热油激发香味'),
|
||||
(25,1,'猪里脊切丝,加料酒、淀粉腌制'),(25,2,'木耳泡发切丝,胡萝卜、青椒切丝'),(25,3,'调鱼香汁:生抽+醋+糖+淀粉+水'),(25,4,'锅中爆香泡椒蒜末豆瓣酱,下肉丝炒至变色,加三丝翻炒,淋入鱼香汁收汁出锅'),
|
||||
(26,1,'鸡腿肉切小丁,加生抽、料酒腌制15分钟'),(26,2,'干辣椒剪段,姜蒜切片'),(26,3,'热锅多油,下鸡丁炸至金黄捞出'),(26,4,'锅中留底油,下干辣椒花椒姜蒜爆香,倒回鸡丁翻炒,撒芝麻出锅'),
|
||||
(27,1,'牛腱子和牛肚分别煮熟,切薄片'),(27,2,'调拌汁:红油+花椒粉+生抽+醋搅匀'),(27,3,'将牛肉片、牛肚片摆盘'),(27,4,'淋上拌汁,撒花生碎和香菜'),
|
||||
(28,1,'鸡腿冷水下锅,加姜、料酒煮15分钟至熟'),(28,2,'捞出浸冰水5分钟使皮脆肉嫩,斩块摆盘'),(28,3,'调酱汁:芝麻酱+红油+生抽+醋+花椒粉+蒜末+糖搅匀'),(28,4,'酱汁淋在鸡块上,撒花生碎、葱花和香菜'),
|
||||
(29,1,'牛肉切丝,加生抽料酒腌制'),(29,2,'芹菜切段,干辣椒剪段'),(29,3,'热锅多油,下牛肉丝煸至水分蒸发'),(29,4,'加豆瓣酱、干辣椒、花椒、芹菜翻炒,生抽调味出锅'),
|
||||
(30,1,'五花肉整块冷水下锅煮20分钟至熟'),(30,2,'捞出放凉切薄片,黄瓜切丝铺底'),(30,3,'将肉片整齐码放在黄瓜上'),(30,4,'调蒜泥汁:蒜末+生抽+红油+醋+糖,淋在肉片上'),
|
||||
-- 粤菜 31-40
|
||||
(31,1,'草鱼片加料酒、淀粉、盐腌制'),(31,2,'酸菜切段焯水,泡椒切段'),(31,3,'锅中炒香姜蒜泡椒,下酸菜翻炒,加水煮开'),(31,4,'滑入鱼片煮至变白,连汤倒入碗中,撒干辣椒花椒浇热油'),
|
||||
(32,1,'鸭血切块,午餐肉切片,毛肚洗净'),(32,2,'豆芽焯水铺碗底'),(32,3,'锅中炒香豆瓣酱,加水煮开,下鸭血午餐肉毛肚煮熟'),(32,4,'倒入碗中,撒蒜末干辣椒花椒,浇热油'),
|
||||
(33,1,'虾去虾线,土豆藕切片,西兰花掰小朵'),(33,2,'所有蔬菜焯水断生'),(33,3,'锅中少油炒化火锅底料,加干辣椒花椒蒜爆香'),(33,4,'下所有食材翻炒均匀出锅'),
|
||||
(34,1,'鸡爪剪去指甲,冷水下锅加姜料酒焯水'),(34,2,'捞出洗净,放入泡椒和泡椒水、白醋、盐'),(34,3,'加入凉白开没过鸡爪'),(34,4,'密封冷藏腌制4小时以上即可'),
|
||||
(35,1,'面条煮熟捞出,过凉水'),(35,2,'肉末下锅炒香,加芽菜炒匀'),(35,3,'调碗底:芝麻酱+红油+生抽+醋+花生碎'),(35,4,'面条放入碗中,浇上肉末芽菜和调料,撒葱花拌匀'),
|
||||
(36,1,'三黄鸡处理干净,锅中水加姜葱料酒烧开'),(36,2,'提着鸡腿浸入沸水中三提三放,使鸡皮定型'),(36,3,'小火煮15分钟,关火焖10分钟'),(36,4,'捞出浸冰水,斩件摆盘,蘸料:生抽+花生油+蒜末+姜末'),
|
||||
(37,1,'鲈鱼处理干净,两面划斜刀,抹盐料酒腌制'),(37,2,'盘中铺姜片,放上鲈鱼,上面再放姜片葱段'),(37,3,'水开后上锅大火蒸8分钟'),(37,4,'倒掉盘中蒸出的水,撒葱丝,淋蒸鱼豉油,浇热油'),
|
||||
(38,1,'基围虾洗净去虾线'),(38,2,'锅中烧水,加姜片料酒煮开'),(38,3,'下虾煮至变色弯曲(约2分钟)立即捞出'),(38,4,'摆盘,蘸料:生抽+姜末+香油'),
|
||||
(39,1,'猪梅花肉切长条,用叉烧酱、生抽、老抽腌制4小时'),(39,2,'烤箱预热200度,烤盘垫锡纸'),(39,3,'烤20分钟后取出刷蜂蜜,翻面再烤15分钟'),(39,4,'取出放凉切片'),
|
||||
(40,1,'猪里脊切块,加盐、料酒、淀粉腌制'),(40,2,'菠萝切块盐水浸泡,青红椒切片'),(40,3,'肉块裹干淀粉炸至金黄,糖醋汁:番茄酱+糖+白醋+水'),(40,4,'锅中炒香糖醋汁,下肉块菠萝青红椒翻炒裹汁出锅');
|
||||
|
||||
INSERT INTO recipe_steps (recipe_id, step_order, content) VALUES
|
||||
-- 粤菜 41-50
|
||||
(41,1,'牛肉切片加生抽、料酒、淀粉腌制'),(41,2,'河粉抖散,热锅多油滑锅'),(41,3,'大火下牛肉炒至变色盛出'),(41,4,'同锅下河粉翻炒,加生抽老抽,加豆芽韭黄牛肉炒匀出锅'),
|
||||
(42,1,'粉丝温水泡软,扇贝去泥沙洗净'),(42,2,'蒜切末,一半炸至金黄做成金银蒜'),(42,3,'扇贝壳上铺粉丝和扇贝,淋金银蒜和蒸鱼豉油'),(42,4,'上锅蒸6分钟,出锅撒葱花淋热油'),
|
||||
(43,1,'排骨斩小块,加生抽、料酒、豆豉、蒜末、淀粉、糖腌制20分钟'),(43,2,'盘中平铺排骨'),(43,3,'水开后上锅大火蒸15-20分钟'),(43,4,'出锅即可'),
|
||||
(44,1,'大米提前浸泡30分钟,腊肠腊肉切薄片'),(44,2,'砂锅刷油,放入大米和水(1:1.2),大火煮开转小火'),(44,3,'米饭快熟时铺上腊肠腊肉,盖上焖5分钟'),(44,4,'沿锅边淋少许油形成锅巴,焯好的油菜摆旁边,淋生抽即可'),
|
||||
(45,1,'排骨焯水去血沫,冬瓜去皮切块'),(45,2,'锅中放排骨、姜片,加足量水'),(45,3,'大火煮开转小火炖40分钟'),(45,4,'加入冬瓜再炖10分钟,加盐出锅'),
|
||||
(46,1,'牛肉切片加生抽、料酒、淀粉腌制'),(46,2,'青椒切块、洋葱切片'),(46,3,'热锅多油滑炒牛肉至变色盛出'),(46,4,'锅中炒香洋葱青椒,倒回牛肉,加蚝油翻炒出锅'),
|
||||
(47,1,'虾仁切粒,加猪肥膘碎、笋丁、盐搅匀做馅'),(47,2,'澄面加生粉,用滚水烫熟揉成面团'),(47,3,'面团分小剂子擀成薄皮,包入虾馅捏成饺子'),(47,4,'上锅蒸6分钟即可'),
|
||||
(48,1,'粘米粉加水调成米浆'),(48,2,'平底蒸盘刷油,倒入一勺米浆晃匀'),(48,3,'打上鸡蛋、撒肉末和生菜'),(48,4,'上锅大火蒸3分钟,刮起卷成肠粉,淋生抽即可'),
|
||||
(49,1,'面粉加酵母、糖、水揉成面团发酵1小时'),(49,2,'叉烧肉切小丁,加蚝油调味'),(49,3,'面团分小剂子擀皮,包入叉烧馅'),(49,4,'上锅蒸12分钟,关火焖2分钟即可'),
|
||||
(50,1,'蛋挞皮提前解冻'),(50,2,'鸡蛋打散,加淡奶油、牛奶、糖搅拌均匀过滤'),(50,3,'蛋液倒入蛋挞皮中,八分满'),(50,4,'烤箱200度烤20分钟至表面焦黄'),
|
||||
-- 湘菜 51-58
|
||||
(51,1,'鱼头处理干净,抹盐和料酒腌制'),(51,2,'盘中垫姜片,放上鱼头'),(51,3,'铺满剁椒,水开后大火蒸12分钟'),(51,4,'倒掉盘中水,撒葱花,淋蒸鱼豉油,浇热油'),
|
||||
(52,1,'五花肉切薄片,青椒切块'),(52,2,'热锅少油,下五花肉煸至卷曲出油'),(52,3,'加蒜片、豆豉炒香'),(52,4,'下青椒大火快炒,加生抽老抽调色出锅'),
|
||||
(53,1,'酸豆角切小段,猪肉切末'),(53,2,'热锅倒油,下肉末炒香'),(53,3,'加干辣椒、蒜末爆香'),(53,4,'下酸豆角大火翻炒,加生抽调味出锅'),
|
||||
(54,1,'腊肉腊肠腊鱼用温水洗净切片'),(54,2,'盘中依次码放腊味'),(54,3,'撒上豆豉和干辣椒,淋少许料酒'),(54,4,'水开后上锅大火蒸20分钟即可'),
|
||||
(55,1,'花菜掰小朵焯水半分钟'),(55,2,'五花肉切薄片'),(55,3,'热锅少油下五花肉煸出油,加蒜干辣椒豆瓣酱炒香'),(55,4,'下花菜大火翻炒,加生抽调味出锅'),
|
||||
(56,1,'小龙虾刷洗干净去虾线'),(56,2,'热锅多油,下小龙虾炸至变红捞出'),(56,3,'锅中留油,下姜蒜豆瓣酱干辣椒花椒炒香'),(56,4,'倒回小龙虾,加啤酒焖煮8分钟,大火收汁撒紫苏出锅'),
|
||||
(57,1,'青椒洗净去蒂,直接在干锅上烙至表皮焦黑'),(57,2,'趁热剥去焦皮,撕成条'),(57,3,'皮蛋切瓣摆盘'),(57,4,'将辣椒条放入,加蒜末、生抽、醋、香油,用擂钵擂拌均匀'),
|
||||
(58,1,'酸菜切碎焯水挤干'),(58,2,'热锅倒油下肉末炒香'),(58,3,'加干辣椒蒜末爆香'),(58,4,'下酸菜大火翻炒,加生抽调味出锅'),
|
||||
-- 东北菜 59-66
|
||||
(59,1,'猪里脊切厚片,加盐、料酒腌制'),(59,2,'淀粉+水调成糊,肉片裹糊'),(59,3,'油温六成热逐片下锅炸至定型捞出,油温升高复炸至金黄酥脆'),(59,4,'另起锅熬糖醋汁:白醋+糖煮至粘稠,加胡萝卜丝葱丝,下肉片快速翻炒裹汁出锅'),
|
||||
(60,1,'茄子土豆青椒切滚刀块'),(60,2,'锅中多油,分别炸土豆和茄子至金黄'),(60,3,'调碗汁:生抽+老抽+糖+盐+水+淀粉'),(60,4,'锅中留油爆香蒜末,下所有食材翻炒,倒入碗汁收汁出锅');
|
||||
|
||||
INSERT INTO recipe_steps (recipe_id, step_order, content) VALUES
|
||||
-- 东北菜继续 + 早餐
|
||||
(61,1,'五花肉切块焯水,粉条泡软'),(61,2,'锅中少许油下五花肉煸至微黄'),(61,3,'加生抽、老抽、八角、姜片,加水没过肉'),(61,4,'炖30分钟后加粉条和白菜,再炖10分钟收汁出锅'),
|
||||
(62,1,'排骨焯水,土豆茄子切块,豆角掰段,玉米切段'),(62,2,'锅中炒香排骨,加生抽老抽上色'),(62,3,'加水没过排骨,先炖20分钟'),(62,4,'加入所有蔬菜再炖15分钟,加盐调味出锅'),
|
||||
(63,1,'猪里脊切丝,加料酒、淀粉、蛋清腌制'),(63,2,'大葱白切丝摆盘,甜面酱+生抽+糖+水调酱汁'),(63,3,'锅中滑炒肉丝至变色盛出'),(63,4,'锅中炒香酱汁,倒回肉丝翻炒裹酱,盛入葱丝盘中,配豆腐皮卷食'),
|
||||
(64,1,'红薯去皮切滚刀块'),(64,2,'油温六成热,下红薯块炸至金黄熟透捞出'),(64,3,'另起锅加白糖和少量水,小火熬至焦黄色'),(64,4,'快速下入炸好的红薯翻滚裹糖,趁热装盘(配一碗凉水拔丝)'),
|
||||
(65,1,'五花肉冷水下锅煮20分钟,捞出切薄片'),(65,2,'酸菜切丝洗两遍挤干'),(65,3,'锅中放肉片和姜片八角,加水炖20分钟'),(65,4,'加入酸菜和粉条再炖10分钟,加盐出锅'),
|
||||
(66,1,'猪瘦肉切块,加盐料酒腌制,裹淀粉糊'),(66,2,'油温六成热下锅炸至金黄捞出'),(66,3,'调酱汁:生抽+醋+糖+水+淀粉'),(66,4,'锅中留底油,下青椒胡萝卜翻炒,倒回肉段,淋酱汁翻炒均匀出锅'),
|
||||
-- 67-72
|
||||
(67,1,'面粉加水揉成软面团醒30分钟'),(67,2,'擀成薄饼,刷油叠层'),(67,3,'平底锅少油,放饼,待鼓起时戳洞灌入蛋液'),(67,4,'两面煎至金黄,刷甜面酱包入生菜即可'),
|
||||
(68,1,'大米提前浸泡30分钟'),(68,2,'皮蛋切丁,瘦肉切丝加姜腌制'),(68,3,'锅中水开下大米,中小火煮25分钟至粥粘稠'),(68,4,'加入皮蛋和肉丝再煮5分钟,加盐调味出锅'),
|
||||
(69,1,'面粉加水调成面糊,打入鸡蛋搅匀'),(69,2,'加入葱花、盐搅拌均匀'),(69,3,'平底锅刷油,倒入一勺面糊摊平'),(69,4,'小火两面煎至金黄即可'),
|
||||
(70,1,'小米洗净,南瓜去皮切小块'),(70,2,'锅中加水和小米,大火煮开转小火'),(70,3,'煮15分钟后加入南瓜'),(70,4,'再煮10分钟至南瓜软烂小米粘稠即可'),
|
||||
(71,1,'猪肉馅加白菜碎、生抽、料酒、盐拌匀'),(71,2,'饺子皮包入馅料'),(71,3,'平底锅倒油摆入饺子,小火煎到底部金黄'),(71,4,'加半碗水盖盖焖5分钟至水收干即可'),
|
||||
(72,1,'鸡蛋煎熟,火腿片略煎'),(72,2,'面包片烤至微黄'),(72,3,'面包片上放生菜、煎蛋、火腿片'),(72,4,'挤上沙拉酱,盖上另一片面包对切即可'),
|
||||
-- 73-78
|
||||
(73,1,'黄豆提前泡8小时,打成豆浆过滤煮沸'),(73,2,'面粉+酵母+泡打粉+水揉成面团醒2小时'),(73,3,'面团擀长条切段,两段叠起用筷子压中线'),(73,4,'油温180度下锅炸至金黄膨胀捞出'),
|
||||
(74,1,'猪肉馅加生抽、盐、料酒、葱花拌匀'),(74,2,'馄饨皮包入馅料捏紧'),(74,3,'锅中水开下馄饨,煮至浮起再煮1分钟'),(74,4,'碗中放紫菜、虾皮、盐、葱花和开水做汤底,盛入馄饨'),
|
||||
(75,1,'面粉+酵母+水揉成面团醒发1小时'),(75,2,'五花肉加生抽老抽八角桂皮姜炖40分钟至软烂切碎'),(75,3,'面团分小剂子擀成饼,平底锅烙至两面金黄'),(75,4,'饼从中间划开夹入肉碎,浇一勺肉汤'),
|
||||
(76,1,'糯米提前泡4小时,荷叶泡软'),(76,2,'鸡腿肉切块加生抽蚝油腌制'),(76,3,'香菇腊肠切片,与鸡肉和糯米拌匀'),(76,4,'荷叶包入糯米鸡,上锅蒸40分钟'),
|
||||
(77,1,'面粉和绿豆面加水调成稀面糊'),(77,2,'平底铛刷油,倒一勺面糊摊成薄圆'),(77,3,'打上鸡蛋摊开,翻面'),(77,4,'刷甜面酱和辣酱,放薄脆,撒葱花卷起'),
|
||||
(78,1,'红豆煮烂加糖压成豆沙'),(78,2,'面粉加酵母水揉成面团醒发1小时'),(78,3,'面团分小剂子擀皮包入豆沙'),(78,4,'上锅蒸15分钟关火焖3分钟');
|
||||
|
||||
INSERT INTO recipe_steps (recipe_id, step_order, content) VALUES
|
||||
-- 汤羹 79-86 + 凉菜 87-94 + 主食 95-100
|
||||
(79,1,'西红柿切块,鸡蛋打散'),(79,2,'锅中倒油下西红柿炒出汁'),(79,3,'加水煮开'),(79,4,'淋入蛋液搅散,加盐调味,撒葱花淋香油'),
|
||||
(80,1,'紫菜撕碎放碗中'),(80,2,'锅中水烧开'),(80,3,'淋入蛋液形成蛋花'),(80,4,'倒入碗中,加盐、虾皮、香油'),
|
||||
(81,1,'排骨焯水备用'),(81,2,'锅中放入排骨、姜片加足水'),(81,3,'大火煮开转小火炖40分钟'),(81,4,'加入玉米段再炖15分钟,加盐出锅'),
|
||||
(82,1,'牛肉末加蛋清淀粉拌匀'),(82,2,'锅中加水烧开,下豆腐丁'),(82,3,'滑入牛肉末搅散煮2分钟'),(82,4,'加盐白胡椒调味,淀粉勾薄芡,撒香菜出锅'),
|
||||
(83,1,'木耳泡发切丝,豆腐切丝'),(83,2,'锅中水开,下豆腐丝木耳丝煮2分钟'),(83,3,'加生抽、盐、白胡椒粉'),(83,4,'淋入蛋液和淀粉水,加醋和香油出锅'),
|
||||
(84,1,'鲫鱼处理干净,两面划刀'),(84,2,'热锅倒油煎鱼至两面金黄'),(84,3,'加开水没过鱼,加姜葱料酒'),(84,4,'大火煮10分钟汤变奶白,加豆腐块再煮5分钟,加盐调味'),
|
||||
(85,1,'排骨焯水,莲藕去皮切厚块'),(85,2,'排骨、莲藕、姜片、红枣一起入砂锅'),(85,3,'加水大火煮开转小火炖1小时'),(85,4,'加盐调味出锅'),
|
||||
(86,1,'银耳提前泡发撕小朵,莲子泡发去芯'),(86,2,'银耳、莲子、红枣入锅加水'),(86,3,'大火煮开转小火炖30分钟'),(86,4,'加冰糖再炖10分钟至银耳出胶'),
|
||||
(87,1,'黄瓜洗净拍碎切段'),(87,2,'蒜切末'),(87,3,'调拌汁:蒜末+生抽+醋+盐+辣椒油+香油'),(87,4,'将拌汁倒入黄瓜中拌匀即可'),
|
||||
(88,1,'内酯豆腐倒扣入盘中'),(88,2,'皮蛋切丁放在豆腐上'),(88,3,'葱切末撒上'),(88,4,'淋生抽、醋、香油、盐调成的汁'),
|
||||
(89,1,'木耳泡发后焯水1分钟'),(89,2,'捞出过凉水沥干'),(89,3,'大蒜切末'),(89,4,'加蒜末、生抽、醋、盐、辣椒油、香油拌匀'),
|
||||
(90,1,'粉丝泡软焯水,黄瓜胡萝卜切丝'),(90,2,'粉丝沥干和蔬菜丝混合'),(90,3,'蒜切末'),(90,4,'加蒜末、生抽、醋、盐、辣椒油、香油拌匀'),
|
||||
(91,1,'番茄洗净切片'),(91,2,'番茄片摆盘'),(91,3,'均匀撒上白糖'),(91,4,'冷藏10分钟更佳'),
|
||||
(92,1,'鸡胸肉冷水下锅煮熟,撕成细丝'),(92,2,'黄瓜切丝'),(92,3,'调酱汁:芝麻酱+生抽+醋+蒜末+辣椒油+盐'),(92,4,'鸡丝黄瓜放入碗中,淋酱汁拌匀'),
|
||||
(93,1,'黄瓜洗净用刀面拍裂切段'),(93,2,'蒜切末'),(93,3,'加蒜末、生抽、醋、盐、辣椒油、香油'),(93,4,'拌匀即可'),
|
||||
(94,1,'海带丝洗净焯水2分钟'),(94,2,'捞出过凉水沥干'),(94,3,'蒜切末'),(94,4,'加蒜末、生抽、醋、盐、辣椒油、香油拌匀'),
|
||||
(95,1,'鸡蛋打散加少许盐,葱切粒'),(95,2,'热锅多油,倒入蛋液快速搅散'),(95,3,'加入米饭大火翻炒压散'),(95,4,'加盐调味,撒大量葱花翻炒出锅'),
|
||||
(96,1,'番茄切块,鸡蛋打散'),(96,2,'锅中少油炒香番茄出汁,加水煮开'),(96,3,'下入面条煮至八分熟'),(96,4,'淋入蛋液成蛋花,加盐调味,撒葱花出锅'),
|
||||
(97,1,'肉末炒香,加甜面酱和黄豆酱小火炸出酱香'),(97,2,'面条煮熟捞出过凉水'),(97,3,'黄瓜切丝'),(97,4,'面条盛碗,码上肉酱和黄瓜丝,撒葱花拌匀'),
|
||||
(98,1,'葱切段,锅中多油小火炸葱至焦黄出香'),(98,2,'葱油过滤备用'),(98,3,'调碗底:生抽+老抽+糖混合'),(98,4,'面条煮熟捞出,浇葱油和酱油汁拌匀'),
|
||||
(99,1,'米粉提前泡软'),(99,2,'鸡蛋炒散,豆芽焯水,火腿切片'),(99,3,'热锅多油下米粉大火快炒'),(99,4,'加入鸡蛋豆芽火腿,生抽调味翻炒出锅'),
|
||||
(100,1,'面条放入碗中'),(100,2,'加酱油、猪油、盐、葱花'),(100,3,'开水冲入碗中搅匀'),(100,4,'煮好的面条捞入碗中即可');
|
||||
59
backend/src/main/resources/db/schema.sql
Normal file
59
backend/src/main/resources/db/schema.sql
Normal file
@@ -0,0 +1,59 @@
|
||||
-- ChowBox 数据库表结构
|
||||
-- MySQL 5.7+
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS chowbox DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE chowbox;
|
||||
|
||||
-- 菜谱表
|
||||
CREATE TABLE IF NOT EXISTS recipes (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL COMMENT '菜名',
|
||||
difficulty TINYINT NOT NULL DEFAULT 2 COMMENT '难度 1-5 星',
|
||||
cook_time INT NOT NULL COMMENT '烹饪时长(分钟)',
|
||||
image_url VARCHAR(255) DEFAULT '' COMMENT '成品图URL',
|
||||
category VARCHAR(50) NOT NULL COMMENT '菜系分类(川菜/粤菜/家常菜等)',
|
||||
season VARCHAR(20) DEFAULT 'all' COMMENT '适宜季节(spring/summer/autumn/winter/all)',
|
||||
meal_time VARCHAR(20) DEFAULT 'all' COMMENT '餐时(breakfast/lunch/dinner/all)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜谱';
|
||||
|
||||
-- 菜谱食材表
|
||||
CREATE TABLE IF NOT EXISTS recipe_ingredients (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
recipe_id BIGINT NOT NULL COMMENT '菜谱ID',
|
||||
ingredient_name VARCHAR(50) NOT NULL COMMENT '食材名称',
|
||||
amount VARCHAR(50) DEFAULT '' COMMENT '用量(如2个/200克)',
|
||||
is_staple TINYINT DEFAULT 1 COMMENT '是否主料(1主料/0辅料)',
|
||||
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜谱食材';
|
||||
|
||||
-- 菜谱步骤表
|
||||
CREATE TABLE IF NOT EXISTS recipe_steps (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
recipe_id BIGINT NOT NULL COMMENT '菜谱ID',
|
||||
step_order INT NOT NULL COMMENT '步骤序号',
|
||||
content VARCHAR(500) NOT NULL COMMENT '步骤描述',
|
||||
image_url VARCHAR(255) DEFAULT '' COMMENT '步骤图URL',
|
||||
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜谱步骤';
|
||||
|
||||
-- 用户偏好表
|
||||
CREATE TABLE IF NOT EXISTS user_preferences (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
openid VARCHAR(64) NOT NULL UNIQUE COMMENT '微信openid',
|
||||
taste VARCHAR(50) DEFAULT '' COMMENT '口味偏好(辣/清淡/酸甜等)',
|
||||
allergies VARCHAR(200) DEFAULT '' COMMENT '过敏/忌口(逗号分隔)',
|
||||
price_range VARCHAR(20) DEFAULT 'all' COMMENT '外卖价格区间(low/medium/high/all)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户偏好';
|
||||
|
||||
-- 开盒记录表
|
||||
CREATE TABLE IF NOT EXISTS box_history (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
openid VARCHAR(64) NOT NULL COMMENT '微信openid',
|
||||
box_type VARCHAR(20) NOT NULL COMMENT '盲盒类型(takeout/fridge/explore)',
|
||||
result_data JSON COMMENT '结果数据',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_openid_time (openid, created_at DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='开盒记录';
|
||||
83
doc/box.md
Normal file
83
doc/box.md
Normal file
@@ -0,0 +1,83 @@
|
||||
以下是“吃啥盲盒”开盒动效的详细分镜描述,覆盖从点击到展示结果的完整过程,可直接交付UI动效设计师和前端开发。
|
||||
|
||||
---
|
||||
|
||||
## 开盒动效分镜 · 吃啥盲盒
|
||||
|
||||
**整体时长**:约1.8-2.2秒
|
||||
**触发动作**:用户点击任一模式入口按钮(外卖盒/冰箱盒/附近盒)
|
||||
**核心情感曲线**:期待→悬念→惊喜→清晰
|
||||
**实现方式建议**:优先使用Lottie + CSS关键帧动画,兼容低端机型可降级为CSS动画序列
|
||||
|
||||
---
|
||||
|
||||
### 第一幕 · 震动蓄力(0ms – 400ms)
|
||||
**目标**:传达“盒子里有东西在动”,制造期待。
|
||||
|
||||
| 时间 | 元素 | 变化描述 | 缓动 |
|
||||
|------|------|----------|------|
|
||||
| 0ms | 点击的按钮/卡片 | 轻微缩放:scale 1.0 → 1.05 → 1.0 | ease-out |
|
||||
| 0ms | 整个盲盒入口 | 出现一圈橙色光晕从中心向外扩散后消失 | ease-out, opacity 1→0 |
|
||||
| 100ms | 盲盒图标(如🎁) | 开始高频微动:左右摇摆 ±3deg,频率先快后慢 | ease-in-out |
|
||||
| 0-400ms | 屏幕背景(盲盒大厅) | 逐渐变暗至亮度0.6,其余元素后退感 | 线性 |
|
||||
|
||||
**配套反馈**:手机轻微震动(调用`wx.vibrateShort` 15ms)
|
||||
|
||||
---
|
||||
|
||||
### 第二幕 · 盒盖开启(400ms – 1000ms)
|
||||
**目标**:视觉上呈现“盒子被打开”的结构变化。
|
||||
|
||||
| 时间 | 元素 | 变化描述 | 缓动 |
|
||||
|------|------|----------|------|
|
||||
| 400ms | 盲盒容器(方形盒子) | 盒盖部分分离,向上位移动画,Y轴偏移 -60rpx,同时opacity 1→0 | ease-in |
|
||||
| 450ms | 盒体内部 | 从缝隙中透出金色光芒,逐渐增强 | ease-out |
|
||||
| 500ms | 盒体本身 | 向下轻微压缩(scaleY 1→0.9→1),模拟受力反弹 | ease-out |
|
||||
| 600ms | 金色光芒 | 蔓延至整个屏幕,形成短暂的白屏过渡(白色遮罩从中心放射,opacity 0→0.8→0) | ease-in-out,持续400ms |
|
||||
| 700ms | 结果承载层(卡片/空状态) | 在光芒最亮时完成挂载,但此时仍被白色遮罩覆盖 | - |
|
||||
|
||||
**配套反馈**:第二次短震(10ms),强化“打开了”的触感。
|
||||
|
||||
---
|
||||
|
||||
### 第三幕 · 展示与定型(1000ms – 1800ms)
|
||||
**目标**:结果内容华丽登场,信息清晰呈现。
|
||||
|
||||
| 时间 | 元素 | 变化描述 | 缓动 |
|
||||
|------|------|----------|------|
|
||||
| 1000ms | 白色遮罩 | 开始淡出(opacity 0.8→0),持续300ms | ease-in |
|
||||
| 1000ms | 结果卡片(外卖/菜谱/店铺) | 从缩放0.85、Y轴+30rpx开始弹出 | ease-out-back(弹性动画) |
|
||||
| 1100ms | 卡片内商家图片 | 单独从模糊(blur 8px)变清晰(0px),持续200ms | ease-out |
|
||||
| 1200ms | 标题文字(“你抽到的外卖是…”) | 逐字或整行从上方飘入,opacity 0→1,Y -20→0 | ease-out |
|
||||
| 1300ms | 评分/距离等次要信息 | 阶梯式出现,每组延迟50ms | ease-out |
|
||||
| 1500ms | 底部操作按钮 | 从下方滑入,Y +40→0,opacity 0→1 | ease-out |
|
||||
| 1600ms | 彩带动效(可选) | 短暂彩色纸屑从卡片背后喷射,3-5片 | 物理抛物线,随机旋转 |
|
||||
|
||||
**配套反馈**:手机长震一次(`wx.vibrateLong`)或播放轻快“叮”音效(需音频资源)
|
||||
|
||||
---
|
||||
|
||||
### 各模式动效差异点
|
||||
|
||||
| 模式 | 第二幕盒内光芒颜色 | 第二幕溅射粒子 | 第四幕彩带 |
|
||||
|------|-------------------|---------------|-----------|
|
||||
| 外卖盲盒 | 橙色→暖黄 | 迷你外卖车图标飘出 | 外卖袋emoji纸屑 |
|
||||
| 冰箱盲盒 | 绿色→浅绿 | 蔬菜粒子(🥬🍅) | 餐具emoji纸屑 |
|
||||
| 探店盲盒 | 紫色→粉金 | 地图标记气泡冒起 | 星星emoji纸屑 |
|
||||
|
||||
---
|
||||
|
||||
### 降级策略(性能受限时)
|
||||
- 移除粒子与彩带,仅保留盒盖位移+光芒过渡+卡片弹性动画。
|
||||
- 所有动画时长缩短至1.2秒,弹性缓动改为普通ease-out。
|
||||
- 关闭震动序列(通过API判断手机性能或用户设置)。
|
||||
|
||||
---
|
||||
|
||||
### 开发关键参数
|
||||
- **Lottie JSON 尺寸**:控制在80KB以内,帧率30fps。
|
||||
- **CSS 动画**:使用`transform`和`opacity`,开启GPU加速(`will-change`)。
|
||||
- **遮罩过渡**:使用`page-mask`绝对定位覆盖,z-index分层。
|
||||
- **依赖资源**:音频文件(.mp3, 5KB以内),震动序列,图片预加载(结果卡片图提前加载)。
|
||||
|
||||
---
|
||||
169
doc/plan.md
Normal file
169
doc/plan.md
Normal file
@@ -0,0 +1,169 @@
|
||||
以下为“吃啥盲盒(ChowBox)”小程序的完整开发方案,整合了此前所有讨论并细化至可落地执行的程度。
|
||||
|
||||
---
|
||||
|
||||
# 吃啥盲盒(ChowBox)小程序 · 产品开发方案 v1.0
|
||||
|
||||
## 一、产品概况
|
||||
|
||||
**产品名称**:吃啥盲盒
|
||||
**英文名**:ChowBox
|
||||
**Slogan**:今天吃啥?开个盲盒。
|
||||
**产品定位**:一站式解决“今天吃什么”的轻决策工具,融合外卖盲盒、冰箱食材做菜、附近探店三种场景,用随机惊喜打破选择困难。
|
||||
**目标用户**:都市白领、学生、独居人群、年轻家庭,经常面临吃饭决策困难的群体。
|
||||
**平台形态**:微信小程序(便于传播及跳转外部平台)
|
||||
|
||||
---
|
||||
|
||||
## 二、核心功能模块
|
||||
|
||||
### 模式一:外卖盲盒(Takeout Box)
|
||||
- **一句话描述**:自动获取附近高质量外卖商家,随机推荐一家,一键跳转下单。
|
||||
- **用户流程**
|
||||
1. 授权地理位置。
|
||||
2. 系统以定位为中心,搜索1.5–3公里内外卖商家。
|
||||
3. 按评分≥4.5、月销量商圈前30%、配送≤40分钟、无严重差评等规则过滤高质量池。
|
||||
4. 加权随机选出1家展示:店名、评分、人均、距离、起送价/配送费、热销Top3、好评亮点。
|
||||
5. 用户点击“去下单”跳转美团/饿了么小程序对应商家页。
|
||||
6. 可点“换一家”重新随机(保证不重复上一次结果)。
|
||||
- **交互细节**:开盒动效(震动→发光→展开卡片),上方提示“你抽到的外卖是…”
|
||||
|
||||
### 模式二:冰箱盲盒(Fridge Box)
|
||||
- **一句话描述**:录入现有食材,智能推荐能做的菜并给出步骤,缺的调料自动提醒。
|
||||
- **用户流程**
|
||||
1. 输入食材(手动/语音/勾选常用),可自定义常备调料(油盐酱醋默认已有)。
|
||||
2. 系统从菜谱库中匹配:完全匹配(所有食材都具备)优先,其次为缺1–2种辅料的菜,标注“还差XX”。
|
||||
3. 随机展示3–5道菜,显示匹配度百分比及缺少食材。
|
||||
4. 点开详情:用料清单、步骤图文/视频、烹饪时长、难度星级。
|
||||
5. 支持一键添加缺少食材至购物清单,或分享菜谱。
|
||||
6. 结合时段推荐(早餐推快手菜,冬季推炖煮)。
|
||||
- **菜谱数据来源**:初始购买/爬取公开菜谱(合规授权),后期支持UGC上传+审核。
|
||||
|
||||
### 模式三:探店盲盒(Explore Box)
|
||||
- **一句话描述**:随机推荐附近一家高评价堂食好店,一键导航前往。
|
||||
- **用户流程**
|
||||
1. 授权定位。
|
||||
2. 搜索1–3公里内餐饮POI(地图API)。
|
||||
3. 按评分≥4.3、评论数≥50、结合用户口味偏好与价格设定过滤。
|
||||
4. 随机推荐1家:店名、菜系、人均、距离、推荐语(提炼用户评价)、招牌菜和环境图。
|
||||
5. 可“导航过去”(调起腾讯/高德地图)或查看详情页(跳转大众点评小程序)。
|
||||
6. 同样支持“换一家”。
|
||||
|
||||
---
|
||||
|
||||
## 三、品牌与UI风格
|
||||
|
||||
- **主色调**:活力橙(#FF6A3D)搭配暖黄,体现美食与惊喜感。
|
||||
- **辅助元素**:盲盒机、问号盒、食材图标等插画风;三个模式用统一“盒子”视觉容器,入口设计成盲盒机样式。
|
||||
- **动效**:开盒过程1.5秒震动→开盖→弹出结果卡片,增强仪式感。
|
||||
- **字体**:圆体或粗圆体,传达亲切年轻气质。
|
||||
- **底部导航**:首页(盲盒大厅)、记录、我的,极简结构。
|
||||
|
||||
---
|
||||
|
||||
## 四、技术架构
|
||||
|
||||
### 整体选型
|
||||
- **前端**:原生微信小程序(WXML + WXSS + JavaScript),使用微信官方开发工具,保证最佳性能和兼容性。
|
||||
- **后端**:Java 8 + Spring Boot 2.x,采用经典 MVC 架构,部署于云服务器或容器化环境。
|
||||
- **数据库**:
|
||||
- 主库:MySQL 5.7+(商家、菜谱、用户数据)
|
||||
- 缓存:Redis(热点POI数据、高频菜谱匹配结果、会话缓存)
|
||||
- **外部服务**:
|
||||
- 高德地图Web API(POI搜索、逆地理、路径规划)
|
||||
- 腾讯位置服务(备选)
|
||||
- 菜谱数据API(自建或合作方)
|
||||
- 微信开放能力(跳转外卖平台小程序、订阅消息、地理授权)
|
||||
|
||||
### 数据流
|
||||
```
|
||||
用户授权定位 → 后端获取经纬度 → 调用地图POI并缓存(按网格+时间)→ 业务规则过滤/加权→ 随机抽取 → 返回结果
|
||||
```
|
||||
|
||||
### 核心算法
|
||||
- **高质量池筛选**:SQL或内存运算完成评分、销量、配送时长过滤。
|
||||
- **随机推荐**:
|
||||
1. 确定当日候选集(离线定时刷新或实时计算)。
|
||||
2. 客户端请求时,服务端从候选集中用加权随机(如评分权重、用户偏好协同因子)抽取1条,同时记录已展示避免短时重复。
|
||||
- **菜谱匹配**:将用户食材列表与菜谱食材表求交集,计算匹配度 = 交集食材数 / 菜谱总食材数,降序排列随机取Top3。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据策略与合规
|
||||
|
||||
| 数据需求 | 获取方式 | 更新频率 |
|
||||
|----------|----------|----------|
|
||||
| 附近餐饮POI(名称、坐标、评分、人均、电话、图片) | 高德/腾讯地图POI搜索API,含扩展信息 | 用户实时请求缓存1小时;后台热门区域每6小时预缓存 |
|
||||
| 外卖配送信息(起送价、配送费、时长) | 外卖开放平台(如有)或地图API中部分连锁商户信息 | 随POI一起 |
|
||||
| 用户评价提炼(推荐语) | 使用公开点评文本摘要(通过合法数据供应商)或高德评分+标签 | 每日更新 |
|
||||
| 菜谱数据 | 自建库+公开菜谱API合作 | 每周迭代 |
|
||||
| 用户偏好 | 本地存储+后台用户画像(可选) | 实时 |
|
||||
|
||||
**合规要点**:
|
||||
- 不使用网页爬虫抓取外卖/点评平台商业数据,所有商家信息源自地图开放API。
|
||||
- 用户定位需明确授权,可拒绝或手动选择位置。
|
||||
- 跳转外卖平台仅通过小程序URL Scheme合法跳转。
|
||||
|
||||
---
|
||||
|
||||
## 六、功能页面结构
|
||||
|
||||
1. **启动页**:Logo+“今天吃啥?开个盲盒”。
|
||||
2. **盲盒大厅(首页)**:三大模式入口(卡片+动效),每日一言。
|
||||
3. **外卖/探店员详情页**:商家大图、信息卡片、招牌菜横滑、吸底操作按钮。
|
||||
4. **冰箱盲盒-食材录入页**:输入框+标签+语音,我的常备调料编辑。
|
||||
5. **菜谱推荐列表页**:菜谱卡片+匹配度+缺少食材高亮。
|
||||
6. **菜谱详情页**:用料、步骤、计时器、购物车按钮。
|
||||
7. **记录页**:最近开盒历史、收藏的店/菜谱。
|
||||
8. **个人中心**:偏好设置(口味、忌口、价格区间)、关于我们。
|
||||
|
||||
---
|
||||
|
||||
## 七、开发路线图(MVP → 全功能)
|
||||
|
||||
**Phase 1:核心MVP(4–6周)**
|
||||
- 完成原生微信小程序项目搭建,实现定位授权。
|
||||
- 搭建 Java 8 + Spring Boot 后端项目,配置 MySQL 数据库连接。
|
||||
- 接入高德POI搜索,实现外卖盲盒基础推荐(过滤+随机)。
|
||||
- 设计 MySQL 数据库表结构,离线导入示例菜谱100道。
|
||||
- 完成冰箱盲盒基本匹配逻辑与详情展示。
|
||||
- UI盲盒开启动效1.0。
|
||||
|
||||
**Phase 2:双模式补全(3–4周)**
|
||||
- 探店盲盒功能上线。
|
||||
- 优化推荐权重,加入用户反馈(喜欢/踩)。
|
||||
- 菜谱库扩充至500+,支持语音录入食材。
|
||||
|
||||
**Phase 3:体验与运营(2–3周)**
|
||||
- 记忆用户偏好,协同过滤简单推荐。
|
||||
- 分享卡片、订阅消息(每日推荐通知)。
|
||||
- 性能优化,误触防护,无结果兜底。
|
||||
|
||||
**Phase 4:上线审核与迭代**
|
||||
- 小程序全功能测试,提交审核。
|
||||
- 数据监控与迭代,探索拍照识食材、多人开盒决策等创新功能。
|
||||
|
||||
---
|
||||
|
||||
## 八、预算与资源预估(最小可行)
|
||||
|
||||
- **研发人员**:1名全栈(或前后端各1兼职)
|
||||
- **设计**:1名UI(可外包关键页面)
|
||||
- **服务器**:初期微信云托管低配(约200–500元/月)
|
||||
- **API费用**:高德地图免费配额(日调用量<30万次)足够,菜谱API或数据购买(约1000–3000元一次性)
|
||||
- **时间**:MVP约2个月可上线测试
|
||||
|
||||
---
|
||||
|
||||
## 九、核心文案与传播素材
|
||||
|
||||
- **小程序简介**:外卖盲盒、冰箱做菜、附近探店,一键解决今天吃啥。
|
||||
- **分享文案**:
|
||||
“我又开出了宝藏店铺!你也来测测今天吃啥吧”
|
||||
“冰箱剩菜变盛宴,这个盲盒有点厉害”
|
||||
- **功能引导**:
|
||||
外卖盲盒页:不知道点什么?让我帮你决定。
|
||||
冰箱盲盒页:拍下你的食材,魔法就出现了。
|
||||
探店盲盒页:好店就在转角,敢不敢盲开一个?
|
||||
|
||||
---
|
||||
163
doc/ui.md
Normal file
163
doc/ui.md
Normal file
@@ -0,0 +1,163 @@
|
||||
下面以线框描述+关键标注的方式呈现“吃啥盲盒”核心页面的UI草图。整体采用**活力橙+暖黄**主色调,圆角卡片、盲盒机质感、轻拟物图标,营造“开盒惊喜”的氛围。
|
||||
|
||||
---
|
||||
|
||||
### 1. 盲盒大厅(首页)
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ 上午 11:30 📍 定位 │ <-- 状态栏/定位按钮
|
||||
├──────────────────────────┤
|
||||
│ │
|
||||
│ 🎁 今天吃啥? │ <-- 大标题,圆体
|
||||
│ 开个盲盒 │
|
||||
│ │
|
||||
│ ┌─ 外卖盲盒 ────────┐ │
|
||||
│ │ 🛵 │ │
|
||||
│ │ 开个外卖盒 │ │ <-- 主按钮,带微动效
|
||||
│ │ 随机高质量外卖 │ │
|
||||
│ └──────────────────┘ │
|
||||
│ ┌─ 冰箱盲盒 ────────┐ │
|
||||
│ │ 🥬 │ │
|
||||
│ │ 开个冰箱盒 │ │
|
||||
│ │ 有啥食材做啥菜 │ │
|
||||
│ └──────────────────┘ │
|
||||
│ ┌─ 探店盲盒 ────────┐ │
|
||||
│ │ 🍜 │ │
|
||||
│ │ 开个附近盒 │ │
|
||||
│ │ 发现隐藏好店 │ │
|
||||
│ └──────────────────┘ │
|
||||
│ │
|
||||
│ “今天宜尝试酸辣口味” │ <-- 每日一言
|
||||
└──────────────────────────┘
|
||||
```
|
||||
**说明**:三个入口设计成盲盒机器视觉卡片,点击后触发开盒动效。上角定位按钮可手动切换位置。
|
||||
|
||||
---
|
||||
|
||||
### 2. 外卖盲盒结果页
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ ← 返回 📤 分享 │
|
||||
├──────────────────────────┤
|
||||
│ 🎉 你抽到的外卖是… │
|
||||
│ │
|
||||
│ ┌────────────────────┐ │
|
||||
│ │ [商家头图] │ │
|
||||
│ │ 🌟 4.8 月销999+ │ │
|
||||
│ │ 川味小厨 │ │
|
||||
│ │ ¥32/人 · 1.2km │ │
|
||||
│ │ 30-40分钟送达 │ │
|
||||
│ │ “水煮鱼绝了!” │ │
|
||||
│ │ [招牌菜1][2][3] │ │
|
||||
│ └────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ 去美团下单 ──────┐ │
|
||||
│ └────────────────────┘ │
|
||||
│ 🔄 再开一个 │ <-- 换一家
|
||||
└──────────────────────────┘
|
||||
```
|
||||
**说明**:商家信息卡片居中,招牌菜可横滑。吸底按钮为主操作,弱化“换一家”防止无限循环。
|
||||
|
||||
---
|
||||
|
||||
### 3. 冰箱盲盒-食材录入页
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ 冰箱里有什么? │
|
||||
│ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ 🔍 输入食材名称 │ │ <-- 输入框,带语音icon
|
||||
│ └──────────────────┘ │
|
||||
│ 常用:🥚鸡蛋 🍅西红柿 🥩鸡胸 🧅洋葱 │
|
||||
│ ➕ 自定义 │
|
||||
│ │
|
||||
│ 📦 我的常备调料 │
|
||||
│ ✔️油 ✔️盐 ✔️酱油 ✔️醋 │
|
||||
│ ✔️料酒 ✔️生抽 ➕添加 │
|
||||
│ │
|
||||
│ ┌─── 开盒做饭!───┐ │
|
||||
│ └──────────────────┘ │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
**说明**:标签化快速输入,可语音录入。常备调料默认勾选,减少重复输入。按钮文案呼应“开盒”。
|
||||
|
||||
---
|
||||
|
||||
### 4. 冰箱盲盒-菜谱推荐列表
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ 为你找到3个菜谱 │
|
||||
│ │
|
||||
│ ┌─ 菜谱卡片 ────────┐ │
|
||||
│ │ 西红柿炒蛋 匹配度95%│ │
|
||||
│ │ 🥬 缺:小葱 │ │
|
||||
│ └──────────────────┘ │
|
||||
│ ┌─ 菜谱卡片 ────────┐ │
|
||||
│ │ 鸡肉沙拉 匹配度80%│ │
|
||||
│ │ 🥬 缺:生菜、沙拉酱 │ │
|
||||
│ └──────────────────┘ │
|
||||
│ ┌─ 菜谱卡片 ────────┐ │
|
||||
│ │ 煎鸡胸 匹配度90%│ │
|
||||
│ │ 🥬 缺:黑胡椒 │ │
|
||||
│ └──────────────────┘ │
|
||||
│ │
|
||||
│ 🔄 换一批菜谱 │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
**说明**:卡片突出匹配度,用绿色高亮匹配,黄色标注缺失食材。优先展示全匹配菜谱。
|
||||
|
||||
---
|
||||
|
||||
### 5. 菜谱详情页
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ [成品图] ⭐难度2 │
|
||||
│ 西红柿炒蛋 ⏱ 10分钟 │
|
||||
├──────────────────────────┤
|
||||
│ 🥚 用料 │
|
||||
│ 鸡蛋2个 西红柿2个 盐 糖 │
|
||||
│ 小葱(家里缺) → 加入购物单│
|
||||
│ │
|
||||
│ 📝 步骤 │
|
||||
│ 1. 鸡蛋打散,加盐搅匀 │
|
||||
│ 2. 西红柿切块备用 │
|
||||
│ 3. 热油炒蛋,盛出 │
|
||||
│ 4. 炒西红柿出汁… │
|
||||
│ │
|
||||
│ 🔖 计时器 📋 购物清单 │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
**说明**:用料缺货高亮并支持一键加入购物清单。步骤清晰,可内置简单计时器。
|
||||
|
||||
---
|
||||
|
||||
### 6. 探店盲盒结果页
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ 🎊 附近发现一家宝藏店 │
|
||||
│ │
|
||||
│ ┌────────────────────┐ │
|
||||
│ │ [店铺头图] │ │
|
||||
│ │ 🏮 藏巷·居酒屋 │ │
|
||||
│ │ ⭐4.6 · 2.3km │ │
|
||||
│ │ 人均 ¥120 │ │
|
||||
│ │ “三文鱼厚切超满足”│ │
|
||||
│ │ 🥘 招牌:鹅肝寿司 │ │
|
||||
│ └────────────────────┘ │
|
||||
│ │
|
||||
│ ┌── 导航过去 ───────┐ │
|
||||
│ └────────────────────┘ │
|
||||
│ 🗺 查看详细点评 │
|
||||
│ 🔄 再开一个 │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
**说明**:强调推荐语和招牌菜,导航为主要操作,跳转外部地图;可查看详情跳大众点评。
|
||||
|
||||
---
|
||||
|
||||
### 全局元素
|
||||
- **颜色**:背景米白(#FFFBF4),主色橙(#FF6A3D),文字深灰(#333),高亮绿(#4CAF50)
|
||||
- **圆角**:卡片16px,按钮24px
|
||||
- **字体**:微信默认中文字体(苹方/雅黑),标题加粗,大小16-20px
|
||||
- **图标**:线性+面性结合,盲盒/食材使用自定义插画风
|
||||
- **错误兜底**:定位失败时显示“手动选择位置”,无商家时显示“附近喵星人占领了,换片区域试试?”
|
||||
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