refactor: use nuxt compatiable 4 folder
This commit is contained in:
43
app/components/BaseFooter.vue
Normal file
43
app/components/BaseFooter.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script lang="ts" setup>
|
||||
import { isClient } from '@vueuse/core'
|
||||
|
||||
const displayICP = ref(true)
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (isClient)
|
||||
displayICP.value = ['cook.yunyoujun.cn', 'localhost', '127.0.0.1'].includes(window.location.hostname)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div p="4 t-2" class="flex flex-col items-center justify-center" text="sm">
|
||||
<CurrentVersion />
|
||||
<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 items-center justify-center" text="xs">
|
||||
<a class="inline-flex items-center justify-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 items-center justify-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>
|
||||
</template>
|
||||
35
app/components/BasketButton.vue
Normal file
35
app/components/BasketButton.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const props = defineProps({
|
||||
isVisible: Boolean,
|
||||
})
|
||||
|
||||
const rStore = useRecipeStore()
|
||||
const { displayedRecipe } = storeToRefs(rStore)
|
||||
|
||||
/**
|
||||
* Show basket button if there are recipes in the basket
|
||||
*/
|
||||
const showBasketBtn = computed(() => {
|
||||
return displayedRecipe.value.length !== rStore.recipesLength && props.isVisible
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
v-show="showBasketBtn"
|
||||
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="22"
|
||||
right="4"
|
||||
text="green-600 dark:green-300"
|
||||
>
|
||||
<span v-if="displayedRecipe.length > 0">
|
||||
<div i-mdi-bowl-mix-outline />
|
||||
</span>
|
||||
<span v-else>
|
||||
<div i-mdi-bowl-outline />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
117
app/components/ChooseFood.vue
Normal file
117
app/components/ChooseFood.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { StuffItem } from '~/types'
|
||||
import { meat, staple, tools, vegetable } from '~/data/food'
|
||||
|
||||
import { useEmojiAnimation } from '~/composables/animation'
|
||||
|
||||
const rStore = useRecipeStore()
|
||||
const { curTool } = storeToRefs(rStore)
|
||||
const curStuff = computed(() => rStore.selectedStuff)
|
||||
|
||||
const recipeBtnRef = ref<HTMLButtonElement>()
|
||||
const { playAnimation } = useEmojiAnimation(recipeBtnRef)
|
||||
|
||||
const gtm = useGtm()
|
||||
const recipePanelRef = ref()
|
||||
const { isVisible, show } = useInvisibleElement(recipePanelRef)
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 m="t-4" text="xl" font="bold" p="1">
|
||||
🥘 先选一下食材
|
||||
</h2>
|
||||
<div>
|
||||
<h2 opacity="90" text="base" font="bold" p="1">
|
||||
🥬 菜菜们
|
||||
</h2>
|
||||
<div>
|
||||
<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>
|
||||
<div m="y-4">
|
||||
<h2 opacity="90" text="base" font="bold" p="1">
|
||||
🥩 肉肉们
|
||||
</h2>
|
||||
<div>
|
||||
<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>
|
||||
<div m="y-4">
|
||||
<h2 opacity="90" text="base" font="bold" p="1">
|
||||
🍚 主食也要一起下锅吗?(不选也行)
|
||||
</h2>
|
||||
<div>
|
||||
<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>
|
||||
<div m="t-4">
|
||||
<h2 text="xl" font="bold" p="1">
|
||||
🍳 再选一下厨具
|
||||
</h2>
|
||||
<div>
|
||||
<ToolTag
|
||||
v-for="item, i in tools" :key="i"
|
||||
:active="curTool === item.name"
|
||||
@click="rStore.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>
|
||||
|
||||
<Transition>
|
||||
<BasketButton ref="recipeBtnRef" :is-visible="isVisible" @click="show" />
|
||||
</Transition>
|
||||
<RecipePanel ref="recipePanelRef" />
|
||||
</div>
|
||||
</template>
|
||||
5
app/components/CommonHeader.vue
Normal file
5
app/components/CommonHeader.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<h1 text-2xl font="bold" my="4">
|
||||
<slot />
|
||||
</h1>
|
||||
</template>
|
||||
19
app/components/Counter.vue
Normal file
19
app/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>
|
||||
26
app/components/FAQItem.vue
Normal file
26
app/components/FAQItem.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
defaultOpen?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Disclosure v-slot="{ open }" :default-open="defaultOpen" as="div" class="mt-2">
|
||||
<DisclosureButton
|
||||
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>
|
||||
<div
|
||||
i-ri-arrow-drop-up-line
|
||||
:class="open ? 'rotate-180 transform' : ''"
|
||||
class="h-5 w-5 text-blue-500"
|
||||
/>
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel class="px-2 pb-2 pt-4 text-sm">
|
||||
<slot />
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</template>
|
||||
31
app/components/FeedbackActions.vue
Normal file
31
app/components/FeedbackActions.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { links } from '~/constants'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a
|
||||
m="2"
|
||||
class="feedback-button"
|
||||
:href="links.contribute" target="_blank"
|
||||
title="居家菜谱投稿"
|
||||
>
|
||||
立即投稿
|
||||
</a>
|
||||
|
||||
<a
|
||||
m="2"
|
||||
class="feedback-button"
|
||||
:href="links.feedback" target="_blank"
|
||||
alt="通过兔小巢反馈"
|
||||
>
|
||||
立即反馈
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.feedback-button {
|
||||
@apply border-none inline-flex justify-center rounded-md 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 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600;
|
||||
}
|
||||
</style>
|
||||
7
app/components/README.md
Normal file
7
app/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/).
|
||||
31
app/components/RandomRecipe.vue
Normal file
31
app/components/RandomRecipe.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
const { count, inc, dec } = useCount()
|
||||
const { random, randomRecipes } = useRandomRecipe(count)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div inline-flex m="y-3">
|
||||
<button rounded-full p-2 btn @click="dec()">
|
||||
<div i-carbon-subtract />
|
||||
</button>
|
||||
<div font="mono" w="15" m-auto inline-block>
|
||||
{{ count }}
|
||||
</div>
|
||||
<button rounded-full p-2 btn @click="inc()">
|
||||
<div i-carbon-add />
|
||||
</button>
|
||||
</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 m="t-8" flex="~ col">
|
||||
<template v-for="recipe, i in randomRecipes" :key="i">
|
||||
<DishTag v-if="recipe" :dish="recipe" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
82
app/components/RecipePanel.vue
Normal file
82
app/components/RecipePanel.vue
Normal 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>
|
||||
5
app/components/RecipePanelTitle.vue
Normal file
5
app/components/RecipePanelTitle.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div text="xl" font="bold" p="1">
|
||||
🍲 来看看组合出的菜谱吧!
|
||||
</div>
|
||||
</template>
|
||||
38
app/components/SearchFoodInput.vue
Normal file
38
app/components/SearchFoodInput.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts" setup>
|
||||
const rStore = useRecipeStore()
|
||||
|
||||
const searchInput = ref<HTMLInputElement>()
|
||||
onMounted(() => {
|
||||
searchInput.value?.focus()
|
||||
})
|
||||
</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"
|
||||
ref="searchInput"
|
||||
v-model="rStore.keyword"
|
||||
placeholder="关键字过滤"
|
||||
aria-label="搜索关键字"
|
||||
type="text"
|
||||
autocomplete="false"
|
||||
p="x4 y2"
|
||||
w="full"
|
||||
text="center"
|
||||
bg="white dark:dark-800"
|
||||
border="~ rounded gray-200 dark:gray-700"
|
||||
class="focus:dark:gray-500"
|
||||
>
|
||||
<label class="hidden" for="input">快速搜索</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
82
app/components/Switch.vue
Normal file
82
app/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 items-center justify-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="slider round inline-flex items-center justify-center" />
|
||||
</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: 0.4s;
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
$size: 20px;
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: $size;
|
||||
width: $size;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.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>
|
||||
57
app/components/TheBottomMenu.vue
Normal file
57
app/components/TheBottomMenu.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<script lang="ts" setup>
|
||||
import type { BottomMenuItem } from '@yunlefun/vue'
|
||||
|
||||
const items: BottomMenuItem[] = [
|
||||
{
|
||||
icon: 'i-ri-home-line',
|
||||
activeIcon: 'i-ri-home-fill',
|
||||
title: '首页',
|
||||
to: '/',
|
||||
},
|
||||
{
|
||||
icon: 'i-ri-compass-2-line',
|
||||
activeIcon: 'i-ri-compass-2-fill',
|
||||
title: '吃什么',
|
||||
to: '/random',
|
||||
},
|
||||
// {
|
||||
// icon: 'i-ri-compass-2-line',
|
||||
// activeIcon: 'i-ri-compass-2-fill',
|
||||
// title: '吃什么',
|
||||
// to: '/about',
|
||||
// },
|
||||
{
|
||||
icon: 'i-ri-question-line',
|
||||
activeIcon: 'i-ri-question-fill',
|
||||
title: '帮助',
|
||||
to: '/help',
|
||||
},
|
||||
{
|
||||
icon: 'i-ri-user-line',
|
||||
activeIcon: 'i-ri-user-fill',
|
||||
title: '我的',
|
||||
to: '/user',
|
||||
},
|
||||
]
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
function onClick(item: BottomMenuItem) {
|
||||
// router.push(item.to || '/')
|
||||
router.replace(item.to || '/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<YlfBottomMenu shadow-2xl pb="$cook-bottom-menu-padding-bottom">
|
||||
<YlfBottomMenuItem
|
||||
v-for="item in items"
|
||||
:key="item.to"
|
||||
:item="item"
|
||||
:active="route.path === item.to"
|
||||
class="pt-3"
|
||||
@click="onClick"
|
||||
/>
|
||||
</YlfBottomMenu>
|
||||
</template>
|
||||
32
app/components/ToggleMode.vue
Normal file
32
app/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>
|
||||
13
app/components/common/BackBtn.vue
Normal file
13
app/components/common/BackBtn.vue
Normal 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>
|
||||
25
app/components/common/DarkToggle.vue
Normal file
25
app/components/common/DarkToggle.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<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>
|
||||
<YlfIconButton
|
||||
class="icon-btn hover:text-yellow-400 !outline-none"
|
||||
text-xl
|
||||
title="切换" @click="toggleDark()"
|
||||
>
|
||||
<div i="ri-sun-line dark:ri-moon-line" />
|
||||
</YlfIconButton>
|
||||
</template>
|
||||
105
app/components/common/SearchRecipe.vue
Normal file
105
app/components/common/SearchRecipe.vue
Normal 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>
|
||||
<div op="70" ml-2 inline-flex cursor-pointer text-base @click="closeModal">
|
||||
取消
|
||||
</div>
|
||||
</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>
|
||||
60
app/components/content/AboutMe.vue
Normal file
60
app/components/content/AboutMe.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="ts" setup>
|
||||
// @ts-expect-error // Ignore this line
|
||||
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>
|
||||
19
app/components/content/AboutMenu.vue
Normal file
19
app/components/content/AboutMenu.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div>
|
||||
<NuxtLink class="icon-btn mx-2 hover:text-orange-400" to="/help" title="帮助">
|
||||
<div i-ri-question-line />
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink class="icon-btn mx-2 hover:text-blue-400" to="/about" title="关于">
|
||||
<div i-ri-information-line />
|
||||
</NuxtLink>
|
||||
|
||||
<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 />
|
||||
</a>
|
||||
|
||||
<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 />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
31
app/components/cookbook/CookbookCard.vue
Normal file
31
app/components/cookbook/CookbookCard.vue
Normal 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>
|
||||
27
app/components/cookbook/CookbookDetail.vue
Normal file
27
app/components/cookbook/CookbookDetail.vue
Normal 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>
|
||||
18
app/components/cookbook/NewCookbookCard.vue
Normal file
18
app/components/cookbook/NewCookbookCard.vue
Normal 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>
|
||||
34
app/components/features/InstallPwa.vue
Normal file
34
app/components/features/InstallPwa.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<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-2" m="2" inline-flex
|
||||
items-center justify-center rounded-md font-bold
|
||||
@click="install"
|
||||
>
|
||||
<div i-ri-install-line mr-1 inline-flex />
|
||||
<span inline-flex>安装到桌面</span>
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
63
app/components/features/ReloadPrompt.vue
Normal file
63
app/components/features/ReloadPrompt.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
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 rounded shadow-lg transition hover:shadow-md"
|
||||
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="rounded shadow 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>
|
||||
20
app/components/layout/CurrentVersion.vue
Normal file
20
app/components/layout/CurrentVersion.vue
Normal 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 now = import.meta.env.VITE_APP_BUILD_TIME
|
||||
const buildDate = (new Date(Number.parseInt(now))).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>
|
||||
22
app/components/layout/SimpleCopyright.vue
Normal file
22
app/components/layout/SimpleCopyright.vue
Normal 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>
|
||||
48
app/components/recipe/RecipeTable.vue
Normal file
48
app/components/recipe/RecipeTable.vue
Normal 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>
|
||||
31
app/components/recipe/RecipeTableItem.vue
Normal file
31
app/components/recipe/RecipeTableItem.vue
Normal 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>
|
||||
52
app/components/tags/DishTag.vue
Normal file
52
app/components/tags/DishTag.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DbRecipeItem } from '~/utils/db'
|
||||
import { tools } from '~/data/food'
|
||||
import type { RecipeItem } from '~/types'
|
||||
import { getEmojisFromStuff } from '~/utils'
|
||||
import { recipeHistories } from '~/composables/store/history'
|
||||
|
||||
const props = defineProps<{
|
||||
dish: RecipeItem | DbRecipeItem
|
||||
}>()
|
||||
|
||||
const gtm = useGtm()
|
||||
|
||||
function triggerGtm(dish: RecipeItem) {
|
||||
recipeHistories.value.push({
|
||||
recipe: dish,
|
||||
time: Date.now(),
|
||||
})
|
||||
|
||||
gtm?.trackEvent({
|
||||
event: 'click',
|
||||
category: `dish_${dish.name}`,
|
||||
action: 'click_recipe',
|
||||
label: '跳转菜谱',
|
||||
})
|
||||
gtm?.trackEvent({
|
||||
event: 'click_dish',
|
||||
action: dish.name,
|
||||
})
|
||||
}
|
||||
|
||||
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)"
|
||||
>
|
||||
<span m="r-1" text="sm blue-700 dark:blue-200">
|
||||
{{ dishLabel }}
|
||||
</span>
|
||||
<template v-for="tool, i in tools">
|
||||
<span v-if="dish.tools?.includes(tool.name)" :key="i" :class="tool.icon" />
|
||||
</template>
|
||||
</a>
|
||||
</template>
|
||||
16
app/components/tags/MeatTag.vue
Normal file
16
app/components/tags/MeatTag.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
active: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="meat-tag rounded tag" 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
app/components/tags/StapleTag.vue
Normal file
15
app/components/tags/StapleTag.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
active: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="rounded tag" 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
app/components/tags/ToolTag.vue
Normal file
16
app/components/tags/ToolTag.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
active: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="rounded tag" 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
app/components/tags/VegetableTag.vue
Normal file
16
app/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>
|
||||
24
app/components/ylf/YlfForm.vue
Normal file
24
app/components/ylf/YlfForm.vue
Normal 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>
|
||||
33
app/components/ylf/YlfFormItem.vue
Normal file
33
app/components/ylf/YlfFormItem.vue
Normal 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>
|
||||
16
app/components/ylf/YlfIconButton.vue
Normal file
16
app/components/ylf/YlfIconButton.vue
Normal 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>
|
||||
22
app/components/ylf/YlfIconItem.vue
Normal file
22
app/components/ylf/YlfIconItem.vue
Normal 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>
|
||||
28
app/components/ylf/YlfSwitch.vue
Normal file
28
app/components/ylf/YlfSwitch.vue
Normal 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>
|
||||
Reference in New Issue
Block a user