refactor: migrate to nuxt
This commit is contained in:
42
composables/animation.ts
Normal file
42
composables/animation.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
25
composables/helper.ts
Normal file
25
composables/helper.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { isClient, useElementBounding } from '@vueuse/core'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
/**
|
||||
* trigger show invisible element
|
||||
* @param target
|
||||
* @returns
|
||||
*/
|
||||
export function useInvisibleElement(target: Ref<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
composables/index.ts
Normal file
3
composables/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './store'
|
||||
|
||||
// others is auto exported
|
||||
81
composables/recipe.ts
Normal file
81
composables/recipe.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { Recipes } from '~/types'
|
||||
|
||||
import type { StuffItem } from '~/data/food'
|
||||
import { useRecipeStore } from '~/composables/store/recipe'
|
||||
|
||||
export function useRecipe(recipe: Recipes) {
|
||||
const gtm = useGtm()
|
||||
|
||||
const rStore = useRecipeStore()
|
||||
const { curMode, curTool } = storeToRefs(rStore)
|
||||
const curStuff = computed(() => rStore.selectedStuff)
|
||||
|
||||
// 默认严格模式
|
||||
const displayedRecipe = computed(() => {
|
||||
// if keyword exist, return result directly
|
||||
const keyword = rStore.keyword
|
||||
if (keyword)
|
||||
return recipe.filter(item => item.name.includes(keyword))
|
||||
|
||||
if (curMode.value === 'strict') {
|
||||
return recipe.filter((item) => {
|
||||
const stuffFlag = curStuff.value.every(stuff => item.stuff.includes(stuff))
|
||||
const toolFlag = item.tools?.includes(curTool.value)
|
||||
return curTool.value ? (stuffFlag && toolFlag) : stuffFlag
|
||||
})
|
||||
}
|
||||
else if (curMode.value === 'loose') {
|
||||
return recipe.filter((item) => {
|
||||
const stuffFlag = curStuff.value.some(stuff => item.stuff.includes(stuff))
|
||||
const toolFlag = item.tools?.includes(curTool.value)
|
||||
|
||||
// 同时存在 厨具和材料,则同时判断
|
||||
if (curTool.value && curStuff.value.length) {
|
||||
return stuffFlag && toolFlag
|
||||
}
|
||||
else {
|
||||
if (curStuff.value.length)
|
||||
return stuffFlag
|
||||
else if (curTool.value)
|
||||
return toolFlag
|
||||
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
// survival
|
||||
else {
|
||||
return recipe.filter((item) => {
|
||||
const stuffFlag = item.stuff.every(stuff => curStuff.value.includes(stuff))
|
||||
const toolFlag = item.tools?.includes(curTool.value)
|
||||
return curTool.value ? (stuffFlag && toolFlag) : stuffFlag
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* toggle tool
|
||||
* @param item
|
||||
*/
|
||||
const clickTool = (item: StuffItem) => {
|
||||
const value = item.name
|
||||
rStore.toggleTools(value)
|
||||
|
||||
gtm?.trackEvent({
|
||||
event: 'click',
|
||||
category: `tool_${value}`,
|
||||
action: 'click_tool',
|
||||
label: '工具',
|
||||
})
|
||||
gtm?.trackEvent({
|
||||
event: 'click_tool',
|
||||
action: item.name,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
displayedRecipe,
|
||||
clickTool,
|
||||
}
|
||||
}
|
||||
12
composables/store/app.ts
Normal file
12
composables/store/app.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
const deferredPrompt = ref<Event | any>()
|
||||
|
||||
return {
|
||||
deferredPrompt,
|
||||
}
|
||||
})
|
||||
|
||||
if (import.meta.hot)
|
||||
import.meta.hot.accept(acceptHMRUpdate(useAppStore, import.meta.hot))
|
||||
3
composables/store/index.ts
Normal file
3
composables/store/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './app'
|
||||
export * from './recipe'
|
||||
export * from './user'
|
||||
105
composables/store/recipe.ts
Normal file
105
composables/store/recipe.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import type { RecipeItem, Recipes } from '~/types'
|
||||
|
||||
import recipeData from '~/data/recipe.json'
|
||||
|
||||
const namespace = 'cook'
|
||||
|
||||
/**
|
||||
* 生成随机菜谱
|
||||
* @param recipes
|
||||
* @returns
|
||||
*/
|
||||
function generateRandomRecipe(recipes: Recipes) {
|
||||
return recipes[Math.floor(Math.random() * recipes.length)]
|
||||
}
|
||||
|
||||
/**
|
||||
* survival: 生存模式
|
||||
* strict: 严格
|
||||
* loose: 模糊
|
||||
*/
|
||||
export type SearchMode = 'survival' | 'loose' | 'strict'
|
||||
|
||||
export const useRecipeStore = defineStore('recipe', () => {
|
||||
const recipes = recipeData as Recipes
|
||||
|
||||
/**
|
||||
* 搜索关键字
|
||||
*/
|
||||
const keyword = ref('')
|
||||
|
||||
// can not exported
|
||||
const curStuff = useStorage(`${namespace}:stuff`, new Set<string>())
|
||||
// const curTools = ref(new Set<string>())
|
||||
const curTool = useStorage(`${namespace}:tool`, '')
|
||||
|
||||
const selectedStuff = computed(() => Array.from(curStuff.value))
|
||||
// const selectedTools = computed(() => Array.from(curTools.value))
|
||||
// const selectedTools = ref('')
|
||||
|
||||
const curMode = useStorage<SearchMode>(`${namespace}:mode`, 'loose')
|
||||
|
||||
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 randomRecipe = ref<RecipeItem>(generateRandomRecipe(recipes))
|
||||
|
||||
return {
|
||||
recipes,
|
||||
|
||||
keyword,
|
||||
curTool,
|
||||
curMode,
|
||||
selectedStuff,
|
||||
|
||||
randomRecipe,
|
||||
random: () => { randomRecipe.value = generateRandomRecipe(recipes) },
|
||||
|
||||
clearKeyWord: () => { keyword.value = '' },
|
||||
toggleStuff,
|
||||
toggleTools,
|
||||
reset,
|
||||
setMode,
|
||||
|
||||
addStuff,
|
||||
}
|
||||
})
|
||||
|
||||
if (import.meta.hot)
|
||||
import.meta.hot.accept(acceptHMRUpdate(useRecipeStore, import.meta.hot))
|
||||
34
composables/store/user.ts
Normal file
34
composables/store/user.ts
Normal 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))
|
||||
Reference in New Issue
Block a user