chore: 导入清理前的完整工程状态(含 Capacitor 原生壳,作为回滚备份点)
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
import type { Difficulty, RecommendationMode } from '@yunyoujun/cook'
|
||||
import type { RecipeItem, StuffItem } from '~/types'
|
||||
import { StorageSerializers, useStorage } from '@vueuse/core'
|
||||
import { createRecipeRecommender, hasAvoidedStuff, matchesTaste, uniqueStuff } from '@yunyoujun/cook/recommend'
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||
import { computed, onMounted, ref, shallowRef } from 'vue'
|
||||
import { useIndexedDB } from '~/composables/db'
|
||||
import { defaultPantry, pantryOptions } from '~/data/pantry'
|
||||
import { trackCookEvent } from '~/utils/analytics'
|
||||
import { db } from '../../utils/db'
|
||||
import { useAppStore } from './app'
|
||||
import { customRecipes } from './customRecipe'
|
||||
|
||||
const namespace = 'cook'
|
||||
export type SearchMode = RecommendationMode
|
||||
|
||||
export const useRecipeStore = defineStore('recipe', () => {
|
||||
const { settings } = useAppStore()
|
||||
const indexedDB = useIndexedDB()
|
||||
const keyword = shallowRef('')
|
||||
const curStuff = settings.keepLocalData
|
||||
? useStorage(`${namespace}:stuff`, new Set<string>(), undefined, { serializer: StorageSerializers.set })
|
||||
: ref(new Set<string>())
|
||||
const curTool = settings.keepLocalData ? useStorage(`${namespace}:tool`, '') : shallowRef('')
|
||||
const curMode = settings.keepLocalData ? useStorage<SearchMode>(`${namespace}:mode`, 'loose') : shallowRef<SearchMode>('loose')
|
||||
const pantry = settings.keepLocalData ? useStorage<string[]>(`${namespace}:pantry`, [...defaultPantry]) : ref([...defaultPantry])
|
||||
// 忌口是个人偏好,和常备调料一样跨会话保留,「清空」不会动它
|
||||
const avoid = settings.keepLocalData ? useStorage<string[]>(`${namespace}:avoid`, []) : ref<string[]>([])
|
||||
const noSpicy = settings.keepLocalData ? useStorage(`${namespace}:no-spicy`, false) : shallowRef(false)
|
||||
const methodGroups = settings.keepLocalData ? useStorage<string[]>(`${namespace}:method-groups`, []) : ref<string[]>([])
|
||||
const cuisines = settings.keepLocalData ? useStorage<string[]>(`${namespace}:cuisines`, []) : ref<string[]>([])
|
||||
const difficulty = settings.keepLocalData ? useStorage<Difficulty[]>(`${namespace}:difficulty`, []) : ref<Difficulty[]>([])
|
||||
|
||||
const selectedStuff = computed(() => uniqueStuff([...curStuff.value]))
|
||||
const selectedPantry = computed(() => uniqueStuff(pantry.value).filter(item => pantryOptions.includes(item)))
|
||||
const selectedAvoid = computed(() => uniqueStuff(avoid.value))
|
||||
|
||||
function toggleStuff(name: string) {
|
||||
const names = uniqueStuff([name])
|
||||
const next = new Set(selectedStuff.value)
|
||||
for (const item of names) {
|
||||
if (next.has(item))
|
||||
next.delete(item)
|
||||
else
|
||||
next.add(item)
|
||||
}
|
||||
curStuff.value = next
|
||||
}
|
||||
|
||||
function addStuff(name: string) {
|
||||
curStuff.value = new Set(uniqueStuff([...selectedStuff.value, name]))
|
||||
}
|
||||
|
||||
function togglePantry(name: string) {
|
||||
if (!pantryOptions.includes(name))
|
||||
return
|
||||
pantry.value = selectedPantry.value.includes(name)
|
||||
? selectedPantry.value.filter(item => item !== name)
|
||||
: [...selectedPantry.value, name]
|
||||
}
|
||||
|
||||
function toggleAvoid(name: string) {
|
||||
avoid.value = selectedAvoid.value.includes(name)
|
||||
? selectedAvoid.value.filter(item => item !== name)
|
||||
: [...selectedAvoid.value, name]
|
||||
}
|
||||
|
||||
function toggleMethodGroup(label: string) {
|
||||
methodGroups.value = methodGroups.value.includes(label)
|
||||
? methodGroups.value.filter(item => item !== label)
|
||||
: [...methodGroups.value, label]
|
||||
}
|
||||
|
||||
function toggleCuisine(label: string) {
|
||||
cuisines.value = cuisines.value.includes(label)
|
||||
? cuisines.value.filter(item => item !== label)
|
||||
: [...cuisines.value, label]
|
||||
}
|
||||
|
||||
function toggleDifficulty(level: Difficulty) {
|
||||
difficulty.value = difficulty.value.includes(level)
|
||||
? difficulty.value.filter(item => item !== level)
|
||||
: [...difficulty.value, level]
|
||||
}
|
||||
|
||||
/** 忌口、做法、菜系、口味、难度这几个对所有列表都生效的条件 */
|
||||
function passesPreferences(recipe: RecipeItem) {
|
||||
return !hasAvoidedStuff(recipe, selectedAvoid.value)
|
||||
&& matchesTaste(recipe, { methodGroups: methodGroups.value, cuisines: cuisines.value, noSpicy: noSpicy.value, difficulty: difficulty.value })
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
keyword.value = ''
|
||||
curTool.value = ''
|
||||
curMode.value = 'loose'
|
||||
methodGroups.value = []
|
||||
cuisines.value = []
|
||||
difficulty.value = []
|
||||
}
|
||||
|
||||
function reset() {
|
||||
curStuff.value = new Set()
|
||||
clearFilters()
|
||||
}
|
||||
|
||||
function toggleTools(name: string) {
|
||||
curTool.value = curTool.value === name ? '' : name
|
||||
}
|
||||
|
||||
function setMode(mode: SearchMode) {
|
||||
curMode.value = mode
|
||||
}
|
||||
|
||||
const recipes = shallowRef<RecipeItem[]>([])
|
||||
const isSearching = shallowRef(true)
|
||||
const loadError = shallowRef('')
|
||||
let loading: Promise<void> | undefined
|
||||
function loadRecipes() {
|
||||
if (loading)
|
||||
return loading
|
||||
isSearching.value = true
|
||||
loadError.value = ''
|
||||
loading = (async () => {
|
||||
try {
|
||||
// Await initialization so first visits and saved selections cannot read an empty DB.
|
||||
await indexedDB.init()
|
||||
recipes.value = await db.recipes.toArray()
|
||||
}
|
||||
catch {
|
||||
loadError.value = '菜谱暂时没有加载成功,请重试。'
|
||||
}
|
||||
finally {
|
||||
isSearching.value = false
|
||||
loading = undefined
|
||||
}
|
||||
})()
|
||||
return loading
|
||||
}
|
||||
|
||||
// 自己添加的菜谱排在前面:匹配程度相同时优先推荐
|
||||
const allRecipes = computed<RecipeItem[]>(() => [...customRecipes.value, ...recipes.value])
|
||||
|
||||
const recommend = computed(() => createRecipeRecommender(allRecipes.value))
|
||||
const recipeMatches = computed(() => recommend.value({
|
||||
stuff: selectedStuff.value,
|
||||
pantry: selectedPantry.value,
|
||||
tool: curTool.value,
|
||||
keyword: keyword.value,
|
||||
mode: curMode.value,
|
||||
avoid: selectedAvoid.value,
|
||||
methodGroups: methodGroups.value,
|
||||
cuisines: cuisines.value,
|
||||
noSpicy: noSpicy.value,
|
||||
difficulty: difficulty.value,
|
||||
}))
|
||||
/** 只选了厨具、还没选食材时,列出用这个厨具能做的菜(保留原来经典视图的用法) */
|
||||
const toolRecipes = computed(() => {
|
||||
if (selectedStuff.value.length || !curTool.value)
|
||||
return []
|
||||
const text = keyword.value.trim()
|
||||
return allRecipes.value.filter(recipe => recipe.tools?.includes(curTool.value)
|
||||
&& (!text || recipe.name.includes(text))
|
||||
&& passesPreferences(recipe))
|
||||
})
|
||||
/** 「吃什么」随机的候选:选了食材就从匹配结果里抽,否则按厨具和忌口从全部菜谱里抽 */
|
||||
const randomPool = computed(() => {
|
||||
if (selectedStuff.value.length)
|
||||
return recipeMatches.value.map(match => match.recipe)
|
||||
return allRecipes.value.filter(recipe => (!curTool.value || recipe.tools?.includes(curTool.value))
|
||||
&& passesPreferences(recipe))
|
||||
})
|
||||
/** 「再补一样」建议:差一种的菜里缺得最多的食材,补上后能直接多做几道 */
|
||||
const unlockSuggestions = computed(() => {
|
||||
const counts = new Map<string, number>()
|
||||
for (const match of recipeMatches.value) {
|
||||
const [missing] = match.missingStuff
|
||||
if (match.missingStuff.length === 1 && missing)
|
||||
counts.set(missing, (counts.get(missing) ?? 0) + 1)
|
||||
}
|
||||
return [...counts]
|
||||
.filter(([, count]) => count >= 2)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 4)
|
||||
.map(([name, count]) => ({ name, count }))
|
||||
})
|
||||
const resultCount = computed(() => selectedStuff.value.length ? recipeMatches.value.length : toolRecipes.value.length)
|
||||
const recipesLength = computed(() => allRecipes.value.length)
|
||||
|
||||
function clickTool(item: StuffItem) {
|
||||
toggleTools(item.name)
|
||||
trackCookEvent('click_tool')
|
||||
}
|
||||
|
||||
onMounted(loadRecipes)
|
||||
|
||||
return {
|
||||
recipesLength,
|
||||
keyword,
|
||||
curTool,
|
||||
curMode,
|
||||
selectedStuff,
|
||||
selectedPantry,
|
||||
selectedAvoid,
|
||||
methodGroups,
|
||||
cuisines,
|
||||
noSpicy,
|
||||
difficulty,
|
||||
isSearching,
|
||||
loadError,
|
||||
loadRecipes,
|
||||
clearKeyWord: () => { keyword.value = '' },
|
||||
clearFilters,
|
||||
toggleStuff,
|
||||
togglePantry,
|
||||
clearPantry: () => { pantry.value = [] },
|
||||
resetPantry: () => { pantry.value = [...defaultPantry] },
|
||||
toggleAvoid,
|
||||
clearAvoid: () => { avoid.value = [] },
|
||||
toggleMethodGroup,
|
||||
toggleCuisine,
|
||||
toggleDifficulty,
|
||||
toggleTools,
|
||||
reset,
|
||||
setMode,
|
||||
addStuff,
|
||||
recipeMatches,
|
||||
toolRecipes,
|
||||
randomPool,
|
||||
unlockSuggestions,
|
||||
resultCount,
|
||||
clickTool,
|
||||
}
|
||||
})
|
||||
|
||||
if (import.meta.hot)
|
||||
import.meta.hot.accept(acceptHMRUpdate(useRecipeStore, import.meta.hot))
|
||||
Reference in New Issue
Block a user