feat: Implement warnings for incompatible food combinations (#82)
* feat: 添加食物相克检测功能及相关数据支持 * chore: 修改 .gitignore 设置,并删除 recipe.json 的 git 引用 * style: 格式化 incompatible-foods.ts 文件中的代码,修改缩紧为 2 * fix: 修复导入语句,确保正确使用 Vue 的 ref 和 computed * refactor: 优化相克检测逻辑,使用 Set 提高性能 * refactor: 移除不必要的 IncompatibleRule 类型定义 * fix: 添加缺失字段检查,确保不完整数据不会被处理 * style: 使用 eslint 优化代码格式
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
# auto generate
|
||||
data/recipe.json
|
||||
app/data/recipe.json
|
||||
app/data/incompatible-foods.json
|
||||
|
||||
.DS_Store
|
||||
.vite-ssg-dist
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { StuffItem } from '~/types'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useEmojiAnimation } from '~/composables/animation'
|
||||
import { useIncompatibleFoods } from '~/composables/incompatible-foods'
|
||||
|
||||
import { meat, staple, tools, vegetable } from '~/data/food'
|
||||
|
||||
@@ -9,6 +10,9 @@ const rStore = useRecipeStore()
|
||||
const { curTool } = storeToRefs(rStore)
|
||||
const curStuff = computed(() => rStore.selectedStuff)
|
||||
|
||||
// 食物相克检测
|
||||
const { warningMessage, hasWarning, checkIncompatibility } = useIncompatibleFoods()
|
||||
|
||||
const recipeBtnRef = ref<HTMLButtonElement>()
|
||||
const { playAnimation } = useEmojiAnimation(recipeBtnRef)
|
||||
|
||||
@@ -17,6 +21,18 @@ const { proxy } = useScriptGoogleTagManager()
|
||||
const recipePanelRef = ref()
|
||||
const { isVisible, show } = useInvisibleElement(recipePanelRef)
|
||||
|
||||
// 监听食材变化,自动检测相克
|
||||
watch(curStuff, (newIngredients) => {
|
||||
checkIncompatibility(newIngredients)
|
||||
}, { deep: true })
|
||||
|
||||
// 页面初始化时也检查一次(处理已有选择的情况)
|
||||
onMounted(() => {
|
||||
if (curStuff.value.length > 0) {
|
||||
checkIncompatibility(curStuff.value)
|
||||
}
|
||||
})
|
||||
|
||||
function toggleStuff(item: StuffItem, category = '', _e?: Event) {
|
||||
rStore.toggleStuff(item.name)
|
||||
|
||||
@@ -41,6 +57,35 @@ function toggleStuff(item: StuffItem, category = '', _e?: Event) {
|
||||
<h2 m="t-4" text="xl" font="bold" p="1">
|
||||
🥘 先选一下食材
|
||||
</h2>
|
||||
|
||||
<!-- 食物相克警告提示 -->
|
||||
<Transition name="incompatible-warning">
|
||||
<div
|
||||
v-if="hasWarning"
|
||||
class="incompatible-warning-box"
|
||||
m="b-4" p="4"
|
||||
border="~ 2 red-300 dark:red-600 rounded-xl"
|
||||
text="red-800 dark:red-200 sm"
|
||||
shadow="lg"
|
||||
relative="~"
|
||||
overflow="hidden"
|
||||
>
|
||||
<div flex="~ items-start gap-3">
|
||||
<div text="2xl" flex="shrink-0" class="animate-pulse">
|
||||
🚨
|
||||
</div>
|
||||
<div flex="1 col gap-1">
|
||||
<div font="bold" text="base">
|
||||
食物相克警告!
|
||||
</div>
|
||||
<div leading="relaxed" whitespace="pre-line">
|
||||
{{ warningMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<div>
|
||||
<h2 opacity="90" text="base" font="bold" p="1">
|
||||
🥬 菜菜们
|
||||
|
||||
88
app/composables/incompatible-foods.ts
Normal file
88
app/composables/incompatible-foods.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { IncompatibleRule } from '~/types'
|
||||
import { computed, onMounted, readonly, ref } from 'vue'
|
||||
import incompatibleFoodsData from '~/data/incompatible-foods.json'
|
||||
|
||||
/**
|
||||
* 食物相克检测 composable
|
||||
*/
|
||||
export function useIncompatibleFoods() {
|
||||
// 用于存储从 JSON 加载的相克规则
|
||||
const incompatibleRules = ref<IncompatibleRule[]>([])
|
||||
// 用于存储并显示给用户的警告信息
|
||||
const warningMessage = ref<string>('')
|
||||
// 加载状态
|
||||
const isLoading = ref(true)
|
||||
|
||||
/**
|
||||
* 在组件挂载后,加载食物相克数据
|
||||
*/
|
||||
onMounted(() => {
|
||||
try {
|
||||
// 直接使用导入的数据
|
||||
incompatibleRules.value = incompatibleFoodsData as IncompatibleRule[]
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to load incompatible foods data:', error)
|
||||
}
|
||||
finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 核心检测函数:检查当前选择的食材是否存在相克组合
|
||||
* @param ingredients - 当前已选的食材列表
|
||||
*/
|
||||
const checkIncompatibility = (ingredients: string[]) => {
|
||||
// 重置警告信息
|
||||
warningMessage.value = ''
|
||||
|
||||
// 如果食材少于2个或规则还没加载完成,无需检测
|
||||
if (ingredients.length < 2 || isLoading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const foundRules: IncompatibleRule[] = []
|
||||
|
||||
const ingredientSet = new Set(ingredients)
|
||||
|
||||
for (const rule of incompatibleRules.value) {
|
||||
// 检查规则中的两种食物是否都存在于我们的食材 Set 中
|
||||
if (ingredientSet.has(rule.foodA) && ingredientSet.has(rule.foodB)) {
|
||||
foundRules.push(rule)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到相克组合,生成警告信息
|
||||
if (foundRules.length > 0) {
|
||||
if (foundRules.length === 1) {
|
||||
const rule = foundRules[0]!
|
||||
warningMessage.value
|
||||
= `🚨 危险组合!\n`
|
||||
+ `【${rule.foodA}】+ 【${rule.foodB}】= 有毒?!\n`
|
||||
+ `${rule.reason}\n`
|
||||
+ `换个搭配会更安全哦~`
|
||||
}
|
||||
else {
|
||||
const warnings = foundRules.map(rule =>
|
||||
`【${rule.foodA}】+ 【${rule.foodB}】(${rule.reason})\n`,
|
||||
).join('')
|
||||
warningMessage.value
|
||||
= `🚨 发现 ${foundRules.length} 个危险组合!\n`
|
||||
+ `${warnings}`
|
||||
+ `建议调整搭配哦~`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算属性:是否有警告信息
|
||||
const hasWarning = computed(() => Boolean(warningMessage.value))
|
||||
|
||||
return {
|
||||
incompatibleRules: readonly(incompatibleRules),
|
||||
warningMessage: readonly(warningMessage),
|
||||
hasWarning,
|
||||
isLoading: readonly(isLoading),
|
||||
checkIncompatibility,
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './incompatible-foods'
|
||||
export * from './store'
|
||||
|
||||
// others is auto exported
|
||||
|
||||
6
app/data/incompatible-foods.csv
Normal file
6
app/data/incompatible-foods.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
foodA,foodB,reason
|
||||
番茄,黄瓜,黄瓜中含有维生素C分解酶,会破坏番茄中的维生素C,营养流失严重
|
||||
牛奶,韭菜,牛奶与韭菜同食会影响钙的吸收,降低营养价值
|
||||
土豆,番茄,土豆会产生大量的盐酸,番茄在较强的酸性环境中会产生不溶于水的沉淀
|
||||
白萝卜,胡萝卜,白萝卜中的维生素C会被胡萝卜中的抗坏血酸酵素破坏
|
||||
芹菜,黄瓜,芹菜中的维生素C会被黄瓜中的维生素C分解酶破坏
|
||||
|
File diff suppressed because one or more lines are too long
8
app/types/incompatible-foods.ts
Normal file
8
app/types/incompatible-foods.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 食物相克规则
|
||||
*/
|
||||
export interface IncompatibleRule {
|
||||
foodA: string
|
||||
foodB: string
|
||||
reason: string
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './cookbook'
|
||||
export * from './incompatible-foods'
|
||||
export * from './recipe'
|
||||
|
||||
@@ -4,8 +4,12 @@ import url from 'node:url'
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||
const recipeCsvFile = path.resolve(__dirname, '../app/data/recipe.csv')
|
||||
const recipeJsonFile = path.resolve(__dirname, '../app/data/recipe.json')
|
||||
const incompatibleFoodsCsvFile = path.resolve(__dirname, '../app/data/incompatible-foods.csv')
|
||||
const incompatibleFoodsJsonFile = path.resolve(__dirname, '../app/data/incompatible-foods.json')
|
||||
|
||||
export const config = {
|
||||
recipeCsvFile,
|
||||
recipeJsonFile,
|
||||
incompatibleFoodsCsvFile,
|
||||
incompatibleFoodsJsonFile,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { RecipeItem, Recipes } from '../app/types'
|
||||
import type { IncompatibleRule, RecipeItem, Recipes } from '../app/types'
|
||||
// convert csv to json
|
||||
import fs from 'node:fs'
|
||||
import consola from 'consola'
|
||||
@@ -48,4 +48,65 @@ function run() {
|
||||
consola.success(`Generate file: ${config.recipeJsonFile}`)
|
||||
}
|
||||
|
||||
run()
|
||||
/**
|
||||
* 转换食物相克数据
|
||||
*/
|
||||
function convertIncompatibleFoods() {
|
||||
consola.info('---')
|
||||
consola.info('Convert Incompatible Foods Data...')
|
||||
|
||||
try {
|
||||
const csvData = fs.readFileSync(config.incompatibleFoodsCsvFile, 'utf-8')
|
||||
const lines = csvData.split(/\r?\n/)
|
||||
|
||||
const headers = 'foodA,foodB,reason'
|
||||
if (lines.length < 2) {
|
||||
throw new Error('No data in incompatible foods csv file')
|
||||
}
|
||||
|
||||
if (lines[0]?.trim() !== headers) {
|
||||
consola.warn(`Headers Changed: ${lines[0]}`)
|
||||
return
|
||||
}
|
||||
|
||||
const incompatibleRules: IncompatibleRule[] = []
|
||||
|
||||
lines.slice(1).forEach((line) => {
|
||||
if (line.trim()) {
|
||||
const attrs = line.split(',')
|
||||
if (attrs.length < 3) {
|
||||
consola.warn(`Invalid line: ${line}`)
|
||||
return
|
||||
}
|
||||
|
||||
const foodA = attrs[0]?.trim()
|
||||
const foodB = attrs[1]?.trim()
|
||||
const reason = attrs[2]?.trim()
|
||||
|
||||
if (!foodA || !foodB || !reason) {
|
||||
consola.warn(`Missing required field(s) in line: ${line}`)
|
||||
return
|
||||
}
|
||||
|
||||
incompatibleRules.push({
|
||||
foodA,
|
||||
foodB,
|
||||
reason,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
fs.writeFileSync(config.incompatibleFoodsJsonFile, JSON.stringify(incompatibleRules, null, 2))
|
||||
consola.success(`Generate file: ${config.incompatibleFoodsJsonFile}`)
|
||||
}
|
||||
catch (error) {
|
||||
consola.error('Failed to convert incompatible foods data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
run()
|
||||
convertIncompatibleFoods()
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user