1
0
mirror of synced 2025-11-06 04:20:50 +08:00

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:
Reina
2025-09-26 18:36:38 +08:00
committed by GitHub
parent cb61ab90fd
commit b13924da4f
10 changed files with 218 additions and 4 deletions

3
.gitignore vendored
View File

@@ -1,5 +1,6 @@
# auto generate
data/recipe.json
app/data/recipe.json
app/data/incompatible-foods.json
.DS_Store
.vite-ssg-dist

View File

@@ -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">
🥬 菜菜们

View 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,
}
}

View File

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

View File

@@ -0,0 +1,6 @@
foodA,foodB,reason
番茄,黄瓜,黄瓜中含有维生素C分解酶会破坏番茄中的维生素C营养流失严重
牛奶,韭菜,牛奶与韭菜同食会影响钙的吸收,降低营养价值
土豆,番茄,土豆会产生大量的盐酸,番茄在较强的酸性环境中会产生不溶于水的沉淀
白萝卜,胡萝卜,白萝卜中的维生素C会被胡萝卜中的抗坏血酸酵素破坏
芹菜,黄瓜,芹菜中的维生素C会被黄瓜中的维生素C分解酶破坏
1 foodA foodB reason
2 番茄 黄瓜 黄瓜中含有维生素C分解酶,会破坏番茄中的维生素C,营养流失严重
3 牛奶 韭菜 牛奶与韭菜同食会影响钙的吸收,降低营养价值
4 土豆 番茄 土豆会产生大量的盐酸,番茄在较强的酸性环境中会产生不溶于水的沉淀
5 白萝卜 胡萝卜 白萝卜中的维生素C会被胡萝卜中的抗坏血酸酵素破坏
6 芹菜 黄瓜 芹菜中的维生素C会被黄瓜中的维生素C分解酶破坏

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
/**
* 食物相克规则
*/
export interface IncompatibleRule {
foodA: string
foodB: string
reason: string
}

View File

@@ -1,2 +1,3 @@
export * from './cookbook'
export * from './incompatible-foods'
export * from './recipe'

View File

@@ -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,
}

View File

@@ -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()