支持查看原料详情
This commit is contained in:
155
src/components/feed/RecipeDetailDialog.vue
Normal file
155
src/components/feed/RecipeDetailDialog.vue
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
:title="`配方详情: ${recipe ? recipe.name : ''}`"
|
||||||
|
@close="handleClose"
|
||||||
|
width="70%"
|
||||||
|
top="5vh"
|
||||||
|
>
|
||||||
|
<div v-if="loading" class="loading-spinner">
|
||||||
|
<el-skeleton :rows="5" animated />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="error" class="error-message">
|
||||||
|
<el-alert
|
||||||
|
title="加载配方详情失败"
|
||||||
|
:description="error"
|
||||||
|
type="error"
|
||||||
|
show-icon
|
||||||
|
@close="error = null"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<el-tabs v-else v-model="activeTab">
|
||||||
|
<el-tab-pane label="原料列表" name="ingredients">
|
||||||
|
<el-table :data="ingredientDetails" style="width: 100%">
|
||||||
|
<el-table-column prop="name" label="原料名称" />
|
||||||
|
<el-table-column prop="percentage" label="占比">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ (scope.row.percentage * 100).toFixed(2) }}%
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="营养成分汇总" name="nutrients">
|
||||||
|
<el-table :data="nutrientSummary" style="width: 100%" ref="nutrientTableRef">
|
||||||
|
<el-table-column prop="name" label="营养素名称" />
|
||||||
|
<el-table-column prop="value" label="总含量">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ scope.row.value.toFixed(4) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="handleClose">关闭</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, watch, nextTick } from 'vue';
|
||||||
|
import { getRawMaterialById } from '../../api/feed';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RecipeDetailDialog',
|
||||||
|
props: {
|
||||||
|
visible: Boolean,
|
||||||
|
recipe: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['update:visible'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const activeTab = ref('ingredients');
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref(null);
|
||||||
|
const ingredientDetails = ref([]);
|
||||||
|
const nutrientSummary = ref([]);
|
||||||
|
const nutrientTableRef = ref(null);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateNutrientSummary = (ingredients) => {
|
||||||
|
const summary = new Map();
|
||||||
|
ingredients.forEach(ing => {
|
||||||
|
if (ing.raw_material_nutrients) {
|
||||||
|
ing.raw_material_nutrients.forEach(nutrient => {
|
||||||
|
const contribution = nutrient.value * ing.percentage;
|
||||||
|
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 Array.from(summary, ([name, value]) => ({ name, value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => props.visible, async (newVal) => {
|
||||||
|
if (newVal && props.recipe) {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
try {
|
||||||
|
const rawMaterialPromises = props.recipe.recipe_ingredients.map(ing => getRawMaterialById(ing.raw_material_id));
|
||||||
|
const rawMaterialResponses = await Promise.all(rawMaterialPromises);
|
||||||
|
|
||||||
|
const details = rawMaterialResponses.map((res, index) => ({
|
||||||
|
...res.data,
|
||||||
|
percentage: props.recipe.recipe_ingredients[index].percentage,
|
||||||
|
}));
|
||||||
|
|
||||||
|
ingredientDetails.value = details;
|
||||||
|
nutrientSummary.value = calculateNutrientSummary(details);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("加载配方详情失败:", err);
|
||||||
|
error.value = err.message || '未知错误';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 重置数据
|
||||||
|
ingredientDetails.value = [];
|
||||||
|
nutrientSummary.value = [];
|
||||||
|
activeTab.value = 'ingredients';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(activeTab, (newTab) => {
|
||||||
|
if (newTab === 'nutrients') {
|
||||||
|
nextTick(() => {
|
||||||
|
if (nutrientTableRef.value) {
|
||||||
|
nutrientTableRef.value.doLayout();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeTab,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
ingredientDetails,
|
||||||
|
nutrientSummary,
|
||||||
|
nutrientTableRef,
|
||||||
|
handleClose,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loading-spinner, .error-message {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
175
src/components/feed/RecipeForm.vue
Normal file
175
src/components/feed/RecipeForm.vue
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
:title="title"
|
||||||
|
@close="handleClose"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="120px"
|
||||||
|
@submit.prevent
|
||||||
|
>
|
||||||
|
<el-form-item label="配方名" prop="name">
|
||||||
|
<el-input v-model="formData.name" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="配方简介" prop="description">
|
||||||
|
<el-input v-model="formData.description" type="textarea" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="loading">确定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, reactive, computed, watch, nextTick } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { createRecipe, updateRecipe } from '../../api/feed';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RecipeForm',
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
recipeData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
isEdit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['update:visible', 'success', 'cancel'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const formRef = ref(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const initialFormData = () => ({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const formData = reactive(initialFormData());
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入配方名', trigger: 'blur' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const title = computed(() => {
|
||||||
|
return props.isEdit ? '编辑配方' : '新增配方';
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
emit('cancel');
|
||||||
|
// 重置表单
|
||||||
|
Object.assign(formData, initialFormData());
|
||||||
|
nextTick(() => {
|
||||||
|
if (formRef.value) {
|
||||||
|
formRef.value.resetFields();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
await formRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const submitData = {
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.isEdit) {
|
||||||
|
await updateRecipe(formData.id, submitData);
|
||||||
|
} else {
|
||||||
|
await createRecipe(submitData);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('success');
|
||||||
|
handleClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存配方失败:', error);
|
||||||
|
ElMessage.error(
|
||||||
|
props.isEdit ? '编辑配方失败: ' + (error.message || '未知错误') : '创建配方失败: ' + (error.message || '未知错误')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.recipeData,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal && Object.keys(newVal).length > 0) {
|
||||||
|
// 填充表单数据
|
||||||
|
formData.id = newVal.id;
|
||||||
|
formData.name = newVal.name;
|
||||||
|
formData.description = newVal.description;
|
||||||
|
} else {
|
||||||
|
// 重置表单数据到初始状态 (新增模式)
|
||||||
|
Object.assign(formData, initialFormData());
|
||||||
|
}
|
||||||
|
nextTick(() => {
|
||||||
|
if (formRef.value) {
|
||||||
|
formRef.value.clearValidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal && !props.isEdit) {
|
||||||
|
// 如果是新增模式且对话框打开,重置表单
|
||||||
|
Object.assign(formData, initialFormData());
|
||||||
|
nextTick(() => {
|
||||||
|
if (formRef.value) {
|
||||||
|
formRef.value.clearValidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
formRef,
|
||||||
|
loading,
|
||||||
|
formData,
|
||||||
|
rules,
|
||||||
|
title,
|
||||||
|
handleClose,
|
||||||
|
handleSubmit,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,15 +8,23 @@
|
|||||||
:highlight-current-row="false"
|
:highlight-current-row="false"
|
||||||
:scrollbar-always-on="true"
|
:scrollbar-always-on="true"
|
||||||
>
|
>
|
||||||
<el-table-column prop="id" label="ID" min-width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
<el-table-column prop="name" label="配方名" min-width="150" />
|
<el-table-column prop="name" label="配方名" min-width="150" />
|
||||||
<el-table-column label="原料种类数" min-width="120">
|
<el-table-column label="原料种类数" width="180" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ scope.row.recipe_ingredients ? scope.row.recipe_ingredients.length : 0 }}
|
<span>{{ scope.row.recipe_ingredients ? scope.row.recipe_ingredients.length : 0 }} 种 </span>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click="$emit('show-details', scope.row)"
|
||||||
|
style="padding: 0; vertical-align: baseline;"
|
||||||
|
>
|
||||||
|
[点击查看详情]
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="description" label="配方简介" min-width="200" />
|
<el-table-column prop="description" label="配方简介" min-width="200" />
|
||||||
<el-table-column label="操作" min-width="150" align="center">
|
<el-table-column label="操作" width="150" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button size="small" @click="$emit('edit', scope.row)">编辑</el-button>
|
<el-button size="small" @click="$emit('edit', scope.row)">编辑</el-button>
|
||||||
<el-button size="small" type="danger" @click="$emit('delete', scope.row)">删除</el-button>
|
<el-button size="small" type="danger" @click="$emit('delete', scope.row)">删除</el-button>
|
||||||
@@ -35,6 +43,6 @@ export default {
|
|||||||
default: () => []
|
default: () => []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['edit', 'delete']
|
emits: ['edit', 'delete', 'show-details']
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -37,8 +37,24 @@
|
|||||||
:recipes="recipes"
|
:recipes="recipes"
|
||||||
@edit="editRecipe"
|
@edit="editRecipe"
|
||||||
@delete="deleteRecipe"
|
@delete="deleteRecipe"
|
||||||
|
@show-details="handleShowDetails"
|
||||||
/>
|
/>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 配方表单对话框 -->
|
||||||
|
<RecipeForm
|
||||||
|
v-model:visible="dialogVisible"
|
||||||
|
:recipe-data="currentRecipe"
|
||||||
|
:is-edit="isEdit"
|
||||||
|
@success="onRecipeSuccess"
|
||||||
|
@cancel="dialogVisible = false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 配方详情对话框 -->
|
||||||
|
<RecipeDetailDialog
|
||||||
|
v-model:visible="detailDialogVisible"
|
||||||
|
:recipe="selectedRecipe"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -46,12 +62,16 @@
|
|||||||
import { Refresh } from '@element-plus/icons-vue';
|
import { Refresh } from '@element-plus/icons-vue';
|
||||||
import { getRecipes, deleteRecipe as deleteRecipeApi } from '../../api/feed.js';
|
import { getRecipes, deleteRecipe as deleteRecipeApi } from '../../api/feed.js';
|
||||||
import RecipeTable from '../../components/feed/RecipeTable.vue';
|
import RecipeTable from '../../components/feed/RecipeTable.vue';
|
||||||
|
import RecipeForm from '../../components/feed/RecipeForm.vue';
|
||||||
|
import RecipeDetailDialog from '../../components/feed/RecipeDetailDialog.vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RecipeList',
|
name: 'RecipeList',
|
||||||
components: {
|
components: {
|
||||||
RecipeTable,
|
RecipeTable,
|
||||||
|
RecipeForm,
|
||||||
|
RecipeDetailDialog,
|
||||||
Refresh
|
Refresh
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -59,6 +79,11 @@ export default {
|
|||||||
recipes: [],
|
recipes: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
dialogVisible: false,
|
||||||
|
currentRecipe: {},
|
||||||
|
isEdit: false,
|
||||||
|
detailDialogVisible: false,
|
||||||
|
selectedRecipe: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
@@ -69,7 +94,7 @@ export default {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
try {
|
try {
|
||||||
const response = await getRecipes();
|
const response = await getRecipes({ page: 1, page_size: 999 });
|
||||||
this.recipes = response.data.list || [];
|
this.recipes = response.data.list || [];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.error = err.message || '未知错误';
|
this.error = err.message || '未知错误';
|
||||||
@@ -79,14 +104,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
addRecipe() {
|
addRecipe() {
|
||||||
console.log('addRecipe triggered');
|
this.currentRecipe = {};
|
||||||
// 后续将实现表单对话框
|
this.isEdit = false;
|
||||||
ElMessage.info('新增功能待实现');
|
this.dialogVisible = true;
|
||||||
},
|
},
|
||||||
editRecipe(recipe) {
|
editRecipe(recipe) {
|
||||||
console.log('editRecipe triggered for:', recipe);
|
this.currentRecipe = JSON.parse(JSON.stringify(recipe));
|
||||||
// 后续将实现表单对话框
|
this.isEdit = true;
|
||||||
ElMessage.info('编辑功能待实现');
|
this.dialogVisible = true;
|
||||||
},
|
},
|
||||||
async deleteRecipe(recipe) {
|
async deleteRecipe(recipe) {
|
||||||
try {
|
try {
|
||||||
@@ -108,6 +133,15 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onRecipeSuccess() {
|
||||||
|
ElMessage.success(this.isEdit ? '配方更新成功' : '配方添加成功');
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.loadRecipes();
|
||||||
|
},
|
||||||
|
handleShowDetails(recipe) {
|
||||||
|
this.selectedRecipe = recipe;
|
||||||
|
this.detailDialogVisible = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user