Merge branch 'YunYouJun:main' into main

This commit is contained in:
Henry
2022-04-26 19:25:23 -07:00
committed by GitHub
23 changed files with 1027 additions and 772 deletions

View File

@@ -13,6 +13,12 @@
本项目初衷是方便特殊时期隔离在家而材料有限的小伙伴,因此菜谱材料会尽量限制在特定范围内。
更多可参见 [来做菜 | 关于](https://cook.yunyoujun.cn/about)。
### Features
本项目支持 PWA使用浏览器打开时可将其添加到主屏幕以获得近原生 APP 的体验。
## 开发
```bash

View File

@@ -7,7 +7,6 @@
<link rel="apple-touch-icon" href="/pwa-192x192.png">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#00aba9">
<meta name="msapplication-TileColor" content="#00aba9">
<meta name="theme-color" content="#ffffff">
<meta name="description" content="好的,今天我们来做菜!">
<title>隔离食用手册</title>
<script>

View File

@@ -1,6 +1,9 @@
{
"private": true,
"packageManager": "pnpm@6.32.3",
"engines": {
"node": ">=14"
},
"scripts": {
"build": "npm run convert && vite-ssg build",
"convert": "esno scripts/convert.ts",
@@ -19,12 +22,12 @@
"pinia": "^2.0.13",
"prism-theme-vars": "^0.2.2",
"vue": "^3.2.33",
"vue-about-me": "^1.2.6",
"vue-about-me": "^1.2.7",
"vue-demi": "^0.12.5",
"vue-router": "^4.0.14"
},
"devDependencies": {
"@antfu/eslint-config": "^0.20.6",
"@antfu/eslint-config": "^0.21.1",
"@iconify-json/fe": "^1.1.1",
"@iconify-json/gg": "^1.1.1",
"@iconify-json/ic": "^1.1.2",
@@ -36,26 +39,27 @@
"consola": "^2.15.3",
"critters": "^0.0.16",
"cross-env": "^7.0.3",
"eslint": "^8.13.0",
"eslint": "^8.14.0",
"esno": "^0.14.1",
"https-localhost": "^4.7.1",
"markdown-it-link-attributes": "^4.0.0",
"markdown-it-prism": "^2.2.4",
"pnpm": "^6.32.9",
"sass": "^1.50.1",
"pnpm": "^6.32.10",
"sass": "^1.51.0",
"star-markdown-css": "^0.3.3",
"typescript": "^4.6.3",
"unocss": "^0.31.6",
"unocss": "^0.31.17",
"unplugin-auto-import": "^0.7.1",
"unplugin-vue-components": "^0.19.3",
"vite": "^2.9.5",
"vite": "^2.9.6",
"vite-plugin-inspect": "^0.5.0",
"vite-plugin-md": "^0.12.4",
"vite-plugin-md": "^0.13.0",
"vite-plugin-pages": "^0.23.0",
"vite-plugin-pwa": "^0.12.0",
"vite-plugin-vue-layouts": "^0.6.0",
"vite-ssg": "^0.19.2",
"vite-ssg-sitemap": "^0.2.4",
"vue-tsc": "^0.34.7"
"vue-toastification": "^2.0.0-rc.5",
"vue-tsc": "^0.34.10"
}
}

1318
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,19 @@
<script setup lang="ts">
import { isDark } from '~/composables'
// https://github.com/vueuse/head
// you can use this to manipulate the document head in any components,
// they will be rendered correctly in the html results with vite-ssg
useHead({
title: '隔离食用手册',
meta: [
{ name: 'description', content: '好的,今天我们来做菜!' },
{
name: 'description',
content: '好的,今天我们来做菜!',
},
{
name: 'theme-color',
content: computed(() => isDark.value ? '#121212' : '#fff'),
},
],
})
</script>

1
src/components.d.ts vendored
View File

@@ -17,6 +17,7 @@ declare module '@vue/runtime-core' {
RouterView: typeof import('vue-router')['RouterView']
StapleTag: typeof import('./components/tags/StapleTag.vue')['default']
Switch: typeof import('./components/Switch.vue')['default']
ToggleMode: typeof import('./components/ToggleMode.vue')['default']
ToolTag: typeof import('./components/tags/ToolTag.vue')['default']
VegetableTag: typeof import('./components/tags/VegetableTag.vue')['default']
}

View File

@@ -11,8 +11,47 @@ const copyright = {
author: '云游君',
authorUrl: 'https://www.yunyoujun.cn',
}
const links = [
{
type: 'github',
label: 'GitHub: YunYouJun',
href: 'https://github.com/YunYouJun',
},
{
type: 'telegram',
label: 'Telegram Channel',
href: 'https://t.me/elpsycn',
},
{
type: 'weibo',
label: '微博:机智的云游君',
href: 'http://weibo.com/jizhideyunyoujun',
},
{
type: 'twitter',
label: 'Twitter: YunYouJun',
href: 'https://twitter.com/YunYouJun',
},
{
type: 'wechat',
label: '微信公众号:云游君',
href: '/wechat',
target: '_self',
},
{
type: 'bilibili',
label: '云游君Official',
href: 'https://space.bilibili.com/1579790',
},
{
type: 'blog',
label: '博客yunyoujun.cn',
href: 'http://www.yunyoujun.cn',
},
]
</script>
<template>
<vue-about-me :is-dark="isDark" :copyright="copyright" />
<vue-about-me :is-dark="isDark" :copyright="copyright" :links="links" />
</template>

View File

@@ -1,8 +1,6 @@
<script lang="ts" setup>
import { useGtm } from '@gtm-support/vue-gtm'
import { isClient } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import Switch from './Switch.vue'
import type { StuffItem } from '~/data/food'
import { meat, staple, tools, vegetable } from '~/data/food'
import recipeData from '~/data/recipe.json'
@@ -10,22 +8,25 @@ import type { Recipe } from '~/types'
import { useRecipeStore } from '~/stores/recipe'
import { useInvisibleElement } from '~/composables/helper'
import { useEmojiAnimation } from '~/composables/animation'
const recipe = ref<Recipe>(recipeData as Recipe)
const rStore = useRecipeStore()
const { strict, curTool } = storeToRefs(rStore)
const { curMode, curTool } = storeToRefs(rStore)
const curStuff = computed(() => rStore.selectedStuff)
// 默认严格模式
const displayedRecipe = computed(() => {
const recipes = recipe.value.filter((item) => {
if (strict.value) {
if (curMode.value === 'strict') {
return recipe.value.filter((item) => {
const stuffFlag = curStuff.value.every(stuff => item.stuff.includes(stuff))
const toolFlag = item.tools?.includes(curTool.value)
return curTool.value ? stuffFlag && toolFlag : stuffFlag
})
}
else {
else if (curMode.value === 'loose') {
return recipe.value.filter((item) => {
const stuffFlag = curStuff.value.some(stuff => item.stuff.includes(stuff))
const toolFlag = item.tools?.includes(curTool.value)
@@ -41,47 +42,20 @@ const displayedRecipe = computed(() => {
return false
}
}
})
return recipes
}
// survival
else {
return recipe.value.filter((item) => {
const stuffFlag = item.stuff.every(stuff => curStuff.value.includes(stuff))
const toolFlag = item.tools?.includes(curTool.value)
return curTool.value ? stuffFlag && toolFlag : stuffFlag
})
}
})
const { x, y } = usePointer()
const recipeBtn = ref<HTMLButtonElement>()
const { top, left } = useElementBounding(recipeBtn)
const playAnimation = (emoji: string) => {
if (!isClient)
return
// 单个 Vue 组件实现不适合创建多个元素和清除动画
const emojiEl = document.createElement('span')
emojiEl.style.position = 'fixed'
emojiEl.style.left = `${x.value}px`
emojiEl.style.top = `${y.value}px`
emojiEl.style.zIndex = '10'
emojiEl.style.transition = 'left .4s linear, top .4s cubic-bezier(0.5, -0.5, 1, 1)'
emojiEl.textContent = emoji
document.body.appendChild(emojiEl)
setTimeout(() => {
// 以防万一,按钮位置没检测出来,就不播放动画了
if (!top.value || !left.value) {
emojiEl.style.top = `${x.value}px`
emojiEl.style.left = `${y.value}px`
}
else {
emojiEl.style.top = `${top.value}px`
emojiEl.style.left = `${left.value + 12}px`
}
}, 1)
emojiEl.ontransitionend = () => {
emojiEl.remove()
}
}
const { playAnimation } = useEmojiAnimation(recipeBtn)
const gtm = useGtm()
@@ -224,11 +198,18 @@ const { isVisible, show } = useInvisibleElement(recipePanel)
</ToolTag>
</div>
<div ref="recipePanel" m="2 t-4" p="2" class="transition shadow hover:shadow-md" bg="gray-400/8">
<div ref="recipePanel" m="2 t-4" p="2" class="relative transition shadow hover:shadow-md" bg="gray-400/8">
<h2 text="xl" font="bold" p="1">
🍲 来看看组合出的菜谱吧
</h2>
<Switch />
<!-- <div class="absolute left-5 top-5 icon-btn">
<div i-ri-compass-line />
</div> -->
<ToggleMode />
<!-- <Switch /> -->
<div p="2">
<Transition mode="out-in">
<span v-if="!curStuff.length && !curTool" text="sm" p="2">

View File

@@ -12,12 +12,12 @@ import { toggleDark } from '~/composables'
<div i="ri-sun-line dark:ri-moon-line" />
</button>
<RouterLink class="icon-btn mx-2 hover:text-blue-400" to="/about" title="关于">
<div i-ri-information-line />
<RouterLink class="icon-btn mx-2 hover:text-orange-400" to="/help" title="帮助">
<div i-ri-question-line />
</RouterLink>
<RouterLink class="icon-btn mx-2 hover:text-green-400" to="/wechat" title="微信公众号 - 云游君">
<div i-ri-wechat-2-line />
<RouterLink class="icon-btn mx-2 hover:text-blue-400" to="/about" title="关于">
<div i-ri-information-line />
</RouterLink>
<a class="icon-btn mx-2 hover:text-pink-400" rel="noreferrer" href="https://space.bilibili.com/1579790" target="_blank" title="BiliBili">

View File

@@ -1,20 +1,20 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { useRecipeStore } from '~/stores/recipe'
const rStore = useRecipeStore()
const { strict } = storeToRefs(rStore)
defineProps<{
strict: boolean
toggleStrict: (val: boolean) => void
}>()
</script>
<template>
<div class="inline-flex justify-center items-center" m="t-2">
<span :class="!strict && 'text-orange-600'" font="bold" m="x-1" @click="strict = false">
<span :class="!strict && 'text-orange-600'" font="bold" m="x-1" @click="toggleStrict(false)">
模糊匹配
</span>
<label m="x-1" class="switch">
<input v-model="strict" type="checkbox">
<input :modelValue="strict" type="checkbox" @update:modelValue="toggleStrict">
<span class="inline-flex justify-center items-center slider round" />
</label>
<span :class="strict && 'text-green-600'" font="bold" m="x-1" @click="strict = true">
<span :class="strict && 'text-green-600'" font="bold" m="x-1" @click="toggleStrict(true)">
精准匹配
</span>
</div>

View File

@@ -0,0 +1,34 @@
<script lang="ts" setup>
import type { SearchMode } from '~/stores/recipe'
import { useRecipeStore } from '~/stores/recipe'
const rStore = useRecipeStore()
const searchModes: {
id: SearchMode
name: string
}[] = [{
id: 'loose',
name: '模糊匹配',
}, {
id: 'strict',
name: '严格匹配',
}, {
id: 'survival',
name: '生存模式',
}]
</script>
<template>
<div>
<button
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'"
:text="mode.id === rStore.curMode ? 'orange-100' : 'orange-800 dark:orange-200'"
@click="rStore.setMode(mode.id)"
>
{{ mode.name }}
</button>
</div>
</template>

View File

@@ -0,0 +1,42 @@
import { isClient } from '@vueuse/core'
import type { Ref } from 'vue'
export function useEmojiAnimation(recipeBtn: Ref<HTMLButtonElement | undefined>) {
const { x, y } = usePointer()
const { top, left } = useElementBounding(recipeBtn)
const playAnimation = (emoji: string) => {
if (!isClient)
return
// 单个 Vue 组件实现不适合创建多个元素和清除动画
const emojiEl = document.createElement('span')
emojiEl.style.position = 'fixed'
emojiEl.style.left = `${x.value}px`
emojiEl.style.top = `${y.value}px`
emojiEl.style.zIndex = '10'
emojiEl.style.transition = 'left .4s linear, top .4s cubic-bezier(0.5, -0.5, 1, 1)'
emojiEl.textContent = emoji
document.body.appendChild(emojiEl)
setTimeout(() => {
// 以防万一,按钮位置没检测出来,就不播放动画了
if (!top.value || !left.value) {
emojiEl.style.top = `${x.value}px`
emojiEl.style.left = `${y.value}px`
}
else {
emojiEl.style.top = `${top.value}px`
emojiEl.style.left = `${left.value + 12}px`
}
}, 1)
emojiEl.ontransitionend = () => {
emojiEl.remove()
}
}
return {
playAnimation,
}
}

View File

@@ -179,7 +179,7 @@ export const tools: StuffItem[] = [
icon: 'i-gg-smart-home-cooker',
},
{
label: '一口啥都能煮的大锅',
label: '一口能炒又能煮的大锅',
name: '一口大锅',
emoji: '',
icon: 'i-mdi-pot-steam-outline',

View File

@@ -4,7 +4,6 @@ import { setupLayouts } from 'virtual:generated-layouts'
import App from './App.vue'
import '@unocss/reset/tailwind.css'
import './styles/main.css'
import './styles/css-vars.scss'
import './styles/index.scss'
import 'uno.css'

View File

@@ -5,7 +5,7 @@ export const install: UserModule = ({ isClient, router }) => {
if (!isClient)
return
router.isReady().then(async() => {
router.isReady().then(async () => {
const { registerSW } = await import('virtual:pwa-register')
registerSW({ immediate: true })
})

9
src/modules/toast.ts Normal file
View File

@@ -0,0 +1,9 @@
import Toast from 'vue-toastification'
import type { UserModule } from '~/types'
import 'vue-toastification/dist/index.css'
export const install: UserModule = ({ app }) => {
// add google tag manager, and add GA4 in gtag
app.use(Toast)
}

View File

@@ -11,7 +11,7 @@ title: 关于
> 希望大家吃的开心!
<div class="inline-flex justify-center items-center">
代码请见<a class="inline-flex items-center justify-center" href="https://github.com/YunYouJun/cook" target="_blank">
代码仓库<a class="inline-flex items-center justify-center" href="https://github.com/YunYouJun/cook" target="_blank">
<div m="r-1" inline-flex i-ri-github-line />YunYouJun/cook</a>
</div>
@@ -25,9 +25,6 @@ title: 关于
</a>
</div>
- 如果您发现了任何「故障」或希望有某个「新功能」,可前往 [Issues](https://github.com/YunYouJun/cook/issues)。
- 如果您有任何想要交流的内容,包括但不局限于建议/反馈/分享等,可前往 [Discussions](https://github.com/YunYouJun/cook/issues)。
## **致谢**
感谢以下小伙伴为本项目提供的数据支持和 QA
@@ -39,8 +36,31 @@ title: 关于
- 晴方啾
- 课代表阿伟
## **友情提示**
## **关于我**
点击首页最上方的大锅图标,可以清空所选食材和工具。(本来想当作彩蛋,但是感觉还挺实用的。)
Hello我是云游君。
很高兴能在这里与你相遇,也很希望这个网站可以真的帮助到你。
同时,我也以我或许不值一提的脸面保证它会以免费开源的形式维护运营下去。
此外,我也会继续尝试做一些有趣或有用的东西,并分享给大家。
你也可以在这些地方找到我。
<AboutMe />
对了,给微信公众号「云游君」发送「做菜」也可以快速找到这个网址。
## [**赞助者**](https://sponsors.yunyoujun.cn)
也非常感谢至今以来的所有赞助者们!
如果觉得我的[小项目们](https://sponsors.yunyoujun.cn/projects)还算有趣的话,要不要考虑[赞助](https://sponsors.yunyoujun.cn/)我?
我会将其公开在[账簿](https://sponsors.yunyoujun.cn/account)中并投入在周边的服务器、域名、CDN 等费用上。
<p align="center">
<a href="https://sponsors.yunyoujun.cn">
<img src='https://cdn.jsdelivr.net/gh/YunYouJun/sponsors/public/sponsors.svg'/>
</a>
</p>

23
src/pages/help.md Normal file
View File

@@ -0,0 +1,23 @@
---
title: 帮助
---
<h3 text="center" font="serif black">
使用帮助
</h3>
- 故障/新功能反馈:[Issues](https://github.com/YunYouJun/cook/issues)
- 交流/建议/分享:[Discussions](https://github.com/YunYouJun/cook/issues)
## **模式说明**
- 模糊匹配:展示所有含当前选中任意食材的菜谱
- 精准匹配:展示所有含当前选中所有食材的菜谱
- 生存模式:展示当前选中食材可制作的所有菜谱
## **友情提示**
- 点击首页最上方的大锅图标,可清空所选食材和工具。
- 本项目支持 PWA使用浏览器打开时可将其添加到主屏幕以获得近原生 APP 的体验。
<br />

View File

@@ -2,9 +2,14 @@ import { acceptHMRUpdate, defineStore } from 'pinia'
const namespace = 'cook'
export const useRecipeStore = defineStore('recipe', () => {
const strict = useStorage(`${namespace}:strict`, false)
/**
* survival: 生存模式
* strict: 严格
* loose: 模糊
*/
export type SearchMode = 'survival' | 'loose' | 'strict'
export const useRecipeStore = defineStore('recipe', () => {
const curStuff = useStorage(`${namespace}:stuff`, new Set<string>())
// const curTools = ref(new Set<string>())
const curTool = useStorage(`${namespace}:tool`, '')
@@ -13,6 +18,8 @@ export const useRecipeStore = defineStore('recipe', () => {
// const selectedTools = computed(() => Array.from(curTools.value))
// const selectedTools = ref('')
const curMode = useStorage<SearchMode>(`${namespace}:mode`, 'loose')
function toggleStuff(name: string) {
if (!curStuff)
return
@@ -33,6 +40,10 @@ export const useRecipeStore = defineStore('recipe', () => {
// curTools.value.add(name)
}
function setMode(mode: SearchMode) {
curMode.value = mode
}
/**
* 重置
*/
@@ -43,12 +54,14 @@ export const useRecipeStore = defineStore('recipe', () => {
}
return {
strict,
curTool,
curMode,
selectedStuff,
toggleStuff,
toggleTools,
reset,
setMode,
}
})

38
src/styles/animation.scss Normal file
View File

@@ -0,0 +1,38 @@
/* we will explain what these classes do next! */
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
// scrollbar
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
border-radius: 2px;
background-color: rgba(255, 255, 255, 0.1);
}
::-webkit-scrollbar-thumb {
border-radius: 2px;
background-color: rgba(122, 122, 122, 0.3);
&:window-inactive {
background-color: rgba(122, 122, 122, 0.3);
}
&:hover {
background-color: rgba(122, 122, 122, 0.7);
}
&:active {
background-color: rgba(122, 122, 122, 0.9);
}
}

View File

@@ -1,5 +1,33 @@
@import './animation.scss';
@import './markdown.scss';
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
}
html.dark {
background: #121212;
}
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: rgb(13,148,136);
opacity: 0.75;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
html {
color: var(--c-text);
background-color: var(--c-bg);
@@ -23,47 +51,11 @@ button {
}
}
hr {opacity: 0.1;}
.tag {
margin: 4px;
padding: 2px 4px;
// border: 1px solid var(--c-text);
}
/* we will explain what these classes do next! */
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
// scrollbar
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
border-radius: 2px;
background-color: rgba(255, 255, 255, 0.1);
}
::-webkit-scrollbar-thumb {
border-radius: 2px;
background-color: rgba(122, 122, 122, 0.3);
&:window-inactive {
background-color: rgba(122, 122, 122, 0.3);
}
&:hover {
background-color: rgba(122, 122, 122, 0.7);
}
&:active {
background-color: rgba(122, 122, 122, 0.9);
}
}

View File

@@ -1,26 +0,0 @@
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
}
html.dark {
background: #121212;
}
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: rgb(13,148,136);
opacity: 0.75;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}

7
vercel.json Normal file
View File

@@ -0,0 +1,7 @@
{
"rewrites": [
{ "source": "/about", "destination": "/index.html" },
{ "source": "/help", "destination": "/index.html" },
{ "source": "/wechat", "destination": "/index.html" }
]
}