commit 802b4ba2296af2b221a4c96f9d09c075c4ca446f Author: 王鹏 Date: Fri May 8 20:02:27 2026 +0800 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c39a3e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# IDE +.idea/ +*.iml + +# Build output +target/ +build/ +dist/ + +# OS +.DS_Store +Thumbs.db + +# WeChat +miniapp/project.private.config.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..26d06fa --- /dev/null +++ b/CLAUDE.md @@ -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. diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..2e90048 --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + com.chowbox + chowbox-backend + 1.0.0-SNAPSHOT + jar + + + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-redis + + + com.baomidou + mybatis-plus-boot-starter + 3.5.5 + + + mysql + mysql-connector-java + 8.0.33 + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/backend/src/main/java/com/chowbox/ChowBoxApplication.java b/backend/src/main/java/com/chowbox/ChowBoxApplication.java new file mode 100644 index 0000000..7a14df3 --- /dev/null +++ b/backend/src/main/java/com/chowbox/ChowBoxApplication.java @@ -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); + } +} diff --git a/backend/src/main/java/com/chowbox/config/GlobalExceptionHandler.java b/backend/src/main/java/com/chowbox/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..0cbade0 --- /dev/null +++ b/backend/src/main/java/com/chowbox/config/GlobalExceptionHandler.java @@ -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, "服务器出了点小差,请稍后再试"); + } +} diff --git a/backend/src/main/java/com/chowbox/config/RedisConfig.java b/backend/src/main/java/com/chowbox/config/RedisConfig.java new file mode 100644 index 0000000..ea597d1 --- /dev/null +++ b/backend/src/main/java/com/chowbox/config/RedisConfig.java @@ -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 redisTemplate(RedisConnectionFactory factory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(factory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + return template; + } +} diff --git a/backend/src/main/java/com/chowbox/config/WebConfig.java b/backend/src/main/java/com/chowbox/config/WebConfig.java new file mode 100644 index 0000000..83af5ef --- /dev/null +++ b/backend/src/main/java/com/chowbox/config/WebConfig.java @@ -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("*"); + } +} diff --git a/backend/src/main/java/com/chowbox/controller/ExploreController.java b/backend/src/main/java/com/chowbox/controller/ExploreController.java new file mode 100644 index 0000000..13a512a --- /dev/null +++ b/backend/src/main/java/com/chowbox/controller/ExploreController.java @@ -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 roll(@RequestBody Map 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); + } +} diff --git a/backend/src/main/java/com/chowbox/controller/FridgeController.java b/backend/src/main/java/com/chowbox/controller/FridgeController.java new file mode 100644 index 0000000..3d19469 --- /dev/null +++ b/backend/src/main/java/com/chowbox/controller/FridgeController.java @@ -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> match(@RequestBody Map body) { + Object ingObj = body.get("ingredients"); + log.info("Fridge match request: ingredients type={}, value={}", ingObj != null ? ingObj.getClass().getSimpleName() : "null", ingObj); + + List 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 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 results = fridgeService.matchRecipes(ingredients, staples); + log.info("Fridge match: {} results", results.size()); + return ApiResponse.ok(results); + } +} diff --git a/backend/src/main/java/com/chowbox/controller/RecipeController.java b/backend/src/main/java/com/chowbox/controller/RecipeController.java new file mode 100644 index 0000000..f47c225 --- /dev/null +++ b/backend/src/main/java/com/chowbox/controller/RecipeController.java @@ -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 detail(@PathVariable Long id) { + Recipe recipe = fridgeService.getRecipeDetail(id); + if (recipe == null) { + return ApiResponse.fail(404, "菜谱不存在"); + } + return ApiResponse.ok(recipe); + } +} diff --git a/backend/src/main/java/com/chowbox/controller/TakeoutController.java b/backend/src/main/java/com/chowbox/controller/TakeoutController.java new file mode 100644 index 0000000..1eb5ae1 --- /dev/null +++ b/backend/src/main/java/com/chowbox/controller/TakeoutController.java @@ -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 roll(@RequestBody Map 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); + } +} diff --git a/backend/src/main/java/com/chowbox/controller/VoiceController.java b/backend/src/main/java/com/chowbox/controller/VoiceController.java new file mode 100644 index 0000000..27b8e2e --- /dev/null +++ b/backend/src/main/java/com/chowbox/controller/VoiceController.java @@ -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> 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", "")); + } +} diff --git a/backend/src/main/java/com/chowbox/mapper/RecipeIngredientMapper.java b/backend/src/main/java/com/chowbox/mapper/RecipeIngredientMapper.java new file mode 100644 index 0000000..1b18621 --- /dev/null +++ b/backend/src/main/java/com/chowbox/mapper/RecipeIngredientMapper.java @@ -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 { +} diff --git a/backend/src/main/java/com/chowbox/mapper/RecipeMapper.java b/backend/src/main/java/com/chowbox/mapper/RecipeMapper.java new file mode 100644 index 0000000..3212a9c --- /dev/null +++ b/backend/src/main/java/com/chowbox/mapper/RecipeMapper.java @@ -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 { +} diff --git a/backend/src/main/java/com/chowbox/mapper/RecipeStepMapper.java b/backend/src/main/java/com/chowbox/mapper/RecipeStepMapper.java new file mode 100644 index 0000000..8577048 --- /dev/null +++ b/backend/src/main/java/com/chowbox/mapper/RecipeStepMapper.java @@ -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 { +} diff --git a/backend/src/main/java/com/chowbox/model/ApiResponse.java b/backend/src/main/java/com/chowbox/model/ApiResponse.java new file mode 100644 index 0000000..47b1dd5 --- /dev/null +++ b/backend/src/main/java/com/chowbox/model/ApiResponse.java @@ -0,0 +1,25 @@ +package com.chowbox.model; + +import lombok.Data; + +@Data +public class ApiResponse { + private int code; + private String message; + private T data; + + public static ApiResponse ok(T data) { + ApiResponse r = new ApiResponse<>(); + r.code = 200; + r.message = "success"; + r.data = data; + return r; + } + + public static ApiResponse fail(int code, String message) { + ApiResponse r = new ApiResponse<>(); + r.code = code; + r.message = message; + return r; + } +} diff --git a/backend/src/main/java/com/chowbox/model/FridgeMatchResult.java b/backend/src/main/java/com/chowbox/model/FridgeMatchResult.java new file mode 100644 index 0000000..f416cdf --- /dev/null +++ b/backend/src/main/java/com/chowbox/model/FridgeMatchResult.java @@ -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 missingIngredients; // 缺少的食材 +} diff --git a/backend/src/main/java/com/chowbox/model/Recipe.java b/backend/src/main/java/com/chowbox/model/Recipe.java new file mode 100644 index 0000000..d04fd46 --- /dev/null +++ b/backend/src/main/java/com/chowbox/model/Recipe.java @@ -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 ingredients; + + @TableField(exist = false) + private List steps; +} diff --git a/backend/src/main/java/com/chowbox/model/RecipeIngredient.java b/backend/src/main/java/com/chowbox/model/RecipeIngredient.java new file mode 100644 index 0000000..2a8300b --- /dev/null +++ b/backend/src/main/java/com/chowbox/model/RecipeIngredient.java @@ -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; +} diff --git a/backend/src/main/java/com/chowbox/model/RecipeStep.java b/backend/src/main/java/com/chowbox/model/RecipeStep.java new file mode 100644 index 0000000..cace298 --- /dev/null +++ b/backend/src/main/java/com/chowbox/model/RecipeStep.java @@ -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; +} diff --git a/backend/src/main/java/com/chowbox/model/Restaurant.java b/backend/src/main/java/com/chowbox/model/Restaurant.java new file mode 100644 index 0000000..f9e51d8 --- /dev/null +++ b/backend/src/main/java/com/chowbox/model/Restaurant.java @@ -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; +} diff --git a/backend/src/main/java/com/chowbox/service/AmapService.java b/backend/src/main/java/com/chowbox/service/AmapService.java new file mode 100644 index 0000000..7851676 --- /dev/null +++ b/backend/src/main/java/com/chowbox/service/AmapService.java @@ -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 redisTemplate; + + private final RestTemplate restTemplate = new RestTemplate(); + + @SuppressWarnings("unchecked") + public List 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 cached = (List) 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 response = restTemplate.getForObject(url, Map.class); + List 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 parseResponse(Map 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> pois = (List>) 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 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 bizExt = (Map) 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 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 photoMap = (Map) 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 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); + } +} diff --git a/backend/src/main/java/com/chowbox/service/FridgeService.java b/backend/src/main/java/com/chowbox/service/FridgeService.java new file mode 100644 index 0000000..c18d3ec --- /dev/null +++ b/backend/src/main/java/com/chowbox/service/FridgeService.java @@ -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 DEFAULT_STAPLES = new HashSet<>(Arrays.asList( + "油", "盐", "酱油", "生抽", "老抽", "醋", "料酒", "蚝油", + "糖", "白糖", "淀粉", "香油", "姜", "蒜", "葱", "干辣椒", "花椒" + )); + + private static final Random RANDOM = new Random(); + + public List matchRecipes(List userIngredients, List customStaples) { + Set userSet = new HashSet<>(); + for (String ing : userIngredients) { + userSet.add(ing.trim()); + } + + // 合并系统默认 + 用户自定义常备调料 + Set allStaples = new HashSet<>(DEFAULT_STAPLES); + if (customStaples != null) { + for (String s : customStaples) { + allStaples.add(s.trim()); + } + } + userSet.addAll(allStaples); + + List allRecipes = recipeMapper.selectList(null); + log.info("FridgeService: total recipes in DB = {}", allRecipes.size()); + List 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 recipeIngredients = ingredientMapper.selectList( + new QueryWrapper().eq("recipe_id", recipe.getId()) + ); + + List 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 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 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 weightedRandomPick(List pool) { + if (pool.size() <= 3) return pool; + + int pickCount = 3 + RANDOM.nextInt(Math.min(3, pool.size() - 2)); // 3-5 + + List shuffled = new ArrayList<>(pool); + Collections.shuffle(shuffled, RANDOM); + + // 加权随机 + List picked = new ArrayList<>(); + List 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 ingredients = ingredientMapper.selectList( + new QueryWrapper().eq("recipe_id", recipeId) + ); + List steps = stepMapper.selectList( + new QueryWrapper().eq("recipe_id", recipeId).orderByAsc("step_order") + ); + + recipe.setIngredients(ingredients); + recipe.setSteps(steps); + return recipe; + } +} diff --git a/backend/src/main/java/com/chowbox/service/TakeoutService.java b/backend/src/main/java/com/chowbox/service/TakeoutService.java new file mode 100644 index 0000000..3c145b4 --- /dev/null +++ b/backend/src/main/java/com/chowbox/service/TakeoutService.java @@ -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> 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 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 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 shown = recentlyShown.getOrDefault(openid, new LinkedHashSet<>()); + List candidates = pool.stream() + .filter(r -> !shown.contains(r.getId())) + .collect(Collectors.toList()); + + if (candidates.isEmpty()) { + // 全部展示过了,清空历史重来 + recentlyShown.remove(openid); + candidates = new ArrayList<>(pool); + } + + // ── 用户偏好过滤 ── + List 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 userShown = recentlyShown.computeIfAbsent(openid, + k -> new LinkedHashSet<>()); + userShown.add(selected.getId()); + // 限制历史大小 + if (userShown.size() > MAX_SHOWN_HISTORY) { + Iterator it = userShown.iterator(); + it.next(); + it.remove(); + } + + // ── 补充合理默认值 ── + enrichRestaurant(selected); + + return selected; + } + + private List filterByRating(List list, double minRating) { + return list.stream() + .filter(r -> r.getRating() != null && r.getRating() >= minRating) + .collect(Collectors.toList()); + } + + /** + * 根据用户偏好过滤餐厅列表,三层过滤:价格 → 口味 → 忌口 + */ + private List filterByPreferences(List list, + String taste, String priceRange, String allergies) { + List 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 filterByPriceRange(List 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 filterByTaste(List 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 filterByAllergies(List 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 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 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 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元"; + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..489e2ae --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -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 diff --git a/backend/src/main/resources/db/data.sql b/backend/src/main/resources/db/data.sql new file mode 100644 index 0000000..08e8acf --- /dev/null +++ b/backend/src/main/resources/db/data.sql @@ -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,'撒上葱花浇上面汤即可'); diff --git a/backend/src/main/resources/db/data_ingredients.sql b/backend/src/main/resources/db/data_ingredients.sql new file mode 100644 index 0000000..85ce116 --- /dev/null +++ b/backend/src/main/resources/db/data_ingredients.sql @@ -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); diff --git a/backend/src/main/resources/db/data_ingredients_2.sql b/backend/src/main/resources/db/data_ingredients_2.sql new file mode 100644 index 0000000..c3cd707 --- /dev/null +++ b/backend/src/main/resources/db/data_ingredients_2.sql @@ -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); diff --git a/backend/src/main/resources/db/data_ingredients_3.sql b/backend/src/main/resources/db/data_ingredients_3.sql new file mode 100644 index 0000000..0c8314b --- /dev/null +++ b/backend/src/main/resources/db/data_ingredients_3.sql @@ -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); diff --git a/backend/src/main/resources/db/data_ingredients_4.sql b/backend/src/main/resources/db/data_ingredients_4.sql new file mode 100644 index 0000000..f886dec --- /dev/null +++ b/backend/src/main/resources/db/data_ingredients_4.sql @@ -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); diff --git a/backend/src/main/resources/db/data_ingredients_5.sql b/backend/src/main/resources/db/data_ingredients_5.sql new file mode 100644 index 0000000..a73f2ba --- /dev/null +++ b/backend/src/main/resources/db/data_ingredients_5.sql @@ -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); diff --git a/backend/src/main/resources/db/data_steps.sql b/backend/src/main/resources/db/data_steps.sql new file mode 100644 index 0000000..fbd083a --- /dev/null +++ b/backend/src/main/resources/db/data_steps.sql @@ -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,'煮好的面条捞入碗中即可'); diff --git a/backend/src/main/resources/db/schema.sql b/backend/src/main/resources/db/schema.sql new file mode 100644 index 0000000..224f61a --- /dev/null +++ b/backend/src/main/resources/db/schema.sql @@ -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='开盒记录'; diff --git a/doc/box.md b/doc/box.md new file mode 100644 index 0000000..67b7894 --- /dev/null +++ b/doc/box.md @@ -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以内),震动序列,图片预加载(结果卡片图提前加载)。 + +--- diff --git a/doc/plan.md b/doc/plan.md new file mode 100644 index 0000000..c3f4acb --- /dev/null +++ b/doc/plan.md @@ -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个月可上线测试 + +--- + +## 九、核心文案与传播素材 + +- **小程序简介**:外卖盲盒、冰箱做菜、附近探店,一键解决今天吃啥。 +- **分享文案**: + “我又开出了宝藏店铺!你也来测测今天吃啥吧” + “冰箱剩菜变盛宴,这个盲盒有点厉害” +- **功能引导**: + 外卖盲盒页:不知道点什么?让我帮你决定。 + 冰箱盲盒页:拍下你的食材,魔法就出现了。 + 探店盲盒页:好店就在转角,敢不敢盲开一个? + +--- diff --git a/doc/ui.md b/doc/ui.md new file mode 100644 index 0000000..3c32dad --- /dev/null +++ b/doc/ui.md @@ -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 +- **图标**:线性+面性结合,盲盒/食材使用自定义插画风 +- **错误兜底**:定位失败时显示“手动选择位置”,无商家时显示“附近喵星人占领了,换片区域试试?” diff --git a/miniapp/app.js b/miniapp/app.js new file mode 100644 index 0000000..a828d45 --- /dev/null +++ b/miniapp/app.js @@ -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); + } + }); + } +}); diff --git a/miniapp/app.json b/miniapp/app.json new file mode 100644 index 0000000..58d6fe3 --- /dev/null +++ b/miniapp/app.json @@ -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" +} diff --git a/miniapp/app.wxss b/miniapp/app.wxss new file mode 100644 index 0000000..1611157 --- /dev/null +++ b/miniapp/app.wxss @@ -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); +} diff --git a/miniapp/components/box-animation/box-animation.js b/miniapp/components/box-animation/box-animation.js new file mode 100644 index 0000000..c2411c2 --- /dev/null +++ b/miniapp/components/box-animation/box-animation.js @@ -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 }); + } + } +}); diff --git a/miniapp/components/box-animation/box-animation.json b/miniapp/components/box-animation/box-animation.json new file mode 100644 index 0000000..467ce29 --- /dev/null +++ b/miniapp/components/box-animation/box-animation.json @@ -0,0 +1,3 @@ +{ + "component": true +} diff --git a/miniapp/components/box-animation/box-animation.wxml b/miniapp/components/box-animation/box-animation.wxml new file mode 100644 index 0000000..abe2707 --- /dev/null +++ b/miniapp/components/box-animation/box-animation.wxml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + {{boxEmoji}} + + + + + {{sparkEmoji}} + {{sparkEmoji}} + {{sparkEmoji}} + {{sparkEmoji}} + + + + + + + + + + + + + + + + + + + + diff --git a/miniapp/components/box-animation/box-animation.wxss b/miniapp/components/box-animation/box-animation.wxss new file mode 100644 index 0000000..d7ed589 --- /dev/null +++ b/miniapp/components/box-animation/box-animation.wxss @@ -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); } +} diff --git a/miniapp/components/ingredient-tag/ingredient-tag.js b/miniapp/components/ingredient-tag/ingredient-tag.js new file mode 100644 index 0000000..069ae28 --- /dev/null +++ b/miniapp/components/ingredient-tag/ingredient-tag.js @@ -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 }); + } + } +}); diff --git a/miniapp/components/ingredient-tag/ingredient-tag.json b/miniapp/components/ingredient-tag/ingredient-tag.json new file mode 100644 index 0000000..467ce29 --- /dev/null +++ b/miniapp/components/ingredient-tag/ingredient-tag.json @@ -0,0 +1,3 @@ +{ + "component": true +} diff --git a/miniapp/components/ingredient-tag/ingredient-tag.wxml b/miniapp/components/ingredient-tag/ingredient-tag.wxml new file mode 100644 index 0000000..afeee22 --- /dev/null +++ b/miniapp/components/ingredient-tag/ingredient-tag.wxml @@ -0,0 +1,4 @@ + + {{icon}} {{name}} + + diff --git a/miniapp/components/ingredient-tag/ingredient-tag.wxss b/miniapp/components/ingredient-tag/ingredient-tag.wxss new file mode 100644 index 0000000..4f68940 --- /dev/null +++ b/miniapp/components/ingredient-tag/ingredient-tag.wxss @@ -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; +} diff --git a/miniapp/components/mode-card/mode-card.js b/miniapp/components/mode-card/mode-card.js new file mode 100644 index 0000000..b247cb6 --- /dev/null +++ b/miniapp/components/mode-card/mode-card.js @@ -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 }); + } + } +}); diff --git a/miniapp/components/mode-card/mode-card.json b/miniapp/components/mode-card/mode-card.json new file mode 100644 index 0000000..467ce29 --- /dev/null +++ b/miniapp/components/mode-card/mode-card.json @@ -0,0 +1,3 @@ +{ + "component": true +} diff --git a/miniapp/components/mode-card/mode-card.wxml b/miniapp/components/mode-card/mode-card.wxml new file mode 100644 index 0000000..2c94907 --- /dev/null +++ b/miniapp/components/mode-card/mode-card.wxml @@ -0,0 +1,10 @@ + + + {{icon}} + + + {{title}} + {{desc}} + + {{actionText}} + diff --git a/miniapp/components/mode-card/mode-card.wxss b/miniapp/components/mode-card/mode-card.wxss new file mode 100644 index 0000000..3d6cf00 --- /dev/null +++ b/miniapp/components/mode-card/mode-card.wxss @@ -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); +} diff --git a/miniapp/components/result-card/result-card.js b/miniapp/components/result-card/result-card.js new file mode 100644 index 0000000..f695f33 --- /dev/null +++ b/miniapp/components/result-card/result-card.js @@ -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: [] } + } +}); diff --git a/miniapp/components/result-card/result-card.json b/miniapp/components/result-card/result-card.json new file mode 100644 index 0000000..467ce29 --- /dev/null +++ b/miniapp/components/result-card/result-card.json @@ -0,0 +1,3 @@ +{ + "component": true +} diff --git a/miniapp/components/result-card/result-card.wxml b/miniapp/components/result-card/result-card.wxml new file mode 100644 index 0000000..14d6213 --- /dev/null +++ b/miniapp/components/result-card/result-card.wxml @@ -0,0 +1,21 @@ + + + + + + {{rating}} + 月销{{sales}} + + {{name}} + + ¥{{avgPrice}}/人 + · + {{distance}} + + {{extra}} + "{{recommendReason}}" + + {{item}} + + + diff --git a/miniapp/components/result-card/result-card.wxss b/miniapp/components/result-card/result-card.wxss new file mode 100644 index 0000000..6de2d46 --- /dev/null +++ b/miniapp/components/result-card/result-card.wxss @@ -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; +} diff --git a/miniapp/pages/explore-result/explore-result.js b/miniapp/pages/explore-result/explore-result.js new file mode 100644 index 0000000..ee68b91 --- /dev/null +++ b/miniapp/pages/explore-result/explore-result.js @@ -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); + } +}); diff --git a/miniapp/pages/explore-result/explore-result.json b/miniapp/pages/explore-result/explore-result.json new file mode 100644 index 0000000..2b9c7d5 --- /dev/null +++ b/miniapp/pages/explore-result/explore-result.json @@ -0,0 +1,7 @@ +{ + "usingComponents": { + "result-card": "/components/result-card/result-card", + "box-animation": "/components/box-animation/box-animation" + }, + "navigationBarTitleText": "探店盲盒" +} diff --git a/miniapp/pages/explore-result/explore-result.wxml b/miniapp/pages/explore-result/explore-result.wxml new file mode 100644 index 0000000..30311c8 --- /dev/null +++ b/miniapp/pages/explore-result/explore-result.wxml @@ -0,0 +1,37 @@ + + + 😿 + {{error}} + + + + + 🎊 附近发现一家宝藏店 + + + + 再开一个 + + + + + + 🍜 + {{result.name}} + ⭐ {{result.rating}} + + + diff --git a/miniapp/pages/explore-result/explore-result.wxss b/miniapp/pages/explore-result/explore-result.wxss new file mode 100644 index 0000000..778dfb7 --- /dev/null +++ b/miniapp/pages/explore-result/explore-result.wxss @@ -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; +} diff --git a/miniapp/pages/fridge-input/fridge-input.js b/miniapp/pages/fridge-input/fridge-input.js new file mode 100644 index 0000000..4929ba6 --- /dev/null +++ b/miniapp/pages/fridge-input/fridge-input.js @@ -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 }); + } +}); diff --git a/miniapp/pages/fridge-input/fridge-input.json b/miniapp/pages/fridge-input/fridge-input.json new file mode 100644 index 0000000..6ff5996 --- /dev/null +++ b/miniapp/pages/fridge-input/fridge-input.json @@ -0,0 +1,6 @@ +{ + "usingComponents": { + "ingredient-tag": "/components/ingredient-tag/ingredient-tag" + }, + "navigationBarTitleText": "冰箱盲盒" +} diff --git a/miniapp/pages/fridge-input/fridge-input.wxml b/miniapp/pages/fridge-input/fridge-input.wxml new file mode 100644 index 0000000..14562fd --- /dev/null +++ b/miniapp/pages/fridge-input/fridge-input.wxml @@ -0,0 +1,66 @@ + + 冰箱里有什么? + + + + + {{voiceRecording ? '🔴' : '🎤'}} + + + + + 常用食材 + + + + + + + + + + + + + + + + + + + 我的常备调料 + 编辑 + + + {{item}} + + + + + + + + + + 编辑常备调料 + 完成 + + + + 添加 + + + + {{item}} + + + + + diff --git a/miniapp/pages/fridge-input/fridge-input.wxss b/miniapp/pages/fridge-input/fridge-input.wxss new file mode 100644 index 0000000..1427c44 --- /dev/null +++ b/miniapp/pages/fridge-input/fridge-input.wxss @@ -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; +} diff --git a/miniapp/pages/index/index.js b/miniapp/pages/index/index.js new file mode 100644 index 0000000..46e5cc8 --- /dev/null +++ b/miniapp/pages/index/index.js @@ -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 }); + } +}); diff --git a/miniapp/pages/index/index.json b/miniapp/pages/index/index.json new file mode 100644 index 0000000..977f344 --- /dev/null +++ b/miniapp/pages/index/index.json @@ -0,0 +1,6 @@ +{ + "usingComponents": { + "mode-card": "/components/mode-card/mode-card" + }, + "navigationBarTitleText": "吃啥盲盒" +} diff --git a/miniapp/pages/index/index.wxml b/miniapp/pages/index/index.wxml new file mode 100644 index 0000000..f92d7c6 --- /dev/null +++ b/miniapp/pages/index/index.wxml @@ -0,0 +1,41 @@ + + + {{greeting}} + {{locationText}} + + + + 🎁 + 今天吃啥? + 开个盲盒 + + + + + + + + + {{dailyTip}} + diff --git a/miniapp/pages/index/index.wxss b/miniapp/pages/index/index.wxss new file mode 100644 index 0000000..05140f7 --- /dev/null +++ b/miniapp/pages/index/index.wxss @@ -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; +} diff --git a/miniapp/pages/mine/mine.js b/miniapp/pages/mine/mine.js new file mode 100644 index 0000000..55847d2 --- /dev/null +++ b/miniapp/pages/mine/mine.js @@ -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', []); + } + } + }); + } +}); diff --git a/miniapp/pages/mine/mine.json b/miniapp/pages/mine/mine.json new file mode 100644 index 0000000..4ec55be --- /dev/null +++ b/miniapp/pages/mine/mine.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "我的" +} diff --git a/miniapp/pages/mine/mine.wxml b/miniapp/pages/mine/mine.wxml new file mode 100644 index 0000000..a80d0a1 --- /dev/null +++ b/miniapp/pages/mine/mine.wxml @@ -0,0 +1,48 @@ + + + + 👤 + {{nickname || '点击登录'}} + + + + 口味偏好 + + 🌶️ 辣 + 🌱 清淡 + 🍋 酸甜 + 😋 都可以 + + + + + 价格区间(外卖) + + ¥ 人均<30 + ¥ 人均30-80 + ¥¥ 人均>80 + 都行 + + + + + 忌口/过敏 + + + + + + 购物清单 ({{shoppingList.length}}项) + 清空 + + + {{item.checked ? '✅' : '⬜'}} + + {{item.name}} {{item.amount}} + 来自:{{item.from}} + + + + + 吃啥盲盒 ChowBox v1.0.0 + diff --git a/miniapp/pages/mine/mine.wxss b/miniapp/pages/mine/mine.wxss new file mode 100644 index 0000000..cb3b30e --- /dev/null +++ b/miniapp/pages/mine/mine.wxss @@ -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); +} diff --git a/miniapp/pages/recipe-detail/recipe-detail.js b/miniapp/pages/recipe-detail/recipe-detail.js new file mode 100644 index 0000000..6350d7b --- /dev/null +++ b/miniapp/pages/recipe-detail/recipe-detail.js @@ -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' }); + } + } +}); diff --git a/miniapp/pages/recipe-detail/recipe-detail.json b/miniapp/pages/recipe-detail/recipe-detail.json new file mode 100644 index 0000000..a7db899 --- /dev/null +++ b/miniapp/pages/recipe-detail/recipe-detail.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "菜谱详情" +} diff --git a/miniapp/pages/recipe-detail/recipe-detail.wxml b/miniapp/pages/recipe-detail/recipe-detail.wxml new file mode 100644 index 0000000..f930fc8 --- /dev/null +++ b/miniapp/pages/recipe-detail/recipe-detail.wxml @@ -0,0 +1,43 @@ + + + 🍲 + + + {{recipe.name}} + + ⭐ 难度{{recipe.difficulty}} + ⏱️ {{recipe.cookTime}}分钟 + {{recipe.category}} + + + + + 用料 + + + {{item.ingredientName}} + {{item.amount}} + + + + + + 步骤 + + + {{item.stepOrder}} + + {{item.content}} + + + + + + + + + 加入购物清单 + + + +加载中… diff --git a/miniapp/pages/recipe-detail/recipe-detail.wxss b/miniapp/pages/recipe-detail/recipe-detail.wxss new file mode 100644 index 0000000..8047c1d --- /dev/null +++ b/miniapp/pages/recipe-detail/recipe-detail.wxss @@ -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); +} diff --git a/miniapp/pages/recipe-list/recipe-list.js b/miniapp/pages/recipe-list/recipe-list.js new file mode 100644 index 0000000..0d9b48e --- /dev/null +++ b/miniapp/pages/recipe-list/recipe-list.js @@ -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); + } +}); diff --git a/miniapp/pages/recipe-list/recipe-list.json b/miniapp/pages/recipe-list/recipe-list.json new file mode 100644 index 0000000..1f66a96 --- /dev/null +++ b/miniapp/pages/recipe-list/recipe-list.json @@ -0,0 +1,6 @@ +{ + "usingComponents": { + "box-animation": "/components/box-animation/box-animation" + }, + "navigationBarTitleText": "菜谱推荐" +} diff --git a/miniapp/pages/recipe-list/recipe-list.wxml b/miniapp/pages/recipe-list/recipe-list.wxml new file mode 100644 index 0000000..36c2eaa --- /dev/null +++ b/miniapp/pages/recipe-list/recipe-list.wxml @@ -0,0 +1,42 @@ + + + + 🥬 + {{results[0].recipe.name}} + 匹配度 {{results[0].matchRate}}% + + + + + 为你找到 {{results.length}} 个菜谱 + + + + + + {{item.recipe.name}} + + ⭐{{item.recipe.difficulty}} + ⏱️{{item.recipe.cookTime}}分钟 + + + + + + {{item.matchRate}}% + + + 缺:{{item}} + + + + + + 没有匹配的菜谱,试试换个食材组合 + 换一批菜谱 + diff --git a/miniapp/pages/recipe-list/recipe-list.wxss b/miniapp/pages/recipe-list/recipe-list.wxss new file mode 100644 index 0000000..2343b30 --- /dev/null +++ b/miniapp/pages/recipe-list/recipe-list.wxss @@ -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; +} diff --git a/miniapp/pages/records/records.js b/miniapp/pages/records/records.js new file mode 100644 index 0000000..6cbe66a --- /dev/null +++ b/miniapp/pages/records/records.js @@ -0,0 +1,8 @@ +Page({ + data: { items: [] }, + onShow() { this.loadRecords(); }, + loadRecords() { + const history = wx.getStorageSync('box_history') || []; + this.setData({ items: history.reverse() }); + } +}); diff --git a/miniapp/pages/records/records.json b/miniapp/pages/records/records.json new file mode 100644 index 0000000..5e7faaa --- /dev/null +++ b/miniapp/pages/records/records.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "开盒记录" +} diff --git a/miniapp/pages/records/records.wxml b/miniapp/pages/records/records.wxml new file mode 100644 index 0000000..19a4838 --- /dev/null +++ b/miniapp/pages/records/records.wxml @@ -0,0 +1,18 @@ + + + 📜 + 还没有开盒记录 + 去首页开个盲盒吧 + + + + + {{item.icon}} + + {{item.name}} + {{item.time}} + + {{item.typeName}} + + + diff --git a/miniapp/pages/records/records.wxss b/miniapp/pages/records/records.wxss new file mode 100644 index 0000000..09d3ca6 --- /dev/null +++ b/miniapp/pages/records/records.wxss @@ -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; +} diff --git a/miniapp/pages/takeout-result/takeout-result.js b/miniapp/pages/takeout-result/takeout-result.js new file mode 100644 index 0000000..56eb384 --- /dev/null +++ b/miniapp/pages/takeout-result/takeout-result.js @@ -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); + } +}); diff --git a/miniapp/pages/takeout-result/takeout-result.json b/miniapp/pages/takeout-result/takeout-result.json new file mode 100644 index 0000000..b729a3a --- /dev/null +++ b/miniapp/pages/takeout-result/takeout-result.json @@ -0,0 +1,7 @@ +{ + "usingComponents": { + "result-card": "/components/result-card/result-card", + "box-animation": "/components/box-animation/box-animation" + }, + "navigationBarTitleText": "外卖盲盒" +} diff --git a/miniapp/pages/takeout-result/takeout-result.wxml b/miniapp/pages/takeout-result/takeout-result.wxml new file mode 100644 index 0000000..78194c1 --- /dev/null +++ b/miniapp/pages/takeout-result/takeout-result.wxml @@ -0,0 +1,41 @@ + + + + 😿 + {{error}} + + + + + + 🎉 你抽到的外卖是… + + + + 再开一个 + + + + + + + 🛵 + {{result.name}} + ⭐ {{result.rating}} + + + diff --git a/miniapp/pages/takeout-result/takeout-result.wxss b/miniapp/pages/takeout-result/takeout-result.wxss new file mode 100644 index 0000000..41a9873 --- /dev/null +++ b/miniapp/pages/takeout-result/takeout-result.wxss @@ -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; +} diff --git a/miniapp/project.config.json b/miniapp/project.config.json new file mode 100644 index 0000000..e40a6ea --- /dev/null +++ b/miniapp/project.config.json @@ -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 + } +} \ No newline at end of file diff --git a/miniapp/sitemap.json b/miniapp/sitemap.json new file mode 100644 index 0000000..9c15760 --- /dev/null +++ b/miniapp/sitemap.json @@ -0,0 +1,6 @@ +{ + "rules": [{ + "action": "allow", + "page": "*" + }] +} diff --git a/miniapp/static/tab-home-active.png b/miniapp/static/tab-home-active.png new file mode 100644 index 0000000..2e8beb5 Binary files /dev/null and b/miniapp/static/tab-home-active.png differ diff --git a/miniapp/static/tab-home.png b/miniapp/static/tab-home.png new file mode 100644 index 0000000..9b023ab Binary files /dev/null and b/miniapp/static/tab-home.png differ diff --git a/miniapp/static/tab-mine-active.png b/miniapp/static/tab-mine-active.png new file mode 100644 index 0000000..b0149b8 Binary files /dev/null and b/miniapp/static/tab-mine-active.png differ diff --git a/miniapp/static/tab-mine.png b/miniapp/static/tab-mine.png new file mode 100644 index 0000000..b20c28f Binary files /dev/null and b/miniapp/static/tab-mine.png differ diff --git a/miniapp/static/tab-records-active.png b/miniapp/static/tab-records-active.png new file mode 100644 index 0000000..3aeb335 Binary files /dev/null and b/miniapp/static/tab-records-active.png differ diff --git a/miniapp/static/tab-records.png b/miniapp/static/tab-records.png new file mode 100644 index 0000000..e422ca3 Binary files /dev/null and b/miniapp/static/tab-records.png differ diff --git a/miniapp/utils/api.js b/miniapp/utils/api.js new file mode 100644 index 0000000..1f3a027 --- /dev/null +++ b/miniapp/utils/api.js @@ -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 +}; diff --git a/miniapp/utils/location.js b/miniapp/utils/location.js new file mode 100644 index 0000000..ab182eb --- /dev/null +++ b/miniapp/utils/location.js @@ -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 }; diff --git a/miniapp/utils/storage.js b/miniapp/utils/storage.js new file mode 100644 index 0000000..0f4c69e --- /dev/null +++ b/miniapp/utils/storage.js @@ -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 };