├── .eslintrc.js
├── .github
└── workflows
│ └── preview.yml
├── .gitignore
├── .gitmodules
├── .vscode
└── launch.json
├── LICENSE
├── README.md
├── assets
├── countdown.jpg
├── detail-actions.jpg
├── home.jpg
├── qrcode.png
├── starred.jpg
└── v2
│ ├── home.jpg
│ ├── image.jpg
│ ├── mystar.jpg
│ ├── preview.gif
│ ├── star.jpg
│ └── timber.jpg
├── cloudfunctionTemplate
└── sendSubscribe.json
├── cloudfunctions
├── addSubscribe
│ ├── index.js
│ └── package.json
├── getCookbook
│ ├── index.js
│ └── package.json
├── saveUserinfo
│ ├── index.js
│ └── package.json
├── sendSubscribe
│ ├── config.json
│ ├── index.js
│ └── package.json
├── setCookbook
│ ├── index.js
│ └── package.json
└── updateViews
│ ├── index.js
│ └── package.json
├── data-v1.js
├── data-v1.json
├── data.json
├── miniprogram
├── app.js
├── app.json
├── app.less
├── assets
│ └── images
│ │ ├── aquatic.png
│ │ ├── breakfast.png
│ │ ├── condiment.png
│ │ ├── dessert.png
│ │ ├── drink.png
│ │ ├── home-cooking.png
│ │ ├── meat_dish.png
│ │ ├── semi-finished.png
│ │ ├── semi-finished1.png
│ │ ├── soup.png
│ │ ├── staple.png
│ │ └── vegetable_dish.png
├── components
│ └── pull-down-list
│ │ ├── index.js
│ │ ├── index.json
│ │ ├── index.less
│ │ └── index.wxml
├── config
│ └── index.js
├── custom-tab-bar
│ ├── index.js
│ ├── index.json
│ ├── index.less
│ └── index.wxml
├── data-v1.js
├── data-v2.json
├── data.js
├── package.json
├── pages
│ ├── changelog
│ │ ├── detail.js
│ │ ├── detail.json
│ │ ├── detail.wxml
│ │ ├── detail.wxss
│ │ ├── index.js
│ │ ├── index.json
│ │ ├── index.wxml
│ │ ├── index.wxss
│ │ └── log.js
│ ├── detail
│ │ ├── index.js
│ │ ├── index.json
│ │ ├── index.less
│ │ └── index.wxml
│ ├── index
│ │ ├── index.js
│ │ ├── index.json
│ │ ├── index.less
│ │ └── index.wxml
│ ├── kitchen
│ │ ├── index.js
│ │ ├── index.json
│ │ ├── index.less
│ │ ├── index.wxml
│ │ └── supported.js
│ ├── learn
│ │ ├── data.js
│ │ ├── detail.js
│ │ ├── detail.json
│ │ ├── detail.less
│ │ ├── detail.wxml
│ │ ├── index.js
│ │ ├── index.json
│ │ ├── index.less
│ │ └── index.wxml
│ ├── my
│ │ ├── index.js
│ │ ├── index.json
│ │ ├── index.less
│ │ └── index.wxml
│ ├── myStarred
│ │ ├── index.js
│ │ ├── index.json
│ │ ├── index.less
│ │ └── index.wxml
│ └── search
│ │ ├── index.js
│ │ ├── index.json
│ │ ├── index.less
│ │ └── index.wxml
├── project.private.config.json
├── sitemap.json
├── templates
│ ├── base.wxml
│ ├── hyperText.wxml
│ └── tabbar.wxml
└── utils
│ ├── ad.js
│ ├── index.js
│ └── request.js
├── package.json
├── project.config.json
├── project.private.config.json
├── retry.json
├── script
├── emoji
│ ├── data.json
│ └── download-image.js
├── gen-tips.js
├── helper.js
├── index.js
└── upload.js
└── sitemap.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Eslint config file
3 | * Documentation: https://eslint.org/docs/user-guide/configuring/
4 | * Install the Eslint extension before using this feature.
5 | */
6 | module.exports = {
7 | env: {
8 | es6: true,
9 | browser: true,
10 | node: true,
11 | },
12 | ecmaFeatures: {
13 | modules: true,
14 | },
15 | parserOptions: {
16 | ecmaVersion: 2018,
17 | sourceType: 'module',
18 | },
19 | globals: {
20 | wx: true,
21 | App: true,
22 | Page: true,
23 | getCurrentPages: true,
24 | getApp: true,
25 | Component: true,
26 | requirePlugin: true,
27 | requireMiniProgram: true,
28 | },
29 | // extends: 'eslint:recommended',
30 | rules: {},
31 | }
32 |
--------------------------------------------------------------------------------
/.github/workflows/preview.yml:
--------------------------------------------------------------------------------
1 | name: PREVIEW
2 |
3 | on:
4 | issue_comment:
5 | types: [created]
6 |
7 | jobs:
8 | request-preview:
9 | runs-on: ubuntu-latest
10 | if: github.event_name == 'issue_comment' && github.event.issue.pull_request && github.event.comment.body == '预览'
11 | steps:
12 | - run: |
13 | timestamp=$(date +%s)
14 | echo "timestamp=${timestamp}" >> $GITHUB_OUTPUT
15 | id: time
16 | - name: echo event
17 | run: |
18 | echo $headref
19 | echo $headsha
20 | echo $baseref
21 | env:
22 | headref: ${{github.event.pull_request.head_ref}}
23 | headsha: ${{github.event.pull_request.head.sha}}
24 | baseref: ${{github.event.pull_request.base_ref}}
25 | - name: Previous Comment
26 | uses: thollander/actions-comment-pull-request@v2
27 | with:
28 | message: |
29 | 正在构建预览的二维码,请稍等...
30 | comment_tag: ${{steps.time.outputs.timestamp}}
31 | - uses: actions/checkout@v2
32 | - uses: actions/setup-node@v2
33 | with:
34 | node-version: '14.6.0'
35 | - run: cd miniprogram && npm install
36 | shell: bash
37 | - name: get preview qrcode
38 | id: preview
39 | uses: LeeJim/setup-miniprogram@main
40 | with:
41 | project_type: miniProgram
42 | action_type: preview
43 | need-pack-npm: true
44 | project_path: ./miniprogram
45 | version: ${{ github.ref_name }}
46 | es6: true
47 | es7: true
48 | minify: true
49 | env:
50 | MINI_APP_ID: ${{ secrets.APP_ID }}
51 | MINI_APP_PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
52 | - name: Upload qrcode to Tencent COS
53 | uses: LeeJim/tencent-cos-action@main
54 | id: cos
55 | with:
56 | secretId: ${{ secrets.TENCENT_COS_SECRET_ID }}
57 | secretKey: ${{ secrets.TENCENT_COS_SECRET_KEY }}
58 | bucket: mp-qrcode-1255404841
59 | region: ap-guangzhou
60 | content: ${{ steps.preview.outputs.preview-qrcode }}
61 | - name: Comment PR
62 | uses: thollander/actions-comment-pull-request@v2
63 | with:
64 | message: |
65 |
66 | comment_tag: ${{steps.time.outputs.timestamp}}
67 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | miniprogram_npm
3 | package-lock.json
4 | /config.js
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "HowToCook"]
2 | path = HowToCook
3 | url = git@github.com:Anduin2017/HowToCook.git
4 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "pwa-node",
9 | "request": "launch",
10 | "name": "Launch Program",
11 | "skipFiles": [
12 | "/**"
13 | ],
14 | "program": "${workspaceFolder}/script/index.js"
15 | }
16 | ]
17 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 lijun
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HowToCookOnMiniprogram
2 |
3 | 小程序版本 **程序员做菜指南**,将程序员精神贯彻到底
4 |
5 | 灵感和数据来源于 [HowToCook](https://github.com/Anduin2017/HowToCook)
6 |
7 | 
8 |
9 |
10 | ## 微创新
11 |
12 | ### 内嵌计时器 ⌛️
13 |
14 | 有些步骤会存在计时的需求,当前做到识别时间,并可以内嵌计时器
15 |
16 |
17 |
18 | ### 可视化分类 😊
19 |
20 | 简洁、美观
21 |
22 |
23 |
24 | ### 收藏 & 点赞 👍
25 |
26 | 方便快速找回常用的菜谱
27 |
28 |
29 |
30 |
31 |
32 |
33 | ### 图片 & 链接 🔗
34 |
35 | 支持图片预览、链接跳转
36 |
37 | > 目前外部链接仅支持复制链接地址
38 |
39 |
40 |
41 | ### 直达买菜 🛒
42 |
43 | 收集了许多常用的买菜小程序,一键直达
44 |
45 |
46 |
47 |
48 | ## 如何贡献
49 |
50 | ### 启动
51 |
52 | 1. `cd miniprogram`
53 | 2. `npm install`
54 | 3. 开发者工具,点击菜单“工具 - 构建 npm”
55 |
56 | ### 小程序
57 |
58 | 源代码位于 miniprogram 目录
59 |
60 | ### 云开发
61 |
62 | 源代码位于 cloudfunctions 目录
63 |
64 | ### 数据解析
65 |
66 | - 解析 markdown 菜谱的脚本位于 `srcipt/index.js`
67 | - 对应命令: `npm run gen-data`
68 |
69 | 由于需要使用云开发 API 来上传图片,因此需要在根目录创建 `config.js`,提供如下配置:
70 |
71 | ```js
72 | module.exports = {
73 | appid: 'wx01462be634a0d447', // appid
74 | secret: '', // 小程序 secret;在微信公众平台获取
75 | cloudEnvId: '' // 云开发环境
76 | }
77 | ```
78 |
79 | ## 后续更新
80 |
81 | ### v1.0.0
82 |
83 | - [x] 增加个人收藏 `v0.2.0`
84 | - [x] 解析并展示 MarkDown 的图片、链接和各种格式 `v0.4.0`
85 | - [x] 增加【学习】模块 `v0.5.0`
86 | - [x] 发送订阅消息 `v1.0.0`
87 | - [x] 增加【买菜】模块 `v1.0.0`
88 |
89 | ### v2.0.0
90 |
91 | - [x] 支持搜索 `v1.1.0`
92 | - [ ] 支持个人分享 (可能受限个人小程序无法做到)
93 | - [x] 将微信云存储改成腾讯云存储 `v.1.1.1`
94 |
95 | ## 补充
96 |
97 | 组件库使用的是 [TDesign Miniprogram](https://github.com/Tencent/tdesign-miniprogram)
98 |
99 | 如果你觉得还不错的话,希望可以点个 star。感恩 🙏🙏
100 |
--------------------------------------------------------------------------------
/assets/countdown.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/assets/countdown.jpg
--------------------------------------------------------------------------------
/assets/detail-actions.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/assets/detail-actions.jpg
--------------------------------------------------------------------------------
/assets/home.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/assets/home.jpg
--------------------------------------------------------------------------------
/assets/qrcode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/assets/qrcode.png
--------------------------------------------------------------------------------
/assets/starred.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/assets/starred.jpg
--------------------------------------------------------------------------------
/assets/v2/home.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/assets/v2/home.jpg
--------------------------------------------------------------------------------
/assets/v2/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/assets/v2/image.jpg
--------------------------------------------------------------------------------
/assets/v2/mystar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/assets/v2/mystar.jpg
--------------------------------------------------------------------------------
/assets/v2/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/assets/v2/preview.gif
--------------------------------------------------------------------------------
/assets/v2/star.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/assets/v2/star.jpg
--------------------------------------------------------------------------------
/assets/v2/timber.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/assets/v2/timber.jpg
--------------------------------------------------------------------------------
/cloudfunctionTemplate/sendSubscribe.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "发布订阅消息",
4 | "value": {
5 | "version": "1.0.0",
6 | "templateId": "vjEDlUYrVJ05CauSw_V9jIWF-okt3OMCBtlz9yvjrfg",
7 | "content": "LOGO全新升级;新增学习模块"
8 | }
9 | }
10 | ]
--------------------------------------------------------------------------------
/cloudfunctions/addSubscribe/index.js:
--------------------------------------------------------------------------------
1 | const cloud = require('wx-server-sdk')
2 |
3 | cloud.init()
4 | const db = cloud.database()
5 |
6 | exports.main = async (event) => {
7 | const { FromUserName, CreateTime, MsgType, Event, List } = event
8 | // 消息订阅
9 | if (MsgType == 'event' && Event == 'subscribe_msg_popup_event') {
10 | let list = []
11 | if (Array.isArray(List)) {
12 | list = List.filter(item => item.SubscribeStatusString == 'accept')
13 | } else if (List.SubscribeStatusString == 'accept') {
14 | list = [List]
15 | }
16 |
17 | if (list.length > 0) {
18 | const { result } = await db.collection('subscribe').add({
19 | data: {
20 | status: 1,
21 | list,
22 | creator: FromUserName,
23 | createTime: CreateTime,
24 | }
25 | })
26 | return result
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/cloudfunctions/addSubscribe/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "addSubscribe",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "wx-server-sdk": "~2.5.3"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/cloudfunctions/getCookbook/index.js:
--------------------------------------------------------------------------------
1 | const cloud = require('wx-server-sdk')
2 |
3 | cloud.init()
4 | const db = cloud.database()
5 | const _ = db.command
6 |
7 | // 云函数入口函数
8 | exports.main = async (event) => {
9 | const { id, kind = 'star', current = 0 } = event
10 | const { OPENID } = cloud.getWXContext()
11 | const size = 10
12 |
13 | if (id) {
14 | const { data } = await db.collection('cookbook').where({ id }).get()
15 | if (data.length) {
16 | const [cookbook] = data;
17 | const { starreds = [], likeds = []} = cookbook;
18 | const starred = !!starreds && starreds.some(item => item.creator == OPENID && !item.isDel)
19 | const liked = !!likeds && likeds.some(item => item.creator == OPENID && !item.isDel);
20 |
21 | return {
22 | errno: 0,
23 | errmsg: '',
24 | data: {
25 | cookbook,
26 | starreds: starreds.filter(item => !item.isDel).length,
27 | starred,
28 | likeds: likeds.filter(item => !item.isDel).length,
29 | liked,
30 | }
31 | }
32 | }
33 | }
34 |
35 | const { data } = await db.collection('cookbook').where({
36 | [`${kind}s`]: _.elemMatch({
37 | creator: _.eq(OPENID),
38 | isDel: _.eq(false)
39 | })
40 | }).get()
41 | // }).limit(size).skip(current * size).get()
42 |
43 | data.sort((a, b) => {
44 | const atime = a[`${kind}s`].find(item => item.creator == OPENID)
45 | const btime = b[`${kind}s`].find(item => item.creator == OPENID)
46 |
47 | return btime.updateTime - atime.updateTime
48 | })
49 |
50 | return {
51 | errno: 0,
52 | errmsg: '',
53 | data,
54 | }
55 | }
--------------------------------------------------------------------------------
/cloudfunctions/getCookbook/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "getCookbook",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "wx-server-sdk": "~2.5.3"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/cloudfunctions/saveUserinfo/index.js:
--------------------------------------------------------------------------------
1 | const cloud = require('wx-server-sdk')
2 |
3 | cloud.init()
4 | const db = cloud.database()
5 |
6 | // 云函数入口函数
7 | exports.main = async (event) => {
8 | const { OPENID: openid } = cloud.getWXContext()
9 | const table = db.collection('user_info')
10 |
11 | const { data } = await table.where({ openid }).get()
12 |
13 | if (data.length > 0) return { errno: 0, errmsg: 'duplicated user' }
14 |
15 | await table.add({
16 | data: {
17 | ...event,
18 | openid,
19 | createTime: new Date()
20 | }
21 | })
22 |
23 | return { errno: 0, errmsg: 'ok' }
24 | }
--------------------------------------------------------------------------------
/cloudfunctions/saveUserinfo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "saveUserinfo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "wx-server-sdk": "~2.5.3"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/cloudfunctions/sendSubscribe/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "permissions": {
3 | "openapi": [
4 | "subscribeMessage.send"
5 | ]
6 | }
7 | }
--------------------------------------------------------------------------------
/cloudfunctions/sendSubscribe/index.js:
--------------------------------------------------------------------------------
1 | // 云函数入口文件
2 | const cloud = require('wx-server-sdk')
3 | const dayjs = require('dayjs')
4 |
5 | cloud.init()
6 | const db = cloud.database()
7 | const _ = db.command
8 |
9 | const SIZE = 100 // 分片读数据的大小
10 | const MAX = 1000 // 限制发送人数
11 |
12 | const sendSubscribeMessage = async ({ openid, templateId, version, content }) => {
13 | const time = dayjs().format('YYYY年M月D日 HH:mm')
14 | const date = dayjs().format('YYYY年M月D日')
15 | const dataMap = {
16 | 'vjEDlUYrVJ05CauSw_V9jIWF-okt3OMCBtlz9yvjrfg': {
17 | character_string1: {
18 | value: version,
19 | },
20 | time2: {
21 | value: time,
22 | },
23 | thing3: {
24 | value: content,
25 | },
26 | },
27 | 'Sbtj4X4gIKWRy0xDeWU8xCl8LejbTpIQ3gWiKh5JFp4': {
28 | thing1: {
29 | value: content,
30 | },
31 | date2: {
32 | value: date,
33 | },
34 | }
35 | }
36 |
37 | const sendResult = await cloud.openapi.subscribeMessage.send({
38 | touser: openid,
39 | templateId,
40 | page: '/pages/index/index',
41 | data: dataMap[templateId]
42 | })
43 |
44 | if (sendResult.errCode != 0) {
45 | console.log(sendResult.errMsg)
46 | }
47 |
48 | return sendResult.errCode == 0
49 | }
50 |
51 | const getSubscriber = async (skip, condtion) => {
52 | const { data } = await db.collection('subscribe')
53 | .where({
54 | list: {
55 | ...condtion,
56 | status: _.neq(0)
57 | }
58 | })
59 | .skip(skip)
60 | .limit(SIZE)
61 | .get()
62 |
63 | return data
64 | }
65 |
66 | const setSubscriber = async (item, templateId) => {
67 | const { list } = item;
68 | const target = list.find(item => item.TemplateId == templateId)
69 |
70 | if (target) {
71 | target.status = 0
72 | const { stats } = await db.collection('subscribe').doc(item._id)
73 | .update({
74 | data: {
75 | list
76 | }
77 | })
78 | return stats.updated == 1
79 | }
80 | return false
81 | }
82 |
83 | // 云函数入口函数
84 | // eslint-disable-next-line
85 | exports.main = async (event) => {
86 | let { version, content, templateId } = event;
87 | let subscribers = []
88 | let count = 0
89 | const condtion = {
90 | TemplateId: templateId
91 | }
92 | let finish = 0
93 | let uniqueIds = new Set()
94 | const afterFinish = () => {
95 | // 保存发送记录
96 | db.collection('subscribe_send_log').add({
97 | data: {
98 | creatTime: new Date(),
99 | member: Array.from(uniqueIds),
100 | templateId,
101 | version,
102 | content
103 | }
104 | })
105 | return { count, finish, uniqueIds }
106 | }
107 |
108 | do {
109 | subscribers = await getSubscriber(count, condtion)
110 | count += subscribers.length
111 | for (let item of subscribers) {
112 | const { creator: openid } = item;
113 | if (uniqueIds.has(openid)) continue // subscribers 去重
114 | uniqueIds.add(openid)
115 | const hasSent = await sendSubscribeMessage({
116 | openid,
117 | templateId,
118 | content,
119 | version
120 | })
121 | if (hasSent) {
122 | const done = await setSubscriber(item, templateId)
123 | if (done) {
124 | finish++
125 | }
126 | }
127 | if (finish >= MAX) return afterFinish() // 此处使用 break 的话,第一次 do 未进循环,无法中断;
128 | }
129 | } while (subscribers.length == SIZE)
130 |
131 | return afterFinish()
132 | }
133 |
--------------------------------------------------------------------------------
/cloudfunctions/sendSubscribe/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sendSubscribe",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "dayjs": "^1.10.8",
13 | "wx-server-sdk": "~2.5.3"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/cloudfunctions/setCookbook/index.js:
--------------------------------------------------------------------------------
1 | const cloud = require('wx-server-sdk')
2 | cloud.init()
3 | const db = cloud.database()
4 | const _ = db.command
5 | const collection = db.collection('cookbook')
6 |
7 | // 云函数入口函数
8 | exports.main = async (event) => {
9 | const { id, type } = event
10 | const { OPENID } = cloud.getWXContext()
11 | const kind = `${type}s`;
12 |
13 | const { data } = await collection.where({ id }).get()
14 | const addToSet = async (cookbook) => {
15 | const { stats } = await collection.where({
16 | id
17 | }).update({
18 | data: {
19 | [`${type}s`]: _.addToSet({
20 | creator: OPENID,
21 | createTime: new Date(),
22 | updateTime: new Date(),
23 | isDel: false
24 | })
25 | }
26 | })
27 | return {
28 | errno: 0,
29 | errmsg: '',
30 | data: {
31 | [type]: stats.updated ? true : false,
32 | likeds: type == 'liked' ? 1 : (cookbook.likeds ? cookbook.likeds.filter(item => !item.isDel).length : 0)
33 | }
34 | }
35 | }
36 |
37 | if (data.length) {
38 | const [ cookbook ] = data;
39 | if (cookbook[kind]) {
40 | const target = cookbook[kind].find(item => item.creator == OPENID)
41 | if (target) {
42 | target.isDel = !target.isDel
43 | target.updateTime = new Date()
44 | await collection.doc(cookbook._id).update({ data: {
45 | [kind]: cookbook[kind]
46 | } })
47 | const likeds = cookbook.likeds ? cookbook.likeds.filter(item => !item.isDel).length : 0;
48 |
49 | return {
50 | errno: 0,
51 | errmsg: '',
52 | data: {
53 | [type]: !target.isDel,
54 | likeds
55 | }
56 | }
57 | } else {
58 | return await addToSet(cookbook)
59 | }
60 | } else {
61 | return await addToSet(cookbook)
62 | }
63 | }
64 |
65 | return null
66 | }
--------------------------------------------------------------------------------
/cloudfunctions/setCookbook/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "setCookbook",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "wx-server-sdk": "~2.5.3"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/cloudfunctions/updateViews/index.js:
--------------------------------------------------------------------------------
1 | const cloud = require('wx-server-sdk')
2 | cloud.init()
3 | const db = cloud.database()
4 | const _ = db.command
5 | const collection = db.collection('views')
6 |
7 | // 云函数入口函数
8 | exports.main = async (event) => {
9 | const { id, type = 'cookbook' } = event;
10 | const { stats } = await collection.where({ id }).update({
11 | data: {
12 | views: _.inc(1)
13 | }
14 | })
15 | if (stats.updated !== 1) {
16 | await collection.add({
17 | data: {
18 | id,
19 | createTime: new Date(),
20 | views: 1,
21 | type
22 | }
23 | })
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/cloudfunctions/updateViews/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "updateViews",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "wx-server-sdk": "~2.5.3"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/miniprogram/app.js:
--------------------------------------------------------------------------------
1 | import { post } from './utils/request';
2 |
3 | App({
4 | onLaunch() {
5 | const updateManager = wx.getUpdateManager()
6 |
7 | wx.login({
8 | success: ({ code }) => {
9 | post('miniprogram/login', { code }).then(data => {
10 | wx.setStorageSync('token', data.key)
11 | }).catch(err => {
12 | console.error(err);
13 | })
14 | },
15 | })
16 |
17 | updateManager.onCheckForUpdate(function (res) {
18 | // console.log(res.hasUpdate)
19 | })
20 |
21 | updateManager.onUpdateReady(function () {
22 | wx.showModal({
23 | title: '更新提示',
24 | content: '新版本已经准备好,是否重启应用?',
25 | success(res) {
26 | if (res.confirm) {
27 | updateManager.applyUpdate()
28 | }
29 | }
30 | })
31 | })
32 | },
33 | globalData: {
34 | userInfo: null
35 | }
36 | })
37 |
--------------------------------------------------------------------------------
/miniprogram/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": [
3 | "pages/index/index",
4 | "pages/kitchen/index",
5 | "pages/detail/index",
6 | "pages/my/index",
7 | "pages/myStarred/index",
8 | "pages/changelog/index",
9 | "pages/changelog/detail",
10 | "pages/learn/index",
11 | "pages/learn/detail",
12 | "pages/search/index"
13 | ],
14 | "tabBar": {
15 | "custom": true,
16 | "color": "#000",
17 | "selectedColor": "#000",
18 | "backgroundColor": "#fff",
19 | "list": [{
20 | "pagePath": "pages/index/index",
21 | "text": "首页"
22 | }, {
23 | "pagePath": "pages/learn/index",
24 | "text": "技巧"
25 | }, {
26 | "pagePath": "pages/kitchen/index",
27 | "text": "表情"
28 | }, {
29 | "pagePath": "pages/my/index",
30 | "text": "我的"
31 | }]
32 | },
33 | "window": {
34 | "backgroundColor": "#f6f6f6",
35 | "backgroundTextStyle": "light",
36 | "navigationBarBackgroundColor": "#fff",
37 | "navigationBarTitleText": "程序员做饭指南",
38 | "navigationBarTextStyle": "black"
39 | },
40 | "sitemapLocation": "sitemap.json",
41 | "usingComponents": {
42 | "list": "./components/pull-down-list/index",
43 | "t-button": "tdesign-miniprogram/button/button",
44 | "t-cell": "tdesign-miniprogram/cell/cell",
45 | "t-count-down": "tdesign-miniprogram/count-down/count-down",
46 | "t-dialog": "tdesign-miniprogram/dialog/dialog",
47 | "t-empty": "tdesign-miniprogram/empty/empty",
48 | "t-icon": "tdesign-miniprogram/icon/icon",
49 | "t-toast": "tdesign-miniprogram/toast/toast",
50 | "t-tag": "tdesign-miniprogram/tag/tag",
51 | "t-tab-bar": "tdesign-miniprogram/tab-bar/tab-bar",
52 | "t-tab-bar-item": "tdesign-miniprogram/tab-bar-item/tab-bar-item",
53 | "t-tabs": "tdesign-miniprogram/tabs/tabs",
54 | "t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel",
55 | "t-loading": "tdesign-miniprogram/loading/loading",
56 | "t-message": "tdesign-miniprogram/message/message",
57 | "t-navbar": "tdesign-miniprogram/navbar/navbar",
58 | "t-search": "tdesign-miniprogram/search/search",
59 | "t-switch": "tdesign-miniprogram/switch/switch",
60 | "t-popup": "tdesign-miniprogram/popup/popup",
61 | "t-image": "tdesign-miniprogram/image/image",
62 | "t-steps": "tdesign-miniprogram/steps/steps",
63 | "t-step": "tdesign-miniprogram/step-item/step-item"
64 | }
65 | }
--------------------------------------------------------------------------------
/miniprogram/app.less:
--------------------------------------------------------------------------------
1 | page {
2 | width: 100%;
3 | box-sizing: border-box;
4 | background-color: #f6f6f6;
5 | }
6 |
7 | .container {
8 | height: 100%;
9 | box-sizing: border-box;
10 | }
11 |
12 | .list {
13 | padding-left: 10rpx;
14 |
15 | .item {
16 | position: relative;
17 | line-height: 40rpx;
18 | padding-left: 28rpx;
19 |
20 | &:before {
21 | content: '';
22 | position: absolute;
23 | top: 15rpx;
24 | left: 0;
25 | display: block;
26 | width: 12rpx;
27 | height: 12rpx;
28 | border-radius: 50%;
29 | border: 4rpx solid #555;
30 | box-sizing: border-box;
31 | }
32 |
33 | + .item {
34 | margin-top: 16rpx;
35 | }
36 | }
37 |
38 | &--inner {
39 | margin: 24rpx 0;
40 | }
41 | }
42 |
43 | .text {
44 |
45 | &--bold {
46 | font-weight: bolder;
47 | }
48 |
49 | &--code {
50 | background: #e6eefb;
51 | color: #0052d9;
52 | padding: 0 4px;
53 | border-radius: 4px;
54 | }
55 |
56 | &--image {
57 | font-family: 't';
58 | color: #333;
59 | text-decoration: underline;
60 |
61 | &::before {
62 | content: '\E074';
63 | }
64 | }
65 |
66 | &--link {
67 | font-family: 't';
68 | text-decoration: underline;
69 |
70 | &:before {
71 | content: '\E07C';
72 | }
73 | }
74 |
75 | &--page {
76 | font-family: 't';
77 | text-decoration: underline;
78 |
79 | &:before {
80 | content: '\E058';
81 | }
82 | }
83 |
84 | &--quote {
85 | display: block;
86 | padding: 12rpx;
87 | font-size: 28rpx;
88 | color: #0052d9;
89 | background-color: #e6eefb;
90 | opacity: .7;
91 | border-radius: 8rpx;
92 | }
93 | }
--------------------------------------------------------------------------------
/miniprogram/assets/images/aquatic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/miniprogram/assets/images/aquatic.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/breakfast.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/miniprogram/assets/images/breakfast.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/condiment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/miniprogram/assets/images/condiment.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/dessert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/miniprogram/assets/images/dessert.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/drink.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/miniprogram/assets/images/drink.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/home-cooking.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/miniprogram/assets/images/home-cooking.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/meat_dish.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/miniprogram/assets/images/meat_dish.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/semi-finished.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/miniprogram/assets/images/semi-finished.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/semi-finished1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/miniprogram/assets/images/semi-finished1.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/soup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/miniprogram/assets/images/soup.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/staple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/miniprogram/assets/images/staple.png
--------------------------------------------------------------------------------
/miniprogram/assets/images/vegetable_dish.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeeJim/HowToCookOnMiniprogram/87968553dcf3ac8b3416e70166298cae22c566fc/miniprogram/assets/images/vegetable_dish.png
--------------------------------------------------------------------------------
/miniprogram/components/pull-down-list/index.js:
--------------------------------------------------------------------------------
1 | const itemHeight = 56 * 2;
2 |
3 | Component({
4 | data: {
5 | childBoxHeight: 0,
6 | },
7 | externalClasses: ['t-class'],
8 | properties: {
9 | defaultOpen: {
10 | type: Boolean,
11 | value: false,
12 | },
13 | name: {
14 | type: String,
15 | value: '',
16 | },
17 | icon: {
18 | type: String,
19 | value: '',
20 | },
21 | list: {
22 | type: Array,
23 | value: [],
24 | observer(list) {
25 | this.setData({
26 | childBoxHeight: this.data.defaultOpen ? itemHeight * list.length : 0,
27 | });
28 | },
29 | },
30 | },
31 | methods: {
32 | switchHandle() {
33 | const { list, childBoxHeight } = this.data;
34 | this.setData({
35 | childBoxHeight: childBoxHeight > 0 ? 0 : list.length * itemHeight,
36 | });
37 | },
38 | handleTap(e) {
39 | this.triggerEvent('click', e.target.dataset);
40 | },
41 | },
42 | });
43 |
--------------------------------------------------------------------------------
/miniprogram/components/pull-down-list/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "component": true
3 | }
--------------------------------------------------------------------------------
/miniprogram/components/pull-down-list/index.less:
--------------------------------------------------------------------------------
1 | .pullDownList {
2 | width: 100%;
3 | box-sizing: border-box;
4 | background-color: #fff;
5 | border-radius: 8rpx;
6 | margin-bottom: 12rpx * 2;
7 | overflow: hidden;
8 |
9 | .switchBox {
10 | height: 60rpx * 2;
11 | display: flex;
12 | justify-content: space-between;
13 | align-items: center;
14 | padding: 0 16rpx * 2;
15 | font-size: 16rpx * 2;
16 | line-height: 24rpx * 2;
17 | color: #333;
18 | }
19 |
20 | .name,
21 | .icon {
22 | transition: opacity .3s;
23 | }
24 |
25 | &.actived {
26 | .name {
27 | opacity: .5;
28 | }
29 |
30 | .icon {
31 | opacity: .6;
32 | }
33 | }
34 |
35 | .icon {
36 | width: 24rpx * 2;
37 | height: 24rpx * 2;
38 | }
39 |
40 | .childBox {
41 | transition: height .3s;
42 |
43 | .child {
44 | box-sizing: border-box;
45 | border-bottom: 1rpx solid #e5e5e5;
46 | height: 56rpx * 2;
47 | display: flex;
48 | justify-content: space-between;
49 | align-items: center;
50 | margin-left: 16rpx * 2;
51 | margin-right: 16rpx * 2;
52 | font-size: 16rpx * 2;
53 |
54 | &:last-of-type {
55 | border-bottom-color: transparent;
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/miniprogram/components/pull-down-list/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ name }}
4 |
5 |
6 |
7 |
14 | {{ item.name }} {{ item.label }}
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/miniprogram/config/index.js:
--------------------------------------------------------------------------------
1 | export const chineseMap = {
2 | breakfast: '早餐',
3 | condiment: '佐料',
4 | dessert: '甜品',
5 | drink: '饮品',
6 | 'home-cooking': '烹饪',
7 | 'semi-finished': '速食',
8 | 'soup': '汤',
9 | 'staple': '主食'
10 | }
11 |
12 | export const titleMap = {
13 | breakfast: '早餐',
14 | condiment: '佐料',
15 | dessert: '甜品',
16 | drink: '饮品',
17 | 'home-cooking': '烹饪',
18 | 'semi-finished': '速食',
19 | 'soup': '汤',
20 | 'staple': '主食',
21 | aquatic: '水产',
22 | 'vegetable_dish': '素菜',
23 | 'meat_dish': '荤菜'
24 | }
25 |
26 | export default {
27 | titleMap,
28 | chineseMap
29 | }
--------------------------------------------------------------------------------
/miniprogram/custom-tab-bar/index.js:
--------------------------------------------------------------------------------
1 |
2 | Component({
3 | data: {
4 | value: 'index',
5 | list: [{
6 | icon: 'home',
7 | value: 'index',
8 | label: '首页',
9 | },{
10 | icon: 'tips',
11 | value: 'learn',
12 | label: '发现',
13 | },{
14 | icon: 'logo-android',
15 | value: 'kitchen',
16 | label: '表情',
17 | }, {
18 | icon: 'user',
19 | value: 'my',
20 | label: '我的'
21 | }]
22 | },
23 | lifetimes: {
24 | ready() {
25 | const pages = getCurrentPages();
26 | const curPage = pages[pages.length - 1];
27 |
28 | if (curPage) {
29 | const nameRe = /pages\/(\w+)\/index/.exec(curPage.route);
30 |
31 | if (nameRe[1]) {
32 | this.setData({
33 | value: nameRe[1]
34 | })
35 | }
36 | }
37 | }
38 | },
39 | methods: {
40 | handleChange(e) {
41 | const { value } = e.detail;
42 |
43 | // this.setData({ value });
44 | wx.switchTab({
45 | url: `/pages/${value}/index`,
46 | })
47 | }
48 | }
49 | })
50 |
--------------------------------------------------------------------------------
/miniprogram/custom-tab-bar/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "component": true,
3 | "usingComponents": {
4 | "t-tab-bar": "tdesign-miniprogram/tab-bar/tab-bar",
5 | "t-tab-bar-item": "tdesign-miniprogram/tab-bar-item/tab-bar-item"
6 | }
7 | }
--------------------------------------------------------------------------------
/miniprogram/custom-tab-bar/index.less:
--------------------------------------------------------------------------------
1 | /* custom-tab-bar/index.wxss */
--------------------------------------------------------------------------------
/miniprogram/custom-tab-bar/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{item.label}}
4 |
5 |
--------------------------------------------------------------------------------
/miniprogram/data-v2.json:
--------------------------------------------------------------------------------
1 | {"no":0,"id":"1c92009b9638ee8f6124d3970b960e64","name":"咖喱炒蟹","category":"aquatic","detail":[{"text":"原料和工具","content":[{"type":"list","items":["青蟹(别称:肉蟹)","咖喱块(推介乐惠蟹黄咖喱)","洋葱","椰浆","鸡蛋","生粉(别称:淀粉)","大蒜"]}]},{"text":"计算","content":["每次制作前需要确定计划做几份。一份正好够 1 个人食用","总量:",{"type":"list","items":["肉蟹 1 只(大约 300g) * 份数","咖喱块 15g(一小块)*份数","椰浆 100ml*份数","鸡蛋 1 个 *份数","洋葱 200g *份数","大蒜 5 瓣 *份数"]}]},{"text":"操作","content":[{"type":"list","items":["肉蟹掀盖后对半砍开,蟹钳用刀背轻轻拍裂,切口和蟹钳蘸一下生粉,不要太多。撒 5g 生粉到蟹盖中,盖住蟹黄,备用","洋葱切成洋葱碎,备用","大蒜切碎,备用","烧一壶开水,备用","起锅烧油,倒入约 20ml 食用油,等待 10 秒让油温升高","将螃蟹切口朝下,轻轻放入锅中,煎 20 秒,这一步主要是封住蟹黄,蟹肉。然后翻面,每面煎 10 秒。煎完将螃蟹取出备用","将螃蟹盖放入锅中,使用勺子舀起锅中热油泼到蟹盖中,煎封住蟹盖中的蟹黄,煎 20 秒后取出备用","不用刷锅,再倒入 10ml 食用油,大火让油温升高至轻微冒烟,将大蒜末,洋葱碎倒入,炒 10 秒钟","将咖喱块放入锅中炒化(10 秒),放入煎好的螃蟹,翻炒均匀","倒入开水 300ml,焖煮 3 分钟。","焖煮完后,倒入椰浆和蛋清,关火,关火后不断翻炒,一直到酱汁变浓稠。","出锅"]}]},{"text":"附加内容","content":[{"type":"list","items":[["做法参考:",{"type":"link","text":"十几年澳门厨房佬教学挂汁的咖喱蟹怎么做","href":"https://www.bilibili.com/video/BV1Nq4y1W7K9"}]]},"如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":["第一次吃咖喱炒蟹是在泰国的建兴酒家中餐厅,爆肉的螃蟹挂满有蟹黄味道的咖喱,味道真的绝,喜欢吃海鲜的程序员绝对不能错过。操作简单,对沿海的程序员非常友好。"],"title":"咖喱炒蟹的做法"}
2 | {"no":1,"id":"4c1da71cdcbc9f07ca0704ae36551ebb","name":"小龙虾","category":"aquatic","detail":[{"text":"原料和工具","content":[{"type":"list","items":["小龙虾","油","香叶","八角","桂皮","青花椒","花椒","子弹头辣椒","葱姜蒜","郫县豆瓣","黄豆酱","啤酒","生抽","盐"]}]},{"text":"计算","content":["以下是两斤小龙虾的量,按比例调整就好。",{"type":"list","items":["小龙虾 = 2 斤","油 = 70 毫升(这是平时炒菜 3 倍量)","香叶 = 两片","八角 = 一个","桂皮 = 3 克","青花椒 = 10 克","花椒 = 10 克","子弹头辣椒 = 5 克","葱 = 一根大葱","姜 = 30 克","蒜 = 7 瓣大蒜","郫县豆瓣 = 30 克","黄豆酱 = 30 克","啤酒 = 500 毫升","生抽 = 30 毫升","盐 = 10 克"]}]},{"text":"操作","content":[{"type":"list","items":["小龙虾刷干净去虾线,葱切 2cm 葱段,姜蒜切末。","烧油,油微热, 下香叶、八角、桂皮、青花椒、花椒、子弹头辣椒。","香料出香气之后下锅葱姜蒜","葱姜蒜爆香后,加入郫县豆瓣、黄豆酱,炒出红油。","下小龙虾,翻炒至变色。","加入啤酒,等啤酒烧开后加入生抽,盐。","将小龙虾完全煮熟后出锅。"]}]},{"text":"附加内容","content":["饭店应该都是油炸一遍,家庭油炸的话,太浪费了,所以在这个菜谱里用油比炒菜油多一些煎一下,实测同样好吃。","去虾线后的虾肉比不去虾线的虾肉口感差一些,并且小龙虾去虾线对新手是一个挑战,能接受虾线的情况下不去虾线也可以。","如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":[{"type":"image","text":"成品","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpg"},"在家里做的小龙虾,肉质细嫩,鲜嫩多汁,干净卫生。"],"title":"小龙虾的做法"}
3 | {"no":2,"id":"8d62749ad4a93291a964d23b5e9ab7c6","name":"微波葱姜黑鳕鱼","category":"aquatic","detail":[{"text":"原料和工具","content":["原料:",{"type":"list","items":["黑鳕鱼,带皮"]},"调味料:",{"type":"list","items":["青葱","姜","料酒","酱油","芝麻油","花生油"]},"工具:",{"type":"list","items":["密封袋"]}]},{"text":"计算","content":["每 2 份:",{"type":"list","items":["黑鳕鱼,带皮,2 片,450g(本菜谱主角,所有调料可根据鳕鱼的实际重量进行比例调整)","青葱,葱白,25g。","青葱,葱绿,10g。","姜,13g。","料酒,5mL。","酱油,25mL。","芝麻油,2mL。","花生油,50mL。"]}]},{"text":"操作","content":[{"type":"list","items":["鱼片分别放入密封袋,鱼皮向下放在盘子中。","取葱白切丝 25g,姜去皮后切丝,10g,混合在一起后分成两半,分别放在袋内鱼片上。","每个袋子倒入 2.5mL 料酒。",["封好密封袋,放入微波炉中,中火(800 瓦)微波至",{"type":"em","text":"不透明且容易散开"},"时(约 3.5-5 分钟),从袋中取出鱼片。"],"去除青葱和姜。","取酱油 25mL,芝麻油 2mL,混合均匀后平均淋在两片鱼片上。","取葱绿切细丝 10g,姜去皮后切丝 3g,混合后分成两份撒在鱼片上。","取花生油 50mL,在小锅中加热至 190℃。","将热油淋到放油葱绿的鱼片上,立刻上桌。"]}]},{"text":"附加内容","content":[{"type":"heading","text":"使用海鲈鱼、罗非鱼、大比目鱼或者龙脷鱼"},"| 鱼类 | 是否切片 | 重量 | 微波时间 |\n| - | - | - | - |\n| 海鲈鱼 | 整条 | 450g | 6.5 分钟 |\n| 罗非鱼 | 整条 | 800g | 6 分钟 |\n| 大比目鱼 | 切片 | 170g | 2.25 分钟 |\n| 龙脷鱼 | 切片 | 170g | 1.5 分钟 |",{"type":"heading","text":"其他变化"},{"type":"list","items":["如果想让香气更为浓郁,在微波前可将葱姜与料酒均匀涂抹在鱼片的两侧,再进行微波加热。"]},"如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":["这道菜改编自西雅图 Veil 餐厅主厨 Johnny Zhu 的母亲 Margaret Lu 的菜谱。卢女士原菜谱是使用罗非鱼来做这道菜,Johnny 改为鳕鱼,但也可以用大比目鱼鱼排,或者海鲈鱼、鳟鱼等。每种鱼的密度有差别,烹饪时间要做微调。"],"title":"微波葱姜黑鳕鱼的做法"}
4 | {"no":3,"id":"1945f8c72fd0da53f003bab173be1750","name":"烤鱼","category":"aquatic","detail":[{"text":"原料和工具","content":[{"type":"list","items":["草鱼(农贸市场或者超市让店家杀掉,去除不要的器官)","大葱","料酒","白胡椒粉","食用盐","大蒜","桂皮","八角","香叶","青花椒","干辣椒段","灯笼椒","火锅底料(随意)","千张","绿豆芽","洋葱","豆瓣酱","芹菜段","熟花生米","白芝麻","香菜(放更好吃,根据个人口味可放可不放)"]}]},{"text":"计算","content":["每份:",{"type":"list","items":["草鱼 大约三斤","大葱 半根","食用油 20ml","料酒 10-15ml","食用盐 5-10g","白胡椒粉 5g-10g","桂皮 一小片","八角 两个","大蒜粒 八个","香叶 两张","青花椒 一小把","干辣椒段 10 个","灯笼椒 4 个","芹菜段 两根","洋葱 半个","千张 一张"]}]},{"text":"操作","content":[{"type":"list","items":[["草鱼(一般 3 斤 )从背部切开,两面沿着鱼的背部往下划几刀,不要划到鱼肚皮,不然不易定型",{"type":"list","items":["可以用热水浇在鱼身上洗去粘液或者用刷子在流动水龙头下面不停的刷洗,直到摸着没有黏糊糊的手感。"]}],"把鱼放到容器中,加入料酒,10g 白胡椒粉,5g 食盐抹匀腌制二十分钟入味。","把半根大葱切成一块一块,大蒜粒中间切开,和八角香叶桂皮放在一个容器中","干辣椒段中间一分为二切开并和灯笼椒装在一个容器中","芹菜切小段","豆芽焯水","千张焯水切成丝","洋葱切成丝。",["烤制鱼",{"type":"list","items":["家里有烤箱的可以在烤盘刷上底油,鱼皮朝下,直到烤制两面金黄,然后撒上孜然粉","如果没有烤箱,可以热锅热油,锅的两边撒上 2g 食盐,下入草鱼开始煎,刚下入的时候不要着急翻动,等一面定型后再翻面,煎制两面金黄,撒上孜然粉,出锅装在盘子里准备。"]}],"锅中撒上 20ml 食用油,等到油热后,把大葱大蒜八角香叶倒入炒香","加上一包火锅底料的一半和 15-20g 豆瓣酱,炒出红油","加入 5g 白糖,10g 食盐,5ml 生抽调味,倒入和食材齐平的清水煮开","依次下入芹菜段,豆芽,千张丝,不用煮熟,稍微烫一下后铺上洋葱丝,放上烤鱼","加入干辣椒,灯笼椒,青花椒","另一个锅烧油,油热后浇在刚加入的辣椒上面激发出香味","最后撒上熟花生米,葱花,白芝麻,香菜","煮 5-6 分钟,美味即成。"]},{"type":"image","text":"示例菜成品","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpg"}]},{"text":"附加内容","content":[{"type":"list","items":[["技术总结:上述",{"type":"list","items":["这道菜的食盐,胡椒粉,孜然粉,食用油,生抽,白糖等的使用量,根据个人口味和食材比例做决定,并不需要严格按照上述分量来做。"]}]]},"如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":[],"title":"烤鱼的做法"}
5 | {"no":4,"id":"c61374357150d4f154481d0f602c9afd","name":"清蒸生蚝","category":"aquatic","detail":[{"text":"原料和工具","content":[{"type":"list","items":["生蚝","葱","蒜","姜","酱油","刷子"]}]},{"text":"计算","content":[{"type":"list","items":["饮用水 1 升","生蚝 6 个","葱 3 颗","蒜 6 瓣","姜 1 小块","酱油 每个生蚝 1 ml"]}]},{"text":"操作","content":[{"type":"list","items":["将生蚝用刷子刷干净(没有刷子用牙刷)。","蒸锅中放水,将蒸屉放上之后,将 6 个生蚝平铺在蒸屉,使用 50%功率,蒸 3 分钟。","用右手拿着湿抹布掀开烫锅盖,将每个生蚝的外壳掀开一半去掉,生蚝的凸面向下,平面向上,每个放 1 根姜丝,10g 蒜末放到生蚝上。","关上烫锅盖,100%功率蒸 3.5 分钟。","停火,用右手拿着抹布掀开烫锅盖,每个放 5ml 酱油。","盛盘。"]}]},{"text":"附加内容","content":["如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":[],"title":"清蒸生蚝的做法"}
6 | {"no":5,"id":"dfb180812517038ef48220686bc7bf6c","name":"清蒸鲈鱼","category":"aquatic","detail":[{"text":"原料和工具","content":[{"type":"list","items":["鲈鱼(害怕杀鱼的同学可以让店家帮忙杀)","香葱","姜","食用油","蒸鱼豉油","料酒","食用盐"]}]},{"text":"计算","content":["每份:",{"type":"list","items":["鲈鱼 一条","香葱 三根","姜 一块","食用油 10-15ml","蒸鱼豉油 10-15ml","料酒 10-15ml","食用盐 5-10g"]}]},{"text":"操作","content":[{"type":"list","items":["姜切片切丝、香葱的葱白切段,葱绿切丝,切丝后放入冷水浸泡备用。","鲈鱼处理好后洗净,用厨房纸擦干,两面分别划几刀,用盐洗掉鱼身的粘液,并用 10g 盐抹遍鱼身的内外,腌制 10 分钟以上。","补充一个鲈鱼改刀和摆盘的方法,改刀后可以让鲈鱼立起来蒸,均匀受热,同时吃起来更加方便,无需翻面。",{"type":"image","text":"改刀","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpg"},{"type":"image","text":"摆盘","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpg"},"鱼肚内塞上姜和葱白,鱼身也撒上姜和葱白,量为备用的一半。蒸鱼的碟子用筷子将鱼跟碟子隔开蒸","水烧热感觉到水温后放进入鱼","大火清蒸 10 分钟。","蒸好的鱼,用干净的盘子装起来并去除身上姜蒜","鱼身浇上 15ml 蒸鱼豉油","鱼身重新撒上姜和葱丝,锅内加上 10ml 食用油并烧热,将食用油淋至鱼身即可出菜"]},{"type":"image","text":"示例菜成品","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpg"}]},{"text":"附加内容","content":["这道菜属于有手就行,关键点在于火候,鱼的大小跟火候都会相关,太久会导致鱼肉太老极度影响口感,太短会导致部分鱼肉没熟。所以大火蒸鱼一般是 10 分钟内较佳。","切记蒸鱼需要用筷子隔开装鱼的盘子,这样做的好处有两点:",{"type":"list","items":["1、鱼在蒸的过程中会将水滴到盘子,如果鱼直接接触会导致鱼食用时会腥。","2、能够将鱼均匀受热。"]},"这道菜难度系数简单,而且味道非常棒哦","如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":[],"title":"清蒸鲈鱼的做法"}
7 | {"no":6,"id":"9465ac50092d0b90efc8a1e041829347","name":"白灼虾","category":"aquatic","detail":[{"text":"原料和工具","content":[{"type":"list","items":["活虾","洋葱","姜","蒜","葱","食用油","酱油","料酒","芝麻","蚝油","香醋"]}]},{"text":"计算","content":["每次制作前需要确定计划做几份。一份正好够 1 个人食用","总量:",{"type":"list","items":["虾 250g * 份数(建议 1-2 人份)","葱 一根","姜 一块","洋葱 一头","蒜 5-8 瓣","食用油 10-15ml","料酒 20 ml","酱油 10-15ml","芝麻 一把","香醋 10 ml","蚝油 10 ml"]}]},{"text":"操作","content":[{"type":"list","items":["洋葱切小块,姜切片,平铺平底锅。","活虾冲洗一下(去除虾线、剪刀减掉虾腿虾须子都是可选操作),控水,铺在平底锅的洋葱、姜片之上。","倒入国内料酒,盖上锅盖,中火 1 分钟,小火 5 分钟,关火 5 分钟。",["和上一步并行操作,制作蘸料:",{"type":"list","items":["葱切成葱花、蒜切碎、倒入酱油、芝麻、香醋,搅拌之。","油烧热,淋入蘸料。"]}],"虾出锅,用干净的盘子装好。"]},{"type":"image","text":"白灼虾","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.webp"}]},{"text":"附加内容","content":[{"type":"heading","text":"技术细节"},{"type":"list","items":["开始不能大火、防止糊底。","如果锅盖有通气口、时间要相应调节一下(考虑增加 30 秒中火)。","蘸料其实也是可选的、也可以是纯的醋,大自然馈赠的鲜虾在没有水带走冲淡鲜甜的情况下口感味道都非常棒的。"]},"如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":["白灼虾非常适合程序员在沿海地区做,类似于清蒸鱼:简单容错、有营养、有满足感,甚至很好看。"],"title":"白灼虾的做法"}
8 | {"no":7,"id":"c213437d1ca248279ccbd1b1703b9347","name":"糖醋鲤鱼","category":"aquatic","detail":[{"text":"原料和工具","content":[{"type":"list","items":["鲤鱼","番茄酱","白糖","白醋","淀粉","盐","葱","姜","料酒","香菜一颗","盆(两个)","菜刀一个","笊篱一个、锅铲一个"]}]},{"text":"计算","content":["注意,该菜只有 3 人以上版本(过大或太小的鱼都是不合适的),所以不需要公式计算,特别适合家庭聚餐时食用,如年夜饭",{"type":"list","items":["鲤鱼 = 约 3 斤","清水 = 50g","番茄酱 = 40g","白糖 = 20g","白醋 = 10g","淀粉 = 10g","盐 = 30g","大葱 = 30g(约半颗)","姜 = 30g","料酒 = 25g"]}]},{"text":"操作","content":[{"type":"list","items":["将鱼清洗干净,确保无鱼鳞等异物","将鱼头朝左,鱼肚朝下,右手持刀。刀竖直切下 1cm,按紧鱼身往左片 3-4cm,再将鱼片中间轻轻划一刀","将鱼放进盆里,然后将大姜切片,大葱切段(随便切切就行了,主要是需要去腥味)","用吃奶的力气将大葱大姜里的汁水挤到盆中","加入 20g 盐,25g 料酒,然后给鲤鱼搓个澡,涂抹均匀",[{"type":"image","text":"腌制","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpg"},""," (腌鱼时间越长,鱼腥味就越小,推荐腌 30 分钟以上)"],"找个干净的盆,加入 100g 面粉、200g 淀粉、180g 水、5g 盐,用手将其搅拌均匀,面糊此时粘稠呈可拉丝状态,然后打入一个鸡蛋,再次搅匀","等待 30 分钟","将鱼放在案板上,用干毛巾将鱼身上的水擦干(这样可以更好的挂糊)","将盆冲洗干净,用干毛巾擦干","起锅烧油,加入约 1L 的油,将油温烧至 7 成热,约 200-240 度","捏起鱼的尾巴,将鱼头沉入锅底,用勺子往鱼的身上淋热油,待面糊成型后,将鱼慢慢放入锅中,拿锅铲轻轻铲起鱼的头部,然后垫上笊篱。防止底部炸糊。","准备一个盛鱼的盘子,放在锅的旁边。","用锅铲从鱼身处轻轻铲入,两个工具配合鱼翻个身。再炸两分钟,还是同样的方式(笊篱托着鱼头,锅铲托着鱼身,将鱼盛入盘中)","将锅中的油倒入擦干的盆中,放置一边,然后将锅刷干净","将 50g 清水、40g 番茄酱、20g 白糖、10g 白醋放入小碗中,搅拌均匀","再准备一个小碗加入 10g 淀粉、10g 水,搅拌成水淀粉","开大火将锅烧热,然后倒入之前准备的料汁,大火烧开,转小火","加入调好的水淀粉,边倒边搅拌,然后 20 秒后关火","将熬好的糖醋汁用勺子均匀地浇在鱼身上,可以加点香菜或葱花点缀,糖醋鲤鱼就做好了",[{"type":"image","text":"成品","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpg"},""," 这里的糖醋汁熬的有点稠了......"]]}]},{"text":"附加内容","content":["这道菜难度系数算中等吧,对新手还是不太友好的......","如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":[],"title":"糖醋鲤鱼的做法"}
9 | {"no":8,"id":"383669bf417e2f621b5521cf85fb853a","name":"红烧鱼头","category":"aquatic","detail":[{"text":"原料和工具","content":[{"type":"list","items":["注:如果有可能,尽量另准备一把菜刀,超市或市场上均有廉价且刀片更厚的菜刀,刀片厚度在 5-7mm 为最佳。","大葱、姜、大蒜、香菜、美人椒","油、盐、鸡精、生抽、老抽、陈醋、黑胡椒粉、料酒","八角、干辣椒","鱼头一个","注:市场直接贩卖的鱼头一般分为两种:白鲢、花鲢。前者价格便宜,后者价格略贵,但口感也更佳!"]}]},{"text":"计算","content":["注意,这道菜仅有足够 2 人食用的版本。",{"type":"list","items":["鱼头一个","大葱 200g","姜 80g","蒜瓣 3-4 个","美人椒 1/4 个","香菜 4 棵","八角两个,干辣椒五个"]}]},{"text":"操作","content":[{"type":"heading","text":"原材料准备"},{"type":"list","items":["葱、姜、蒜、香菜、美人椒分别清洗干净。","干辣椒与八角稍微冲洗即可。","大葱切两半。后半段大葱(葱白处)切段,每段长度约 4cm。前半段(葱叶处)先切段,再将每段劈为四瓣。","姜切片,每片厚度约 3mm。","大蒜拍碎。","拿出两棵香菜去根,切为 1.5cm 香菜碎。","将美人椒切为厚度为 3mm 的辣椒圈。","干辣椒切四段。"]},{"type":"heading","text":"腌制鱼头"},{"type":"list","items":["注:下文所述的鱼身是购买鱼头时所附带的鱼肉。","将鱼头去鳞,清洗鱼头处未被清理干净的内脏。","剁去鱼鳍、清理鱼鳃。",["将鱼头下巴与鱼身连接的地方剁开,鱼身剁块,鱼头剁成四/六瓣。",{"type":"list","items":["注:鱼的处理很难用文字完全表述,可以搜索鱼头处理相关视频。"]}],"将剁好的鱼头进行清洗,最好洗掉鱼块上滞留的血水。","将清洗好的鱼块放入盆中,加入 5g 盐、10g 生抽、10g 料酒。放入葱(前半段切碎的那个)、1/3 姜片。将其拌匀,静置 1-2 小时。"]},{"type":"heading","text":"最终步骤"},{"type":"list","items":["加入 30ml 油,等待锅热...",["油热,将锅关至小火",{"type":"list","items":[["如果不明白为何要这样做,请查看",{"type":"link","text":"学习炒与煎","href":"../../tips/learn/学习炒与煎.md"},"中的翻炒辅料。"]]}],"放入姜片,慢慢翻炒,以姜片中的大部分汁水被炒出,以金黄色为准。","放入葱段,翻炒至葱段略显发白。","放入蒜碎、八角、干辣椒,翻炒 5 秒。","将腌制好的鱼头倒入锅中,翻炒 2-3 分钟。","倒入 500ml 清水,加入 2g 盐、3g 鸡精、5g 生抽、3g 老抽、5g 料酒、2g 黑胡椒粉、3g 陈醋。","将两棵香菜放入锅中,盖上锅盖。","调至大火,将水烧开。","调至中火,慢焖入味。","当汤汁减少一半时,打开锅盖。","调至大火收汁,汤汁剩余 1/3 时,关火盛至小盆中。","注:将锅中的汤汁均匀淋到鱼头上,盛盘时可以将锅中煮的香菜放入小盆底部,这样能让成品菜好看又好吃。","将香菜放至已经盛出的鱼头上,把切好的美人椒圈放在香菜之上。","色香味俱全的红烧鱼头出炉!"]}]},{"text":"附加内容","content":["如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":[{"type":"list","items":[[{"type":"strong","text":"WARNING"}," 如果没有使用过菜刀剁过肉类食物,那么并不推荐使用该菜单!!!"],"在操作中,锋利的菜刀可能会划伤手指,请一定要小心。"]}],"title":"红烧鱼头的做法"}
10 | {"no":9,"id":"4b2beca30fbffdf3a4efc85a049c2cd8","name":"红烧鲤鱼","category":"aquatic","detail":[{"text":"原料和工具","content":[{"type":"list","items":["大葱、姜、大蒜、干辣椒","油、盐、生抽、老抽、陈醋、蚝油、料酒、白糖","鲤鱼、五花肉"]}]},{"text":"计算","content":[" 2 人食用的版本。",{"type":"list","items":["鲤鱼 (大约 2 斤)","五花肉 100g","大葱 200g","姜 80g","蒜瓣 3-4 个","干辣椒两个","白糖 50g"]}]},{"text":"操作","content":["注:下文所述的鱼是购买时卖家简易处理后的,已刮鱼鳞已去内脏。",{"type":"list","items":["葱、姜、蒜、干辣椒分别清洗干净。","葱白处切段,每段长度约 4cm,再将每段劈为四瓣。","姜切片,每片厚度约 3mm。","一个大蒜拍碎切末,其余蒜切为二瓣。","干辣椒切四段。","五花肉切片,约 4cm*4cm。","清洗鱼。","鱼背肉厚处拉几道斜口,方便入味","锅里多倒点油,烧至 7 成热(刚刚开始冒烟),下入鱼炸 1 分钟至鱼皮稍稍变硬捞出备用(注意不要一下锅就拨弄鱼,等炸一会再拨弄、翻面),炸鱼的油倒出,锅里留一点底油","将锅里底油烧热,下入五花肉,煸出香味。","放入干辣椒、葱、姜、蒜瓣,翻炒 1 分钟。","将炸好的鱼倒入锅中。",["沿锅边倒入",{"type":"list","items":["50ml 料酒","50ml 陈醋","50ml 味极鲜","20ml 老抽调色","5ml 蚝油提鲜","5g 匙盐","50g 白糖","清水没过鱼面。"]}],"调至中火,将水烧开。","调至小火,慢焖入味。","15 分钟 后,打开锅盖,挑出锅里的葱、姜、蒜、干辣椒。","调至大火收汁,汤汁剩余 1/4 时,撒点蒜末,关火盛出。","红烧鲤鱼出锅!"]}]},{"text":"附加内容","content":["基于下列原因,需要在红烧鲤鱼中添加五花肉:",{"type":"list","items":["五花肉煸炒的时候会出猪油,相比较于植物油,动物油脂更香。","煸炒至金黄的五花肉炖煮过后吃起来也很香的,相当于配菜。","鱼类本身脂肪含量少,所以香味欠缺,着重的是肉质的口感。所以一般做鱼类菜都建议用猪油,想要一锅奶白香醇的鱼汤。猪油是最好的选择~"]},"如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":[],"title":"红烧鲤鱼的做法"}
11 | {"no":10,"id":"69651a4a0acfc5520941f32a67fc2c12","name":"葱烧海参","category":"aquatic","detail":[{"text":"原料和工具","content":[{"type":"list","items":[["泡发好的海参",{"type":"image","text":"海参","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpeg"}],"大葱葱白"]}]},{"text":"计算","content":["每份:",{"type":"list","items":["泡发好的海参(北极参) 4 个","大葱葱白 1 根大葱的葱白即可","食用油 20-25ml","耗油 20g","生抽 5g","白糖 2g","淀粉 2g"]}]},{"text":"操作","content":[{"type":"list","items":["葱白切成 1cm 的段,备用。","海参切成 1cm 的段,备用。",["准备一个空碗,倒入 20g 耗油, 10g 生抽, 2g 白糖,搅拌均匀。",{"type":"image","text":"料汁","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpeg"}],"另一个空碗倒入淀粉,水,制备水淀粉,勾芡用。","热锅,锅内放入 20ml - 25ml 食用油。等待 10 秒让油温升高。",["放入葱白,调",{"type":"em","text":"小火"},",注意不要让葱白变焦。大概煎 3-5 分钟即可。",{"type":"image","text":"葱白","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpeg"}],"用筷子夹出葱白,放入盘中备用。",["倒入调好的料汁,炒香,",{"type":"strong","text":"等待 1 - 2 分钟"}," 。"],"放入切好的海参,翻炒 1 分钟",["加入 100 ml 的水, 中小火, ",{"type":"strong","text":"等待 5 分钟"}],"等待锅中汤汁快干的时候,加入水淀粉,加入前面取出的葱白",["在外观",{"type":"em","text":"呈粘稠状态"},"后关火,盛盘 ",{"type":"image","text":"成品","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpeg"}]]}]},{"text":"附加内容","content":[{"type":"list","items":["操作时,需要注意观察锅中的水量,如快见底的时候就直接接入水淀粉即可。"]},"如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":[{"type":"image","text":"示例菜成品","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpeg"},"这道菜的做法并不难,就是海参泡发是需要时间的。疫情隔离在家,干海参是过年前存的年货,正好拿出来尝试一下。"],"title":"葱烧海参的做法"}
12 | {"no":11,"id":"4b028d83d1fc29690551be6eabfd348d","name":"蒜蓉虾","category":"aquatic","detail":[{"text":"原料和工具","content":[{"type":"list","items":["海虾","蒜蓉酱","食用油","生抽"]}]},{"text":"计算","content":["每份:",{"type":"list","items":["海虾 8 只","蒜蓉酱 50 g","食用油 20 ml","生抽 5 ml"]}]},{"text":"操作","content":[{"type":"list","items":["用刀从从虾头中间切开,切到距离虾尾 1 cm","将蒜蓉酱铺在虾身中间,放在盘子中","锅中倒入热水,将盘子放入锅中,大火蒸 3 分钟","烧热油,倒入虾盘中,倒入生抽"]},[{"type":"image","text":"示例菜成品","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpeg"},"\n",{"type":"image","text":"示例菜成品","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpeg"}]]},{"text":"附加内容","content":["如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":["蒜蓉虾是广东省地方传统名菜,色香味俱全。"],"title":"蒜蓉虾的做法"}
13 | {"no":12,"id":"f584ce40ecbaedbf5db71231d672bc39","name":"蛏抱蛋","category":"aquatic","detail":[{"text":"原料和工具","content":[{"type":"list","items":["蛏子","鸡蛋","食用油","洋葱","淀粉","生抽","鸡精","料酒"]}]},{"text":"计算","content":["每份:",{"type":"list","items":["蛏子 200 g","鸡蛋 2 个","食用油 100 ml","洋葱 0.25 个","淀粉 20 g","生抽 5 ml","鸡精 5 ml","料酒 5 ml"]}]},{"text":"操作","content":[{"type":"list","items":["烧开水,将蛏子放入水中,水煮 2 分钟后,捞上来去壳,放入大碗","往大碗中加入洋葱、生抽、料酒、鸡精、生粉后,充分搅拌","往大碗中打入 2 个 鸡蛋,继续搅拌","起锅烧油,倒入碗中蛏子,煎炸至单面金黄后,翻面继续煎炸"]},[{"type":"image","text":"示例菜成品","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpeg"},"\n",{"type":"image","text":"示例菜成品","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpeg"},"\n",{"type":"image","text":"示例菜成品","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpeg"}]]},{"text":"附加内容","content":[{"type":"list","items":["更多情况下,福州当地会选用马蹄(解腻)和马铃薯(洗油),因为疫情期间买不到,所以选用了凤梨","闽菜以甜为主,如果吃不惯的可以放弃白糖","可以通过勺子敲打瘦肉块,听到声响来判断是否炸透瘦肉"]},"如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":["蛏抱蛋,是流行于福建省福州地区的传统家常菜"],"title":"蛏抱蛋的做法"}
14 | {"no":13,"id":"b9afa3fd6ec09859f66582bf24c8c724","name":"香煎翘嘴鱼","category":"aquatic","detail":[{"text":"原料和工具","content":[{"type":"list","items":["翘嘴鱼(肉食性鱼类,肉细腻,口感好)","姜","葱","蒜","青椒","香菜","老抽","白糖","豆瓣酱","料酒","生抽","盐"]}]},{"text":"计算","content":["注:这个量大概是 2-3 个人的量","每份:",{"type":"list","items":["翘嘴鱼:2 斤最佳","姜沫:20g","葱:半根(50 克)","蒜:4 个","香菜:个人口味","老抽:2ml(不太喜欢重口的可以不放)","白糖:10g","干辣椒:4-6 个(根据个人口味选择)","料酒:100ml","生抽:4ml","盐:约 50g 用于腌制鱼","食用油:100ml"]}]},{"text":"操作","content":[{"type":"list","items":["鱼开背杀好(让卖鱼的杀好,千万不要剖腹杀鱼,切记是开背),清洗干净","鱼表面用盐涂抹均匀,倒入料酒约 80ml,姜末 20g,放入冰箱保鲜层进行腌制 1-2 天","取出腌制好的鱼,用绳挂起晾晒至半干(约 1-2 天,具体时间需结合气温与阳光)","食用前请将鱼用清水清洗,沥干水分(防止水遇油飞溅)","开大火将锅烧热,迅速改小火,锅中放油,尽量保持整个锅表面有油,将鱼沿锅边划入锅内(先煎鱼背面)","鱼入锅后(和翻面后),不要着急移动鱼的位置(此时容易破皮),煎约 30 秒后,尝试晃动锅","背面煎约 1 分钟后,翻面煎约 1-2 分钟,煎至两面金黄","等两面都煎好时,把鱼推向锅边一点,留点空间放入豆瓣酱炒香味,放入姜蒜,","炒出佐料香味后,加入料酒,生抽,老抽,倒入热水,水量和鱼平齐或者少点","此时改中大火,煮 5-10 分钟,后放入青椒断,白糖,鸡精,十三香,陈醋","改小火 2-5 分钟,放入葱,香菜,即可出锅"]}]},{"text":"附加内容","content":[{"type":"list","items":["切记鱼是开背杀,腌制时,鱼表面涂抹均匀食盐即可,腌制时间,晾晒时间 1-2 天即可","煎鱼全程小火,刚入锅时不要移动鱼,会破皮","青椒断放入后注意观察熟透程度,里面的青椒很好吃,另外香菜最后放","注意火候的切换,豆瓣酱,白糖的调鲜效果,与陈醋的增香效果"]},"如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":[{"type":"image","text":"香煎翘嘴鱼","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpeg"}],"title":"香煎翘嘴鱼的做法"}
15 | {"no":14,"id":"879344dba80275ba1a30a8c204c1f6cd","name":"鳊鱼炖豆腐","category":"aquatic","detail":[{"text":"原料和工具","content":[{"type":"list","items":["鳊鱼(鱼可以让摊主帮忙处理好)","老豆腐","姜","葱","蒜","老抽","桂皮(可选)","冰糖","干辣椒(不吃辣可以不放)","料酒","生抽","盐","八角(可选)","香叶(可选)","热水"]}]},{"text":"计算","content":["注:这个量大概是 2-3 个人的量","每份:",{"type":"list","items":["鳊鱼:550 克","老豆腐:400 克","姜:5 片","葱:半根(50 克)","蒜:4 个","老抽:2ml(不太喜欢重口的可以不放)","桂皮:1 块","冰糖:5 块","干辣椒:4-6 个(根据个人口味选择)","料酒:5ml","生抽:4ml","盐:5-8 克(根据个人口味选择)","八角:1 个","香叶:1-3 片","食用油:10ml","热水:400 克"]}]},{"text":"操作","content":[{"type":"list","items":["鳊鱼改刀,放上姜片和料酒腌制 5-10 分钟","老豆腐切块后放入水中备用","锅中加油,可以放点盐在锅里,防止煎鱼的时候粘锅,把腌制的鱼用厨房纸擦干水分,把鱼放到锅中,两面都煎一下","等两面都煎好时,把鱼推向锅边一点,留点空间放入葱姜蒜,干辣椒,香叶,八角炒出味道","炒出佐料香味后,加入料酒,生抽,老抽,冰糖,桂皮,倒入热水,水量和鱼平齐或者少点","大火烧开后,放入老豆腐,豆腐贴在锅边,加入食盐,转小火","小火烧 10-15 分钟,然后大火收点汁,即可出锅"]}]},{"text":"附加内容","content":[{"type":"list","items":["鱼的两面,各煎 2-4 分钟即可","煎鱼的时候全程中小火","最后出锅时,加入一些切碎的大蒜,味道会更好"]},"如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":[{"type":"image","text":"鳊鱼炖豆腐","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpg"}],"title":"鳊鱼炖豆腐的做法"}
16 | {"no":15,"id":"3a48e3a8e2d2c486a253599e937c16f7","name":"黄油煎虾","category":"aquatic","detail":[{"text":"原料和工具","content":[{"type":"list","items":["鲜虾(强推肉质紧实的九节虾,普通明虾也可以)","黄油(推荐安佳,一次用一小盒 7g)","黑胡椒粒(瓶磨的那种)","生抽","盐","白糖","米酒"]}]},{"text":"计算","content":["每份:",{"type":"list","items":["鲜虾 300g","黄油 7g","黑胡椒粒 大概 15ml","食用油 45ml","生抽 10ml","米酒 5ml","白糖 10ml","盐 2.5ml"]}]},{"text":"操作","content":[{"type":"list","items":["鲜虾摘除头部,顺带扯出虾线(这步处理不好可在下一步开背时取出虾线),使用剪刀剪开或菜刀片开虾背,沥干水分备用","调制酱汁:小碗放入上述量的全部生抽、米酒、白糖、盐搅匀备用","中大火热锅,热锅内放入食用油,等待 10 秒让油温升高","虾全部放入锅中,开始瓶磨黑胡椒,均匀地撒在虾上翻炒","虾变色后加入黄油,黄油完全融化后倒入调制酱汁,继续翻炒","大火翻炒 15 秒收汁即可装盘"]}]},{"text":"附加内容","content":[{"type":"list","items":["虾开背才更好入味,不过处理时切记切记小心用刀,新手容易伤到手",{"type":"link","text":"冯小厨的菜谱","href":"https://www.bilibili.com/video/BV1g541177cd"}]},"如果您遵循本指南的制作流程而发现有问题或可以改进的流程,请提出 Issue 或 Pull request 。"]}],"desc":[{"type":"image","text":"示例菜成品","href":"https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/3449c9e5e332f1dbb81505cd739fbf3f.jpg"},"黄油煎虾是一道制作相对简单、风味极佳的菜式,主要耗时在于处理活虾,总耗时在一个小时内,适合初学者进行烹饪。"],"title":"黄油煎虾的做法"}
17 |
--------------------------------------------------------------------------------
/miniprogram/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "HowToCookOnMiniprogram",
3 | "version": "0.2.1",
4 | "description": "",
5 | "main": "app.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [
10 | "howtocook",
11 | "miniprogram",
12 | "wechat"
13 | ],
14 | "author": "leejim",
15 | "license": "MIT",
16 | "dependencies": {
17 | "tdesign-miniprogram": "^1.0.0-rc.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/miniprogram/pages/changelog/detail.js:
--------------------------------------------------------------------------------
1 | import log from './log'
2 |
3 | Page({
4 | data: {
5 | item: {},
6 | detail: [],
7 | },
8 |
9 | onLoad: function (options) {
10 | if (options.index) {
11 | const item = log[options.index]
12 | this.setData({
13 | item,
14 | index: options.index,
15 | detail: item.detail
16 | })
17 | wx.setNavigationBarTitle({
18 | title: `v${item.version} 更新`
19 | })
20 | }
21 | },
22 |
23 | onShareAppMessage: function () {
24 | const { index, item } = this.data
25 | return {
26 | title: `程序员做饭指南 - v${item.version} 更新`,
27 | path: `/pages/changelog/detail?index=${index}`
28 | }
29 | }
30 | })
--------------------------------------------------------------------------------
/miniprogram/pages/changelog/detail.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {}
3 | }
--------------------------------------------------------------------------------
/miniprogram/pages/changelog/detail.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{item.type}}
5 |
6 |
7 |
--------------------------------------------------------------------------------
/miniprogram/pages/changelog/detail.wxss:
--------------------------------------------------------------------------------
1 | /* pages/changelog/detail.wxss */
--------------------------------------------------------------------------------
/miniprogram/pages/changelog/index.js:
--------------------------------------------------------------------------------
1 | import log from './log'
2 |
3 | Page({
4 | data: {
5 | log
6 | },
7 |
8 | onLoad: function (options) {
9 |
10 | },
11 |
12 | onShareAppMessage: function () {
13 | return {
14 | title: '程序员做饭-更新日志',
15 | path: '/pages/changelog/index'
16 | }
17 | }
18 | })
--------------------------------------------------------------------------------
/miniprogram/pages/changelog/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "更新日志",
3 | "usingComponents": {}
4 | }
--------------------------------------------------------------------------------
/miniprogram/pages/changelog/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/miniprogram/pages/changelog/index.wxss:
--------------------------------------------------------------------------------
1 | /* pages/changelog/index.wxss */
--------------------------------------------------------------------------------
/miniprogram/pages/changelog/log.js:
--------------------------------------------------------------------------------
1 | export default [{
2 | version: '1.1.1',
3 | detail: [{
4 | type: 'feature',
5 | value: '从订阅消息进来,增加订阅消息弹窗'
6 | }],
7 | date: '2022-03-27'
8 | }, {
9 | version: '1.1.0',
10 | detail: [{
11 | type: 'feature',
12 | value: '支持搜索'
13 | }],
14 | date: '2022-03-19'
15 | }, {
16 | version: '1.0.0',
17 | detail: [{
18 | type: 'feature',
19 | value: '增加买菜模块'
20 | },{
21 | type: 'feature',
22 | value: '全新 LOGO'
23 | },{
24 | type: 'feature',
25 | value: '发送订阅消息'
26 | },{
27 | type: 'feature',
28 | value: '“学习”更名为“技巧”'
29 | }],
30 | date: '2022-03-17'
31 | }, {
32 | version: '0.5.0',
33 | detail: [{
34 | type: 'feature',
35 | value: '增加学习模块'
36 | }],
37 | date: '2022-03-14'
38 | }, {
39 | version: '0.4.0',
40 | detail: [{
41 | type: 'feature',
42 | value: '支持图片预览'
43 | }, {
44 | type: 'feature',
45 | value: '支持链接跳转'
46 | }, {
47 | type: 'feature',
48 | value: '展示各种文字格式'
49 | }],
50 | date: '2022-03-13'
51 | }, {
52 | version: '0.3.1',
53 | detail: [{
54 | type: 'feature',
55 | value: '详情页:增加广告开关提醒'
56 | },{
57 | type: 'feature',
58 | value: '个人中心:增加更新日志'
59 | }],
60 | date: '2022-03-11'
61 | },{
62 | version: '0.3.0',
63 | detail: [{
64 | type: 'feature',
65 | value: '增加广告(用于实验)'
66 | },{
67 | type: 'feature',
68 | value: '个人中心:增加广告开关'
69 | }],
70 | date: '2022-03-11'
71 | }, {
72 | version: '0.2.1',
73 | detail: [{
74 | type: 'feature',
75 | value: '增加版本更新机制'
76 | },{
77 | type: 'bug',
78 | value: '修复按钮中图标没对齐的问题'
79 | }],
80 | date: '2022-03-10'
81 | }, {
82 | version: '0.2.0',
83 | detail: [{
84 | type: 'feature',
85 | value: '支持点赞、收藏'
86 | }, {
87 | type: 'feature',
88 | value: '个人中心:支持打开我的收藏'
89 | }],
90 | date: '2022-03-08'
91 | }, {
92 | version: '0.1.3',
93 | detail: [{
94 | type: 'feature',
95 | value: '详情页:优化 UI'
96 | }, {
97 | type: 'feature',
98 | value: '个人中心:新增当前版本信息'
99 | }, {
100 | type: 'feature',
101 | value: '个人中心:支持跳转 TDesign 组件库小程序'
102 | }, {
103 | type: 'bug',
104 | value: '修复偶尔无法触发倒计时的问题'
105 | }],
106 | date: '2022-03-06'
107 | }, {
108 | version: '0.1.2',
109 | detail: [{
110 | type: 'feature',
111 | value: '详情页:优化倒计时样式'
112 | }],
113 | date: '2022-03-05'
114 | }, {
115 | version: '0.1.0',
116 | detail: [{
117 | type: 'feature',
118 | value: '首发版本 🎉'
119 | }],
120 | date: '2022-03-05'
121 | }]
--------------------------------------------------------------------------------
/miniprogram/pages/detail/index.js:
--------------------------------------------------------------------------------
1 | import infos from '../../data'
2 | import tips from '../learn/data'
3 | import Toast from 'tdesign-miniprogram/toast/index';
4 | import Message from 'tdesign-miniprogram/message/index';
5 | import { post } from '../../utils/request';
6 |
7 | Page({
8 | data: {
9 | index: 0,
10 | id: null,
11 | visible: true,
12 | liked: false,
13 | starred: false,
14 | done: false,
15 | stepIndexes: [],
16 | adFlag: false,
17 | },
18 |
19 | async onLoad(options) {
20 | const { id } = options;
21 | if (id) {
22 | this.setData({ id })
23 | const target = infos.find(item => item.id == id)
24 | if (target) {
25 | this.setData({
26 | ...target
27 | })
28 | const operation = this.data.detail.find(item => item.text == '操作')
29 | if (operation) {
30 | this.setData({
31 | stepIndexes: new Array(operation.content.length).fill(0)
32 | })
33 | }
34 | this.getData()
35 | this.updateViews()
36 | }
37 | }
38 | },
39 |
40 | onShow() {
41 | const adFlagStorage = wx.getStorageSync('ad-flag')
42 |
43 | this.setData({
44 | adFlag: adFlagStorage === '' ? true : adFlagStorage,
45 | })
46 | },
47 |
48 | async getData() {
49 | const { id } = this.data;
50 |
51 | this.setData({ done: false })
52 | try {
53 | const [starCount, likeCount, star, like] = await Promise.all([
54 | post('action/get/all', { id, type: 'star'}),
55 | post('action/get/all', { id, type: 'like'}),
56 | post('action/get', { id, type: 'star'}),
57 | post('action/get', { id, type: 'like' })
58 | ])
59 | this.setData({
60 | done: true,
61 | likeCount,
62 | starCount,
63 | star,
64 | like
65 | });
66 | } catch(err) {
67 | console.log(err);
68 | }
69 | },
70 |
71 | updateViews() {
72 | const { id } = this.data;
73 | // wx.cloud.callFunction({
74 | // name: 'updateViews',
75 | // data: { id },
76 | // type: 'cookbook'
77 | // })
78 | },
79 |
80 | toMyCenter() {
81 | wx.switchTab({
82 | url: '/pages/my/index'
83 | })
84 | },
85 |
86 | toNext(e) {
87 | const { max, index } = e.target.dataset;
88 |
89 | wx.vibrateShort()
90 | this.setData({
91 | startTimeout: false,
92 | timeout: 0,
93 | [`stepIndexes[${index}]`]: (this.data.stepIndexes[index] + 1) % max,
94 | })
95 | },
96 |
97 | handleStart(e) {
98 | const { time } = e.currentTarget.dataset;
99 |
100 | wx.vibrateShort()
101 | this.setData({
102 | startTimeout: true,
103 | timeout: time * 1000
104 | })
105 | },
106 |
107 | handleCountdown(e) {
108 | const { hours, minutes, seconds } = e.detail
109 |
110 | if (hours <= 0 && minutes <= 0 && seconds <= 0) {
111 | wx.vibrateLong()
112 | this.setData({
113 | startTimeout: false
114 | })
115 | Toast({
116 | context: this,
117 | selector: '#t-toast',
118 | message: '时间到!',
119 | });
120 | }
121 | },
122 |
123 | async toggleStarOrLike(e) {
124 | const { dataset } = e.currentTarget;
125 | const { id } = this.data;
126 | const { type } = dataset;
127 |
128 | try {
129 | const data = await post('action/create', { type, id });
130 |
131 | this.setData({ [type]: data })
132 | wx.hideLoading()
133 | } catch(err) {
134 | console.log(err);
135 | }
136 | },
137 |
138 | handlePreview({ target }) {
139 | const { src } = target.dataset;
140 |
141 | wx.previewImage({
142 | urls: [src],
143 | success() {
144 | console.log('success');
145 | },
146 | fail(e) {
147 | console.log(e);
148 | }
149 | })
150 | },
151 |
152 | handleLink({ target }) {
153 |
154 | const { src } = target.dataset;
155 |
156 | if (src.startsWith('http')) {
157 | wx.setClipboardData({
158 | data: src,
159 | }).then(() => {
160 | Message.info({
161 | offset: [20, 32],
162 | duration: 5000,
163 | content: '链接已复制,暂不支持直接打开网页',
164 | });
165 | }).catch(() => {
166 | Message.info({
167 | offset: [20, 32],
168 | duration: 5000,
169 | content: '链接无法复制,请稍后重试',
170 | });
171 | })
172 | } else {
173 | const match = /\/([^\/]+)\.md/.exec(src);
174 |
175 | if (match[1]) {
176 | const cookbook = infos.find(item => item.name.includes(match[1]))
177 |
178 | if (cookbook) {
179 | wx.navigateTo({
180 | url: './index?id=' + cookbook.id
181 | })
182 | }
183 |
184 | const tip = tips.find(item => item.name.includes(match[1]))
185 | if (tip) {
186 | wx.navigateTo({
187 | url: '/pages/learn/detail?no=' + tip.no
188 | })
189 | }
190 | }
191 | }
192 | },
193 |
194 | onShareAppMessage() {
195 | return {
196 | title: this.data.title || '程序员做饭指南',
197 | path: '/pages/detail/index?id=' + this.data.id
198 | }
199 | },
200 | })
--------------------------------------------------------------------------------
/miniprogram/pages/detail/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "程序员做饭",
3 | "usingComponents": {}
4 | }
--------------------------------------------------------------------------------
/miniprogram/pages/detail/index.less:
--------------------------------------------------------------------------------
1 | @primary-color: #0052d9;
2 |
3 | .container {
4 | padding: 32rpx;
5 | padding-bottom: calc(88rpx + 24rpx + env(safe-area-inset-bottom));
6 | }
7 |
8 | .title {
9 | font-size: 40rpx;
10 | font-weight: 700;
11 | line-height: 56rpx;
12 | margin: 48rpx 0 0;
13 | color: rgba(0, 0, 0, 0.9);
14 | }
15 |
16 | .desc {
17 | font-size: 24rpx;
18 | color: #999;
19 | margin: 8rpx 0 32rpx;
20 | line-height: 36rpx;
21 | }
22 |
23 | .tab-panel {
24 | padding: 48rpx 24rpx;
25 | }
26 |
27 | .title-block {
28 | display: flex;
29 | justify-content: center;
30 | align-items: center;
31 | }
32 |
33 | .detail-title {
34 | position: relative;
35 | font-size: 32rpx;
36 | font-weight: bold;
37 | text-align: center;
38 | margin: 32rpx 0 52rpx;
39 | background: #fff;
40 |
41 | &:before {
42 | content: '';
43 | display: block;
44 | position: absolute;
45 | left: -20rpx;
46 | top: 50%;
47 | width: 52rpx;
48 | height: 52rpx;
49 | transform: translateY(-50%);
50 | background-color: pink;
51 | border-radius: 50%;
52 | opacity: .3;
53 | }
54 | }
55 |
56 | .step-title {
57 | line-height: 44rpx;
58 | }
59 |
60 | .paragraph {
61 | margin: 8rpx;
62 | }
63 |
64 | .button {
65 | display: inline-flex;
66 | align-items: center;
67 | justify-content: center;
68 | min-width: 120rpx;
69 | padding: 0 8rpx;
70 | border: 1px solid #333;
71 | font-size: 24rpx;
72 | border-radius: 4rpx;
73 | margin: 0 10rpx;
74 | height: 46rpx;
75 | box-sizing: border-box;
76 |
77 | &--primary {
78 | color: #fff;
79 | background-color: @primary-color;
80 | border: 1px solid @primary-color;
81 | }
82 | }
83 |
84 | .popup {
85 | display: flex;
86 | text-align: center;
87 | justify-content: center;
88 | background-color: #fff;
89 | padding: 32rpx 0;
90 | transform: translateY(-100%);
91 | transition: all .3s ease-in-out;
92 |
93 | &--top {
94 | position: fixed;
95 | top: 0;
96 | left: 0;
97 | width: 100%;
98 | }
99 |
100 | &--active {
101 | transform: translateY(0);
102 | }
103 | }
104 |
105 | .no-border-radius {
106 | border-radius: none;
107 | }
108 |
109 | .no-border {
110 | border: 0;
111 | }
112 |
113 | .actions {
114 | position: fixed;
115 | left: 0;
116 | right: 0;
117 | bottom: 0;
118 | padding-bottom: env(safe-area-inset-bottom, 28rpx);
119 | background-color: #fff;
120 | box-shadow: #ccc 0 0 10px;
121 | transform: translateY(100%);
122 | transition: all .3s ease;
123 | display: flex;
124 | align-items: center;
125 |
126 | &__button {
127 | display: flex !important;
128 | border: 0 !important;
129 | border-radius: 0 !important;
130 | }
131 |
132 | &--active {
133 | transform: translateY(0);
134 | }
135 |
136 | &__text {
137 | line-height: 36rpx;
138 | }
139 |
140 | &__icon {
141 | margin-right: 8rpx;
142 |
143 | &--active {
144 | color: #0052d9;
145 | }
146 | }
147 | }
148 |
149 | .ad-tips {
150 | font-size: 20rpx;
151 | margin-top: 16rpx;
152 | color: #999;
153 | text-align: center;
154 |
155 | &__route {
156 | color: #0052d9;
157 | }
158 | }
159 |
160 | .button-group {
161 | display: flex;
162 | justify-self: start;
163 | }
--------------------------------------------------------------------------------
/miniprogram/pages/detail/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{title}}
5 |
6 | 可以动手试试
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {{step}}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 下一步
38 |
39 |
40 | 重新做一次
41 |
42 |
43 | 开启倒计时
44 |
45 |
46 |
47 |
48 |
49 | {{detail.text}}
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {{detail.text}}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | 广告可以在 “个人中心” 关闭
74 |
75 |
76 |
77 |
78 |
79 |
82 |
83 |
84 |
85 |
86 | {{likeCount ? likeCount + ' 人' : ''}}喜欢
87 |
88 |
89 | 收藏
90 |
91 |
92 |
93 |
94 | module.exports.getTime = function(str) {
95 | if (typeof str != 'string') {
96 | str = str.map(function(item) {
97 | return typeof item == 'string' ? item : item.text
98 | }).join('')
99 | }
100 | var res = getRegExp('([0-9\.半一两三四]+)\s*(小时|min|分钟|秒|s)').exec(str);
101 | if (res) {
102 | var unit = ['min', '分钟'].indexOf(res[2]) > -1 ? 60 : 1;
103 | if (res[2] == '小时') unit = 3600;
104 | var map = {
105 | '半': 0.5,
106 | '一': 1,
107 | '两': 2,
108 | '三': 3,
109 | };
110 | var val = res[1];
111 | val = getRegExp('\d+').test(val) ? val : map[val];
112 |
113 | return val * unit;
114 | }
115 | }
116 |
117 | module.exports.getType = function(obj) {
118 | if ("Array" === obj.constructor) return 'array'
119 |
120 | return typeof obj;
121 | }
122 |
--------------------------------------------------------------------------------
/miniprogram/pages/index/index.js:
--------------------------------------------------------------------------------
1 | import infos from '../../data'
2 | import utils from '../../utils/index.js'
3 | import { chineseMap, titleMap } from '../../config/index.js'
4 | import Toast from 'tdesign-miniprogram/toast/index';
5 |
6 | let isSubscribeShow = false;
7 |
8 | Page({
9 | data: {
10 | list: [],
11 | searchKeyword: '',
12 | chineseMap,
13 | categoryIndex: 0,
14 | subscribeModalVisible: false,
15 | },
16 |
17 | onLoad() {
18 | const menu = utils.groupBy(infos, 'category');
19 | const list = Object.entries(menu).filter(([item]) => item !== 'template').map(([category, list]) => {
20 | return {
21 | name: titleMap[category],
22 | icon: `/assets/images/${category}.png`,
23 | list
24 | }
25 | })
26 | this.setData({
27 | list
28 | })
29 | if (wx.getUserProfile) {
30 | this.setData({
31 | canIUseGetUserProfile: true
32 | })
33 | }
34 |
35 | const { scene } = wx.getLaunchOptionsSync() // https://developers.weixin.qq.com/miniprogram/dev/reference/scene-list.html
36 | if (scene === 1107 && !isSubscribeShow) {
37 | this.setData({
38 | subscribeModalVisible: true
39 | })
40 | isSubscribeShow = true
41 | }
42 | },
43 |
44 | handleChange(e) {
45 | this.setData({ categoryIndex: e.detail.value });
46 | },
47 |
48 | handleTap(e) {
49 | const { id } = e.target.dataset.item;
50 |
51 | wx.navigateTo({
52 | url: '../detail/index?id=' + id
53 | })
54 | },
55 |
56 | handleToSearch() {
57 | const content = this.data.searchKeyword.trim();
58 | wx.navigateTo({
59 | url: '/pages/search/index?keyword=' + content,
60 | }).then(() => {
61 | this.setData({ searchKeyword: '' })
62 | })
63 | },
64 |
65 | handleSubscribe() {
66 | const tmplIds = ['vjEDlUYrVJ05CauSw_V9jIWF-okt3OMCBtlz9yvjrfg', 'Sbtj4X4gIKWRy0xDeWU8xCl8LejbTpIQ3gWiKh5JFp4'];
67 | wx.requestSubscribeMessage({
68 | tmplIds,
69 | success: async (res) => {
70 | const accept = tmplIds.some(key => res[key] === 'accept')
71 | Toast({
72 | context: this,
73 | selector: '#t-toast',
74 | message: accept ? '订阅成功' : '你拒绝了订阅',
75 | });
76 | },
77 | complete: () => {
78 | this.setData({ subscribeModalVisible: false })
79 | }
80 | })
81 | },
82 |
83 | onShareAppMessage() {
84 | return {
85 | title: '程序员做饭指南',
86 | path: '/pages/index/index'
87 | }
88 | },
89 | })
90 |
--------------------------------------------------------------------------------
/miniprogram/pages/index/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "程序员做饭",
3 | "navigationStyle": "custom",
4 | "backgroundColor": "#fff",
5 | "usingComponents": {
6 | "t-side-bar": "tdesign-miniprogram/side-bar/side-bar",
7 | "t-side-bar-item": "tdesign-miniprogram/side-bar-item/side-bar-item"
8 | }
9 | }
--------------------------------------------------------------------------------
/miniprogram/pages/index/index.less:
--------------------------------------------------------------------------------
1 | page {
2 | padding-bottom: constant(safe-area-inset-bottom);
3 | padding-bottom: env(safe-area-inset-bottom);
4 | }
5 |
6 | .brand {
7 | display: flex;
8 | align-items: center;
9 | }
10 |
11 | .logo {
12 | width: 50rpx;
13 | height: 50rpx;
14 | }
15 |
16 | .name {
17 | display: flex;
18 | align-items: center;
19 | font-size: 28rpx;
20 | }
21 |
22 | .side-bar {
23 | --td-side-bar-width: 256rpx;
24 | overflow-y: scroll;
25 | }
26 |
27 | .side-bar-item {
28 | justify-content: flex-start !important;
29 | }
30 |
31 | .card {
32 | margin: 24rpx;
33 | padding: 24rpx;
34 | border-radius: 8rpx;
35 | box-shadow: 2rpx 2rpx 10rpx rgba(0, 0, 0, .3);
36 | }
37 |
38 | .wrapper {
39 | box-sizing: border-box;
40 | height: 100vh;
41 | border-bottom: env(safe-area-inset-bottom, 28rpx) solid #fff;
42 | padding-bottom: 50px ;
43 | }
44 |
45 | scroll-view {
46 | height: 100%;
47 | }
48 |
49 | .search-box {
50 | --td-search-height: 64rpx;
51 | margin-left: 32rpx;
52 | width: 252px;
53 | }
54 |
55 | .container {
56 | display: flex;
57 | height: calc(100vh - 362rpx);
58 | box-sizing: border-box;
59 | }
60 |
61 | .content {
62 | flex: 1;
63 | }
--------------------------------------------------------------------------------
/miniprogram/pages/index/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/miniprogram/pages/kitchen/index.js:
--------------------------------------------------------------------------------
1 | import { knownSupportedEmoji } from './supported';
2 | let emojiData = {};
3 | const LIMIT = 5;
4 | import { show as showAd, create as createAd } from '../../utils/ad'
5 |
6 | Page({
7 | combineOriginUrl: '',
8 |
9 | data: {
10 | visible: false,
11 | knownSupportedEmoji,
12 | enableEmoji: [],
13 | leftEmoji: '',
14 | rightEmoji: '',
15 | pos: '',
16 | loadFail: false,
17 | size: 30,
18 | combineEnable: false,
19 | combineUrl: '',
20 | error: ''
21 | },
22 |
23 | onLoad() {
24 | wx.showLoading({ title: '初始化数据ing...', mask: true })
25 | wx.request({
26 | url: 'https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/emoji/emoji.json',
27 | success(res) {
28 | emojiData = res.data;
29 | },
30 | fail(err) {
31 | console.log(err);
32 | },
33 | complete() {
34 | wx.hideLoading()
35 | }
36 | })
37 |
38 | // load ad
39 | this.combineImageAd = createAd('adunit-ac72850d870bb556');
40 | this.combineImageAd.onClose((res) => {
41 | if (res && res.isEnded) {
42 | const combineUrl = this.getCombineCacheUrl();
43 | this.setData({
44 | combineUrl
45 | })
46 | wx.setStorageSync('combine-video-had-watched', true)
47 | } else {
48 | wx.showModal({
49 | content: '仅需看 1 次视频,长期可用',
50 | cancelText: '放弃',
51 | confirmText: '继续',
52 | success: (res) => {
53 | if (res.confirm) {
54 | showAd(this.combineImageAd)
55 | } else {
56 | this.setData({
57 | leftEmoji: '',
58 | rightEmoji: ''
59 | })
60 | }
61 | }
62 | })
63 | }
64 | })
65 | this.combineImageAd.onError((err) => {
66 | wx.showToast({ title: '广告加载失败,获得免费使用特权', icon: 'none' })
67 | const combineUrl = this.getCombineCacheUrl();
68 | this.setData({
69 | combineUrl
70 | })
71 | })
72 |
73 | this.downloadImageAd = createAd('adunit-7f93198b831fa868');
74 | this.downloadImageAd.onClose((res) => {
75 | if (res && res.isEnded) {
76 | const url = this.getCombineCacheUrl();
77 | this.downloadImage(url);
78 | wx.setStorageSync('download-video-had-watched', true)
79 | } else {
80 | wx.showModal({
81 | content: '仅需看 1 次视频,长期可下载',
82 | cancelText: '放弃',
83 | confirmText: '继续',
84 | success: (res) => {
85 | if (res.confirm) {
86 | showAd(this.downloadImageAd)
87 | }
88 | }
89 | })
90 | }
91 | })
92 | this.downloadImageAd.onError((err) => {
93 | wx.showToast({ title: '广告加载失败,获得免费下载特权', icon: 'none' })
94 | this.downloadImage(url);
95 | })
96 | },
97 |
98 | handleSelected(e) {
99 | const { pos } = e.currentTarget.dataset;
100 | const { leftEmoji } = this.data;
101 |
102 | if (pos == 'right') {
103 | if (leftEmoji == '') {
104 | wx.showToast({ icon: 'none', title: '从左边开始' })
105 | return;
106 | }
107 | this.setData({
108 | enableEmoji: knownSupportedEmoji.map(e => {
109 | const target = emojiData[leftEmoji].find(item => {
110 | if (e === leftEmoji) {
111 | return item.leftEmoji == e && item.rightEmoji == e;
112 | }
113 | return item.leftEmoji == e || item.rightEmoji == e;
114 | })
115 | return {
116 | isValid: !!target,
117 | id: e,
118 | // date: target ? target.date : ''
119 | }
120 | })
121 | })
122 | }
123 | this.setData({ visible: true, pos, size: 30 })
124 | },
125 |
126 | onVisibleChange(e) {
127 | this.setData({ visible: e.detail.visible })
128 | },
129 |
130 | handleSelectedEmoji(e) {
131 | const { pos, rightEmoji } = this.data;
132 | const { id, valid } = e.target.dataset;
133 |
134 | if (pos == 'right' && !valid) return;
135 |
136 | if (pos == 'left' && rightEmoji) {
137 | this.setData({ rightEmoji: '', combineUrl: '', combineEnable: false })
138 | }
139 |
140 | this.setData({ [`${pos}Emoji`]: id, visible: false });
141 |
142 | if (pos == 'right') {
143 | this.getCombineImageOriginUrl()
144 | this.handleLimit()
145 | }
146 | },
147 |
148 | handleLimit() {
149 | const combineTimes = wx.getStorageSync('combine-times') || 0;
150 | const videoHadWatched = wx.getStorageSync('combine-video-had-watched') || false;
151 |
152 | if (Number(combineTimes) > LIMIT && !videoHadWatched && this.combineImageAd) {
153 | if (this.combineImageAd) {
154 | showAd(this.combineImageAd)
155 | }
156 | } else {
157 | const combineUrl = this.getCombineCacheUrl();
158 | this.setData({
159 | combineUrl
160 | })
161 | }
162 | },
163 |
164 | getCombineImageOriginUrl() {
165 | const { leftEmoji, rightEmoji } = this.data;
166 | const fix = str => str.split("-")
167 | .filter(x => x !== "fe0f")
168 | .join("_")
169 |
170 | const target = emojiData[leftEmoji].find(item => item.leftEmoji == leftEmoji && item.rightEmoji == rightEmoji || item.leftEmoji == rightEmoji && item.rightEmoji == leftEmoji);
171 |
172 | if (target) {
173 | this.combineOriginUrl = `https://www.gstatic.com/android/keyboard/emojikitchen/${target.date}/u${fix(target.leftEmoji)}/u${fix(target.leftEmoji)}_u${fix(target.rightEmoji)}.png`;
174 | }
175 | },
176 |
177 | onCombileLoadError() {
178 | this.setData({ loadFail: true });
179 | const { leftEmoji, rightEmoji } = this.data;
180 |
181 | if (!this.combineOriginUrl) return
182 |
183 | wx.showLoading({ title: '绘制中', mask: true })
184 |
185 | wx.request({
186 | url: `https://api.africans.cn/cooking`,
187 | method: 'POST',
188 | data: {
189 | url: this.combineOriginUrl,
190 | leftEmoji,
191 | rightEmoji
192 | },
193 | success: (res) => {
194 | if (res.data.errcode == 0) {
195 | this.setData({ loadFail: false })
196 | } else {
197 | wx.showToast({
198 | title: '未知错误', icon: '',
199 | })
200 | }
201 | },
202 | fail: (err) => {
203 | wx.showToast({
204 | title: err.errmsg
205 | })
206 | this.setData({ loadFail: false })
207 | },
208 | complete() {
209 | wx.hideLoading()
210 | }
211 | })
212 | },
213 |
214 | onScrollToLower() {
215 | this.setData({
216 | size: this.data.size + 30
217 | })
218 | },
219 |
220 | onCombineLoaded() {
221 | const combineTimes = wx.getStorageSync('combine-times');
222 |
223 | this.setData({ combineEnable: true });
224 | wx.setStorageSync('combine-times', Number(combineTimes) + 1);
225 | },
226 |
227 | handleShowTips() {
228 | const { leftEmoji, rightEmoji } = this.data;
229 |
230 | if (!leftEmoji && !rightEmoji) {
231 | wx.showToast({ title: '先点击左上角', icon: 'none' })
232 | }
233 | },
234 |
235 | downloadImage(src) {
236 | const handleFail = (err) => {
237 | console.log(err);
238 | this.setData({ error: err.errMsg })
239 | wx.showToast({ title: `${err.errMsg}`, icon: 'none' })
240 | }
241 | wx.getImageInfo({
242 | src
243 | }).then(({ path }) => {
244 | wx.saveImageToPhotosAlbum({
245 | filePath: path
246 | }).then(() => {
247 | const downloadTimes = wx.getStorageSync('download-times') || 0
248 |
249 | wx.showToast({ title: '下载成功~', icon : 'none' });
250 | wx.setStorageSync('download-times', Number(downloadTimes) + 1)
251 | }).catch(handleFail)
252 | }).catch(handleFail)
253 | },
254 |
255 | getCombineCacheUrl() {
256 | const { leftEmoji, rightEmoji } = this.data;
257 | const url = `https://how-to-cook-1255404841.cos.ap-shanghai.myqcloud.com/combine/${leftEmoji}---${rightEmoji}.png`;
258 |
259 | return url;
260 | },
261 |
262 | handleDownload() {
263 | const url = this.getCombineCacheUrl();
264 | const downloadTimes = wx.getStorageSync('download-times') || 0;
265 | const videoHadWatched = wx.getStorageSync('download-video-had-watched') || false;
266 |
267 | if (Number(downloadTimes) > LIMIT && !videoHadWatched && this.downloadImageAd) {
268 | showAd(this.downloadImageAd)
269 | } else {
270 | this.downloadImage(url);
271 | }
272 | },
273 |
274 | onShareAppMessage() {
275 | return {
276 | title: '表情厨房',
277 | path: '/pages/kitchen/index'
278 | }
279 | },
280 | })
--------------------------------------------------------------------------------
/miniprogram/pages/kitchen/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "component": true,
3 | "navigationBarTitleText": "表情厨房"
4 | }
5 |
--------------------------------------------------------------------------------
/miniprogram/pages/kitchen/index.less:
--------------------------------------------------------------------------------
1 | .box {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | height: 120px;
6 | width: 120px;
7 | border-radius: 6px;
8 | background-color: #efefef;
9 | font-size: 58px;
10 | color: #ccc;
11 |
12 | &-result {
13 | background-color: #fff;
14 | margin: 32px auto;
15 | border-radius: 50%;
16 | width: 150px;
17 | height: 150px;
18 | }
19 | }
20 |
21 | .container {
22 | display: flex;
23 | padding: 24px;
24 | background-color: #e4e4e4;
25 | justify-content: space-around;
26 | }
27 | .block {
28 | background: #fff;
29 | color: #333;
30 | display: flex;
31 | align-items: center;
32 | justify-content: space-evenly;
33 | flex-wrap: wrap;
34 | }
35 |
36 | .popup-wrapper {
37 | height: 60vh;
38 | }
39 |
40 | .emoji {
41 | width: 100rpx;
42 | height: 100rpx;
43 | padding: 24rpx;
44 | }
45 |
46 | .actions {
47 | display: flex;
48 | justify-content: center;
49 | }
--------------------------------------------------------------------------------
/miniprogram/pages/kitchen/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
26 |
27 |
28 | {{error}}
29 |
30 |
31 | 下载表情
32 |
33 |
34 |
37 |
38 |
39 |
64 |
65 |
66 |
67 | module.exports.fix = function(str) {
68 | return str.split("-")
69 | .filter(function(x) { return x !== "fe0f" })
70 | .join("_")
71 | }
72 |
--------------------------------------------------------------------------------
/miniprogram/pages/kitchen/supported.js:
--------------------------------------------------------------------------------
1 | module.exports.knownSupportedEmoji = [
2 | "1f600", // 😀
3 | "1f603", // 😃
4 | "1f604", // 😄
5 | "1f601", // 😁
6 | "1f606", // 😆
7 | "1f605", // 😅
8 | "1f602", // 😂
9 | "1f923", // 🤣
10 | "1f62d", // 😭
11 | "1f617", // 😗
12 | "1f619", // 😙
13 | "1f61a", // 😚
14 | "1f618", // 😘
15 | "1f970", // 🥰
16 | "1f60d", // 😍
17 | "1f929", // 🤩
18 | "1f973", // 🥳
19 | "1f917", // 🤗
20 | "1f643", // 🙃
21 | "1f642", // 🙂
22 | "1f972", // 🥲
23 | "1f979", // 🥹
24 | "263a-fe0f", // ☺️
25 | "1f60a", // 😊
26 | "1f60f", // 😏
27 | "1f60c", // 😌
28 | "1f609", // 😉
29 | "1fae2", // 🫢
30 | "1f92d", // 🤭
31 | "1f636", // 😶
32 | "1f610", // 😐
33 | "1f611", // 😑
34 | "1f614", // 😔
35 | "1f60b", // 😋
36 | "1f61b", // 😛
37 | "1f61d", // 😝
38 | "1f61c", // 😜
39 | "1f92a", // 🤪
40 | "1fae1", // 🫡
41 | "1f914", // 🤔
42 | "1f928", // 🤨
43 | "1f9d0", // 🧐
44 | "1f644", // 🙄
45 | "1f612", // 😒
46 | "1f624", // 😤
47 | "1f620", // 😠
48 | "1f621", // 😡
49 | "1f92c", // 🤬
50 | "2639-fe0f", // ☹️
51 | "1f641", // 🙁
52 | "1fae4", // 🫤
53 | "1f615", // 😕
54 | "1f61f", // 😟
55 | "1f97a", // 🥺
56 | "1f633", // 😳
57 | "1f62c", // 😬
58 | "1f910", // 🤐
59 | "1f92b", // 🤫
60 | "1f630", // 😰
61 | "1f628", // 😨
62 | "1f627", // 😧
63 | "1f626", // 😦
64 | "1f62e", // 😮
65 | "1f62f", // 😯
66 | "1f632", // 😲
67 | "1fae3", // 🫣
68 | "1f631", // 😱
69 | "1f92f", // 🤯
70 | "1f622", // 😢
71 | "1f625", // 😥
72 | "1f613", // 😓
73 | "1f61e", // 😞
74 | "1f62e-200d-1f4a8", // 😮💨
75 | "1f616", // 😖
76 | "1f623", // 😣
77 | "1f629", // 😩
78 | "1f62b", // 😫
79 | "1f924", // 🤤
80 | "1f971", // 🥱
81 | "1f634", // 😴
82 | "1f62a", // 😪
83 | "1f31b", // 🌛
84 | "1f31c", // 🌜
85 | "1f31a", // 🌚
86 | "1f31d", // 🌝
87 | "1f31e", // 🌞
88 | "1f922", // 🤢
89 | "1f92e", // 🤮
90 | "1f927", // 🤧
91 | "1f912", // 🤒
92 | "1f915", // 🤕
93 | "1f974", // 🥴
94 | "1fae0", // 🫠
95 | "1f636-200d-1f32b-fe0f", // 😶🌫️
96 | "1fae5", // 🫥
97 | "1f635", // 😵
98 | "1f975", // 🥵
99 | "1f976", // 🥶
100 | "1f637", // 😷
101 | "1f607", // 😇
102 | "1f920", // 🤠
103 | "1f911", // 🤑
104 | "1f60e", // 😎
105 | "1f913", // 🤓
106 | "1f978", // 🥸
107 | "1f925", // 🤥
108 | "1f921", // 🤡
109 | "1f47b", // 👻
110 | "1f4a9", // 💩
111 | "1f47d", // 👽
112 | "1f916", // 🤖
113 | "1f383", // 🎃
114 | "1f608", // 😈
115 | "1f47f", // 👿
116 | "1f4a5", // 💥
117 | "1f4af", // 💯
118 | "1fae7", // 🫧
119 | "1f573-fe0f", // 🕳️
120 | "1f38a", // 🎊
121 | "2764-fe0f", // ❤️
122 | "1f9e1", // 🧡
123 | "1f49b", // 💛
124 | "1f49a", // 💚
125 | "1f499", // 💙
126 | "1f49c", // 💜
127 | "1f90e", // 🤎
128 | "1f5a4", // 🖤
129 | "1f90d", // 🤍
130 | "2665-fe0f", // ♥️
131 | "1f498", // 💘
132 | "1f49d", // 💝
133 | "1f496", // 💖
134 | "1f497", // 💗
135 | "1f493", // 💓
136 | "1f49e", // 💞
137 | "1f495", // 💕
138 | "1f48c", // 💌
139 | "1f49f", // 💟
140 | "2763-fe0f", // ❣️
141 | "2764-fe0f-200d-1fa79", // ❤️🩹
142 | "1f494", // 💔
143 | "1f48b", // 💋
144 | "1f9e0", // 🧠
145 | "1fac0", // 🫀
146 | "1fac1", // 🫁
147 | "1fa78", // 🩸
148 | "1f9a0", // 🦠
149 | "1f9b7", // 🦷
150 | "1f9b4", // 🦴
151 | "1f480", // 💀
152 | "1f440", // 👀
153 | "1f441-fe0f", // 👁️
154 | "1fae6", // 🫦
155 | "1f490", // 💐
156 | "1f339", // 🌹
157 | "1f33a", // 🌺
158 | "1f337", // 🌷
159 | "1f338", // 🌸
160 | "1f4ae", // 💮
161 | "1f3f5-fe0f", // 🏵️
162 | "1f33b", // 🌻
163 | "1f33c", // 🌼
164 | "1f344", // 🍄
165 | "1f331", // 🌱
166 | "1f33f", // 🌿
167 | "1f343", // 🍃
168 | "1f340", // 🍀
169 | "1fab4", // 🪴
170 | "1f335", // 🌵
171 | "1f334", // 🌴
172 | "1f333", // 🌳
173 | "1f332", // 🌲
174 | "1fab9", // 🪹
175 | "1fab5", // 🪵
176 | "1faa8", // 🪨
177 | "26c4", // ⛄
178 | "1f30a", // 🌊
179 | "1f32c-fe0f", // 🌬️
180 | "1f300", // 🌀
181 | "1f32a-fe0f", // 🌪️
182 | "1f525", // 🔥
183 | "2601-fe0f", // ☁️
184 | "1f30b", // 🌋
185 | "1f3d6-fe0f", // 🏖️
186 | "26a1", // ⚡
187 | "26c5", // ⛅
188 | "1f329-fe0f", // 🌩️
189 | "1f327-fe0f", // 🌧️
190 | "1f4a7", // 💧
191 | "1f308", // 🌈
192 | "2b50", // ⭐
193 | "1f31f", // 🌟
194 | "1f4ab", // 💫
195 | "2604-fe0f", // ☄️
196 | "1fa90", // 🪐
197 | "1f30d", // 🌍
198 | "1f648", // 🙈
199 | "1f435", // 🐵
200 | "1f981", // 🦁
201 | "1f42f", // 🐯
202 | "1f431", // 🐱
203 | "1f436", // 🐶
204 | "1f43b", // 🐻
205 | "1f428", // 🐨
206 | "1f43c", // 🐼
207 | "1f42d", // 🐭
208 | "1f430", // 🐰
209 | "1f99d", // 🦝
210 | "1f437", // 🐷
211 | "1f984", // 🦄
212 | "1f422", // 🐢
213 | "1f429", // 🐩
214 | "1f410", // 🐐
215 | "1f98c", // 🦌
216 | "1f999", // 🦙
217 | "1f9a5", // 🦥
218 | "1f994", // 🦔
219 | "1f987", // 🦇
220 | "1f426", // 🐦
221 | "1f54a-fe0f", // 🕊
222 | "1f989", // 🦉
223 | "1f9a9", // 🦩
224 | "1f427", // 🐧
225 | "1f41f", // 🐟
226 | "1f99e", // 🦞
227 | "1f980", // 🦀
228 | "1f419", // 🐙
229 | "1fab8", // 🪸
230 | "1f982", // 🦂
231 | "1f577-fe0f", // 🕷️
232 | "1f41a", // 🐚
233 | "1f40c", // 🐌
234 | "1f997", // 🦗
235 | "1fab2", // 🪲
236 | "1fab3", // 🪳
237 | "1f41d", // 🐝
238 | "1f41e", // 🐞
239 | "1f98b", // 🦋
240 | "1f43e", // 🐾
241 | "1f353", // 🍓
242 | "1f352", // 🍒
243 | "1f349", // 🍉
244 | "1f34a", // 🍊
245 | "1f96d", // 🥭
246 | "1f34d", // 🍍
247 | "1f34c", // 🍌
248 | "1f34b", // 🍋
249 | "1f348", // 🍈
250 | "1f350", // 🍐
251 | "1f95d", // 🥝
252 | "1fad2", // 🫒
253 | "1fad0", // 🫐
254 | "1f347", // 🍇
255 | "1f965", // 🥥
256 | "1f345", // 🍅
257 | "1f336-fe0f", // 🌶️
258 | "1f955", // 🥕
259 | "1f360", // 🍠
260 | "1f9c5", // 🧅
261 | "1f33d", // 🌽
262 | "1f966", // 🥦
263 | "1f952", // 🥒
264 | "1fad1", // 🫑
265 | "1f951", // 🥑
266 | "1f9c4", // 🧄
267 | "1f954", // 🥔
268 | "1fad8", // 🫘
269 | "1f330", // 🌰
270 | "1f95c", // 🥜
271 | "1f35e", // 🍞
272 | "1fad3", // 🫓
273 | "1f950", // 🥐
274 | "1f96f", // 🥯
275 | "1f95e", // 🥞
276 | "1f373", // 🍳
277 | "1f9c0", // 🧀
278 | "1f969", // 🥩
279 | "1f356", // 🍖
280 | "1f354", // 🍔
281 | "1f32d", // 🌭
282 | "1f96a", // 🥪
283 | "1f968", // 🥨
284 | "1f35f", // 🍟
285 | "1fad4", // 🫔
286 | "1f32e", // 🌮
287 | "1f32f", // 🌯
288 | "1f959", // 🥙
289 | "1f9c6", // 🧆
290 | "1f958", // 🥘
291 | "1f35d", // 🍝
292 | "1f96b", // 🥫
293 | "1fad5", // 🫕
294 | "1f963", // 🥣
295 | "1f957", // 🥗
296 | "1f372", // 🍲
297 | "1f35b", // 🍛
298 | "1f35c", // 🍜
299 | "1f363", // 🍣
300 | "1f364", // 🍤
301 | "1f35a", // 🍚
302 | "1f371", // 🍱
303 | "1f359", // 🍙
304 | "1f358", // 🍘
305 | "1f365", // 🍥
306 | "1f960", // 🥠
307 | "1f367", // 🍧
308 | "1f368", // 🍨
309 | "1f366", // 🍦
310 | "1f370", // 🍰
311 | "1f382", // 🎂
312 | "1f9c1", // 🧁
313 | "1f36b", // 🍫
314 | "1f369", // 🍩
315 | "1f36a", // 🍪
316 | "1f9c2", // 🧂
317 | "1f37f", // 🍿
318 | "1f9cb", // 🧋
319 | "1f37c", // 🍼
320 | "1f375", // 🍵
321 | "2615", // ☕
322 | "1f9c9", // 🧉
323 | "1f379", //🍹
324 | "1f37d-fe0f", // 🍽️
325 | "1f6d1", // 🛑
326 | "1f6a8", // 🚨
327 | "1f6df", // 🛟
328 | "2693", // ⚓
329 | "1f697", // 🚗
330 | "1f3ce-fe0f", // 🏎️
331 | "1f695", // 🚕
332 | "1f68c", // 🚌
333 | "1f682", // 🚂
334 | "1f6f8", // 🛸
335 | "1f680", // 🚀
336 | "2708-fe0f", // ✈️
337 | "1f3a2", // 🎢
338 | "1f3a1", // 🎡
339 | "1f3aa", // 🎪
340 | "1f3db-fe0f", // 🏛️
341 | "1f3df-fe0f", // 🏟️
342 | "1f3e0", // 🏠
343 | "1f3d5-fe0f", // 🏕️
344 | "1f3dd-fe0f", // 🏝️
345 | "1f307", // 🌇
346 | "1f388", // 🎈
347 | "1f380", // 🎀
348 | "1f381", // 🎁
349 | "1faa9", // 🪩
350 | "1f397-fe0f", // 🎗️
351 | "1f947", // 🥇
352 | "1f948", // 🥈
353 | "1f949", // 🥉
354 | "1f3c5", // 🏅
355 | "1f396-fe0f", // 🎖
356 | "1f3c6", // 🏆
357 | "26bd", // ⚽
358 | "26be", // ⚾
359 | "1f94e", // 🥎
360 | "1f3c0", // 🏀
361 | "1f3d0", // 🏐
362 | "1f3c8", // 🏈
363 | "1f3c9", // 🏉
364 | "1f3be", // 🎾
365 | "1f945", // 🥅
366 | "1f3f8", // 🏸
367 | "1f94d", // 🥍
368 | "1f3cf", // 🏏
369 | "1f3d1", // 🏑
370 | "1f3d2", // 🏒
371 | "1f94c", // 🥌
372 | "1f6f7", // 🛷
373 | "1f3bf", // 🎿
374 | "26f8-fe0f", // ⛸️
375 | "1f6fc", // 🛼
376 | "1fa70", // 🩰
377 | "1f6f9", // 🛹
378 | "26f3", // ⛳
379 | "1f3af", // 🎯
380 | "1f3f9", // 🏹
381 | "1f94f", // 🥏
382 | "1fa83", // 🪃
383 | "1fa81", // 🪁
384 | "1f93f", // 🤿
385 | "1f3bd", // 🎽
386 | "1f94b", // 🥋
387 | "1f94a", // 🥊
388 | "1f3b1", // 🎱
389 | "1f3d3", // 🏓
390 | "1f3b3", // 🎳
391 | "265f-fe0f", // ♟️
392 | "1fa80", // 🪀
393 | "1f9e9", // 🧩
394 | "1f3ae", // 🎮
395 | "1f3b2", // 🎲
396 | "1f3b0", // 🎰
397 | "1f3b4", // 🎴
398 | "1f004", // 🀄
399 | "1f0cf", // 🃏
400 | "1fa84", // 🪄
401 | "1f4f7", // 📷
402 | "1f3a8", // 🎨
403 | "1f58c-fe0f", // 🖌️
404 | "1f58d-fe0f", // 🖍️
405 | "1faa1", // 🪡
406 | "1f9f5", // 🧵
407 | "1f9f6", // 🧶
408 | "1f3b9", // 🎹
409 | "1f3b7", // 🎷
410 | "1f3ba", // 🎺
411 | "1f3b8", // 🎸
412 | "1fa95", // 🪕
413 | "1f3bb", // 🎻
414 | "1fa98", // 🪘
415 | "1f941", // 🥁
416 | "1fa97", // 🪗
417 | "1f3a4", // 🎤
418 | "1f3a7", // 🎧
419 | "1f399-fe0f", // 🎙️
420 | "1f4fa", // 📺
421 | "1f39e-fe0f", // 🎞️
422 | "1f3ac", // 🎬
423 | "1f3ad", // 🎭
424 | "1f39f-fe0f", // 🎟️
425 | "1f4f1", // 📱
426 | "260e-fe0f", // ☎️
427 | "1f50b", // 🔋
428 | "1faab", // 🪫
429 | "1f4bf", // 💿
430 | "1f4be", // 💾
431 | "1f4b8", // 💸
432 | "2696-fe0f", // ⚖️
433 | "1f4a1", // 💡
434 | "1f9fc", // 🧼
435 | "1f9e6", // 🧦
436 | "1f451", // 👑
437 | "2602-fe0f", // ☂️
438 | "1f48e", // 💎
439 | "1f6e0-fe0f", // 🛠️
440 | "26d3-fe0f", // ⛓️
441 | "1f5d1-fe0f", // 🗑️
442 | "1f58a-fe0f", // 🖊️
443 | "2712-fe0f", // ✒️
444 | "270f-fe0f", // ✏️
445 | "1f4da", // 📚
446 | "1f5c3-fe0f", // 🗃️
447 | "1f4f0", // 📰
448 | "1f50e", // 🔎
449 | "1f9ff", // 🧿
450 | "1f52e", // 🔮
451 | "1f5dd-fe0f", // 🗝️
452 | "1f512", // 🔒
453 | "2648", // ♈
454 | "2649", // ♉
455 | "264a", // ♊
456 | "264b", // ♋
457 | "264c", // ♌
458 | "264d", // ♍
459 | "264e", // ♎
460 | "264f", // ♏
461 | "2650", // ♐
462 | "2651", // ♑
463 | "2652", // ♒
464 | "2653", // ♓
465 | "26ce", // ⛎
466 | "2757", // ❗
467 | "2753", // ❓
468 | "2049-fe0f", // ⁉️
469 | "1f198", // 🆘
470 | "1f4f4", // 📴
471 | "1f508", // 🔈
472 | "26a0-fe0f", // ⚠️
473 | "267b-fe0f", // ♻️
474 | "2705", // ✅
475 | "1f195", // 🆕
476 | "1f193", // 🆓
477 | "1f199", // 🆙
478 | "1f197", // 🆗
479 | "1f192", // 🆒
480 | "1f6ae", // 🚮
481 | "262e-fe0f", // ☮️
482 | "262f-fe0f", // ☯️
483 | "267e-fe0f", // ♾️
484 | "2716-fe0f", // ✖️
485 | "2795", // ➕
486 | "2796", // ➖
487 | "2797", // ➗
488 | "27b0", // ➰
489 | "27bf", // ➿
490 | "3030-fe0f", // 〰️
491 | "00a9-fe0f", // ©️
492 | "00ae-fe0f", // ®️
493 | "2122-fe0f", // ™️
494 | "2660-fe0f", // ♠️
495 | "1f5ef-fe0f", // 🗯️
496 | "1f4ac", // 💬
497 | ];
--------------------------------------------------------------------------------
/miniprogram/pages/learn/data.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | "no": 0,
4 | "name": "油温判断技巧",
5 | "content": [
6 | {
7 | "type": "heading",
8 | "text": "油温判断技巧",
9 | "depth": 1
10 | },
11 | {
12 | "type": "list",
13 | "items": [
14 | "油温在 120°C-140°C 之间:适合软炸、滑炒, 把筷子放入油锅中,周围基本不起泡泡,无青烟、无响声、油温平静。",
15 | "油温在 150°C-160°C 之间:最佳烹饪温度,把筷子放入油锅中,周围会冒出少许油泡,略有青烟,油从四周往中间翻动。",
16 | "油温在 160°C-180°C 之间:适合上色炸酥,把筷子放入油锅中,大量青烟上升,油面反而较平静。",
17 | "(注) 最好买把油温枪,谨慎使用温度计。"
18 | ]
19 | },
20 | {
21 | "type": "blockquote",
22 | "text": "网络视频教程中所谓的 “几成油温” 指的是相对于十成油温,为 300°C。\n"
23 | }
24 | ]
25 | },
26 | {
27 | "no": 1,
28 | "name": "辅料技巧",
29 | "content": [
30 | {
31 | "type": "heading",
32 | "text": "辅料技巧",
33 | "depth": 1
34 | },
35 | {
36 | "type": "list",
37 | "items": [
38 | "辅料的放入顺序基本为下:先放姜、后放葱和蒜、辣椒、再放干料(八角/花椒/麻椒)、再放干辣椒。以上每一步骤根据所做菜的不同,可以把不需要的辅料从队列中被移除。",
39 | "姜的含水量是最大的,这意味着我们需要更多的时间将姜的汁水煸出。",
40 | "如果你使用的是葱段(葱段最好使用菜刀拍两下),那么我推荐你先放入葱段,再放入蒜碎,如果你使用的是葱花,那么可以将这两种辅料一起下锅。注:葱段中的汁水更难被炒出。",
41 | "如果你做的是炒菜,那么我更推荐你在没放姜之前先放入干料,这可以让油变得更有味道,以至于炒出来的菜更香。为什么炖菜、焖菜不这样做,是因为你可能需要翻炒很多辅料,以至于辅料翻炒时间过长导致干料变黑、变苦。",
42 | "将干辣椒放在最后是因为干辣椒很容易因为锅的温度而变黑,干辣椒稍微翻炒几秒钟即可。",
43 | "注:不论你喜欢做什么菜系,小火将这些辅料炒至金黄,都可以将整道菜变得更有香味,这是调料所不能给予的。",
44 | [
45 | "放盐时机与盐量控制",
46 | {
47 | "type": "list",
48 | "items": [
49 | "快炒料理先加盐,盐量=食材总重量x0.9%;",
50 | "肉食料理八成熟时加盐,盐量=(食材总重量+30mL汁水)x(1~1.2%); ",
51 | "汤料理最后时再加盐,盐量=最开始的水量x0.8%。",
52 | [
53 | "一天的总盐量不建议超过5g,参考",
54 | {
55 | "type": "link",
56 | "text": "WHO",
57 | "href": "https://www.who.int/news-room/fact-sheets/detail/salt-reduction"
58 | }
59 | ]
60 | ]
61 | }
62 | ]
63 | ]
64 | }
65 | ]
66 | },
67 | {
68 | "no": 2,
69 | "name": "学习炒与煎",
70 | "content": [
71 | {
72 | "type": "heading",
73 | "text": "炒/煎",
74 | "depth": 1
75 | },
76 | {
77 | "type": "heading",
78 | "text": "器具",
79 | "depth": 2
80 | },
81 | "可使用普通金属制(铁/不锈钢/铝)炒/煎锅或不粘锅。",
82 | {
83 | "type": "heading",
84 | "text": "注意事项",
85 | "depth": 3
86 | },
87 | {
88 | "type": "list",
89 | "items": [
90 | "使用普通锅炒菜不粘的方法:"
91 | ]
92 | },
93 | {
94 | "type": "heading",
95 | "text": "先炒鸡蛋法",
96 | "depth": 4
97 | },
98 | {
99 | "type": "list",
100 | "items": [
101 | "不管你炒什么菜之前都炒个鸡蛋,炒完不刷锅,再炒下个菜时就不粘。"
102 | ]
103 | },
104 | {
105 | "type": "heading",
106 | "text": "热锅凉油法",
107 | "depth": 4
108 | },
109 | {
110 | "type": "list",
111 | "items": [
112 | "记住一定要是热锅凉油,首先热锅",
113 | "干净的锅什么都不放,干烧,使其受热均匀,烧热",
114 | "放入凉油,旋转锅子,使油沾满整个锅(可以来回旋转使其受热均匀)",
115 | "看到有气体从锅中发出时,就表示锅子的油已经烧热了",
116 | "把油倒出来,倒出来后不要刷锅",
117 | "可以重复上述步骤 2-3 遍以得到更好的不粘效果",
118 | "注意:如果是燃气,可能会喷火,注意安全"
119 | ]
120 | },
121 | {
122 | "type": "heading",
123 | "text": "热锅双油法",
124 | "depth": 4
125 | },
126 | {
127 | "type": "list",
128 | "items": [
129 | [
130 | "首先热锅",
131 | {
132 | "type": "list",
133 | "items": [
134 | "干净的锅什么都不放,干烧,使其受热均匀,烧热",
135 | "放入“少量凉油”,旋转锅子,使油沾满整个锅(可以来回旋转使其受热均匀)",
136 | "看到有气体从锅中发出时,就表示锅子的油已经烧热了",
137 | "再继续放入凉油,开始炒菜",
138 | "注意:如果是燃气,可能会喷火,注意安全。"
139 | ]
140 | }
141 | ]
142 | ]
143 | },
144 | "补充:",
145 | {
146 | "type": "list",
147 | "items": [
148 | "目的是使油挂满锅底,所有市面上的家用锅都适用,挂油后秒变不粘锅。",
149 | [
150 | "使用不粘锅煎炒食物不会粘锅。不粘锅的功能来源于其内壁上的涂层。",
151 | {
152 | "type": "strong",
153 | "text": "金属锅铲会划伤涂层。使用不粘锅时应使用木制或硅胶锅铲以避免损坏涂层。"
154 | }
155 | ]
156 | ]
157 | },
158 | {
159 | "type": "heading",
160 | "text": "流程",
161 | "depth": 3
162 | },
163 | "开火——直接将锅平放于火上,烧热——将油倒入锅中,烧热——放入菜品,翻炒——出锅前记得放调料",
164 | {
165 | "type": "heading",
166 | "text": "注意事项",
167 | "depth": 3
168 | },
169 | {
170 | "type": "list",
171 | "items": [
172 | "判断锅/油是否烧热时,可将手平放于锅的上方感受热量;油热后方可放入食材。",
173 | [
174 | "倒油入锅前,务必确认锅的内部没有残余水份。",
175 | {
176 | "type": "strong",
177 | "text": "水会导致热油飞溅,造成危险。"
178 | }
179 | ],
180 | "接上条,食材放入油锅前,应当沥干水份(蛋液没事);同理,不可将未解冻的食材放入油锅,以免冰化后造成危险。",
181 | [
182 | {
183 | "type": "strong",
184 | "text": "若油锅起火,切不可倒水灭火"
185 | },
186 | "。这样做会使火势扩大。火刚起时,可迅速关火,盖上锅盖。"
187 | ]
188 | ]
189 | }
190 | ]
191 | },
192 | {
193 | "no": 3,
194 | "name": "学习焯水",
195 | "content": [
196 | {
197 | "type": "heading",
198 | "text": "焯水",
199 | "depth": 1
200 | },
201 | "焯水是做饭的一道工序,读作 chāo shuǐ。",
202 | "焯水指将初步加工的原料放在开水锅中加热至半熟或全熟,取出以备进一步烹调或调味。 它是烹调中特别是冷拌菜不可缺少的一道工序。 对菜肴的色、香、味,特别是色起着关键作用。 ",
203 | "大部分蔬菜和带有腥羶气味的肉类原料都需要焯水。",
204 | {
205 | "type": "heading",
206 | "text": "操作",
207 | "depth": 2
208 | },
209 | {
210 | "type": "heading",
211 | "text": "开水锅焯水",
212 | "depth": 3
213 | },
214 | "开水锅焯水,就是将锅内的水加热,然后将原料下锅。下锅后及时翻动,时间要短,不要过火。",
215 | "这种方法多用于植物性原料,如:芹菜、菠菜、莴笋等。 焯水时要特别注意火候,时间稍长,颜色就会变淡,而且也不脆、嫩。 因此放入锅内后,水微开时即可捞出晾凉。",
216 | {
217 | "type": "list",
218 | "items": [
219 | "叶类蔬菜原料应先焯水再切片,以免营养成分损失过多。",
220 | "焯水时应水宽火旺,以使投入原料后能及时开锅;焯制绿叶蔬菜时,应略滚即捞出。",
221 | "蔬菜类原料在焯水后应立即投凉控干,以免因余热而使之变黄、熟烂的现象发生。",
222 | "蔬菜焯水可以放入适量色拉油如花生油、玉米油、大豆油以保持翠绿。"
223 | ]
224 | },
225 | {
226 | "type": "heading",
227 | "text": "冷水锅焯水",
228 | "depth": 3
229 | },
230 | "冷水锅焯水是将原料与冷水同时下锅。 水要没过原料,然后烧开,目的是使原料成熟,便于进一步加工。",
231 | "土豆、胡萝卜等因体积大,不易成熟,需要煮的时间长一些。",
232 | "有些动物性原料,如:白肉、牛百页、牛肚领等,也是冷水下锅加热成熟后再进一步加工的。有些用于煮汤的动物性原料也要冷水下锅,在加热过程中使营养物质逐渐溢出,使汤味鲜美,如用热水锅,则会造成蛋白质凝固。",
233 | {
234 | "type": "list",
235 | "items": [
236 | "锅内的加水量不宜过多,以淹没原料为度。",
237 | "在逐渐加热过程中,必须对原料勤翻动,以使原料受热均匀,达到焯水的目的。"
238 | ]
239 | },
240 | {
241 | "type": "heading",
242 | "text": "额外注意事项",
243 | "depth": 2
244 | },
245 | {
246 | "type": "list",
247 | "items": [
248 | "焯水有时也会使原料内的一些不稳定、可溶性营养物质溢出,特别是新鲜蔬菜中的水溶性维生素更容易受到损失",
249 | "动物类原料与植物类原料要分别焯水;色味较重的与色味较轻的要分别焯水;块状大的要与块状小的分别焯水,以防彼此串味",
250 | "焯制动物性原料后,汤汁可在撇沫澄清后作为鲜汤使用"
251 | ]
252 | },
253 | {
254 | "type": "heading",
255 | "text": "肉的焯水",
256 | "depth": 3
257 | },
258 | {
259 | "type": "list",
260 | "items": [
261 | "肉类原料经过开水焯过后变色即可,捞出沥干水分后可以进行下一步的烹调。",
262 | "肉类焯水后需要洗去沾附的血沫污渍,记得用温水清洗,否则肉热胀冷缩会吸附污渍,导致无法洗净血沫。"
263 | ]
264 | },
265 | {
266 | "type": "heading",
267 | "text": "青菜的焯水",
268 | "depth": 3
269 | },
270 | {
271 | "type": "list",
272 | "items": [
273 | "洗青菜时,在清水里撒一些盐,这样可以把青菜里的虫子清洗出来",
274 | "焯过后的青菜应立即浸入冷水中,以保持颜色和口感。如果不用冷水浸,青菜会因为开水的余温变的不再清脆,而出现烂烂的感觉"
275 | ]
276 | }
277 | ]
278 | },
279 | {
280 | "no": 4,
281 | "name": "学习煮",
282 | "content": [
283 | {
284 | "type": "heading",
285 | "text": "煮",
286 | "depth": 1
287 | },
288 | {
289 | "type": "heading",
290 | "text": "流程",
291 | "depth": 2
292 | },
293 | "倒水入锅——开火,将锅放于火上加热——水开(水翻滚,有大量气泡冒出)后放入食材",
294 | {
295 | "type": "heading",
296 | "text": "注意事项",
297 | "depth": 3
298 | },
299 | {
300 | "type": "list",
301 | "items": [
302 | [
303 | "加热时盖上锅盖可以加快受热。",
304 | {
305 | "type": "strong",
306 | "text": "但这样做有溢锅的风险"
307 | },
308 | "。持续加热后,过渡翻腾的流体可能会冒出锅外,这就是溢锅。"
309 | ],
310 | {
311 | "type": "strong",
312 | "text": "若即将溢锅,立刻关小火并打开锅盖即可。"
313 | },
314 | "想要加快受热又避免溢锅,可以半开锅盖,留出气体出口;也可在后期关小火,并时时注意锅中情况。",
315 | "根据烹饪需要,食材也可冷水下锅。不过这样水烧开需要的时间更久。"
316 | ]
317 | }
318 | ]
319 | },
320 | {
321 | "no": 5,
322 | "name": "学习蒸",
323 | "content": [
324 | {
325 | "type": "heading",
326 | "text": "蒸",
327 | "depth": 1
328 | },
329 | {
330 | "type": "heading",
331 | "text": "方式",
332 | "depth": 2
333 | },
334 | {
335 | "type": "heading",
336 | "text": "蒸锅",
337 | "depth": 3
338 | },
339 | "蒸锅为多层结构,最底部用于盛水,利用水开后产生的水蒸气的热量,加热上层食物。",
340 | {
341 | "type": "list",
342 | "items": [
343 | "蒸锅最底层加入适量水",
344 | "将食物放于上层蒸屉中",
345 | "蒸锅放于火上加热"
346 | ]
347 | },
348 | {
349 | "type": "heading",
350 | "text": "铁锅",
351 | "depth": 3
352 | },
353 | "如果没有蒸锅,只有普通的铁锅(非平底锅),可以在锅底放置一个三脚架,并注入足够的水,以此达到类似于蒸锅的效果。",
354 | {
355 | "type": "list",
356 | "items": [
357 | "铁锅底部加入足量水",
358 | "放入三脚架",
359 | "将食物置于三脚架上",
360 | "开火关盖"
361 | ]
362 | },
363 | "",
364 | {
365 | "type": "heading",
366 | "text": "注意事项",
367 | "depth": 2
368 | },
369 | {
370 | "type": "list",
371 | "items": [
372 | "由于热源为水蒸气,较低的蒸屉中的食物底部可能被水浸湿。可将蒸笼布放在食物底下以避免这种情况。用筷子搭个放食物的简易支架也可以。",
373 | "可以利用智能设备设置计时器,提醒关火,以防忘记以致烧干。",
374 | [
375 | "在使用蒸笼制作食品的过程中,需要注意底部区域的剩余水量,",
376 | {
377 | "type": "strong",
378 | "text": "特别是铁锅"
379 | },
380 | ",避免干锅从而造成安全问题。"
381 | ],
382 | [
383 | "(可选)使用铁锅蒸食物时,可以在三脚架上面放置一个 ",
384 | {
385 | "type": "image",
386 | "text": "蒸盘",
387 | "href": "cloud://cloud1-9g17b7v0e010c809.636c-cloud1-9g17b7v0e010c809-1309933988/q-sign-algorithm=sha1&q-ak=AKIDBBD-eV03JaQ4CkqSBvhzPE7AEwgfblokvbQMZ6lMJdBChPChOSftKjkLccDqruQ8&q-sign-time=1647237321;1647238221&q-key-time=1647237321;1647238221&q-header-list=&q-url-param-list=&q-signature=6cf910a6d0385ecf9aca48d5fde797073c71213c"
388 | },
389 | "。"
390 | ]
391 | ]
392 | }
393 | ]
394 | },
395 | {
396 | "no": 6,
397 | "name": "学习腌",
398 | "content": [
399 | {
400 | "type": "heading",
401 | "text": "腌(肉)",
402 | "depth": 1
403 | },
404 | {
405 | "type": "heading",
406 | "text": "注意",
407 | "depth": 2
408 | },
409 | "此处所描述的腌渍是食材烹饪前处理的步骤,并非制作咸肉或腌制香肠等成品",
410 | {
411 | "type": "heading",
412 | "text": "腌渍",
413 | "depth": 2
414 | },
415 | "在烹饪前腌制肉类是让肉类预先入味的常用方法。一般腌渍的对象是生肉。根据菜品的需求,可以自行确定肉类改刀的大小。",
416 | " 例如炸鸡米花,鸡胸肉是在改刀为骰子大小的小块后放入碗中腌渍\n 例如烤全羊,羊腿,半扇或整扇羊肉不必改刀即可用大量调味料涂抹在表面从而腌渍入味",
417 | "根据菜品的不同,腌渍所选的调味料、辅料可以是任何种类。有时候为了不同的口味,辅料也可能需要预先处理。",
418 | {
419 | "type": "heading",
420 | "text": "腌渍基本概念",
421 | "depth": 2
422 | },
423 | "此处介绍的是正常口味的腌渍过程。",
424 | {
425 | "type": "list",
426 | "items": [
427 | "一般来说,肉量越大(比如一次性腌渍 5kg 鸡翅),体积越大(比如一整个羊腿),口味越重,则需要调味料和辅料越多",
428 | "一般来说,计划腌渍的时间越长,使用的调味料和辅料越少",
429 | "腌渍时应使用料均匀覆盖在所有的表面。如果是肉片、肉丝,应该用手尽量抓匀、搅匀。如果是整个羊腿,应该用手或刷子在表面刷匀",
430 | "一般炒肉、炸肉需要提前腌渍。炒肉应该保证肉鲜嫩的口感,烹调往往需要大火且时间较短。短时间烹饪不容易入味时,提前腌渍就能弥补口味的不足"
431 | ]
432 | },
433 | {
434 | "type": "heading",
435 | "text": "腌渍手法",
436 | "depth": 2
437 | },
438 | {
439 | "type": "list",
440 | "items": [
441 | "细肉丝、薄肉片:由于肉质较脆弱,需要尽量轻柔。手指呈娃娃机钳子的形状,轻微抓匀腌料。然后向一个方向轻轻搅匀即可",
442 | "肉丝、肉片、肉块:手法同上,但是力量可以稍大",
443 | "鸡腿、鸡翅等大小:先在食材上改几道花刀。鸡翅根、鸡腿可以用刀扎对穿孔。然后先在碗里混合好腌料,在把食材放入料碗中裹匀",
444 | "羊腿等大小:一般可以在肉较厚的位置扎对穿孔。然后腌料混合好后均匀涂抹在食材表面"
445 | ]
446 | },
447 | {
448 | "type": "heading",
449 | "text": "腌渍容器及时间",
450 | "depth": 2
451 | },
452 | {
453 | "type": "list",
454 | "items": [
455 | "选择能装下食材和腌料的容器即可。包括碗、盘子、托盘等。此时是开口腌渍,一般时间较短,常见 0.5-2 小时的腌渍时间。(烤)羊腿等也可以如此腌渍,但时间较长",
456 | "可以选择足够大的食品密封袋腌渍。此时是封口腌渍,一般时间很长,例如隔夜腌渍,或腌渍不易入味的排骨等。常见 4 小时-隔夜。此时用料要稍微减少,防止成菜口感太重太咸"
457 | ]
458 | },
459 | {
460 | "type": "heading",
461 | "text": "常用的腌渍用料",
462 | "depth": 2
463 | },
464 | {
465 | "type": "list",
466 | "items": [
467 | "生抽:调酱香且带有咸味的底味。可用于几乎所有肉类",
468 | "老抽:咸味并不强烈,但是易于染色。用于调底色和增香。一般不应大量使用防止产生豆腥味。可用于几乎所有红肉类(较少用),猪肝等可以多加",
469 | "食盐:咸味但炒制后不带有酱香味。可用于所有肉类",
470 | "白(砂)糖:调甜味(量大),也可以为肉增加鲜嫩的口感(量少)。可用于所有禽畜类肉类,但鱼类和海鲜并不常用。",
471 | "红糖:调甜味和红糖特有的口感,口味比白糖略重。可用于几乎所有肉类(一般肉色较深或者成菜颜色较深)",
472 | "蚝油:增加鲜、咸、甜的口味。一般用于红肉",
473 | "白醋/米醋:增加酸的口味。较少使用",
474 | "陈醋/香醋:不仅带有酸的口味,还能为菜品增香增色。香醋比较适合深色鱼类(尤其是烤鱼)",
475 | "料酒:去腥增香。可用于几乎所有肉类。但白色肉类应该少用",
476 | "黄酒:去腥增香,效果比料酒更好,香味比料酒更复杂。一般用于白肉类。红肉也可用,但是效果与料酒相当。",
477 | "五香粉/十三香:为肉类增加香味,是最简单的复合香料。五香粉仅仅增加香味,十三香的香味比较独特,有辨识度。用此类香料腌渍应该控制用量。可用于几乎所有肉类,但鱼类和海鲜不常用",
478 | "辣椒粉:辣椒粉分为很多种。不谈辣椒的种类,从研磨精细度划分有辣椒粉/辣椒面,辣椒碎等。除了为肉类增加辣味,还能为成菜配色。用辣椒腌渍的菜品应该避免辣椒过量。可用于几乎所有需要辣味底味的肉类,但烹调时间应该略加控制,防止辣椒味道变苦,或者颜色变深",
479 | "孜然粉、小茴香粉:一般用磨粉作为腌料,不用颗粒,这样可以使肉类更容易入味。可用于几乎所有红肉和鸡肉",
480 | [
481 | "X 椒粉:为肉类增加辛、辣、香、呛的口味。使用应该适量,防止盖过其他口味",
482 | {
483 | "type": "list",
484 | "items": [
485 | "黑胡椒粉:口味辛、辣。可用于几乎所有红肉",
486 | "白胡椒粉:口味辛、香。比黑胡椒略弱,突出香味。可用于几乎所有肉类",
487 | "花椒粉:口味辛、呛。有花椒特殊的香味,比较有辨识度。可用于几乎所有肉类"
488 | ]
489 | }
490 | ],
491 | "豆瓣酱:为肉类增加豆类的酱香和咸味、辣味。可用于几乎所有红肉类",
492 | "葱姜蒜:葱姜去腥增香,去除异味;蒜增加辛香味。葱可根据需要切段或者切片;姜一般切片,有些场景需要去皮;蒜可切片或切碎。葱姜不想出现在成菜中,或者口味需要较轻,可以将葱姜块放于有极少量清水的碗里挤压出汁,用葱姜水腌渍肉类。蒜一般不直接加入。可用于所有肉类",
493 | "海鲜酱、虾酱等:为肉类增加鲜、咸味。海鲜酱口味偏甜,虾酱口味偏重。可用于几乎所有肉类,但使用场景不多",
494 | "豆豉:为肉类增加发酵豆类的香味和咸味。可用于几乎所有红肉类,但是使用的并不多",
495 | [
496 | [
497 | "生粉:即为淀粉。生粉是上浆的重要腌料。上浆越厚,或者需要口感越滑嫩,需要的生粉越多。可用于几乎所有肉类。",
498 | {
499 | "type": "codespan",
500 | "text": "生粉可作为简易的油炸外衣使用(一般根据需要还需加入面粉等),此时一般不在腌渍时加入"
501 | }
502 | ],
503 | {
504 | "type": "list",
505 | "items": [
506 | "玉米淀粉、土豆淀粉:粘性一般最大",
507 | "红薯淀粉:粘性略低"
508 | ]
509 | }
510 | ],
511 | "油:在腌渍时加入适量油进行油封,可以锁住水分和风味。如使用开口容器腌渍,且时间较长(例如在碗里),油封能极大程度保证肉质不变干或变柴。可用于几乎所有肉类。油封后炒制应略微减少底油,油炸则没有区别"
512 | ]
513 | },
514 | {
515 | "type": "heading",
516 | "text": "几种较为通用的腌渍公式",
517 | "depth": 2
518 | },
519 | {
520 | "type": "list",
521 | "items": [
522 | "牛肉:使用适量生抽,少量料酒,少量白砂糖腌渍。根据口味选用食盐(补充咸味),蚝油和极少量海鲜酱(蚝油牛肉),五香粉/十三香(洋葱炒牛肉)。慎用葱姜",
523 | "鸡肉(包括鸡胸肉和鸡翅):使用适量生抽,较少量白砂糖,少量料酒腌渍。根据口味选用食盐(补充咸味),五香粉/十三香(炸鸡米花),极少量老抽(香煎鸡翅中)",
524 | "白色鱼肉:使用适量食盐,少量料酒/黄酒腌渍。根据口味选用海鲜酱/海鲜酱油/蒸鱼豉油(香煎带鱼),葱姜(水)(烤带皮鱼肉)",
525 | "红色鱼肉:使用适量生抽,少量料酒腌渍。根据口味选用海鲜酱油/少量蒸鱼豉油(香煎三文鱼),红糖(北欧香烤三文鱼)",
526 | "猪肝:使用适量生抽,适量料酒腌渍。根据口味选用生粉和适量老抽(滑炒猪肝),少量糖等"
527 | ]
528 | },
529 | {
530 | "type": "heading",
531 | "text": "菜品实战示例",
532 | "depth": 2
533 | },
534 | {
535 | "type": "list",
536 | "items": [
537 | [
538 | "洋葱炒牛肉:以一人份的 150g 牛肉为例。牛肉应切片,成菜口感应嫩滑,需炒制",
539 | {
540 | "type": "list",
541 | "items": [
542 | "生抽 10ml(约 2 汤匙)",
543 | "料酒 5ml(约 1 汤匙)",
544 | "白砂糖 2.5-10g(约 1-4 茶匙,根据口味甜度选择)",
545 | "孜然粉 5g(约 2 茶匙)",
546 | "生粉 10-15g(约 1 小把)",
547 | "油 10ml(约 2 汤匙)",
548 | "(可选)十三香 1g(约 0.5 茶匙)",
549 | "(可选)黑胡椒粉 1g(约 0.5 茶匙)"
550 | ]
551 | }
552 | ],
553 | [
554 | "蚝油牛肉:以一人份的 150g 牛肉为例。牛肉应切片,成才口感应嫩滑且上浆感足,此菜口感偏甜,需炒制",
555 | {
556 | "type": "list",
557 | "items": [
558 | "生抽 5ml(约 1 汤匙)",
559 | "料酒 5ml(约 1 汤匙)",
560 | "蚝油 10-20ml(约 2-4 汤匙,根据口味咸度选择,蚝油比较咸)",
561 | "白砂糖 5-15g(约 2-6 茶匙,根据口味甜度选择)",
562 | "生粉 25-35g(约 1 大把)",
563 | "油 10ml(约 2 汤匙)"
564 | ]
565 | }
566 | ],
567 | [
568 | "五香盐酥鸡:以一人份的 150g 鸡胸肉为例。鸡肉应切成骰子形状,需炸制",
569 | {
570 | "type": "list",
571 | "items": [
572 | "生抽 10ml(约 2 汤匙)",
573 | "料酒 2.5ml(约 0.5 汤匙)",
574 | "五香粉 5g(约 2 茶匙)或十三香 2.5-5g(约 1-2 茶匙)",
575 | "(可选)孜然粉 1g(约 0.5 茶匙)",
576 | "(可选)白胡椒粉 1g(约 0.5 茶匙)"
577 | ]
578 | }
579 | ],
580 | [
581 | "蜜汁烤鸡翅:以一人份的 250g 带骨鸡翅中为例。鸡翅上应切几道花刀,成菜咸甜,但突出甜口,需烤制",
582 | {
583 | "type": "list",
584 | "items": [
585 | "生抽 10ml(约 2 汤匙)",
586 | "料酒 2.5ml(约 0.5 汤匙)",
587 | "白砂糖 5-15g(约 2-6 茶匙,根据口味甜度选择)",
588 | "蜂蜜/糖浆 10-20ml(约 2-4 汤匙,根据口味甜度选择。如白砂糖超过或等于 10g,建议只加入 10ml)",
589 | "(可选)五香粉 2.5g(约 1 茶匙。不可用十三香)"
590 | ]
591 | }
592 | ],
593 | [
594 | "香烤三文鱼:以一人份的 200g 去骨三文鱼排为例。鱼肉不应改刀,需烤箱烤制",
595 | {
596 | "type": "list",
597 | "items": [
598 | "生抽 10ml(约 2 汤匙)",
599 | "料酒 2.5ml(约 0.5 汤匙)",
600 | "红糖 10-20g(约 4-8 茶匙,根据口味甜度选择)",
601 | "意大利黑醋/镇江香醋 2.5-5ml(约 0.5-1 汤匙,根据口味酸度选择)",
602 | "肉豆蔻粉 2.5g(约 1 茶匙)",
603 | "百里香粉 1g(约 0.5 茶匙)",
604 | "姜粉 1g(约 0.5 茶匙)",
605 | "迷迭香粉 1-2g(约 0.5-1 茶匙)",
606 | "(可选)白胡椒粉 1g(约 0.5 茶匙)",
607 | "(可选)干辣椒碎 2.5-10g(约 1-4 茶匙,根据口味辣度选择)"
608 | ]
609 | }
610 | ]
611 | ]
612 | }
613 | ]
614 | },
615 | {
616 | "no": 7,
617 | "name": "微波炉",
618 | "content": [
619 | {
620 | "type": "heading",
621 | "text": "使用微波炉",
622 | "depth": 1
623 | },
624 | {
625 | "type": "heading",
626 | "text": "什么是微波炉",
627 | "depth": 2
628 | },
629 | [
630 | "微波炉是 1945 年由 ",
631 | {
632 | "type": "link",
633 | "text": "珀西·勒巴朗·斯宾塞",
634 | "href": "https://zh.wikipedia.org/wiki/%E7%8F%80%E8%A5%BF%C2%B7%E5%8B%92%E5%B7%B4%E6%9C%97%C2%B7%E6%96%AF%E8%B3%93%E5%A1%9E"
635 | },
636 | " 在担任雷达系统工程师时,由于发现雷达一开启他口袋里的巧克力棒就开始融化,从而产生构想并发明的。"
637 | ],
638 | {
639 | "type": "heading",
640 | "text": "工作方式",
641 | "depth": 3
642 | },
643 | "微波时通过磁控管制造的频率 24.5 亿赫兹的电磁波,这个频率会使水和油的分子振动并发热。",
644 | {
645 | "type": "heading",
646 | "text": "流程",
647 | "depth": 2
648 | },
649 | "微波炉在很多烹饪任务中效果相当出色。",
650 | "强火适用于:",
651 | {
652 | "type": "list",
653 | "items": [
654 | "[烹煮] 烹煮蔬菜",
655 | "[烹煮] 软化含水率高的硬质蔬菜(如马铃薯、洋葱和朝鲜蓟)",
656 | "[膨化] 爆点心,如泡芙、印度帕帕达姆薄脆饼、爆米花。"
657 | ]
658 | },
659 | "中火适用于:",
660 | {
661 | "type": "list",
662 | "items": [
663 | [
664 | "[烹煮] 海鲜 (例如 ",
665 | {
666 | "type": "page",
667 | "text": "微波葱姜黑鳕鱼",
668 | "href": "../../dishes/home-cooking/微波葱姜黑鳕鱼.md"
669 | },
670 | ")"
671 | ],
672 | "[烹煮] 软化肉类",
673 | "[脱水] 干燥蔬果皮",
674 | "[脱水] 制作肉干",
675 | "[炸] 炸脆香料植物",
676 | "[炸] 软化叶类蔬菜",
677 | "[加热] 加热剩菜"
678 | ]
679 | },
680 | "弱火用于:",
681 | {
682 | "type": "list",
683 | "items": [
684 | "[解冻] 解冻食物",
685 | "[解冻] 融化黄油和巧克力"
686 | ]
687 | },
688 | {
689 | "type": "heading",
690 | "text": "注意事项",
691 | "depth": 2
692 | },
693 | {
694 | "type": "list",
695 | "items": [
696 | "由于微波的波长为 12.2 cm,因此微波炉加热小物体的速度要比大物体慢。因此如果是很小的食材,建议聚集在一起进行加热。",
697 | "微波仅能深入食物几厘米,因此有时候外部很烫了,内部可能还是冰凉的。解决办法是将食材加上少量液体放进密封袋,或放入碗中后蒙上保鲜膜,让容器内产生足够的蒸汽来弥补微波炉容易烹饪不均的缺点。",
698 | "tips:打开密封袋时,当心蒸汽喷出",
699 | "微波只能加热水和油,因此保鲜膜和密封袋都不会被微波加热。但含金属的盘子会变烫甚至破裂,请务必使用瓷、玻璃容器或微波炉专用烤盘。"
700 | ]
701 | }
702 | ]
703 | },
704 | {
705 | "no": 8,
706 | "name": "食品安全",
707 | "content": [
708 | {
709 | "type": "heading",
710 | "text": "食品安全",
711 | "depth": 1
712 | },
713 | {
714 | "type": "heading",
715 | "text": "中毒",
716 | "depth": 2
717 | },
718 | "以下食物有造成中毒的风险:",
719 | {
720 | "type": "list",
721 | "items": [
722 | "未成熟的青西红柿",
723 | "未烹饪熟的四季豆",
724 | "未煮熟的豆角",
725 | "发芽的土豆",
726 | "生豆浆",
727 | "泡发时间过长的木耳",
728 | "……(欢迎补充)"
729 | ]
730 | },
731 | {
732 | "type": "heading",
733 | "text": "沙门氏菌感染",
734 | "depth": 2
735 | },
736 | "沙门氏菌较常见于动物源性食物,包括蔬菜也可能因受粪便污染而含有沙门氏菌。",
737 | "下列食品有造成沙门氏菌感染的风险:",
738 | {
739 | "type": "list",
740 | "items": [
741 | "未完全煮熟的蛋",
742 | "未完全煮熟的肉",
743 | "未经过杀菌的奶"
744 | ]
745 | },
746 | {
747 | "type": "heading",
748 | "text": "黄曲霉素",
749 | "depth": 2
750 | },
751 | "黄曲霉素常由黄曲霉及寄生曲霉等另外几种霉菌在霉变的谷物中产生,如大米、豆类、花生等,是目前为止最强的致癌物质。加热至 280℃以上才开始分解,所以一般的加热不易破坏其结构。",
752 | "下列食品有造成黄曲霉素中毒的风险:",
753 | {
754 | "type": "list",
755 | "items": [
756 | "腐坏的花生",
757 | "腐坏的大米",
758 | "腐坏的玉米"
759 | ]
760 | },
761 | {
762 | "type": "heading",
763 | "text": "寄生虫",
764 | "depth": 2
765 | },
766 | "寄生虫可通过空气,饮用水,食物和直接接触进入人体。若寄生虫进入人体循环系统,一方面可以攻击白细胞,另一方面可达肺、肝等脏器或是堵塞血管或淋巴管道,会引起如肝硬化、门脉高压、象皮病等疾病。而人如果是猪肉绦虫的中间宿主,寄生虫甚至会达眼球、心脏和大脑,危及生命。",
767 | "下列食品最好确保完全烧熟,否则可能在体内留下相应的寄生虫:",
768 | {
769 | "type": "list",
770 | "items": [
771 | "田螺:管圆线虫",
772 | "生鱼片:肝吸虫",
773 | "黄鳝:颚口线虫",
774 | "牛蛙:曼氏裂头蚴寄生虫",
775 | "猪肉:猪肉绦虫",
776 | "牛肉:牛肉绦虫"
777 | ]
778 | },
779 | {
780 | "type": "heading",
781 | "text": "食品安全温度",
782 | "depth": 2
783 | },
784 | "通过足够的温度加热食物并保持一定的时间,可以在一定程度上减小细菌、寄生虫存活的风险。\n各类食品有不同的温度要求,烹饪者测量温度应该使用厨房用温度计测量食物中心温度。",
785 | "测量温度应该使用:厨房用温度计\n测量食物中心温度",
786 | "下列是业界标准的食物安全温度:",
787 | "猪肉\n 整块:71°C\n 碎肉:71°C",
788 | "禽类\n 整块:74°C\n 碎肉:74°C\n 全只:85°C",
789 | "牛肉-羊肉\n 整块:\n 3 分熟:63°C\n 5 分熟:71°C\n 7 分熟:77°C\n 碎肉:71°C",
790 | "蛋类:74°C",
791 | "剩菜再加热:74°C"
792 | ]
793 | },
794 | {
795 | "no": 9,
796 | "name": "高压力锅",
797 | "content": [
798 | {
799 | "type": "heading",
800 | "text": "蒸(米)/炖(使用电饭煲/高压锅/电压力锅)",
801 | "depth": 1
802 | },
803 | {
804 | "type": "heading",
805 | "text": "什么是压力锅",
806 | "depth": 2
807 | },
808 | "压力锅其实是一般的锅加上可锁紧的半密封盖,盖上有阀门,可用于控制锅内的压力。",
809 | {
810 | "type": "heading",
811 | "text": "工作方式",
812 | "depth": 3
813 | },
814 | "压力锅的工作方式是让蒸汽积聚在锅中,提高锅内的压力。锅内压力提高时,水的沸点也随之提高,可使含水的食物烹煮温度超过 100 ℃。",
815 | {
816 | "type": "heading",
817 | "text": "优点",
818 | "depth": 3
819 | },
820 | {
821 | "type": "list",
822 | "items": [
823 | "由于压力锅的实际烹饪温度较高,因此可以大幅缩短烹饪时间。",
824 | "压力锅内部的高温可促进褐变和焦糖化,能够产生独有的风味。"
825 | ]
826 | },
827 | {
828 | "type": "heading",
829 | "text": "流程",
830 | "depth": 2
831 | },
832 | {
833 | "type": "list",
834 | "items": [
835 | [
836 | "食材和水放入内胆后盒盖,",
837 | {
838 | "type": "strong",
839 | "text": "确保锅体密封"
840 | },
841 | ",加热。"
842 | ],
843 | "对于韧性较大的食材,如蹄筋类食物,使用高压锅可以较轻松将其煮烂,获得较好口感。",
844 | "压力锅通常有一个自锁阀(浮子阀)。在蒸煮时,随着锅内压力增大,自锁阀会启动并锁闭,隔绝锅内与锅外气体,为锅内增压创造条件。自锁阀启动后还会锁住锅盖,防止强行打开,起到安全保障作用。在蒸煮时需要确认自锁阀不被异物遮挡,让高压锅正常工作。",
845 | [
846 | "切换至保温状态后,",
847 | {
848 | "type": "strong",
849 | "text": "通过排气阀将锅内蒸汽排空方可开盖"
850 | }
851 | ]
852 | ]
853 | },
854 | {
855 | "type": "heading",
856 | "text": "注意事项",
857 | "depth": 3
858 | },
859 | {
860 | "type": "list",
861 | "items": [
862 | {
863 | "type": "strong",
864 | "text": "水蒸气很烫,不要凑到排气阀上。"
865 | },
866 | [
867 | "烹饪",
868 | {
869 | "type": "strong",
870 | "text": "流质食物"
871 | },
872 | "的过程中,",
873 | {
874 | "type": "strong",
875 | "text": "不要手动排气"
876 | },
877 | ",小心喷溅(可以将食材放入密封罐或者真空包装袋中再用高压锅烹饪)。"
878 | ],
879 | [
880 | "烹饪部分菜系(如汤类)手动放气",
881 | {
882 | "type": "strong",
883 | "text": "可能会影响食物的味道以及口感"
884 | },
885 | "。"
886 | ],
887 | [
888 | "开盖前需确认蒸气已排空。开盖时请勿一次性全部打开,尤其是",
889 | {
890 | "type": "strong",
891 | "text": "不要对着人正面开盖"
892 | },
893 | ",以免蒸气烫伤。"
894 | ],
895 | "蒸煮完成后,随着高压锅内气压降低至与外界气压平衡,自锁阀会松开。这个可以作为锅盖是否能打开的判断标志。",
896 | "高压锅的密封依赖锅盖里的密封橡胶圈,对于老旧的高压锅需要检查密封橡胶圈是否仍然有效。",
897 | "确认橡胶圈完全干净,任何微粒卡在其中都可能破坏密闭环境。",
898 | "很多压力锅有一个安全线,材料和液体不应该超过这个线,太多的食材和液体可能会让水蒸气喷涌堵塞排气阀,或喷溅出太多水蒸气不好清理。没有安全线的压力锅,最好也不要让水位线超过锅体的 2/3。",
899 | [
900 | {
901 | "type": "strong",
902 | "text": "不要使用高压锅烹饪燕麦或者挂面等容易产生泡沫的食物"
903 | },
904 | "。泡沫可能会阻塞蒸汽阀和泄压管。"
905 | ],
906 | "烹饪过程中,当压力阀升高并喷出蒸汽或者烟雾时,说明高压锅内部过度加压,压力阀为了保证安全,释放出了多余的压力。尽管喷出的蒸汽带有浓郁的香味会带来较高的愉悦感,但一来食物的风味有损失,二来过度加压可能会使部分类型高压锅的卡槽弯曲。因此当看到喷出蒸汽时,可减小火力。",
907 | "tips:从侧面开盖是一种不错的选择。"
908 | ]
909 | }
910 | ]
911 | },
912 | {
913 | "no": 10,
914 | "name": "厨房准备",
915 | "content": [
916 | {
917 | "type": "heading",
918 | "text": "厨房准备",
919 | "depth": 1
920 | },
921 | "在阅读和参考菜谱之前,假想你已经在厨房中准备好了下列物品。这些物品不会在原材料和工具部分提及。",
922 | {
923 | "type": "code",
924 | "text": "燃气灶,饮用水,炒锅,蒸锅,煮锅,电饭锅,食用油,洗菜盆,碟子,碗,筷子,勺子,汤勺,漏勺,洗涤剂,抹布,钢丝球,菜刀,削皮刀"
925 | },
926 | "下列材料可能会被高频使用。建议提前为厨房采购好,并永远保障有新鲜的可以取用。",
927 | {
928 | "type": "code",
929 | "text": "大葱,小葱,生姜,大蒜,花椒,八角,桂皮,香叶\n干辣椒,小米椒,生抽,老抽,蚝油,料酒\n黑醋,白醋,豆瓣酱,冰糖,棉白糖,盐,鸡精\n黑胡椒,白胡椒,五香粉,玉米淀粉,番薯淀粉"
930 | },
931 | "如果你预计将被隔离很久,建议同样采购好下列内容:",
932 | {
933 | "type": "code",
934 | "text": "冰箱、微波炉、保鲜膜、保鲜袋\n鸡蛋、青椒、胡萝卜、黄瓜、西红柿、木耳、里脊肉、方便面、茄子、米"
935 | },
936 | "如果你非常想追求形式化、标准化和仪式感,并且想拥有一个与众不同的有趣厨房,那就同样采购下列内容:",
937 | {
938 | "type": "code",
939 | "text": "天平、游标卡尺、量筒、停表、烧杯、测温枪、移液器"
940 | },
941 | [
942 | "其它针对每道菜的原材料,请具体参考菜品本身的",
943 | {
944 | "type": "codespan",
945 | "text": "所需原材料"
946 | },
947 | "章节。"
948 | ]
949 | ]
950 | },
951 | {
952 | "no": 11,
953 | "name": "如何选择现在吃什么",
954 | "content": [
955 | {
956 | "type": "heading",
957 | "text": "如何决策吃什么",
958 | "depth": 1
959 | },
960 | "如何决策吃什么也是我做菜之前一大难题。所以只能用数学描述一下了。",
961 | {
962 | "type": "heading",
963 | "text": "计算方法",
964 | "depth": 2
965 | },
966 | {
967 | "type": "heading",
968 | "text": "计算荤菜和素菜数量",
969 | "depth": 3
970 | },
971 | {
972 | "type": "list",
973 | "items": [
974 | "菜的数量 = 人数 + 1。",
975 | "荤菜比素菜多一个,或一样多即可。"
976 | ]
977 | },
978 | "由此得到荤菜数量和素菜数量,再在上一步的菜谱中选择即可。",
979 | {
980 | "type": "heading",
981 | "text": "形式语言描述",
982 | "depth": 4
983 | },
984 | [
985 | "当 有人数 ",
986 | {
987 | "type": "codespan",
988 | "text": "N"
989 | },
990 | " 时,\n设 ",
991 | {
992 | "type": "codespan",
993 | "text": "素菜数"
994 | },
995 | " 为 ",
996 | {
997 | "type": "codespan",
998 | "text": "a"
999 | },
1000 | ", ",
1001 | {
1002 | "type": "codespan",
1003 | "text": "荤菜数"
1004 | },
1005 | "为 ",
1006 | {
1007 | "type": "codespan",
1008 | "text": "b"
1009 | },
1010 | "。\n",
1011 | {
1012 | "type": "codespan",
1013 | "text": "N"
1014 | },
1015 | ", ",
1016 | {
1017 | "type": "codespan",
1018 | "text": "a"
1019 | },
1020 | ", ",
1021 | {
1022 | "type": "codespan",
1023 | "text": "b"
1024 | },
1025 | "均为整数。"
1026 | ],
1027 | "此时有下列不等式组:",
1028 | {
1029 | "type": "list",
1030 | "items": [
1031 | "a + b = N + 1",
1032 | "a ≤ b ≤ a+1"
1033 | ]
1034 | },
1035 | "解得",
1036 | {
1037 | "type": "list",
1038 | "items": [
1039 | "a = floor(N/2)",
1040 | "b = ceil(N/2)"
1041 | ]
1042 | },
1043 | {
1044 | "type": "heading",
1045 | "text": "菜的选择",
1046 | "depth": 3
1047 | },
1048 | {
1049 | "type": "list",
1050 | "items": [
1051 | "如果人数超过 8 人,考虑在荤菜中增加鱼类荤菜。",
1052 | "如果有小孩,考虑增加有甜味的菜。",
1053 | "考虑增加特色菜、拿手菜。",
1054 | [
1055 | "注意决策荤菜时不要全部使用同一种动物的肉。考虑顺序为:",
1056 | {
1057 | "type": "codespan",
1058 | "text": "猪肉"
1059 | },
1060 | "、",
1061 | {
1062 | "type": "codespan",
1063 | "text": "鸡肉"
1064 | },
1065 | "、",
1066 | {
1067 | "type": "codespan",
1068 | "text": "牛肉"
1069 | },
1070 | "、",
1071 | {
1072 | "type": "codespan",
1073 | "text": "羊肉"
1074 | },
1075 | "、",
1076 | {
1077 | "type": "codespan",
1078 | "text": "鸭肉"
1079 | },
1080 | "、",
1081 | {
1082 | "type": "codespan",
1083 | "text": "鱼肉"
1084 | },
1085 | "。"
1086 | ],
1087 | "不要选择奇奇怪怪的动物做荤菜。"
1088 | ]
1089 | }
1090 | ]
1091 | }
1092 | ]
--------------------------------------------------------------------------------
/miniprogram/pages/learn/detail.js:
--------------------------------------------------------------------------------
1 | const info = require('./data.js').default
2 | const cookbooks = require('../../data').default
3 | // import Toast from 'tdesign-miniprogram/toast/index';
4 | import Message from 'tdesign-miniprogram/message/index';
5 |
6 | Page({
7 | data: {
8 | info: []
9 | },
10 |
11 | onLoad: function (options) {
12 | const { no = 0 } = options;
13 | const target = info.find(item => item.no == no);
14 |
15 | if (target) {
16 | this.setData({
17 | info: target.content
18 | })
19 | wx.setNavigationBarTitle({
20 | title: target.name
21 | })
22 | }
23 | },
24 |
25 | handlePreview({ target }) {
26 | const { src } = target.dataset;
27 |
28 | wx.previewImage({
29 | urls: [src],
30 | success() {
31 | console.log('success');
32 | },
33 | fail(e) {
34 | console.log(e);
35 | }
36 | })
37 | },
38 |
39 | handleLink({ target }) {
40 |
41 | const { src } = target.dataset;
42 |
43 | if (src.startsWith('http')) {
44 | wx.setClipboardData({
45 | data: src,
46 | }).then(() => {
47 | Message.info({
48 | offset: [20, 32],
49 | duration: 5000,
50 | content: '链接已复制,暂不支持直接打开网页',
51 | });
52 | }).catch(() => {
53 | Message.info({
54 | offset: [20, 32],
55 | duration: 5000,
56 | content: '链接无法复制,请稍后重试',
57 | });
58 | })
59 | } else {
60 | const match = /\/([^\/]+)\.md/.exec(src);
61 |
62 | if (match[1]) {
63 | const cookbook = cookbooks.find(item => item.name.includes(match[1]))
64 |
65 | if (cookbook) {
66 | wx.navigateTo({
67 | url: '/pages/detail/index?id=' + cookbook.id
68 | })
69 | }
70 |
71 | const tips = info.find(item => item.name.includes(match[1]))
72 | if (tips) {
73 | wx.navigateTo({
74 | url: '/pages/learn/detail?no=' + tips.no
75 | })
76 | }
77 | }
78 | }
79 | },
80 |
81 | /**
82 | * 用户点击右上角分享
83 | */
84 | onShareAppMessage: function () {
85 |
86 | }
87 | })
--------------------------------------------------------------------------------
/miniprogram/pages/learn/detail.json:
--------------------------------------------------------------------------------
1 | {
2 | "backgroundColor": "#fff",
3 | "usingComponents": {}
4 | }
--------------------------------------------------------------------------------
/miniprogram/pages/learn/detail.less:
--------------------------------------------------------------------------------
1 | page {
2 | background-color: #fff;
3 | font-size: 28rpx;
4 | line-height: 46rpx;
5 | }
6 |
7 | .container {
8 | padding: 24rpx;
9 | }
10 |
11 | .h1, .h2, .h3, .h4 {
12 | font-weight: bold;
13 | margin-bottom: 12rpx;
14 | line-height: 1.5;
15 | }
16 |
17 | .h1 {
18 | font-size: 38rpx;
19 | }
20 |
21 | .h2 {
22 | font-size: 36rpx;
23 | }
24 |
25 | .h3 {
26 | font-size: 32rpx;
27 | }
28 |
29 | .h4 {
30 | font-size: 30rpx;
31 | }
32 |
33 | .paragraph {
34 | margin: 12rpx 0;
35 | }
36 |
37 | .list {
38 | margin: 24rpx 0;
39 | font-size: 28rpx;
40 | line-height: 36rpx;
41 | }
42 |
43 | .flex-box {
44 | display: flex;
45 | justify-content: center;
46 | align-items: center;
47 |
48 | &__title {
49 | position: relative;
50 | font-weight: bold;
51 | text-align: center;
52 | margin: 32rpx 0 52rpx;
53 | background: #fff;
54 |
55 | &:before {
56 | content: '';
57 | display: block;
58 | position: absolute;
59 | left: -20rpx;
60 | top: 50%;
61 | width: 52rpx;
62 | height: 52rpx;
63 | transform: translateY(-50%);
64 | background-color: pink;
65 | border-radius: 50%;
66 | opacity: .3;
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/miniprogram/pages/learn/detail.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{section.text}}
8 |
9 | {{section.text}}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/miniprogram/pages/learn/index.js:
--------------------------------------------------------------------------------
1 | const info = require('./data.js').default
2 |
3 | Page({
4 |
5 | data: {
6 | info,
7 | },
8 |
9 | onLoad: function (options) {
10 |
11 | },
12 |
13 | onShareAppMessage: function () {
14 |
15 | }
16 | })
--------------------------------------------------------------------------------
/miniprogram/pages/learn/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {}
3 | }
--------------------------------------------------------------------------------
/miniprogram/pages/learn/index.less:
--------------------------------------------------------------------------------
1 | page {
2 | padding: 48rpx 32rpx 200rpx;
3 | }
4 |
--------------------------------------------------------------------------------
/miniprogram/pages/learn/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/miniprogram/pages/my/index.js:
--------------------------------------------------------------------------------
1 | import Toast from 'tdesign-miniprogram/toast/index';
2 |
3 | const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
4 |
5 | Page({
6 | data: {
7 | avatarUrl: defaultAvatarUrl,
8 | version: '',
9 | adFlag: true,
10 | },
11 |
12 | onLoad() {
13 | const { miniProgram } = wx.getAccountInfoSync();
14 |
15 | this.setData({
16 | version: miniProgram.version || '0.1.0'
17 | })
18 | },
19 |
20 | onShow() {
21 | const adFlagStorage = wx.getStorageSync('ad-flag')
22 |
23 | this.setData({
24 | adFlag: adFlagStorage === '' ? true : adFlagStorage,
25 | })
26 | },
27 |
28 | onChooseAvatar(e) {
29 | const { avatarUrl } = e.detail;
30 |
31 | this.setData({ avatarUrl })
32 | wx.cloud.callFunction({
33 | name: 'saveUserinfo',
34 | data: {
35 | avatarUrl,
36 | },
37 | })
38 | },
39 |
40 | handleCopy({ target }) {
41 | const { msg } = target.dataset;
42 |
43 | wx.setClipboardData({
44 | data: msg
45 | })
46 | },
47 |
48 | handleToMP() {
49 | wx.navigateToMiniProgram({
50 | appId: 'wx6f3e38f61d138c04'
51 | })
52 | },
53 |
54 | handleSubscribe() {
55 | const tmplIds = ['vjEDlUYrVJ05CauSw_V9jIWF-okt3OMCBtlz9yvjrfg', 'Sbtj4X4gIKWRy0xDeWU8xCl8LejbTpIQ3gWiKh5JFp4'];
56 | wx.requestSubscribeMessage({
57 | tmplIds,
58 | success: async (res) => {
59 | const accept = tmplIds.filter(key => res[key] === 'accept');
60 | wx.request({
61 | url: 'http://dev.africans.cn/miniprogram/add-subscribe',
62 | method: 'POST',
63 | data: {
64 | templateIds: accept,
65 | token: wx.getStorageSync('token')
66 | },
67 | success(res) {
68 | console.log(res);
69 | },
70 | fail(err) {
71 | console.log(err);
72 | }
73 | })
74 | Toast({
75 | context: this,
76 | selector: '#t-toast',
77 | message: accept.length > 0 ? '订阅成功' : '你拒绝了订阅',
78 | });
79 | }
80 | })
81 | },
82 |
83 | handleToggleAd({ detail }) {
84 | const adFlag = detail.value
85 |
86 | wx.setStorageSync('ad-flag',adFlag)
87 | this.setData({
88 | adFlag
89 | })
90 | },
91 |
92 | onShareAppMessage() {
93 | return {
94 | title: '程序员做饭指南',
95 | path: '/pages/index/index'
96 | }
97 | },
98 | })
--------------------------------------------------------------------------------
/miniprogram/pages/my/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "个人中心",
3 | "usingComponents": {}
4 | }
--------------------------------------------------------------------------------
/miniprogram/pages/my/index.less:
--------------------------------------------------------------------------------
1 | .avatar {
2 | width: 120rpx;
3 | height: 120rpx;
4 | border-radius: 30rpx;
5 | overflow: hidden;
6 |
7 | &-button {
8 | margin-right: 32rpx;
9 | height: 120rpx;
10 | padding: 0;
11 | background: transparent;
12 |
13 | &::after, &::before {
14 | display: none;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/miniprogram/pages/my/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | TDesign Miniprogram
27 |
28 |
29 |
30 |
31 |
32 |
33 | {{version}}
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/miniprogram/pages/myStarred/index.js:
--------------------------------------------------------------------------------
1 | import config from '../../config/index.js'
2 |
3 | Page({
4 |
5 | data: {
6 | list : [],
7 | chineseMap: {},
8 | loading: false,
9 | themeMap: {
10 | dessert: 'danger',
11 | breakfast: 'success',
12 | staple: 'warning'
13 | }
14 | },
15 |
16 | onShow() {
17 | this.getList()
18 | },
19 |
20 | async getList() {
21 | this.setData({
22 | loading: true
23 | })
24 | const { result } = await wx.cloud.callFunction({
25 | name: 'getCookbook',
26 | data: {
27 | kind: 'starred',
28 | }
29 | })
30 |
31 | if (result.errno == 0) {
32 | this.setData({
33 | loading: false,
34 | list: result.data,
35 | chineseMap: config.chineseMap
36 | })
37 | }
38 | }
39 | })
--------------------------------------------------------------------------------
/miniprogram/pages/myStarred/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "我的收藏",
3 | "usingComponents": {}
4 | }
--------------------------------------------------------------------------------
/miniprogram/pages/myStarred/index.less:
--------------------------------------------------------------------------------
1 |
2 | .container {
3 | padding: 32rpx 0;
4 | }
5 |
6 | .title {
7 | margin-left: 46rpx;
8 | margin-bottom: 32rpx;
9 | font-size: 42rpx;
10 | }
11 |
12 | .box {
13 | display: flex;
14 | justify-content: center;
15 | align-items: center;
16 | }
17 |
18 | .empty-box {
19 | height: 100%;
20 | display: flex;
21 | align-items: center;
22 | justify-content: center;
23 | }
24 |
25 | page .t-tag {
26 | display: flex;
27 | }
--------------------------------------------------------------------------------
/miniprogram/pages/myStarred/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{chineseMap[item.category]}}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/miniprogram/pages/search/index.js:
--------------------------------------------------------------------------------
1 | import cookbook from '../../data.js';
2 | import config from '../../config/index'
3 |
4 | Page({
5 | data: {
6 | keyword: '',
7 | result: [],
8 | isLoading: false,
9 | focus: false,
10 | chineseMap: config.chineseMap,
11 | themeMap: {
12 | dessert: 'danger',
13 | breakfast: 'success',
14 | staple: 'warning'
15 | }
16 | },
17 |
18 | onLoad(options) {
19 | let keyword = options.keyword.trim()
20 | if (keyword) {
21 | this.setData({ keyword });
22 | this.doSearch(keyword)
23 | }
24 | },
25 |
26 | handleSearchClear() {
27 | this.setData({
28 | result: [],
29 | keyword: '',
30 | focus: true
31 | })
32 | },
33 |
34 | handleSearch({ detail }) {
35 | this.setData({ isLoading: true })
36 | if (this.lastTrigger) {
37 | clearTimeout(this.lastTrigger)
38 | }
39 | this.lastTrigger = setTimeout(() => {
40 | this.doSearch(detail.value)
41 | }, 500)
42 | },
43 |
44 | doSearch(keyword) {
45 | if (keyword == '') {
46 | this.setData({
47 | result: [],
48 | isLoading: false
49 | })
50 | return
51 | };
52 | const fuzzySearch = (key) => {
53 | if (typeof key == 'string') return key.indexOf(keyword) > -1
54 | if (Array.isArray(key)) {
55 | return key.some(item => fuzzySearch(item))
56 | }
57 | if (typeof key == 'object') {
58 | return Object.keys(key).some(item => fuzzySearch(item))
59 | }
60 | return false
61 | }
62 | const result = cookbook.filter(cb => {
63 | const isTitleMatch = cb.name.indexOf(keyword) > -1;
64 | // const isDescMatch = fuzzySearch(cb.desc)
65 |
66 | return isTitleMatch;
67 | })
68 | this.setData({
69 | result,
70 | isLoading: false
71 | })
72 | this.updateViews(keyword)
73 | },
74 |
75 | updateViews(key) {
76 | wx.cloud.callFunction({
77 | name: 'updateViews',
78 | data: {
79 | id: key,
80 | type: 'search'
81 | },
82 | })
83 | },
84 |
85 | onShareAppMessage() {
86 | return {
87 | title: '我推荐的菜谱',
88 | path: '/pages/search/index?keyword=' + this.data.keyword
89 | }
90 | }
91 | })
--------------------------------------------------------------------------------
/miniprogram/pages/search/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {}
3 | }
--------------------------------------------------------------------------------
/miniprogram/pages/search/index.less:
--------------------------------------------------------------------------------
1 | page {
2 | background-color: #fff;
3 | }
4 |
5 | .container {
6 | padding: 32rpx;
7 | }
8 |
9 | .result {
10 | margin-top: 24rpx;
11 | }
12 |
13 | .tips {
14 | text-align: center;
15 | font-size: 28rpx;
16 | color: #999;
17 | margin-bottom: 24rpx;
18 | }
19 |
20 | .loading-box {
21 | margin-top: 24rpx;
22 | display: flex;
23 | justify-content: center;
24 | }
--------------------------------------------------------------------------------
/miniprogram/pages/search/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 搜索到与 “{{keyword}}” 相关的 {{result.length}} 个结果
9 |
10 | {{chineseMap[item.category]}}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/miniprogram/project.private.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "condition": {
3 | "plugin": {
4 | "list": []
5 | },
6 | "game": {
7 | "list": []
8 | },
9 | "gamePlugin": {
10 | "list": []
11 | },
12 | "miniprogram": {
13 | "list": [
14 | {
15 | "name": "Detail",
16 | "pathName": "pages/detail/index",
17 | "query": "",
18 | "scene": null,
19 | "launchMode": "default"
20 | }
21 | ]
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/miniprogram/sitemap.json:
--------------------------------------------------------------------------------
1 | {
2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
3 | "rules": [{
4 | "action": "allow",
5 | "page": "*"
6 | }]
7 | }
--------------------------------------------------------------------------------
/miniprogram/templates/base.wxml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/miniprogram/templates/hyperText.wxml:
--------------------------------------------------------------------------------
1 |
2 | {{tokens}}
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{item}}
25 | {{item.text}}
26 | {{item.text}}
27 | {{item.text}}
28 | {{item.text}}
29 | {{item.text}}
30 | {{item.text}}
31 | {{item.text}}
32 |
33 |
34 |
35 |
36 | module.exports.getType = function(obj) {
37 | if ("Array" === obj.constructor) return 'array'
38 |
39 | return typeof obj;
40 | }
41 |
--------------------------------------------------------------------------------
/miniprogram/templates/tabbar.wxml:
--------------------------------------------------------------------------------
1 |
5 |
6 | {{item.text}}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/miniprogram/utils/ad.js:
--------------------------------------------------------------------------------
1 | export const create = (adUnitId) => {
2 | if (wx.createRewardedVideoAd) {
3 | const ad = wx.createRewardedVideoAd({
4 | adUnitId,
5 | multiton: true
6 | })
7 | return ad;
8 | }
9 | }
10 |
11 | export const show = (videoAd) => {
12 | // 用户触发广告后,显示激励视频广告
13 | if (videoAd) {
14 | videoAd.show().catch(() => {
15 | // 失败重试
16 | videoAd.load()
17 | .then(() => videoAd.show())
18 | .catch(err => {
19 | console.log('激励视频 广告显示失败')
20 | })
21 | })
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/miniprogram/utils/index.js:
--------------------------------------------------------------------------------
1 | const formatTime = date => {
2 | const year = date.getFullYear()
3 | const month = date.getMonth() + 1
4 | const day = date.getDate()
5 | const hour = date.getHours()
6 | const minute = date.getMinutes()
7 | const second = date.getSeconds()
8 |
9 | return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}`
10 | }
11 |
12 | const formatNumber = n => {
13 | n = n.toString()
14 | return n[1] ? n : `0${n}`
15 | }
16 |
17 | const groupBy = (arr, identity) => {
18 | const rect = {}
19 | arr.forEach(item => {
20 | const key = item[identity]
21 | if (!(key in rect)) {
22 | rect[key] = []
23 | }
24 | rect[key].push(item)
25 | })
26 | return rect;
27 | }
28 |
29 | export default {
30 | formatTime,
31 | groupBy
32 | }
33 |
--------------------------------------------------------------------------------
/miniprogram/utils/request.js:
--------------------------------------------------------------------------------
1 | const domain = 'http://dev.africans.cn';
2 |
3 | export function post(cmd, data = {}) {
4 | const innerData = {};
5 |
6 | if (cmd != 'miniprogram/login') {
7 | innerData.token = wx.getStorageSync('token');
8 | }
9 | return new Promise((resolve, reject) => {
10 | wx.request({
11 | method: 'POST',
12 | url: `${domain}/${cmd}`,
13 | data: {
14 | id: 'wy9HvIHSJfNYikzCR3BZ6R6VwTu4ILykYbnevVNYPBY=',
15 | ...innerData,
16 | ...data
17 | },
18 | success({ data, statusCode}) {
19 | if (statusCode == 200) {
20 | resolve(data.data);
21 | } else {
22 | reject(data.data)
23 | }
24 | },
25 | fail(err) {
26 | console.log('request fail: ', err);
27 | reject(err)
28 | }
29 | })
30 | })
31 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "howtocookonminiprogram",
3 | "version": "1.0.0",
4 | "description": "小程序版本程序员做菜指南,将程序员精神进行到底",
5 | "main": "index.js",
6 | "scripts": {
7 | "gen-data": "node script/index.js",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/LeeJim/HowToCookOnMiniprogram.git"
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "ISC",
17 | "bugs": {
18 | "url": "https://github.com/LeeJim/HowToCookOnMiniprogram/issues"
19 | },
20 | "homepage": "https://github.com/LeeJim/HowToCookOnMiniprogram#readme",
21 | "dependencies": {
22 | "glob": "^7.2.0",
23 | "magic-string": "^0.26.1",
24 | "marked": "^4.0.12"
25 | },
26 | "devDependencies": {
27 | "axios": "^0.26.1",
28 | "cli-progress": "^3.10.0",
29 | "commonmark": "^0.30.0",
30 | "cos-nodejs-sdk-v5": "^2.11.7",
31 | "form-data": "^4.0.0",
32 | "md5": "^2.3.0",
33 | "md5-file": "^5.0.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/project.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "项目配置文件,详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
3 | "miniprogramRoot": "miniprogram/",
4 | "cloudfunctionRoot": "cloudfunctions/",
5 | "packOptions": {
6 | "ignore": [
7 | {
8 | "value": "HowToCook",
9 | "type": "folder"
10 | },
11 | {
12 | "value": "node_modules",
13 | "type": "folder"
14 | }
15 | ],
16 | "include": []
17 | },
18 | "setting": {
19 | "urlCheck": false,
20 | "es6": true,
21 | "enhance": true,
22 | "postcss": true,
23 | "preloadBackgroundData": false,
24 | "minified": true,
25 | "newFeature": false,
26 | "coverView": true,
27 | "nodeModules": false,
28 | "autoAudits": false,
29 | "showShadowRootInWxmlPanel": true,
30 | "scopeDataCheck": false,
31 | "uglifyFileName": false,
32 | "checkInvalidKey": true,
33 | "checkSiteMap": true,
34 | "uploadWithSourceMap": true,
35 | "compileHotReLoad": false,
36 | "lazyloadPlaceholderEnable": false,
37 | "useMultiFrameRuntime": true,
38 | "useApiHook": true,
39 | "useApiHostProcess": true,
40 | "babelSetting": {
41 | "ignore": [],
42 | "disablePlugins": [],
43 | "outputPath": ""
44 | },
45 | "useIsolateContext": true,
46 | "userConfirmedBundleSwitch": false,
47 | "packNpmManually": false,
48 | "packNpmRelationList": [],
49 | "minifyWXSS": true,
50 | "disableUseStrict": false,
51 | "minifyWXML": true,
52 | "showES6CompileOption": false,
53 | "useCompilerPlugins": [
54 | "less"
55 | ],
56 | "ignoreUploadUnusedFiles": true
57 | },
58 | "compileType": "miniprogram",
59 | "libVersion": "2.22.1",
60 | "appid": "wx01462be634a0d447",
61 | "projectname": "HowToCookOnMiniprogram",
62 | "cloudfunctionTemplateRoot": "cloudfunctionTemplate/",
63 | "srcMiniprogramRoot": "miniprogram/",
64 | "editorSetting": {
65 | "tabIndent": "insertSpaces",
66 | "tabSize": 2
67 | },
68 | "condition": {
69 | "search": {
70 | "list": []
71 | },
72 | "conversation": {
73 | "list": []
74 | },
75 | "game": {
76 | "list": []
77 | },
78 | "plugin": {
79 | "list": []
80 | },
81 | "gamePlugin": {
82 | "list": []
83 | },
84 | "miniprogram": {
85 | "list": []
86 | }
87 | }
88 | }
--------------------------------------------------------------------------------
/project.private.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "setting": {
3 | "compileHotReLoad": true
4 | },
5 | "condition": {
6 | "miniprogram": {
7 | "list": [
8 | {
9 | "name": "收藏页",
10 | "pathName": "pages/myStarred/index",
11 | "query": "",
12 | "launchMode": "default",
13 | "scene": null
14 | },
15 | {
16 | "name": "个人中心",
17 | "pathName": "pages/my/index",
18 | "query": "",
19 | "launchMode": "default",
20 | "scene": null
21 | },
22 | {
23 | "name": "更新日志",
24 | "pathName": "pages/changelog/index",
25 | "query": "",
26 | "launchMode": "default",
27 | "scene": null
28 | },
29 | {
30 | "name": "Learn",
31 | "pathName": "pages/learn/index",
32 | "query": "",
33 | "launchMode": "default",
34 | "scene": null
35 | },
36 | {
37 | "name": "买菜",
38 | "pathName": "pages/cart/index",
39 | "query": "",
40 | "launchMode": "default",
41 | "scene": null
42 | },
43 | {
44 | "name": "search",
45 | "pathName": "pages/search/index",
46 | "query": "keyword=鸡蛋",
47 | "launchMode": "default",
48 | "scene": null
49 | },
50 | {
51 | "name": "订阅",
52 | "pathName": "pages/index/index",
53 | "query": "from=subscribe",
54 | "launchMode": "default",
55 | "scene": 1107
56 | }
57 | ]
58 | }
59 | },
60 | "projectname": "HowToCookOnMiniprogram",
61 | "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html"
62 | }
--------------------------------------------------------------------------------
/retry.json:
--------------------------------------------------------------------------------
1 | {"retry":[]}
--------------------------------------------------------------------------------
/script/emoji/download-image.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = require('fs')
3 | const axios = require('axios')
4 | var tunnel = require('tunnel');
5 |
6 | var tunnelingAgent = tunnel.httpsOverHttp({
7 | proxy: {
8 | host: '127.0.0.1',
9 | port: 7890
10 | }
11 | });
12 |
13 | const emojiData = require('./data.json');
14 | const { knownSupportedEmoji } = require('../../miniprogram/pages/kitchen/supported.js')
15 | const fix = (str) => str.split("-")
16 | .filter(x => x !== "fe0f")
17 | .join("_")
18 | let counter = 0;
19 | let retry = [];
20 | let skipped = 0;
21 |
22 | const download = async (url, filename) => {
23 | const _path = path.resolve(__dirname, '../../assets/emoji/', filename);
24 | const lastIndex = filename.lastIndexOf('/')
25 |
26 | if (fs.existsSync(_path)) {
27 | const stat = fs.statSync(_path);
28 |
29 | if (stat.size > 0) {
30 | console.log(++skipped, ' skipped');
31 | return;
32 | }
33 | }
34 |
35 | if (lastIndex > -1) {
36 | fs.mkdirSync(path.resolve(__dirname, '../../assets/emoji/' + filename.slice(0, lastIndex)), { recursive: true })
37 | }
38 | const writer = fs.createWriteStream(_path);
39 | try {
40 | const res = await axios.request({
41 | url,
42 | httpsAgent: tunnelingAgent,
43 | proxy: false,
44 | timeout: 5000,
45 | responseType: 'stream'
46 | })
47 | res.data.pipe(writer)
48 | counter++;
49 | console.log(counter, 'has finished');
50 | } catch(e) {
51 | console.log(url, e.message);
52 | retry.push({ url, filename})
53 | }
54 | }
55 |
56 | const main = async () => {
57 | for(let emoji of knownSupportedEmoji) {
58 | const name = `emoji_u${fix(emoji)}.svg`;
59 | const url = `https://raw.githubusercontent.com/googlefonts/noto-emoji/main/svg/${name}`;
60 | console.log(url);
61 | await download(url, name);
62 |
63 | // if (emoji in emojiData) {
64 | // for (let combo of emojiData[emoji]) {
65 | // const { date, leftEmoji, rightEmoji } = combo;
66 | // const name = `${date}/u${fix(leftEmoji)}/u${fix(leftEmoji)}_u${fix(rightEmoji)}.png`;
67 | // const comboUrl = `https://www.gstatic.com/android/keyboard/emojikitchen/${name}`
68 | // await download(comboUrl, name)
69 | // }
70 | // }
71 | console.log(emoji, 'has downloaded');
72 | }
73 | }
74 |
75 | fs.writeFileSync('./retry.json', JSON.stringify({ retry }));
76 |
77 | main()
78 |
--------------------------------------------------------------------------------
/script/gen-tips.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = require('fs')
3 | const glob = require('glob')
4 | const { marked } = require('marked')
5 | const MagicString = require('magic-string')
6 | const cliProgress = require('cli-progress');
7 |
8 | const { flattenToken } = require('./helper')
9 | const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
10 | const dest = path.resolve(__dirname, '../miniprogram/pages/learn/data.js')
11 |
12 | glob(path.resolve(__dirname, '../HowToCook/tips/**/*.md'), {}, async (err, files) => {
13 | if (err) console.log(err)
14 | const ans = []
15 | let no = 0;
16 |
17 | bar.start(files.length, 0);
18 |
19 | for (let p of files) {
20 | const { name, dir } = path.parse(p)
21 | const content = fs.readFileSync(path.resolve(p), { encoding: 'utf-8'})
22 | const tokens = marked.lexer(content);
23 | const article = {
24 | no: no++,
25 | name,
26 | content: [],
27 | }
28 |
29 | for (let token of tokens) {
30 | const { type } = token
31 | const { content } = article;
32 |
33 | if (type == 'space') continue
34 |
35 | content.push(await flattenToken(token, dir))
36 | }
37 |
38 | ans.push(article)
39 | bar.update(no)
40 | }
41 |
42 | bar.stop()
43 | const s = new MagicString(JSON.stringify(ans, null, 2))
44 | s.prepend('export default ')
45 | fs.writeFileSync(dest, s.toString())
46 | })
--------------------------------------------------------------------------------
/script/helper.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = require('fs')
3 | const axios = require('axios')
4 | const FormData = require('form-data')
5 |
6 | const config = require('../config')
7 |
8 | let accessToken = ''
9 |
10 | const getAccessToken = async() => {
11 | if (accessToken) return accessToken
12 |
13 | const { data } = await axios.get('https://api.weixin.qq.com/cgi-bin/token', {
14 | params: {
15 | grant_type: 'client_credential',
16 | appid: config.appid,
17 | secret: config.secret
18 | }
19 | })
20 | if (data.access_token) {
21 | accessToken = data.access_token
22 | return accessToken
23 | }
24 | throw new TypeError(data)
25 | }
26 |
27 | const uploadImage = async (filePath) => {
28 | const cloudPath = 'cookbook'
29 | const token = await getAccessToken()
30 | const url = 'https://api.weixin.qq.com/tcb/uploadfile?access_token=' + token
31 | const { data } = await axios.post(url, {
32 | env: config.cloudEnvId,
33 | path: cloudPath
34 | })
35 | if (data.errcode == 0) {
36 | const { url, token, authorization, file_id, cos_file_id} = data;
37 | const form = new FormData()
38 |
39 | form.append('key', authorization);
40 | form.append('Signature', authorization);
41 | form.append('x-cos-security-token', token);
42 | form.append('x-cos-meta-fileid', cos_file_id);
43 | form.append('file', fs.createReadStream(filePath));
44 |
45 | return new Promise((resolve, reject) => {
46 | form.submit(url, (err) => {
47 | if (err) reject(err)
48 | resolve(file_id.replace(cloudPath, '') + authorization)
49 | })
50 | })
51 | } else {
52 | throw new TypeError(data.errmsg)
53 | }
54 | }
55 |
56 | const flattenToken = async (token, dir) => {
57 | const flatten = async (token) => {
58 | const { type, tokens, text, href, depth } = token
59 | switch(type) {
60 | case 'heading':
61 | return { type, text, depth }
62 | case 'list':
63 | const items = await Promise.all(token.items.map(async item => await flatten(item)))
64 | return { type, items }
65 | case 'list_item':
66 | if (tokens.length == 1 && tokens[0].tokens.length == 1) {
67 | const final = tokens[0].tokens[0];
68 | return final.type == 'text' ? text : await flatten(final);
69 | }
70 | if (tokens.length == 1) {
71 | return await flatten(tokens[0])
72 | }
73 | // console.log(token);
74 | return await Promise.all(tokens.map(async item => await flatten(item)))
75 | case 'row':
76 | return { type, row: token.rows }
77 | case 'text':
78 | case 'paragraph':
79 | if (!tokens) return text
80 | if (tokens.length == 1) {
81 | return tokens[0].type == 'text' ? text : await flatten(tokens[0])
82 | }
83 | return await await Promise.all(tokens.map(async item => await flatten(item)))
84 | case 'strong':
85 | case 'codespan':
86 | case 'em':
87 | return { type, text }
88 | case 'html':
89 | const ans = / {
13 | if (err) {
14 | console.log(err)
15 | }
16 | const dishes = []
17 | let no = 0
18 |
19 | bar.start(files.length, 0);
20 | for(let p of files) {
21 | const { name, dir } = path.parse(p)
22 | const [ ,category ] = /dishes\/([\w-]+)\//g.exec(p)
23 | const content = fs.readFileSync(path.resolve(p), { encoding: 'utf-8'})
24 | let menu = {
25 | no,
26 | id: md5(name),
27 | name,
28 | category,
29 | detail: [],
30 | desc: []
31 | }
32 | const flattenToken = async (token) => {
33 | const { type, tokens, text, href } = token
34 | switch(type) {
35 | case 'list':
36 | const items = await Promise.all(token.items.map(async item => await flattenToken(item)))
37 | return { type, items }
38 | case 'list_item':
39 | if (tokens.length == 1 && tokens[0].tokens.length == 1) {
40 | const final = tokens[0].tokens[0];
41 | return final.type == 'text' ? text : await flattenToken(final);
42 | }
43 | if (tokens.length == 1) {
44 | return await flattenToken(tokens[0])
45 | }
46 | // console.log(token);
47 | return await Promise.all(tokens.map(async item => await flattenToken(item)))
48 | case 'row':
49 | return { type, row: token.rows }
50 | case 'text':
51 | case 'paragraph':
52 | if (!tokens) return text
53 | if (tokens.length == 1) {
54 | return tokens[0].type == 'text' ? text : await flattenToken(tokens[0])
55 | }
56 | return await await Promise.all(tokens.map(async item => await flattenToken(item)))
57 | case 'strong':
58 | case 'codespan':
59 | case 'em':
60 | return { type, text }
61 | case 'html':
62 | const ans = / 2) {
112 | target.push({ type, text })
113 | }
114 | } else if (type !== 'space') {
115 | if (target == null) {
116 | menu.desc.push(await flattenToken(token))
117 | } else {
118 | const tmp = await flattenToken(token)
119 | target.push(tmp)
120 | }
121 | }
122 | }
123 | bar.update(no)
124 | dishes.push(menu)
125 | }
126 |
127 | bar.stop()
128 |
129 | const jsonData = new MagicString('');
130 | const s = new MagicString(JSON.stringify(dishes, null, 2))
131 | dishes.forEach(item => {
132 | jsonData.append(JSON.stringify(item) + '\n')
133 | })
134 |
135 | s.prepend('export default ')
136 | fs.writeFileSync('./miniprogram/data.js', s.toString())
137 | fs.writeFileSync('./data-v2.json', jsonData.toString())
138 | })
--------------------------------------------------------------------------------
/script/upload.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const COS = require('cos-nodejs-sdk-v5')
3 | const FormData = require('form-data');
4 | const axios = require('axios')
5 | const md5File = require('md5-file')
6 | const path = require('path')
7 |
8 | const config = require('../config')
9 |
10 | const cos = new COS({
11 | SecretId: config.cos.secretId,
12 | SecretKey: config.cos.secretKey
13 | });
14 |
15 | let accessToken = ''
16 | const getAccessToken = async() => {
17 | if (accessToken) return accessToken
18 |
19 | const { data } = await axios.get('https://api.weixin.qq.com/cgi-bin/token', {
20 | params: {
21 | grant_type: 'client_credential',
22 | appid: config.appid,
23 | secret: config.secret
24 | }
25 | })
26 | if (data.access_token) {
27 | accessToken = data.access_token
28 | return accessToken
29 | }
30 | throw new TypeError(data)
31 | }
32 |
33 | const uploadWxCloud = async(filePath) => {
34 | const cloudPath = 'cookbook'
35 | const token = await getAccessToken()
36 | const url = 'https://api.weixin.qq.com/tcb/uploadfile?access_token=' + token
37 | const { data } = await axios.post(url, {
38 | env: config.cloudEnvId,
39 | path: cloudPath
40 | })
41 | if (data.errcode == 0) {
42 | const { url, token, authorization, file_id, cos_file_id} = data;
43 | const form = new FormData()
44 |
45 | form.append('key', authorization);
46 | form.append('Signature', authorization);
47 | form.append('x-cos-security-token', token);
48 | form.append('x-cos-meta-fileid', cos_file_id);
49 | form.append('file', fs.createReadStream(filePath));
50 |
51 | return new Promise((resolve, reject) => {
52 | form.submit(url, (err) => {
53 | if (err) reject(err)
54 | resolve(file_id.replace(cloudPath, '') + authorization)
55 | })
56 | })
57 | } else {
58 | throw new TypeError(data.errmsg)
59 | }
60 | }
61 |
62 | const uploadImage = async (filePath) => {
63 | const body = fs.createReadStream(filePath);
64 | const key = md5File.sync(filePath);
65 |
66 | return new Promise((resolve, reject) => {
67 | cos.putObject({
68 | Bucket: 'how-to-cook-1255404841',
69 | Region: 'ap-shanghai',
70 | Key: key,
71 | Body: body
72 | }, (err, data) => {
73 | if (err) reject(err)
74 | resolve('https://' + data.Location)
75 | })
76 | })
77 | }
78 |
79 | module.exports.uploadWxCloud = uploadWxCloud;
80 | module.exports.uploadImage = uploadImage;
--------------------------------------------------------------------------------
/sitemap.json:
--------------------------------------------------------------------------------
1 | {
2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
3 | "rules": [{
4 | "action": "allow",
5 | "page": "*"
6 | }]
7 | }
--------------------------------------------------------------------------------