Compare commits
25 Commits
80ab64e428
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2734306690 | |||
| 543adc1ad6 | |||
| 7bcd8fb873 | |||
| ae87ddb56d | |||
| e9545c9be1 | |||
| e1f38fd995 | |||
| 2649ed048a | |||
| b368c172c5 | |||
| 93f66d844c | |||
| 7166d5049f | |||
| 908af8eaa5 | |||
| 091ba71069 | |||
| 0985744184 | |||
| eec7e64e7d | |||
| 01db083b8e | |||
| 1ddd3f8c90 | |||
| aa50fdc9de | |||
| 24344e380a | |||
| 19d4dd64d4 | |||
| ef82203ed7 | |||
| 24d9b07c97 | |||
| ede3d6b330 | |||
| 9af7e0d005 | |||
| 6507b3ee14 | |||
| 2a38cf5bc0 |
@@ -3186,6 +3186,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/feed/recipes/generate-prioritized-stock/{pig_type_id}": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "根据指定的猪类型ID,优先使用有库存的原料,自动计算并创建一个配方。",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"饲料管理-配方"
|
||||||
|
],
|
||||||
|
"summary": "使用优先有库存原料的策略生成配方",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "猪类型ID",
|
||||||
|
"name": "pig_type_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "业务码为201代表创建成功",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/controller.Response"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/dto.GenerateRecipeResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/feed/recipes/{id}": {
|
"/api/v1/feed/recipes/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -3387,6 +3433,12 @@
|
|||||||
],
|
],
|
||||||
"summary": "获取当前库存列表",
|
"summary": "获取当前库存列表",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "只查询有库存的原料",
|
||||||
|
"name": "has_stock",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "排序字段, 例如 \"stock DESC\"",
|
"description": "排序字段, 例如 \"stock DESC\"",
|
||||||
@@ -7262,6 +7314,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 255
|
"maxLength": 255
|
||||||
},
|
},
|
||||||
|
"max_addition_ratio": {
|
||||||
|
"description": "最大添加比例",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"description": "原料名称",
|
"description": "原料名称",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -7331,6 +7387,14 @@
|
|||||||
"dto.CurrentStockResponse": {
|
"dto.CurrentStockResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"last_operation_source_type": {
|
||||||
|
"description": "上次库存变动的来源类型",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/models.StockLogSourceType"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"last_updated": {
|
"last_updated": {
|
||||||
"description": "最后更新时间",
|
"description": "最后更新时间",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -8819,6 +8883,10 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"max_addition_ratio": {
|
||||||
|
"description": "最大添加比例",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -9192,7 +9260,8 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"change_amount",
|
"change_amount",
|
||||||
"raw_material_id"
|
"raw_material_id",
|
||||||
|
"source_type"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"change_amount": {
|
"change_amount": {
|
||||||
@@ -9207,6 +9276,18 @@
|
|||||||
"description": "备注",
|
"description": "备注",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 255
|
"maxLength": 255
|
||||||
|
},
|
||||||
|
"source_id": {
|
||||||
|
"description": "来源ID, 例如: 配方ID, 采购单ID等",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"source_type": {
|
||||||
|
"description": "库存变动来源类型",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/models.StockLogSourceType"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -9859,6 +9940,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 255
|
"maxLength": 255
|
||||||
},
|
},
|
||||||
|
"max_addition_ratio": {
|
||||||
|
"description": "最大添加比例",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"description": "原料名称",
|
"description": "原料名称",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -647,6 +647,15 @@ export const generateRecipeFromAllMaterials = (pigTypeId) => {
|
|||||||
return http.post(`/api/v1/feed/recipes/generate-from-all-materials/${pigTypeId}`);
|
return http.post(`/api/v1/feed/recipes/generate-from-all-materials/${pigTypeId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据指定的猪类型ID,优先使用有库存的原料,自动计算并创建一个配方。
|
||||||
|
* @param {number} pigTypeId - 猪类型ID
|
||||||
|
* @returns {Promise<Response<GenerateRecipeResponse>>}
|
||||||
|
*/
|
||||||
|
export const generatePrioritizedStockRecipe = (pigTypeId) => {
|
||||||
|
return http.post(`/api/v1/feed/recipes/generate-prioritized-stock/${pigTypeId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export const FeedApi = {
|
export const FeedApi = {
|
||||||
getNutrients,
|
getNutrients,
|
||||||
@@ -682,4 +691,5 @@ export const FeedApi = {
|
|||||||
updateRecipe,
|
updateRecipe,
|
||||||
deleteRecipe,
|
deleteRecipe,
|
||||||
generateRecipeFromAllMaterials,
|
generateRecipeFromAllMaterials,
|
||||||
|
generatePrioritizedStockRecipe,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { PaginationDTO, Response, StockLogSourceType } from '../enums';
|
|||||||
* @typedef {object} StockAdjustmentRequest
|
* @typedef {object} StockAdjustmentRequest
|
||||||
* @property {number} change_amount - 变动数量, 正数为入库, 负数为出库, 单位: g
|
* @property {number} change_amount - 变动数量, 正数为入库, 负数为出库, 单位: g
|
||||||
* @property {number} raw_material_id - 要调整的原料ID
|
* @property {number} raw_material_id - 要调整的原料ID
|
||||||
|
* @property {StockLogSourceType} source_type - 库存变动来源类型
|
||||||
|
* @property {number} [source_id] - 来源ID, 例如: 配方ID, 采购单ID等
|
||||||
* @property {string} [remarks] - 备注
|
* @property {string} [remarks] - 备注
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ import { PaginationDTO, Response, StockLogSourceType } from '../enums';
|
|||||||
* @property {number} raw_material_id - 原料ID
|
* @property {number} raw_material_id - 原料ID
|
||||||
* @property {string} raw_material_name - 原料名称
|
* @property {string} raw_material_name - 原料名称
|
||||||
* @property {number} stock - 当前库存量, 单位: g
|
* @property {number} stock - 当前库存量, 单位: g
|
||||||
|
* @property {StockLogSourceType} [last_operation_source_type] - 上次库存变动的来源类型
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,6 +54,7 @@ import { PaginationDTO, Response, StockLogSourceType } from '../enums';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} GetCurrentStockListParams
|
* @typedef {object} GetCurrentStockListParams
|
||||||
|
* @property {boolean} [has_stock] - 只查询有库存的原料
|
||||||
* @property {string} [order_by] - 排序字段, 例如 "stock DESC"
|
* @property {string} [order_by] - 排序字段, 例如 "stock DESC"
|
||||||
* @property {number} [page] - 页码
|
* @property {number} [page] - 页码
|
||||||
* @property {number} [page_size] - 每页数量
|
* @property {number} [page_size] - 每页数量
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
:before-close="handleCancel"
|
:before-close="handleCancel"
|
||||||
>
|
>
|
||||||
<el-form :model="form" ref="generateRecipeForm" label-width="100px">
|
<el-form :model="form" ref="generateRecipeForm" label-width="100px">
|
||||||
<el-form-item label="生成对象" prop="selectedPigType" :rules="[{ required: true, message: '请选择生成对象', trigger: 'change' }]">
|
<el-form-item label="生成对象" prop="selectedPigType"
|
||||||
|
:rules="[{ required: true, message: '请选择生成对象', trigger: 'change' }]">
|
||||||
<el-select v-model="form.selectedPigType" placeholder="请选择猪品种-猪年龄阶段" style="width: 100%;">
|
<el-select v-model="form.selectedPigType" placeholder="请选择猪品种-猪年龄阶段" style="width: 100%;">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in pigTypesOptions"
|
v-for="item in pigTypesOptions"
|
||||||
@@ -17,9 +18,12 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="生成方式" prop="selectedGenerationMethod" :rules="[{ required: true, message: '请选择生成方式', trigger: 'change' }]">
|
<el-form-item label="生成方式" prop="selectedGenerationMethod"
|
||||||
|
:rules="[{ required: true, message: '请选择生成方式', trigger: 'change' }]">
|
||||||
<el-select v-model="form.selectedGenerationMethod" placeholder="请选择生成方式" style="width: 100%;">
|
<el-select v-model="form.selectedGenerationMethod" placeholder="请选择生成方式" style="width: 100%;">
|
||||||
<el-option label="使用系统中所有可用的原料" value="all_raw_materials"></el-option>
|
<el-option label="使用系统中所有可用的原料" value="all_raw_materials"></el-option>
|
||||||
|
<!-- 新增选项 -->
|
||||||
|
<el-option label="优先使用有库存的原料" value="prefer_in_stock_materials"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -92,9 +96,19 @@ export default {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const pigTypeId = parseInt(form.selectedPigType); // 获取选中的 pigType ID
|
const pigTypeId = parseInt(form.selectedPigType); // 获取选中的 pigType ID
|
||||||
|
let response = null; // 声明 response 变量
|
||||||
|
|
||||||
// 调用一键生成配方的接口
|
if (form.selectedGenerationMethod === 'all_raw_materials') {
|
||||||
const response = await FeedApi.generateRecipeFromAllMaterials(pigTypeId);
|
// 调用使用所有原料生成配方的接口
|
||||||
|
response = await FeedApi.generateRecipeFromAllMaterials(pigTypeId);
|
||||||
|
} else if (form.selectedGenerationMethod === 'prefer_in_stock_materials') {
|
||||||
|
// 调用优先使用有库存原料生成配方的接口
|
||||||
|
response = await FeedApi.generatePrioritizedStockRecipe(pigTypeId);
|
||||||
|
} else {
|
||||||
|
ElMessage.error('未知的生成方式');
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
ElMessage.success('配方生成成功!');
|
ElMessage.success('配方生成成功!');
|
||||||
|
|||||||
@@ -199,12 +199,23 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 处理查看营养需求详情
|
// 处理查看营养需求详情
|
||||||
const handleViewNutrientRequirements = (pigType) => {
|
const handleViewNutrientRequirements = async (pigType) => { // 添加 async
|
||||||
currentNutrientRequirements.value = pigType.pig_nutrient_requirements || [];
|
try {
|
||||||
currentBreedName.value = pigType.breed_name;
|
// 强制重新获取该 pigType 的最新详情
|
||||||
currentAgeStageName.value = pigType.age_stage_name;
|
const response = await FeedApi.getPigTypeById(pigType.id);
|
||||||
currentPigTypeId.value = pigType.id; // 设置当前的 pigType ID
|
if (response.data) {
|
||||||
|
currentNutrientRequirements.value = response.data.pig_nutrient_requirements || [];
|
||||||
|
currentBreedName.value = response.data.breed_name;
|
||||||
|
currentAgeStageName.value = response.data.age_stage_name;
|
||||||
|
currentPigTypeId.value = response.data.id; // 设置当前的 pigType ID
|
||||||
showNutrientDialog.value = true;
|
showNutrientDialog.value = true;
|
||||||
|
} else {
|
||||||
|
ElMessage.error('获取猪类型详情失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取猪类型详情失败:', error);
|
||||||
|
ElMessage.error('获取猪类型详情失败');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (row) => {
|
const handleEdit = (row) => {
|
||||||
|
|||||||
@@ -275,12 +275,23 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 处理查看营养需求详情
|
// 处理查看营养需求详情
|
||||||
const handleViewNutrientRequirements = (pigType) => {
|
const handleViewNutrientRequirements = async (pigType) => { // 添加 async
|
||||||
currentNutrientRequirements.value = pigType.pig_nutrient_requirements || [];
|
try {
|
||||||
currentBreedName.value = pigType.breed_name;
|
// 强制重新获取该 pigType 的最新详情
|
||||||
currentAgeStageName.value = pigType.age_stage_name;
|
const response = await FeedApi.getPigTypeById(pigType.id);
|
||||||
currentPigTypeId.value = pigType.id; // 设置当前的 pigType ID
|
if (response.data) {
|
||||||
|
currentNutrientRequirements.value = response.data.pig_nutrient_requirements || [];
|
||||||
|
currentBreedName.value = response.data.breed_name;
|
||||||
|
currentAgeStageName.value = response.data.age_stage_name;
|
||||||
|
currentPigTypeId.value = response.data.id; // 设置当前的 pigType ID
|
||||||
showNutrientDialog.value = true;
|
showNutrientDialog.value = true;
|
||||||
|
} else {
|
||||||
|
ElMessage.error('获取猪类型详情失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取猪类型详情失败:', error);
|
||||||
|
ElMessage.error('获取猪类型详情失败');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (row) => {
|
const handleEdit = (row) => {
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export default {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用API更新营养需求
|
// 调用API更新营养需求
|
||||||
await FeedApi.updatePigNutrientRequirements(props.pigTypeId, { pig_nutrient_requirements: requirementsToSave });
|
await FeedApi.updatePigTypeNutrientRequirements(props.pigTypeId, { nutrient_requirements: requirementsToSave });
|
||||||
ElMessage.success('营养需求更新成功');
|
ElMessage.success('营养需求更新成功');
|
||||||
emit('save'); // 通知父组件保存成功
|
emit('save'); // 通知父组件保存成功
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
359
src/components/feed/RecipeCompareDialog.vue
Normal file
359
src/components/feed/RecipeCompareDialog.vue
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
<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>
|
||||||
0
src/components/feed/RecipeComparisonDialog.vue
Normal file
0
src/components/feed/RecipeComparisonDialog.vue
Normal file
@@ -65,12 +65,18 @@
|
|||||||
</el-tabs>
|
</el-tabs>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
|
<el-button v-if="!isEditing" @click="openCompareDialog">对比</el-button>
|
||||||
<el-button v-if="!isEditing" @click="handleEdit">编辑配方</el-button>
|
<el-button v-if="!isEditing" @click="handleEdit">编辑配方</el-button>
|
||||||
<el-button v-else @click="handleCancelEdit">取消</el-button>
|
<el-button v-else @click="handleCancelEdit">取消</el-button>
|
||||||
<el-button v-if="isEditing" type="primary" @click="handleSaveRecipe">保存</el-button>
|
<el-button v-if="isEditing" type="primary" @click="handleSaveRecipe">保存</el-button>
|
||||||
<el-button v-else @click="handleClose">关闭</el-button>
|
<el-button v-else @click="handleClose">关闭</el-button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
<RecipeCompareDialog
|
||||||
|
v-model:visible="compareDialogVisible"
|
||||||
|
:current-recipe="recipe"
|
||||||
|
@close="compareDialogVisible = false"
|
||||||
|
/>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -79,6 +85,7 @@ import { ref, watch, nextTick, computed } from 'vue';
|
|||||||
import { getRawMaterialById, getRawMaterials, updateRecipe, getRecipeById } from '../../api/feed';
|
import { getRawMaterialById, getRawMaterials, updateRecipe, getRecipeById } from '../../api/feed';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { Delete } from '@element-plus/icons-vue';
|
import { Delete } from '@element-plus/icons-vue';
|
||||||
|
import RecipeCompareDialog from './RecipeCompareDialog.vue'; // 引入对比组件
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RecipeDetailDialog',
|
name: 'RecipeDetailDialog',
|
||||||
@@ -101,11 +108,16 @@ export default {
|
|||||||
const nutrientTableRef = ref(null);
|
const nutrientTableRef = ref(null);
|
||||||
const allRawMaterials = ref([]); // 所有可用原料列表
|
const allRawMaterials = ref([]); // 所有可用原料列表
|
||||||
const newIngredientId = ref(null); // 待添加的新原料ID
|
const newIngredientId = ref(null); // 待添加的新原料ID
|
||||||
|
const compareDialogVisible = ref(false); // 控制对比对话框的显示
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openCompareDialog = () => {
|
||||||
|
compareDialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
const calculateNutrientSummary = (ingredients) => {
|
const calculateNutrientSummary = (ingredients) => {
|
||||||
const summary = new Map();
|
const summary = new Map();
|
||||||
ingredients.forEach(ing => {
|
ingredients.forEach(ing => {
|
||||||
@@ -213,11 +225,11 @@ export default {
|
|||||||
|
|
||||||
// 保存配方
|
// 保存配方
|
||||||
const handleSaveRecipe = async () => {
|
const handleSaveRecipe = async () => {
|
||||||
// 验证占比总和是否为100
|
// 验证占比总和是否不超过100
|
||||||
const totalPercentage = localIngredientDetails.value.reduce((sum, ing) => sum + ing.percentage, 0);
|
const totalPercentage = localIngredientDetails.value.reduce((sum, ing) => sum + ing.percentage, 0);
|
||||||
|
|
||||||
if (Math.abs(totalPercentage - 100) > 0.001) { // 允许浮点数误差
|
if (totalPercentage > 100.001) { // 允许浮点数误差
|
||||||
ElMessage.error(`原料总占比必须为100%,当前为${totalPercentage.toFixed(2)}%`);
|
ElMessage.error(`原料总占比不能超过100%,当前为${totalPercentage.toFixed(2)}%`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,8 +297,14 @@ export default {
|
|||||||
newIngredientId,
|
newIngredientId,
|
||||||
updateNutrientSummary,
|
updateNutrientSummary,
|
||||||
Delete, // 暴露 Delete 图标组件
|
Delete, // 暴露 Delete 图标组件
|
||||||
|
compareDialogVisible, // 暴露对比对话框的可见性
|
||||||
|
openCompareDialog, // 暴露打开对比对话框的方法
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
Delete,
|
||||||
|
RecipeCompareDialog, // 注册对比组件
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
258
src/components/inventory/StockAdjustmentDialog.vue
Normal file
258
src/components/inventory/StockAdjustmentDialog.vue
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="400px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
|
||||||
|
<el-form-item label="原料名称">
|
||||||
|
<el-input v-model="rawMaterial.raw_material_name" disabled></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="操作类型">
|
||||||
|
<el-radio-group v-model="form.operationType">
|
||||||
|
<el-radio label="in">存入</el-radio>
|
||||||
|
<el-radio label="out">取出</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="变更类型" prop="source_type">
|
||||||
|
<el-select v-model="form.source_type" placeholder="请选择变更类型" style="width: 100%;">
|
||||||
|
<el-option
|
||||||
|
v-for="item in availableSourceTypes"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="变动数量" prop="change_amount">
|
||||||
|
<div class="amount-unit-wrapper">
|
||||||
|
<el-input-number
|
||||||
|
v-model="displayAmount"
|
||||||
|
:min="computedMin"
|
||||||
|
:precision="computedPrecision"
|
||||||
|
:step="computedStep"
|
||||||
|
controls-position="right"
|
||||||
|
class="amount-input"
|
||||||
|
></el-input-number>
|
||||||
|
<el-select v-model="unit" placeholder="单位" class="unit-select">
|
||||||
|
<el-option label="g" value="g"></el-option>
|
||||||
|
<el-option label="kg" value="kg"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remarks">
|
||||||
|
<el-input v-model="form.remarks" type="textarea" :rows="2"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确认</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, reactive, computed, watch } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { InventoryApi } from '../../api/inventory';
|
||||||
|
import { StockLogSourceType } from '../../enums'; // 导入 StockLogSourceType
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StockAdjustmentDialog',
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
rawMaterial: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['update:visible', 'success'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const formRef = ref(null);
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (val) => emit('update:visible', val),
|
||||||
|
});
|
||||||
|
|
||||||
|
const unit = ref('g'); // 默认单位为克
|
||||||
|
const form = reactive({
|
||||||
|
operationType: 'in', // 默认存入
|
||||||
|
source_type: StockLogSourceType.MANUAL, // 默认变更类型
|
||||||
|
change_amount: 1000, // 内部始终以克为单位
|
||||||
|
remarks: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogTitle = computed(() => {
|
||||||
|
return form.operationType === 'in' ? '存入库存' : '取出库存';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 定义存入和取出对应的变更类型
|
||||||
|
const inSourceTypes = [
|
||||||
|
{ value: StockLogSourceType.PURCHASE, label: '采购入库' },
|
||||||
|
{ value: StockLogSourceType.FERMENT_END, label: '发酵入库' },
|
||||||
|
{ value: StockLogSourceType.MANUAL, label: '手动盘点' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const outSourceTypes = [
|
||||||
|
{ value: StockLogSourceType.FEEDING, label: '饲喂出库' },
|
||||||
|
{ value: StockLogSourceType.DETERIORATE, label: '变质出库' },
|
||||||
|
{ value: StockLogSourceType.SALE, label: '售卖出库' },
|
||||||
|
{ value: StockLogSourceType.MISCELLANEOUS, label: '杂用领取' },
|
||||||
|
{ value: StockLogSourceType.FERMENT_START, label: '发酵出库' },
|
||||||
|
{ value: StockLogSourceType.MANUAL, label: '手动盘点' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 根据操作类型动态计算可用的变更类型
|
||||||
|
const availableSourceTypes = computed(() => {
|
||||||
|
const types = form.operationType === 'in' ? inSourceTypes : outSourceTypes;
|
||||||
|
// 如果当前选中的 source_type 不在新的可用列表中,则重置为第一个可用类型或 MANUAL
|
||||||
|
if (!types.some(item => item.value === form.source_type)) {
|
||||||
|
form.source_type = types.length > 0 ? types[0].value : StockLogSourceType.MANUAL;
|
||||||
|
}
|
||||||
|
return types;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:用于 el-input-number 的显示值和输入值转换
|
||||||
|
const displayAmount = computed({
|
||||||
|
get() {
|
||||||
|
if (unit.value === 'kg') {
|
||||||
|
return form.change_amount / 1000;
|
||||||
|
}
|
||||||
|
return form.change_amount;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
if (unit.value === 'kg') {
|
||||||
|
form.change_amount = val * 1000;
|
||||||
|
} else {
|
||||||
|
form.change_amount = val;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:el-input-number 的步长
|
||||||
|
const computedStep = computed(() => {
|
||||||
|
return unit.value === 'kg' ? 0.1 : 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:el-input-number 的精度
|
||||||
|
const computedPrecision = computed(() => {
|
||||||
|
return unit.value === 'kg' ? 2 : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:el-input-number 的最小值
|
||||||
|
const computedMin = computed(() => {
|
||||||
|
return unit.value === 'kg' ? 0.001 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.visible, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
// 弹窗打开时重置表单和单位
|
||||||
|
form.change_amount = 1000; // 默认1000克
|
||||||
|
form.remarks = '';
|
||||||
|
form.operationType = 'in'; // 默认存入
|
||||||
|
form.source_type = StockLogSourceType.MANUAL; // 默认变更类型
|
||||||
|
unit.value = 'g'; // 默认单位为克
|
||||||
|
formRef.value?.clearValidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
source_type: [
|
||||||
|
{ required: true, message: '请选择变更类型', trigger: 'change' },
|
||||||
|
],
|
||||||
|
change_amount: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入变动数量',
|
||||||
|
trigger: 'blur',
|
||||||
|
// 验证器直接作用于内部的 form.change_amount (克)
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (form.change_amount === null || form.change_amount === undefined || form.change_amount <= 0) {
|
||||||
|
callback(new Error('数量必须大于0'));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
|
||||||
|
let finalChangeAmount = form.change_amount; // form.change_amount 已经是克
|
||||||
|
if (form.operationType === 'out') {
|
||||||
|
finalChangeAmount = -finalChangeAmount;
|
||||||
|
// 检查是否超出库存 (props.rawMaterial.stock 也是克)
|
||||||
|
if (props.rawMaterial.stock + finalChangeAmount < 0) {
|
||||||
|
ElMessage.error('取出数量不能大于当前库存!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
raw_material_id: props.rawMaterial.raw_material_id,
|
||||||
|
change_amount: finalChangeAmount,
|
||||||
|
source_type: form.source_type, // 添加 source_type
|
||||||
|
remarks: form.remarks,
|
||||||
|
};
|
||||||
|
|
||||||
|
await InventoryApi.adjustStock(data);
|
||||||
|
ElMessage.success(`${form.operationType === 'in' ? '存入' : '取出'}成功!`);
|
||||||
|
emit('success');
|
||||||
|
dialogVisible.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== false) { // 阻止表单验证失败时的错误提示
|
||||||
|
ElMessage.error('操作失败: ' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
formRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
formRef,
|
||||||
|
dialogVisible,
|
||||||
|
dialogTitle,
|
||||||
|
form,
|
||||||
|
rules,
|
||||||
|
handleSubmit,
|
||||||
|
handleClose,
|
||||||
|
unit,
|
||||||
|
displayAmount,
|
||||||
|
computedStep,
|
||||||
|
computedPrecision,
|
||||||
|
computedMin,
|
||||||
|
availableSourceTypes, // 暴露给模板
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.amount-unit-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-input {
|
||||||
|
flex: 1; /* 让输入框占据剩余空间 */
|
||||||
|
margin-right: 10px; /* 输入框和选择器之间的间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit-select {
|
||||||
|
width: 80px; /* 固定单位选择器的宽度 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
281
src/components/inventory/StockListTable.vue
Normal file
281
src/components/inventory/StockListTable.vue
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-table
|
||||||
|
ref="mainTable"
|
||||||
|
:data="stockList"
|
||||||
|
style="width: 100%"
|
||||||
|
v-loading="loading"
|
||||||
|
row-key="raw_material_id"
|
||||||
|
@expand-change="handleExpandChange"
|
||||||
|
@row-click="handleRowClick"
|
||||||
|
>
|
||||||
|
<el-table-column type="expand">
|
||||||
|
<template #default="props">
|
||||||
|
<div style="padding: 10px 20px;">
|
||||||
|
<h4 style="margin-bottom: 16px;">历史操作记录 ({{ props.row.raw_material_name }})</h4>
|
||||||
|
<el-table :data="props.row.stock_logs" border v-loading="props.row.stock_log_loading">
|
||||||
|
<el-table-column prop="happened_at" label="操作时间" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatRFC3339(scope.row.happened_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="source_type" label="操作类型" width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ getStockLogSourceTypeLabel(scope.row.source_type) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="change_amount" label="变动数量" width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatChangeAmount(scope.row.change_amount) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="before_quantity" label="变动前库存" width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatWeight(scope.row.before_quantity) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="after_quantity" label="变动后库存" width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatWeight(scope.row.after_quantity) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="remarks" label="备注"></el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-if="props.row.stock_log_pagination && props.row.stock_log_pagination.total > 0"
|
||||||
|
style="margin-top: 20px;"
|
||||||
|
:current-page="props.row.stock_log_pagination.page"
|
||||||
|
:page-size="props.row.stock_log_pagination.page_size"
|
||||||
|
:total="props.row.stock_log_pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="(size) => handleStockLogSizeChange(props.row, size)"
|
||||||
|
@current-change="(page) => handleStockLogCurrentChange(props.row, page)"
|
||||||
|
></el-pagination>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="raw_material_name" label="原料名称"></el-table-column>
|
||||||
|
<el-table-column prop="stock" label="当前库存">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatWeight(scope.row.stock) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="last_updated" label="最后更新时间">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatRFC3339(scope.row.last_updated) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="last_operation_source_type" label="上次操作类型">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ getStockLogSourceTypeLabel(scope.row.last_operation_source_type) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button size="small" type="primary" @click="handleAdjust(scope.row)">调整库存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
:current-page="pagination.page"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:page-size="pagination.page_size"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="pagination.total">
|
||||||
|
</el-pagination>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, onMounted, watch, nextTick } from 'vue';
|
||||||
|
import { InventoryApi } from '../../api/inventory';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { formatRFC3339, formatWeight, formatChangeAmount } from '../../utils/format'; // 导入 formatChangeAmount
|
||||||
|
import { getStockLogSourceTypeLabel } from '../../enums'; // 导入 getStockLogSourceTypeLabel
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StockListTable',
|
||||||
|
props: {
|
||||||
|
stockFilter: {
|
||||||
|
type: String,
|
||||||
|
default: 'all', // 默认值与父组件保持一致
|
||||||
|
},
|
||||||
|
searchRawMaterialName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['adjust-stock'], // 声明组件将发出的事件
|
||||||
|
setup(props, { expose, emit }) { // 接收 emit 函数
|
||||||
|
const stockList = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const pagination = ref({
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// el-table 的引用,用于控制行的展开/折叠
|
||||||
|
const mainTable = ref(null);
|
||||||
|
|
||||||
|
const fetchStockList = async (filter = props.stockFilter, rawMaterialName = props.searchRawMaterialName) => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: pagination.value.page,
|
||||||
|
page_size: pagination.value.page_size,
|
||||||
|
};
|
||||||
|
if (filter === 'in_stock') {
|
||||||
|
params.has_stock = true;
|
||||||
|
}
|
||||||
|
if (rawMaterialName) {
|
||||||
|
params.raw_material_name = rawMaterialName;
|
||||||
|
}
|
||||||
|
const res = await InventoryApi.getCurrentStockList(params);
|
||||||
|
// 初始化 stock_logs 和 stock_log_pagination
|
||||||
|
stockList.value = res.data.list.map(item => ({
|
||||||
|
...item,
|
||||||
|
stock_logs: [], // 存储历史操作记录
|
||||||
|
stock_log_loading: false, // 历史操作记录的加载状态
|
||||||
|
stock_log_pagination: { // 历史操作记录的分页信息
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
pagination.value.total = res.data.pagination.total;
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取库存列表失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSizeChange = (val) => {
|
||||||
|
pagination.value.page_size = val;
|
||||||
|
fetchStockList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
pagination.value.page = val;
|
||||||
|
fetchStockList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理库存调整操作
|
||||||
|
const handleAdjust = (row) => {
|
||||||
|
emit('adjust-stock', row); // 只传递行数据
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理行展开/折叠事件
|
||||||
|
* @param {object} row - 当前行数据
|
||||||
|
* @param {Array<object>} expandedRows - 所有已展开的行数据
|
||||||
|
*/
|
||||||
|
const handleExpandChange = async (row, expandedRows) => {
|
||||||
|
const isExpanded = expandedRows.some(r => r.raw_material_id === row.raw_material_id);
|
||||||
|
// 仅在展开时且数据未加载时才请求
|
||||||
|
if (isExpanded) {
|
||||||
|
// 确保每次展开都重新加载第一页数据,或者根据当前分页状态加载
|
||||||
|
// 如果是第一次展开,或者用户手动切换了分页,则需要重新获取
|
||||||
|
await fetchStockLogs(row);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定原料的历史操作记录
|
||||||
|
* @param {object} row - 当前原料行数据
|
||||||
|
*/
|
||||||
|
const fetchStockLogs = async (row) => {
|
||||||
|
row.stock_log_loading = true;
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
raw_material_id: row.raw_material_id,
|
||||||
|
page: row.stock_log_pagination.page,
|
||||||
|
page_size: row.stock_log_pagination.page_size,
|
||||||
|
order_by: 'happened_at DESC', // 按时间倒序
|
||||||
|
};
|
||||||
|
const res = await InventoryApi.getStockLogList(params);
|
||||||
|
if (res.data) {
|
||||||
|
row.stock_logs = res.data.list;
|
||||||
|
row.stock_log_pagination.total = res.data.pagination.total;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取库存历史操作记录失败:', error);
|
||||||
|
ElMessage.error('获取库存历史操作记录失败');
|
||||||
|
} finally {
|
||||||
|
row.stock_log_loading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理历史操作记录分页的每页大小变化
|
||||||
|
* @param {object} row - 当前原料行数据
|
||||||
|
* @param {number} size - 新的每页大小
|
||||||
|
*/
|
||||||
|
const handleStockLogSizeChange = async (row, size) => {
|
||||||
|
row.stock_log_pagination.page_size = size;
|
||||||
|
row.stock_log_pagination.page = 1; // 重置页码到第一页
|
||||||
|
await fetchStockLogs(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理历史操作记录分页的当前页码变化
|
||||||
|
* @param {object} row - 当前原料行数据
|
||||||
|
* @param {number} page - 新的页码
|
||||||
|
*/
|
||||||
|
const handleStockLogCurrentChange = async (row, page) => {
|
||||||
|
row.stock_log_pagination.page = page;
|
||||||
|
await fetchStockLogs(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理行点击事件,用于切换行的展开状态
|
||||||
|
const handleRowClick = (row, column) => {
|
||||||
|
// 如果点击的是操作列,则不执行任何操作
|
||||||
|
if (column && column.label === '操作') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 否则,切换行的展开状态
|
||||||
|
mainTable.value.toggleRowExpansion(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchStockList();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.stockFilter, (newFilter) => {
|
||||||
|
pagination.value.page = 1; // 筛选条件变化时重置页码
|
||||||
|
fetchStockList(newFilter, props.searchRawMaterialName);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.searchRawMaterialName, (newRawMaterialName) => {
|
||||||
|
pagination.value.page = 1; // 搜索条件变化时重置页码
|
||||||
|
fetchStockList(props.stockFilter, newRawMaterialName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 暴露 fetchStockList 方法给父组件
|
||||||
|
expose({
|
||||||
|
fetchStockList,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
stockList,
|
||||||
|
loading,
|
||||||
|
pagination,
|
||||||
|
mainTable, // 暴露 mainTable ref
|
||||||
|
handleSizeChange,
|
||||||
|
handleCurrentChange,
|
||||||
|
formatRFC3339,
|
||||||
|
formatWeight,
|
||||||
|
formatChangeAmount, // 暴露 formatChangeAmount
|
||||||
|
handleAdjust,
|
||||||
|
getStockLogSourceTypeLabel,
|
||||||
|
handleExpandChange,
|
||||||
|
handleStockLogSizeChange,
|
||||||
|
handleStockLogCurrentChange,
|
||||||
|
handleRowClick,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
25
src/enums.js
25
src/enums.js
@@ -79,6 +79,31 @@ export const StockLogSourceType = {
|
|||||||
FERMENT_END: '发酵入库', // 发酵料产出,作为新原料计入库存
|
FERMENT_END: '发酵入库', // 发酵料产出,作为新原料计入库存
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据库存变动来源的键或值获取其对应的中文标签。
|
||||||
|
* @param {string} sourceTypeKeyOrValue - 库存变动来源的键 (如 "PURCHASE") 或值 (如 "采购入库")。
|
||||||
|
* @returns {string} 对应的中文标签,如果未找到则返回 '--'。
|
||||||
|
*/
|
||||||
|
export function getStockLogSourceTypeLabel(sourceTypeKeyOrValue) {
|
||||||
|
if (!sourceTypeKeyOrValue) {
|
||||||
|
return '--';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试直接通过键查找
|
||||||
|
if (StockLogSourceType[sourceTypeKeyOrValue]) {
|
||||||
|
return StockLogSourceType[sourceTypeKeyOrValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试通过值反向查找
|
||||||
|
for (const key in StockLogSourceType) {
|
||||||
|
if (StockLogSourceType[key] === sourceTypeKeyOrValue) {
|
||||||
|
return StockLogSourceType[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '--';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用药原因
|
* 用药原因
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
|
|||||||
@@ -115,6 +115,12 @@
|
|||||||
</el-icon>
|
</el-icon>
|
||||||
<template #title>配方管理</template>
|
<template #title>配方管理</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/inventory/stock">
|
||||||
|
<el-icon>
|
||||||
|
<Tickets/>
|
||||||
|
</el-icon>
|
||||||
|
<template #title>库存管理</template>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
|
|
||||||
@@ -444,7 +450,7 @@ export default {
|
|||||||
|
|
||||||
const activeMenu = computed(() => {
|
const activeMenu = computed(() => {
|
||||||
const path = route.path;
|
const path = route.path;
|
||||||
if (path.startsWith('/monitor') || path.startsWith('/pms') || path.startsWith('/devices') || path.startsWith('/device-templates') || path.startsWith('/alarms') || path.startsWith('/feed')) {
|
if (path.startsWith('/monitor') || path.startsWith('/pms') || path.startsWith('/devices') || path.startsWith('/device-templates') || path.startsWith('/alarms') || path.startsWith('/feed') || path.startsWith('/inventory')) {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
return route.path;
|
return route.path;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import NutrientList from '../views/feed/NutrientList.vue'; // 修正拼写错误
|
|||||||
import PigAgeStageList from '../views/feed/PigAgeStageList.vue'; // 导入 PigAgeStageList 组件
|
import PigAgeStageList from '../views/feed/PigAgeStageList.vue'; // 导入 PigAgeStageList 组件
|
||||||
import PigBreedList from '../views/feed/PigBreedList.vue'; // 导入 PigBreedList 组件
|
import PigBreedList from '../views/feed/PigBreedList.vue'; // 导入 PigBreedList 组件
|
||||||
import RecipeList from '../views/feed/RecipeList.vue';
|
import RecipeList from '../views/feed/RecipeList.vue';
|
||||||
|
import StockManagement from '../views/inventory/StockManagement.vue';
|
||||||
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -45,6 +46,7 @@ const routes = [
|
|||||||
{path: '/feed/pig-age-stages', component: PigAgeStageList, meta: {requiresAuth: true, title: '年龄阶段管理'}}, // 添加年龄阶段管理路由
|
{path: '/feed/pig-age-stages', component: PigAgeStageList, meta: {requiresAuth: true, title: '年龄阶段管理'}}, // 添加年龄阶段管理路由
|
||||||
{path: '/feed/pig-breeds', component: PigBreedList, meta: {requiresAuth: true, title: '品种管理'}}, // 添加品种管理路由
|
{path: '/feed/pig-breeds', component: PigBreedList, meta: {requiresAuth: true, title: '品种管理'}}, // 添加品种管理路由
|
||||||
{path: '/feed/recipes', component: RecipeList, meta: {requiresAuth: true, title: '配方管理'}},
|
{path: '/feed/recipes', component: RecipeList, meta: {requiresAuth: true, title: '配方管理'}},
|
||||||
|
{path: '/inventory/stock', component: StockManagement, meta: {requiresAuth: true, title: '库存管理'}},
|
||||||
|
|
||||||
{path: '/monitor/device-command-logs', component: DeviceCommandLogView, meta: {requiresAuth: true, title: '设备命令日志'}},
|
{path: '/monitor/device-command-logs', component: DeviceCommandLogView, meta: {requiresAuth: true, title: '设备命令日志'}},
|
||||||
{path: '/monitor/medication-logs', component: MedicationLogsView, meta: {requiresAuth: true, title: '用药记录'}},
|
{path: '/monitor/medication-logs', component: MedicationLogsView, meta: {requiresAuth: true, title: '用药记录'}},
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
* @returns {string} - 格式化后的字符串,如果输入无效则返回空字符串或提示
|
* @returns {string} - 格式化后的字符串,如果输入无效则返回空字符串或提示
|
||||||
*/
|
*/
|
||||||
export function formatRFC3339(rfc3339String) {
|
export function formatRFC3339(rfc3339String) {
|
||||||
if (!rfc3339String) {
|
if (!rfc3339String || rfc3339String.startsWith('0001-01-01')) {
|
||||||
return '--'; // 或者返回空字符串 ''
|
return '--';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -18,5 +18,42 @@ export function formatRFC3339(rfc3339String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将以克为单位的重量转换为更合适的单位(克或千克)并格式化
|
||||||
|
* @param {number} grams - 以克为单位的重量
|
||||||
|
* @returns {string} - 格式化后的重量字符串 (例如 "1.5 kg" 或 "500 g")
|
||||||
|
*/
|
||||||
|
export function formatWeight(grams) {
|
||||||
|
if (typeof grams !== 'number' || isNaN(grams)) {
|
||||||
|
return '--';
|
||||||
|
}
|
||||||
|
if (grams >= 1000) {
|
||||||
|
return (grams / 1000).toFixed(2) + ' kg';
|
||||||
|
} else {
|
||||||
|
return grams + ' g';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化库存变动数量,显示正负号和单位 (kg 或 g)
|
||||||
|
* @param {number} changeAmount - 变动数量,单位为克 (正数入库,负数出库)
|
||||||
|
* @returns {string} - 格式化后的变动数量字符串 (例如 "+1.5 kg", "-500 g")
|
||||||
|
*/
|
||||||
|
export function formatChangeAmount(changeAmount) {
|
||||||
|
if (typeof changeAmount !== 'number' || isNaN(changeAmount)) {
|
||||||
|
return '--';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 明确处理正号、负号和零
|
||||||
|
const sign = changeAmount > 0 ? '+' : (changeAmount < 0 ? '-' : '');
|
||||||
|
const absoluteAmount = Math.abs(changeAmount);
|
||||||
|
|
||||||
|
if (absoluteAmount >= 1000) {
|
||||||
|
return sign + (absoluteAmount / 1000).toFixed(2) + ' kg';
|
||||||
|
} else {
|
||||||
|
return sign + absoluteAmount + ' g';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 你未来还可以添加其他全局格式化函数
|
// 你未来还可以添加其他全局格式化函数
|
||||||
// export function formatCurrency(number) { ... }
|
// export function formatCurrency(number) { ... }
|
||||||
@@ -107,7 +107,7 @@ export default {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
try {
|
try {
|
||||||
const response = await getRecipes({ page: 1, page_size: 999 });
|
const response = await getRecipes({ page: 1, page_size: 999, order_by: "id DESC" });
|
||||||
this.recipes = response.data.list || [];
|
this.recipes = response.data.list || [];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.error = err.message || '未知错误';
|
this.error = err.message || '未知错误';
|
||||||
|
|||||||
139
src/views/inventory/StockManagement.vue
Normal file
139
src/views/inventory/StockManagement.vue
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<div class="stock-management-container">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="title-container">
|
||||||
|
<h2 class="page-title">库存管理</h2>
|
||||||
|
<el-button type="text" @click="refreshList" class="refresh-btn" title="刷新库存列表">
|
||||||
|
<el-icon :size="20"><Refresh /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="filter-controls">
|
||||||
|
<el-input
|
||||||
|
v-model="searchRawMaterialName"
|
||||||
|
placeholder="按原料名称搜索"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="refreshList"
|
||||||
|
style="width: 200px; margin-right: 10px;"
|
||||||
|
></el-input>
|
||||||
|
<el-select v-model="stockFilter" placeholder="筛选库存" @change="refreshList" style="width: 150px;">
|
||||||
|
<el-option label="所有原料" value="all"></el-option>
|
||||||
|
<el-option label="有库存的原料" value="in_stock"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<stock-list-table
|
||||||
|
ref="stockListTableRef"
|
||||||
|
:stockFilter="stockFilter"
|
||||||
|
:searchRawMaterialName="searchRawMaterialName"
|
||||||
|
@adjust-stock="handleAdjustStock"
|
||||||
|
></stock-list-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 库存调整弹窗 -->
|
||||||
|
<stock-adjustment-dialog
|
||||||
|
v-model:visible="showAdjustmentDialog"
|
||||||
|
:rawMaterial="currentRawMaterialForAdjustment"
|
||||||
|
@success="refreshList"
|
||||||
|
></stock-adjustment-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Refresh } from '@element-plus/icons-vue';
|
||||||
|
import StockListTable from '../../components/inventory/StockListTable.vue';
|
||||||
|
import StockAdjustmentDialog from '../../components/inventory/StockAdjustmentDialog.vue'; // 导入库存调整弹窗组件
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StockManagement',
|
||||||
|
components: {
|
||||||
|
StockListTable,
|
||||||
|
Refresh,
|
||||||
|
StockAdjustmentDialog, // 注册组件
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const stockListTableRef = ref(null);
|
||||||
|
const stockFilter = ref('all'); // 默认显示所有原料
|
||||||
|
const searchRawMaterialName = ref(''); // 搜索原料名称
|
||||||
|
|
||||||
|
// 库存调整弹窗相关
|
||||||
|
const showAdjustmentDialog = ref(false);
|
||||||
|
const currentRawMaterialForAdjustment = ref(null);
|
||||||
|
// currentOperationType 不再需要在这里管理,由 StockAdjustmentDialog 内部管理
|
||||||
|
|
||||||
|
const refreshList = () => {
|
||||||
|
if (stockListTableRef.value) {
|
||||||
|
stockListTableRef.value.fetchStockList(stockFilter.value, searchRawMaterialName.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理库存调整操作
|
||||||
|
* @param {object} rawMaterial - 当前操作的原料数据
|
||||||
|
*/
|
||||||
|
const handleAdjustStock = (rawMaterial) => {
|
||||||
|
currentRawMaterialForAdjustment.value = { ...rawMaterial };
|
||||||
|
showAdjustmentDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
stockListTableRef,
|
||||||
|
stockFilter,
|
||||||
|
searchRawMaterialName,
|
||||||
|
refreshList,
|
||||||
|
// 库存调整弹窗相关
|
||||||
|
showAdjustmentDialog,
|
||||||
|
currentRawMaterialForAdjustment,
|
||||||
|
handleAdjustStock,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stock-management-container {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
color: black;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user