359 lines
13 KiB
Vue
359 lines
13 KiB
Vue
<template>
|
|
<el-dialog
|
|
:model-value="visible"
|
|
title="配方对比"
|
|
@close="handleClose"
|
|
width="80%"
|
|
top="5vh"
|
|
>
|
|
<el-form :inline="true" class="compare-form">
|
|
<el-form-item label="对比类型">
|
|
<el-select v-model="compareType" placeholder="请选择对比类型" style="width: 200px;">
|
|
<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="选择配方">
|
|
<el-select v-model="selectedCompareRecipeId" filterable placeholder="请选择对比配方" style="width: 300px;">
|
|
<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="选择猪类型">
|
|
<el-select v-model="selectedPigTypeId" filterable placeholder="请选择猪类型" style="width: 300px;">
|
|
<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" class="table-wrapper">
|
|
<el-table :data="compareResult" style="width: 100%" border>
|
|
<el-table-column label="营养素" fixed>
|
|
<template #default="scope">
|
|
<el-tooltip :content="nutrientsDescriptionMap[scope.row.nutrientName]" placement="top">
|
|
<span>{{ scope.row.nutrientName }}</span>
|
|
</el-tooltip>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column :label="currentRecipe.name">
|
|
<template #default="scope">
|
|
<span :style="{ color: scope.row.currentRecipeValue > scope.row.compareRecipeValue ? 'green' : (scope.row.currentRecipeValue < scope.row.compareRecipeValue ? 'red' : 'inherit') }">
|
|
{{ scope.row.currentRecipeValue !== undefined ? scope.row.currentRecipeValue.toFixed(2) : '-' }}
|
|
</span>
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<template v-if="compareType === 'recipe'">
|
|
<el-table-column :label="compareRecipeName">
|
|
<template #default="scope">
|
|
<span :style="{ color: scope.row.compareRecipeValue > scope.row.currentRecipeValue ? 'green' : (scope.row.compareRecipeValue < scope.row.currentRecipeValue ? 'red' : '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 + ' (下限)'">
|
|
<template #default="scope">
|
|
{{ scope.row.minRequirement !== undefined ? scope.row.minRequirement.toFixed(2) : '-' }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column :label="pigTypeName + ' (上限)'">
|
|
<template #default="scope">
|
|
{{ scope.row.maxRequirement !== undefined ? scope.row.maxRequirement.toFixed(2) : '-' }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="是否达标" 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 { getNutrients, 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 nutrientsDescriptionMap = 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 fetchNutrientsDescriptions = async () => {
|
|
try {
|
|
const response = await getNutrients({ page_size: 999 });
|
|
nutrientsDescriptionMap.value = response.data.list.reduce((map, nutrient) => {
|
|
map[nutrient.name] = nutrient.description;
|
|
return map;
|
|
}, {});
|
|
} 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();
|
|
fetchNutrientsDescriptions(); // 新增:在对话框显示时获取营养素描述
|
|
}
|
|
}, { immediate: true });
|
|
|
|
return {
|
|
compareType,
|
|
recipeList,
|
|
selectedCompareRecipeId,
|
|
pigTypeList,
|
|
selectedPigTypeId,
|
|
comparing,
|
|
compareError,
|
|
compareResult,
|
|
compareRecipeName,
|
|
pigTypeName,
|
|
handleClose,
|
|
canCompare,
|
|
startCompare,
|
|
nutrientsDescriptionMap, // 新增:返回 nutrientsDescriptionMap
|
|
};
|
|
},
|
|
};
|
|
</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;
|
|
}
|
|
.table-wrapper {
|
|
margin: 0 auto; /* 水平居中 */
|
|
max-width: 100%; /* 确保不超过父容器宽度 */
|
|
}
|
|
</style> |