refactor: use nuxt compatiable 4 folder

This commit is contained in:
YunYouJun
2024-09-15 18:07:50 +08:00
parent 7a52b024dd
commit 41bdc3346f
96 changed files with 2577 additions and 2673 deletions

View File

@@ -0,0 +1,42 @@
import { isClient } from '@vueuse/core'
import type { Ref } from 'vue'
export function useEmojiAnimation(recipeBtn: Ref<HTMLButtonElement | undefined>) {
const { x, y } = usePointer()
const { top, left } = useElementBounding(recipeBtn)
const playAnimation = (emoji: string) => {
if (!isClient)
return
// 单个 Vue 组件实现不适合创建多个元素和清除动画
const emojiEl = document.createElement('span')
emojiEl.style.position = 'fixed'
emojiEl.style.left = `${x.value}px`
emojiEl.style.top = `${y.value}px`
emojiEl.style.zIndex = '10'
emojiEl.style.transition = 'left .4s linear, top .4s cubic-bezier(0.5, -0.5, 1, 1)'
emojiEl.textContent = emoji
document.body.appendChild(emojiEl)
setTimeout(() => {
// 以防万一,按钮位置没检测出来,就不播放动画了
if (!top.value || !left.value) {
emojiEl.style.top = `${x.value}px`
emojiEl.style.left = `${y.value}px`
}
else {
emojiEl.style.top = `${top.value}px`
emojiEl.style.left = `${left.value + 12}px`
}
}, 1)
emojiEl.ontransitionend = () => {
emojiEl.remove()
}
}
return {
playAnimation,
}
}

20
app/composables/count.ts Normal file
View File

@@ -0,0 +1,20 @@
import { useStorage } from '@vueuse/core'
export function useCount() {
const count = useStorage('count', 5)
function inc() {
count.value += 1
}
function dec() {
if (count.value <= 1)
return
count.value -= 1
}
return {
count,
inc,
dec,
}
}

17
app/composables/db.ts Normal file
View File

@@ -0,0 +1,17 @@
import { useStorage } from '@vueuse/core'
import { lastDbUpdated, namespace } from '~/constants'
import { db, initDb } from '~/utils/db'
export function useIndexedDB() {
const dbUpdated = useStorage(`${namespace}:lastDbUpdated`, lastDbUpdated)
return {
init: async () => {
const count = await db.recipes.count()
if (!count || dbUpdated.value !== lastDbUpdated) {
await initDb()
dbUpdated.value = lastDbUpdated
}
},
}
}

24
app/composables/helper.ts Normal file
View File

@@ -0,0 +1,24 @@
import type { MaybeComputedElementRef } from '@vueuse/core'
import { isClient, useElementBounding } from '@vueuse/core'
/**
* trigger show invisible element
* @param target
*/
export function useInvisibleElement(target: MaybeComputedElementRef<HTMLElement>) {
const { top } = useElementBounding(target)
const isVisible = computed(() => {
return isClient ? window.scrollY < top.value : true
})
const show = () => {
if (isClient)
window.scrollTo(0, top.value)
}
return {
isVisible,
show,
}
}

3
app/composables/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './store'
// others is auto exported

34
app/composables/recipe.ts Normal file
View File

@@ -0,0 +1,34 @@
import { useStorage } from '@vueuse/core'
import { namespace } from '~/constants'
import type { DbRecipeItem } from '~/utils/db'
/**
* 随机几道菜
* @param total
*/
export function useRandomRecipe(total: Ref<number>) {
const randomRecipes = useStorage<(DbRecipeItem | undefined)[]>(`${namespace}:random:recipes`, [])
async function random() {
const length = await db.recipes.count()
const randomArr = generateRandomArray(length, total.value)
const result = await db.recipes.bulkGet(randomArr)
if (result)
randomRecipes.value = result.filter(item => !!item)
}
watch(total, () => {
random()
})
onMounted(() => {
// 如果没有随机菜谱,就生成一次
if (randomRecipes.value.length <= 0)
random()
})
return {
random,
randomRecipes,
}
}

View File

@@ -0,0 +1,19 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
import { useStorage } from '@vueuse/core'
import { ref } from 'vue'
import { defaultSettings } from '../../utils/settings'
import { namespace } from '../../constants'
export const useAppStore = defineStore('app', () => {
const deferredPrompt = ref<Event | any>()
const settings = useStorage(`${namespace}:settings`, defaultSettings)
return {
deferredPrompt,
settings,
}
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useAppStore, import.meta.hot))

View 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`, [])

View File

@@ -0,0 +1,3 @@
export * from './app'
export * from './recipe'
export * from './user'

View File

@@ -0,0 +1,185 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
import { useStorage } from '@vueuse/core'
import { computed, onMounted, ref, watch } from 'vue'
import { useGtm } from '@gtm-support/vue-gtm'
import { db } from '../../utils/db'
import { useAppStore } from './app'
import type { RecipeItem, StuffItem } from '~/types'
const namespace = 'cook'
/**
* survival: 生存模式
* strict: 严格
* loose: 模糊
*/
export type SearchMode = 'survival' | 'loose' | 'strict'
export const useRecipeStore = defineStore('recipe', () => {
const gtm = useGtm()
const { settings } = useAppStore()
/**
* 搜索关键字
*/
const keyword = ref('')
// can not exported
const curStuff = settings.keepLocalData ? useStorage(`${namespace}:stuff`, new Set<string>()) : ref(new Set<string>())
// const curTools = ref(new Set<string>())
const curTool = settings.keepLocalData ? useStorage(`${namespace}:tool`, '') : ref('')
const curMode = settings.keepLocalData ? useStorage<SearchMode>(`${namespace}:mode`, 'loose') : ref<SearchMode>('loose')
const selectedStuff = computed(() => Array.from(curStuff.value))
// const selectedTools = computed(() => Array.from(curTools.value))
// const selectedTools = ref('')
function toggleStuff(name: string) {
if (!curStuff.value)
return
if (curStuff.value.has(name))
curStuff.value.delete(name)
else
curStuff.value.add(name)
}
function toggleTools(name: string) {
if (curTool.value === name)
curTool.value = ''
else
curTool.value = name
// if (curTools.value.has(name))
// curTools.value.delete(name)
// else
// curTools.value.add(name)
}
function setMode(mode: SearchMode) {
curMode.value = mode
}
/**
* 重置
*/
function reset() {
curStuff.value.clear()
// curTools.value.clear()
curTool.value = ''
}
function addStuff(name: string) {
curStuff.value.add(name)
}
const isSearching = ref(false)
/**
* 搜索菜谱
*/
async function searchRecipes() {
isSearching.value = true
let result: RecipeItem[] = []
if (curMode.value === 'strict') {
result = await db.recipes.filter((item) => {
const stuffFlag = selectedStuff.value.every(stuff => item.stuff.includes(stuff))
const toolFlag = item.tools.includes(curTool.value)
return curTool.value ? (stuffFlag && toolFlag) : stuffFlag
}).toArray()
}
else if (curMode.value === 'loose') {
result = await db.recipes.filter((item) => {
const stuffFlag = selectedStuff.value.some(stuff => item.stuff.includes(stuff))
const toolFlag = Boolean(item.tools?.includes(curTool.value))
// 同时存在 厨具和材料,则同时判断
if (curTool.value && selectedStuff.value.length) {
return stuffFlag && toolFlag
}
else {
if (selectedStuff.value.length)
return stuffFlag
else if (curTool.value)
return toolFlag
return false
}
}).toArray()
}
// survival
else {
result = await db.recipes.filter((item) => {
const stuffFlag = item.stuff.every(stuff => selectedStuff.value.includes(stuff))
const toolFlag = item.tools?.includes(curTool.value)
return Boolean(curTool.value ? (stuffFlag && toolFlag) : stuffFlag)
}).toArray()
}
if (keyword.value)
result = result.filter(item => item.name.includes(keyword.value))
isSearching.value = false
return result
}
// 默认严格模式
const displayedRecipe = ref<RecipeItem[]>([])
// fix curStuff watch
watch(() => [keyword.value, selectedStuff.value, curTool.value, curMode.value], async () => {
displayedRecipe.value = [...(await searchRecipes())]
})
/**
* toggle tool
* @param item
*/
const clickTool = (item: StuffItem) => {
const value = item.name
toggleTools(value)
gtm?.trackEvent({
event: 'click',
category: `tool_${value}`,
action: 'click_tool',
label: '工具',
})
gtm?.trackEvent({
event: 'click_tool',
action: item.name,
})
}
const recipesLength = ref(0)
onMounted(async () => {
db.recipes.count().then((count) => {
recipesLength.value = count
})
displayedRecipe.value = await searchRecipes()
})
return {
recipesLength,
keyword,
curTool,
curMode,
selectedStuff,
isSearching,
clearKeyWord: () => { keyword.value = '' },
toggleStuff,
toggleTools,
reset,
setMode,
addStuff,
// useRecipe
displayedRecipe,
clickTool,
}
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useRecipeStore, import.meta.hot))

View File

@@ -0,0 +1,34 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
/**
* Current name of the user.
*/
const savedName = ref('')
const previousNames = ref(new Set<string>())
const usedNames = computed(() => Array.from(previousNames.value))
const otherNames = computed(() => usedNames.value.filter(name => name !== savedName.value))
/**
* Changes the current name of the user and saves the one that was used
* before.
*
* @param name - new name to set
*/
function setNewName(name: string) {
if (savedName.value)
previousNames.value.add(savedName.value)
savedName.value = name
}
return {
setNewName,
otherNames,
savedName,
}
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot))