refactor: migrate to nuxt
This commit is contained in:
59
components/AboutMe.vue
Normal file
59
components/AboutMe.vue
Normal 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
85
components/BaseFooter.vue
Normal 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">
|
||||
©️ <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
180
components/ChooseFood.vue
Normal 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
19
components/Counter.vue
Normal 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
21
components/DarkToggle.vue
Normal 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
28
components/InstallPwa.vue
Normal 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
25
components/Menu.vue
Normal 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
7
components/README.md
Normal 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/).
|
||||
12
components/RandomRecipe.vue
Normal file
12
components/RandomRecipe.vue
Normal 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>
|
||||
64
components/ReloadPrompt.vue
Normal file
64
components/ReloadPrompt.vue
Normal 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>
|
||||
32
components/SearchFoodInput.vue
Normal file
32
components/SearchFoodInput.vue
Normal 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
82
components/Switch.vue
Normal 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
32
components/ToggleMode.vue
Normal 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
16
components/WrapperMd.vue
Normal 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>
|
||||
45
components/tags/DishTag.vue
Normal file
45
components/tags/DishTag.vue
Normal 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>
|
||||
16
components/tags/MeatTag.vue
Normal file
16
components/tags/MeatTag.vue
Normal 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>
|
||||
15
components/tags/StapleTag.vue
Normal file
15
components/tags/StapleTag.vue
Normal 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>
|
||||
16
components/tags/ToolTag.vue
Normal file
16
components/tags/ToolTag.vue
Normal 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>
|
||||
16
components/tags/VegetableTag.vue
Normal file
16
components/tags/VegetableTag.vue
Normal 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>
|
||||
Reference in New Issue
Block a user