62 Commits
v1.1.5 ... dev

Author SHA1 Message Date
YunYouJun
cb61ab90fd chore: ignore lint 2025-08-17 07:10:14 +08:00
YunYouJun
7b849b7e0a feat: add android init 2025-08-17 07:08:06 +08:00
YunYouJun
5045f7f91b feat: add tauri basic structure 2025-08-17 06:35:52 +08:00
YunYouJun
0ec5a4d51e test: fix vitest mock gtm api 2025-08-17 04:07:16 +08:00
YunYouJun
6095d97b2a fix: footer date in edgeone without cloudflare env 2025-08-17 03:28:50 +08:00
YunYouJun
13b6ad6047 refactor: migrate nuxt gtm module to nuxt/scripts 2025-08-17 03:05:00 +08:00
YunYouJun
190c05918b chore: update readme link 2025-01-24 11:45:55 +08:00
YunYouJun
42c244573c ci: remove vercel 2025-01-24 11:44:02 +08:00
YunYouJun
21f74b3c48 chore: release v1.2.4 2025-01-24 11:40:38 +08:00
YunYouJun
5fa5944dff chore: fix ci node version 2025-01-24 11:40:02 +08:00
YunYouJun
1af9e36fc0 chore: upgrade readme 2025-01-24 11:36:35 +08:00
YunYouJun
65fea75825 chore: release v1.2.3 2025-01-24 11:35:46 +08:00
YunYouJun
5e97da4731 chore: upgrade deps & fix lint 2025-01-24 11:27:43 +08:00
George Chen
78fa350ced feat: use <button> to improve a11y and keyboard navigation experience (#74) 2025-01-12 01:35:19 +08:00
YunYouJun
11283f6f94 chore: fix lint ignore recipe.json 2024-12-30 02:04:51 +08:00
YunYouJun
e9b4d3ac38 chore: upgrade deps with nuxt 2024-12-30 02:02:12 +08:00
YunYouJun
7fe1f95d9d ci: fix nuxt lint 2024-09-15 18:26:51 +08:00
YunYouJun
86b565dfa9 ci: add lint-staged check 2024-09-15 18:14:52 +08:00
YunYouJun
41bdc3346f refactor: use nuxt compatiable 4 folder 2024-09-15 18:07:50 +08:00
YunYouJun
7a52b024dd ci: add vercel tag deplou 2024-09-15 17:45:11 +08:00
YunYouJun
10073b3a07 chore: fix ci lint/typecheck/test 2024-04-28 19:36:17 +08:00
YunYouJun
778594652c fix: ios qq 500 error 2024-04-28 19:18:31 +08:00
YunYouJun
b00b52cba8 chore: upgrade deps 2024-03-13 15:58:41 +08:00
YunYouJun
ab7f4e20f1 ci: use node lts 2024-01-28 06:57:17 +08:00
YunYouJun
d42e3cf65d chore: revert if commitSha buildDate 2024-01-27 03:46:30 +08:00
YunYouJun
f59c34499a test: fix relative path 2024-01-27 03:45:50 +08:00
YunYouJun
e741f5d5a0 chore: upgrade deps 2024-01-27 03:25:20 +08:00
KazariEX
99d7d660bb fix: div in span (#61) 2023-11-16 14:10:34 +08:00
YunYouJun
87025cd17e chore: release v1.2.2 2023-11-13 00:41:24 +08:00
YunYouJun
68a4b2dbeb chore: upgrade deps 2023-11-13 00:41:15 +08:00
YunYouJun
1ab402af8b feat: add random recipes localStorage 2023-11-13 00:31:29 +08:00
YunYouJun
b2cf053446 chore: update lock 2023-11-11 19:53:44 +08:00
YunYouJun
37bab9269c chore: release v1.2.1 2023-11-11 19:53:07 +08:00
YunYouJun
44f9b40ee5 feat: add 麻婆豆腐, close #55 2023-11-11 19:51:51 +08:00
YunYouJun
4ddd5ac2d2 chore: fix data updated time 2023-11-11 19:46:44 +08:00
YunYouJun
9df47e977e chore: fix 可乐饼 link 2023-11-11 19:43:43 +08:00
KazariEX
5fb76f24dc fix: keyword filter (#57) 2023-11-07 15:33:16 +08:00
YunYouJun
014f129b8e chore: fix style details cursor size 2023-11-07 02:18:49 +08:00
YunYouJun
2949c45839 chore: release v1.2.0 2023-11-07 02:00:11 +08:00
YunYouJun
81586158d5 chore: fix lint 2023-11-07 02:00:03 +08:00
YunYouJun
bfb6ea3e14 feat: add history 2023-11-07 01:59:29 +08:00
YunYouJun
7d26f9ba84 feat: add search keyword 2023-11-07 01:06:30 +08:00
YunYouJun
9f12401922 chore: release v1.1.9 2023-11-07 00:25:00 +08:00
YunYouJun
69c689df67 fix: curStuff watch & remove useless data 2023-11-07 00:20:18 +08:00
YunYouJun
8ca8c4aac8 fix: curStuff watch 2023-11-07 00:12:56 +08:00
YunYouJun
5ab297cc47 chore: release v1.1.8 2023-11-06 14:34:40 +08:00
YunYouJun
7f87c47320 fix: icon padding bottom & remove useless recipe 2023-11-06 14:34:19 +08:00
YunYouJun
dd2de09325 ci: add check for dev 2023-11-06 02:30:08 +08:00
YunYouJun
e55d9a59c3 chore: fix lint 2023-11-06 02:28:14 +08:00
YunYouJun
0232595083 test: fix nuxt vitest 2023-11-06 02:26:45 +08:00
YunYouJun
d3ab8fb2c1 fix: add css var safe padding bottom 2023-11-06 02:00:56 +08:00
YunYouJun
85c8f4fcd1 chore: update simple copyright 2023-11-06 01:46:37 +08:00
YunYouJun
f2f878e2f4 chore: add simple copyright 2023-11-06 01:19:43 +08:00
YunYouJun
246a65599a fix: lint & add no scalable 2023-11-06 00:36:09 +08:00
YunYouJun
203ab27496 chore: upgrade deps & fix lint 2023-11-05 21:57:05 +08:00
YunYouJun
e861c5bd65 chore: fix typecheck 2023-08-20 05:02:20 +08:00
YunYouJun
94a9ff5007 feat: show custom cookbook 2023-08-20 04:59:14 +08:00
YunYouJun
98123b0039 feat: add settings page 2023-08-20 02:28:00 +08:00
YunYouJun
95ce2c4dac chore: fix readme & title 2023-08-03 23:19:37 +08:00
YunYouJun
1bd0a6721e chore: release before upload 2023-07-30 21:23:14 +08:00
YunYouJun
974d523f9d chore: release v1.1.6 2023-07-30 21:20:58 +08:00
YunYouJun
e9427f7fb0 chore: add release with files 2023-07-30 21:20:53 +08:00
223 changed files with 22822 additions and 10236 deletions

1
.env.example Normal file
View File

@@ -0,0 +1 @@
APPLE_DEVELOPMENT_TEAM=

View File

@@ -1,2 +0,0 @@
dist
public

View File

@@ -1,7 +0,0 @@
{
"extends": [
"@antfu",
"@unocss"
],
"ignorePatterns": "*.json"
}

View File

@@ -4,10 +4,12 @@ on:
push: push:
branches: branches:
- main - main
- dev
pull_request: pull_request:
branches: branches:
- main - main
- dev
jobs: jobs:
lint: lint:
@@ -21,7 +23,7 @@ jobs:
- name: Set node - name: Set node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16.x node-version: lts/*
cache: pnpm cache: pnpm
- name: Install - name: Install
@@ -41,7 +43,7 @@ jobs:
- name: Set node - name: Set node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16.x node-version: lts/*
cache: pnpm cache: pnpm
- name: Install - name: Install
@@ -58,7 +60,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [16.x] node-version: [lts/*]
os: [ubuntu-latest] os: [ubuntu-latest]
fail-fast: false fail-fast: false

View File

@@ -17,10 +17,10 @@ jobs:
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
# after pnpm # after pnpm
- name: Use Node.js 16 - name: Use Node.js LTS
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: lts/*
registry-url: https://registry.npmjs.org/ registry-url: https://registry.npmjs.org/
cache: pnpm cache: pnpm
@@ -28,6 +28,13 @@ jobs:
run: pnpm install run: pnpm install
- run: npm run generate --if-present - run: npm run generate --if-present
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: .output/public/
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: Cook Dist name: Cook Dist

4
.gitignore vendored
View File

@@ -1,5 +1,5 @@
# auto generate # auto generate
src/data/recipe.json data/recipe.json
.DS_Store .DS_Store
.vite-ssg-dist .vite-ssg-dist
@@ -7,6 +7,7 @@ src/data/recipe.json
*.local *.local
esbuild-kit esbuild-kit
tsx-*
# nuxt # nuxt
node_modules node_modules
@@ -16,3 +17,4 @@ dist
.nuxt .nuxt
.env .env
.idea/ .idea/
.vercel

2
.npmrc
View File

@@ -1,3 +1,5 @@
shamefully-hoist=true shamefully-hoist=true
strict-peer-dependencies=false strict-peer-dependencies=false
shell-emulator=true shell-emulator=true
auto-install-peers=false
ignore-workspace-root-check=true

58
.vscode/settings.json vendored
View File

@@ -1,11 +1,57 @@
{ {
"cSpell.words": ["Vitesse", "Vite", "unocss", "vitest", "vueuse", "pinia", "demi", "antfu", "iconify", "intlify", "vitejs", "unplugin", "pnpm"], "cSpell.words": [
"prettier.enable": false, "antfu",
"editor.codeActionsOnSave": { "demi",
"source.fixAll.eslint": true, "iconify",
}, "intlify",
"nuxi",
"pinia",
"pnpm",
"unocss",
"unplugin",
"Vite",
"vitejs",
"Vitesse",
"vitest",
"vueuse"
],
"files.associations": { "files.associations": {
"*.css": "postcss", "*.css": "postcss"
}, },
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false, "editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off" },
{ "rule": "*-indent", "severity": "off" },
{ "rule": "*-spacing", "severity": "off" },
{ "rule": "*-spaces", "severity": "off" },
{ "rule": "*-order", "severity": "off" },
{ "rule": "*-dangle", "severity": "off" },
{ "rule": "*-newline", "severity": "off" },
{ "rule": "*quotes", "severity": "off" },
{ "rule": "*semi", "severity": "off" }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml"
],
"typescript.tsdk": "node_modules/typescript/lib"
} }

View File

@@ -9,9 +9,10 @@
### 网页版本 ### 网页版本
- 网站链接:[cook.yunyoujun.cn](https://cook.yunyoujun.cn) - 网站链接Cloudflare[cook.yunyoujun.cn](https://cook.yunyoujun.cn)
- 备用[cook.yyj.moe](https://cook.yyj.moe) - 国内加速(腾讯云)[cook.yunle.fun](https://cook.yunle.fun)
- 开发版[cook.yunle.app](https://cook.yunle.app) - 备用Netlify[cook.yyj.moe](https://cook.yyj.moe)
- 开发版Vercel[cook.yunle.app](https://cook.yunle.app)
### 小程序版本 ### 小程序版本
@@ -73,8 +74,6 @@ docker stop cook
感谢以下小伙伴为本项目提供的数据支持和 QA 感谢以下小伙伴为本项目提供的数据支持和 QA
- [Runny](https://weibo.com/runny) - [Runny](https://weibo.com/runny)
- 山竹太凉
- leo
- 麒麟 - 麒麟
- 晴方啾 - 晴方啾
- 课代表阿伟 - 课代表阿伟

5
app.config.ts Normal file
View File

@@ -0,0 +1,5 @@
export default defineAppConfig({
theme: {
primaryColor: '#ababab',
},
})

View File

@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { installPrompt } from './utils/pwa' import { useIndexedDB } from '~/composables/db'
import { appName } from '~/constants' import { appName } from '~/constants'
import { installPrompt } from './utils/pwa'
// https://nuxt.com/docs/api/composables/use-head // https://nuxt.com/docs/api/composables/use-head
useHead({ useHead({

View File

@@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { isClient } from '@vueuse/core' import { isClient } from '@vueuse/core'
import pkg from '~/package.json'
const displayICP = ref(true) const displayICP = ref(true)
@@ -8,24 +7,11 @@ onBeforeMount(() => {
if (isClient) if (isClient)
displayICP.value = ['cook.yunyoujun.cn', 'localhost', '127.0.0.1'].includes(window.location.hostname) displayICP.value = ['cook.yunyoujun.cn', 'localhost', '127.0.0.1'].includes(window.location.hostname)
}) })
const commitSha = (import.meta.env.VITE_COMMIT_REF || '').slice(0, 7)
const now = import.meta.env.VITE_APP_BUILD_TIME
const buildDate = (new Date(Number.parseInt(now) * 1000)).toLocaleDateString()
</script> </script>
<template> <template>
<div p="4 t-2" class="flex flex-col items-center justify-center" text="sm"> <div p="4 t-2" class="flex flex-col items-center justify-center" text="sm">
<div v-if="commitSha && buildDate" mb-2> <CurrentVersion />
<span>
当前版本 v{{ pkg.version }}{{ buildDate }}:
</span>
<span>
<a border="b-1 dashed" :href="`https://github.com/YunYouJun/cook/commit/${commitSha}`" target="_blank" alt="Cook | GitHub Commit">
{{ commitSha }}
</a>
</span>
</div>
<a v-if="displayICP" opacity="80" class="flex" href="https://beian.miit.gov.cn/" target="_blank"> <a v-if="displayICP" opacity="80" class="flex" href="https://beian.miit.gov.cn/" target="_blank">
苏ICP备17038157号 苏ICP备17038157号
</a> </a>
@@ -36,13 +22,6 @@ const buildDate = (new Date(Number.parseInt(now) * 1000)).toLocaleDateString()
<span m="l-1" class="inline-flex" style="margin-top: 1px;">B </span> <span m="l-1" class="inline-flex" style="margin-top: 1px;">B </span>
</a> </a>
</div> </div>
<div mt-2>
本站点由
<a color="#F6821F" href="https://www.cloudflare-cn.com/" target="_blank" title="Cloudflare" border="b-1 dashed">
<span>Cloudflare</span>
</a>
提供 CDN 支持
</div>
<div m="t-2" opacity="80" class="flex items-center justify-center"> <div m="t-2" opacity="80" class="flex items-center justify-center">
©&nbsp;<a href="https://github.com/YunYouJun/cook" target="_blank">Cook</a> ©&nbsp;<a href="https://github.com/YunYouJun/cook" target="_blank">Cook</a>
<div text="xs" m="x-1" i-ri-cloud-line /> <div text="xs" m="x-1" i-ri-cloud-line />

View File

@@ -8,7 +8,10 @@ const props = defineProps({
const rStore = useRecipeStore() const rStore = useRecipeStore()
const { displayedRecipe } = storeToRefs(rStore) const { displayedRecipe } = storeToRefs(rStore)
const showBasketBtn = computed(async () => { /**
* Show basket button if there are recipes in the basket
*/
const showBasketBtn = computed(() => {
return displayedRecipe.value.length !== rStore.recipesLength && props.isVisible return displayedRecipe.value.length !== rStore.recipesLength && props.isVisible
}) })
</script> </script>
@@ -18,7 +21,8 @@ const showBasketBtn = computed(async () => {
v-show="showBasketBtn" v-show="showBasketBtn"
class="fixed z-9 inline-flex cursor-pointer items-center justify-center rounded rounded-full shadow hover:shadow-md" class="fixed z-9 inline-flex cursor-pointer items-center justify-center rounded rounded-full shadow hover:shadow-md"
bg="green-50 dark:green-900" w="10" h="10" bg="green-50 dark:green-900" w="10" h="10"
bottom="18" right="4" bottom="22"
right="4"
text="green-600 dark:green-300" text="green-600 dark:green-300"
> >
<span v-if="displayedRecipe.length > 0"> <span v-if="displayedRecipe.length > 0">

View File

@@ -1,10 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { StuffItem } from '~/types'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import type { StuffItem } from '~/data/food'
import { meat, staple, tools, vegetable } from '~/data/food'
import { useEmojiAnimation } from '~/composables/animation' import { useEmojiAnimation } from '~/composables/animation'
import { meat, staple, tools, vegetable } from '~/data/food'
const rStore = useRecipeStore() const rStore = useRecipeStore()
const { curTool } = storeToRefs(rStore) const { curTool } = storeToRefs(rStore)
const curStuff = computed(() => rStore.selectedStuff) const curStuff = computed(() => rStore.selectedStuff)
@@ -12,7 +12,8 @@ const curStuff = computed(() => rStore.selectedStuff)
const recipeBtnRef = ref<HTMLButtonElement>() const recipeBtnRef = ref<HTMLButtonElement>()
const { playAnimation } = useEmojiAnimation(recipeBtnRef) const { playAnimation } = useEmojiAnimation(recipeBtnRef)
const gtm = useGtm() const { proxy } = useScriptGoogleTagManager()
const recipePanelRef = ref() const recipePanelRef = ref()
const { isVisible, show } = useInvisibleElement(recipePanelRef) const { isVisible, show } = useInvisibleElement(recipePanelRef)
@@ -22,13 +23,13 @@ function toggleStuff(item: StuffItem, category = '', _e?: Event) {
if (curStuff.value.includes(item.name)) if (curStuff.value.includes(item.name))
playAnimation(item.emoji) playAnimation(item.emoji)
gtm?.trackEvent({ proxy.dataLayer.push({
event: 'click', event: 'click',
category: `${category}_${item.name}`, category: `${category}_${item.name}`,
action: 'click_stuff', action: 'click_stuff',
label: '食材', label: '食材',
}) })
gtm?.trackEvent({ proxy.dataLayer.push({
event: 'click_stuff', event: 'click_stuff',
action: item.name, action: item.name,
}) })

View File

@@ -3,13 +3,14 @@ import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
defineProps<{ defineProps<{
title: string title: string
defaultOpen?: boolean
}>() }>()
</script> </script>
<template> <template>
<Disclosure v-slot="{ open }" as="div" class="mt-2"> <Disclosure v-slot="{ open }" :default-open="defaultOpen" as="div" class="mt-2">
<DisclosureButton <DisclosureButton
class="w-full flex justify-between rounded-lg bg-blue-100 px-4 py-2 text-left text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring focus-visible:ring-blue-500 focus-visible:ring-opacity-75" class="w-full flex justify-between rounded-lg bg-blue-100 px-4 py-2 text-left text-sm text-blue-900 font-medium hover:bg-blue-200 focus:outline-none focus-visible:ring focus-visible:ring-blue-500 focus-visible:ring-opacity-75"
> >
<span>{{ title }}</span> <span>{{ title }}</span>
<div <div

View File

@@ -5,22 +5,23 @@ const { random, randomRecipes } = useRandomRecipe(count)
<template> <template>
<div inline-flex m="y-3"> <div inline-flex m="y-3">
<button rounded-full p-2 btn @click="dec()"> <button btn rounded-full p-2 @click="dec()">
<div i-carbon-subtract /> <div i-carbon-subtract />
</button> </button>
<div font="mono" w="15" m-auto inline-block> <div font="mono" w="15" m-auto inline-block>
{{ count }} {{ count }}
</div> </div>
<button rounded-full p-2 btn @click="inc()"> <button btn rounded-full p-2 @click="inc()">
<div i-carbon-add /> <div i-carbon-add />
</button> </button>
</div> </div>
<button cursor-pointer class="inline-flex inline-flex items-center justify-center rounded-md border-none bg-blue-600 px-3 py-1.5 text-sm text-white font-semibold leading-6 shadow-sm hover:bg-blue-500 focus-visible:outline-2 focus-visible:outline-blue-600 focus-visible:outline-offset-2 focus-visible:outline" @click="random">
<div class="transition" hover="text-blue-500" i-ri-refresh-line mr-1 inline-flex />
<div>随机一下</div>
</button>
<div v-show="randomRecipes.length > 0"> <div v-show="randomRecipes.length > 0">
<button cursor-pointer class="inline-flex inline-flex items-center justify-center rounded-md border-none bg-blue-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-blue-500 focus-visible:outline-2 focus-visible:outline-blue-600 focus-visible:outline-offset-2 focus-visible:outline" @click="random">
<div class="transition" hover="text-blue-500" i-ri-refresh-line mr-1 inline-flex />
<div>随机一下</div>
</button>
<div m="t-8" flex="~ col"> <div m="t-8" flex="~ col">
<template v-for="recipe, i in randomRecipes" :key="i"> <template v-for="recipe, i in randomRecipes" :key="i">
<DishTag v-if="recipe" :dish="recipe" /> <DishTag v-if="recipe" :dish="recipe" />

View File

@@ -0,0 +1,82 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
const rStore = useRecipeStore()
const { selectedStuff, curTool } = storeToRefs(rStore)
const showSearchInput = ref(false)
const showTooltip = computed(() => !selectedStuff.value.length && !curTool.value)
</script>
<template>
<div
class="recipe-panel relative shadow transition hover:shadow-md"
m="x-2 y-4" p="2"
bg="gray-400/8"
>
<RecipePanelTitle />
<ToggleMode />
<button absolute right-4 top-4 @click="showSearchInput = !showSearchInput">
<div v-if="!showSearchInput" i-ri-search-line />
<div v-else i-ri-search-fill />
</button>
<div class="cook-recipes" p="2">
<SearchFoodInput v-if="showSearchInput" />
<Transition mode="out-in">
<span v-if="showTooltip" text="sm" p="2">
你要先选食材或工具哦
</span>
<div
v-else-if="rStore.isSearching"
relative flex items-center justify-center p-6
text-xl
>
<div class="magnifying-glass" i-ri-search-line inline-flex />
</div>
<div v-else-if="rStore.displayedRecipe.length">
<DishTag v-for="item, i in rStore.displayedRecipe" :key="i" :dish="item" />
</div>
<div v-else text="sm">
<span>还没有完美匹配的菜谱呢</span>
<br>
<span>大胆尝试一下或者</span>
<a href="#" @click="rStore.reset()">
<strong>换个组合</strong>
</a>
<span></span>
<br>
<div m="t-1">
<span>欢迎来</span>
<a class="text-blue-600 font-bold dark:text-blue-400" href="https://docs.qq.com/sheet/DQk1vdkhFV0twQVNS?tab=uykkic" target="_blank">这里</a>
<span>反馈新的菜谱</span>
</div>
</div>
</Transition>
</div>
</div>
</template>
<style>
@keyframes circle-rotate {
from {
transform: rotate(0turn) translateY(60%) rotate(1turn);
}
to {
transform: rotate(1turn) translateY(60%) rotate(0turn);
}
}
.magnifying-glass {
margin: auto;
animation: circle-rotate 4s linear infinite;
}
</style>

View File

@@ -43,22 +43,22 @@ defineProps<{
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
-webkit-transition: .4s; -webkit-transition: 0.4s;
transition: .4s; transition: 0.4s;
} }
$size: 20px; $size: 20px;
.slider:before { .slider:before {
position: absolute; position: absolute;
content: ""; content: '';
height: $size; height: $size;
width: $size; width: $size;
left: 4px; left: 4px;
bottom: 4px; bottom: 4px;
background-color: white; background-color: white;
-webkit-transition: .4s; -webkit-transition: 0.4s;
transition: .4s; transition: 0.4s;
} }
input:checked + .slider { input:checked + .slider {

View File

@@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { BottomMenuItem } from '@yunlefun/vue' import type { BottomMenuItem } from '@yunlefun/vue'
import { ref } from 'vue'
const items: BottomMenuItem[] = [ const items: BottomMenuItem[] = [
{ {
@@ -38,20 +37,20 @@ const items: BottomMenuItem[] = [
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const active = ref(route.path)
function onClick(item: BottomMenuItem) { function onClick(item: BottomMenuItem) {
active.value = item.to || '' // router.push(item.to || '/')
router.push(item.to || '/') router.replace(item.to || '/')
} }
</script> </script>
<template> <template>
<YlfBottomMenu shadow-2xl> <YlfBottomMenu shadow-2xl pb="$cook-bottom-menu-padding-bottom">
<YlfBottomMenuItem <YlfBottomMenuItem
v-for="item in items" v-for="item in items"
:key="item.to" :key="item.to"
:item="item" :item="item"
:active="active === item.to" :active="route.path === item.to"
class="pt-3"
@click="onClick" @click="onClick"
/> />
</YlfBottomMenu> </YlfBottomMenu>

View File

@@ -21,7 +21,7 @@ const searchModes: {
<template> <template>
<div> <div>
<button <button
v-for="mode in searchModes" :key="mode.id" class="rounded px-2 tag" v-for="mode in searchModes" :key="mode.id" class="tag rounded px-2"
:bg="mode.id === rStore.curMode ? 'orange-500 dark:orange-600 opacity-100' : 'orange-300 opacity-20'" :bg="mode.id === rStore.curMode ? 'orange-500 dark:orange-600 opacity-100' : 'orange-300 opacity-20'"
:text="mode.id === rStore.curMode ? 'orange-100' : 'orange-800 dark:orange-200'" :text="mode.id === rStore.curMode ? 'orange-100' : 'orange-800 dark:orange-200'"
@click="rStore.setMode(mode.id)" @click="rStore.setMode(mode.id)"

View File

@@ -0,0 +1,13 @@
<script lang="ts" setup>
const router = useRouter()
function back() {
router.back()
}
</script>
<template>
<YlfIconButton
icon="i-ri-arrow-left-s-line"
@click="back"
/>
</template>

View File

@@ -15,7 +15,11 @@ function toggleDark() {
</script> </script>
<template> <template>
<button class="mx-2 icon-btn hover:text-yellow-400 !outline-none" title="切换" @click="toggleDark()"> <YlfIconButton
class="icon-btn hover:text-yellow-400 !outline-none"
text-xl
title="切换" @click="toggleDark()"
>
<div i="ri-sun-line dark:ri-moon-line" /> <div i="ri-sun-line dark:ri-moon-line" />
</button> </YlfIconButton>
</template> </template>

View File

@@ -0,0 +1,105 @@
<script setup lang="ts">
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
import { db } from '~/utils/db'
const isOpen = ref(false)
function closeModal() {
isOpen.value = false
}
function openModal() {
isOpen.value = true
}
const keyword = ref('')
async function getFilterRecipes(keyword: string) {
return db.recipes.filter((recipe) => {
return recipe.name.includes(keyword)
}).toArray()
}
const filteredRecipes = computedAsync(async () => {
return await getFilterRecipes(keyword.value)
})
</script>
<template>
<YlfIconButton
absolute right-4 top-4
class="icon-btn hover:text-yellow-400 !outline-none"
text-xl
title="切换" @click="openModal"
>
<div i="ri-search-line" />
</YlfIconButton>
<TransitionRoot appear :show="isOpen" as="template">
<Dialog as="div" class="relative z-10" @close="closeModal">
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="fixed inset-0 bg-black/10" />
</TransitionChild>
<div class="fixed inset-0 overflow-y-auto">
<div
class="h-full flex justify-center text-center"
>
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100"
leave="duration-200 ease-in"
leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95"
>
<DialogPanel
class="h-full max-w-xl w-full transform overflow-hidden bg-white p-4 text-left align-middle shadow-xl transition-all dark:bg-dark-600"
md="rounded-2xl"
overflow="auto"
flex="~ col"
>
<DialogTitle
as="h3"
class="flex items-center justify-center text-lg font-medium leading-6"
>
<div relative inline-flex flex="grow">
<div
i-ri-search-line
class="absolute left-3 top-2 cursor-pointer text-gray-400"
/>
<input
v-model="keyword"
type="text"
class="w-full rounded-full bg-transparent text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 placeholder-gray-400"
border="~ rounded-full gray-300 op-50 focus:border-blue-500"
placeholder="搜索菜谱"
autofocus py-2 pl-10 pr-3
>
<div
v-if="keyword" i-ri-close-line
class="absolute right-3 top-2 cursor-pointer text-gray-400"
@click="keyword = ''"
/>
</div>
<button op="70" ml-2 inline-flex cursor-pointer text-base @click="closeModal">
取消
</button>
</DialogTitle>
<div flex="~ col grow" overflow="auto" class="mt-2" text-xs>
<DishTag v-for="item, i in filteredRecipes" :key="i" :dish="item" />
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import VueAboutMe from 'vue-about-me' import { VueAboutMe } from 'vue-about-me'
import 'vue-about-me/style.css' import 'vue-about-me/style.css'
const color = useColorMode() const color = useColorMode()

View File

@@ -1,18 +1,18 @@
<template> <template>
<div> <div>
<NuxtLink class="mx-2 icon-btn hover:text-orange-400" to="/help" title="帮助"> <NuxtLink class="icon-btn mx-2 hover:text-orange-400" to="/help" title="帮助">
<div i-ri-question-line /> <div i-ri-question-line />
</NuxtLink> </NuxtLink>
<NuxtLink class="mx-2 icon-btn hover:text-blue-400" to="/about" title="关于"> <NuxtLink class="icon-btn mx-2 hover:text-blue-400" to="/about" title="关于">
<div i-ri-information-line /> <div i-ri-information-line />
</NuxtLink> </NuxtLink>
<a class="mx-2 icon-btn hover:text-pink-400" rel="noreferrer" href="https://space.bilibili.com/1579790" target="_blank" title="BiliBili"> <a class="icon-btn mx-2 hover:text-pink-400" rel="noreferrer" href="https://space.bilibili.com/1579790" target="_blank" title="BiliBili">
<div i-ri-bilibili-line /> <div i-ri-bilibili-line />
</a> </a>
<a class="hover:text-black-400 mx-2 icon-btn" rel="noreferrer" href="https://github.com/YunYouJun/cook" target="_blank" title="GitHub"> <a class="icon-btn hover:text-black-400 mx-2" rel="noreferrer" href="https://github.com/YunYouJun/cook" target="_blank" title="GitHub">
<div i-ri-github-line /> <div i-ri-github-line />
</a> </a>
</div> </div>

View File

@@ -0,0 +1,31 @@
<script lang="ts" setup>
import type { Cookbook } from '~/types'
defineProps<{
cookbook: Cookbook
}>()
const showDetail = ref(false)
</script>
<template>
<button
class="bg-$c-bg-alt"
h-36 w-full inline-flex cursor-pointer items-center justify-center shadow
@click="showDetail = true"
>
<slot />
</button>
<CookbookDetail
v-if="showDetail"
absolute bottom-17 left-2 right-2 top-2 z-1 overflow-hidden shadow
:cookbook="cookbook"
>
<YlfIconButton
icon="i-ri-close-line"
class="absolute right-2 top-2"
@click="showDetail = false"
/>
</CookbookDetail>
</template>

View File

@@ -0,0 +1,27 @@
<script lang="ts" setup>
import type { Cookbook } from '~/types'
const props = defineProps<{
cookbook: Cookbook
}>()
const recipes = ref<Cookbook['recipes']>(props.cookbook.recipes)
onMounted(async () => {
recipes.value = ((await import('../../data/recipe.json')).default) as unknown as Cookbook['recipes']
})
</script>
<template>
<div class="bg-$c-bg-alt" flex="~ col">
<h3 mt-4 font-bold>
{{ cookbook.title }}
</h3>
<sub op="90" my-3>
{{ cookbook.description }}
</sub>
<div mx-auto mt-2 p-0 border="1px" overflow-y="scroll">
<RecipeTable h="full" :recipes="recipes" />
</div>
<slot />
</div>
</template>

View File

@@ -0,0 +1,18 @@
<script lang="ts" setup>
definePageMeta({
layout: 'child',
title: '新建食谱书',
})
</script>
<template>
<NuxtLink
class="bg-$c-bg-alt"
h-36 w-full inline-flex cursor-pointer items-center justify-center shadow
to="/cookbooks/new"
>
<slot>
<div i-ri-add-line />
</slot>
</NuxtLink>
</template>

View File

@@ -0,0 +1,13 @@
<script lang="ts" setup>
// migrate to 腾讯云 EdgeOne
</script>
<template>
<div mt-2>
本站点由
<a color="#F6821F" href="https://www.cloudflare-cn.com/" target="_blank" title="Cloudflare" border="b-1 dashed">
<span>Cloudflare</span>
</a>
提供 CDN 支持
</div>
</template>

View File

@@ -0,0 +1,29 @@
<template>
<FAQItem :default-open="true" title="关于">
<div text-left>
<ul>
<li>
它诞生于 2022 4 时值疫情风控期间希望能帮助期间的伙伴根据现有食材寻找到合适的菜谱故原名隔离食用手册
</li>
<li>
如今那个时期已离我们远去故去掉隔离二字但也很高兴能在这里继续与你相遇希望它能继续发光发热在日常生活中帮助到大家
</li>
<li>
<div class="inline-flex items-center justify-center">
代码仓库<a class="inline-flex items-center justify-center" href="https://github.com/YunYouJun/cook" target="_blank">
<div m="r-1" i-ri-github-line inline-flex />YunYouJun/cook</a>
</div>
</li>
<li>
<div class="inline-flex items-center justify-center">
菜谱视频来源
<a class="inline-flex items-center text-sm text-blue-600 dark:text-blue-400" href="https://docs.qq.com/sheet/DQk1vdkhFV0twQVNS" target="_blank">
<div m="r-1" i-ri-bilibili-line inline-flex />
<span class="inline-flex">隔离食用手册大全</span>
</a>
</div>
</li>
</ul>
</div>
</FAQItem>
</template>

View File

@@ -0,0 +1,20 @@
<script lang="ts" setup>
import pkg from '~/../package.json'
const commitSha = (import.meta.env.VITE_COMMIT_REF || '').slice(0, 7)
const date = import.meta.env.VITE_APP_BUILD_DATE
const buildDate = (new Date(date)).toLocaleDateString()
</script>
<template>
<div v-if="commitSha && buildDate" mb-2 text-sm>
<span>
当前版本 v{{ pkg.version }}{{ buildDate }}:
</span>
<span>
<a border="b-1 dashed" :href="`https://github.com/YunYouJun/cook/commit/${commitSha}`" target="_blank" alt="Cook | GitHub Commit">
{{ commitSha }}
</a>
</span>
</div>
</template>

View File

@@ -0,0 +1,22 @@
<template>
<div text="center sm" my-3>
<CurrentVersion />
<div flex="~" items-center justify-center gap="2">
<a
href="https://github.com/YunYouJun/cook" target="_blank"
class="inline-flex items-center justify-center"
>
<div i-ri-github-line mr-1 />
<span>Code</span>
</a>
by
<a
href="https://www.bilibili.com/opus/649847454294868008" target="_blank"
class="inline-flex items-center justify-center"
>
<div i-ri-bilibili-line mr-1 class="text-pink-400" />
<span>云游君</span>
</a>
</div>
</div>
</template>

View File

@@ -0,0 +1,48 @@
<script lang="ts" setup>
import type { Recipes } from '~/types'
defineProps<{
recipes: Recipes
}>()
</script>
<template>
<table
class="recipe-table bg-$c-bg"
overflow="auto" h="full"
>
<thead>
<tr>
<th />
<th>
名称
</th>
<th>
工具
</th>
<th>
材料
</th>
</tr>
</thead>
<tbody>
<RecipeTableItem
v-for="(recipe, i) in recipes"
:key="recipe.name"
:index="i" :recipe="recipe"
/>
</tbody>
</table>
</template>
<style lang="scss">
.recipe-table {
font-size: 0.8rem;
}
tr,
th,
td {
border: 1px solid black;
}
</style>

View File

@@ -0,0 +1,31 @@
<script lang="ts" setup>
import type { RecipeItem } from '~/types'
defineProps<{
index: number
recipe: RecipeItem
}>()
</script>
<template>
<tr>
<td>
{{ index }}
</td>
<td>
<a
class="text-blue-500" font-bold
:href="recipe.link || `https://www.bilibili.com/video/${recipe.bv}`"
target="_blank"
>
{{ recipe.name }}
</a>
</td>
<td>
{{ recipe.tools.join('、') }}
</td>
<td>
{{ recipe.stuff.join('、') }}
</td>
</tr>
</template>

View File

@@ -0,0 +1,11 @@
<template>
<div
class="tagSize pureTag rounded"
p="x-2"
border="~ yellow-200 dark:yellow-800"
bg="yellow-300 opacity-20"
text="yellow-800 dark:yellow-200"
>
<slot />
</div>
</template>

View File

@@ -1,25 +1,31 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { DbRecipeItem } from 'utils/db'
import { tools } from '~/data/food'
import type { RecipeItem } from '~/types' import type { RecipeItem } from '~/types'
import type { DbRecipeItem } from '~/utils/db'
import { recipeHistories } from '~/composables/store/history'
import { tools } from '~/data/food'
import { getEmojisFromStuff } from '~/utils' import { getEmojisFromStuff } from '~/utils'
const props = defineProps<{ const props = defineProps<{
dish: RecipeItem | DbRecipeItem dish: RecipeItem | DbRecipeItem
}>() }>()
const gtm = useGtm() const { proxy } = useScriptGoogleTagManager()
function triggerGtm(val: string) { function triggerGtm(dish: RecipeItem) {
gtm?.trackEvent({ recipeHistories.value.push({
recipe: dish,
time: Date.now(),
})
proxy.dataLayer.push({
event: 'click', event: 'click',
category: `dish_${val}`, category: `dish_${dish.name}`,
action: 'click_recipe', action: 'click_recipe',
label: '跳转菜谱', label: '跳转菜谱',
}) })
gtm?.trackEvent({ proxy.dataLayer.push({
event: 'click_dish', event: 'click_dish',
action: val, action: dish.name,
}) })
} }
@@ -31,16 +37,16 @@ const dishLabel = computed(() => {
<template> <template>
<a <a
:href="dish.link || `https://www.bilibili.com/video/${dish.bv}`" target="_blank" class="dish-tag rounded tag" p="x-2" :href="dish.link || `https://www.bilibili.com/video/${dish.bv}`" target="_blank" class="dish-tag tag rounded" p="x-2"
border="~ blue-200 dark:blue-800" border="~ blue-200 dark:blue-800"
bg="blue-300 opacity-20" bg="blue-300 opacity-20"
@click="triggerGtm(dish.name)" @click="triggerGtm(dish)"
> >
<span m="r-1" class="inline-flex items-center justify-center" text="sm blue-700 dark:blue-200"> <span m="r-1" text="sm blue-700 dark:blue-200">
{{ dishLabel }} {{ dishLabel }}
</span> </span>
<span v-for="tool, i in tools" :key="i" inline-flex> <template v-for="tool, i in tools">
<div v-if="dish.tools?.includes(tool.name)" :class="tool.icon" /> <span v-if="dish.tools?.includes(tool.name)" :key="i" :class="tool.icon" />
</span> </template>
</a> </a>
</template> </template>

View File

@@ -5,12 +5,12 @@ defineProps<{
</script> </script>
<template> <template>
<span <button
class="meat-tag rounded tag" p="x-2" class="meat-tag tag rounded" p="x-2"
border="~ red-200 dark:red-800" border="~ red-200 dark:red-800"
:bg="active ? 'red-500 opacity-90' : 'red-300 opacity-20'" :bg="active ? 'red-500 opacity-90' : 'red-300 opacity-20'"
:text="active ? 'red-100' : 'red-800 dark:red-200'" :text="active ? 'red-100' : 'red-800 dark:red-200'"
> >
<slot /> <slot />
</span> </button>
</template> </template>

View File

@@ -5,11 +5,11 @@ defineProps<{
</script> </script>
<template> <template>
<span <button
class="rounded tag" p="x-2" border="~ yellow-200 dark:yellow-800" class="tag rounded" p="x-2" border="~ yellow-200 dark:yellow-800"
:bg="active ? 'yellow-500 dark:yellow-600 opacity-100' : 'yellow-300 opacity-20'" :bg="active ? 'yellow-500 dark:yellow-600 opacity-100' : 'yellow-300 opacity-20'"
:text="active ? 'yellow-100' : 'yellow-800 dark:yellow-200'" :text="active ? 'yellow-100' : 'yellow-800 dark:yellow-200'"
> >
<slot /> <slot />
</span> </button>
</template> </template>

View File

@@ -5,12 +5,12 @@ defineProps<{
</script> </script>
<template> <template>
<span <button
class="rounded tag" p="x-2" class="tag rounded" p="x-2"
border="~ stone-200 dark:stone-600" border="~ stone-200 dark:stone-600"
:bg="active ? 'stone-600 opacity-100' : 'stone-300 opacity-5'" :bg="active ? 'stone-600 opacity-100' : 'stone-300 opacity-5'"
:text="active ? 'stone-100' : 'stone-800 dark:stone-200'" :text="active ? 'stone-100' : 'stone-800 dark:stone-200'"
> >
<slot /> <slot />
</span> </button>
</template> </template>

View File

@@ -5,12 +5,12 @@ defineProps<{
</script> </script>
<template> <template>
<span <button
class="vegetable-tag rounded tag" p="x-2" class="vegetable-tag tag rounded" p="x-2"
border="~ green-200 dark:green-800" border="~ green-200 dark:green-800"
:bg="active ? 'green-600 opacity-90' : 'green-300 opacity-20'" :bg="active ? 'green-600 opacity-90' : 'green-300 opacity-20'"
:text="active ? 'green-100' : 'green-800 dark:green-200'" :text="active ? 'green-100' : 'green-800 dark:green-200'"
> >
<slot /> <slot />
</span> </button>
</template> </template>

View File

@@ -0,0 +1,24 @@
<template>
<div class="ylf-form" flex="~ col" rounded-md>
<slot />
</div>
</template>
<style lang="scss">
.ylf-form {
background-color: var(--ylf-c-bg-alt);
border: 1px solid var(--ylf-c-border);
margin: 10px 0;
overflow: hidden;
.ylf-form-item {
border-bottom: 1px solid var(--ylf-c-border);
&:last-child {
border-bottom: none;
}
}
}
</style>

View File

@@ -0,0 +1,33 @@
<script lang="ts" setup>
import { NuxtLink } from '#components'
defineProps<{
icon?: string
label?: string
/**
* Router link
*/
to?: string
}>()
</script>
<template>
<component
:is="to ? NuxtLink : 'div'"
:to="to"
class="ylf-form-item"
w-full flex cursor-pointer items-center justify-between p-2
hover:bg-gray-100
dark:hover:bg-dark-400
>
<div v-if="label" class="text-sm" inline-flex items-center justify-center>
<div v-if="icon" :class="icon" mr-2 inline-flex />
<span>{{ label }}</span>
</div>
<div inline-flex>
<slot>
<div v-if="to" i-ri-arrow-right-s-line />
</slot>
</div>
</component>
</template>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
defineProps<{
icon?: string
}>()
</script>
<template>
<button
class="ylf-icon-button hover:(bg-blue-300 bg-opacity-20)"
h-10 w-10 inline-flex items-center justify-center rounded-full
>
<slot>
<div v-if="icon" text-xl :class="icon" />
</slot>
</button>
</template>

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
defineProps<{
icon: string
label: string
to: string
}>()
</script>
<template>
<NuxtLink
:to="to"
flex="~ col"
border="~ solid dark:$ylf-c-border"
bg="$ylf-c-bg-alt"
class="inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium decoration-none hover-bg-gray-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 dark:hover:bg-dark-400"
>
<div :class="icon" inline-flex text-lg />
<div mt-2 inline-flex text-xs>
{{ label }}
</div>
</NuxtLink>
</template>

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
import { Switch } from '@headlessui/vue'
defineProps<{
modelValue: boolean
}>()
const emit = defineEmits(['update:modelValue'])
function updateModelValue(value: boolean) {
emit('update:modelValue', value)
}
</script>
<template>
<Switch
:model-value="modelValue"
:class="modelValue ? 'bg-blue-600' : 'bg-gray'"
class="relative h-6 w-11 inline-flex shrink-0 cursor-pointer border-2 border-transparent rounded-full transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
@update:model-value="updateModelValue"
>
<span class="sr-only">Use setting</span>
<span
aria-hidden="true"
:class="modelValue ? 'translate-x-5' : 'translate-x-0'"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out"
/>
</Switch>
</template>

View File

@@ -1,5 +1,5 @@
import { isClient } from '@vueuse/core'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { isClient } from '@vueuse/core'
export function useEmojiAnimation(recipeBtn: Ref<HTMLButtonElement | undefined>) { export function useEmojiAnimation(recipeBtn: Ref<HTMLButtonElement | undefined>) {
const { x, y } = usePointer() const { x, y } = usePointer()

View File

@@ -1,5 +1,6 @@
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { lastDbUpdated, namespace } from '~/constants' import { lastDbUpdated, namespace } from '~/constants'
import { db, initDb } from '~/utils/db'
export function useIndexedDB() { export function useIndexedDB() {
const dbUpdated = useStorage(`${namespace}:lastDbUpdated`, lastDbUpdated) const dbUpdated = useStorage(`${namespace}:lastDbUpdated`, lastDbUpdated)

View File

@@ -4,7 +4,6 @@ import { isClient, useElementBounding } from '@vueuse/core'
/** /**
* trigger show invisible element * trigger show invisible element
* @param target * @param target
* @returns
*/ */
export function useInvisibleElement(target: MaybeComputedElementRef<HTMLElement>) { export function useInvisibleElement(target: MaybeComputedElementRef<HTMLElement>) {
const { top } = useElementBounding(target) const { top } = useElementBounding(target)

View File

@@ -1,12 +1,13 @@
import type { DbRecipeItem } from 'utils/db' import type { DbRecipeItem } from '~/utils/db'
import { useStorage } from '@vueuse/core'
import { namespace } from '~/constants'
/** /**
* *
* @param total * @param total
* @returns
*/ */
export function useRandomRecipe(total: Ref<number>) { export function useRandomRecipe(total: Ref<number>) {
const randomRecipes = ref<(DbRecipeItem | undefined)[]>([]) const randomRecipes = useStorage<(DbRecipeItem | undefined)[]>(`${namespace}:random:recipes`, [])
async function random() { async function random() {
const length = await db.recipes.count() const length = await db.recipes.count()
const randomArr = generateRandomArray(length, total.value) const randomArr = generateRandomArray(length, total.value)
@@ -20,7 +21,9 @@ export function useRandomRecipe(total: Ref<number>) {
}) })
onMounted(() => { onMounted(() => {
random() // 如果没有随机菜谱,就生成一次
if (randomRecipes.value.length <= 0)
random()
}) })
return { return {

View File

@@ -1,10 +1,17 @@
import { useStorage } from '@vueuse/core'
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
import { ref } from 'vue'
import { namespace } from '../../constants'
import { defaultSettings } from '../../utils/settings'
export const useAppStore = defineStore('app', () => { export const useAppStore = defineStore('app', () => {
const deferredPrompt = ref<Event | any>() const deferredPrompt = ref<Event | any>()
const settings = useStorage(`${namespace}:settings`, defaultSettings)
return { return {
deferredPrompt, deferredPrompt,
settings,
} }
}) })

View File

@@ -0,0 +1,10 @@
import type { RecipeItem } from '~/types'
import { useStorage } from '@vueuse/core'
import { namespace } from '~/constants'
export interface RecipeHistoryItem {
recipe: RecipeItem
time: number
}
export const recipeHistories = useStorage<RecipeHistoryItem[]>(`${namespace}:history`, [])

View File

@@ -1,10 +1,9 @@
import { acceptHMRUpdate, defineStore } from 'pinia' import type { RecipeItem, StuffItem } from '~/types'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { acceptHMRUpdate, defineStore } from 'pinia'
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { useGtm } from '@gtm-support/vue-gtm'
import type { RecipeItem } from 'types'
import type { StuffItem } from '../../data/food'
import { db } from '../../utils/db' import { db } from '../../utils/db'
import { useAppStore } from './app'
const namespace = 'cook' const namespace = 'cook'
@@ -16,7 +15,8 @@ const namespace = 'cook'
export type SearchMode = 'survival' | 'loose' | 'strict' export type SearchMode = 'survival' | 'loose' | 'strict'
export const useRecipeStore = defineStore('recipe', () => { export const useRecipeStore = defineStore('recipe', () => {
const gtm = useGtm() const { proxy } = useScriptGoogleTagManager()
const { settings } = useAppStore()
/** /**
* *
@@ -24,10 +24,10 @@ export const useRecipeStore = defineStore('recipe', () => {
const keyword = ref('') const keyword = ref('')
// can not exported // can not exported
const curStuff = useStorage(`${namespace}:stuff`, new Set<string>()) const curStuff = settings.keepLocalData ? useStorage(`${namespace}:stuff`, new Set<string>()) : ref(new Set<string>())
// const curTools = ref(new Set<string>()) // const curTools = ref(new Set<string>())
const curTool = useStorage(`${namespace}:tool`, '') const curTool = settings.keepLocalData ? useStorage(`${namespace}:tool`, '') : ref('')
const curMode = useStorage<SearchMode>(`${namespace}:mode`, 'loose') const curMode = settings.keepLocalData ? useStorage<SearchMode>(`${namespace}:mode`, 'loose') : ref<SearchMode>('loose')
const selectedStuff = computed(() => Array.from(curStuff.value)) const selectedStuff = computed(() => Array.from(curStuff.value))
// const selectedTools = computed(() => Array.from(curTools.value)) // const selectedTools = computed(() => Array.from(curTools.value))
@@ -73,13 +73,10 @@ export const useRecipeStore = defineStore('recipe', () => {
const isSearching = ref(false) const isSearching = ref(false)
/** /**
* *
* @returns
*/ */
async function searchRecipes() { async function searchRecipes() {
isSearching.value = true isSearching.value = true
let result: RecipeItem[] = [] let result: RecipeItem[] = []
if (keyword.value)
result = await db.recipes.filter(item => item.name.includes(keyword.value)).toArray()
if (curMode.value === 'strict') { if (curMode.value === 'strict') {
result = await db.recipes.filter((item) => { result = await db.recipes.filter((item) => {
@@ -116,14 +113,18 @@ export const useRecipeStore = defineStore('recipe', () => {
}).toArray() }).toArray()
} }
if (keyword.value)
result = result.filter(item => item.name.includes(keyword.value))
isSearching.value = false isSearching.value = false
return result return result
} }
// 默认严格模式 // 默认严格模式
const displayedRecipe = ref<RecipeItem[]>([]) const displayedRecipe = ref<RecipeItem[]>([])
watch([keyword, curStuff, curTool, curMode], async () => { // fix curStuff watch
displayedRecipe.value = await searchRecipes() watch(() => [keyword.value, selectedStuff.value, curTool.value, curMode.value], async () => {
displayedRecipe.value = [...(await searchRecipes())]
}) })
/** /**
@@ -134,13 +135,13 @@ export const useRecipeStore = defineStore('recipe', () => {
const value = item.name const value = item.name
toggleTools(value) toggleTools(value)
gtm?.trackEvent({ proxy.dataLayer.push({
event: 'click', event: 'click',
category: `tool_${value}`, category: `tool_${value}`,
action: 'click_tool', action: 'click_tool',
label: '工具', label: '工具',
}) })
gtm?.trackEvent({ proxy.dataLayer.push({
event: 'click_tool', event: 'click_tool',
action: item.name, action: item.name,
}) })

View File

@@ -1,5 +1,5 @@
import process from 'node:process'
import type { ModuleOptions } from '@vite-pwa/nuxt' import type { ModuleOptions } from '@vite-pwa/nuxt'
import process from 'node:process'
import { appDescription, appName } from '../constants/index' import { appDescription, appName } from '../constants/index'
const scope = '/' const scope = '/'

View File

@@ -1,7 +1,7 @@
export const appName = '隔离食用手册' export const appName = '食用手册'
export const appDescription = '好的,今天我们来做菜!' export const appDescription = '好的,今天我们来做菜!'
export const namespace = 'cook' export const namespace = 'cook'
export const lastDbUpdated = '2022-07-27 03:05:02' export const lastDbUpdated = '2023-11-11 19:51:02'
export * from './links' export * from './links'

View File

@@ -7,4 +7,9 @@ export const links = {
* *
*/ */
feedback: 'https://support.qq.com/product/507827', feedback: 'https://support.qq.com/product/507827',
/**
* changelog
*/
changelog: 'https://docs.yunyoujun.cn/projects/cook/changelog',
} }

View File

@@ -1,29 +1,4 @@
export interface StuffItem { import type { StuffItem } from '../types'
/**
*
*/
name: string
/**
* 🥔
*/
emoji: string
/**
*
*/
image?: string
/**
* 西/
*/
alias?: string
/**
*
*/
icon?: string
/**
*
*/
label?: string
}
/** /**
* *

File diff suppressed because it is too large Load Diff

1
app/data/recipe.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -6,9 +6,9 @@ By default, `default.vue` will be used unless an alternative is specified in the
```html ```html
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
layout: 'home', layout: 'home',
}) })
</script> </script>
``` ```

22
app/layouts/child.vue Normal file
View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
defineProps<{
title?: string
}>()
const route = useRoute()
</script>
<template>
<main class="text-center text-gray-700 dark:text-gray-200" p="t-5 b-15">
<div flex items-center justify-between>
<BackBtn ml-3 />
<h2 flex items-center justify-center text-lg font="bold">
{{ route.meta.title }}
</h2>
<DarkToggle mr-3 />
</div>
<slot />
<TheBottomMenu fixed bottom-0 left-0 right-0 />
</main>
</template>

8
app/layouts/default.vue Normal file
View File

@@ -0,0 +1,8 @@
<template>
<main class="cook-main text-center text-gray-700 dark:text-gray-200" p="t-8 b-$cook-bottom-menu-height">
<slot />
<DarkToggle absolute left-4 top-4 />
<SearchRecipe />
<TheBottomMenu fixed bottom-0 left-0 right-0 />
</main>
</template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const router = useRouter() // const router = useRouter()
</script> </script>
<template> <template>
@@ -9,9 +9,9 @@ const router = useRouter()
</div> </div>
<div>菜谱消失了</div> <div>菜谱消失了</div>
<div> <div>
<button text-sm btn m="3 t8" @click="router.back()"> <NuxtLink btn text-sm m="3 t8" to="/">
返回 返回主页
</button> </NuxtLink>
</div> </div>
</main> </main>
</template> </template>

View File

@@ -1,7 +1,5 @@
<template> <template>
<div> <div>
<button> 关于
设置
</button>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,24 @@
<script lang="ts" setup>
import { defaultCookbook } from '~/utils'
definePageMeta({
layout: 'child',
title: '自定义菜谱',
})
</script>
<template>
<div>
<h3>
开发中敬请期待
</h3>
<div grid="~ cols-3" gap="4" p="4">
<CookbookCard :cookbook="defaultCookbook">
默认菜谱
</CookbookCard>
<NewCookbookCard />
</div>
</div>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<div>
新建 Cookbook
</div>
</template>

141
app/pages/help.vue Normal file
View File

@@ -0,0 +1,141 @@
<template>
<div>
<div class="w-full">
<CommonHeader>
帮助
</CommonHeader>
<InstallPwa />
<FeedbackActions />
<div class="mx-auto max-w-md w-full rounded-2xl p-2" text-left>
<FAQItem title="未来计划?">
计划增加新功能如自定义菜谱与使用其他用户分享的菜谱
</FAQItem>
<FAQItem title="什么是模式?">
<ul>
<li><b>模糊匹配</b>展示所有含当前选中任意食材的菜谱</li>
<li><b>精准匹配</b>展示所有含当前选中所有食材的菜谱</li>
<li><b>生存模式</b>展示当前选中食材即可制作的所有菜谱</li>
</ul>
</FAQItem>
<FAQItem title="如何快速清空所选食材和工具?">
<div inline-flex items-center justify-center>
点击顶部 <div i-mdi-pot-steam-outline mx-1 inline-block /> 图标即可
</div>
</FAQItem>
<FAQItem title="是否有微信小程序?">
因不可抗力小程序因跳转 B 站视频而被判定为导流违规下架
将不再提供小程序版本
<br>
<br>
搜索微信公众号<b>云游君</b>并发送<b>做菜</b>也可以快速找到本网站
</FAQItem>
<FAQItem title="是否有 APP?">
<b>暂时没有开发 APP 的计划</b>
<br>
但我们正在优化 <b>PWA</b> 的体验以便您可以直接将本站添加到桌面并享受<b>类似 APP 的体验</b>
<br>
你可以使用浏览器打开点击上方的<b>安装到桌面</b>或在菜单中点击<b>添加到主屏幕</b>
</FAQItem>
<FAQItem title="未来是否会收费?">
该项目将以免费开源的形式运营
<br>
您可以考虑赞助本项目以支持我们的开发
我会将其投入在周边的服务器域名CDN 等费用上
<ul mt-1>
<li>
<a href="https://afdian.net/a/yunyoujun" target="_blank">爱发电赞助</a>
</li>
<li>
<a href="https://sponsors.yunyoujun.cn/" target="_blank">我要直接打钱</a>
</li>
</ul>
</FAQItem>
<FAQItem title="页面无法点击、资源加载失败?">
<blockquote>
试试无痕模式是否正常
</blockquote>
<br>
<ol>
<li>
<b>清除 Cookie</b>
<ol>
<li>
点击浏览器网址前方的 🔒 图标
</li>
<li>
点击Cookie并清除
</li>
</ol>
</li>
<li>
<b>强制刷新缓存</b>
<ul>
<li>Windows: <code>Ctrl + F5</code></li>
<li>macOS: <code>Cmd + Shift + R</code></li>
</ul>
</li>
</ol>
</FAQItem>
<hr h="1" my="4" bg-black>
<HelpAbout />
<FAQItem title="关于我">
<div text-left>
我的个人微信公众号云游君会分享一些生活和写的<a href="https://sponsors.yunyoujun.cn/projects" target="_blank">
小玩具们
</a>
<a inline-flex py-4 href="https://cdn.yunyoujun.cn/img/about/white-qrcode-and-search.jpg" target="_blank">
<img src="https://cdn.yunyoujun.cn/img/about/white-qrcode-and-search.jpg">
</a>
</div>
<AboutMe />
</FAQItem>
<FAQItem title="致谢">
<p>
感谢以下小伙伴为本项目提供的数据支持和 QA
</p>
<ul mt-2 text-left text-sm>
<li>
<a href="https://weibo.com/runny" target="_blank">Runny</a>
</li>
<li>
麒麟
</li>
<li>
晴方啾
</li>
<li>
课代表阿伟
</li>
</ul>
</FAQItem>
<FAQItem title="赞助者们">
<div>
感谢至今以来所有的<a href="https://afdian.net/a/yunyoujun" class="text-purple" target="_blank">赞助者</a>你们的支持是我持续维护和开发新项目的动力
</div>
<div pt-2>
<a href="https://sponsors.yunyoujun.cn" target="_blank">
<img src="https://sponsors.yunyoujun.cn/sponsors.svg">
</a>
</div>
</FAQItem>
</div>
</div>
<BaseFooter mt-4 />
</div>
</template>

24
app/pages/index.vue Normal file
View File

@@ -0,0 +1,24 @@
<script lang="ts" setup>
const rStore = useRecipeStore()
</script>
<template>
<div>
<div text-4xl>
<button
class="cursor-pointer transition active:text-green-800 hover:(text-green-600)"
title="重置"
@click="rStore.reset"
>
<div v-if="rStore.selectedStuff.length" i-mdi-pot-steam-outline />
<div v-else i-mdi-pot-mix-outline />
</button>
</div>
<p text="sm" m="b-4">
好的今天我们来做菜
</p>
<ChooseFood />
<SimpleCopyright />
</div>
</template>

View File

@@ -0,0 +1,12 @@
<script lang="ts" setup>
definePageMeta({
layout: 'child',
title: '我的收藏',
})
</script>
<template>
<div>
施工中...
</div>
</template>

View File

@@ -0,0 +1,38 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import { recipeHistories } from '~/composables/store/history'
definePageMeta({
layout: 'child',
title: '历史记录',
})
// todo
// clear one history
function clearAllHistory() {
recipeHistories.value = []
}
</script>
<template>
<div pt-2>
<button
text="blue-900 dark:blue-200"
bg="blue-300 op-20 hover:(blue-800 op-20) dark:hover:(blue-200 op-20)"
class="inline-flex items-center justify-center border border-transparent rounded-md px-4 py-2 text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
@click="clearAllHistory"
>
<span i-ri-eraser-line />
<span class="ml-1">清空记录</span>
</button>
<div flex="~ col">
<div v-for="history in recipeHistories" :key="history.recipe.name" mt-2>
<DateTag>
{{ dayjs(history.time).format('YYYY-MM-DD HH:mm:ss') }}
</DateTag>
<DishTag :dish="history.recipe" />
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,12 @@
<script lang="ts" setup>
definePageMeta({
layout: 'child',
title: '菜谱 - ?',
})
</script>
<template>
<div>
asd
</div>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<div>
新建 Recipe
</div>
</template>

32
app/pages/settings.vue Normal file
View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
const app = useAppStore()
definePageMeta({
layout: 'child',
})
</script>
<template>
<div>
<CommonHeader>
设置
</CommonHeader>
<div
class="mx-auto max-w-md w-full"
px-2
text-left
>
<YlfForm>
<YlfFormItem label="暗色模式">
<YlfSwitch v-model="app.settings.keepLocalData" />
</YlfFormItem>
<YlfFormItem label="离开网页后保留选中数据">
<YlfSwitch v-model="app.settings.keepLocalData" />
</YlfFormItem>
<YlfFormItem label="更多设置,敬请期待" />
</YlfForm>
</div>
</div>
</template>

47
app/pages/user.vue Normal file
View File

@@ -0,0 +1,47 @@
<script lang="ts" setup>
import { links } from '~/constants'
</script>
<template>
<div>
<CommonHeader>
我的
</CommonHeader>
<div
class="mx-auto max-w-md w-full"
px-2
text-left
>
<div mt-2 gap="3" grid="~ cols-3">
<YlfIconItem to="/recipes/history" icon="i-ri-history-line" label="历史记录" />
<YlfIconItem to="/recipes/collect" icon="i-ri-star-line" label="我的收藏" />
<YlfIconItem to="/cookbooks" icon="i-ri-article-line" label="自定义菜谱" />
</div>
<YlfForm>
<YlfFormItem icon="i-ri-feedback-line" label="立即反馈" :to="links.feedback" target="_blank" />
<YlfFormItem icon="i-ri-mail-send-line" label="立即投稿" :to="links.contribute" target="_blank" />
</YlfForm>
<YlfForm>
<YlfFormItem icon="i-ri-file-history-line" label="更新日志" :to="links.changelog" target="_blank" />
</YlfForm>
<YlfForm>
<YlfFormItem icon="i-ri-settings-line" label="设置" to="/settings" />
</YlfForm>
<!-- <YlfForm>
<YlfFormItem icon="i-ri-article-line" label="自定义菜谱 TODO" to="/cookbooks/" />
</YlfForm> -->
<YlfForm>
<YlfFormItem icon="i-ri-question-line" label="帮助" to="/help" />
<YlfFormItem icon="i-ri-information-line" label="关于" to="/help" />
</YlfForm>
</div>
<BaseFooter mt-4 />
</div>
</template>

30
app/styles/css-vars.scss Normal file
View File

@@ -0,0 +1,30 @@
:root {
--c-primary: #5fc178;
--c-text: #333;
--c-bg: white;
--c-bg-alt: #f9fbfd;
}
.dark {
--c-text: #fafafa;
--c-bg: #121212;
--c-bg-alt: #333;
}
// ylf
:root {
--ylf-c-bg-alt: var(--c-bg-alt);
--ylf-c-border: #eaeaea;
}
.dark {
--ylf-c-border: #666;
}
// cook custom
:root {
--cook-bottom-menu-padding-bottom: 20px;
--cook-bottom-menu-height: calc(64px + var(--cook-bottom-menu-padding-bottom));
}

View File

@@ -8,10 +8,8 @@ body,
height: 100%; height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
}
html.dark { background: var(--c-bg);
background: #121212;
} }
input:focus { input:focus {
@@ -23,7 +21,7 @@ input:focus {
} }
#nprogress .bar { #nprogress .bar {
background: rgb(13,148,136); background: rgb(13, 148, 136);
opacity: 0.75; opacity: 0.75;
position: fixed; position: fixed;
z-index: 1031; z-index: 1031;
@@ -38,7 +36,7 @@ html {
background-color: var(--c-bg); background-color: var(--c-bg);
scroll-behavior: smooth; scroll-behavior: smooth;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
@@ -49,18 +47,20 @@ a {
button { button {
outline: none; outline: none;
&:focus, &:focus,
&:active { &:active {
outline: none; outline: none;
} }
} }
hr {opacity: 0.1;} hr {
opacity: 0.1;
}
.tag { .tag,
.tagSize {
margin: 4px; margin: 4px;
padding: 2px 4px; padding: 2px 4px;
// border: 1px solid var(--c-text); // border: 1px solid var(--c-text);
} }

View File

@@ -10,7 +10,7 @@ blockquote {
border-left: 0.25em solid #ddd; border-left: 0.25em solid #ddd;
padding: 0 1em; padding: 0 1em;
color: #777; color: #777;
quotes: '\\201C''\\201D''\\2018''\\2019'; quotes: '\\201C' '\\201D' '\\2018' '\\2019';
} }
ol { ol {

22
app/types/cookbook.ts Normal file
View File

@@ -0,0 +1,22 @@
import type { Recipes } from './recipe'
export interface Cookbook {
/**
* 菜谱 ID自定义唯一标识符
*/
id: string
cover?: string
/**
* 菜谱名称
*/
title: string
description: string
author: string | string[]
/**
* 菜谱
*/
recipes: Recipes
createdAt: string
updatedAt: string
}

2
app/types/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './cookbook'
export * from './recipe'

View File

@@ -40,3 +40,30 @@ export interface RecipeItem {
} }
export type Recipes = RecipeItem[] export type Recipes = RecipeItem[]
export interface StuffItem {
/**
*
*/
name: string
/**
* 🥔
*/
emoji: string
/**
*
*/
image?: string
/**
* 西/
*/
alias?: string
/**
*
*/
icon?: string
/**
*
*/
label?: string
}

11
app/utils/cookbook.ts Normal file
View File

@@ -0,0 +1,11 @@
import type { Cookbook } from '~/types'
export const defaultCookbook: Cookbook = {
id: 'default',
title: '默认菜谱',
description: '记录了一些特殊时期常用的菜谱',
author: [''],
recipes: [],
updatedAt: '',
createdAt: '2021-04-04',
}

View File

@@ -1,8 +1,8 @@
import type { Table } from 'dexie' import type { Table } from 'dexie'
import Dexie from 'dexie'
import type { RecipeItem } from '~/types' import type { RecipeItem } from '~/types'
import Dexie from 'dexie'
export interface DbRecipeItem extends RecipeItem { export interface DbRecipeItem extends RecipeItem {
id?: number id?: number
} }

View File

@@ -1,5 +1,7 @@
import { meat, staple, vegetable } from '~/data/food' import { meat, staple, vegetable } from '~/data/food'
export * from './cookbook'
const foodItems = [...vegetable, ...meat, ...staple] const foodItems = [...vegetable, ...meat, ...staple]
const foodEmojiMap = new Map() const foodEmojiMap = new Map()
foodItems.forEach((item) => { foodItems.forEach((item) => {
@@ -9,7 +11,6 @@ foodItems.forEach((item) => {
/** /**
* get emojis from stuff name array * get emojis from stuff name array
* @param stuff * @param stuff
* @returns
*/ */
export function getEmojisFromStuff(stuff: string[]) { export function getEmojisFromStuff(stuff: string[]) {
const emojis: string[] = stuff.map(name => foodEmojiMap.get(name)).filter(item => !!item) const emojis: string[] = stuff.map(name => foodEmojiMap.get(name)).filter(item => !!item)

View File

@@ -3,7 +3,6 @@ import { isClient } from '@vueuse/core'
/** /**
* - https://web.dev/customize-install/#detect-install * - https://web.dev/customize-install/#detect-install
* - [Trigger installation from your PWA](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/How_to/Trigger_install_prompt) * - [Trigger installation from your PWA](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/How_to/Trigger_install_prompt)
* @returns
*/ */
export function installPrompt() { export function installPrompt() {
if (!isClient) if (!isClient)

View File

@@ -1,7 +1,5 @@
/** /**
* *
* @param recipes
* @returns
*/ */
export function generateRandomArray(length: number, total = 1) { export function generateRandomArray(length: number, total = 1) {
const randomArr: number[] = [] const randomArr: number[] = []

Some files were not shown because too many files have changed in this diff Show More