Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5efd7b0533 | ||
|
|
a80d9163b8 | ||
|
|
d25148c731 | ||
|
|
d42e3cf65d | ||
|
|
f59c34499a | ||
|
|
e741f5d5a0 | ||
|
|
99d7d660bb | ||
|
|
87025cd17e | ||
|
|
68a4b2dbeb | ||
|
|
1ab402af8b | ||
|
|
b2cf053446 | ||
|
|
37bab9269c | ||
|
|
44f9b40ee5 | ||
|
|
4ddd5ac2d2 | ||
|
|
9df47e977e | ||
|
|
5fb76f24dc | ||
|
|
014f129b8e | ||
|
|
2949c45839 | ||
|
|
81586158d5 | ||
|
|
bfb6ea3e14 | ||
|
|
7d26f9ba84 | ||
|
|
9f12401922 | ||
|
|
69c689df67 | ||
|
|
8ca8c4aac8 |
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
SD_API_BASE_URL=
|
||||||
|
OPENAI_API_KEY=
|
||||||
20
.vscode/settings.json
vendored
20
.vscode/settings.json
vendored
@@ -1,5 +1,20 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": ["Vitesse", "Vite", "unocss", "vitest", "vueuse", "pinia", "demi", "antfu", "iconify", "intlify", "vitejs", "unplugin", "pnpm"],
|
"cSpell.words": [
|
||||||
|
"antfu",
|
||||||
|
"demi",
|
||||||
|
"iconify",
|
||||||
|
"intlify",
|
||||||
|
"nuxi",
|
||||||
|
"pinia",
|
||||||
|
"pnpm",
|
||||||
|
"unocss",
|
||||||
|
"unplugin",
|
||||||
|
"Vite",
|
||||||
|
"vitejs",
|
||||||
|
"Vitesse",
|
||||||
|
"vitest",
|
||||||
|
"vueuse"
|
||||||
|
],
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.css": "postcss"
|
"*.css": "postcss"
|
||||||
},
|
},
|
||||||
@@ -65,5 +80,6 @@
|
|||||||
"json",
|
"json",
|
||||||
"jsonc",
|
"jsonc",
|
||||||
"yaml"
|
"yaml"
|
||||||
]
|
],
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,8 +73,6 @@ docker stop cook
|
|||||||
感谢以下小伙伴为本项目提供的数据支持和 QA !
|
感谢以下小伙伴为本项目提供的数据支持和 QA !
|
||||||
|
|
||||||
- [Runny](https://weibo.com/runny)
|
- [Runny](https://weibo.com/runny)
|
||||||
- 山竹太凉
|
|
||||||
- leo
|
|
||||||
- 麒麟
|
- 麒麟
|
||||||
- 晴方啾
|
- 晴方啾
|
||||||
- 课代表阿伟
|
- 课代表阿伟
|
||||||
|
|||||||
5
app.config.ts
Normal file
5
app.config.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default defineAppConfig({
|
||||||
|
theme: {
|
||||||
|
primaryColor: '#ababab',
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -10,7 +10,7 @@ defineProps<{
|
|||||||
<template>
|
<template>
|
||||||
<Disclosure v-slot="{ open }" :default-open="defaultOpen" as="div" class="mt-2">
|
<Disclosure v-slot="{ open }" :default-open="defaultOpen" as="div" class="mt-2">
|
||||||
<DisclosureButton
|
<DisclosureButton
|
||||||
class="w-full flex justify-between rounded-lg bg-blue-100 px-4 py-2 text-left text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring focus-visible:ring-blue-500 focus-visible:ring-opacity-75"
|
class="w-full flex justify-between rounded-lg bg-blue-100 px-4 py-2 text-left text-sm text-blue-900 font-medium hover:bg-blue-200 focus:outline-none focus-visible:ring focus-visible:ring-blue-500 focus-visible:ring-opacity-75"
|
||||||
>
|
>
|
||||||
<span>{{ title }}</span>
|
<span>{{ title }}</span>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ const { random, randomRecipes } = useRandomRecipe(count)
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="randomRecipes.length > 0">
|
<button cursor-pointer class="inline-flex inline-flex items-center justify-center rounded-md border-none bg-blue-600 px-3 py-1.5 text-sm 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">
|
||||||
<button cursor-pointer class="inline-flex inline-flex items-center justify-center rounded-md border-none bg-blue-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-blue-500 focus-visible:outline-2 focus-visible:outline-blue-600 focus-visible:outline-offset-2 focus-visible:outline" @click="random">
|
|
||||||
<div class="transition" hover="text-blue-500" i-ri-refresh-line mr-1 inline-flex />
|
<div class="transition" hover="text-blue-500" i-ri-refresh-line mr-1 inline-flex />
|
||||||
<div>随机一下</div>
|
<div>随机一下</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div v-show="randomRecipes.length > 0">
|
||||||
<div m="t-8" flex="~ col">
|
<div m="t-8" flex="~ col">
|
||||||
<template v-for="recipe, i in randomRecipes" :key="i">
|
<template v-for="recipe, i in randomRecipes" :key="i">
|
||||||
<DishTag v-if="recipe" :dish="recipe" />
|
<DishTag v-if="recipe" :dish="recipe" />
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const showTooltip = computed(() => !selectedStuff.value.length && !curTool.value
|
|||||||
<br>
|
<br>
|
||||||
<div m="t-1">
|
<div m="t-1">
|
||||||
<span>欢迎来</span>
|
<span>欢迎来</span>
|
||||||
<a class="font-bold text-blue-600 dark:text-blue-400" href="https://docs.qq.com/sheet/DQk1vdkhFV0twQVNS?tab=uykkic" target="_blank">这里</a>
|
<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>
|
<span>反馈新的菜谱!</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,22 +43,22 @@ defineProps<{
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
-webkit-transition: .4s;
|
-webkit-transition: 0.4s;
|
||||||
transition: .4s;
|
transition: 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
$size: 20px;
|
$size: 20px;
|
||||||
|
|
||||||
.slider:before {
|
.slider:before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
content: "";
|
content: '';
|
||||||
height: $size;
|
height: $size;
|
||||||
width: $size;
|
width: $size;
|
||||||
left: 4px;
|
left: 4px;
|
||||||
bottom: 4px;
|
bottom: 4px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
-webkit-transition: .4s;
|
-webkit-transition: 0.4s;
|
||||||
transition: .4s;
|
transition: 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked + .slider {
|
input:checked + .slider {
|
||||||
|
|||||||
178
components/ai/AiChooseFood.vue
Normal file
178
components/ai/AiChooseFood.vue
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { StuffItem } from '~/types'
|
||||||
|
import { meat, staple, vegetable } from '~/data/food'
|
||||||
|
|
||||||
|
import { useEmojiAnimation } from '~/composables/animation'
|
||||||
|
import type { AIRecipeInfo } from '~/packages/ai/src'
|
||||||
|
import { generateRecipeInfo, getRecipeImage } from '~/utils/api'
|
||||||
|
|
||||||
|
const rStore = useRecipeStore()
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// cook recipe
|
||||||
|
const cooking = ref(false)
|
||||||
|
const recipeImg = ref('')
|
||||||
|
|
||||||
|
const aiRecipeInfo = ref<AIRecipeInfo>({
|
||||||
|
名称: '名称',
|
||||||
|
介绍: '介绍',
|
||||||
|
})
|
||||||
|
|
||||||
|
async function cook() {
|
||||||
|
cooking.value = true
|
||||||
|
const foods = rStore.selectedStuff
|
||||||
|
|
||||||
|
// reset
|
||||||
|
aiRecipeInfo.value = ({
|
||||||
|
名称: '起名中...',
|
||||||
|
介绍: '正在思考怎么介绍...',
|
||||||
|
})
|
||||||
|
recipeImg.value = ''
|
||||||
|
|
||||||
|
// generate
|
||||||
|
const [info, img] = await Promise.all([generateRecipeInfo(foods), getRecipeImage(foods)])
|
||||||
|
aiRecipeInfo.value = info
|
||||||
|
recipeImg.value = img
|
||||||
|
|
||||||
|
cooking.value = false
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<button
|
||||||
|
m-auto
|
||||||
|
flex items-center justify-center
|
||||||
|
class="rounded bg-yellow px-4 py-2 text-orange-900 font-black shadow hover:shadow-md active:shadow-inset"
|
||||||
|
@click="cook()"
|
||||||
|
>
|
||||||
|
<div v-if="cooking" class="mr-2 inline-flex" i-svg-spinners:clock />
|
||||||
|
<span>做黑暗料理 🥘</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="recipe-panel relative shadow transition"
|
||||||
|
m="x-2 y-4" p="2"
|
||||||
|
bg="gray-400/8"
|
||||||
|
>
|
||||||
|
<div text="xl" font="bold" p="1">
|
||||||
|
「{{ aiRecipeInfo['名称'] }}」
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cook-recipes text-center" p="2">
|
||||||
|
<img
|
||||||
|
v-if="recipeImg"
|
||||||
|
class="m-auto w-25 rounded shadow transition hover:shadow-md"
|
||||||
|
:src="recipeImg"
|
||||||
|
alt="recipes"
|
||||||
|
>
|
||||||
|
<div v-else class="m-auto h-25 w-25 rounded bg-gray shadow transition hover:shadow-md" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ aiRecipeInfo['介绍'] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
105
components/common/SearchRecipe.vue
Normal file
105
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-3 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>
|
||||||
@@ -3,7 +3,7 @@ import pkg from '~/package.json'
|
|||||||
|
|
||||||
const commitSha = (import.meta.env.VITE_COMMIT_REF || '').slice(0, 7)
|
const commitSha = (import.meta.env.VITE_COMMIT_REF || '').slice(0, 7)
|
||||||
const now = import.meta.env.VITE_APP_BUILD_TIME
|
const now = import.meta.env.VITE_APP_BUILD_TIME
|
||||||
const buildDate = (new Date(Number.parseInt(now) * 1000)).toLocaleDateString()
|
const buildDate = (new Date(Number.parseInt(now))).toLocaleDateString()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ defineProps<{
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr, th, td {
|
tr,
|
||||||
|
th,
|
||||||
|
td {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { DbRecipeItem } from '~/utils/db'
|
|||||||
import { tools } from '~/data/food'
|
import { tools } from '~/data/food'
|
||||||
import type { RecipeItem } from '~/types'
|
import type { RecipeItem } from '~/types'
|
||||||
import { getEmojisFromStuff } from '~/utils'
|
import { getEmojisFromStuff } from '~/utils'
|
||||||
|
import { recipeHistories } from '~/composables/store/history'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
dish: RecipeItem | DbRecipeItem
|
dish: RecipeItem | DbRecipeItem
|
||||||
@@ -10,16 +11,21 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const gtm = useGtm()
|
const gtm = useGtm()
|
||||||
|
|
||||||
function triggerGtm(val: string) {
|
function triggerGtm(dish: RecipeItem) {
|
||||||
|
recipeHistories.value.push({
|
||||||
|
recipe: dish,
|
||||||
|
time: Date.now(),
|
||||||
|
})
|
||||||
|
|
||||||
gtm?.trackEvent({
|
gtm?.trackEvent({
|
||||||
event: 'click',
|
event: 'click',
|
||||||
category: `dish_${val}`,
|
category: `dish_${dish.name}`,
|
||||||
action: 'click_recipe',
|
action: 'click_recipe',
|
||||||
label: '跳转菜谱',
|
label: '跳转菜谱',
|
||||||
})
|
})
|
||||||
gtm?.trackEvent({
|
gtm?.trackEvent({
|
||||||
event: 'click_dish',
|
event: 'click_dish',
|
||||||
action: val,
|
action: dish.name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,13 +40,13 @@ const dishLabel = computed(() => {
|
|||||||
:href="dish.link || `https://www.bilibili.com/video/${dish.bv}`" target="_blank" class="dish-tag rounded tag" p="x-2"
|
:href="dish.link || `https://www.bilibili.com/video/${dish.bv}`" target="_blank" class="dish-tag rounded tag" p="x-2"
|
||||||
border="~ blue-200 dark:blue-800"
|
border="~ blue-200 dark:blue-800"
|
||||||
bg="blue-300 opacity-20"
|
bg="blue-300 opacity-20"
|
||||||
@click="triggerGtm(dish.name)"
|
@click="triggerGtm(dish)"
|
||||||
>
|
>
|
||||||
<span m="r-1" class="inline-flex items-center justify-center" text="sm blue-700 dark:blue-200">
|
<span m="r-1" text="sm blue-700 dark:blue-200">
|
||||||
{{ dishLabel }}
|
{{ dishLabel }}
|
||||||
</span>
|
</span>
|
||||||
<span v-for="tool, i in tools" :key="i" inline-flex>
|
<template v-for="tool, i in tools">
|
||||||
<div v-if="dish.tools?.includes(tool.name)" :class="tool.icon" />
|
<span v-if="dish.tools?.includes(tool.name)" :key="i" :class="tool.icon" />
|
||||||
</span>
|
</template>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ defineProps<{
|
|||||||
:is="to ? NuxtLink : 'div'"
|
:is="to ? NuxtLink : 'div'"
|
||||||
:to="to"
|
:to="to"
|
||||||
class="ylf-form-item"
|
class="ylf-form-item"
|
||||||
w-full flex cursor-pointer items-center justify-between p-3
|
w-full flex cursor-pointer items-center justify-between p-2
|
||||||
hover:bg-gray-100
|
hover:bg-gray-100
|
||||||
dark:hover:bg-dark-400
|
dark:hover:bg-dark-400
|
||||||
>
|
>
|
||||||
<div v-if="label" class="text-md" inline-flex items-center justify-center>
|
<div v-if="label" class="text-sm" inline-flex items-center justify-center>
|
||||||
<div v-if="icon" :class="icon" mr-2 inline-flex />
|
<div v-if="icon" :class="icon" mr-2 inline-flex />
|
||||||
<span>{{ label }}</span>
|
<span>{{ label }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
22
components/ylf/YlfIconItem.vue
Normal file
22
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>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useStorage } from '@vueuse/core'
|
||||||
|
import { namespace } from '~/constants'
|
||||||
import type { DbRecipeItem } from '~/utils/db'
|
import type { DbRecipeItem } from '~/utils/db'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -5,7 +7,7 @@ import type { DbRecipeItem } from '~/utils/db'
|
|||||||
* @param total
|
* @param total
|
||||||
*/
|
*/
|
||||||
export function useRandomRecipe(total: Ref<number>) {
|
export function useRandomRecipe(total: Ref<number>) {
|
||||||
const randomRecipes = ref<(DbRecipeItem | undefined)[]>([])
|
const randomRecipes = useStorage<(DbRecipeItem | undefined)[]>(`${namespace}:random:recipes`, [])
|
||||||
async function random() {
|
async function random() {
|
||||||
const length = await db.recipes.count()
|
const length = await db.recipes.count()
|
||||||
const randomArr = generateRandomArray(length, total.value)
|
const randomArr = generateRandomArray(length, total.value)
|
||||||
@@ -19,6 +21,8 @@ export function useRandomRecipe(total: Ref<number>) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 如果没有随机菜谱,就生成一次
|
||||||
|
if (randomRecipes.value.length <= 0)
|
||||||
random()
|
random()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { defaultSettings } from '~/utils/settings'
|
import { defaultSettings } from '../../utils/settings'
|
||||||
import { namespace } from '~/constants'
|
import { namespace } from '../../constants'
|
||||||
|
|
||||||
export const useAppStore = defineStore('app', () => {
|
export const useAppStore = defineStore('app', () => {
|
||||||
const deferredPrompt = ref<Event | any>()
|
const deferredPrompt = ref<Event | any>()
|
||||||
|
|||||||
10
composables/store/history.ts
Normal file
10
composables/store/history.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { useStorage } from '@vueuse/core'
|
||||||
|
import { namespace } from '~/constants'
|
||||||
|
import type { RecipeItem } from '~/types'
|
||||||
|
|
||||||
|
export interface RecipeHistoryItem {
|
||||||
|
recipe: RecipeItem
|
||||||
|
time: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const recipeHistories = useStorage<RecipeHistoryItem[]>(`${namespace}:history`, [])
|
||||||
@@ -78,8 +78,6 @@ export const useRecipeStore = defineStore('recipe', () => {
|
|||||||
async function searchRecipes() {
|
async function searchRecipes() {
|
||||||
isSearching.value = true
|
isSearching.value = true
|
||||||
let result: RecipeItem[] = []
|
let result: RecipeItem[] = []
|
||||||
if (keyword.value)
|
|
||||||
result = await db.recipes.filter(item => item.name.includes(keyword.value)).toArray()
|
|
||||||
|
|
||||||
if (curMode.value === 'strict') {
|
if (curMode.value === 'strict') {
|
||||||
result = await db.recipes.filter((item) => {
|
result = await db.recipes.filter((item) => {
|
||||||
@@ -116,14 +114,18 @@ export const useRecipeStore = defineStore('recipe', () => {
|
|||||||
}).toArray()
|
}).toArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (keyword.value)
|
||||||
|
result = result.filter(item => item.name.includes(keyword.value))
|
||||||
|
|
||||||
isSearching.value = false
|
isSearching.value = false
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认严格模式
|
// 默认严格模式
|
||||||
const displayedRecipe = ref<RecipeItem[]>([])
|
const displayedRecipe = ref<RecipeItem[]>([])
|
||||||
watch([keyword, curStuff, curTool, curMode], async () => {
|
// fix curStuff watch
|
||||||
displayedRecipe.value = await searchRecipes()
|
watch(() => [keyword.value, selectedStuff.value, curTool.value, curMode.value], async () => {
|
||||||
|
displayedRecipe.value = [...(await searchRecipes())]
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ export const appName = '食用手册'
|
|||||||
export const appDescription = '好的,今天我们来做菜!'
|
export const appDescription = '好的,今天我们来做菜!'
|
||||||
|
|
||||||
export const namespace = 'cook'
|
export const namespace = 'cook'
|
||||||
export const lastDbUpdated = '2022-07-27 03:05:02'
|
export const lastDbUpdated = '2023-11-11 19:51:02'
|
||||||
|
|
||||||
export * from './links'
|
export * from './links'
|
||||||
|
|||||||
29
data/food.ts
29
data/food.ts
@@ -7,63 +7,78 @@ export const vegetable: StuffItem[] = [
|
|||||||
{
|
{
|
||||||
name: '土豆',
|
name: '土豆',
|
||||||
emoji: '🥔',
|
emoji: '🥔',
|
||||||
|
en: 'potato',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '胡萝卜',
|
name: '胡萝卜',
|
||||||
emoji: '🥕',
|
emoji: '🥕',
|
||||||
|
en: 'carrot',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '花菜',
|
name: '花菜',
|
||||||
emoji: '🥦',
|
emoji: '🥦',
|
||||||
|
en: 'cauliflower',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '白萝卜',
|
name: '白萝卜',
|
||||||
emoji: '🥣',
|
emoji: '🥣',
|
||||||
|
en: 'radish',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '西葫芦',
|
name: '西葫芦',
|
||||||
emoji: '🥒',
|
emoji: '🥒',
|
||||||
|
en: 'zucchini',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '番茄',
|
name: '番茄',
|
||||||
emoji: '🍅',
|
emoji: '🍅',
|
||||||
alias: '西红柿',
|
alias: '西红柿',
|
||||||
|
en: 'tomato',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '芹菜',
|
name: '芹菜',
|
||||||
emoji: '🥬',
|
emoji: '🥬',
|
||||||
|
en: 'celery',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '黄瓜',
|
name: '黄瓜',
|
||||||
emoji: '🥒',
|
emoji: '🥒',
|
||||||
|
en: 'cucumber',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '洋葱',
|
name: '洋葱',
|
||||||
emoji: '🧅',
|
emoji: '🧅',
|
||||||
|
en: 'onion',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '莴笋',
|
name: '莴笋',
|
||||||
emoji: '🎍',
|
emoji: '🎍',
|
||||||
|
en: 'lettuce',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '菌菇',
|
name: '菌菇',
|
||||||
emoji: '🍄',
|
emoji: '🍄',
|
||||||
|
en: 'mushroom',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '茄子',
|
name: '茄子',
|
||||||
emoji: '🍆',
|
emoji: '🍆',
|
||||||
|
en: 'eggplant',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '豆腐',
|
name: '豆腐',
|
||||||
emoji: '🍲',
|
emoji: '🍲',
|
||||||
|
en: 'tofu',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '包菜',
|
name: '包菜',
|
||||||
emoji: '🥗',
|
emoji: '🥗',
|
||||||
|
en: 'cabbage',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '白菜',
|
name: '白菜',
|
||||||
emoji: '🥬',
|
emoji: '🥬',
|
||||||
|
en: 'cabbage',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -74,42 +89,52 @@ export const meat: StuffItem[] = [
|
|||||||
{
|
{
|
||||||
name: '午餐肉',
|
name: '午餐肉',
|
||||||
emoji: '🥓',
|
emoji: '🥓',
|
||||||
|
en: 'bacon',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '香肠',
|
name: '香肠',
|
||||||
emoji: '🌭',
|
emoji: '🌭',
|
||||||
|
en: 'sausage',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '腊肠',
|
name: '腊肠',
|
||||||
emoji: '🌭',
|
emoji: '🌭',
|
||||||
|
en: 'sausage',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '鸡肉',
|
name: '鸡肉',
|
||||||
emoji: '🐤',
|
emoji: '🐤',
|
||||||
|
en: 'chicken',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '猪肉',
|
name: '猪肉',
|
||||||
emoji: '🐷',
|
emoji: '🐷',
|
||||||
|
en: 'pork',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '鸡蛋',
|
name: '鸡蛋',
|
||||||
emoji: '🥚',
|
emoji: '🥚',
|
||||||
|
en: 'egg',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '虾',
|
name: '虾',
|
||||||
emoji: '🦐',
|
emoji: '🦐',
|
||||||
|
en: 'shrimp',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '牛肉',
|
name: '牛肉',
|
||||||
emoji: '🐮',
|
emoji: '🐮',
|
||||||
|
en: 'beef',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '骨头',
|
name: '骨头',
|
||||||
emoji: '🦴',
|
emoji: '🦴',
|
||||||
|
en: 'bone',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '鱼(Todo)',
|
name: '鱼(Todo)',
|
||||||
emoji: '🐟',
|
emoji: '🐟',
|
||||||
|
en: 'fish',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -120,18 +145,22 @@ export const staple: StuffItem[] = [
|
|||||||
{
|
{
|
||||||
name: '面食',
|
name: '面食',
|
||||||
emoji: '🍝',
|
emoji: '🍝',
|
||||||
|
en: 'noodles',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '面包',
|
name: '面包',
|
||||||
emoji: '🍞',
|
emoji: '🍞',
|
||||||
|
en: 'bread',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '米',
|
name: '米',
|
||||||
emoji: '🍚',
|
emoji: '🍚',
|
||||||
|
en: 'rice',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '方便面',
|
name: '方便面',
|
||||||
emoji: '🍜',
|
emoji: '🍜',
|
||||||
|
en: 'instant noodles',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -217,7 +217,6 @@ BBQ烟熏手撕猪肉,猪肉,BV1DV411x7SH,复杂,,烤,烤箱,
|
|||||||
微波炉版照烧鸡腿饭,米、鸡肉、胡萝卜、花菜,BV16Z4y1V774,,,,微波炉,
|
微波炉版照烧鸡腿饭,米、鸡肉、胡萝卜、花菜,BV16Z4y1V774,,,,微波炉,
|
||||||
微波炉版蒸蛋羹,鸡蛋,BV19T4y1D7Zd,,,,微波炉,
|
微波炉版蒸蛋羹,鸡蛋,BV19T4y1D7Zd,,,,微波炉,
|
||||||
微波炉版煮米饭,米,BV193411W7nb,,,,微波炉,
|
微波炉版煮米饭,米,BV193411W7nb,,,,微波炉,
|
||||||
豉油鸡翅,鸡肉,BV1vz4y1d775,普通,茶餐厅,煮,一口大锅,
|
|
||||||
水煮肉片,猪肉、芹菜、莴笋,BV1ZZ4y1379N,普通,川菜,炒,一口大锅,
|
水煮肉片,猪肉、芹菜、莴笋,BV1ZZ4y1379N,普通,川菜,炒,一口大锅,
|
||||||
脆口黄瓜,黄瓜,BV1Tb4y1X7ow,简单,脆口,凉拌,一口大锅,
|
脆口黄瓜,黄瓜,BV1Tb4y1X7ow,简单,脆口,凉拌,一口大锅,
|
||||||
白萝卜汤,白萝卜,BV1HJ411L7xA,简单,单一食材,煮,一口大锅,
|
白萝卜汤,白萝卜,BV1HJ411L7xA,简单,单一食材,煮,一口大锅,
|
||||||
@@ -297,7 +296,7 @@ BBQ烟熏手撕猪肉,猪肉,BV1DV411x7SH,复杂,,烤,烤箱,
|
|||||||
名古屋鸡翅,鸡肉,BV1ET4y1A7Xd,普通,日式,炸,一口大锅,
|
名古屋鸡翅,鸡肉,BV1ET4y1A7Xd,普通,日式,炸,一口大锅,
|
||||||
日式炖白萝,白萝卜,BV17b411B7H1,简单,日式,炖,一口大锅,
|
日式炖白萝,白萝卜,BV17b411B7H1,简单,日式,炖,一口大锅,
|
||||||
炸虾天妇罗,虾,BV1e5411t7LY,困难,日式,炸,一口大锅,
|
炸虾天妇罗,虾,BV1e5411t7LY,困难,日式,炸,一口大锅,
|
||||||
可乐饼,土豆、洋葱、肉、鸡蛋,BV1yW411Q7sa,普通,日式菜,炸,一口大锅,
|
可乐饼,土豆、洋葱、肉、鸡蛋,BV17x411U75q,普通,日式菜,炸,一口大锅,
|
||||||
清炒莴笋丝,莴笋、胡萝卜,BV1qK411H7RL,简单,爽口,炒,一口大锅,
|
清炒莴笋丝,莴笋、胡萝卜,BV1qK411H7RL,简单,爽口,炒,一口大锅,
|
||||||
莴笋泡菜,莴笋,BV1h741127rS,简单,爽口,泡菜,一口大锅,
|
莴笋泡菜,莴笋,BV1h741127rS,简单,爽口,泡菜,一口大锅,
|
||||||
口蘑汤,菌菇,BV1e64y1h776,简单,汤,煎、炖,一口大锅,
|
口蘑汤,菌菇,BV1e64y1h776,简单,汤,煎、炖,一口大锅,
|
||||||
@@ -598,3 +597,4 @@ biangbiang面,面食,BV1844y157GL,简单,,油泼,一口大锅,
|
|||||||
电饭煲番茄牛肉焖饭,番茄、牛肉、米,BV1Bv411C7X3,普通,,,一口大锅、电饭煲,
|
电饭煲番茄牛肉焖饭,番茄、牛肉、米,BV1Bv411C7X3,普通,,,一口大锅、电饭煲,
|
||||||
电饭煲排骨土豆焖饭,猪肉、土豆、米,BV1Bv411C7X3,普通,,,一口大锅、电饭煲,
|
电饭煲排骨土豆焖饭,猪肉、土豆、米,BV1Bv411C7X3,普通,,,一口大锅、电饭煲,
|
||||||
米布丁,米、鸡蛋,BV1hr4y1k7A5,简单,,,一口大锅、电饭煲,
|
米布丁,米、鸡蛋,BV1hr4y1k7A5,简单,,,一口大锅、电饭煲,
|
||||||
|
麻婆豆腐,豆腐,BV1it4y1X75m,简单,,,一口大锅,
|
||||||
|
|||||||
|
File diff suppressed because one or more lines are too long
@@ -1,7 +1,8 @@
|
|||||||
import antfu from '@antfu/eslint-config'
|
import antfu from '@antfu/eslint-config'
|
||||||
import unocss from '@unocss/eslint-config/flat'
|
|
||||||
|
|
||||||
export default antfu(
|
export default antfu(
|
||||||
{},
|
{
|
||||||
unocss,
|
unocss: true,
|
||||||
|
formatters: true,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ By default, `default.vue` will be used unless an alternative is specified in the
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'home',
|
layout: 'home',
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const route = useRoute()
|
|||||||
<main class="text-center text-gray-700 dark:text-gray-200" p="t-5 b-15">
|
<main class="text-center text-gray-700 dark:text-gray-200" p="t-5 b-15">
|
||||||
<div flex items-center justify-between>
|
<div flex items-center justify-between>
|
||||||
<BackBtn ml-3 />
|
<BackBtn ml-3 />
|
||||||
<h2 flex items-center justify-center text-lg>
|
<h2 flex items-center justify-center text-lg font="bold">
|
||||||
{{ route.meta.title }}
|
{{ route.meta.title }}
|
||||||
</h2>
|
</h2>
|
||||||
<DarkToggle mr-3 />
|
<DarkToggle mr-3 />
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="cook-main text-center text-gray-700 dark:text-gray-200" p="t-8 b-$cook-bottom-menu-height">
|
<main class="cook-main text-center text-gray-700 dark:text-gray-200" p="t-8 b-$cook-bottom-menu-height">
|
||||||
<slot />
|
<slot />
|
||||||
<DarkToggle absolute right-3 top-5 />
|
<DarkToggle absolute left-3 top-5 />
|
||||||
|
<SearchRecipe />
|
||||||
<TheBottomMenu fixed bottom-0 left-0 right-0 />
|
<TheBottomMenu fixed bottom-0 left-0 right-0 />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
6
layouts/preview.vue
Normal file
6
layouts/preview.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<main class="cook-main text-center text-gray-700 dark:text-gray-200" p="t-8 b-$cook-bottom-menu-height">
|
||||||
|
<slot />
|
||||||
|
<DarkToggle absolute left-3 top-4 />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
16
netlify.toml
16
netlify.toml
@@ -1,11 +1,11 @@
|
|||||||
[build.environment]
|
|
||||||
NODE_VERSION = "16"
|
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
publish = "dist"
|
publish = "dist"
|
||||||
command = "pnpm run build"
|
command = "pnpm run build"
|
||||||
|
|
||||||
|
[build.environment]
|
||||||
|
NODE_VERSION = "16"
|
||||||
|
|
||||||
[[redirects]]
|
[[redirects]]
|
||||||
from = "/*"
|
from = "/*"
|
||||||
to = "/index.html"
|
to = "/index.html"
|
||||||
status = 200
|
status = 200
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ Object.assign(process.env, {
|
|||||||
VITE_COMMIT_REF: process.env.CF_PAGES_COMMIT_SHA || '',
|
VITE_COMMIT_REF: process.env.CF_PAGES_COMMIT_SHA || '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// add build time to env
|
||||||
|
import.meta.env.VITE_APP_BUILD_TIME = new Date().getTime().toString()
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
|
||||||
@@ -13,9 +15,10 @@ export default defineNuxtConfig({
|
|||||||
'@vueuse/nuxt',
|
'@vueuse/nuxt',
|
||||||
'@unocss/nuxt',
|
'@unocss/nuxt',
|
||||||
'@pinia/nuxt',
|
'@pinia/nuxt',
|
||||||
|
|
||||||
|
'@nuxt/test-utils/module',
|
||||||
'@nuxtjs/color-mode',
|
'@nuxtjs/color-mode',
|
||||||
'@vite-pwa/nuxt',
|
'@vite-pwa/nuxt',
|
||||||
'nuxt-vitest',
|
|
||||||
|
|
||||||
'@zadigetvoltaire/nuxt-gtm',
|
'@zadigetvoltaire/nuxt-gtm',
|
||||||
|
|
||||||
@@ -26,7 +29,8 @@ export default defineNuxtConfig({
|
|||||||
// when using generate, payload js assets included in sw precache manifest
|
// when using generate, payload js assets included in sw precache manifest
|
||||||
// but missing on offline, disabling extraction it until fixed
|
// but missing on offline, disabling extraction it until fixed
|
||||||
payloadExtraction: false,
|
payloadExtraction: false,
|
||||||
inlineSSRStyles: false,
|
// inlineSSRStyles: false,
|
||||||
|
renderJsonPayloads: true,
|
||||||
typedPages: true,
|
typedPages: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
83
package.json
83
package.json
@@ -1,66 +1,73 @@
|
|||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.1.8",
|
"version": "1.2.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@8.10.2",
|
"packageManager": "pnpm@8.15.5",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=16"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run convert && cross-env VITE_APP_BUILD_TIME=$(date +%s) nuxi build",
|
"build": "npm run convert && nuxt build",
|
||||||
|
"build:static": "npm run convert && nuxt generate",
|
||||||
"convert": "tsx scripts/convert.ts",
|
"convert": "tsx scripts/convert.ts",
|
||||||
"dev": "cross-env VITE_APP_BUILD_TIME=$(date +%s) nuxi dev --host",
|
"dev": "nuxt dev --host",
|
||||||
"dev:pwa": "VITE_PLUGIN_PWA=true nuxi dev",
|
"dev:pwa": "VITE_PLUGIN_PWA=true nuxi dev",
|
||||||
"generate": "nuxi generate",
|
"generate": "nuxt generate",
|
||||||
"start:generate": "npx serve .output/public",
|
"start:generate": "npx serve .output/public",
|
||||||
"start": "node .output/server/index.mjs",
|
"start": "node .output/server/index.mjs",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"postinstall": "nuxi prepare && npm run convert",
|
"postinstall": "nuxt prepare && npm run convert",
|
||||||
|
"preview": "serve dist",
|
||||||
"preview-https": "serve dist",
|
"preview-https": "serve dist",
|
||||||
"release": "bumpp",
|
"release": "bumpp",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"typecheck": "vue-tsc --noEmit"
|
"typecheck": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
"vue-about-me": "^1.2.7"
|
"vue-about-me": "^1.2.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^1.1.0",
|
"@antfu/eslint-config": "^2.9.0",
|
||||||
"@headlessui/vue": "^1.7.16",
|
"@headlessui/vue": "^1.7.19",
|
||||||
"@iconify-json/carbon": "^1.1.21",
|
"@iconify-json/carbon": "^1.1.31",
|
||||||
"@iconify-json/fe": "^1.1.7",
|
"@iconify-json/fe": "^1.1.10",
|
||||||
"@iconify-json/gg": "^1.1.6",
|
"@iconify-json/gg": "^1.1.9",
|
||||||
"@iconify-json/ic": "^1.1.14",
|
"@iconify-json/ic": "^1.1.17",
|
||||||
"@iconify-json/mdi": "^1.1.55",
|
"@iconify-json/mdi": "^1.1.64",
|
||||||
"@iconify-json/ri": "^1.1.12",
|
"@iconify-json/ri": "^1.1.20",
|
||||||
"@iconify-json/twemoji": "^1.1.12",
|
"@iconify-json/svg-spinners": "^1.1.2",
|
||||||
"@nuxt/devtools": "^1.0.0",
|
"@iconify-json/twemoji": "^1.1.15",
|
||||||
"@nuxtjs/color-mode": "^3.3.0",
|
"@nuxt/devtools": "^1.1.3",
|
||||||
|
"@nuxt/test-utils": "^3.12.0",
|
||||||
|
"@nuxtjs/color-mode": "^3.3.3",
|
||||||
"@pinia/nuxt": "^0.5.1",
|
"@pinia/nuxt": "^0.5.1",
|
||||||
"@pinia/testing": "^0.1.3",
|
"@pinia/testing": "^0.1.3",
|
||||||
"@unocss/eslint-config": "^0.57.2",
|
"@unocss/eslint-config": "^0.58.6",
|
||||||
"@unocss/nuxt": "^0.57.2",
|
"@unocss/nuxt": "^0.58.6",
|
||||||
"@vite-pwa/nuxt": "^0.1.1",
|
"@vite-pwa/nuxt": "^0.6.0",
|
||||||
"@vue/test-utils": "^2.4.1",
|
"@vue/test-utils": "^2.4.5",
|
||||||
"@vueuse/nuxt": "^10.5.0",
|
"@vueuse/nuxt": "^10.9.0",
|
||||||
"@yunlefun/vue": "0.0.8-beta.4",
|
"@yunlefun/vue": "^0.1.1",
|
||||||
"@zadigetvoltaire/nuxt-gtm": "^0.0.13",
|
"@zadigetvoltaire/nuxt-gtm": "^0.0.13",
|
||||||
|
"bumpp": "^9.4.0",
|
||||||
"consola": "^3.2.3",
|
"consola": "^3.2.3",
|
||||||
"cross-env": "^7.0.3",
|
"dexie": "^3.2.7",
|
||||||
"dexie": "^3.2.4",
|
"eslint": "^8.57.0",
|
||||||
"eslint": "^8.53.0",
|
"eslint-plugin-format": "^0.1.0",
|
||||||
"fake-indexeddb": "^5.0.1",
|
"fake-indexeddb": "^5.0.2",
|
||||||
"happy-dom": "^12.10.3",
|
"happy-dom": "^14.3.1",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^24.0.0",
|
||||||
"nuxt": "^3.8.0",
|
"nuxt": "^3.9.3",
|
||||||
"nuxt-vitest": "^0.11.2",
|
"nuxt-vitest": "^0.11.5",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"sass": "^1.69.5",
|
"sass": "^1.72.0",
|
||||||
|
"serve": "^14.2.1",
|
||||||
"star-markdown-css": "^0.4.2",
|
"star-markdown-css": "^0.4.2",
|
||||||
"tsx": "^3.14.0",
|
"tsx": "^4.7.1",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.4.3",
|
||||||
"unocss": "^0.57.2",
|
"unocss": "^0.58.6",
|
||||||
"vitest": "^0.34.6",
|
"vitest": "^1.4.0",
|
||||||
"vue-tsc": "^1.8.22"
|
"vue-tsc": "^2.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/ai/.env.example
Normal file
1
packages/ai/.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
OPENAI_API_KEY=
|
||||||
5
packages/ai/package.json
Normal file
5
packages/ai/package.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"openai": "^4.29.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
197
packages/ai/pnpm-lock.yaml
generated
Normal file
197
packages/ai/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
lockfileVersion: '6.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
devDependencies:
|
||||||
|
openai:
|
||||||
|
specifier: ^4.29.2
|
||||||
|
version: 4.29.2
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
/@types/node-fetch@2.6.11:
|
||||||
|
resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.19.26
|
||||||
|
form-data: 4.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/node@18.19.26:
|
||||||
|
resolution: {integrity: sha512-+wiMJsIwLOYCvUqSdKTrfkS8mpTp+MPINe6+Np4TAGFWWRWiBQ5kSq9nZGCSPkzx9mvT+uEukzpX4MOSCydcvw==}
|
||||||
|
dependencies:
|
||||||
|
undici-types: 5.26.5
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/abort-controller@3.0.0:
|
||||||
|
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||||
|
engines: {node: '>=6.5'}
|
||||||
|
dependencies:
|
||||||
|
event-target-shim: 5.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/agentkeepalive@4.5.0:
|
||||||
|
resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
|
||||||
|
engines: {node: '>= 8.0.0'}
|
||||||
|
dependencies:
|
||||||
|
humanize-ms: 1.2.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/asynckit@0.4.0:
|
||||||
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/base-64@0.1.0:
|
||||||
|
resolution: {integrity: sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/charenc@0.0.2:
|
||||||
|
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/combined-stream@1.0.8:
|
||||||
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dependencies:
|
||||||
|
delayed-stream: 1.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/crypt@0.0.2:
|
||||||
|
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/delayed-stream@1.0.0:
|
||||||
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/digest-fetch@1.3.0:
|
||||||
|
resolution: {integrity: sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==}
|
||||||
|
dependencies:
|
||||||
|
base-64: 0.1.0
|
||||||
|
md5: 2.3.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/event-target-shim@5.0.1:
|
||||||
|
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/form-data-encoder@1.7.2:
|
||||||
|
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/form-data@4.0.0:
|
||||||
|
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
dependencies:
|
||||||
|
asynckit: 0.4.0
|
||||||
|
combined-stream: 1.0.8
|
||||||
|
mime-types: 2.1.35
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/formdata-node@4.4.1:
|
||||||
|
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
|
||||||
|
engines: {node: '>= 12.20'}
|
||||||
|
dependencies:
|
||||||
|
node-domexception: 1.0.0
|
||||||
|
web-streams-polyfill: 4.0.0-beta.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/humanize-ms@1.2.1:
|
||||||
|
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||||
|
dependencies:
|
||||||
|
ms: 2.1.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/is-buffer@1.1.6:
|
||||||
|
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/md5@2.3.0:
|
||||||
|
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
|
||||||
|
dependencies:
|
||||||
|
charenc: 0.0.2
|
||||||
|
crypt: 0.0.2
|
||||||
|
is-buffer: 1.1.6
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/mime-db@1.52.0:
|
||||||
|
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/mime-types@2.1.35:
|
||||||
|
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dependencies:
|
||||||
|
mime-db: 1.52.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/ms@2.1.3:
|
||||||
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/node-domexception@1.0.0:
|
||||||
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
|
engines: {node: '>=10.5.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/node-fetch@2.7.0:
|
||||||
|
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||||
|
engines: {node: 4.x || >=6.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
encoding: ^0.1.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
encoding:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
whatwg-url: 5.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/openai@4.29.2:
|
||||||
|
resolution: {integrity: sha512-cPkT6zjEcE4qU5OW/SoDDuXEsdOLrXlAORhzmaguj5xZSPlgKvLhi27sFWhLKj07Y6WKNWxcwIbzm512FzTBNQ==}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.19.26
|
||||||
|
'@types/node-fetch': 2.6.11
|
||||||
|
abort-controller: 3.0.0
|
||||||
|
agentkeepalive: 4.5.0
|
||||||
|
digest-fetch: 1.3.0
|
||||||
|
form-data-encoder: 1.7.2
|
||||||
|
formdata-node: 4.4.1
|
||||||
|
node-fetch: 2.7.0
|
||||||
|
web-streams-polyfill: 3.3.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/tr46@0.0.3:
|
||||||
|
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/undici-types@5.26.5:
|
||||||
|
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/web-streams-polyfill@3.3.3:
|
||||||
|
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/web-streams-polyfill@4.0.0-beta.3:
|
||||||
|
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||||
|
engines: {node: '>= 14'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/webidl-conversions@3.0.1:
|
||||||
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/whatwg-url@5.0.0:
|
||||||
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
dependencies:
|
||||||
|
tr46: 0.0.3
|
||||||
|
webidl-conversions: 3.0.1
|
||||||
|
dev: true
|
||||||
53
packages/ai/src/api.ts
Normal file
53
packages/ai/src/api.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import consola from 'consola'
|
||||||
|
import type OpenAI from 'openai'
|
||||||
|
import { baseChatCompletionCreateParams, baseModel, config, openai } from './config'
|
||||||
|
|
||||||
|
// TODO: pass params
|
||||||
|
|
||||||
|
export async function getCompletion(msg: string) {
|
||||||
|
const chatCompletion = await openai.chat.completions.create({
|
||||||
|
...baseChatCompletionCreateParams,
|
||||||
|
messages: [{ role: 'user', content: msg }],
|
||||||
|
model: baseModel,
|
||||||
|
})
|
||||||
|
|
||||||
|
return chatCompletion.choices
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 ai 生成的菜谱信息
|
||||||
|
*/
|
||||||
|
export async function getAIRecipeInfo(zhFoods: string[]) {
|
||||||
|
/**
|
||||||
|
* 限制输入长度
|
||||||
|
*/
|
||||||
|
const promptFoods = zhFoods.join('、').slice(0, config.inputMaxLength)
|
||||||
|
|
||||||
|
// 尽可能少的 token
|
||||||
|
const tooltip = `
|
||||||
|
使用以下材料【${promptFoods}】做一道菜,请为这道菜起个名字,最好带有文化底蕴。
|
||||||
|
不要使用生僻字和标点符号。
|
||||||
|
并给出一个有趣的不超过100字的介绍。
|
||||||
|
格式类型:{
|
||||||
|
"名称": "",
|
||||||
|
"介绍": ""
|
||||||
|
}
|
||||||
|
直接给出可以被 JSON.parse 解析的字符串,不需要解释内容。`
|
||||||
|
|
||||||
|
const messages: OpenAI.ChatCompletionMessageParam[] = [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: tooltip,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const chatCompletion = await openai.chat.completions.create({
|
||||||
|
...baseChatCompletionCreateParams,
|
||||||
|
messages,
|
||||||
|
model: baseModel,
|
||||||
|
// stream: true
|
||||||
|
})
|
||||||
|
|
||||||
|
consola.debug(chatCompletion)
|
||||||
|
return chatCompletion.choices[0].message
|
||||||
|
}
|
||||||
26
packages/ai/src/config.ts
Normal file
26
packages/ai/src/config.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import 'dotenv/config'
|
||||||
|
import process from 'node:process'
|
||||||
|
import OpenAI from 'openai'
|
||||||
|
|
||||||
|
const deepseekApiUrl = 'https://api.deepseek.com/v1'
|
||||||
|
|
||||||
|
const aiServiceUrl = process.env.AI_SERVICE_URL || deepseekApiUrl
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
inputMaxLength: 300,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const openai = new OpenAI({
|
||||||
|
baseURL: aiServiceUrl,
|
||||||
|
apiKey: process.env.OPENAI_API_KEY, // This is the default and can be omitted
|
||||||
|
})
|
||||||
|
|
||||||
|
export const baseModel = process.env.MODEL_NAME || 'deepseek-chat'
|
||||||
|
|
||||||
|
export const baseChatCompletionCreateParams: Partial<OpenAI.ChatCompletionCreateParamsNonStreaming> = {
|
||||||
|
max_tokens: 100,
|
||||||
|
// TODO: for use control
|
||||||
|
// presence_penalty: 0,
|
||||||
|
// frequency_penalty: 0,
|
||||||
|
// stream: true
|
||||||
|
}
|
||||||
3
packages/ai/src/index.ts
Normal file
3
packages/ai/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './api'
|
||||||
|
export * from './config'
|
||||||
|
export * from './types'
|
||||||
7
packages/ai/src/types.ts
Normal file
7
packages/ai/src/types.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* AI 食谱
|
||||||
|
*/
|
||||||
|
export interface AIRecipeInfo {
|
||||||
|
名称: string
|
||||||
|
介绍: string
|
||||||
|
}
|
||||||
25
pages/ai.vue
Normal file
25
pages/ai.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'preview',
|
||||||
|
})
|
||||||
|
|
||||||
|
const rStore = useRecipeStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div text-4xl>
|
||||||
|
<button
|
||||||
|
class="cursor-pointer transition active:text-green-800 hover:(text-green-600)"
|
||||||
|
title="重置"
|
||||||
|
@click="rStore.reset"
|
||||||
|
>
|
||||||
|
<div v-if="rStore.selectedStuff.length" i-mdi-pot-steam-outline />
|
||||||
|
<div v-else i-mdi-pot-mix-outline />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p text="sm" m="b-4">
|
||||||
|
好的,今天我们来做菜!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<AiChooseFood />
|
||||||
|
</template>
|
||||||
@@ -137,12 +137,6 @@
|
|||||||
<li>
|
<li>
|
||||||
<a href="https://weibo.com/runny" target="_blank">Runny</a>
|
<a href="https://weibo.com/runny" target="_blank">Runny</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
山竹太凉
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
leo
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
麒麟
|
麒麟
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
12
pages/recipes/collect.vue
Normal file
12
pages/recipes/collect.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'child',
|
||||||
|
title: '我的收藏',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
施工中...
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
38
pages/recipes/history.vue
Normal file
38
pages/recipes/history.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { recipeHistories } from '~/composables/store/history'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'child',
|
||||||
|
title: '历史记录',
|
||||||
|
})
|
||||||
|
|
||||||
|
// todo
|
||||||
|
// clear one history
|
||||||
|
function clearAllHistory() {
|
||||||
|
recipeHistories.value = []
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div pt-2>
|
||||||
|
<div
|
||||||
|
text="blue-900 dark:blue-200"
|
||||||
|
bg="blue-300 op-20 hover:(blue-800 op-20) dark:hover:(blue-200 op-20)"
|
||||||
|
class="inline-flex items-center justify-center border border-transparent rounded-md px-4 py-2 text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
|
||||||
|
@click="clearAllHistory"
|
||||||
|
>
|
||||||
|
<div i-ri-eraser-line />
|
||||||
|
<span class="ml-1">清空记录</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div flex="~ col">
|
||||||
|
<div v-for="history in recipeHistories" :key="history.recipe.name" mt-2>
|
||||||
|
<StapleTag :active="false">
|
||||||
|
{{ dayjs(history.time).format('YYYY-MM-DD HH:mm:ss') }}
|
||||||
|
</StapleTag>
|
||||||
|
<DishTag :dish="history.recipe" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -13,6 +13,12 @@ import { links } from '~/constants'
|
|||||||
px-2
|
px-2
|
||||||
text-left
|
text-left
|
||||||
>
|
>
|
||||||
|
<div mt-2 gap="3" grid="~ cols-3">
|
||||||
|
<YlfIconItem to="/recipes/history" icon="i-ri-history-line" label="历史记录" />
|
||||||
|
<YlfIconItem to="/recipes/collect" icon="i-ri-star-line" label="我的收藏" />
|
||||||
|
<YlfIconItem to="/cookbooks" icon="i-ri-article-line" label="自定义菜谱" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<YlfForm>
|
<YlfForm>
|
||||||
<YlfFormItem icon="i-ri-feedback-line" label="立即反馈" :to="links.feedback" target="_blank" />
|
<YlfFormItem icon="i-ri-feedback-line" label="立即反馈" :to="links.feedback" target="_blank" />
|
||||||
<YlfFormItem icon="i-ri-mail-send-line" label="立即投稿" :to="links.contribute" target="_blank" />
|
<YlfFormItem icon="i-ri-mail-send-line" label="立即投稿" :to="links.contribute" target="_blank" />
|
||||||
@@ -22,9 +28,9 @@ import { links } from '~/constants'
|
|||||||
<YlfFormItem icon="i-ri-settings-line" label="设置" to="/settings" />
|
<YlfFormItem icon="i-ri-settings-line" label="设置" to="/settings" />
|
||||||
</YlfForm>
|
</YlfForm>
|
||||||
|
|
||||||
<YlfForm>
|
<!-- <YlfForm>
|
||||||
<YlfFormItem icon="i-ri-article-line" label="自定义菜谱" to="/cookbooks/" />
|
<YlfFormItem icon="i-ri-article-line" label="自定义菜谱 TODO" to="/cookbooks/" />
|
||||||
</YlfForm>
|
</YlfForm> -->
|
||||||
|
|
||||||
<YlfForm>
|
<YlfForm>
|
||||||
<YlfFormItem icon="i-ri-question-line" label="帮助" to="/help" />
|
<YlfFormItem icon="i-ri-question-line" label="帮助" to="/help" />
|
||||||
|
|||||||
8009
pnpm-lock.yaml
generated
8009
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
packages:
|
||||||
|
- packages/*
|
||||||
3
server/api/README.md
Normal file
3
server/api/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# SD API
|
||||||
|
|
||||||
|
- [SD WebUI API](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/API)
|
||||||
74
server/api/recipes/image/generate.ts
Normal file
74
server/api/recipes/image/generate.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// http://localhost:3001/api/recipes/image/generate
|
||||||
|
|
||||||
|
import process from 'node:process'
|
||||||
|
import { meat, staple, vegetable } from '~/data/food'
|
||||||
|
|
||||||
|
// internal temp
|
||||||
|
// const sdBaseUrl = 'http://30.30.168.63:7860/'
|
||||||
|
const sdBaseUrl = process.env.SD_API_BASE_URL
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
// denoising_strength: 0,
|
||||||
|
prompt: 'food', // 提示词
|
||||||
|
negative_prompt: '', // 反向提示词
|
||||||
|
seed: -1, // 种子,随机数
|
||||||
|
batch_size: 2, // 每次张数
|
||||||
|
n_iter: 1, // 生成批次
|
||||||
|
steps: 50, // 生成步数
|
||||||
|
cfg_scale: 7, // 关键词相关性
|
||||||
|
width: 512, // 宽度
|
||||||
|
height: 512, // 高度
|
||||||
|
restore_faces: false, // 脸部修复
|
||||||
|
tiling: false, // 可平埔
|
||||||
|
// override_settings: {
|
||||||
|
// sd_model_checkpoint: 'wlop-any.ckpt [7331f3bc87]',
|
||||||
|
// }, // 一般用于修改本次的生成图片的stable diffusion 模型,用法需保持一致
|
||||||
|
// script_args: [
|
||||||
|
// 0,
|
||||||
|
// true,
|
||||||
|
// true,
|
||||||
|
// 'LoRA',
|
||||||
|
// 'dingzhenlora_v1(fa7c1732cc95)',
|
||||||
|
// 1,
|
||||||
|
// 1,
|
||||||
|
// ], // 一般用于lora模型或其他插件参数,如示例,我放入了一个lora模型, 1,1为两个权重值,一般只用到前面的权重值1
|
||||||
|
sampler_index: 'Euler', // 采样方法
|
||||||
|
}
|
||||||
|
|
||||||
|
// with api /sdapi/v1/txt2img
|
||||||
|
|
||||||
|
export interface Txt2ImgResponse {
|
||||||
|
images: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const stuffItems = [
|
||||||
|
...vegetable,
|
||||||
|
...meat,
|
||||||
|
...staple,
|
||||||
|
]
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const body = await readBody(e)
|
||||||
|
|
||||||
|
const zhFoods = body.foods as string[]
|
||||||
|
const enFoods = zhFoods.map((food) => {
|
||||||
|
const item = stuffItems.find(item => item.name === food)
|
||||||
|
if (item)
|
||||||
|
return item.en
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: 过滤 prompt 只能是食材
|
||||||
|
// <lora:TODO:1>,
|
||||||
|
payload.prompt = `food focus,transparent background,${enFoods.join(',')}`
|
||||||
|
|
||||||
|
console.log(payload.prompt)
|
||||||
|
|
||||||
|
const data = await $fetch<Txt2ImgResponse>('/sdapi/v1/txt2img', {
|
||||||
|
baseURL: sdBaseUrl,
|
||||||
|
body: payload,
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
console.log(data)
|
||||||
|
return data
|
||||||
|
})
|
||||||
40
server/api/recipes/text/generate.ts
Normal file
40
server/api/recipes/text/generate.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import consola from 'consola'
|
||||||
|
import type { AIRecipeInfo } from '~/packages/ai/src'
|
||||||
|
import { getAIRecipeInfo } from '~/packages/ai/src'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const body = await readBody(event)
|
||||||
|
const zhFoods = body.foods as string[]
|
||||||
|
|
||||||
|
consola.debug(zhFoods)
|
||||||
|
|
||||||
|
const data = await getAIRecipeInfo(zhFoods)
|
||||||
|
const { content } = data
|
||||||
|
|
||||||
|
let unWrapperContent = content || ''
|
||||||
|
const startPos = unWrapperContent.indexOf('{')
|
||||||
|
const endPos = unWrapperContent.lastIndexOf('}')
|
||||||
|
|
||||||
|
if (startPos === -1 || endPos === -1) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
unWrapperContent = unWrapperContent.slice(startPos, endPos + 1)
|
||||||
|
unWrapperContent = (unWrapperContent || '{}')?.replace('```json\n', '').replace('```', '')
|
||||||
|
|
||||||
|
unWrapperContent = unWrapperContent.endsWith('}') ? unWrapperContent : `${unWrapperContent}}`
|
||||||
|
|
||||||
|
let coupletData: AIRecipeInfo | undefined
|
||||||
|
try {
|
||||||
|
coupletData = JSON.parse(unWrapperContent) as AIRecipeInfo
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(content)
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return coupletData
|
||||||
|
})
|
||||||
7
server/api/recipes/text/test.ts
Normal file
7
server/api/recipes/text/test.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const startAt = Date.now()
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
export default defineEventHandler(() => ({
|
||||||
|
pageview: count++,
|
||||||
|
startAt,
|
||||||
|
}))
|
||||||
File diff suppressed because one or more lines are too long
@@ -26,5 +26,7 @@
|
|||||||
// cook custom
|
// cook custom
|
||||||
:root {
|
:root {
|
||||||
--cook-bottom-menu-padding-bottom: 20px;
|
--cook-bottom-menu-padding-bottom: 20px;
|
||||||
--cook-bottom-menu-height: calc(64px + var(--cook-bottom-menu-padding-bottom));
|
--cook-bottom-menu-height: calc(
|
||||||
|
64px + var(--cook-bottom-menu-padding-bottom)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ body,
|
|||||||
background: var(--c-bg);
|
background: var(--c-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
input:focus {
|
input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
@@ -22,7 +21,7 @@ input:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#nprogress .bar {
|
#nprogress .bar {
|
||||||
background: rgb(13,148,136);
|
background: rgb(13, 148, 136);
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1031;
|
z-index: 1031;
|
||||||
@@ -55,11 +54,12 @@ button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {opacity: 0.1;}
|
hr {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
// border: 1px solid var(--c-text);
|
// border: 1px solid var(--c-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ blockquote {
|
|||||||
border-left: 0.25em solid #ddd;
|
border-left: 0.25em solid #ddd;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
color: #777;
|
color: #777;
|
||||||
quotes: '\\201C''\\201D''\\2018''\\2019';
|
quotes: '\\201C' '\\201D' '\\2018' '\\2019';
|
||||||
}
|
}
|
||||||
|
|
||||||
ol {
|
ol {
|
||||||
|
|||||||
@@ -66,4 +66,9 @@ export interface StuffItem {
|
|||||||
* 显示标签
|
* 显示标签
|
||||||
*/
|
*/
|
||||||
label?: string
|
label?: string
|
||||||
|
/**
|
||||||
|
* 英文名称, for ai keyword
|
||||||
|
* @example 'potato'
|
||||||
|
*/
|
||||||
|
en?: string
|
||||||
}
|
}
|
||||||
|
|||||||
0
utils/api/ai.ts
Normal file
0
utils/api/ai.ts
Normal file
24
utils/api/index.ts
Normal file
24
utils/api/index.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// filter prompt
|
||||||
|
export async function generateRecipeImage(foods: string[]) {
|
||||||
|
return $fetch('/api/recipes/image/generate', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
foods,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateRecipeInfo(foods: string[]) {
|
||||||
|
console.log(foods)
|
||||||
|
return $fetch('/api/recipes/text/generate', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
foods,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRecipeImage(foods: string[]) {
|
||||||
|
const data = await generateRecipeImage(foods)
|
||||||
|
return `data:image/png;base64,${data.images[0]}`
|
||||||
|
}
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
import { defineVitestConfig } from 'nuxt-vitest/config'
|
import path from 'node:path'
|
||||||
|
import { defineVitestConfig } from '@nuxt/test-utils/config'
|
||||||
|
|
||||||
export default defineVitestConfig({
|
export default defineVitestConfig({
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'~/': path.resolve(__dirname, './'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
test: {
|
test: {
|
||||||
include: ['test/**/*.test.ts'],
|
include: ['test/**/*.test.ts'],
|
||||||
environment: 'nuxt',
|
environment: 'nuxt',
|
||||||
|
|||||||
Reference in New Issue
Block a user