Files
pig-farm-controller-fe/src/components/feed/RecipeCompareDialog.vue

331 lines
12 KiB
Vue
Raw Normal View History

2025-11-27 20:35:16 +08:00
<template>
<el-dialog
:model-value="visible"
title="配方对比"
@close="handleClose"
width="80%"
top="5vh"
>
<el-form :inline="true" class="compare-form">
<el-form-item label="对比类型">
2025-11-27 20:40:01 +08:00
<el-select v-model="compareType" placeholder="请选择对比类型" style="width: 200px;">
2025-11-27 20:35:16 +08:00
<el-option label="配方" value="recipe"></el-option>
<el-option label="猪的营养需求" value="pigType"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="compareType === 'recipe'" label="选择配方">
2025-11-27 20:40:01 +08:00
<el-select v-model="selectedCompareRecipeId" filterable placeholder="请选择对比配方" style="width: 300px;">
2025-11-27 20:35:16 +08:00
<el-option
v-for="item in recipeList"
:key="item.id"
:label="item.name"
:value="item.id"
:disabled="item.id === currentRecipe.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="compareType === 'pigType'" label="选择猪类型">
2025-11-27 20:40:01 +08:00
<el-select v-model="selectedPigTypeId" filterable placeholder="请选择猪类型" style="width: 300px;">
2025-11-27 20:35:16 +08:00
<el-option
v-for="item in pigTypeList"
:key="item.id"
:label="item.breed_name + ' - ' + item.age_stage_name"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="startCompare" :disabled="!canCompare">开始对比</el-button>
</el-form-item>
</el-form>
<el-divider />
<div v-if="comparing" class="loading-spinner">
<el-skeleton :rows="5" animated />
</div>
<div v-else-if="compareError" class="error-message">
<el-alert
title="对比数据加载失败"
:description="compareError"
type="error"
show-icon
@close="compareError = null"
/>
</div>
<div v-else-if="compareResult.length > 0">
<el-table :data="compareResult" style="width: 100%" border>
<el-table-column prop="nutrientName" label="营养素" width="150" fixed />
<el-table-column :label="currentRecipe.name" width="150">
<template #default="scope">
{{ scope.row.currentRecipeValue !== undefined ? scope.row.currentRecipeValue.toFixed(2) : '-' }}
</template>
</el-table-column>
<template v-if="compareType === 'recipe'">
<el-table-column :label="compareRecipeName" width="150">
<template #default="scope">
<span :style="{ color: scope.row.compareRecipeValue > scope.row.currentRecipeValue ? 'green' : 'inherit' }">
{{ scope.row.compareRecipeValue !== undefined ? scope.row.compareRecipeValue.toFixed(2) : '-' }}
</span>
</template>
</el-table-column>
</template>
<template v-else-if="compareType === 'pigType'">
<el-table-column :label="pigTypeName + ' (下限)'" width="150">
<template #default="scope">
{{ scope.row.minRequirement !== undefined ? scope.row.minRequirement.toFixed(2) : '-' }}
</template>
</el-table-column>
<el-table-column :label="pigTypeName + ' (上限)'" width="150">
<template #default="scope">
{{ scope.row.maxRequirement !== undefined ? scope.row.maxRequirement.toFixed(2) : '-' }}
</template>
</el-table-column>
<el-table-column label="是否达标" width="100" align="center">
<template #default="scope">
<el-icon v-if="scope.row.isMet" color="green"><Check /></el-icon>
<el-icon v-else color="red"><Close /></el-icon>
</template>
</el-table-column>
</template>
</el-table>
</div>
<div v-else class="no-data-message">
<el-empty description="请选择对比项并点击开始对比" />
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
</span>
</template>
</el-dialog>
</template>
<script>
import { ref, watch, computed } from 'vue';
import { getRecipes, getRecipeById, getPigTypes, getPigTypeById, getRawMaterialById } from '../../api/feed';
import { ElMessage } from 'element-plus';
import { Check, Close } from '@element-plus/icons-vue';
export default {
name: 'RecipeCompareDialog',
components: {
Check,
Close,
},
props: {
visible: Boolean,
currentRecipe: {
type: Object,
required: true,
},
},
emits: ['update:visible'],
setup(props, { emit }) {
const compareType = ref('recipe'); // 'recipe' or 'pigType'
const recipeList = ref([]);
const selectedCompareRecipeId = ref(null);
const pigTypeList = ref([]);
const selectedPigTypeId = ref(null);
const comparing = ref(false);
const compareError = ref(null);
const compareResult = ref([]);
const compareRecipeName = ref('');
const pigTypeName = ref('');
const handleClose = () => {
emit('update:visible', false);
// 重置状态
compareType.value = 'recipe';
selectedCompareRecipeId.value = null;
selectedPigTypeId.value = null;
compareResult.value = [];
compareError.value = null;
compareRecipeName.value = '';
pigTypeName.value = '';
};
const canCompare = computed(() => {
if (compareType.value === 'recipe') {
return selectedCompareRecipeId.value !== null;
} else if (compareType.value === 'pigType') {
return selectedPigTypeId.value !== null;
}
return false;
});
// 获取所有配方列表
const fetchRecipeList = async () => {
try {
const response = await getRecipes({ page_size: 999 });
recipeList.value = response.data.list || [];
} catch (err) {
ElMessage.error('获取配方列表失败: ' + (err.message || '未知错误'));
}
};
// 获取所有猪类型列表
const fetchPigTypeList = async () => {
try {
const response = await getPigTypes({ page: 1, page_size: 999 });
pigTypeList.value = response.data.list || [];
} catch (err) {
ElMessage.error('获取猪类型列表失败: ' + (err.message || '未知错误'));
}
};
// 计算配方的营养成分汇总
const calculateRecipeNutrientSummary = async (recipe) => {
const summary = new Map();
if (!recipe || !recipe.recipe_ingredients || recipe.recipe_ingredients.length === 0) {
return summary;
}
const rawMaterialPromises = recipe.recipe_ingredients.map(ing => getRawMaterialById(ing.raw_material_id));
const rawMaterialResponses = await Promise.all(rawMaterialPromises);
const ingredientDetails = rawMaterialResponses.map((res, index) => ({
...res.data,
percentage: recipe.recipe_ingredients[index].percentage,
}));
ingredientDetails.forEach(ing => {
if (ing.raw_material_nutrients) {
ing.raw_material_nutrients.forEach(nutrient => {
const contribution = nutrient.value * (ing.percentage / 100);
if (summary.has(nutrient.nutrient_name)) {
summary.set(nutrient.nutrient_name, summary.get(nutrient.nutrient_name) + contribution);
} else {
summary.set(nutrient.nutrient_name, contribution);
}
});
}
});
return summary;
};
const startCompare = async () => {
comparing.value = true;
compareError.value = null;
compareResult.value = [];
try {
const currentRecipeNutrients = await calculateRecipeNutrientSummary(props.currentRecipe);
if (compareType.value === 'recipe') {
const compareRecipe = recipeList.value.find(r => r.id === selectedCompareRecipeId.value);
if (!compareRecipe) {
throw new Error('未找到对比配方');
}
compareRecipeName.value = compareRecipe.name;
const otherRecipeDetails = await getRecipeById(selectedCompareRecipeId.value);
const otherRecipeNutrients = await calculateRecipeNutrientSummary(otherRecipeDetails.data);
const allNutrientNames = new Set([...currentRecipeNutrients.keys(), ...otherRecipeNutrients.keys()]);
allNutrientNames.forEach(name => {
const currentRecipeValueRaw = currentRecipeNutrients.has(name) ? currentRecipeNutrients.get(name) : undefined;
const compareRecipeValueRaw = otherRecipeNutrients.has(name) ? otherRecipeNutrients.get(name) : undefined;
const currentRecipeValue = currentRecipeValueRaw !== undefined ? parseFloat(currentRecipeValueRaw.toFixed(2)) : undefined;
const compareRecipeValue = compareRecipeValueRaw !== undefined ? parseFloat(compareRecipeValueRaw.toFixed(2)) : undefined;
compareResult.value.push({
nutrientName: name,
currentRecipeValue,
compareRecipeValue,
});
});
} else if (compareType.value === 'pigType') {
const pigType = pigTypeList.value.find(p => p.id === selectedPigTypeId.value);
if (!pigType) {
throw new Error('未找到猪类型');
}
pigTypeName.value = `${pigType.breed_name} - ${pigType.age_stage_name}`;
const pigTypeDetails = await getPigTypeById(selectedPigTypeId.value);
const pigNutrientRequirements = new Map();
pigTypeDetails.data.pig_nutrient_requirements.forEach(req => {
pigNutrientRequirements.set(req.nutrient_name, {
min: req.min_requirement,
max: req.max_requirement,
});
});
const allNutrientNames = new Set([...currentRecipeNutrients.keys(), ...pigNutrientRequirements.keys()]);
allNutrientNames.forEach(name => {
const currentRecipeValueRaw = currentRecipeNutrients.has(name) ? currentRecipeNutrients.get(name) : undefined;
const requirementRaw = pigNutrientRequirements.has(name) ? pigNutrientRequirements.get(name) : { min: undefined, max: undefined };
const currentRecipeValue = currentRecipeValueRaw !== undefined ? parseFloat(currentRecipeValueRaw.toFixed(2)) : undefined;
const minRequirement = requirementRaw.min !== undefined ? parseFloat(requirementRaw.min.toFixed(2)) : undefined;
const maxRequirement = requirementRaw.max !== undefined ? parseFloat(requirementRaw.max.toFixed(2)) : undefined;
const isMet = (minRequirement === undefined && maxRequirement === undefined) || // 如果猪没有这个营养素的需求,则认为达标
(currentRecipeValue !== undefined &&
(minRequirement === undefined || currentRecipeValue >= minRequirement) &&
(maxRequirement === undefined || currentRecipeValue <= maxRequirement));
compareResult.value.push({
nutrientName: name,
currentRecipeValue,
minRequirement: minRequirement,
maxRequirement: maxRequirement,
isMet: isMet,
});
});
}
} catch (err) {
console.error("对比失败:", err);
compareError.value = err.message || '未知错误';
ElMessage.error('对比失败: ' + (err.message || '未知错误'));
} finally {
comparing.value = false;
}
};
watch(() => props.visible, (newVal) => {
if (newVal) {
fetchRecipeList();
fetchPigTypeList();
}
}, { immediate: true });
return {
compareType,
recipeList,
selectedCompareRecipeId,
pigTypeList,
selectedPigTypeId,
comparing,
compareError,
compareResult,
compareRecipeName,
pigTypeName,
handleClose,
canCompare,
startCompare,
};
},
};
</script>
<style scoped>
.compare-form {
margin-bottom: 20px;
}
.loading-spinner, .error-message, .no-data-message {
padding: 20px;
text-align: center;
}
.dialog-footer {
text-align: right;
}
</style>