refactor: migrate to nuxt

This commit is contained in:
YunYouJun
2023-07-30 03:08:42 +08:00
parent c23f39e8c0
commit 0dfec1831b
90 changed files with 7864 additions and 3962 deletions

59
components/AboutMe.vue Normal file
View File

@@ -0,0 +1,59 @@
<script lang="ts" setup>
import VueAboutMe from 'vue-about-me'
import 'vue-about-me/style.css'
const color = useColorMode()
const isDark = computed(() => color.value === 'dark')
const copyright = {
name: 'Cook',
repo: 'YunYouJun/cook',
color: '#0078E7',
iconUrl: 'https://sponsors.yunyoujun.cn',
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>
<VueAboutMe :is-dark="isDark" :copyright="copyright" :links="links" />
</template>

85
components/BaseFooter.vue Normal file
View File

@@ -0,0 +1,85 @@
<script lang="ts" setup>
import { isClient } from '@vueuse/core'
import { links } from '~/constants'
const displayICP = ref(true)
onBeforeMount(() => {
if (isClient)
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(parseInt(now) * 1000)).toLocaleDateString()
</script>
<template>
<div p="4 t-2" class="flex flex-col justify-center items-center" text="sm">
<div>
<a
m="2"
border="b-1 dashed"
class="inline-flex text-sm text-blue-600 dark:text-blue-400"
:href="links.contribute" target="_blank"
title="居家菜谱投稿"
>
立即投稿
</a>
<a
m="2"
class="inline-flex text-sm text-blue-600 dark:text-blue-400"
:href="links.feedback" target="_blank"
alt="通过兔小巢反馈"
>
立即反馈
</a>
</div>
<div v-if="commitSha && buildDate" mb-2>
<span>
当前版本{{ 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">
苏ICP备17038157号
</a>
<div m="t-2" class="inline-flex justify-center items-center" text="xs">
<a class="inline-flex justify-center items-center" style="color: #ea7b99" href="https://www.bilibili.com/blackboard/dynamic/306882" target="_blank">
<span inline-flex>菜谱视频来源</span>
<div class="inline-flex" i-ri-bilibili-line />
<span m="l-1" class="inline-flex" style="margin-top: 1px;">B </span>
</a>
</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 justify-center items-center">
©&nbsp;<a href="https://github.com/YunYouJun/cook" target="_blank">Cook</a>
<div text="xs" m="x-1" i-ri-cloud-line />
<a href="https://www.yunyoujun.cn" target="_blank">云游君</a>
</div>
<div m="t-2" opacity="80">
<a href="https://yunle.fun" target="_blank" title="云乐坊">
云乐坊工作室
</a>
</div>
<!-- 欢迎赞助 -->
<!-- <div m="t-2" opacity="80" class="footer-support flex justify-center items-center">
<span>本网站由</span><a class="footer-support-logo" href="https://www.upyun.com" target="blank" title="又拍云">
<img m="x-1" width="50" src="https://cdn.yunyoujun.cn/img/logo/upyun-logo.png" alt="又拍云">
</a><span>提供 CDN 加速</span>
</div> -->
</div>
</template>
constants

180
components/ChooseFood.vue Normal file
View File

@@ -0,0 +1,180 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import type { StuffItem } from '~/data/food'
import { meat, staple, tools, vegetable } from '~/data/food'
import { useInvisibleElement } from '~/composables/helper'
import { useEmojiAnimation } from '~/composables/animation'
import { useRecipe } from '~/composables/recipe'
const rStore = useRecipeStore()
const { curTool } = storeToRefs(rStore)
const curStuff = computed(() => rStore.selectedStuff)
const { displayedRecipe, clickTool } = useRecipe(rStore.recipes)
const recipeBtn = ref<HTMLButtonElement>()
const { playAnimation } = useEmojiAnimation(recipeBtn)
const gtm = useGtm()
function toggleStuff(item: StuffItem, category = '', _e?: Event) {
rStore.toggleStuff(item.name)
if (curStuff.value.includes(item.name))
playAnimation(item.emoji)
gtm?.trackEvent({
event: 'click',
category: `${category}_${item.name}`,
action: 'click_stuff',
label: '食材',
})
gtm?.trackEvent({
event: 'click_stuff',
action: item.name,
})
}
const recipePanel = ref()
const { isVisible, show } = useInvisibleElement(recipePanel)
</script>
<template>
<Transition>
<button
v-show="displayedRecipe.length !== rStore.recipes.length && isVisible"
ref="recipeBtn"
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" bottom="4" right="4"
text="green-600 dark:green-300"
@click="show"
>
<span v-if="displayedRecipe.length">
<div i-mdi-bowl-mix-outline />
</span>
<span v-else>
<div i-mdi-bowl-outline />
</span>
</button>
</Transition>
<h2 m="t-4" text="xl" font="bold" p="1">
🥘 先选一下食材
</h2>
<div>
<h2 opacity="90" text="base" font="bold" p="1">
🥬 菜菜们
</h2>
<VegetableTag
v-for="item, i in vegetable" :key="i"
:active="curStuff.includes(item.name)"
@click="toggleStuff(item, 'vegetable')"
>
<span v-if="item.emoji" class="inline-flex">{{ item.emoji }}</span>
<span v-else-if="item.image" class="inline-flex">
<img class="inline-flex" w="2" h="2" width="10" height="10" :src="item.image" :alt="item.name">
</span>
<span class="inline-flex" m="l-1">
{{
item.name
}}
</span>
</VegetableTag>
</div>
<div m="y-4">
<h2 opacity="90" text="base" font="bold" p="1">
🥩 肉肉们
</h2>
<MeatTag
v-for="item, i in meat" :key="i"
:active="curStuff.includes(item.name)"
@click="toggleStuff(item, 'meat')"
>
<span>{{ item.emoji }}</span>
<span m="l-1">
{{
item.name
}}
</span>
</MeatTag>
</div>
<div m="y-4">
<h2 opacity="90" text="base" font="bold" p="1">
🍚 主食也要一起下锅吗不选也行
</h2>
<StapleTag
v-for="item, i in staple" :key="i"
:active="curStuff.includes(item.name)"
@click="toggleStuff(item, 'staple')"
>
<span>{{ item.emoji }}</span>
<span m="l-1">
{{
item.name
}}
</span>
</StapleTag>
</div>
<div m="t-4">
<h2 text="xl" font="bold" p="1">
🍳 再选一下厨具
</h2>
<ToolTag
v-for="item, i in tools" :key="i"
:active="curTool === item.name"
@click="clickTool(item)"
>
<span v-if="item.emoji" class="inline-flex">{{ item.emoji }}</span>
<span v-else-if="item.icon" class="inline-flex">
<div :class="item.icon" />
</span>
<span class="inline-flex" m="l-1">
{{
item.label || item.name
}}
</span>
</ToolTag>
</div>
<div ref="recipePanel" m="2 t-4" p="2" class="relative shadow transition hover:shadow-md" bg="gray-400/8">
<h2 text="xl" font="bold" p="1">
🍲 来看看组合出的菜谱吧
</h2>
<ToggleMode />
<!-- <Switch /> -->
<div class="cook-recipes" p="2">
<SearchFoodInput />
<Transition mode="out-in">
<div class="cook-filter-recipes">
<span v-if="!curStuff.length && !curTool" text="sm" p="2">
你要先选食材或工具哦
</span>
<span v-else-if="displayedRecipe.length">
<DishTag v-for="item, i in displayedRecipe" :key="i" :dish="item" />
</span>
<span v-else text="sm">
还没有完美匹配的菜谱呢
<br>
大胆尝试一下或者<a href="#" @click="rStore.reset()">
<strong>换个组合</strong></a>
<br>
<span m="t-1">欢迎来
<a class="font-bold text-blue-600 dark:text-blue-400" href="https://docs.qq.com/sheet/DQk1vdkhFV0twQVNS?tab=uykkic" target="_blank">这里</a>
反馈新的菜谱
</span>
</span>
</div>
</Transition>
<hr m="y-2">
<RandomRecipe />
</div>
</div>
</template>

19
components/Counter.vue Normal file
View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
const props = defineProps<{
initial: number
}>()
const { count, inc, dec } = useCounter(props.initial)
</script>
<template>
<div>
{{ count }}
<button class="inc" @click="inc()">
+
</button>
<button class="dec" @click="dec()">
-
</button>
</div>
</template>

21
components/DarkToggle.vue Normal file
View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
const color = useColorMode()
useHead({
meta: [{
id: 'theme-color',
name: 'theme-color',
content: () => color.value === 'dark' ? '#222222' : '#ffffff',
}],
})
function toggleDark() {
color.preference = color.value === 'dark' ? 'light' : 'dark'
}
</script>
<template>
<button class="mx-2 icon-btn hover:text-yellow-400 !outline-none" title="切换" @click="toggleDark()">
<div i="ri-sun-line dark:ri-moon-line" />
</button>
</template>

28
components/InstallPwa.vue Normal file
View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
const app = useAppStore()
function install() {
const deferredPrompt = app.deferredPrompt
// Show the install prompt
deferredPrompt.prompt()
// Wait for the user to respond to the prompt
deferredPrompt.userChoice.then((choiceResult: any) => {
if (choiceResult.outcome === 'accepted')
// eslint-disable-next-line no-console
console.log('User accepted the install prompt')
else
// eslint-disable-next-line no-console
console.log('User dismissed the install prompt')
})
}
</script>
<template>
<Transition>
<div v-if="app.deferredPrompt" text="center" m="t-2">
<button class="shadow" text="white" bg="green-500" p="x-4 y-0" m="2" @click="install">
安装
</button>
</div>
</Transition>
</template>

25
components/Menu.vue Normal file
View File

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

7
components/README.md Normal file
View File

@@ -0,0 +1,7 @@
## Components
Components in this dir will be auto-registered and on-demand, powered by [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components).
### Icons
You can use icons from almost any icon sets by the power of [Iconify](https://iconify.design/).

View File

@@ -0,0 +1,12 @@
<script lang="ts" setup>
const rStore = useRecipeStore()
</script>
<template>
<div class="inline-flex items-center justify-center">
今天吃什么<div class="transition" hover="text-blue-500" i-ri-refresh-line inline-block cursor-pointer @click="rStore.random" />
</div>
<div m="t-2">
<DishTag :dish="rStore.randomRecipe" />
</div>
</template>

View File

@@ -0,0 +1,64 @@
<script setup lang="ts">
// @ts-expect-error remove pwa
import { useRegisterSW } from 'virtual:pwa-register/vue'
const {
offlineReady,
needRefresh,
updateServiceWorker,
} = useRegisterSW()
async function close() {
offlineReady.value = false
needRefresh.value = false
}
</script>
<template>
<div
v-if="offlineReady || needRefresh"
class="pwa-toast transition shadow-lg hover:shadow-md rounded"
border="~ stone-200 dark:stone-600"
text="center"
p="4"
m="4"
bg="white dark:dark-800"
role="alert"
>
<div class="message" m="b-4">
<span v-if="offlineReady">
可以离线使用啦
</span>
<span v-else>
更新了新的内容
</span>
</div>
<button
v-if="needRefresh"
m="x-2" p="x-4 y-1" text="sm white"
class="rounded shadow transition active:shadow-md"
bg="green-500 active:green-600"
@click="updateServiceWorker()"
>
更新
</button>
<button
m="x-2" p="x-4 y-1" text="sm"
class="shadow rounded transition active:shadow-md"
border="~ stone-200 dark:stone-600"
bg="active:(white opacity-20)"
@click="close"
>
关闭
</button>
</div>
</template>
<style>
.pwa-toast {
position: fixed;
right: 0;
bottom: 0;
z-index: 1;
}
</style>

View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
const rStore = useRecipeStore()
</script>
<template>
<div m="auto b-2" max-w="500px">
<div relative text-xs>
<div
v-if="rStore.keyword" cursor="pointer"
absolute right-2 inline-flex justify="center" items-center h="full" opacity="70"
@click="rStore.clearKeyWord()"
>
<div i-ri-close-line />
</div>
<input
id="input"
v-model="rStore.keyword"
placeholder="关键字过滤"
aria-label="搜索关键字"
type="text"
autocomplete="false"
p="x4 y2"
w="full"
text="center"
bg="transparent"
border="~ rounded gray-200 dark:gray-700"
class="focus:(dark:gray-500)"
>
<label class="hidden" for="input">快速搜索</label>
</div>
</div>
</template>

82
components/Switch.vue Normal file
View File

@@ -0,0 +1,82 @@
<script lang="ts" setup>
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="toggleStrict(false)">
模糊匹配
</span>
<label m="x-1" class="switch">
<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="toggleStrict(true)">
精准匹配
</span>
</div>
</template>
<style lang="scss">
.switch {
position: relative;
display: inline-block;
width: 48px;
height: 28px;
input {
opacity: 0;
width: 0;
height: 0;
}
}
.slider {
@apply bg-orange-600;
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
-webkit-transition: .4s;
transition: .4s;
}
$size: 20px;
.slider:before {
position: absolute;
content: "";
height: $size;
width: $size;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
@apply bg-green-600;
}
input:checked + .slider:before {
-webkit-transform: translateX($size);
-ms-transform: translateX($size);
transform: translateX($size);
}
/* Rounded sliders */
.slider.round {
border-radius: 28px;
&:before {
border-radius: 50%;
}
}
</style>

32
components/ToggleMode.vue Normal file
View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
import type { SearchMode } from '~/composables/store/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="rounded px-2 tag"
: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>

16
components/WrapperMd.vue Normal file
View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
defineProps<{
frontmatter: {
title: string
}
}>()
</script>
<template>
<div m="t-4" class="max-w-900px m-auto text-left">
<h3 text="center 3xl" font="serif !black">
{{ frontmatter?.title }}
</h3>
<slot class="markdown-body" />
</div>
</template>

View File

@@ -0,0 +1,45 @@
<script lang="ts" setup>
import { tools } from '~/data/food'
import type { RecipeItem } from '~/types'
import { getEmojisFromStuff } from '~/utils'
const props = defineProps<{
dish: RecipeItem
}>()
const gtm = useGtm()
function triggerGtm(val: string) {
gtm?.trackEvent({
event: 'click',
category: `dish_${val}`,
action: 'click_recipe',
label: '跳转菜谱',
})
gtm?.trackEvent({
event: 'click_dish',
action: val,
})
}
const dishLabel = computed(() => {
const emojis = getEmojisFromStuff(props.dish.stuff)
return `${props.dish.tags?.includes('杂烩') ? '🍲' : emojis.join(' ')} ${props.dish.name}`
})
</script>
<template>
<a
:href="dish.link || `https://www.bilibili.com/video/${dish.bv}`" target="_blank" class="dish-tag rounded tag" p="x-2"
border="~ blue-200 dark:blue-800"
bg="blue-300 opacity-20"
@click="triggerGtm(dish.name)"
>
<span m="r-1" class="inline-flex items-center justify-center" text="sm blue-700 dark:blue-200">
{{ dishLabel }}
</span>
<span v-for="tool, i in tools" :key="i" inline-flex>
<div v-if="dish.tools?.includes(tool.name)" :class="tool.icon" />
</span>
</a>
</template>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
defineProps<{
active: boolean
}>()
</script>
<template>
<span
class="meat-tag tag rounded" p="x-2"
border="~ red-200 dark:red-800"
:bg="active ? 'red-500 opacity-90' : 'red-300 opacity-20'"
:text="active ? 'red-100' : 'red-800 dark:red-200'"
>
<slot />
</span>
</template>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
defineProps<{
active: boolean
}>()
</script>
<template>
<span
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'"
:text="active ? 'yellow-100' : 'yellow-800 dark:yellow-200'"
>
<slot />
</span>
</template>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
defineProps<{
active: boolean
}>()
</script>
<template>
<span
class="tag rounded" p="x-2"
border="~ stone-200 dark:stone-600"
:bg="active ? 'stone-600 opacity-100' : 'stone-300 opacity-5'"
:text="active ? 'stone-100' : 'stone-800 dark:stone-200'"
>
<slot />
</span>
</template>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
defineProps<{
active: boolean
}>()
</script>
<template>
<span
class="vegetable-tag rounded tag" p="x-2"
border="~ green-200 dark:green-800"
:bg="active ? 'green-600 opacity-90' : 'green-300 opacity-20'"
:text="active ? 'green-100' : 'green-800 dark:green-200'"
>
<slot />
</span>
</template>