Compare commits
39 Commits
018f736d2e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f06688237 | |||
| d97a55a992 | |||
| e0729ce600 | |||
| 3ace1a35ee | |||
| 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 | |||
| 80ab64e428 | |||
| fded64ef8f | |||
| 6e2da3f3c2 | |||
| ba96470c8e | |||
| 5fb1808943 | |||
| 17ae47fa68 | |||
| 6387a5798b | |||
| cc9a85e85a | |||
| cb4da5effa | |||
| dd243ad2e7 |
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"schemes": [],
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "这是一个用于管理猪场设备的后端服务。",
|
||||
@@ -15,8 +14,6 @@
|
||||
},
|
||||
"version": "1.0"
|
||||
},
|
||||
"host": "",
|
||||
"basePath": "",
|
||||
"paths": {
|
||||
"/api/v1/alarm/threshold/active-alarms": {
|
||||
"get": {
|
||||
@@ -2722,6 +2719,18 @@
|
||||
],
|
||||
"summary": "获取原料列表",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "number",
|
||||
"description": "参考价格最大值",
|
||||
"name": "max_reference_price",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"description": "参考价格最小值",
|
||||
"name": "min_reference_price",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "按原料名称模糊查询",
|
||||
@@ -2793,7 +2802,7 @@
|
||||
"summary": "创建原料",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "原料信息",
|
||||
"description": "原料信息,包含名称、描述和参考价格",
|
||||
"name": "rawMaterial",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
@@ -2895,7 +2904,7 @@
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "更新后的原料信息",
|
||||
"description": "更新后的原料信息,包含名称、描述和参考价格",
|
||||
"name": "rawMaterial",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
@@ -3128,6 +3137,98 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/feed/recipes/generate-from-all-materials/{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/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}": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -3262,6 +3363,274 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/feed/recipes/{id}/ai-diagnose": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "使用AI对指定配方进行点评,并针对目标猪类型给出建议。",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"饲料管理-配方"
|
||||
],
|
||||
"summary": "AI点评配方",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "配方ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "猪类型ID",
|
||||
"name": "pig_type_id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "业务码为200代表AI点评成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/controller.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/dto.ReviewRecipeResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/inventory/stock/adjust": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "手动调整指定原料的库存量。",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"库存管理"
|
||||
],
|
||||
"summary": "调整原料库存",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "库存调整请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.StockAdjustmentRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "业务码为200代表调整成功",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/controller.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/dto.StockLogResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/inventory/stock/current": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "获取所有原料的当前库存列表,支持分页和过滤。",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"库存管理"
|
||||
],
|
||||
"summary": "获取当前库存列表",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "只查询有库存的原料",
|
||||
"name": "has_stock",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "排序字段, 例如 \"stock DESC\"",
|
||||
"name": "order_by",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "页码",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "每页数量",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "按原料名称模糊查询",
|
||||
"name": "raw_material_name",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "业务码为200代表成功获取列表",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/controller.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/dto.ListCurrentStockResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/inventory/stock/logs": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "获取原料库存变动历史记录,支持分页、过滤和时间范围查询。",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"库存管理"
|
||||
],
|
||||
"summary": "获取库存变动日志",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "结束时间 (RFC3339格式)",
|
||||
"name": "end_time",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "排序字段",
|
||||
"name": "order_by",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "页码",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "每页数量",
|
||||
"name": "page_size",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "按原料ID精确查询",
|
||||
"name": "raw_material_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"enum": [
|
||||
"采购入库",
|
||||
"饲喂出库",
|
||||
"变质出库",
|
||||
"售卖出库",
|
||||
"杂用领取",
|
||||
"手动盘点",
|
||||
"发酵出库",
|
||||
"发酵入库"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"description": "按来源类型查询",
|
||||
"name": "source_types",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "开始时间 (RFC3339格式, e.g., \"2023-01-01T00:00:00Z\")",
|
||||
"name": "start_time",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "业务码为200代表成功获取列表",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/controller.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/dto.ListStockLogResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/monitor/device-command-logs": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -6995,10 +7364,18 @@
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"max_addition_ratio": {
|
||||
"description": "最大添加比例",
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"description": "原料名称",
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
},
|
||||
"reference_price": {
|
||||
"description": "参考价格(kg/元)",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7057,6 +7434,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.CurrentStockResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"last_operation_source_type": {
|
||||
"description": "上次库存变动的来源类型",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.StockLogSourceType"
|
||||
}
|
||||
]
|
||||
},
|
||||
"last_updated": {
|
||||
"description": "最后更新时间",
|
||||
"type": "string"
|
||||
},
|
||||
"raw_material_id": {
|
||||
"description": "原料ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"raw_material_name": {
|
||||
"description": "原料名称",
|
||||
"type": "string"
|
||||
},
|
||||
"stock": {
|
||||
"description": "当前库存量, 单位: g",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.DeleteDeviceThresholdAlarmDTO": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -7188,6 +7594,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.GenerateRecipeResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "新生成的配方描述",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "新生成的配方ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "新生成的配方名称",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.HistoricalAlarmDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7254,6 +7677,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ListCurrentStockResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.CurrentStockResponse"
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"$ref": "#/definitions/dto.PaginationDTO"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ListDeviceCommandLogResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7535,6 +7972,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ListStockLogResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.StockLogResponse"
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"$ref": "#/definitions/dto.PaginationDTO"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ListTaskExecutionLogResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -8482,6 +8933,10 @@
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_addition_ratio": {
|
||||
"description": "最大添加比例",
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8491,6 +8946,10 @@
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.RawMaterialNutrientDTO"
|
||||
}
|
||||
},
|
||||
"reference_price": {
|
||||
"description": "参考价格(kg/元)",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8751,6 +9210,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ReviewRecipeResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ai_model": {
|
||||
"description": "使用的 AI 模型",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.AIModel"
|
||||
}
|
||||
]
|
||||
},
|
||||
"review_message": {
|
||||
"description": "点评内容",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.SellPigsRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -8847,6 +9323,76 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.StockAdjustmentRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"change_amount",
|
||||
"raw_material_id",
|
||||
"source_type"
|
||||
],
|
||||
"properties": {
|
||||
"change_amount": {
|
||||
"description": "变动数量, 正数为入库, 负数为出库, 单位: g",
|
||||
"type": "number"
|
||||
},
|
||||
"raw_material_id": {
|
||||
"description": "要调整的原料ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"remarks": {
|
||||
"description": "备注",
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"source_id": {
|
||||
"description": "来源ID, 例如: 配方ID, 采购单ID等",
|
||||
"type": "integer"
|
||||
},
|
||||
"source_type": {
|
||||
"description": "库存变动来源类型",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.StockLogSourceType"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.StockLogResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"after_quantity": {
|
||||
"type": "number"
|
||||
},
|
||||
"before_quantity": {
|
||||
"type": "number"
|
||||
},
|
||||
"change_amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"happened_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"raw_material_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"raw_material_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"remarks": {
|
||||
"type": "string"
|
||||
},
|
||||
"source_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"source_type": {
|
||||
"$ref": "#/definitions/models.StockLogSourceType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.SubPlanResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -9461,10 +10007,18 @@
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"max_addition_ratio": {
|
||||
"description": "最大添加比例",
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"description": "原料名称",
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
},
|
||||
"reference_price": {
|
||||
"description": "参考价格(kg/元)",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -9592,6 +10146,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.AIModel": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Gemini"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"AI_MODEL_GEMINI"
|
||||
]
|
||||
},
|
||||
"models.AlarmCode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -10066,6 +10629,43 @@
|
||||
"FatalLevel"
|
||||
]
|
||||
},
|
||||
"models.StockLogSourceType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"采购入库",
|
||||
"饲喂出库",
|
||||
"变质出库",
|
||||
"售卖出库",
|
||||
"杂用领取",
|
||||
"手动盘点",
|
||||
"发酵出库",
|
||||
"发酵入库"
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"StockLogSourceFermentEnd": "发酵料产出,作为新原料计入库存",
|
||||
"StockLogSourceFermentStart": "原料投入发酵,从库存中扣除"
|
||||
},
|
||||
"x-enum-descriptions": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"原料投入发酵,从库存中扣除",
|
||||
"发酵料产出,作为新原料计入库存"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"StockLogSourcePurchase",
|
||||
"StockLogSourceFeeding",
|
||||
"StockLogSourceDeteriorate",
|
||||
"StockLogSourceSale",
|
||||
"StockLogSourceMiscellaneous",
|
||||
"StockLogSourceManual",
|
||||
"StockLogSourceFermentStart",
|
||||
"StockLogSourceFermentEnd"
|
||||
]
|
||||
},
|
||||
"models.TaskType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
62
node_modules/.package-lock.json
generated
vendored
62
node_modules/.package-lock.json
generated
vendored
@@ -46,6 +46,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.4.tgz",
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -1908,6 +1909,7 @@
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
@@ -2063,6 +2065,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@vue/cli-service/-/cli-service-5.0.9.tgz",
|
||||
"integrity": "sha512-yTX7GVyM19tEbd+y5/gA6MkVKA6K61nVYHYAivD61Hx6odVFmQsaC3/R3cWAHM1P5oVKCevBbumPljbT+tFG2w==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-compilation-targets": "^7.12.16",
|
||||
"@soda/friendly-errors-webpack-plugin": "^1.8.0",
|
||||
@@ -2747,6 +2750,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2801,6 +2805,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -3268,6 +3273,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -3878,6 +3884,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/css-loader/-/css-loader-6.11.0.tgz",
|
||||
"integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"icss-utils": "^5.1.0",
|
||||
"postcss": "^8.4.33",
|
||||
@@ -4632,6 +4639,7 @@
|
||||
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -5292,6 +5300,21 @@
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"ideallyInert": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -6425,12 +6448,14 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash-unified": {
|
||||
"version": "1.0.3",
|
||||
@@ -6660,6 +6685,18 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/marked/-/marked-17.0.1.tgz",
|
||||
"integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -7571,6 +7608,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -8794,6 +8832,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -9676,20 +9715,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.12.0",
|
||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.12.0.tgz",
|
||||
@@ -9846,6 +9871,7 @@
|
||||
"version": "3.5.21",
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.21.tgz",
|
||||
"integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.21",
|
||||
"@vue/compiler-sfc": "3.5.21",
|
||||
@@ -9941,6 +9967,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
|
||||
"integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"de-indent": "^1.0.2",
|
||||
"he": "^1.2.0"
|
||||
@@ -9999,6 +10026,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.101.3.tgz",
|
||||
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
@@ -10096,6 +10124,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/webpack-cli/-/webpack-cli-5.1.4.tgz",
|
||||
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "^0.5.0",
|
||||
"@webpack-cli/configtest": "^2.1.1",
|
||||
@@ -10267,6 +10296,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.3.3.tgz",
|
||||
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
|
||||
48
package-lock.json
generated
48
package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"axios": "^1.6.0",
|
||||
"cron-parser": "^5.4.0",
|
||||
"element-plus": "^2.4.0",
|
||||
"marked": "^17.0.1",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"vue3-cron-plus-picker": "^1.0.2"
|
||||
@@ -74,6 +75,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.4.tgz",
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -1936,6 +1938,7 @@
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
@@ -2091,6 +2094,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@vue/cli-service/-/cli-service-5.0.9.tgz",
|
||||
"integrity": "sha512-yTX7GVyM19tEbd+y5/gA6MkVKA6K61nVYHYAivD61Hx6odVFmQsaC3/R3cWAHM1P5oVKCevBbumPljbT+tFG2w==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-compilation-targets": "^7.12.16",
|
||||
"@soda/friendly-errors-webpack-plugin": "^1.8.0",
|
||||
@@ -2775,6 +2779,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2829,6 +2834,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -3296,6 +3302,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -3906,6 +3913,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/css-loader/-/css-loader-6.11.0.tgz",
|
||||
"integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"icss-utils": "^5.1.0",
|
||||
"postcss": "^8.4.33",
|
||||
@@ -4660,6 +4668,7 @@
|
||||
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -6467,12 +6476,14 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash-unified": {
|
||||
"version": "1.0.3",
|
||||
@@ -6702,6 +6713,18 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/marked/-/marked-17.0.1.tgz",
|
||||
"integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -7613,6 +7636,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -8836,6 +8860,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -9718,20 +9743,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.12.0",
|
||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.12.0.tgz",
|
||||
@@ -9888,6 +9899,7 @@
|
||||
"version": "3.5.21",
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.21.tgz",
|
||||
"integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.21",
|
||||
"@vue/compiler-sfc": "3.5.21",
|
||||
@@ -9983,6 +9995,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
|
||||
"integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"de-indent": "^1.0.2",
|
||||
"he": "^1.2.0"
|
||||
@@ -10041,6 +10054,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.101.3.tgz",
|
||||
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
@@ -10138,6 +10152,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/webpack-cli/-/webpack-cli-5.1.4.tgz",
|
||||
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "^0.5.0",
|
||||
"@webpack-cli/configtest": "^2.1.1",
|
||||
@@ -10309,6 +10324,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.3.3.tgz",
|
||||
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"axios": "^1.6.0",
|
||||
"cron-parser": "^5.4.0",
|
||||
"element-plus": "^2.4.0",
|
||||
"marked": "^17.0.1",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"vue3-cron-plus-picker": "^1.0.2"
|
||||
|
||||
@@ -239,6 +239,8 @@ import {PaginationDTO, Response} from '../enums';
|
||||
* @property {string} name
|
||||
* @property {string} description
|
||||
* @property {Array<RawMaterialNutrientDTO>} raw_material_nutrients
|
||||
* @property {number} [reference_price] - 参考价格(kg/元)
|
||||
* @property {number} [max_addition_ratio] - 最大添加比例(%)
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -251,6 +253,8 @@ import {PaginationDTO, Response} from '../enums';
|
||||
* @typedef {object} RawMaterialsParams
|
||||
* @property {string} [name] - 按原料名称模糊查询
|
||||
* @property {string} [nutrient_name] - 按营养名称模糊查询
|
||||
* @property {number} [max_reference_price] - 参考价格最大值
|
||||
* @property {number} [min_reference_price] - 参考价格最小值
|
||||
* @property {string} [order_by] - 排序字段,例如 "id DESC"
|
||||
* @property {number} [page]
|
||||
* @property {number} [page_size]
|
||||
@@ -260,12 +264,16 @@ import {PaginationDTO, Response} from '../enums';
|
||||
* @typedef {object} CreateRawMaterialRequest
|
||||
* @property {string} name - 原料名称
|
||||
* @property {string} [description] - 描述
|
||||
* @property {number} [reference_price] - 参考价格(kg/元)
|
||||
* @property {number} [max_addition_ratio] - 最大添加比例(%)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} UpdateRawMaterialRequest
|
||||
* @property {string} name - 原料名称
|
||||
* @property {string} [description] - 描述
|
||||
* @property {number} [reference_price] - 参考价格(kg/元)
|
||||
* @property {number} [max_addition_ratio] - 最大添加比例(%)
|
||||
*/
|
||||
|
||||
// --- Recipe ---
|
||||
@@ -273,7 +281,7 @@ import {PaginationDTO, Response} from '../enums';
|
||||
/**
|
||||
* @typedef {object} RecipeIngredientDto
|
||||
* @property {number} raw_material_id - 原料ID
|
||||
* @property {number} [percentage] - 原料在配方中的百分比 (0-1之间)
|
||||
* @property {number} [percentage] - 原料在配方中的百分比 (0-100之间)
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -312,6 +320,24 @@ import {PaginationDTO, Response} from '../enums';
|
||||
* @property {Array<RecipeIngredientDto>} [recipe_ingredients] - 配方原料组成
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} GenerateRecipeResponse
|
||||
* @property {number} id - 新生成的配方ID
|
||||
* @property {string} name - 新生成的配方名称
|
||||
* @property {string} description - 新生成的配方描述
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {string} AIModel - AI模型枚举
|
||||
* @enum {string}
|
||||
* @property {"Gemini"} Gemini - Gemini模型
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} ReviewRecipeResponse
|
||||
* @property {AIModel} ai_model - 使用的 AI 模型
|
||||
* @property {string} review_message - 点评内容
|
||||
*/
|
||||
|
||||
// --- API Functions ---
|
||||
|
||||
@@ -623,6 +649,34 @@ export const deleteRecipe = (id) => {
|
||||
return http.delete(`/api/v1/feed/recipes/${id}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用系统中所有可用的原料一键生成配方
|
||||
* @param {number} pigTypeId - 猪类型ID
|
||||
* @returns {Promise<Response<GenerateRecipeResponse>>}
|
||||
*/
|
||||
export const generateRecipeFromAllMaterials = (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}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用AI对指定配方进行点评,并针对目标猪类型给出建议。
|
||||
* @param {number} id - 配方ID
|
||||
* @param {number} pigTypeId - 猪类型ID
|
||||
* @returns {Promise<Response<ReviewRecipeResponse>>}
|
||||
*/
|
||||
export const aiDiagnoseRecipe = (id, pigTypeId) => {
|
||||
return http.get(`/api/v1/feed/recipes/${id}/ai-diagnose`, {params: {pig_type_id: pigTypeId}, timeout: 0});
|
||||
};
|
||||
|
||||
|
||||
export const FeedApi = {
|
||||
getNutrients,
|
||||
@@ -657,4 +711,7 @@ export const FeedApi = {
|
||||
getRecipeById,
|
||||
updateRecipe,
|
||||
deleteRecipe,
|
||||
generateRecipeFromAllMaterials,
|
||||
generatePrioritizedStockRecipe,
|
||||
aiDiagnoseRecipe,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AlarmApi } from './alarm.js'; // 导入告警API
|
||||
import { HealthApi } from './health.js'; // 导入健康检查API
|
||||
import { DeviceTemplateApi } from './deviceTemplate.js'; // 导入设备模板API
|
||||
import { FeedApi } from './feed.js'; // 导入饲料管理API
|
||||
import { InventoryApi } from './inventory.js'; // 导入库存管理API
|
||||
|
||||
/**
|
||||
* API客户端
|
||||
@@ -19,6 +20,7 @@ export class ApiClient {
|
||||
this.alarms = AlarmApi; // 添加告警API
|
||||
this.deviceTemplates = DeviceTemplateApi; // 添加设备模板API
|
||||
this.feeds = FeedApi; // 添加饲料管理API
|
||||
this.inventory = InventoryApi; // 添加库存管理API
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
108
src/api/inventory.js
Normal file
108
src/api/inventory.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import http from '../utils/http';
|
||||
import { PaginationDTO, Response, StockLogSourceType } from '../enums';
|
||||
|
||||
// --- Typedefs for Inventory Management ---
|
||||
|
||||
/**
|
||||
* @typedef {object} StockAdjustmentRequest
|
||||
* @property {number} change_amount - 变动数量, 正数为入库, 负数为出库, 单位: g
|
||||
* @property {number} raw_material_id - 要调整的原料ID
|
||||
* @property {StockLogSourceType} source_type - 库存变动来源类型
|
||||
* @property {number} [source_id] - 来源ID, 例如: 配方ID, 采购单ID等
|
||||
* @property {string} [remarks] - 备注
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} StockLogResponse
|
||||
* @property {number} after_quantity
|
||||
* @property {number} before_quantity
|
||||
* @property {number} change_amount
|
||||
* @property {string} happened_at
|
||||
* @property {number} id
|
||||
* @property {number} raw_material_id
|
||||
* @property {string} raw_material_name
|
||||
* @property {string} remarks
|
||||
* @property {number} source_id
|
||||
* @property {StockLogSourceType} source_type
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} ListStockLogResponse
|
||||
* @property {Array<StockLogResponse>} list
|
||||
* @property {PaginationDTO} pagination
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} CurrentStockResponse
|
||||
* @property {string} last_updated - 最后更新时间
|
||||
* @property {number} raw_material_id - 原料ID
|
||||
* @property {string} raw_material_name - 原料名称
|
||||
* @property {number} stock - 当前库存量, 单位: g
|
||||
* @property {StockLogSourceType} [last_operation_source_type] - 上次库存变动的来源类型
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} ListCurrentStockResponse
|
||||
* @property {Array<CurrentStockResponse>} list
|
||||
* @property {PaginationDTO} pagination
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} AdjustStockParams
|
||||
* @property {StockAdjustmentRequest} request - 库存调整请求
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} GetCurrentStockListParams
|
||||
* @property {boolean} [has_stock] - 只查询有库存的原料
|
||||
* @property {string} [order_by] - 排序字段, 例如 "stock DESC"
|
||||
* @property {number} [page] - 页码
|
||||
* @property {number} [page_size] - 每页数量
|
||||
* @property {string} [raw_material_name] - 按原料名称模糊查询
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} GetStockLogListParams
|
||||
* @property {string} [end_time] - 结束时间 (RFC3339格式)
|
||||
* @property {string} [order_by] - 排序字段
|
||||
* @property {number} [page] - 页码
|
||||
* @property {number} [page_size] - 每页数量
|
||||
* @property {number} [raw_material_id] - 按原料ID精确查询
|
||||
* @property {Array<StockLogSourceType>} [source_types] - 按来源类型查询
|
||||
* @property {string} [start_time] - 开始时间 (RFC3339格式, e.g., "2023-01-01T00:00:00Z")
|
||||
*/
|
||||
|
||||
// --- API Functions ---
|
||||
|
||||
/**
|
||||
* 调整原料库存
|
||||
* @param {StockAdjustmentRequest} data - 库存调整请求
|
||||
* @returns {Promise<Response<StockLogResponse>>}
|
||||
*/
|
||||
export const adjustStock = (data) => {
|
||||
return http.post('/api/v1/inventory/stock/adjust', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前库存列表
|
||||
* @param {GetCurrentStockListParams} params - 查询参数
|
||||
* @returns {Promise<Response<ListCurrentStockResponse>>}
|
||||
*/
|
||||
export const getCurrentStockList = (params) => {
|
||||
return http.get('/api/v1/inventory/stock/current', { params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取库存变动日志
|
||||
* @param {GetStockLogListParams} params - 查询参数
|
||||
* @returns {Promise<Response<ListStockLogResponse>>}
|
||||
*/
|
||||
export const getStockLogList = (params) => {
|
||||
return http.get('/api/v1/inventory/stock/logs', { params });
|
||||
};
|
||||
|
||||
export const InventoryApi = {
|
||||
adjustStock,
|
||||
getCurrentStockList,
|
||||
getStockLogList,
|
||||
};
|
||||
283
src/components/feed/AIRecipeReviewDialog.vue
Normal file
283
src/components/feed/AIRecipeReviewDialog.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="AI点评配方"
|
||||
width="80%"
|
||||
:before-close="handleClose"
|
||||
destroy-on-close
|
||||
align-center
|
||||
>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="配方名称:">
|
||||
<span>{{ recipe ? recipe.name : 'N/A' }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="目标猪类型:">
|
||||
<el-select
|
||||
v-model="selectedPigTypeId"
|
||||
placeholder="请选择目标猪类型"
|
||||
filterable
|
||||
:loading="loadingPigTypes"
|
||||
:disabled="loadingAIReview"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in pigTypes"
|
||||
:key="item.id"
|
||||
:label="item.label"
|
||||
:value="item.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 将AI点评结果移出el-form -->
|
||||
<div v-if="aiReviewResult || error" style="margin-top: 20px;">
|
||||
<label class="el-form-item__label" style="width: 100px;">AI点评结果:</label>
|
||||
<div class="ai-review-content-wrapper" style="margin-top: 10px;">
|
||||
<el-alert
|
||||
v-if="error"
|
||||
:title="error"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
></el-alert>
|
||||
<div v-else-if="aiReviewResult" class="ai-review-content">
|
||||
<p style="text-align: center; margin-bottom: 20px; color: #888;">
|
||||
由 <strong>{{ aiReviewResult.ai_model }}</strong> 生成
|
||||
</p>
|
||||
<div v-html="renderedReview" class="markdown-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose" :disabled="loadingAIReview">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:loading="loadingAIReview"
|
||||
:disabled="!selectedPigTypeId || loadingPigTypes"
|
||||
>
|
||||
{{ aiReviewResult ? '重新点评' : '开始点评' }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getPigTypes, aiDiagnoseRecipe } from '../../api/feed';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { marked } from 'marked';
|
||||
|
||||
export default {
|
||||
name: 'AIRecipeReviewDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
recipe: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['update:visible', 'cancel'],
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: this.visible,
|
||||
pigTypes: [], // 格式化后的猪类型列表
|
||||
selectedPigTypeId: null,
|
||||
aiReviewResult: null,
|
||||
loadingPigTypes: false,
|
||||
loadingAIReview: false,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
renderedReview() {
|
||||
if (this.aiReviewResult && this.aiReviewResult.review_message) {
|
||||
return marked(this.aiReviewResult.review_message);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
this.dialogVisible = newVal;
|
||||
if (newVal) {
|
||||
this.resetState();
|
||||
this.loadPigTypes();
|
||||
}
|
||||
},
|
||||
dialogVisible(newVal) {
|
||||
this.$emit('update:visible', newVal);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 重置组件内部状态
|
||||
*/
|
||||
resetState() {
|
||||
this.selectedPigTypeId = null;
|
||||
this.aiReviewResult = null;
|
||||
this.error = null;
|
||||
this.pigTypes = [];
|
||||
},
|
||||
/**
|
||||
* 加载猪类型列表
|
||||
*/
|
||||
async loadPigTypes() {
|
||||
this.loadingPigTypes = true;
|
||||
this.error = null;
|
||||
try {
|
||||
// 获取所有猪类型,假设一次性获取足够的数据
|
||||
const response = await getPigTypes({ page: 1, page_size: 1000 });
|
||||
if (response.code === 2000 && response.data && response.data.list) {
|
||||
this.pigTypes = response.data.list.map(type => ({
|
||||
id: type.id,
|
||||
label: `${type.breed_name} - ${type.age_stage_name}`,
|
||||
}));
|
||||
} else {
|
||||
this.error = response.msg || '获取猪类型失败';
|
||||
ElMessage.error(this.error);
|
||||
}
|
||||
} catch (err) {
|
||||
this.error = '加载猪类型失败: ' + (err.message || '未知错误');
|
||||
ElMessage.error(this.error);
|
||||
console.error('加载猪类型失败:', err);
|
||||
} finally {
|
||||
this.loadingPigTypes = false;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 处理确认按钮点击,调用AI点评接口
|
||||
*/
|
||||
async handleConfirm() {
|
||||
if (!this.recipe || !this.recipe.id) {
|
||||
ElMessage.warning('未选择配方');
|
||||
return;
|
||||
}
|
||||
if (!this.selectedPigTypeId) {
|
||||
ElMessage.warning('请选择目标猪类型');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingAIReview = true;
|
||||
this.aiReviewResult = null;
|
||||
this.error = null;
|
||||
try {
|
||||
const response = await aiDiagnoseRecipe(this.recipe.id, this.selectedPigTypeId);
|
||||
if (response.code === 2000 && response.data) {
|
||||
this.aiReviewResult = response.data;
|
||||
ElMessage.success('AI点评成功');
|
||||
} else {
|
||||
this.error = response.msg || 'AI点评失败';
|
||||
ElMessage.error(this.error);
|
||||
}
|
||||
} catch (err) {
|
||||
this.error = 'AI点评请求失败: ' + (err.message || '未知错误');
|
||||
ElMessage.error(this.error);
|
||||
console.error('AI点评请求失败:', err);
|
||||
} finally {
|
||||
this.loadingAIReview = false;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 处理对话框关闭
|
||||
*/
|
||||
handleClose() {
|
||||
this.dialogVisible = false;
|
||||
this.$emit('cancel');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ai-review-content {
|
||||
background-color: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-body :deep(h1),
|
||||
.markdown-body :deep(h2),
|
||||
.markdown-body :deep(h3) {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.markdown-body :deep(h1) {
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: .3em;
|
||||
}
|
||||
|
||||
.markdown-body :deep(h2) {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: .3em;
|
||||
}
|
||||
|
||||
.markdown-body :deep(h3) {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.markdown-body :deep(p) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(ul),
|
||||
.markdown-body :deep(ol) {
|
||||
padding-left: 2em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(li) {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.markdown-body :deep(table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(th),
|
||||
.markdown-body :deep(td) {
|
||||
border: 1px solid #dfe2e5;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(th) {
|
||||
background-color: #f6f8fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body :deep(blockquote) {
|
||||
color: #6a737d;
|
||||
border-left: .25em solid #dfe2e5;
|
||||
padding: 0 1em;
|
||||
margin-left: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(code) {
|
||||
padding: .2em .4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: rgba(27,31,35,.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
178
src/components/feed/GenerateRecipeDialog.vue
Normal file
178
src/components/feed/GenerateRecipeDialog.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
title="一键生成配方"
|
||||
v-model="dialogVisible"
|
||||
width="500px"
|
||||
:before-close="handleCancel"
|
||||
>
|
||||
<el-form :model="form" ref="generateRecipeForm" label-width="100px">
|
||||
<el-form-item label="生成对象" prop="selectedPigType"
|
||||
:rules="[{ required: true, message: '请选择生成对象', trigger: 'change' }]">
|
||||
<el-select v-model="form.selectedPigType" placeholder="请选择猪品种-猪年龄阶段" style="width: 100%;">
|
||||
<el-option
|
||||
v-for="item in pigTypesOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="生成方式" prop="selectedGenerationMethod"
|
||||
:rules="[{ required: true, message: '请选择生成方式', trigger: 'change' }]">
|
||||
<el-select v-model="form.selectedGenerationMethod" placeholder="请选择生成方式" style="width: 100%;">
|
||||
<el-option label="使用系统中所有可用的原料" value="all_raw_materials"></el-option>
|
||||
<!-- 新增选项 -->
|
||||
<el-option label="优先使用有库存的原料" value="prefer_in_stock_materials"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleGenerate" :loading="loading">生成</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ref, reactive, watch, onMounted, computed} from 'vue';
|
||||
import {ElMessage} from 'element-plus';
|
||||
import {FeedApi} from '../../api/feed'; // 假设 FeedApi 包含生成配方接口
|
||||
|
||||
export default {
|
||||
name: 'GenerateRecipeDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:visible', 'success', 'cancel'],
|
||||
setup(props, {emit}) {
|
||||
const generateRecipeForm = ref(null);
|
||||
const loading = ref(false);
|
||||
const pigTypesOptions = ref([]);
|
||||
|
||||
const form = reactive({
|
||||
selectedPigType: '',
|
||||
selectedGenerationMethod: 'all_raw_materials', // 默认选中
|
||||
});
|
||||
|
||||
// 计算属性,用于控制 dialog 的显示
|
||||
const dialogVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit('update:visible', val),
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取猪种类数据并格式化为下拉选项
|
||||
*/
|
||||
const fetchPigTypes = async () => {
|
||||
try {
|
||||
const response = await FeedApi.getPigTypes({page: 1, page_size: 999}); // 调用 FeedApi 中的 getPigTypes 方法获取猪类型列表
|
||||
if (response.data && response.data.list) {
|
||||
pigTypesOptions.value = response.data.list.map(pigType => ({
|
||||
label: `${pigType.breed_name}-${pigType.age_stage_name}`,
|
||||
value: `${pigType.id}`, // 下拉框的值直接是 pigType 的 ID
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取猪种类失败:', error);
|
||||
ElMessage.error('获取猪种类失败');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理生成配方逻辑
|
||||
*/
|
||||
const handleGenerate = async () => {
|
||||
if (!generateRecipeForm.value) return;
|
||||
|
||||
generateRecipeForm.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const pigTypeId = parseInt(form.selectedPigType); // 获取选中的 pigType ID
|
||||
let response = null; // 声明 response 变量
|
||||
|
||||
if (form.selectedGenerationMethod === 'all_raw_materials') {
|
||||
// 调用使用所有原料生成配方的接口
|
||||
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) {
|
||||
ElMessage.success('配方生成成功!');
|
||||
emit('success', response.data.name, response.data.description); // 传递配方名称和简介
|
||||
dialogVisible.value = false; // 关闭弹窗
|
||||
} else {
|
||||
ElMessage.error('配方生成失败:未知错误');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('一键生成配方失败:', error);
|
||||
ElMessage.error('一键生成配方失败: ' + (error.response?.data?.message || error.message || '未知错误'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
ElMessage.warning('请检查表单填写');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理取消操作
|
||||
*/
|
||||
const handleCancel = () => {
|
||||
dialogVisible.value = false;
|
||||
emit('cancel');
|
||||
// 重置表单字段
|
||||
if (generateRecipeForm.value) {
|
||||
generateRecipeForm.value.resetFields();
|
||||
}
|
||||
form.selectedPigType = ''; // 手动清空,因为 resetFields 不会清空未绑定 prop 的字段
|
||||
};
|
||||
|
||||
// 监听 visible 变化,当弹窗打开时加载数据
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
fetchPigTypes();
|
||||
// 每次打开时重置表单
|
||||
if (generateRecipeForm.value) {
|
||||
generateRecipeForm.value.resetFields();
|
||||
}
|
||||
form.selectedPigType = '';
|
||||
form.selectedGenerationMethod = 'all_raw_materials';
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 首次加载时也获取一次,以防万一
|
||||
// fetchPigTypes(); // 移到 watch 中,确保每次打开弹窗都刷新数据
|
||||
});
|
||||
|
||||
return {
|
||||
generateRecipeForm,
|
||||
dialogVisible,
|
||||
form,
|
||||
pigTypesOptions,
|
||||
loading,
|
||||
handleGenerate,
|
||||
handleCancel,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -40,7 +40,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 移除 ID 列 -->
|
||||
<el-table-column prop="name" label="营养名称"></el-table-column>
|
||||
<el-table-column prop="name" label="营养名称" width="250"></el-table-column>
|
||||
<el-table-column prop="description" label="描述"></el-table-column>
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="年龄阶段名称"></el-table-column>
|
||||
<el-table-column prop="name" label="年龄阶段名称" width="150"></el-table-column>
|
||||
<el-table-column prop="description" label="描述"></el-table-column>
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
@@ -199,12 +199,23 @@ export default {
|
||||
};
|
||||
|
||||
// 处理查看营养需求详情
|
||||
const handleViewNutrientRequirements = (pigType) => {
|
||||
currentNutrientRequirements.value = pigType.pig_nutrient_requirements || [];
|
||||
currentBreedName.value = pigType.breed_name;
|
||||
currentAgeStageName.value = pigType.age_stage_name;
|
||||
currentPigTypeId.value = pigType.id; // 设置当前的 pigType ID
|
||||
showNutrientDialog.value = true;
|
||||
const handleViewNutrientRequirements = async (pigType) => { // 添加 async
|
||||
try {
|
||||
// 强制重新获取该 pigType 的最新详情
|
||||
const response = await FeedApi.getPigTypeById(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;
|
||||
} else {
|
||||
ElMessage.error('获取猪类型详情失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取猪类型详情失败:', error);
|
||||
ElMessage.error('获取猪类型详情失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (row) => {
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="品种名称"></el-table-column>
|
||||
<el-table-column prop="name" label="品种名称" width="150"></el-table-column>
|
||||
<el-table-column prop="description" label="描述"></el-table-column>
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
@@ -275,12 +275,23 @@ export default {
|
||||
};
|
||||
|
||||
// 处理查看营养需求详情
|
||||
const handleViewNutrientRequirements = (pigType) => {
|
||||
currentNutrientRequirements.value = pigType.pig_nutrient_requirements || [];
|
||||
currentBreedName.value = pigType.breed_name;
|
||||
currentAgeStageName.value = pigType.age_stage_name;
|
||||
currentPigTypeId.value = pigType.id; // 设置当前的 pigType ID
|
||||
showNutrientDialog.value = true;
|
||||
const handleViewNutrientRequirements = async (pigType) => { // 添加 async
|
||||
try {
|
||||
// 强制重新获取该 pigType 的最新详情
|
||||
const response = await FeedApi.getPigTypeById(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;
|
||||
} else {
|
||||
ElMessage.error('获取猪类型详情失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取猪类型详情失败:', error);
|
||||
ElMessage.error('获取猪类型详情失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (row) => {
|
||||
|
||||
@@ -132,7 +132,7 @@ export default {
|
||||
|
||||
try {
|
||||
// 调用API更新营养需求
|
||||
await FeedApi.updatePigNutrientRequirements(props.pigTypeId, { pig_nutrient_requirements: requirementsToSave });
|
||||
await FeedApi.updatePigTypeNutrientRequirements(props.pigTypeId, { nutrient_requirements: requirementsToSave });
|
||||
ElMessage.success('营养需求更新成功');
|
||||
emit('save'); // 通知父组件保存成功
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<el-form :model="formData" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form :model="formData" :rules="rules" ref="formRef" label-width="150px">
|
||||
<el-form-item label="原料名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入原料名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="参考价格(kg/元)" prop="reference_price">
|
||||
<el-input-number v-model="formData.reference_price" :min="0" :precision="2" :step="0.01" controls-position="right" style="width: 100%;"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="最大添加比例(%)" prop="max_addition_ratio">
|
||||
<el-input-number v-model="formData.max_addition_ratio" :min="0" :max="100" :precision="2" :step="1" controls-position="right" style="width: 100%;"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
@@ -34,6 +40,8 @@ export default {
|
||||
default: () => ({
|
||||
name: '',
|
||||
description: '',
|
||||
reference_price: 0,
|
||||
max_addition_ratio: 0,
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -43,6 +51,8 @@ export default {
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
reference_price: 0,
|
||||
max_addition_ratio: 0,
|
||||
});
|
||||
|
||||
// 监听 initialData 变化,用于编辑模式下初始化表单
|
||||
@@ -52,6 +62,8 @@ export default {
|
||||
if (newVal) {
|
||||
formData.name = newVal.name || '';
|
||||
formData.description = newVal.description || '';
|
||||
formData.reference_price = newVal.reference_price || 0;
|
||||
formData.max_addition_ratio = newVal.max_addition_ratio || 0;
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
@@ -62,6 +74,15 @@ export default {
|
||||
{ required: true, message: '请输入原料名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
reference_price: [
|
||||
{ required: true, message: '请输入参考价格', trigger: 'change' },
|
||||
{ type: 'number', message: '参考价格必须是数字', trigger: 'change' },
|
||||
{ min: 0, type: 'number', message: '参考价格不能小于0', trigger: 'change' },
|
||||
],
|
||||
max_addition_ratio: [
|
||||
{ required: true, message: '请输入最大添加比例', trigger: 'change' },
|
||||
{ type: 'number', min: 0, max: 100, message: '比例必须在 0 到 100 之间', trigger: 'change' },
|
||||
],
|
||||
};
|
||||
|
||||
const submitForm = () => {
|
||||
@@ -85,6 +106,8 @@ export default {
|
||||
// 手动重置 formData,因为 resetFields 不会重置未绑定 prop 的字段
|
||||
formData.name = '';
|
||||
formData.description = '';
|
||||
formData.reference_price = 0;
|
||||
formData.max_addition_ratio = 0;
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -38,7 +38,14 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="原料名称"></el-table-column>
|
||||
<el-table-column prop="name" label="原料名称" width="150"></el-table-column>
|
||||
<el-table-column prop="reference_price" label="参考价格(kg/元)" width="180"></el-table-column>
|
||||
<el-table-column
|
||||
prop="max_addition_ratio"
|
||||
label="最大添加比例(%)"
|
||||
width="180"
|
||||
:formatter="formatMaxAdditionRatio"
|
||||
></el-table-column>
|
||||
<el-table-column prop="description" label="描述"></el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
@@ -147,7 +154,7 @@ export default {
|
||||
// 处理行点击事件
|
||||
const handleRowClick = (row, column) => {
|
||||
// 如果点击的是操作列,则不执行任何操作
|
||||
if (column.label === '操作') {
|
||||
if (column && column.label === '操作') {
|
||||
return;
|
||||
}
|
||||
// 否则,切换行的展开状态
|
||||
@@ -184,6 +191,13 @@ export default {
|
||||
emit('edit-nutrients', row); // 触发 edit-nutrients 事件
|
||||
};
|
||||
|
||||
const formatMaxAdditionRatio = (row, column, cellValue) => {
|
||||
if (typeof cellValue === 'number') {
|
||||
return cellValue.toFixed(2);
|
||||
}
|
||||
return cellValue;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchRawMaterials();
|
||||
});
|
||||
@@ -205,6 +219,7 @@ export default {
|
||||
handleDelete,
|
||||
handleEditNutrients,
|
||||
fetchRawMaterials, // 将方法暴露出去
|
||||
formatMaxAdditionRatio,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
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
@@ -1,41 +1,42 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
:title="`配方详情: ${recipe ? recipe.name : ''}` + (isEditing ? ' (编辑中)' : '')"
|
||||
@close="handleClose"
|
||||
width="70%"
|
||||
top="5vh"
|
||||
:model-value="visible"
|
||||
:title="`配方详情: ${recipe ? recipe.name : ''}` + (isEditing ? ' (编辑中)' : '')"
|
||||
@close="handleClose"
|
||||
width="70%"
|
||||
top="5vh"
|
||||
>
|
||||
<div v-if="loading" class="loading-spinner">
|
||||
<el-skeleton :rows="5" animated />
|
||||
<el-skeleton :rows="5" animated/>
|
||||
</div>
|
||||
<div v-else-if="error" class="error-message">
|
||||
<el-alert
|
||||
<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">
|
||||
<div v-if="!isEditing">
|
||||
<el-table :data="ingredientDetails" style="width: 100%">
|
||||
<el-table-column prop="name" label="原料名称" />
|
||||
<el-table-column prop="name" label="原料名称"/>
|
||||
<el-table-column prop="percentage" label="占比">
|
||||
<template #default="scope">
|
||||
{{ (scope.row.percentage * 100).toFixed(2) }}%
|
||||
{{ scope.row.percentage.toFixed(2) }}%
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-table :data="localIngredientDetails" style="width: 100%">
|
||||
<el-table-column prop="name" label="原料名称" />
|
||||
<el-table-column prop="name" label="原料名称"/>
|
||||
<el-table-column label="占比">
|
||||
<template #default="scope">
|
||||
<el-input-number v-model="scope.row.percentage" :min="0" :max="1" :step="0.01" :precision="2" @change="updateNutrientSummary"></el-input-number>
|
||||
<el-input-number v-model="scope.row.percentage" :min="0" :max="100" :step="1" :precision="2"
|
||||
@change="updateNutrientSummary"></el-input-number>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
@@ -46,7 +47,8 @@
|
||||
</el-table>
|
||||
<div class="add-ingredient-section">
|
||||
<el-select v-model="newIngredientId" placeholder="选择要添加的原料" filterable style="flex-grow: 1;">
|
||||
<el-option v-for="item in availableRawMaterials" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
<el-option v-for="item in availableRawMaterials" :key="item.id" :label="item.name"
|
||||
:value="item.id"></el-option>
|
||||
</el-select>
|
||||
<el-button type="primary" @click="addIngredient" style="margin-left: 10px;">添加原料</el-button>
|
||||
</div>
|
||||
@@ -54,31 +56,45 @@
|
||||
</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="name" label="营养素名称"/>
|
||||
<el-table-column prop="value" label="总含量">
|
||||
<template #default="scope">
|
||||
{{ scope.row.value.toFixed(4) }}
|
||||
<template #default="scope">
|
||||
{{ scope.row.value.toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<template #footer>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button v-if="!isEditing" @click="openAIReviewDialog">AI点评</el-button>
|
||||
<el-button v-if="!isEditing" @click="openCompareDialog">对比</el-button>
|
||||
<el-button v-if="!isEditing" @click="handleEdit">编辑配方</el-button>
|
||||
<el-button v-else @click="handleCancelEdit">取消</el-button>
|
||||
<el-button v-if="isEditing" type="primary" @click="handleSaveRecipe">保存</el-button>
|
||||
<el-button v-else @click="handleClose">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
<RecipeCompareDialog
|
||||
v-model:visible="compareDialogVisible"
|
||||
:current-recipe="recipe"
|
||||
@close="compareDialogVisible = false"
|
||||
/>
|
||||
<AIRecipeReviewDialog
|
||||
v-model:visible="aiReviewDialogVisible"
|
||||
:recipe="recipe"
|
||||
@cancel="aiReviewDialogVisible = false"
|
||||
/>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, watch, nextTick, computed } from 'vue';
|
||||
import { getRawMaterialById, getRawMaterials, updateRecipe, getRecipeById } from '../../api/feed';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Delete } from '@element-plus/icons-vue';
|
||||
import {ref, watch, nextTick, computed} from 'vue';
|
||||
import {getRawMaterialById, getRawMaterials, updateRecipe, getRecipeById} from '../../api/feed';
|
||||
import {ElMessage} from 'element-plus';
|
||||
import {Delete} from '@element-plus/icons-vue';
|
||||
import RecipeCompareDialog from './RecipeCompareDialog.vue';
|
||||
import AIRecipeReviewDialog from './AIRecipeReviewDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'RecipeDetailDialog',
|
||||
@@ -89,29 +105,39 @@ export default {
|
||||
default: () => null,
|
||||
},
|
||||
},
|
||||
emits: ['update:visible', 'recipe-updated'], // 添加 recipe-updated 事件
|
||||
setup(props, { emit }) {
|
||||
const isEditing = ref(false); // 控制是否处于编辑模式
|
||||
emits: ['update:visible', 'recipe-updated'],
|
||||
setup(props, {emit}) {
|
||||
const isEditing = ref(false);
|
||||
const activeTab = ref('ingredients');
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const ingredientDetails = ref([]); // 显示模式下的原料详情
|
||||
const localIngredientDetails = ref([]); // 编辑模式下的原料详情
|
||||
const ingredientDetails = ref([]);
|
||||
const localIngredientDetails = ref([]);
|
||||
const nutrientSummary = ref([]);
|
||||
const nutrientTableRef = ref(null);
|
||||
const allRawMaterials = ref([]); // 所有可用原料列表
|
||||
const newIngredientId = ref(null); // 待添加的新原料ID
|
||||
const allRawMaterials = ref([]);
|
||||
const newIngredientId = ref(null);
|
||||
const compareDialogVisible = ref(false);
|
||||
const aiReviewDialogVisible = ref(false);
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
|
||||
const openCompareDialog = () => {
|
||||
compareDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const openAIReviewDialog = () => {
|
||||
aiReviewDialogVisible.value = true;
|
||||
};
|
||||
|
||||
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;
|
||||
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 {
|
||||
@@ -120,19 +146,14 @@ export default {
|
||||
});
|
||||
}
|
||||
});
|
||||
return Array.from(summary, ([name, value]) => ({ name, value }));
|
||||
return Array.from(summary, ([name, value]) => ({name, value}));
|
||||
};
|
||||
|
||||
// 计算属性,用于过滤掉已经存在的原料
|
||||
const availableRawMaterials = computed(() => {
|
||||
const existingIngredientIds = new Set(localIngredientDetails.value.map(ing => ing.id));
|
||||
return allRawMaterials.value.filter(material => !existingIngredientIds.has(material.id));
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取并设置配方详情数据
|
||||
* @param {number} recipeId - 配方ID
|
||||
*/
|
||||
const fetchAndSetRecipeDetails = async (recipeId) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
@@ -148,8 +169,8 @@ export default {
|
||||
percentage: latestRecipe.recipe_ingredients[index].percentage,
|
||||
}));
|
||||
|
||||
ingredientDetails.value = details; // 用于显示模式
|
||||
localIngredientDetails.value = JSON.parse(JSON.stringify(details)); // 用于编辑模式,深拷贝
|
||||
ingredientDetails.value = details;
|
||||
localIngredientDetails.value = JSON.parse(JSON.stringify(details));
|
||||
|
||||
nutrientSummary.value = calculateNutrientSummary(details);
|
||||
|
||||
@@ -165,12 +186,11 @@ export default {
|
||||
if (newVal && props.recipe) {
|
||||
await fetchAndSetRecipeDetails(props.recipe.id);
|
||||
} else {
|
||||
// 重置数据
|
||||
ingredientDetails.value = [];
|
||||
localIngredientDetails.value = [];
|
||||
nutrientSummary.value = [];
|
||||
activeTab.value = 'ingredients';
|
||||
isEditing.value = false; // 关闭对话框时重置编辑状态
|
||||
isEditing.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -184,17 +204,14 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
// 实时更新营养成分汇总
|
||||
const updateNutrientSummary = () => {
|
||||
nutrientSummary.value = calculateNutrientSummary(localIngredientDetails.value);
|
||||
};
|
||||
|
||||
// 进入编辑模式
|
||||
const handleEdit = async () => {
|
||||
isEditing.value = true;
|
||||
// 获取所有原料列表
|
||||
try {
|
||||
const response = await getRawMaterials({ page_size: 999 });
|
||||
const response = await getRawMaterials({page_size: 999});
|
||||
if (response.data && response.data.list) {
|
||||
allRawMaterials.value = response.data.list;
|
||||
}
|
||||
@@ -203,29 +220,24 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
// 取消编辑
|
||||
const handleCancelEdit = () => {
|
||||
isEditing.value = false;
|
||||
// 恢复到原始数据
|
||||
localIngredientDetails.value = JSON.parse(JSON.stringify(ingredientDetails.value));
|
||||
updateNutrientSummary(); // 重新计算营养成分
|
||||
updateNutrientSummary();
|
||||
};
|
||||
|
||||
// 保存配方
|
||||
const handleSaveRecipe = async () => {
|
||||
// 验证占比总和是否为1
|
||||
const totalPercentage = localIngredientDetails.value.reduce((sum, ing) => sum + ing.percentage, 0);
|
||||
|
||||
if (Math.abs(totalPercentage - 1) > 0.001) { // 允许浮点数误差
|
||||
ElMessage.error(`原料总占比必须为100%,当前为${(totalPercentage * 100).toFixed(2)}%`);
|
||||
if (totalPercentage > 100.001) {
|
||||
ElMessage.error(`原料总占比不能超过100%,当前为${totalPercentage.toFixed(2)}%`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 构造保存数据
|
||||
const recipeToSave = {
|
||||
id: props.recipe.id,
|
||||
name: props.recipe.name, // 名称不变
|
||||
description: props.recipe.description, // 描述不变
|
||||
name: props.recipe.name,
|
||||
description: props.recipe.description,
|
||||
recipe_ingredients: localIngredientDetails.value.map(ing => ({
|
||||
raw_material_id: ing.id,
|
||||
percentage: ing.percentage,
|
||||
@@ -236,21 +248,18 @@ export default {
|
||||
await updateRecipe(recipeToSave.id, recipeToSave);
|
||||
ElMessage.success('配方更新成功');
|
||||
isEditing.value = false;
|
||||
emit('recipe-updated'); // 通知父组件配方已更新
|
||||
// 重新加载配方详情以刷新显示
|
||||
emit('recipe-updated');
|
||||
await fetchAndSetRecipeDetails(props.recipe.id);
|
||||
} catch (err) {
|
||||
ElMessage.error('保存配方失败: ' + (err.message || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
// 移除原料
|
||||
const removeIngredient = (index) => {
|
||||
localIngredientDetails.value.splice(index, 1);
|
||||
updateNutrientSummary();
|
||||
};
|
||||
|
||||
// 添加原料
|
||||
const addIngredient = () => {
|
||||
if (!newIngredientId.value) {
|
||||
ElMessage.warning('请选择要添加的原料');
|
||||
@@ -258,7 +267,7 @@ export default {
|
||||
}
|
||||
const materialToAdd = allRawMaterials.value.find(m => m.id === newIngredientId.value);
|
||||
if (materialToAdd && !localIngredientDetails.value.some(ing => ing.id === materialToAdd.id)) {
|
||||
localIngredientDetails.value.push({ ...materialToAdd, percentage: 0 }); // 默认占比0
|
||||
localIngredientDetails.value.push({...materialToAdd, percentage: 0});
|
||||
newIngredientId.value = null;
|
||||
updateNutrientSummary();
|
||||
} else if (localIngredientDetails.value.some(ing => ing.id === materialToAdd.id)) {
|
||||
@@ -284,9 +293,18 @@ export default {
|
||||
availableRawMaterials,
|
||||
newIngredientId,
|
||||
updateNutrientSummary,
|
||||
Delete, // 暴露 Delete 图标组件
|
||||
Delete,
|
||||
compareDialogVisible,
|
||||
openCompareDialog,
|
||||
aiReviewDialogVisible,
|
||||
openAIReviewDialog,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
Delete,
|
||||
RecipeCompareDialog,
|
||||
AIRecipeReviewDialog,
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -295,9 +313,11 @@ export default {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.add-ingredient-section {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
|
||||
@@ -24,8 +24,9 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="配方简介"/>
|
||||
<el-table-column label="操作" width="150" align="center">
|
||||
<el-table-column label="操作" width="250" align="center">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" @click="$emit('ai-review', scope.row)">AI点评</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>
|
||||
</template>
|
||||
@@ -43,6 +44,7 @@ export default {
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
emits: ['edit', 'delete', 'show-details']
|
||||
// 声明新的ai-review事件
|
||||
emits: ['edit', 'delete', 'show-details', 'ai-review']
|
||||
};
|
||||
</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>
|
||||
49
src/enums.js
49
src/enums.js
@@ -64,6 +64,46 @@ export const LogChangeType = {
|
||||
CORRECTION: '盘点校正',
|
||||
};
|
||||
|
||||
/**
|
||||
* 库存变动来源类型
|
||||
* @enum {string}
|
||||
*/
|
||||
export const StockLogSourceType = {
|
||||
PURCHASE: '采购入库',
|
||||
FEEDING: '饲喂出库',
|
||||
DETERIORATE: '变质出库',
|
||||
SALE: '售卖出库',
|
||||
MISCELLANEOUS: '杂用领取',
|
||||
MANUAL: '手动盘点',
|
||||
FERMENT_START: '发酵出库', // 原料投入发酵,从库存中扣除
|
||||
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}
|
||||
@@ -263,6 +303,7 @@ export const ZapcoreLevel = {
|
||||
PANIC: 4,
|
||||
FATAL: 5,
|
||||
INVALID: 6,
|
||||
NUM_LEVELS: 7, // 新增的级别数量
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -289,6 +330,14 @@ export const AlarmSourceType = {
|
||||
SYSTEM: '系统',
|
||||
};
|
||||
|
||||
/**
|
||||
* AI模型
|
||||
* @enum {string}
|
||||
*/
|
||||
export const AIModel = {
|
||||
GEMINI: 'Gemini',
|
||||
};
|
||||
|
||||
/**
|
||||
* 操作符
|
||||
* @enum {string}
|
||||
|
||||
@@ -115,6 +115,12 @@
|
||||
</el-icon>
|
||||
<template #title>配方管理</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/inventory/stock">
|
||||
<el-icon>
|
||||
<Tickets/>
|
||||
</el-icon>
|
||||
<template #title>库存管理</template>
|
||||
</el-menu-item>
|
||||
|
||||
</el-sub-menu>
|
||||
|
||||
@@ -444,7 +450,7 @@ export default {
|
||||
|
||||
const activeMenu = computed(() => {
|
||||
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 route.path;
|
||||
|
||||
24
src/main.js
24
src/main.js
@@ -1,3 +1,27 @@
|
||||
// 全局屏蔽 ResizeObserver 错误
|
||||
// 这是一个常见的 workaround,用于处理某些环境下 ResizeObserver 导致的循环限制错误。
|
||||
// 它通过包装 ResizeObserver 的回调函数来捕获并阻止错误向上冒泡,但不会修复根本原因。
|
||||
if (typeof window !== 'undefined' && window.ResizeObserver) {
|
||||
const originalResizeObserver = window.ResizeObserver;
|
||||
window.ResizeObserver = class ResizeObserver extends originalResizeObserver {
|
||||
constructor(callback) {
|
||||
super((entries, observer) => {
|
||||
// 使用 requestAnimationFrame 延迟执行回调,以避免 ResizeObserver loop limit exceeded 错误
|
||||
window.requestAnimationFrame(() => {
|
||||
try {
|
||||
callback(entries, observer);
|
||||
} catch (error) {
|
||||
// 捕获 ResizeObserver 错误,并打印到控制台,防止应用崩溃
|
||||
console.error('ResizeObserver 错误被捕获:', error);
|
||||
// 如果需要,可以在这里选择断开观察者,以防止进一步的错误
|
||||
// observer.disconnect();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
import {createApp} from 'vue';
|
||||
import ElementPlus from 'element-plus';
|
||||
import 'element-plus/dist/index.css';
|
||||
|
||||
@@ -28,6 +28,7 @@ import NutrientList from '../views/feed/NutrientList.vue'; // 修正拼写错误
|
||||
import PigAgeStageList from '../views/feed/PigAgeStageList.vue'; // 导入 PigAgeStageList 组件
|
||||
import PigBreedList from '../views/feed/PigBreedList.vue'; // 导入 PigBreedList 组件
|
||||
import RecipeList from '../views/feed/RecipeList.vue';
|
||||
import StockManagement from '../views/inventory/StockManagement.vue';
|
||||
|
||||
|
||||
const routes = [
|
||||
@@ -45,6 +46,7 @@ const routes = [
|
||||
{path: '/feed/pig-age-stages', component: PigAgeStageList, meta: {requiresAuth: true, title: '年龄阶段管理'}}, // 添加年龄阶段管理路由
|
||||
{path: '/feed/pig-breeds', component: PigBreedList, 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/medication-logs', component: MedicationLogsView, meta: {requiresAuth: true, title: '用药记录'}},
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* @returns {string} - 格式化后的字符串,如果输入无效则返回空字符串或提示
|
||||
*/
|
||||
export function formatRFC3339(rfc3339String) {
|
||||
if (!rfc3339String) {
|
||||
return '--'; // 或者返回空字符串 ''
|
||||
if (!rfc3339String || rfc3339String.startsWith('0001-01-01')) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
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) { ... }
|
||||
@@ -9,7 +9,10 @@
|
||||
<el-icon :size="20"><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button type="primary" @click="addRecipe">新增配方</el-button>
|
||||
<div>
|
||||
<el-button type="primary" @click="addRecipe">新增配方</el-button>
|
||||
<el-button type="success" @click="openGenerateRecipeDialog">一键生成配方</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -38,6 +41,7 @@
|
||||
@edit="editRecipe"
|
||||
@delete="deleteRecipe"
|
||||
@show-details="handleShowDetails"
|
||||
@ai-review="openAIRecipeReviewDialog"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
@@ -55,6 +59,20 @@
|
||||
v-model:visible="detailDialogVisible"
|
||||
:recipe="selectedRecipe"
|
||||
/>
|
||||
|
||||
<!-- 一键生成配方对话框 -->
|
||||
<GenerateRecipeDialog
|
||||
v-model:visible="generateRecipeDialogVisible"
|
||||
@success="onGenerateRecipeSuccess"
|
||||
@cancel="generateRecipeDialogVisible = false"
|
||||
/>
|
||||
|
||||
<!-- AI点评配方对话框 -->
|
||||
<AIRecipeReviewDialog
|
||||
v-model:visible="aiReviewDialogVisible"
|
||||
:recipe="selectedRecipeForAIReview"
|
||||
@cancel="aiReviewDialogVisible = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -65,6 +83,8 @@ 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 GenerateRecipeDialog from '../../components/feed/GenerateRecipeDialog.vue';
|
||||
import AIRecipeReviewDialog from '../../components/feed/AIRecipeReviewDialog.vue'; // 引入新的AI点评组件
|
||||
|
||||
export default {
|
||||
name: 'RecipeList',
|
||||
@@ -72,7 +92,9 @@ export default {
|
||||
RecipeTable,
|
||||
RecipeForm,
|
||||
RecipeDetailDialog,
|
||||
Refresh
|
||||
Refresh,
|
||||
GenerateRecipeDialog,
|
||||
AIRecipeReviewDialog, // 注册新的AI点评组件
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -84,6 +106,9 @@ export default {
|
||||
isEdit: false,
|
||||
detailDialogVisible: false,
|
||||
selectedRecipe: null,
|
||||
generateRecipeDialogVisible: false, // 控制一键生成配方弹窗的显示
|
||||
aiReviewDialogVisible: false, // 控制AI点评弹窗的显示
|
||||
selectedRecipeForAIReview: null, // 存储当前选择进行AI点评的配方
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
@@ -94,7 +119,7 @@ export default {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
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 || [];
|
||||
} catch (err) {
|
||||
this.error = err.message || '未知错误';
|
||||
@@ -141,7 +166,26 @@ export default {
|
||||
handleShowDetails(recipe) {
|
||||
this.selectedRecipe = recipe;
|
||||
this.detailDialogVisible = true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 打开一键生成配方对话框
|
||||
*/
|
||||
openGenerateRecipeDialog() {
|
||||
this.generateRecipeDialogVisible = true;
|
||||
},
|
||||
onGenerateRecipeSuccess(recipeName, recipeDescription) {
|
||||
ElMessage.success(`配方 "${recipeName}" 生成成功: ${recipeDescription}`);
|
||||
this.generateRecipeDialogVisible = false;
|
||||
this.loadRecipes(); // 刷新配方列表
|
||||
},
|
||||
/**
|
||||
* 打开AI点评配方对话框
|
||||
* @param {object} recipe - 需要AI点评的配方对象
|
||||
*/
|
||||
openAIRecipeReviewDialog(recipe) {
|
||||
this.selectedRecipeForAIReview = recipe;
|
||||
this.aiReviewDialogVisible = true;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
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