|
<!DOCTYPE html> |
|
<html lang="es"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Formulador de Piensos para Cerdos</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
.custom-shadow { |
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
|
} |
|
.ingredient-input:hover { |
|
transform: translateY(-2px); |
|
transition: all 0.2s ease; |
|
} |
|
.nutrition-bar { |
|
height: 8px; |
|
border-radius: 4px; |
|
} |
|
.slide-fade-enter-active { |
|
transition: all 0.3s ease-out; |
|
} |
|
.slide-fade-leave-active { |
|
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1); |
|
} |
|
.slide-fade-enter-from, |
|
.slide-fade-leave-to { |
|
transform: translateX(20px); |
|
opacity: 0; |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-50 min-h-screen"> |
|
<div class="container mx-auto px-4 py-8"> |
|
|
|
<header class="mb-10 text-center"> |
|
<h1 class="text-4xl font-bold text-indigo-800 mb-2">Formulador de Piensos para Cerdos</h1> |
|
<p class="text-lg text-gray-600">Crea f贸rmulas nutricionales 贸ptimas para cada etapa productiva</p> |
|
</header> |
|
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
|
|
|
<div class="lg:col-span-2"> |
|
<div class="bg-white rounded-xl p-6 custom-shadow"> |
|
<div class="flex flex-wrap gap-4 mb-6"> |
|
<button @click="selectPreset('lechones')" class="flex-1 bg-pink-100 hover:bg-pink-200 text-pink-800 py-3 px-4 rounded-lg transition-colors"> |
|
<i class="fas fa-baby mr-2"></i> Lechones |
|
</button> |
|
<button @click="selectPreset('crecimiento')" class="flex-1 bg-blue-100 hover:bg-blue-200 text-blue-800 py-3 px-4 rounded-lg transition-colors"> |
|
<i class="fas fa-chart-line mr-2"></i> Crecimiento |
|
</button> |
|
<button @click="selectPreset('engorde')" class="flex-1 bg-green-100 hover:bg-green-200 text-green-800 py-3 px-4 rounded-lg transition-colors"> |
|
<i class="fas fa-weight mr-2"></i> Engorde |
|
</button> |
|
<button @click="selectPreset('reproductoras')" class="flex-1 bg-purple-100 hover:bg-purple-200 text-purple-800 py-3 px-4 rounded-lg transition-colors"> |
|
<i class="fas fa-venus mr-2"></i> Reproductoras |
|
</button> |
|
</div> |
|
|
|
<div class="mb-6"> |
|
<label class="block text-gray-700 font-medium mb-2">Nombre de la f贸rmula</label> |
|
<input v-model="formulaName" type="text" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> |
|
</div> |
|
|
|
<div class="mb-6"> |
|
<label class="block text-gray-700 font-medium mb-2">Descripci贸n</label> |
|
<textarea v-model="formulaDescription" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"></textarea> |
|
</div> |
|
|
|
<h3 class="text-xl font-semibold text-gray-800 mb-4">Ingredientes</h3> |
|
|
|
<div class="space-y-4 mb-6"> |
|
<div v-for="(ingredient, index) in ingredients" :key="index" class="ingredient-input bg-gray-50 p-4 rounded-lg custom-shadow"> |
|
<div class="flex flex-wrap gap-4 mb-3"> |
|
<div class="flex-1 min-w-[150px]"> |
|
<label class="block text-sm text-gray-600 mb-1">Ingrediente</label> |
|
<select v-model="ingredient.name" class="w-full px-3 py-2 border border-gray-300 rounded-lg"> |
|
<option v-for="item in ingredientOptions" :value="item.name">{{ item.name }}</option> |
|
</select> |
|
</div> |
|
<div class="flex-1 min-w-[100px]"> |
|
<label class="block text-sm text-gray-600 mb-1">Porcentaje (%)</label> |
|
<input v-model.number="ingredient.percentage" type="number" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg"> |
|
</div> |
|
<div class="flex items-end"> |
|
<button @click="removeIngredient(index)" class="bg-red-100 hover:bg-red-200 text-red-600 px-3 py-2 rounded-lg"> |
|
<i class="fas fa-trash"></i> |
|
</button> |
|
</div> |
|
</div> |
|
<div v-if="getIngredientDetails(ingredient.name)" class="text-xs text-gray-500"> |
|
Prote铆na: {{ getIngredientDetails(ingredient.name).protein }}% | |
|
Energ铆a: {{ getIngredientDetails(ingredient.name).energy }} kcal/kg | |
|
Fibra: {{ getIngredientDetails(ingredient.name).fiber }}% |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<button @click="addIngredient" class="bg-indigo-100 hover:bg-indigo-200 text-indigo-700 py-2 px-4 rounded-lg mb-6"> |
|
<i class="fas fa-plus mr-2"></i> A帽adir Ingrediente |
|
</button> |
|
|
|
<div class="flex flex-wrap gap-4"> |
|
<button @click="calculateNutrition" class="bg-green-600 hover:bg-green-700 text-white py-3 px-6 rounded-lg font-medium"> |
|
<i class="fas fa-calculator mr-2"></i> Calcular Nutrici贸n |
|
</button> |
|
<button @click="saveFormula" class="bg-indigo-600 hover:bg-indigo-700 text-white py-3 px-6 rounded-lg font-medium"> |
|
<i class="fas fa-save mr-2"></i> Guardar F贸rmula |
|
</button> |
|
<button @click="resetForm" class="bg-gray-200 hover:bg-gray-300 text-gray-800 py-3 px-6 rounded-lg font-medium"> |
|
<i class="fas fa-redo mr-2"></i> Reiniciar |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="lg:col-span-1"> |
|
<div class="bg-white rounded-xl p-6 custom-shadow sticky top-6"> |
|
<h3 class="text-xl font-semibold text-gray-800 mb-4">Resultados Nutricionales</h3> |
|
|
|
<div v-if="totalPercentage > 0" class="mb-4"> |
|
<div class="flex justify-between mb-1"> |
|
<span class="text-sm font-medium">Total ingredientes</span> |
|
<span class="text-sm font-medium">{{ totalPercentage }}%</span> |
|
</div> |
|
<div class="w-full bg-gray-200 rounded-full h-2.5"> |
|
<div :class="totalPercentage < 100 ? 'bg-yellow-500' : 'bg-green-500'" |
|
class="h-2.5 rounded-full" |
|
:style="'width: ' + Math.min(totalPercentage, 100) + '%'"></div> |
|
</div> |
|
<p v-if="totalPercentage < 100" class="text-xs text-yellow-600 mt-1">La f贸rmula est谩 incompleta (faltan {{ 100 - totalPercentage }}%)</p> |
|
<p v-if="totalPercentage > 100" class="text-xs text-red-600 mt-1">La f贸rmula excede el 100% ({{ totalPercentage - 100 }}% de m谩s)</p> |
|
</div> |
|
|
|
<div v-if="nutritionResults" class="space-y-4"> |
|
<div class="p-4 bg-blue-50 rounded-lg"> |
|
<h4 class="font-medium text-blue-800 mb-2">Prote铆na</h4> |
|
<div class="flex justify-between mb-1"> |
|
<span class="text-sm">{{ nutritionResults.protein }}%</span> |
|
<span class="text-sm">{{ getTargetRange('protein').min }}-{{ getTargetRange('protein').max }}% (objetivo)</span> |
|
</div> |
|
<div class="nutrition-bar bg-gray-200"> |
|
<div class="nutrition-bar bg-blue-500" :style="'width: ' + getNutritionPercentage('protein') + '%'"></div> |
|
</div> |
|
<p v-if="nutritionResults.protein < getTargetRange('protein').min" class="text-xs text-red-600 mt-1">Bajo en prote铆na</p> |
|
<p v-if="nutritionResults.protein > getTargetRange('protein').max" class="text-xs text-red-600 mt-1">Alto en prote铆na</p> |
|
</div> |
|
|
|
<div class="p-4 bg-green-50 rounded-lg"> |
|
<h4 class="font-medium text-green-800 mb-2">Energ铆a</h4> |
|
<div class="flex justify-between mb-1"> |
|
<span class="text-sm">{{ nutritionResults.energy }} kcal/kg</span> |
|
<span class="text-sm">{{ getTargetRange('energy').min }}-{{ getTargetRange('energy').max }} kcal/kg (objetivo)</span> |
|
</div> |
|
<div class="nutrition-bar bg-gray-200"> |
|
<div class="nutrition-bar bg-green-500" :style="'width: ' + getNutritionPercentage('energy') + '%'"></div> |
|
</div> |
|
</div> |
|
|
|
<div class="p-4 bg-yellow-50 rounded-lg"> |
|
<h4 class="font-medium text-yellow-800 mb-2">Fibra</h4> |
|
<div class="flex justify-between mb-1"> |
|
<span class="text-sm">{{ nutritionResults.fiber }}%</span> |
|
<span class="text-sm">{{ getTargetRange('fiber').min }}-{{ getTargetRange('fiber').max }}% (objetivo)</span> |
|
</div> |
|
<div class="nutrition-bar bg-gray-200"> |
|
<div class="nutrition-bar bg-yellow-500" :style="'width: ' + getNutritionPercentage('fiber') + '%'"></div> |
|
</div> |
|
</div> |
|
|
|
<div class="p-4 bg-purple-50 rounded-lg"> |
|
<h4 class="font-medium text-purple-800 mb-2">Costo estimado</h4> |
|
<p class="text-lg font-semibold">${{ nutritionResults.cost.toFixed(2) }} por tonelada</p> |
|
</div> |
|
</div> |
|
<div v-else class="text-center py-8 text-gray-500"> |
|
<i class="fas fa-calculator text-4xl mb-3 text-gray-300"></i> |
|
<p>Ingresa los ingredientes y haz clic en "Calcular Nutrici贸n" para ver los resultados</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div v-if="savedFormulas.length > 0" class="mt-12"> |
|
<h2 class="text-2xl font-bold text-gray-800 mb-6">F贸rmulas Guardadas</h2> |
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> |
|
<div v-for="(formula, index) in savedFormulas" :key="index" class="bg-white rounded-xl p-6 custom-shadow hover:shadow-md transition-shadow"> |
|
<div class="flex justify-between items-start mb-3"> |
|
<h3 class="text-lg font-semibold text-indigo-700">{{ formula.name }}</h3> |
|
<button @click="deleteFormula(index)" class="text-red-400 hover:text-red-600"> |
|
<i class="fas fa-trash"></i> |
|
</button> |
|
</div> |
|
<p class="text-sm text-gray-600 mb-4">{{ formula.description }}</p> |
|
|
|
<div class="mb-4"> |
|
<div class="flex justify-between text-sm mb-1"> |
|
<span class="font-medium">Composici贸n:</span> |
|
<span class="text-gray-500">{{ getFormulaTotal(formula.ingredients) }}%</span> |
|
</div> |
|
<div class="w-full bg-gray-200 rounded-full h-2"> |
|
<div class="bg-indigo-500 h-2 rounded-full" :style="'width: ' + getFormulaTotal(formula.ingredients) + '%'"></div> |
|
</div> |
|
</div> |
|
|
|
<ul class="space-y-1 mb-4"> |
|
<li v-for="(ing, i) in formula.ingredients" :key="i" class="flex justify-between text-sm"> |
|
<span>{{ ing.name }}</span> |
|
<span class="font-medium">{{ ing.percentage }}%</span> |
|
</li> |
|
</ul> |
|
|
|
<button @click="loadFormula(index)" class="w-full bg-indigo-50 hover:bg-indigo-100 text-indigo-700 py-2 px-4 rounded-lg text-sm"> |
|
<i class="fas fa-edit mr-2"></i> Cargar esta f贸rmula |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
const app = { |
|
data() { |
|
return { |
|
formulaName: '', |
|
formulaDescription: '', |
|
ingredients: [ |
|
{ name: 'Ma铆z', percentage: 0 }, |
|
{ name: 'Soja', percentage: 0 } |
|
], |
|
nutritionResults: null, |
|
savedFormulas: [], |
|
currentPreset: null, |
|
ingredientOptions: [ |
|
{ name: 'Ma铆z', protein: 8.5, energy: 3400, fiber: 2.3, cost: 0.15 }, |
|
{ name: 'Soja', protein: 44.0, energy: 3300, fiber: 6.0, cost: 0.35 }, |
|
{ name: 'Harina de pescado', protein: 65.0, energy: 3000, fiber: 0.5, cost: 0.80 }, |
|
{ name: 'Leche en polvo', protein: 26.0, energy: 4900, fiber: 0.0, cost: 1.20 }, |
|
{ name: 'Harina de trigo', protein: 13.0, energy: 3300, fiber: 2.5, cost: 0.25 }, |
|
{ name: 'Grasa animal', protein: 0.0, energy: 7700, fiber: 0.0, cost: 0.50 }, |
|
{ name: 'Sorgo', protein: 10.0, energy: 3300, fiber: 2.5, cost: 0.18 }, |
|
{ name: 'Salvado de trigo', protein: 15.5, energy: 2500, fiber: 10.0, cost: 0.12 }, |
|
{ name: 'Minerales', protein: 0.0, energy: 0.0, fiber: 0.0, cost: 1.50 }, |
|
{ name: 'Vitaminas', protein: 0.0, energy: 0.0, fiber: 0.0, cost: 2.00 } |
|
], |
|
presetTargets: { |
|
lechones: { |
|
protein: { min: 22, max: 24 }, |
|
energy: { min: 3400, max: 3600 }, |
|
fiber: { min: 3, max: 5 } |
|
}, |
|
crecimiento: { |
|
protein: { min: 18, max: 20 }, |
|
energy: { min: 3300, max: 3500 }, |
|
fiber: { min: 4, max: 6 } |
|
}, |
|
engorde: { |
|
protein: { min: 14, max: 16 }, |
|
energy: { min: 3400, max: 3600 }, |
|
fiber: { min: 5, max: 7 } |
|
}, |
|
reproductoras: { |
|
protein: { min: 16, max: 18 }, |
|
energy: { min: 3200, max: 3400 }, |
|
fiber: { min: 6, max: 8 } |
|
} |
|
} |
|
} |
|
}, |
|
computed: { |
|
totalPercentage() { |
|
return this.ingredients.reduce((sum, ing) => sum + (Number(ing.percentage) || 0), 0); |
|
} |
|
}, |
|
methods: { |
|
addIngredient() { |
|
this.ingredients.push({ name: 'Ma铆z', percentage: 0 }); |
|
}, |
|
removeIngredient(index) { |
|
if (this.ingredients.length > 1) { |
|
this.ingredients.splice(index, 1); |
|
} |
|
}, |
|
getIngredientDetails(name) { |
|
return this.ingredientOptions.find(item => item.name === name); |
|
}, |
|
calculateNutrition() { |
|
if (this.totalPercentage === 0) return; |
|
|
|
let totalProtein = 0; |
|
let totalEnergy = 0; |
|
let totalFiber = 0; |
|
let totalCost = 0; |
|
|
|
this.ingredients.forEach(ing => { |
|
const details = this.getIngredientDetails(ing.name); |
|
if (details) { |
|
const weight = ing.percentage / this.totalPercentage; |
|
totalProtein += details.protein * weight; |
|
totalEnergy += details.energy * weight; |
|
totalFiber += details.fiber * weight; |
|
totalCost += details.cost * ing.percentage; |
|
} |
|
}); |
|
|
|
|
|
const adjustment = this.totalPercentage !== 100 ? (100 / this.totalPercentage) : 1; |
|
|
|
this.nutritionResults = { |
|
protein: (totalProtein * adjustment).toFixed(1), |
|
energy: Math.round(totalEnergy * adjustment), |
|
fiber: (totalFiber * adjustment).toFixed(1), |
|
cost: totalCost * 10 |
|
}; |
|
}, |
|
saveFormula() { |
|
if (!this.formulaName.trim()) { |
|
alert('Por favor ingresa un nombre para la f贸rmula'); |
|
return; |
|
} |
|
|
|
if (this.totalPercentage === 0) { |
|
alert('La f贸rmula no contiene ingredientes'); |
|
return; |
|
} |
|
|
|
this.savedFormulas.push({ |
|
name: this.formulaName, |
|
description: this.formulaDescription, |
|
ingredients: JSON.parse(JSON.stringify(this.ingredients)), |
|
preset: this.currentPreset |
|
}); |
|
|
|
|
|
localStorage.setItem('savedFormulas', JSON.stringify(this.savedFormulas)); |
|
|
|
this.formulaName = ''; |
|
this.formulaDescription = ''; |
|
alert('F贸rmula guardada con 茅xito!'); |
|
}, |
|
loadFormula(index) { |
|
const formula = this.savedFormulas[index]; |
|
this.formulaName = formula.name; |
|
this.formulaDescription = formula.description; |
|
this.ingredients = JSON.parse(JSON.stringify(formula.ingredients)); |
|
this.currentPreset = formula.preset; |
|
this.calculateNutrition(); |
|
}, |
|
deleteFormula(index) { |
|
if (confirm('驴Est谩s seguro de eliminar esta f贸rmula?')) { |
|
this.savedFormulas.splice(index, 1); |
|
localStorage.setItem('savedFormulas', JSON.stringify(this.savedFormulas)); |
|
} |
|
}, |
|
resetForm() { |
|
this.formulaName = ''; |
|
this.formulaDescription = ''; |
|
this.ingredients = [{ name: 'Ma铆z', percentage: 0 }, { name: 'Soja', percentage: 0 }]; |
|
this.nutritionResults = null; |
|
this.currentPreset = null; |
|
}, |
|
selectPreset(preset) { |
|
this.currentPreset = preset; |
|
|
|
|
|
if (preset === 'lechones') { |
|
this.ingredients = [ |
|
{ name: 'Ma铆z', percentage: 50 }, |
|
{ name: 'Soja', percentage: 25 }, |
|
{ name: 'Leche en polvo', percentage: 15 }, |
|
{ name: 'Harina de pescado', percentage: 10 } |
|
]; |
|
this.formulaName = 'F贸rmula para Lechones'; |
|
this.formulaDescription = 'Alta en prote铆na y energ铆a para el desarrollo inicial'; |
|
} |
|
else if (preset === 'crecimiento') { |
|
this.ingredients = [ |
|
{ name: 'Ma铆z', percentage: 60 }, |
|
{ name: 'Soja', percentage: 30 }, |
|
{ name: 'Harina de trigo', percentage: 10 } |
|
]; |
|
this.formulaName = 'F贸rmula para Crecimiento'; |
|
this.formulaDescription = 'Balance de prote铆na y energ铆a para desarrollo 贸ptimo'; |
|
} |
|
else if (preset === 'engorde') { |
|
this.ingredients = [ |
|
{ name: 'Ma铆z', percentage: 70 }, |
|
{ name: 'Soja', percentage: 20 }, |
|
{ name: 'Grasa animal', percentage: 10 } |
|
]; |
|
this.formulaName = 'F贸rmula para Engorde'; |
|
this.formulaDescription = 'Alta en energ铆a para aumento de peso'; |
|
} |
|
else if (preset === 'reproductoras') { |
|
this.ingredients = [ |
|
{ name: 'Ma铆z', percentage: 60 }, |
|
{ name: 'Soja', percentage: 30 }, |
|
{ name: 'Minerales', percentage: 5 }, |
|
{ name: 'Vitaminas', percentage: 5 } |
|
]; |
|
this.formulaName = 'F贸rmula para Reproductoras'; |
|
this.formulaDescription = 'Balance nutricional para salud y reproducci贸n'; |
|
} |
|
|
|
this.calculateNutrition(); |
|
}, |
|
getTargetRange(nutrient) { |
|
if (this.currentPreset && this.presetTargets[this.currentPreset]) { |
|
return this.presetTargets[this.currentPreset][nutrient]; |
|
} |
|
|
|
return { |
|
protein: { min: 14, max: 24 }, |
|
energy: { min: 3200, max: 3600 }, |
|
fiber: { min: 3, max: 8 } |
|
}; |
|
}, |
|
getNutritionPercentage(nutrient) { |
|
const value = parseFloat(this.nutritionResults[nutrient]); |
|
const range = this.getTargetRange(nutrient); |
|
const target = (range.min + range.max) / 2; |
|
const span = range.max - range.min; |
|
|
|
|
|
let percentage = ((value - (target - span/2)) / span) * 100; |
|
|
|
|
|
return Math.min(100, Math.max(0, percentage)); |
|
}, |
|
getFormulaTotal(ingredients) { |
|
return ingredients.reduce((sum, ing) => sum + (Number(ing.percentage) || 0), 0); |
|
} |
|
}, |
|
mounted() { |
|
|
|
const saved = localStorage.getItem('savedFormulas'); |
|
if (saved) { |
|
this.savedFormulas = JSON.parse(saved); |
|
} |
|
} |
|
}; |
|
|
|
Vue.createApp(app).mount('body'); |
|
}); |
|
</script> |
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> |
|
</body> |
|
</html> |