Compare commits
	
		
			60 Commits
		
	
	
		
			e5c2d38559
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b4662a2b10 | |||
| 840fb98988 | |||
| fcd1b1c140 | |||
| 22f633e20b | |||
| e704249d75 | |||
| 0ff82657ab | |||
| fff309f56b | |||
| d3700c8835 | |||
| ea44c9caea | |||
| ab8ea6977f | |||
| d5f91b9b12 | |||
| a8d9b033f3 | |||
| e1fb3055bb | |||
| 6d452394aa | |||
| 62801326cb | |||
| ee2f3e12d2 | |||
| e341e53fe2 | |||
| 03eed8202b | |||
| 624592a63d | |||
| 2191bf2bdf | |||
| d7d68684e4 | |||
| d85cfb303b | |||
| 2e5c04d2d4 | |||
| 2fe6f0c576 | |||
| c176fadafe | |||
| ced688a7a3 | |||
| 263af9fa3a | |||
| 1a55330734 | |||
| 0026f24002 | |||
| 2d6bb69387 | |||
| 70e3a4f2b0 | |||
| 98cd24ee29 | |||
| accbb1a9f6 | |||
| d38feb938d | |||
| edfc641f93 | |||
| 0f9429be45 | |||
| dc3f311037 | |||
| 828c3bbe36 | |||
| 0a5e1635f1 | |||
| 1df5854201 | |||
| 2b1dd4c6a5 | |||
| 52a647b88a | |||
| b1a8611554 | |||
| 0cddf99456 | |||
| 1b45e61daf | |||
| 9c6467176c | |||
| fdc568753c | |||
| a3e9465b88 | |||
| 96c36f9ce1 | |||
| c68dff6123 | |||
| a457b9713c | |||
| 76d01af86c | |||
| b3ab17660a | |||
| 5737973197 | |||
| 9a701b339b | |||
| 7710abcf9e | |||
| edef58568d | |||
| 1c3b3a5151 | |||
| a2a9cc1450 | |||
| effc2c06e0 | 
							
								
								
									
										26
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,12 +1,22 @@ | |||||||
| # ---> Vue | # Logs | ||||||
| # gitignore template for Vue.js projects | npm-debug.log* | ||||||
| # | yarn-error.log* | ||||||
| # Recommended template: Node.gitignore |  | ||||||
|  |  | ||||||
| # TODO: where does this rule come from? | # Dependencies | ||||||
|  | node_modules/ | ||||||
|  |  | ||||||
|  | # Build output | ||||||
|  | dist/ | ||||||
|  |  | ||||||
|  | # Environment variables | ||||||
|  | .env* | ||||||
|  | *.local | ||||||
|  |  | ||||||
|  | # IDE config | ||||||
|  | .idea/ | ||||||
|  |  | ||||||
|  | # Docs | ||||||
| docs/_book | docs/_book | ||||||
|  |  | ||||||
| # TODO: where does this rule come from? | # Tests | ||||||
| test/ | test/ | ||||||
|  |  | ||||||
| .idea/ |  | ||||||
							
								
								
									
										6338
									
								
								docs/swagger.json
									
									
									
									
									
								
							
							
						
						
									
										6338
									
								
								docs/swagger.json
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										14
									
								
								node_modules/.package-lock.json
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								node_modules/.package-lock.json
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -9676,6 +9676,20 @@ | |||||||
|         "node": ">= 0.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": { |     "node_modules/undici-types": { | ||||||
|       "version": "7.12.0", |       "version": "7.12.0", | ||||||
|       "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.12.0.tgz", |       "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.12.0.tgz", | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -9718,6 +9718,20 @@ | |||||||
|         "node": ">= 0.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": { |     "node_modules/undici-types": { | ||||||
|       "version": "7.12.0", |       "version": "7.12.0", | ||||||
|       "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.12.0.tgz", |       "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.12.0.tgz", | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								src/App.vue
									
									
									
									
									
								
							| @@ -1,14 +1,31 @@ | |||||||
| <template> | <template> | ||||||
|   <MainLayout /> |   <div id="app"> | ||||||
|  |     <template v-if="isLoginPage"> | ||||||
|  |       <router-view /> | ||||||
|  |     </template> | ||||||
|  |     <template v-else> | ||||||
|  |       <MainLayout /> | ||||||
|  |     </template> | ||||||
|  |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|  | import { computed } from 'vue'; | ||||||
|  | import { useRoute } from 'vue-router'; | ||||||
| import MainLayout from './layouts/MainLayout.vue'; | import MainLayout from './layouts/MainLayout.vue'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   name: 'App', |   name: 'App', | ||||||
|   components: { |   components: { | ||||||
|     MainLayout |     MainLayout | ||||||
|  |   }, | ||||||
|  |   setup() { | ||||||
|  |     const route = useRoute(); | ||||||
|  |     const isLoginPage = computed(() => route.path === '/login'); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       isLoginPage | ||||||
|  |     }; | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|   | |||||||
							
								
								
									
										89
									
								
								src/api/areaController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/api/areaController.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | |||||||
|  | import http from '../utils/http'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} Response | ||||||
|  |  * @property {number} code - 业务状态码 | ||||||
|  |  * @property {object} [data] - 业务数据 | ||||||
|  |  * @property {string} [message] - 提示信息 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} AreaControllerResponse | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {string} name | ||||||
|  |  * @property {string} network_id | ||||||
|  |  * @property {string} location | ||||||
|  |  * @property {string} status | ||||||
|  |  * @property {object} properties | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} updated_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} CreateAreaControllerRequest | ||||||
|  |  * @property {string} name | ||||||
|  |  * @property {string} network_id | ||||||
|  |  * @property {string} [location] | ||||||
|  |  * @property {object} [properties] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} UpdateAreaControllerRequest | ||||||
|  |  * @property {string} name | ||||||
|  |  * @property {string} network_id | ||||||
|  |  * @property {string} [location] | ||||||
|  |  * @property {object} [properties] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取系统中所有区域主控的列表 | ||||||
|  |  * @returns {Promise<Array<AreaControllerResponse>>} | ||||||
|  |  */ | ||||||
|  | export const getAreaControllers = () => { | ||||||
|  |   return http.get('/api/v1/area-controllers'); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据提供的信息创建一个新区域主控 | ||||||
|  |  * @param {CreateAreaControllerRequest} areaControllerData - 区域主控信息 | ||||||
|  |  * @returns {Promise<AreaControllerResponse>} | ||||||
|  |  */ | ||||||
|  | export const createAreaController = (areaControllerData) => { | ||||||
|  |   return http.post('/api/v1/area-controllers', areaControllerData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据ID获取单个区域主控的详细信息 | ||||||
|  |  * @param {string} id - 区域主控ID | ||||||
|  |  * @returns {Promise<AreaControllerResponse>} | ||||||
|  |  */ | ||||||
|  | export const getAreaControllerById = (id) => { | ||||||
|  |   return http.get(`/api/v1/area-controllers/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据ID更新一个已存在的区域主控信息 | ||||||
|  |  * @param {string} id - 区域主控ID | ||||||
|  |  * @param {UpdateAreaControllerRequest} areaControllerData - 要更新的区域主控信息 | ||||||
|  |  * @returns {Promise<AreaControllerResponse>} | ||||||
|  |  */ | ||||||
|  | export const updateAreaController = (id, areaControllerData) => { | ||||||
|  |   return http.put(`/api/v1/area-controllers/${id}`, areaControllerData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据ID删除一个区域主控(软删除) | ||||||
|  |  * @param {string} id - 区域主控ID | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const deleteAreaController = (id) => { | ||||||
|  |   return http.delete(`/api/v1/area-controllers/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const AreaControllerApi = { | ||||||
|  |   list: getAreaControllers, | ||||||
|  |   create: createAreaController, | ||||||
|  |   getById: getAreaControllerById, | ||||||
|  |   update: updateAreaController, | ||||||
|  |   delete: deleteAreaController, | ||||||
|  | }; | ||||||
| @@ -1,53 +1,203 @@ | |||||||
| import http from '../utils/http.js'; | import http from '../utils/http'; | ||||||
|  |  | ||||||
|  | // --- Typedefs --- | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 设备管理API |  * @typedef {object} Response | ||||||
|  |  * @property {number} code - 业务状态码 | ||||||
|  |  * @property {object} [data] - 业务数据 | ||||||
|  |  * @property {string} [message] - 提示信息 | ||||||
|  */ |  */ | ||||||
| export class DeviceApi { |  | ||||||
|   /** |  | ||||||
|    * 获取设备列表 |  | ||||||
|    * @returns {Promise} 设备列表 |  | ||||||
|    */ |  | ||||||
|   static list() { |  | ||||||
|     return http.get('/api/v1/devices'); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** | /** | ||||||
|    * 创建新设备 |  * @typedef {object} DeviceResponse | ||||||
|    * @param {Object} deviceData 设备数据 |  * @property {number} id | ||||||
|    * @returns {Promise} 创建结果 |  * @property {string} name | ||||||
|    */ |  * @property {string} location | ||||||
|   static create(deviceData) { |  * @property {number} area_controller_id | ||||||
|     return http.post('/api/v1/devices', deviceData); |  * @property {string} area_controller_name | ||||||
|   } |  * @property {number} device_template_id | ||||||
|  |  * @property {string} device_template_name | ||||||
|  |  * @property {object} properties | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} updated_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|   /** | /** | ||||||
|    * 获取设备详情 |  * @typedef {object} CreateDeviceRequest | ||||||
|    * @param {string|number} id 设备ID |  * @property {string} name | ||||||
|    * @returns {Promise} 设备详情 |  * @property {string} [location] | ||||||
|    */ |  * @property {number} area_controller_id | ||||||
|   static get(id) { |  * @property {number} device_template_id | ||||||
|     return http.get(`/api/v1/devices/${id}`); |  * @property {object} [properties] | ||||||
|   } |  */ | ||||||
|  |  | ||||||
|   /** | /** | ||||||
|    * 更新设备信息 |  * @typedef {object} UpdateDeviceRequest | ||||||
|    * @param {string|number} id 设备ID |  * @property {string} name | ||||||
|    * @param {Object} deviceData 设备数据 |  * @property {string} [location] | ||||||
|    * @returns {Promise} 更新结果 |  * @property {number} area_controller_id | ||||||
|    */ |  * @property {number} device_template_id | ||||||
|   static update(id, deviceData) { |  * @property {object} [properties] | ||||||
|     return http.put(`/api/v1/devices/${id}`, deviceData); |  */ | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** | /** | ||||||
|    * 删除设备 |  * @typedef {object} ManualControlDeviceRequest | ||||||
|    * @param {string|number} id 设备ID |  * @property {string} [action] - Action 不传表示这是一个传感器, 会触发一次采集 | ||||||
|    * @returns {Promise} 删除结果 |  */ | ||||||
|    */ |  | ||||||
|   static delete(id) { |  | ||||||
|     return http.delete(`/api/v1/devices/${id}`); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export default DeviceApi; | /** | ||||||
|  |  * @typedef {object} AreaControllerResponse | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {string} name | ||||||
|  |  * @property {string} network_id | ||||||
|  |  * @property {string} location | ||||||
|  |  * @property {string} status | ||||||
|  |  * @property {object} properties | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} updated_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} CreateAreaControllerRequest | ||||||
|  |  * @property {string} name | ||||||
|  |  * @property {string} network_id | ||||||
|  |  * @property {string} [location] | ||||||
|  |  * @property {object} [properties] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} UpdateAreaControllerRequest | ||||||
|  |  * @property {string} name | ||||||
|  |  * @property {string} network_id | ||||||
|  |  * @property {string} [location] | ||||||
|  |  * @property {object} [properties] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // --- Device API Functions --- | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取系统中所有设备的列表 | ||||||
|  |  * @returns {Promise<Array<DeviceResponse>>} | ||||||
|  |  */ | ||||||
|  | export const getDevices = () => { | ||||||
|  |   return http.get('/api/v1/devices'); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据提供的信息创建一个新设备 | ||||||
|  |  * @param {CreateDeviceRequest} deviceData - 设备信息 | ||||||
|  |  * @returns {Promise<DeviceResponse>} | ||||||
|  |  */ | ||||||
|  | export const createDevice = (deviceData) => { | ||||||
|  |   return http.post('/api/v1/devices', deviceData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据设备ID获取单个设备的详细信息 | ||||||
|  |  * @param {string} id - 设备ID | ||||||
|  |  * @returns {Promise<DeviceResponse>} | ||||||
|  |  */ | ||||||
|  | export const getDeviceById = (id) => { | ||||||
|  |   return http.get(`/api/v1/devices/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据设备ID更新一个已存在的设备信息 | ||||||
|  |  * @param {string} id - 设备ID | ||||||
|  |  * @param {UpdateDeviceRequest} deviceData - 要更新的设备信息 | ||||||
|  |  * @returns {Promise<DeviceResponse>} | ||||||
|  |  */ | ||||||
|  | export const updateDevice = (id, deviceData) => { | ||||||
|  |   return http.put(`/api/v1/devices/${id}`, deviceData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据设备ID删除一个设备(软删除) | ||||||
|  |  * @param {string} id - 设备ID | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const deleteDevice = (id) => { | ||||||
|  |   return http.delete(`/api/v1/devices/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据设备ID和指定的动作(开启或关闭)来手动控制设备 | ||||||
|  |  * @param {string} id - 设备ID | ||||||
|  |  * @param {ManualControlDeviceRequest} manualControlData - 手动控制指令 | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const manualControlDevice = (id, manualControlData) => { | ||||||
|  |   return http.post(`/api/v1/devices/manual-control/${id}`, manualControlData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // --- AreaController API Functions --- | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取系统中所有区域主控的列表 | ||||||
|  |  * @returns {Promise<Array<AreaControllerResponse>>} | ||||||
|  |  */ | ||||||
|  | export const getAreaControllers = () => { | ||||||
|  |   return http.get('/api/v1/area-controllers'); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 创建一个新区域主控 | ||||||
|  |  * @param {CreateAreaControllerRequest} areaControllerData - 区域主控信息 | ||||||
|  |  * @returns {Promise<AreaControllerResponse>} | ||||||
|  |  */ | ||||||
|  | export const createAreaController = (areaControllerData) => { | ||||||
|  |   return http.post('/api/v1/area-controllers', areaControllerData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据ID获取单个区域主控的详细信息 | ||||||
|  |  * @param {string} id - 区域主控ID | ||||||
|  |  * @returns {Promise<AreaControllerResponse>} | ||||||
|  |  */ | ||||||
|  | export const getAreaControllerById = (id) => { | ||||||
|  |   return http.get(`/api/v1/area-controllers/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据ID更新一个已存在的区域主控信息 | ||||||
|  |  * @param {string} id - 区域主控ID | ||||||
|  |  * @param {UpdateAreaControllerRequest} areaControllerData - 要更新的区域主控信息 | ||||||
|  |  * @returns {Promise<AreaControllerResponse>} | ||||||
|  |  */ | ||||||
|  | export const updateAreaController = (id, areaControllerData) => { | ||||||
|  |   return http.put(`/api/v1/area-controllers/${id}`, areaControllerData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据ID删除一个区域主控 | ||||||
|  |  * @param {string} id - 区域主控ID | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const deleteAreaController = (id) => { | ||||||
|  |   return http.delete(`/api/v1/area-controllers/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // --- API Wrappers --- | ||||||
|  |  | ||||||
|  | // AreaControllerApi 封装 | ||||||
|  | export const AreaControllerApi = { | ||||||
|  |   list: getAreaControllers, | ||||||
|  |   create: createAreaController, | ||||||
|  |   getById: getAreaControllerById, | ||||||
|  |   update: updateAreaController, | ||||||
|  |   delete: deleteAreaController | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // DeviceApi 封装 | ||||||
|  | export const DeviceApi = { | ||||||
|  |   list: getDevices, | ||||||
|  |   create: createDevice, | ||||||
|  |   getById: getDeviceById, | ||||||
|  |   update: updateDevice, | ||||||
|  |   delete: deleteDevice, | ||||||
|  |   manualControl: manualControlDevice | ||||||
|  | }; | ||||||
|   | |||||||
							
								
								
									
										110
									
								
								src/api/deviceTemplate.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/api/deviceTemplate.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | |||||||
|  | import http from '../utils/http'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} Response | ||||||
|  |  * @property {number} code - 业务状态码 | ||||||
|  |  * @property {object} [data] - 业务数据 | ||||||
|  |  * @property {string} [message] - 提示信息 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('执行器'|'传感器')} DeviceCategory | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('信号强度'|'电池电量'|'温度'|'湿度'|'重量')} SensorType | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ValueDescriptor | ||||||
|  |  * @property {SensorType} type | ||||||
|  |  * @property {number} [multiplier] - 乘数,用于原始数据转换 | ||||||
|  |  * @property {number} [offset] - 偏移量,用于原始数据转换 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} DeviceTemplateResponse | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {string} name | ||||||
|  |  * @property {string} [description] | ||||||
|  |  * @property {string} [manufacturer] | ||||||
|  |  * @property {DeviceCategory} category | ||||||
|  |  * @property {object} commands | ||||||
|  |  * @property {Array<ValueDescriptor>} values | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} updated_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} CreateDeviceTemplateRequest | ||||||
|  |  * @property {string} name | ||||||
|  |  * @property {string} [description] | ||||||
|  |  * @property {string} [manufacturer] | ||||||
|  |  * @property {DeviceCategory} category | ||||||
|  |  * @property {object} commands | ||||||
|  |  * @property {Array<ValueDescriptor>} [values] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} UpdateDeviceTemplateRequest | ||||||
|  |  * @property {string} name | ||||||
|  |  * @property {string} [description] | ||||||
|  |  * @property {string} [manufacturer] | ||||||
|  |  * @property {DeviceCategory} category | ||||||
|  |  * @property {object} commands | ||||||
|  |  * @property {Array<ValueDescriptor>} [values] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取系统中所有设备模板的列表 | ||||||
|  |  * @returns {Promise<Array<DeviceTemplateResponse>>} | ||||||
|  |  */ | ||||||
|  | const getDeviceTemplates = () => { | ||||||
|  |   return http.get('/api/v1/device-templates'); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据提供的信息创建一个新设备模板 | ||||||
|  |  * @param {CreateDeviceTemplateRequest} deviceTemplateData - 设备模板信息 | ||||||
|  |  * @returns {Promise<DeviceTemplateResponse>} | ||||||
|  |  */ | ||||||
|  | const createDeviceTemplate = (deviceTemplateData) => { | ||||||
|  |   return http.post('/api/v1/device-templates', deviceTemplateData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据设备模板ID获取单个设备模板的详细信息 | ||||||
|  |  * @param {number} id - 设备模板ID | ||||||
|  |  * @returns {Promise<DeviceTemplateResponse>} | ||||||
|  |  */ | ||||||
|  | const getDeviceTemplateById = (id) => { | ||||||
|  |   return http.get(`/api/v1/device-templates/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据设备模板ID更新一个已存在的设备模板信息 | ||||||
|  |  * @param {number} id - 设备模板ID | ||||||
|  |  * @param {UpdateDeviceTemplateRequest} deviceTemplateData - 要更新的设备模板信息 | ||||||
|  |  * @returns {Promise<DeviceTemplateResponse>} | ||||||
|  |  */ | ||||||
|  | const updateDeviceTemplate = (id, deviceTemplateData) => { | ||||||
|  |   return http.put(`/api/v1/device-templates/${id}`, deviceTemplateData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据设备模板ID删除一个设备模板(软删除) | ||||||
|  |  * @param {number} id - 设备模板ID | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | const deleteDeviceTemplate = (id) => { | ||||||
|  |   return http.delete(`/api/v1/device-templates/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const DeviceTemplateApi = { | ||||||
|  |   getDeviceTemplates, | ||||||
|  |   createDeviceTemplate, | ||||||
|  |   getDeviceTemplateById, | ||||||
|  |   updateDeviceTemplate, | ||||||
|  |   deleteDeviceTemplate, | ||||||
|  | }; | ||||||
| @@ -1,15 +1,18 @@ | |||||||
| import DeviceApi from './device.js'; | import { AreaControllerApi, DeviceApi } from './device.js'; | ||||||
| import PlanApi from './plan.js'; | import { PlanApi } from './plan.js'; | ||||||
| import UserApi from './user.js'; | import { UserApi } from './user.js'; | ||||||
|  | import { DeviceTemplateApi } from './deviceTemplate.js'; // 导入设备模板API | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * API客户端 |  * API客户端 | ||||||
|  */ |  */ | ||||||
| export class ApiClient { | export class ApiClient { | ||||||
|   constructor() { |   constructor() { | ||||||
|  |     this.areaControllers = AreaControllerApi; | ||||||
|     this.devices = DeviceApi; |     this.devices = DeviceApi; | ||||||
|     this.plans = PlanApi; |     this.plans = PlanApi; | ||||||
|     this.users = UserApi; |     this.users = UserApi; | ||||||
|  |     this.deviceTemplates = DeviceTemplateApi; // 添加设备模板API | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										900
									
								
								src/api/monitor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										900
									
								
								src/api/monitor.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,900 @@ | |||||||
|  | import http from '../utils/http'; | ||||||
|  |  | ||||||
|  | // --- Typedefs --- | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PaginationDTO | ||||||
|  |  * @property {number} page | ||||||
|  |  * @property {number} page_size | ||||||
|  |  * @property {number} total | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} DeviceCommandLogDTO | ||||||
|  |  * @property {string} message_id | ||||||
|  |  * @property {number} device_id | ||||||
|  |  * @property {string} sent_at | ||||||
|  |  * @property {string} acknowledged_at | ||||||
|  |  * @property {boolean} received_success | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListDeviceCommandLogResponse | ||||||
|  |  * @property {Array<DeviceCommandLogDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} DeviceCommandLogsParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [device_id] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  * @property {boolean} [received_success] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} FeedFormulaDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {string} name | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PenDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {string} name | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} FeedUsageRecordDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} pen_id | ||||||
|  |  * @property {PenDTO} pen | ||||||
|  |  * @property {number} feed_formula_id | ||||||
|  |  * @property {FeedFormulaDTO} feed_formula | ||||||
|  |  * @property {number} amount | ||||||
|  |  * @property {string} recorded_at | ||||||
|  |  * @property {string} remarks | ||||||
|  |  * @property {number} operator_id | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListFeedUsageRecordResponse | ||||||
|  |  * @property {Array<FeedUsageRecordDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} FeedUsageRecordsParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [pen_id] | ||||||
|  |  * @property {number} [feed_formula_id] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  * @property {number} [operator_id] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('预防'|'治疗'|'保健')} MedicationReasonType | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} MedicationDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {string} name | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} MedicationLogDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} pig_batch_id | ||||||
|  |  * @property {number} medication_id | ||||||
|  |  * @property {MedicationDTO} medication | ||||||
|  |  * @property {number} dosage_used | ||||||
|  |  * @property {number} target_count | ||||||
|  |  * @property {MedicationReasonType} reason | ||||||
|  |  * @property {string} description | ||||||
|  |  * @property {string} happened_at | ||||||
|  |  * @property {number} operator_id | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListMedicationLogResponse | ||||||
|  |  * @property {Array<MedicationLogDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} MedicationLogsParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [pig_batch_id] | ||||||
|  |  * @property {number} [medication_id] | ||||||
|  |  * @property {string} [reason] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  * @property {number} [operator_id] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('邮件'|'企业微信'|'飞书'|'日志')} NotifierType | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('发送成功'|'发送失败'|'已跳过')} NotificationStatus | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} NotificationDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} user_id | ||||||
|  |  * @property {NotifierType} notifier_type | ||||||
|  |  * @property {string} to_address | ||||||
|  |  * @property {string} title | ||||||
|  |  * @property {string} message | ||||||
|  |  * @property {number} level - 日志级别, 见 ZapcoreLevel 枚举 | ||||||
|  |  * @property {string} alarm_timestamp | ||||||
|  |  * @property {NotificationStatus} status | ||||||
|  |  * @property {string} error_message | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} updated_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListNotificationResponse | ||||||
|  |  * @property {Array<NotificationDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} NotificationsParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  * @property {number} [level] - 日志级别, 见 ZapcoreLevel 枚举 | ||||||
|  |  * @property {NotifierType} [notifier_type] | ||||||
|  |  * @property {NotificationStatus} [status] | ||||||
|  |  * @property {number} [user_id] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('等待中'|'已完成'|'已超时')} PendingCollectionStatus | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PendingCollectionDTO | ||||||
|  |  * @property {string} correlation_id | ||||||
|  |  * @property {number} device_id | ||||||
|  |  * @property {Array<number>} command_metadata | ||||||
|  |  * @property {PendingCollectionStatus} status | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} fulfilled_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListPendingCollectionResponse | ||||||
|  |  * @property {Array<PendingCollectionDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PendingCollectionsParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [device_id] | ||||||
|  |  * @property {string} [status] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('死亡'|'淘汰'|'销售'|'购买'|'转入'|'转出'|'盘点校正')} LogChangeType | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigBatchLogDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} pig_batch_id | ||||||
|  |  * @property {LogChangeType} change_type | ||||||
|  |  * @property {number} before_count | ||||||
|  |  * @property {number} after_count | ||||||
|  |  * @property {number} change_count | ||||||
|  |  * @property {string} reason | ||||||
|  |  * @property {number} operator_id | ||||||
|  |  * @property {string} happened_at | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} updated_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListPigBatchLogResponse | ||||||
|  |  * @property {Array<PigBatchLogDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigBatchLogsParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [pig_batch_id] | ||||||
|  |  * @property {string} [change_type] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  * @property {number} [operator_id] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigPurchaseDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} pig_batch_id | ||||||
|  |  * @property {number} quantity | ||||||
|  |  * @property {number} unit_price | ||||||
|  |  * @property {number} total_price | ||||||
|  |  * @property {string} supplier | ||||||
|  |  * @property {string} purchase_date | ||||||
|  |  * @property {string} remarks | ||||||
|  |  * @property {number} operator_id | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} updated_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListPigPurchaseResponse | ||||||
|  |  * @property {Array<PigPurchaseDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigPurchasesParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [pig_batch_id] | ||||||
|  |  * @property {string} [supplier] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  * @property {number} [operator_id] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigSaleDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} pig_batch_id | ||||||
|  |  * @property {number} quantity | ||||||
|  |  * @property {number} unit_price | ||||||
|  |  * @property {number} total_price | ||||||
|  |  * @property {string} buyer | ||||||
|  |  * @property {string} sale_date | ||||||
|  |  * @property {string} remarks | ||||||
|  |  * @property {number} operator_id | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} updated_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListPigSaleResponse | ||||||
|  |  * @property {Array<PigSaleDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigSalesParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [pig_batch_id] | ||||||
|  |  * @property {string} [buyer] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  * @property {number} [operator_id] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('患病'|'康复'|'死亡'|'淘汰'|'转入'|'转出'|'其他')} PigBatchSickPigReasonType | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('原地治疗'|'病猪栏治疗')} PigBatchSickPigTreatmentLocation | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigSickLogDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} pig_batch_id | ||||||
|  |  * @property {number} pen_id | ||||||
|  |  * @property {PigBatchSickPigReasonType} reason | ||||||
|  |  * @property {PigBatchSickPigTreatmentLocation} treatment_location | ||||||
|  |  * @property {number} before_count | ||||||
|  |  * @property {number} after_count | ||||||
|  |  * @property {number} change_count | ||||||
|  |  * @property {string} remarks | ||||||
|  |  * @property {number} operator_id | ||||||
|  |  * @property {string} happened_at | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} updated_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListPigSickLogResponse | ||||||
|  |  * @property {Array<PigSickLogDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigSickLogsParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [pig_batch_id] | ||||||
|  |  * @property {number} [pen_id] | ||||||
|  |  * @property {string} [reason] | ||||||
|  |  * @property {string} [treatment_location] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  * @property {number} [operator_id] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('群内调栏'|'跨群调栏'|'销售'|'死亡'|'淘汰'|'新购入'|'产房转入')} PigTransferType | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigTransferLogDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} pig_batch_id | ||||||
|  |  * @property {number} pen_id | ||||||
|  |  * @property {PigTransferType} type | ||||||
|  |  * @property {number} quantity | ||||||
|  |  * @property {string} remarks | ||||||
|  |  * @property {string} correlation_id | ||||||
|  |  * @property {number} operator_id | ||||||
|  |  * @property {string} transfer_time | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} updated_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListPigTransferLogResponse | ||||||
|  |  * @property {Array<PigTransferLogDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigTransferLogsParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [pig_batch_id] | ||||||
|  |  * @property {number} [pen_id] | ||||||
|  |  * @property {string} [transfer_type] | ||||||
|  |  * @property {string} [correlation_id] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  * @property {number} [operator_id] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('已开始'|'已完成'|'失败'|'已取消'|'等待中')} ExecutionStatus | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PlanExecutionLogDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} plan_id | ||||||
|  |  * @property {string} plan_name | ||||||
|  |  * @property {ExecutionStatus} status | ||||||
|  |  * @property {string} started_at | ||||||
|  |  * @property {string} ended_at | ||||||
|  |  * @property {string} error | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} updated_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListPlanExecutionLogResponse | ||||||
|  |  * @property {Array<PlanExecutionLogDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PlanExecutionLogsParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [plan_id] | ||||||
|  |  * @property {string} [status] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} RawMaterialDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {string} name | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} RawMaterialPurchaseDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} raw_material_id | ||||||
|  |  * @property {RawMaterialDTO} raw_material | ||||||
|  |  * @property {number} amount | ||||||
|  |  * @property {number} unit_price | ||||||
|  |  * @property {number} total_price | ||||||
|  |  * @property {string} supplier | ||||||
|  |  * @property {string} purchase_date | ||||||
|  |  * @property {string} created_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListRawMaterialPurchaseResponse | ||||||
|  |  * @property {Array<RawMaterialPurchaseDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} RawMaterialPurchasesParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [raw_material_id] | ||||||
|  |  * @property {string} [supplier] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('采购入库'|'饲喂出库'|'变质出库'|'售卖出库'|'杂用领取'|'手动盘点')} StockLogSourceType | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} RawMaterialStockLogDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} raw_material_id | ||||||
|  |  * @property {number} change_amount | ||||||
|  |  * @property {StockLogSourceType} source_type | ||||||
|  |  * @property {number} source_id | ||||||
|  |  * @property {string} remarks | ||||||
|  |  * @property {string} happened_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListRawMaterialStockLogResponse | ||||||
|  |  * @property {Array<RawMaterialStockLogDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} RawMaterialStockLogsParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [raw_material_id] | ||||||
|  |  * @property {string} [source_type] | ||||||
|  |  * @property {number} [source_id] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('信号强度'|'电池电量'|'温度'|'湿度'|'重量')} SensorType | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} SensorDataDTO | ||||||
|  |  * @property {number} regional_controller_id | ||||||
|  |  * @property {number} device_id | ||||||
|  |  * @property {SensorType} sensor_type | ||||||
|  |  * @property {Array<number>} data | ||||||
|  |  * @property {string} time | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListSensorDataResponse | ||||||
|  |  * @property {Array<SensorDataDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} SensorDataParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [device_id] | ||||||
|  |  * @property {string} [sensor_type] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} TaskDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {string} name | ||||||
|  |  * @property {string} description | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} TaskExecutionLogDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} plan_execution_log_id | ||||||
|  |  * @property {number} task_id | ||||||
|  |  * @property {TaskDTO} task | ||||||
|  |  * @property {ExecutionStatus} status | ||||||
|  |  * @property {string} output | ||||||
|  |  * @property {string} started_at | ||||||
|  |  * @property {string} ended_at | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} updated_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListTaskExecutionLogResponse | ||||||
|  |  * @property {Array<TaskExecutionLogDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} TaskExecutionLogsParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [plan_execution_log_id] | ||||||
|  |  * @property {number} [task_id] | ||||||
|  |  * @property {string} [status] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('成功'|'失败')} AuditStatus | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} UserActionLogDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} user_id | ||||||
|  |  * @property {string} username | ||||||
|  |  * @property {string} action_type | ||||||
|  |  * @property {string} description | ||||||
|  |  * @property {string} http_method | ||||||
|  |  * @property {string} http_path | ||||||
|  |  * @property {string} source_ip | ||||||
|  |  * @property {Array<number>} target_resource | ||||||
|  |  * @property {AuditStatus} status | ||||||
|  |  * @property {string} result_details | ||||||
|  |  * @property {string} time | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListUserActionLogResponse | ||||||
|  |  * @property {Array<UserActionLogDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} UserActionLogsParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [user_id] | ||||||
|  |  * @property {string} [username] | ||||||
|  |  * @property {string} [action_type] | ||||||
|  |  * @property {string} [status] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} WeighingBatchDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} pig_batch_id | ||||||
|  |  * @property {string} description | ||||||
|  |  * @property {string} weighing_time | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} updated_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListWeighingBatchResponse | ||||||
|  |  * @property {Array<WeighingBatchDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} WeighingBatchesParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [pig_batch_id] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} WeighingRecordDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} weighing_batch_id | ||||||
|  |  * @property {number} pen_id | ||||||
|  |  * @property {number} weight | ||||||
|  |  * @property {string} remark | ||||||
|  |  * @property {number} operator_id | ||||||
|  |  * @property {string} weighing_time | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} updated_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListWeighingRecordResponse | ||||||
|  |  * @property {Array<WeighingRecordDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} WeighingRecordsParams | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [weighing_batch_id] | ||||||
|  |  * @property {number} [pen_id] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  * @property {number} [operator_id] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | // --- Enums --- | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 日志级别, 对应后端的 zapcore.Level | ||||||
|  |  * @enum {number} | ||||||
|  |  */ | ||||||
|  | export const ZapcoreLevel = { | ||||||
|  |   Debug: -1, | ||||||
|  |   Info: 0, | ||||||
|  |   Warn: 1, | ||||||
|  |   Error: 2, | ||||||
|  |   DPanic: 3, | ||||||
|  |   Panic: 4, | ||||||
|  |   Fatal: 5, | ||||||
|  |   _minLevel: -1, | ||||||
|  |   _maxLevel: 5, | ||||||
|  |   Invalid: 6, | ||||||
|  |   _numLevels: 7, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // --- Functions --- | ||||||
|  |  | ||||||
|  | const processResponse = (responseData) => { | ||||||
|  |   const data = responseData.data; | ||||||
|  |   return { | ||||||
|  |     list: data.list || [], | ||||||
|  |     total: data.pagination ? data.pagination.total : 0, | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取设备命令日志列表 | ||||||
|  |  * @param {DeviceCommandLogsParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<DeviceCommandLogDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getDeviceCommandLogs = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/device-command-logs', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取饲料使用记录列表 | ||||||
|  |  * @param {FeedUsageRecordsParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<FeedUsageRecordDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getFeedUsageRecords = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/feed-usage-records', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取用药记录列表 | ||||||
|  |  * @param {MedicationLogsParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<MedicationLogDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getMedicationLogs = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/medication-logs', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 批量查询通知 | ||||||
|  |  * @param {NotificationsParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<NotificationDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getNotifications = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/notifications', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取待采集请求列表 | ||||||
|  |  * @param {PendingCollectionsParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<PendingCollectionDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getPendingCollections = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/pending-collections', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取猪批次日志列表 | ||||||
|  |  * @param {PigBatchLogsParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<PigBatchLogDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getPigBatchLogs = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/pig-batch-logs', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取猪只采购记录列表 | ||||||
|  |  * @param {PigPurchasesParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<PigPurchaseDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getPigPurchases = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/pig-purchases', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取猪只售卖记录列表 | ||||||
|  |  * @param {PigSalesParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<PigSaleDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getPigSales = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/pig-sales', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取病猪日志列表 | ||||||
|  |  * @param {PigSickLogsParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<PigSickLogDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getPigSickLogs = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/pig-sick-logs', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取猪只迁移日志列表 | ||||||
|  |  * @param {PigTransferLogsParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<PigTransferLogDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getPigTransferLogs = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/pig-transfer-logs', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取计划执行日志列表 | ||||||
|  |  * @param {PlanExecutionLogsParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<PlanExecutionLogDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getPlanExecutionLogs = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/plan-execution-logs', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取原料采购记录列表 | ||||||
|  |  * @param {RawMaterialPurchasesParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<RawMaterialPurchaseDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getRawMaterialPurchases = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/raw-material-purchases', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取原料库存日志列表 | ||||||
|  |  * @param {RawMaterialStockLogsParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<RawMaterialStockLogDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getRawMaterialStockLogs = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/raw-material-stock-logs', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取传感器数据列表 | ||||||
|  |  * @param {SensorDataParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<SensorDataDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getSensorData = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/sensor-data', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取任务执行日志列表 | ||||||
|  |  * @param {TaskExecutionLogsParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<TaskExecutionLogDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getTaskExecutionLogs = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/task-execution-logs', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取用户操作日志列表 | ||||||
|  |  * @param {UserActionLogsParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<UserActionLogDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getUserActionLogs = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/user-action-logs', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取批次称重记录列表 | ||||||
|  |  * @param {WeighingBatchesParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<WeighingBatchDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getWeighingBatches = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/weighing-batches', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取单次称重记录列表 | ||||||
|  |  * @param {WeighingRecordsParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<{list: Array<WeighingRecordDTO>, total: number}>} | ||||||
|  |  */ | ||||||
|  | export const getWeighingRecords = async (params) => { | ||||||
|  |   const newParams = { ...params, page_size: params.page_size }; | ||||||
|  |   const responseData = await http.get('/api/v1/monitor/weighing-records', { params: newParams }); | ||||||
|  |   return processResponse(responseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const MonitorApi = { | ||||||
|  |   getDeviceCommandLogs, | ||||||
|  |   getFeedUsageRecords, | ||||||
|  |   getMedicationLogs, | ||||||
|  |   getNotifications, | ||||||
|  |   getPendingCollections, | ||||||
|  |   getPigBatchLogs, | ||||||
|  |   getPigPurchases, | ||||||
|  |   getPigSales, | ||||||
|  |   getPigSickLogs, | ||||||
|  |   getPigTransferLogs, | ||||||
|  |   getPlanExecutionLogs, | ||||||
|  |   getRawMaterialPurchases, | ||||||
|  |   getRawMaterialStockLogs, | ||||||
|  |   getSensorData, | ||||||
|  |   getTaskExecutionLogs, | ||||||
|  |   getUserActionLogs, | ||||||
|  |   getWeighingBatches, | ||||||
|  |   getWeighingRecords, | ||||||
|  | }; | ||||||
							
								
								
									
										103
									
								
								src/api/pen.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								src/api/pen.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | |||||||
|  | import http from '../utils/http'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} Response | ||||||
|  |  * @property {number} code - 业务状态码 | ||||||
|  |  * @property {object} [data] - 业务数据 | ||||||
|  |  * @property {string} [message] - 提示信息 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PenResponse | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} house_id | ||||||
|  |  * @property {string} pen_number | ||||||
|  |  * @property {number} capacity | ||||||
|  |  * @property {number} current_pig_count | ||||||
|  |  * @property {number} pig_batch_id | ||||||
|  |  * @property {('空闲'|'使用中'|'病猪栏'|'康复栏'|'清洗消毒'|'维修中')} status | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} CreatePenRequest | ||||||
|  |  * @property {number} house_id | ||||||
|  |  * @property {string} pen_number | ||||||
|  |  * @property {number} capacity | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} UpdatePenRequest | ||||||
|  |  * @property {number} house_id | ||||||
|  |  * @property {string} pen_number | ||||||
|  |  * @property {number} capacity | ||||||
|  |  * @property {('空闲'|'使用中'|'病猪栏'|'康复栏'|'清洗消毒'|'维修中')} status | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} UpdatePenStatusRequest | ||||||
|  |  * @property {('空闲'|'使用中'|'病猪栏'|'康复栏'|'清洗消毒'|'维修中')} status | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取所有猪栏的列表 | ||||||
|  |  * @returns {Promise<Array<PenResponse>>} | ||||||
|  |  */ | ||||||
|  | export const getPens = () => { | ||||||
|  |   return http.get('/api/v1/pens'); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 创建一个新的猪栏 | ||||||
|  |  * @param {CreatePenRequest} penData - 猪栏信息 | ||||||
|  |  * @returns {Promise<PenResponse>} | ||||||
|  |  */ | ||||||
|  | export const createPen = (penData) => { | ||||||
|  |   return http.post('/api/v1/pens', penData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据ID获取单个猪栏信息 | ||||||
|  |  * @param {number} id - 猪栏ID | ||||||
|  |  * @returns {Promise<PenResponse>} | ||||||
|  |  */ | ||||||
|  | export const getPenById = (id) => { | ||||||
|  |   return http.get(`/api/v1/pens/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 更新一个已存在的猪栏信息 | ||||||
|  |  * @param {number} id - 猪栏ID | ||||||
|  |  * @param {UpdatePenRequest} penData - 猪栏信息 | ||||||
|  |  * @returns {Promise<PenResponse>} | ||||||
|  |  */ | ||||||
|  | export const updatePen = (id, penData) => { | ||||||
|  |   return http.put(`/api/v1/pens/${id}`, penData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据ID删除一个猪栏 | ||||||
|  |  * @param {number} id - 猪栏ID | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const deletePen = (id) => { | ||||||
|  |   return http.delete(`/api/v1/pens/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 更新指定猪栏的当前状态 | ||||||
|  |  * @param {number} id - 猪栏ID | ||||||
|  |  * @param {UpdatePenStatusRequest} statusData - 新的猪栏状态 | ||||||
|  |  * @returns {Promise<PenResponse>} | ||||||
|  |  */ | ||||||
|  | export const updatePenStatus = (id, statusData) => { | ||||||
|  |   return http.put(`/api/v1/pens/${id}/status`, statusData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const PenApi = { | ||||||
|  |   getPens, | ||||||
|  |   createPen, | ||||||
|  |   getPenById, | ||||||
|  |   updatePen, | ||||||
|  |   deletePen, | ||||||
|  |   updatePenStatus, | ||||||
|  | }; | ||||||
							
								
								
									
										424
									
								
								src/api/pigBatch.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										424
									
								
								src/api/pigBatch.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,424 @@ | |||||||
|  | import http from '../utils/http'; | ||||||
|  |  | ||||||
|  | // --- Typedefs --- | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} Response | ||||||
|  |  * @property {number} code - 业务状态码 | ||||||
|  |  * @property {object} [data] - 业务数据 | ||||||
|  |  * @property {string} [message] - 提示信息 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('自繁'|'外购')} PigBatchOriginType | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('保育'|'生长'|'育肥'|'待售'|'已出售'|'已归档')} PigBatchStatus | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigBatchResponseDTO | ||||||
|  |  * @property {number} id - 批次ID | ||||||
|  |  * @property {string} batch_number - 批次编号 | ||||||
|  |  * @property {PigBatchOriginType} origin_type - 批次来源 | ||||||
|  |  * @property {string} [start_date] - 批次开始日期 | ||||||
|  |  * @property {string} [end_date] - 批次结束日期 | ||||||
|  |  * @property {number} initial_count - 初始数量 | ||||||
|  |  * @property {PigBatchStatus} status - 批次状态 | ||||||
|  |  * @property {boolean} is_active - 是否活跃 | ||||||
|  |  * @property {string} create_time - 创建时间 | ||||||
|  |  * @property {string} update_time - 更新时间 | ||||||
|  |  * @property {number} current_total_quantity - 当前总数 | ||||||
|  |  * @property {number} current_total_pigs_in_pens - 当前存栏总数 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigBatchesParams | ||||||
|  |  * @property {boolean} [is_active] - 是否活跃 (true/false) | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigBatchCreateDTO | ||||||
|  |  * @property {string} batch_number - 批次编号,必填 | ||||||
|  |  * @property {PigBatchOriginType} origin_type - 批次来源,必填 | ||||||
|  |  * @property {string} start_date - 批次开始日期,必填 | ||||||
|  |  * @property {number} initial_count - 初始数量,必填,最小为1 | ||||||
|  |  * @property {PigBatchStatus} status - 批次状态,必填 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigBatchUpdateDTO | ||||||
|  |  * @property {string} [batch_number] - 批次编号,可选 | ||||||
|  |  * @property {PigBatchOriginType} [origin_type] - 批次来源,可选 | ||||||
|  |  * @property {string} [start_date] - 批次开始日期,可选 | ||||||
|  |  * @property {string} [end_date] - 批次结束日期,可选 | ||||||
|  |  * @property {number} [initial_count] - 初始数量,可选 | ||||||
|  |  * @property {PigBatchStatus} [status] - 批次状态,可选 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} AssignEmptyPensToBatchRequest | ||||||
|  |  * @property {Array<number>} pen_ids - 待分配的猪栏ID列表 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} BuyPigsRequest | ||||||
|  |  * @property {number} pen_id - 猪栏ID | ||||||
|  |  * @property {number} quantity - 买入猪只数量 | ||||||
|  |  * @property {number} unit_price - 单价 | ||||||
|  |  * @property {number} total_price - 总价 | ||||||
|  |  * @property {string} trader_name - 交易方名称 | ||||||
|  |  * @property {string} trade_date - 交易日期 | ||||||
|  |  * @property {string} [remarks] - 备注 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} SellPigsRequest | ||||||
|  |  * @property {number} pen_id - 猪栏ID | ||||||
|  |  * @property {number} quantity - 卖出猪只数量 | ||||||
|  |  * @property {number} unit_price - 单价 | ||||||
|  |  * @property {number} total_price - 总价 | ||||||
|  |  * @property {string} trader_name - 交易方名称 | ||||||
|  |  * @property {string} trade_date - 交易日期 | ||||||
|  |  * @property {string} [remarks] - 备注 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} MovePigsIntoPenRequest | ||||||
|  |  * @property {number} to_pen_id - 目标猪栏ID | ||||||
|  |  * @property {number} quantity - 移入猪只数量 | ||||||
|  |  * @property {string} [remarks] - 备注 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} TransferPigsWithinBatchRequest | ||||||
|  |  * @property {number} from_pen_id - 源猪栏ID | ||||||
|  |  * @property {number} to_pen_id - 目标猪栏ID | ||||||
|  |  * @property {number} quantity - 调栏猪只数量 | ||||||
|  |  * @property {string} [remarks] - 备注 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} TransferPigsAcrossBatchesRequest | ||||||
|  |  * @property {number} dest_batch_id - 目标猪批次ID | ||||||
|  |  * @property {number} from_pen_id - 源猪栏ID | ||||||
|  |  * @property {number} to_pen_id - 目标猪栏ID | ||||||
|  |  * @property {number} quantity - 调栏猪只数量 | ||||||
|  |  * @property {string} [remarks] - 备注 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ReclassifyPenToNewBatchRequest | ||||||
|  |  * @property {number} pen_id - 待划拨的猪栏ID | ||||||
|  |  * @property {number} to_batch_id - 目标猪批次ID | ||||||
|  |  * @property {string} [remarks] - 备注 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('原地治疗'|'病猪栏治疗')} PigBatchSickPigTreatmentLocation | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} RecordSickPigsRequest | ||||||
|  |  * @property {number} pen_id - 猪栏ID | ||||||
|  |  * @property {number} quantity - 病猪数量 | ||||||
|  |  * @property {PigBatchSickPigTreatmentLocation} treatment_location - 治疗地点 | ||||||
|  |  * @property {string} happened_at - 发生时间 | ||||||
|  |  * @property {string} [remarks] - 备注 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} RecordSickPigRecoveryRequest | ||||||
|  |  * @property {number} pen_id - 猪栏ID | ||||||
|  |  * @property {number} quantity - 康复猪数量 | ||||||
|  |  * @property {PigBatchSickPigTreatmentLocation} treatment_location - 治疗地点 | ||||||
|  |  * @property {string} happened_at - 发生时间 | ||||||
|  |  * @property {string} [remarks] - 备注 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} RecordSickPigDeathRequest | ||||||
|  |  * @property {number} pen_id - 猪栏ID | ||||||
|  |  * @property {number} quantity - 死亡猪数量 | ||||||
|  |  * @property {PigBatchSickPigTreatmentLocation} treatment_location - 治疗地点 | ||||||
|  |  * @property {string} happened_at - 发生时间 | ||||||
|  |  * @property {string} [remarks] - 备注 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} RecordSickPigCullRequest | ||||||
|  |  * @property {number} pen_id - 猪栏ID | ||||||
|  |  * @property {number} quantity - 淘汰猪数量 | ||||||
|  |  * @property {PigBatchSickPigTreatmentLocation} treatment_location - 治疗地点 | ||||||
|  |  * @property {string} happened_at - 发生时间 | ||||||
|  |  * @property {string} [remarks] - 备注 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} RecordDeathRequest | ||||||
|  |  * @property {number} pen_id - 猪栏ID | ||||||
|  |  * @property {number} quantity - 死亡猪数量 | ||||||
|  |  * @property {string} happened_at - 发生时间 | ||||||
|  |  * @property {string} [remarks] - 备注 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} RecordCullRequest | ||||||
|  |  * @property {number} pen_id - 猪栏ID | ||||||
|  |  * @property {number} quantity - 淘汰猪数量 | ||||||
|  |  * @property {string} happened_at - 发生时间 | ||||||
|  |  * @property {string} [remarks] - 备注 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PenResponse | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} house_id | ||||||
|  |  * @property {string} pen_number | ||||||
|  |  * @property {number} capacity | ||||||
|  |  * @property {number} current_pig_count | ||||||
|  |  * @property {number} pig_batch_id | ||||||
|  |  * @property {('空闲'|'使用中'|'病猪栏'|'康复栏'|'清洗消毒'|'维修中')} status | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigHouseResponse | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {string} name | ||||||
|  |  * @property {string} description | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | // --- 猪批次基础操作 --- | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取所有猪批次的列表 | ||||||
|  |  * @param {PigBatchesParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<Array<PigBatchResponseDTO>>} | ||||||
|  |  */ | ||||||
|  | export const getPigBatches = (params) => { | ||||||
|  |     return http.get('/api/v1/pig-batches', params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 创建一个新的猪批次 | ||||||
|  |  * @param {PigBatchCreateDTO} batchData - 猪批次信息 | ||||||
|  |  * @returns {Promise<PigBatchResponseDTO>} | ||||||
|  |  */ | ||||||
|  | export const createPigBatch = (batchData) => { | ||||||
|  |     return http.post('/api/v1/pig-batches', batchData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据ID获取单个猪批次信息 | ||||||
|  |  * @param {number} id - 猪批次ID | ||||||
|  |  * @returns {Promise<PigBatchResponseDTO>} | ||||||
|  |  */ | ||||||
|  | export const getPigBatchById = (id) => { | ||||||
|  |     return http.get(`/api/v1/pig-batches/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 更新一个已存在的猪批次信息 | ||||||
|  |  * @param {number} id - 猪批次ID | ||||||
|  |  * @param {PigBatchUpdateDTO} batchData - 猪批次信息 | ||||||
|  |  * @returns {Promise<PigBatchResponseDTO>} | ||||||
|  |  */ | ||||||
|  | export const updatePigBatch = (id, batchData) => { | ||||||
|  |     return http.put(`/api/v1/pig-batches/${id}`, batchData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据ID删除一个猪批次 | ||||||
|  |  * @param {number} id - 猪批次ID | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const deletePigBatch = (id) => { | ||||||
|  |     return http.delete(`/api/v1/pig-batches/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // --- 猪批次业务操作 --- | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 为猪批次分配空栏 | ||||||
|  |  * @param {number} id - 猪批次ID | ||||||
|  |  * @param {AssignEmptyPensToBatchRequest} pensData - 待分配的猪栏ID列表 | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const assignPensToBatch = (id, pensData) => { | ||||||
|  |     return http.post(`/api/v1/pig-batches/assign-pens/${id}`, pensData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 从猪批次移除空栏 | ||||||
|  |  * @param {number} penID - 待移除的猪栏ID | ||||||
|  |  * @param {number} batchID - 猪批次ID | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const removePenFromBatch = (penID, batchID) => { | ||||||
|  |     return http.delete(`/api/v1/pig-batches/remove-pen/${penID}/${batchID}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 处理买猪的业务逻辑 | ||||||
|  |  * @param {number} id - 猪批次ID | ||||||
|  |  * @param {BuyPigsRequest} buyData - 买猪请求信息 | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const buyPigsForBatch = (id, buyData) => { | ||||||
|  |     return http.post(`/api/v1/pig-batches/buy-pigs/${id}`, buyData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 处理卖猪的业务逻辑 | ||||||
|  |  * @param {number} id - 猪批次ID | ||||||
|  |  * @param {SellPigsRequest} sellData - 卖猪请求信息 | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const sellPigsFromBatch = (id, sellData) => { | ||||||
|  |     return http.post(`/api/v1/pig-batches/sell-pigs/${id}`, sellData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 将猪只从“虚拟库存”移入指定猪栏 | ||||||
|  |  * @param {number} id - 猪批次ID | ||||||
|  |  * @param {MovePigsIntoPenRequest} moveData - 移入猪只请求信息 | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const movePigsIntoPen = (id, moveData) => { | ||||||
|  |     return http.post(`/api/v1/pig-batches/move-pigs-into-pen/${id}`, moveData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 群内调栏 | ||||||
|  |  * @param {number} id - 猪批次ID | ||||||
|  |  * @param {TransferPigsWithinBatchRequest} transferData - 群内调栏请求信息 | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const transferPigsWithinBatch = (id, transferData) => { | ||||||
|  |     return http.post(`/api/v1/pig-batches/transfer-within-batch/${id}`, transferData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 跨猪群调栏 | ||||||
|  |  * @param {number} sourceBatchID - 源猪批次ID | ||||||
|  |  * @param {TransferPigsAcrossBatchesRequest} transferData - 跨群调栏请求信息 | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const transferPigsAcrossBatches = (sourceBatchID, transferData) => { | ||||||
|  |     return http.post(`/api/v1/pig-batches/transfer-across-batches/${sourceBatchID}`, transferData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 将猪栏划拨到新批次 | ||||||
|  |  * @param {number} fromBatchID - 源猪批次ID | ||||||
|  |  * @param {ReclassifyPenToNewBatchRequest} reclassifyData - 划拨请求信息 | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const reclassifyPenToNewBatch = (fromBatchID, reclassifyData) => { | ||||||
|  |     return http.post(`/api/v1/pig-batches/reclassify-pen/${fromBatchID}`, reclassifyData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // --- 猪只数量变更记录 --- | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 记录新增病猪事件 | ||||||
|  |  * @param {number} id - 猪批次ID | ||||||
|  |  * @param {RecordSickPigsRequest} sickData - 记录病猪请求信息 | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const recordSickPigsInBatch = (id, sickData) => { | ||||||
|  |     return http.post(`/api/v1/pig-batches/record-sick-pigs/${id}`, sickData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 记录病猪康复事件 | ||||||
|  |  * @param {number} id - 猪批次ID | ||||||
|  |  * @param {RecordSickPigRecoveryRequest} recoveryData - 记录病猪康复请求信息 | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const recordSickPigRecoveryInBatch = (id, recoveryData) => { | ||||||
|  |     return http.post(`/api/v1/pig-batches/record-sick-pig-recovery/${id}`, recoveryData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 记录病猪死亡事件 | ||||||
|  |  * @param {number} id - 猪批次ID | ||||||
|  |  * @param {RecordSickPigDeathRequest} deathData - 记录病猪死亡请求信息 | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const recordSickPigDeathInBatch = (id, deathData) => { | ||||||
|  |     return http.post(`/api/v1/pig-batches/record-sick-pig-death/${id}`, deathData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 记录病猪淘汰事件 | ||||||
|  |  * @param {number} id - 猪批次ID | ||||||
|  |  * @param {RecordSickPigCullRequest} cullData - 记录病猪淘汰请求信息 | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const recordSickPigCullInBatch = (id, cullData) => { | ||||||
|  |     return http.post(`/api/v1/pig-batches/record-sick-pig-cull/${id}`, cullData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 记录正常猪只死亡事件 | ||||||
|  |  * @param {number} id - 猪批次ID | ||||||
|  |  * @param {RecordDeathRequest} deathData - 记录正常猪只死亡请求信息 | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const recordDeathInBatch = (id, deathData) => { | ||||||
|  |     return http.post(`/api/v1/pig-batches/record-death/${id}`, deathData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 记录正常猪只淘汰事件 | ||||||
|  |  * @param {number} id - 猪批次ID | ||||||
|  |  * @param {RecordCullRequest} cullData - 记录正常猪只淘汰请求信息 | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const recordCullInBatch = (id, cullData) => { | ||||||
|  |     return http.post(`/api/v1/pig-batches/record-cull/${id}`, cullData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // --- 新增的猪栏和猪舍API --- | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取所有猪栏的列表 | ||||||
|  |  * @returns {Promise<Array<PenResponse>>} | ||||||
|  |  */ | ||||||
|  | export const getAllPens = () => { | ||||||
|  |     return http.get('/api/v1/pens'); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取所有猪舍的列表 | ||||||
|  |  * @returns {Promise<Array<PigHouseResponse>>} | ||||||
|  |  */ | ||||||
|  | export const getAllPigHouses = () => { | ||||||
|  |     return http.get('/api/v1/pig-houses'); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const PigBatchApi = { | ||||||
|  |     getPigBatches, | ||||||
|  |     createPigBatch, | ||||||
|  |     getPigBatchById, | ||||||
|  |     updatePigBatch, | ||||||
|  |     deletePigBatch, | ||||||
|  |     assignPensToBatch, | ||||||
|  |     removePenFromBatch, | ||||||
|  |     buyPigsForBatch, | ||||||
|  |     sellPigsFromBatch, | ||||||
|  |     movePigsIntoPen, | ||||||
|  |     transferPigsWithinBatch, | ||||||
|  |     transferPigsAcrossBatches, | ||||||
|  |     reclassifyPenToNewBatch, | ||||||
|  |     recordSickPigsInBatch, | ||||||
|  |     recordSickPigRecoveryInBatch, | ||||||
|  |     recordSickPigDeathInBatch, | ||||||
|  |     recordSickPigCullInBatch, | ||||||
|  |     recordDeathInBatch, | ||||||
|  |     recordCullInBatch, | ||||||
|  |     getAllPens, | ||||||
|  |     getAllPigHouses, | ||||||
|  | }; | ||||||
							
								
								
									
										80
									
								
								src/api/pigHouse.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/api/pigHouse.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | |||||||
|  | import http from '../utils/http'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} Response | ||||||
|  |  * @property {number} code - 业务状态码 | ||||||
|  |  * @property {object} [data] - 业务数据 | ||||||
|  |  * @property {string} [message] - 提示信息 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PigHouseResponse | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {string} name | ||||||
|  |  * @property {string} description | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} CreatePigHouseRequest | ||||||
|  |  * @property {string} name | ||||||
|  |  * @property {string} [description] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} UpdatePigHouseRequest | ||||||
|  |  * @property {string} name | ||||||
|  |  * @property {string} [description] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取所有猪舍的列表 | ||||||
|  |  * @returns {Promise<Array<PigHouseResponse>>} | ||||||
|  |  */ | ||||||
|  | export const getPigHouses = () => { | ||||||
|  |   return http.get('/api/v1/pig-houses'); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 创建一个新的猪舍 | ||||||
|  |  * @param {CreatePigHouseRequest} pigHouseData - 猪舍信息 | ||||||
|  |  * @returns {Promise<PigHouseResponse>} | ||||||
|  |  */ | ||||||
|  | export const createPigHouse = (pigHouseData) => { | ||||||
|  |   return http.post('/api/v1/pig-houses', pigHouseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据ID获取单个猪舍信息 | ||||||
|  |  * @param {number} id - 猪舍ID | ||||||
|  |  * @returns {Promise<PigHouseResponse>} | ||||||
|  |  */ | ||||||
|  | export const getPigHouseById = (id) => { | ||||||
|  |   return http.get(`/api/v1/pig-houses/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 更新一个已存在的猪舍信息 | ||||||
|  |  * @param {number} id - 猪舍ID | ||||||
|  |  * @param {UpdatePigHouseRequest} pigHouseData - 猪舍信息 | ||||||
|  |  * @returns {Promise<PigHouseResponse>} | ||||||
|  |  */ | ||||||
|  | export const updatePigHouse = (id, pigHouseData) => { | ||||||
|  |   return http.put(`/api/v1/pig-houses/${id}`, pigHouseData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据ID删除一个猪舍 | ||||||
|  |  * @param {number} id - 猪舍ID | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | export const deletePigHouse = (id) => { | ||||||
|  |   return http.delete(`/api/v1/pig-houses/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const PigHouseApi = { | ||||||
|  |   getPigHouses, | ||||||
|  |   createPigHouse, | ||||||
|  |   getPigHouseById, | ||||||
|  |   updatePigHouse, | ||||||
|  |   deletePigHouse, | ||||||
|  | }; | ||||||
							
								
								
									
										247
									
								
								src/api/plan.js
									
									
									
									
									
								
							
							
						
						
									
										247
									
								
								src/api/plan.js
									
									
									
									
									
								
							| @@ -1,71 +1,196 @@ | |||||||
| import http from '../utils/http.js'; | import http from '../utils/http'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 计划管理API |  * @typedef {('计划分析'|'等待'|'下料'|'全量采集')} TaskType | ||||||
|  */ |  */ | ||||||
| export class PlanApi { |  | ||||||
|   /** |  | ||||||
|    * 获取计划列表 |  | ||||||
|    * @returns {Promise} 计划列表 |  | ||||||
|    */ |  | ||||||
|   static list() { |  | ||||||
|     return http.get('/api/v1/plans'); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** | /** | ||||||
|    * 创建新计划 |  * @typedef {object} TaskRequest | ||||||
|    * @param {Object} planData 计划数据 |  * @property {string} [name] | ||||||
|    * @returns {Promise} 创建结果 |  * @property {string} [description] | ||||||
|    */ |  * @property {TaskType} [type] | ||||||
|   static create(planData) { |  * @property {object} [parameters] | ||||||
|     return http.post('/api/v1/plans', planData); |  * @property {number} [execution_order] | ||||||
|   } |  */ | ||||||
|  |  | ||||||
|   /** | /** | ||||||
|    * 获取计划详情 |  * @typedef {('自动'|'手动')} PlanExecutionType | ||||||
|    * @param {string|number} id 计划ID |  */ | ||||||
|    * @returns {Promise} 计划详情 |  | ||||||
|    */ |  | ||||||
|   static get(id) { |  | ||||||
|     return http.get(`/api/v1/plans/${id}`); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** | /** | ||||||
|    * 更新计划信息 |  * @typedef {object} CreatePlanRequest | ||||||
|    * @param {string|number} id 计划ID |  * @property {string} name | ||||||
|    * @param {Object} planData 计划数据 |  * @property {string} [description] | ||||||
|    * @returns {Promise} 更新结果 |  * @property {PlanExecutionType} execution_type | ||||||
|    */ |  * @property {string} [cron_expression] | ||||||
|   static update(id, planData) { |  * @property {number} [execute_num] | ||||||
|     return http.put(`/api/v1/plans/${id}`, planData); |  * @property {Array<TaskRequest>} [tasks] | ||||||
|   } |  * @property {Array<number>} [sub_plan_ids] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|   /** | /** | ||||||
|    * 删除计划 |  * @typedef {object} UpdatePlanRequest | ||||||
|    * @param {string|number} id 计划ID |  * @property {string} [name] | ||||||
|    * @returns {Promise} 删除结果 |  * @property {string} [description] | ||||||
|    */ |  * @property {PlanExecutionType} execution_type | ||||||
|   static delete(id) { |  * @property {string} [cron_expression] | ||||||
|     return http.delete(`/api/v1/plans/${id}`); |  * @property {number} [execute_num] | ||||||
|   } |  * @property {Array<TaskRequest>} [tasks] | ||||||
|  |  * @property {Array<number>} [sub_plan_ids] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|   /** | /** | ||||||
|    * 启动计划 |  * @typedef {object} TaskResponse | ||||||
|    * @param {string|number} id 计划ID |  * @property {number} id | ||||||
|    * @returns {Promise} 启动结果 |  * @property {number} plan_id | ||||||
|    */ |  * @property {string} name | ||||||
|   static start(id) { |  * @property {string} description | ||||||
|     return http.post(`/api/v1/plans/${id}/start`); |  * @property {TaskType} type | ||||||
|   } |  * @property {object} parameters | ||||||
|  |  * @property {number} execution_order | ||||||
|  |  */ | ||||||
|  |  | ||||||
|   /** | /** | ||||||
|    * 停止计划 |  * @typedef {object} SubPlanResponse | ||||||
|    * @param {string|number} id 计划ID |  * @property {number} id | ||||||
|    * @returns {Promise} 停止结果 |  * @property {number} parent_plan_id | ||||||
|    */ |  * @property {number} child_plan_id | ||||||
|   static stop(id) { |  * @property {number} execution_order | ||||||
|     return http.post(`/api/v1/plans/${id}/stop`); |  * @property {PlanResponse} child_plan | ||||||
|   } |  */ | ||||||
| } |  | ||||||
|  |  | ||||||
| export default PlanApi; | /** | ||||||
|  |  * @typedef {('已禁用'|'已启用'|'执行完毕'|'执行失败')} PlanStatus | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('子计划'|'任务')} PlanContentType | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('自定义任务'|'系统任务')} PlanType | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PlanResponse | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {string} name | ||||||
|  |  * @property {string} description | ||||||
|  |  * @property {PlanExecutionType} execution_type | ||||||
|  |  * @property {string} cron_expression | ||||||
|  |  * @property {number} execute_num | ||||||
|  |  * @property {number} execute_count | ||||||
|  |  * @property {PlanStatus} status | ||||||
|  |  * @property {PlanContentType} content_type | ||||||
|  |  * @property {PlanType} plan_type | ||||||
|  |  * @property {Array<TaskResponse>} tasks | ||||||
|  |  * @property {Array<SubPlanResponse>} sub_plans | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListPlansResponse | ||||||
|  |  * @property {Array<PlanResponse>} plans | ||||||
|  |  * @property {number} total | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PlanExecutionLogDTO | ||||||
|  |  * @property {string} created_at | ||||||
|  |  * @property {string} ended_at | ||||||
|  |  * @property {string} error | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} plan_id | ||||||
|  |  * @property {string} plan_name | ||||||
|  |  * @property {string} started_at | ||||||
|  |  * @property {string} status | ||||||
|  |  * @property {string} updated_at | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} Response | ||||||
|  |  * @property {number} code - 业务状态码 | ||||||
|  |  * @property {object} [data] - 业务数据 | ||||||
|  |  * @property {string} [message] - 提示信息 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取所有计划的列表 | ||||||
|  |  * @param {object} params - 查询参数 | ||||||
|  |  * @param {number} [params.page] - 页码 | ||||||
|  |  * @param {number} [params.page_size] - 每页大小 | ||||||
|  |  * @param {('所有任务'|'自定义任务'|'系统任务')} [params.plan_type] - 计划类型 | ||||||
|  |  * @returns {Promise<ListPlansResponse>} | ||||||
|  |  */ | ||||||
|  | const getPlans = (params) => { | ||||||
|  |   const newParams = { | ||||||
|  |     page: params.page, | ||||||
|  |     page_size: params.page_size, | ||||||
|  |     plan_type: params.plan_type, | ||||||
|  |   }; | ||||||
|  |   return http.get('/api/v1/plans', { params: newParams }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 创建一个新的计划 | ||||||
|  |  * @param {CreatePlanRequest} planData - 计划信息 | ||||||
|  |  * @returns {Promise<PlanResponse>} | ||||||
|  |  */ | ||||||
|  | const createPlan = (planData) => { | ||||||
|  |   return http.post('/api/v1/plans', planData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据计划ID获取单个计划的详细信息 | ||||||
|  |  * @param {number} id - 计划ID | ||||||
|  |  * @returns {Promise<PlanResponse>} | ||||||
|  |  */ | ||||||
|  | const getPlanById = (id) => { | ||||||
|  |   return http.get(`/api/v1/plans/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据计划ID更新计划的详细信息。系统计划不允许修改。 | ||||||
|  |  * @param {number} id - 计划ID | ||||||
|  |  * @param {UpdatePlanRequest} planData - 更新后的计划信息 | ||||||
|  |  * @returns {Promise<PlanResponse>} | ||||||
|  |  */ | ||||||
|  | const updatePlan = (id, planData) => { | ||||||
|  |   return http.put(`/api/v1/plans/${id}`, planData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据计划ID删除计划。(软删除)系统计划不允许删除。 | ||||||
|  |  * @param {number} id - 计划ID | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | const deletePlan = (id) => { | ||||||
|  |   return http.delete(`/api/v1/plans/${id}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据计划ID启动一个计划的执行。系统计划不允许手动启动。 | ||||||
|  |  * @param {number} id - 计划ID | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | const startPlan = (id) => { | ||||||
|  |   return http.post(`/api/v1/plans/${id}/start`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 根据计划ID停止一个正在执行的计划。系统计划不能被停止。 | ||||||
|  |  * @param {number} id - 计划ID | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | const stopPlan = (id) => { | ||||||
|  |   return http.post(`/api/v1/plans/${id}/stop`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const PlanApi = { | ||||||
|  |   getPlans, | ||||||
|  |   createPlan, | ||||||
|  |   getPlanById, | ||||||
|  |   updatePlan, | ||||||
|  |   deletePlan, | ||||||
|  |   startPlan, | ||||||
|  |   stopPlan, | ||||||
|  | }; | ||||||
|   | |||||||
							
								
								
									
										159
									
								
								src/api/user.js
									
									
									
									
									
								
							
							
						
						
									
										159
									
								
								src/api/user.js
									
									
									
									
									
								
							| @@ -1,26 +1,143 @@ | |||||||
| import http from '../utils/http.js'; | import http from '../utils/http'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 用户管理API |  * @typedef {object} CreateUserRequest | ||||||
|  |  * @property {string} username | ||||||
|  |  * @property {string} password | ||||||
|  */ |  */ | ||||||
| export class UserApi { |  | ||||||
|   /** |  | ||||||
|    * 创建新用户 |  | ||||||
|    * @param {Object} userData 用户数据 |  | ||||||
|    * @returns {Promise} 创建结果 |  | ||||||
|    */ |  | ||||||
|   static create(userData) { |  | ||||||
|     return http.post('/api/v1/users', userData); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** | /** | ||||||
|    * 用户登录 |  * @typedef {object} CreateUserResponse | ||||||
|    * @param {Object} credentials 登录凭证 {username, password} |  * @property {number} id | ||||||
|    * @returns {Promise} 登录结果 |  * @property {string} username | ||||||
|    */ |  */ | ||||||
|   static login(credentials) { |  | ||||||
|     return http.post('/api/v1/users/login', credentials); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export default UserApi; | /** | ||||||
|  |  * @typedef {object} LoginRequest | ||||||
|  |  * @property {string} identifier - Identifier 可以是用户名、邮箱、手机号、微信号或飞书账号 | ||||||
|  |  * @property {string} password | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} LoginResponse | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {string} username | ||||||
|  |  * @property {string} token | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('成功'|'失败')} AuditStatus | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} UserActionLogDTO | ||||||
|  |  * @property {number} id | ||||||
|  |  * @property {number} user_id | ||||||
|  |  * @property {string} username | ||||||
|  |  * @property {string} action_type | ||||||
|  |  * @property {string} description | ||||||
|  |  * @property {string} http_method | ||||||
|  |  * @property {string} http_path | ||||||
|  |  * @property {string} source_ip | ||||||
|  |  * @property {Array<number>} target_resource | ||||||
|  |  * @property {AuditStatus} status | ||||||
|  |  * @property {string} result_details | ||||||
|  |  * @property {string} time | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} PaginationDTO | ||||||
|  |  * @property {number} page | ||||||
|  |  * @property {number} page_size | ||||||
|  |  * @property {number} total | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} ListUserActionLogResponse | ||||||
|  |  * @property {Array<UserActionLogDTO>} list | ||||||
|  |  * @property {PaginationDTO} pagination | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} UserHistoryParams | ||||||
|  |  * @property {string} [action_type] | ||||||
|  |  * @property {string} [end_time] | ||||||
|  |  * @property {string} [order_by] | ||||||
|  |  * @property {number} [page] | ||||||
|  |  * @property {number} [page_size] | ||||||
|  |  * @property {string} [start_time] | ||||||
|  |  * @property {string} [status] | ||||||
|  |  * @property {number} [user_id] | ||||||
|  |  * @property {string} [username] | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {('邮件'|'企业微信'|'飞书'|'日志')} NotifierType | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} SendTestNotificationRequest | ||||||
|  |  * @property {NotifierType} type - Type 指定要测试的通知渠道 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef {object} Response | ||||||
|  |  * @property {number} code - 业务状态码 | ||||||
|  |  * @property {object} [data] - 业务数据 | ||||||
|  |  * @property {string} [message] - 提示信息 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 创建一个新用户 | ||||||
|  |  * @param {CreateUserRequest} userData - 用户信息 | ||||||
|  |  * @returns {Promise<CreateUserResponse>} | ||||||
|  |  */ | ||||||
|  | const createUser = (userData) => { | ||||||
|  |   return http.post('/api/v1/users', userData); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 用户登录 | ||||||
|  |  * @param {LoginRequest} credentials - 登录凭证 | ||||||
|  |  * @returns {Promise<LoginResponse>} | ||||||
|  |  */ | ||||||
|  | const login = (credentials) => { | ||||||
|  |   return http.post('/api/v1/users/login', credentials); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 获取用户操作日志列表 | ||||||
|  |  * @param {UserHistoryParams} params - 查询参数 | ||||||
|  |  * @returns {Promise<ListUserActionLogResponse>} | ||||||
|  |  */ | ||||||
|  | const getUserActionLogs = (params) => { | ||||||
|  |   const newParams = { | ||||||
|  |     action_type: params.action_type, | ||||||
|  |     end_time: params.end_time, | ||||||
|  |     order_by: params.order_by, | ||||||
|  |     page: params.page, | ||||||
|  |     page_size: params.page_size, | ||||||
|  |     start_time: params.start_time, | ||||||
|  |     status: params.status, | ||||||
|  |     user_id: params.user_id, | ||||||
|  |     username: params.username, | ||||||
|  |   }; | ||||||
|  |   return http.get('/api/v1/monitor/user-action-logs', { params: newParams }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 发送测试通知 | ||||||
|  |  * @param {number} id - 用户ID | ||||||
|  |  * @param {SendTestNotificationRequest} data - 请求体 | ||||||
|  |  * @returns {Promise<Response>} | ||||||
|  |  */ | ||||||
|  | const sendTestNotification = (id, data) => { | ||||||
|  |   return http.post(`/api/v1/users/${id}/notifications/test`, data); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const UserApi = { | ||||||
|  |   createUser, | ||||||
|  |   login, | ||||||
|  |   getUserActionLogs, | ||||||
|  |   sendTestNotification, | ||||||
|  | }; | ||||||
|   | |||||||
							
								
								
									
										80
									
								
								src/components/AllocatePigsDialog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/components/AllocatePigsDialog.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | |||||||
|  | <template> | ||||||
|  |   <el-dialog | ||||||
|  |     title="分配猪只" | ||||||
|  |     :model-value="visible" | ||||||
|  |     @update:model-value="$emit('update:visible', $event)" | ||||||
|  |     width="30%" | ||||||
|  |     @close="resetForm" | ||||||
|  |   > | ||||||
|  |     <el-form ref="form" :model="form" :rules="rules" label-width="100px"> | ||||||
|  |       <el-form-item label="未分配数量"> | ||||||
|  |         <span>{{ unassignedPigCount }}</span> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="分配数量" prop="quantity"> | ||||||
|  |         <el-input-number v-model="form.quantity" :min="1" :max="unassignedPigCount" /> | ||||||
|  |       </el-form-item> | ||||||
|  |     </el-form> | ||||||
|  |     <template #footer> | ||||||
|  |       <span class="dialog-footer"> | ||||||
|  |         <el-button @click="$emit('update:visible', false)">取 消</el-button> | ||||||
|  |         <el-button type="primary" @click="handleConfirm">确 定</el-button> | ||||||
|  |       </span> | ||||||
|  |     </template> | ||||||
|  |   </el-dialog> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   name: 'AllocatePigsDialog', | ||||||
|  |   props: { | ||||||
|  |     visible: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: true | ||||||
|  |     }, | ||||||
|  |     unassignedPigCount: { | ||||||
|  |       type: Number, | ||||||
|  |       required: true | ||||||
|  |     }, | ||||||
|  |     penId: { | ||||||
|  |       type: Number, | ||||||
|  |       required: true | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   emits: ['update:visible', 'confirm'], | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       form: { | ||||||
|  |         quantity: 1 | ||||||
|  |       }, | ||||||
|  |       rules: { | ||||||
|  |         quantity: [ | ||||||
|  |           { required: true, message: '请输入分配数量', trigger: 'blur' }, | ||||||
|  |           { type: 'integer', message: '请输入整数', trigger: 'blur' }, | ||||||
|  |           { validator: this.validateQuantity, trigger: 'blur' } | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     validateQuantity(rule, value, callback) { | ||||||
|  |       if (value > this.unassignedPigCount) { | ||||||
|  |         callback(new Error('分配数量不能超过未分配数量')); | ||||||
|  |       } else { | ||||||
|  |         callback(); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     handleConfirm() { | ||||||
|  |       this.$refs.form.validate(valid => { | ||||||
|  |         if (valid) { | ||||||
|  |           this.$emit('confirm', { penId: this.penId, quantity: this.form.quantity }); | ||||||
|  |           this.$emit('update:visible', false); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     resetForm() { | ||||||
|  |       this.$refs.form.resetFields(); | ||||||
|  |       this.form.quantity = 1; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
| @@ -14,32 +14,32 @@ | |||||||
|       @submit.prevent |       @submit.prevent | ||||||
|     > |     > | ||||||
|       <!-- 基础信息 --> |       <!-- 基础信息 --> | ||||||
|       <el-form-item label="设备名" prop="name"> |       <el-form-item label="名称" prop="name"> | ||||||
|         <el-input v-model="formData.name" /> |         <el-input v-model="formData.name" /> | ||||||
|       </el-form-item> |       </el-form-item> | ||||||
|        |        | ||||||
|       <el-form-item label="设备类型" prop="type"> |       <el-form-item label="类型" prop="type"> | ||||||
|         <el-select v-model="formData.type" @change="handleTypeChange"> |         <el-select v-model="formData.type" @change="handleTypeChange" :disabled="isEdit"> | ||||||
|           <el-option label="区域主控" value="area_controller" /> |           <el-option label="区域主控" value="area_controller" /> | ||||||
|           <el-option label="普通设备" value="device" /> |           <el-option label="普通设备" value="device" /> | ||||||
|         </el-select> |         </el-select> | ||||||
|       </el-form-item> |       </el-form-item> | ||||||
|        |        | ||||||
|       <el-form-item label="设备位置描述" prop="location"> |       <el-form-item label="位置描述" prop="location"> | ||||||
|         <el-input v-model="formData.location" type="textarea" /> |         <el-input v-model="formData.location" type="textarea" /> | ||||||
|       </el-form-item> |       </el-form-item> | ||||||
|        |        | ||||||
|       <!-- 区域主控类型额外字段 --> |       <!-- 区域主控类型额外字段 --> | ||||||
|       <div v-if="formData.type === 'area_controller'"> |       <div v-if="formData.type === 'area_controller'"> | ||||||
|         <el-form-item label="LoRa地址" prop="loraAddress"> |         <el-form-item label="网络ID" prop="network_id"> | ||||||
|           <el-input v-model="formData.loraAddress" /> |           <el-input v-model="formData.network_id" /> | ||||||
|         </el-form-item> |         </el-form-item> | ||||||
|       </div> |       </div> | ||||||
|        |        | ||||||
|       <!-- 普通设备类型额外字段 --> |       <!-- 普通设备类型额外字段 --> | ||||||
|       <div v-if="formData.type === 'device'"> |       <div v-if="formData.type === 'device'"> | ||||||
|         <el-form-item label="区域主控" prop="parentControllerId"> |         <el-form-item label="所属区域主控" prop="area_controller_id"> | ||||||
|           <el-select v-model="formData.parentControllerId" placeholder="请选择区域主控"> |           <el-select v-model="formData.area_controller_id" placeholder="请选择区域主控"> | ||||||
|             <el-option |             <el-option | ||||||
|               v-for="controller in areaControllers" |               v-for="controller in areaControllers" | ||||||
|               :key="controller.id" |               :key="controller.id" | ||||||
| @@ -49,38 +49,29 @@ | |||||||
|           </el-select> |           </el-select> | ||||||
|         </el-form-item> |         </el-form-item> | ||||||
|          |          | ||||||
|         <el-form-item label="设备种类" prop="subType"> |         <el-form-item label="设备模板" prop="device_template_id"> | ||||||
|           <el-select v-model="formData.subType" placeholder="请选择设备种类"> |           <el-select v-model="formData.device_template_id" placeholder="请选择设备模板"> | ||||||
|             <el-option label="风扇" value="fan" /> |             <el-option | ||||||
|             <el-option label="温度传感器" value="temperature" /> |               v-for="template in deviceTemplates" | ||||||
|             <el-option label="湿度传感器" value="humidity" /> |               :key="template.id" | ||||||
|             <el-option label="氨气传感器" value="ammonia" /> |               :label="template.name" | ||||||
|             <el-option label="饲料阀门" value="feed_valve" /> |               :value="template.id" | ||||||
|             <el-option label="水帘" value="water_curtain" /> |             /> | ||||||
|           </el-select> |           </el-select> | ||||||
|         </el-form-item> |         </el-form-item> | ||||||
|          |          | ||||||
|         <el-form-item label="485总线号" prop="busNumber"> |         <el-form-item label="485总线号" prop="properties.bus_number"> | ||||||
|           <el-input-number  |           <el-input-number  | ||||||
|             v-model="formData.busNumber"  |             v-model="formData.properties.bus_number"  | ||||||
|             :min="0" |             :min="0" | ||||||
|             controls-position="right" |             controls-position="right" | ||||||
|             style="width: 100%" |             style="width: 100%" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </el-form-item> | ||||||
|          |          | ||||||
|         <el-form-item label="485总线地址" prop="busAddress"> |         <el-form-item label="485总线地址" prop="properties.bus_address"> | ||||||
|           <el-input-number  |           <el-input-number  | ||||||
|             v-model="formData.busAddress"  |             v-model="formData.properties.bus_address"  | ||||||
|             :min="0" |  | ||||||
|             controls-position="right" |  | ||||||
|             style="width: 100%" |  | ||||||
|           /> |  | ||||||
|         </el-form-item> |  | ||||||
|          |  | ||||||
|         <el-form-item label="继电器通道号" prop="relayChannel"> |  | ||||||
|           <el-input-number  |  | ||||||
|             v-model="formData.relayChannel"  |  | ||||||
|             :min="0" |             :min="0" | ||||||
|             controls-position="right" |             controls-position="right" | ||||||
|             style="width: 100%" |             style="width: 100%" | ||||||
| @@ -99,9 +90,10 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { ref, reactive, onMounted, watch, computed } from 'vue'; | import { ref, reactive, onMounted, watch, computed, nextTick } from 'vue'; | ||||||
| import { ElMessage } from 'element-plus'; | import { ElMessage } from 'element-plus'; | ||||||
| import { DeviceApi } from '../api/device.js'; | import { AreaControllerApi, DeviceApi } from '../api/device.js'; | ||||||
|  | import deviceTemplateService from '../services/deviceTemplateService.js'; // 导入设备模板服务 | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   name: 'DeviceForm', |   name: 'DeviceForm', | ||||||
| @@ -121,121 +113,161 @@ export default { | |||||||
|   }, |   }, | ||||||
|   emits: ['update:visible', 'success', 'cancel'], |   emits: ['update:visible', 'success', 'cancel'], | ||||||
|   setup(props, { emit }) { |   setup(props, { emit }) { | ||||||
|     // 表单引用 |  | ||||||
|     const formRef = ref(null); |     const formRef = ref(null); | ||||||
|      |  | ||||||
|     // 加载状态 |  | ||||||
|     const loading = ref(false); |     const loading = ref(false); | ||||||
|      |  | ||||||
|     // 区域主控列表 |  | ||||||
|     const areaControllers = ref([]); |     const areaControllers = ref([]); | ||||||
|  |     const deviceTemplates = ref([]); // 新增设备模板列表状态 | ||||||
|      |      | ||||||
|     // 表单数据 |     const initialFormData = () => ({ | ||||||
|     const formData = reactive({ |  | ||||||
|       id: '', |       id: '', | ||||||
|       name: '', |       name: '', | ||||||
|       type: '', |       type: 'device', // 默认创建普通设备 | ||||||
|       location: '', |       location: '', | ||||||
|       // 区域主控字段 |       network_id: '', // 区域主控字段 | ||||||
|       loraAddress: '', |       area_controller_id: '', // 普通设备字段 | ||||||
|       // 普通设备字段 |       device_template_id: '', // 普通设备字段 | ||||||
|       parentControllerId: '', |       properties: { // 嵌套的properties对象 | ||||||
|       subType: '', |         bus_number: 0, | ||||||
|       busNumber: 0, |         bus_address: 0, | ||||||
|       busAddress: 0, |       } | ||||||
|       relayChannel: 0 |  | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     // 表单验证规则 |     const formData = reactive(initialFormData()); | ||||||
|     const rules = { |      | ||||||
|  |     const rules = computed(() => ({ | ||||||
|       name: [ |       name: [ | ||||||
|         { required: true, message: '请输入设备名', trigger: 'blur' } |         { required: true, message: '请输入名称', trigger: 'blur' } | ||||||
|       ], |       ], | ||||||
|       type: [ |       type: [ | ||||||
|         { required: true, message: '请选择设备类型', trigger: 'change' } |         { required: true, message: '请选择类型', trigger: 'change' } | ||||||
|       ], |       ], | ||||||
|       location: [ |       location: [ | ||||||
|         { required: true, message: '请输入设备位置描述', trigger: 'blur' } |         { required: true, message: '请输入位置描述', trigger: 'blur' } | ||||||
|       ], |       ], | ||||||
|       loraAddress: [ |       network_id: [ | ||||||
|         { required: true, message: '请输入LoRa地址', trigger: 'blur' } |         { required: formData.type === 'area_controller', message: '请输入网络ID', trigger: 'blur' } | ||||||
|       ], |       ], | ||||||
|       parentControllerId: [ |       area_controller_id: [ | ||||||
|         { required: true, message: '请选择区域主控', trigger: 'change' } |         { required: formData.type === 'device', message: '请选择所属区域主控', trigger: 'change' } | ||||||
|       ], |       ], | ||||||
|       subType: [ |       device_template_id: [ | ||||||
|         { required: true, message: '请选择设备种类', trigger: 'change' } |         { required: formData.type === 'device', message: '请选择设备模板', trigger: 'change' } | ||||||
|       ], |       ], | ||||||
|       busNumber: [ |       'properties.bus_number': [ | ||||||
|         { required: true, message: '请输入485总线号', trigger: 'blur' } |         { required: formData.type === 'device', message: '请输入485总线号', trigger: 'blur' } | ||||||
|       ], |       ], | ||||||
|       busAddress: [ |       'properties.bus_address': [ | ||||||
|         { required: true, message: '请输入485总线地址', trigger: 'blur' } |         { required: formData.type === 'device', message: '请输入485总线地址', trigger: 'blur' } | ||||||
|       ], |       ], | ||||||
|       relayChannel: [ |     })); | ||||||
|         { required: true, message: '请输入继电器通道号', trigger: 'blur' } |  | ||||||
|       ] |  | ||||||
|     }; |  | ||||||
|      |      | ||||||
|     // 标题计算 |  | ||||||
|     const title = computed(() => { |     const title = computed(() => { | ||||||
|       return props.isEdit ? '编辑设备' : '添加设备'; |       return props.isEdit ? '编辑设备' : '添加设备'; | ||||||
|     }); |     }); | ||||||
|      |      | ||||||
|     // 处理类型切换 |  | ||||||
|     const handleTypeChange = (value) => { |     const handleTypeChange = (value) => { | ||||||
|       // 清除之前的数据 |       // 清除不同类型特有的字段 | ||||||
|       if (value === 'area_controller') { |       if (value === 'area_controller') { | ||||||
|         // 清除普通设备字段 |         formData.area_controller_id = ''; | ||||||
|         formData.parentControllerId = ''; |         formData.device_template_id = ''; | ||||||
|         formData.subType = ''; |         formData.properties = { bus_number: 0, bus_address: 0 }; | ||||||
|         formData.busNumber = 0; |  | ||||||
|         formData.busAddress = 0; |  | ||||||
|         formData.relayChannel = 0; |  | ||||||
|       } else { |       } else { | ||||||
|         // 清除区域主控字段 |         formData.network_id = ''; | ||||||
|         formData.loraAddress = ''; |  | ||||||
|       } |       } | ||||||
|  |       // 触发验证规则更新 | ||||||
|  |       nextTick(() => { | ||||||
|  |         if (formRef.value) { | ||||||
|  |           formRef.value.clearValidate(); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|     }; |     }; | ||||||
|      |      | ||||||
|     // 获取区域主控列表 |  | ||||||
|     const loadAreaControllers = async () => { |     const loadAreaControllers = async () => { | ||||||
|       try { |       try { | ||||||
|         const response = await DeviceApi.list(); |         const response = await AreaControllerApi.list(); | ||||||
|         // 筛选出类型为区域主控的设备 |         areaControllers.value = response.data || []; | ||||||
|         areaControllers.value = response.data.filter(device => device.type === 'area_controller'); |  | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|         console.error('获取区域主控列表失败:', error); |         console.error('获取区域主控列表失败:', error); | ||||||
|         areaControllers.value = []; |         areaControllers.value = []; | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     // 关闭对话框 |     // 新增:加载设备模板列表 | ||||||
|  |     const loadDeviceTemplates = async () => { | ||||||
|  |       try { | ||||||
|  |         const response = await deviceTemplateService.getDeviceTemplates(); | ||||||
|  |         deviceTemplates.value = response.data || []; | ||||||
|  |       } catch (error) { | ||||||
|  |         console.error('获取设备模板列表失败:', error); | ||||||
|  |         deviceTemplates.value = []; | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |      | ||||||
|     const handleClose = () => { |     const handleClose = () => { | ||||||
|       emit('update:visible', false); |       emit('update:visible', false); | ||||||
|       emit('cancel'); |       emit('cancel'); | ||||||
|  |       // 重置表单 | ||||||
|  |       Object.assign(formData, initialFormData()); | ||||||
|  |       nextTick(() => { | ||||||
|  |         if (formRef.value) { | ||||||
|  |           formRef.value.resetFields(); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     const getSubmitData = () => { | ||||||
|  |       const data = { | ||||||
|  |         name: formData.name, | ||||||
|  |         location: formData.location, | ||||||
|  |         properties: formData.properties // properties直接作为对象传递 | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       if (formData.type === 'area_controller') { | ||||||
|  |         data.network_id = formData.network_id; | ||||||
|  |       } else { | ||||||
|  |         data.area_controller_id = formData.area_controller_id; | ||||||
|  |         data.device_template_id = formData.device_template_id; | ||||||
|  |       } | ||||||
|  |       return data; | ||||||
|     }; |     }; | ||||||
|      |      | ||||||
|     // 提交表单 |  | ||||||
|     const handleSubmit = async () => { |     const handleSubmit = async () => { | ||||||
|  |       if (!formRef.value) return; | ||||||
|       await formRef.value.validate(async (valid) => { |       await formRef.value.validate(async (valid) => { | ||||||
|         if (valid) { |         if (valid) { | ||||||
|           loading.value = true; |           loading.value = true; | ||||||
|           try { |           try { | ||||||
|             let result; |             let result; | ||||||
|  |             const submitData = getSubmitData(); | ||||||
|  |  | ||||||
|             if (props.isEdit) { |             if (props.isEdit) { | ||||||
|               // 编辑设备 |               if (formData.type === 'area_controller') { | ||||||
|               result = await DeviceApi.update(formData.id, getSubmitData()); |                 result = await AreaControllerApi.update(formData.id, submitData); | ||||||
|  |               } else { | ||||||
|  |                 result = await DeviceApi.update(formData.id, submitData); | ||||||
|  |               } | ||||||
|             } else { |             } else { | ||||||
|               // 创建设备 |               if (formData.type === 'area_controller') { | ||||||
|               result = await DeviceApi.create(getSubmitData()); |                 result = await AreaControllerApi.create(submitData); | ||||||
|  |               } else { | ||||||
|  |                 result = await DeviceApi.create(submitData); | ||||||
|  |               } | ||||||
|             } |             } | ||||||
|              |              | ||||||
|             emit('success', result); |             // 适配DeviceList的树形结构,添加type和parent_id | ||||||
|  |             const processedResult = { | ||||||
|  |               ...result.data, | ||||||
|  |               type: formData.type | ||||||
|  |             }; | ||||||
|  |             if (formData.type === 'device') { | ||||||
|  |               processedResult.parent_id = processedResult.area_controller_id; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             emit('success', processedResult); | ||||||
|             handleClose(); |             handleClose(); | ||||||
|           } catch (error) { |           } catch (error) { | ||||||
|             console.error('保存设备失败:', error); |             console.error('保存设备失败:', error); | ||||||
|             ElMessage.error(props.isEdit ? '编辑设备失败' : '创建设备失败'); |             ElMessage.error(props.isEdit ? '编辑设备失败: ' + (error.message || '未知错误') : '创建设备失败: ' + (error.message || '未知错误')); | ||||||
|           } finally { |           } finally { | ||||||
|             loading.value = false; |             loading.value = false; | ||||||
|           } |           } | ||||||
| @@ -243,91 +275,71 @@ export default { | |||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
|      |      | ||||||
|     // 获取提交数据 |  | ||||||
|     const getSubmitData = () => { |  | ||||||
|       const data = { |  | ||||||
|         name: formData.name, |  | ||||||
|         type: formData.type, |  | ||||||
|         location: formData.location |  | ||||||
|       }; |  | ||||||
|        |  | ||||||
|       // 添加properties字段,作为JSON对象传递 |  | ||||||
|       const properties = {}; |  | ||||||
|        |  | ||||||
|       if (formData.type === 'area_controller') { |  | ||||||
|         properties.lora_address = formData.loraAddress; |  | ||||||
|       } else if (formData.type === 'device') { |  | ||||||
|         properties.bus_number = formData.busNumber; |  | ||||||
|         properties.bus_address = formData.busAddress; |  | ||||||
|         properties.relay_channel = formData.relayChannel; |  | ||||||
|         data.parent_id = formData.parentControllerId; |  | ||||||
|         data.sub_type = formData.subType; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       // 直接使用properties对象,不进行序列化 |  | ||||||
|       data.properties = properties; |  | ||||||
|        |  | ||||||
|       return data; |  | ||||||
|     }; |  | ||||||
|      |  | ||||||
|     // 监听设备数据变化 |  | ||||||
|     watch(() => props.deviceData, (newVal) => { |     watch(() => props.deviceData, (newVal) => { | ||||||
|       if (newVal && Object.keys(newVal).length > 0) { |       if (newVal && Object.keys(newVal).length > 0) { | ||||||
|         // 填充表单数据 |         // 重置表单以清除旧数据和验证状态 | ||||||
|         Object.keys(formData).forEach(key => { |         Object.assign(formData, initialFormData()); | ||||||
|           // 处理字段名映射 |         nextTick(() => { | ||||||
|           let dataKey = key; |           if (formRef.value) { | ||||||
|           if (key === 'parentControllerId') { |             formRef.value.clearValidate(); | ||||||
|             dataKey = 'parent_id'; |  | ||||||
|           } else if (key === 'subType') { |  | ||||||
|             dataKey = 'sub_type'; |  | ||||||
|           } |  | ||||||
|            |  | ||||||
|           if (newVal[dataKey] !== undefined) { |  | ||||||
|             formData[key] = newVal[dataKey]; |  | ||||||
|           } |           } | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  |         formData.id = newVal.id; | ||||||
|  |         formData.name = newVal.name; | ||||||
|  |         formData.type = newVal.type; | ||||||
|  |         formData.location = newVal.location; | ||||||
|  |  | ||||||
|  |         if (newVal.type === 'area_controller') { | ||||||
|  |           formData.network_id = newVal.network_id || ''; | ||||||
|  |         } else if (newVal.type === 'device') { | ||||||
|  |           formData.area_controller_id = newVal.area_controller_id || newVal.parent_id || ''; | ||||||
|  |           formData.device_template_id = newVal.device_template_id || ''; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         // 处理properties对象的数据填充 |         // 处理properties对象的数据填充 | ||||||
|         if (newVal.properties) { |         if (newVal.properties) { | ||||||
|           const props = typeof newVal.properties === 'string' ? JSON.parse(newVal.properties) : newVal.properties; |           // 确保properties是一个对象,如果API返回的是字符串则尝试解析 | ||||||
|           if (formData.type === 'area_controller') { |           const propsData = typeof newVal.properties === 'string' ? JSON.parse(newVal.properties) : newVal.properties; | ||||||
|             formData.loraAddress = props.lora_address || ''; |           Object.assign(formData.properties, propsData); | ||||||
|           } else if (formData.type === 'device') { |  | ||||||
|             formData.busNumber = props.bus_number || 0; |  | ||||||
|             formData.busAddress = props.bus_address || 0; |  | ||||||
|             formData.relayChannel = props.relay_channel || 0; |  | ||||||
|           } |  | ||||||
|         } |         } | ||||||
|       } else { |       } else { | ||||||
|         // 重置表单数据 |         // 如果没有传入deviceData,则重置为初始状态 | ||||||
|         Object.keys(formData).forEach(key => { |         Object.assign(formData, initialFormData()); | ||||||
|           if (key === 'busNumber' || key === 'busAddress' || key === 'relayChannel') { |         nextTick(() => { | ||||||
|             formData[key] = 0; |           if (formRef.value) { | ||||||
|           } else { |             formRef.value.clearValidate(); | ||||||
|             formData[key] = ''; |  | ||||||
|           } |           } | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     }, { immediate: true }); |     }, { immediate: true }); | ||||||
|      |      | ||||||
|     // 监听visible属性变化,当对话框打开时重新加载区域主控列表 |  | ||||||
|     watch(() => props.visible, (newVal) => { |     watch(() => props.visible, (newVal) => { | ||||||
|       if (newVal) { |       if (newVal) { | ||||||
|         // 对话框打开时重新加载区域主控列表 |  | ||||||
|         loadAreaControllers(); |         loadAreaControllers(); | ||||||
|  |         loadDeviceTemplates(); // 新增:对话框打开时加载设备模板 | ||||||
|  |         // 如果是新增,确保type是默认值,且清空所有字段 | ||||||
|  |         if (!props.isEdit) { | ||||||
|  |           Object.assign(formData, initialFormData()); | ||||||
|  |           nextTick(() => { | ||||||
|  |             if (formRef.value) { | ||||||
|  |               formRef.value.clearValidate(); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     }, { immediate: true }); |     }, { immediate: true }); | ||||||
|      |      | ||||||
|     // 组件挂载时加载区域主控列表 |  | ||||||
|     onMounted(() => { |     onMounted(() => { | ||||||
|       loadAreaControllers(); |       loadAreaControllers(); | ||||||
|  |       loadDeviceTemplates(); // 新增:组件挂载时加载设备模板 | ||||||
|     }); |     }); | ||||||
|      |      | ||||||
|     return { |     return { | ||||||
|       formRef, |       formRef, | ||||||
|       loading, |       loading, | ||||||
|       areaControllers, |       areaControllers, | ||||||
|  |       deviceTemplates, // 暴露设备模板列表 | ||||||
|       formData, |       formData, | ||||||
|       rules, |       rules, | ||||||
|       title, |       title, | ||||||
|   | |||||||
							
								
								
									
										288
									
								
								src/components/DeviceTemplateForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										288
									
								
								src/components/DeviceTemplateForm.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,288 @@ | |||||||
|  | <template> | ||||||
|  |   <el-dialog | ||||||
|  |     :model-value="visible" | ||||||
|  |     :title="title" | ||||||
|  |     @close="handleClose" | ||||||
|  |     :close-on-click-modal="false" | ||||||
|  |     width="600px" | ||||||
|  |   > | ||||||
|  |     <el-form | ||||||
|  |       ref="formRef" | ||||||
|  |       :model="formData" | ||||||
|  |       :rules="rules" | ||||||
|  |       label-width="120px" | ||||||
|  |       @submit.prevent | ||||||
|  |     > | ||||||
|  |       <el-form-item label="名称" prop="name"> | ||||||
|  |         <el-input v-model="formData.name" /> | ||||||
|  |       </el-form-item> | ||||||
|  |  | ||||||
|  |       <el-form-item label="制造商" prop="manufacturer"> | ||||||
|  |         <el-input v-model="formData.manufacturer" /> | ||||||
|  |       </el-form-item> | ||||||
|  |  | ||||||
|  |       <el-form-item label="描述" prop="description"> | ||||||
|  |         <el-input v-model="formData.description" type="textarea" /> | ||||||
|  |       </el-form-item> | ||||||
|  |  | ||||||
|  |       <el-form-item label="类别" prop="category"> | ||||||
|  |         <el-select v-model="formData.category" placeholder="请选择类别" @change="handleCategoryChange"> | ||||||
|  |           <el-option label="执行器" value="执行器" /> | ||||||
|  |           <el-option label="传感器" value="传感器" /> | ||||||
|  |         </el-select> | ||||||
|  |       </el-form-item> | ||||||
|  |  | ||||||
|  |       <el-form-item label="指令 (JSON)" prop="commands"> | ||||||
|  |         <el-input | ||||||
|  |           v-model="formData.commands" | ||||||
|  |           type="textarea" | ||||||
|  |           :rows="5" | ||||||
|  |           placeholder="请输入JSON格式的指令参数" | ||||||
|  |         /> | ||||||
|  |       </el-form-item> | ||||||
|  |  | ||||||
|  |       <el-form-item | ||||||
|  |         v-if="formData.category === '传感器'" | ||||||
|  |         label="值描述 (JSON)" | ||||||
|  |         prop="values" | ||||||
|  |       > | ||||||
|  |         <el-input | ||||||
|  |           v-model="formData.values" | ||||||
|  |           type="textarea" | ||||||
|  |           :rows="5" | ||||||
|  |           placeholder="请输入JSON格式的值描述数组" | ||||||
|  |         /> | ||||||
|  |       </el-form-item> | ||||||
|  |     </el-form> | ||||||
|  |  | ||||||
|  |     <template #footer> | ||||||
|  |       <span class="dialog-footer"> | ||||||
|  |         <el-button @click="handleClose">取消</el-button> | ||||||
|  |         <el-button type="primary" @click="handleSubmit" :loading="loading">确定</el-button> | ||||||
|  |       </span> | ||||||
|  |     </template> | ||||||
|  |   </el-dialog> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { ref, reactive, computed, watch, nextTick } from 'vue'; | ||||||
|  | import { ElMessage } from 'element-plus'; | ||||||
|  | import deviceTemplateService from '../services/deviceTemplateService.js'; | ||||||
|  |  | ||||||
|  | // 默认的JSON模板 | ||||||
|  | const DEFAULT_ACTUATOR_COMMANDS = JSON.stringify({ | ||||||
|  |   modbus_start_address: 0, | ||||||
|  |   modbus_quantity: 1 | ||||||
|  | }, null, 2); | ||||||
|  |  | ||||||
|  | const DEFAULT_SENSOR_COMMANDS = JSON.stringify({ | ||||||
|  |   modbus_function_code: 3, | ||||||
|  |   modbus_start_address: 0, | ||||||
|  |   modbus_quantity: 1 | ||||||
|  | }, null, 2); | ||||||
|  |  | ||||||
|  | const DEFAULT_SENSOR_VALUES = JSON.stringify([ | ||||||
|  |   { | ||||||
|  |     type: "temperature", | ||||||
|  |     multiplier: 0.1, | ||||||
|  |     offset: 0 | ||||||
|  |   } | ||||||
|  | ], null, 2); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'DeviceTemplateForm', | ||||||
|  |   props: { | ||||||
|  |     visible: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|  |     templateData: { | ||||||
|  |       type: Object, | ||||||
|  |       default: () => ({}), | ||||||
|  |     }, | ||||||
|  |     isEdit: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   emits: ['update:visible', 'success', 'cancel'], | ||||||
|  |   setup(props, { emit }) { | ||||||
|  |     const formRef = ref(null); | ||||||
|  |     const loading = ref(false); | ||||||
|  |  | ||||||
|  |     const initialFormData = () => ({ | ||||||
|  |       id: '', | ||||||
|  |       name: '', | ||||||
|  |       manufacturer: '', | ||||||
|  |       description: '', | ||||||
|  |       category: '执行器', // 默认执行器 | ||||||
|  |       commands: DEFAULT_ACTUATOR_COMMANDS, // 预填充执行器指令 | ||||||
|  |       values: '', | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const formData = reactive(initialFormData()); | ||||||
|  |  | ||||||
|  |     // JSON 验证器 | ||||||
|  |     const validateJson = (rule, value, callback) => { | ||||||
|  |       if (!value) { | ||||||
|  |         return callback(new Error(rule.message)); | ||||||
|  |       } | ||||||
|  |       try { | ||||||
|  |         JSON.parse(value); | ||||||
|  |         callback(); | ||||||
|  |       } catch (e) { | ||||||
|  |         callback(new Error('请输入有效的 JSON 格式')); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const rules = computed(() => ({ | ||||||
|  |       name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }], | ||||||
|  |       category: [{ required: true, message: '请选择模板类别', trigger: 'change' }], | ||||||
|  |       commands: [ | ||||||
|  |         { required: true, message: '请输入指令参数', trigger: 'blur' }, | ||||||
|  |         { validator: validateJson, message: '指令参数必须是有效的 JSON 格式', trigger: 'blur' }, | ||||||
|  |       ], | ||||||
|  |       values: [ | ||||||
|  |         { required: formData.category === '传感器', message: '请输入值描述', trigger: 'blur' }, | ||||||
|  |         { validator: validateJson, message: '值描述必须是有效的 JSON 格式', trigger: 'blur' }, | ||||||
|  |       ], | ||||||
|  |     })); | ||||||
|  |  | ||||||
|  |     const title = computed(() => { | ||||||
|  |       return props.isEdit ? '编辑设备模板' : '新增设备模板'; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const handleCategoryChange = (newCategory) => { | ||||||
|  |       if (newCategory === '执行器') { | ||||||
|  |         formData.commands = DEFAULT_ACTUATOR_COMMANDS; | ||||||
|  |         formData.values = ''; // 执行器没有values | ||||||
|  |       } else if (newCategory === '传感器') { | ||||||
|  |         formData.commands = DEFAULT_SENSOR_COMMANDS; | ||||||
|  |         formData.values = DEFAULT_SENSOR_VALUES; // 传感器预填充values | ||||||
|  |       } | ||||||
|  |       nextTick(() => { | ||||||
|  |         if (formRef.value) { | ||||||
|  |           formRef.value.clearValidate(); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const handleClose = () => { | ||||||
|  |       emit('update:visible', false); | ||||||
|  |       emit('cancel'); | ||||||
|  |       // 重置表单 | ||||||
|  |       Object.assign(formData, initialFormData()); | ||||||
|  |       nextTick(() => { | ||||||
|  |         if (formRef.value) { | ||||||
|  |           formRef.value.resetFields(); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const handleSubmit = async () => { | ||||||
|  |       if (!formRef.value) return; | ||||||
|  |       await formRef.value.validate(async (valid) => { | ||||||
|  |         if (valid) { | ||||||
|  |           loading.value = true; | ||||||
|  |           try { | ||||||
|  |             const submitData = { | ||||||
|  |               name: formData.name, | ||||||
|  |               manufacturer: formData.manufacturer, | ||||||
|  |               description: formData.description, | ||||||
|  |               category: formData.category, | ||||||
|  |               commands: JSON.parse(formData.commands), | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             if (formData.category === '传感器' && formData.values) { | ||||||
|  |               submitData.values = JSON.parse(formData.values); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (props.isEdit) { | ||||||
|  |               await deviceTemplateService.updateDeviceTemplate(formData.id, submitData); | ||||||
|  |             } else { | ||||||
|  |               await deviceTemplateService.createDeviceTemplate(submitData); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             emit('success'); | ||||||
|  |             handleClose(); | ||||||
|  |           } catch (error) { | ||||||
|  |             console.error('保存设备模板失败:', error); | ||||||
|  |             ElMessage.error( | ||||||
|  |               props.isEdit ? '编辑设备模板失败: ' + (error.message || '未知错误') : '创建设备模板失败: ' + (error.message || '未知错误') | ||||||
|  |             ); | ||||||
|  |           } finally { | ||||||
|  |             loading.value = false; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     watch( | ||||||
|  |       () => props.templateData, | ||||||
|  |       (newVal) => { | ||||||
|  |         if (newVal && Object.keys(newVal).length > 0) { | ||||||
|  |           // 填充表单数据 | ||||||
|  |           formData.id = newVal.id; | ||||||
|  |           formData.name = newVal.name; | ||||||
|  |           formData.manufacturer = newVal.manufacturer; | ||||||
|  |           formData.description = newVal.description; | ||||||
|  |           if (newVal.category === 'sensor') { | ||||||
|  |             formData.category = '传感器'; | ||||||
|  |           } else if (newVal.category === 'actuator') { | ||||||
|  |             formData.category = '执行器'; | ||||||
|  |           } else { | ||||||
|  |             formData.category = newVal.category; | ||||||
|  |           } | ||||||
|  |           // 格式化JSON显示 | ||||||
|  |           formData.commands = newVal.commands ? JSON.stringify(newVal.commands, null, 2) : ''; | ||||||
|  |           formData.values = newVal.values ? JSON.stringify(newVal.values, null, 2) : ''; | ||||||
|  |         } else { | ||||||
|  |           // 重置表单数据到初始状态 (新增模式) | ||||||
|  |           Object.assign(formData, initialFormData()); | ||||||
|  |         } | ||||||
|  |         nextTick(() => { | ||||||
|  |           if (formRef.value) { | ||||||
|  |             formRef.value.clearValidate(); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       }, | ||||||
|  |       { immediate: true } | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     watch( | ||||||
|  |       () => props.visible, | ||||||
|  |       (newVal) => { | ||||||
|  |         if (newVal && !props.isEdit) { | ||||||
|  |           // 如果是新增模式且对话框打开,重置表单 | ||||||
|  |           Object.assign(formData, initialFormData()); | ||||||
|  |           nextTick(() => { | ||||||
|  |             if (formRef.value) { | ||||||
|  |               formRef.value.clearValidate(); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       { immediate: true } | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       formRef, | ||||||
|  |       loading, | ||||||
|  |       formData, | ||||||
|  |       rules, | ||||||
|  |       title, | ||||||
|  |       handleCategoryChange, | ||||||
|  |       handleClose, | ||||||
|  |       handleSubmit, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .dialog-footer { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: flex-end; | ||||||
|  |   gap: 10px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										268
									
								
								src/components/GenericMonitorList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								src/components/GenericMonitorList.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,268 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="generic-monitor-list"> | ||||||
|  |     <el-card shadow="never"> | ||||||
|  |       <el-form :inline="true" :model="filters" class="filter-form"> | ||||||
|  |         <el-form-item v-for="col in filterableColumns" :key="col.dataIndex" :label="col.title"> | ||||||
|  |           <template v-if="col.filterType === 'text'"> | ||||||
|  |             <el-input | ||||||
|  |                 v-model="filters[col.dataIndex]" | ||||||
|  |                 :placeholder="`搜索 ${col.title}`" | ||||||
|  |                 clearable | ||||||
|  |                 @change="handleFilterChange(col.dataIndex, filters[col.dataIndex])" | ||||||
|  |             ></el-input> | ||||||
|  |           </template> | ||||||
|  |           <template v-else-if="col.filterType === 'number'"> | ||||||
|  |             <el-input-number | ||||||
|  |                 v-model="filters[col.dataIndex]" | ||||||
|  |                 :placeholder="`搜索 ${col.title}`" | ||||||
|  |                 :controls="false" | ||||||
|  |                 clearable | ||||||
|  |                 @change="handleFilterChange(col.dataIndex, filters[col.dataIndex])" | ||||||
|  |             ></el-input-number> | ||||||
|  |           </template> | ||||||
|  |           <template v-else-if="col.filterType === 'dateRange'"> | ||||||
|  |             <el-date-picker | ||||||
|  |                 v-model="filters[col.dataIndex]" | ||||||
|  |                 type="datetimerange" | ||||||
|  |                 range-separator="至" | ||||||
|  |                 start-placeholder="开始日期" | ||||||
|  |                 end-placeholder="结束日期" | ||||||
|  |                 @change="handleFilterChange(col.dataIndex, filters[col.dataIndex])" | ||||||
|  |             ></el-date-picker> | ||||||
|  |           </template> | ||||||
|  |           <template v-else-if="col.filterType === 'select'"> | ||||||
|  |             <el-select | ||||||
|  |                 v-model="filters[col.dataIndex]" | ||||||
|  |                 :placeholder="`选择 ${col.title}`" | ||||||
|  |                 clearable | ||||||
|  |                 @change="handleFilterChange(col.dataIndex, filters[col.dataIndex])" | ||||||
|  |             > | ||||||
|  |               <el-option | ||||||
|  |                   v-for="option in col.filterOptions" | ||||||
|  |                   :key="option.value" | ||||||
|  |                   :label="option.text" | ||||||
|  |                   :value="option.value" | ||||||
|  |               ></el-option> | ||||||
|  |             </el-select> | ||||||
|  |           </template> | ||||||
|  |           <template v-else-if="col.filterType === 'boolean'"> | ||||||
|  |             <el-select | ||||||
|  |                 v-model="filters[col.dataIndex]" | ||||||
|  |                 :placeholder="`选择 ${col.title}`" | ||||||
|  |                 clearable | ||||||
|  |                 @change="handleFilterChange(col.dataIndex, filters[col.dataIndex])" | ||||||
|  |             > | ||||||
|  |               <el-option label="是" :value="true"></el-option> | ||||||
|  |               <el-option label="否" :value="false"></el-option> | ||||||
|  |             </el-select> | ||||||
|  |           </template> | ||||||
|  |         </el-form-item> | ||||||
|  |         <el-form-item> | ||||||
|  |           <el-button type="primary" @click="loadData">查询</el-button> | ||||||
|  |           <el-button @click="resetFilters">重置</el-button> | ||||||
|  |         </el-form-item> | ||||||
|  |       </el-form> | ||||||
|  |  | ||||||
|  |       <el-table | ||||||
|  |           :data="data" | ||||||
|  |           v-loading="loading" | ||||||
|  |           border | ||||||
|  |           stripe | ||||||
|  |           style="width: 100%" | ||||||
|  |           table-layout="auto" | ||||||
|  |           :fit="true" | ||||||
|  |           :scrollbar-always-on="true" | ||||||
|  |           @sort-change="handleSortChange" | ||||||
|  |       > | ||||||
|  |         <el-table-column | ||||||
|  |             v-for="col in tableColumns" | ||||||
|  |             :key="col.key" | ||||||
|  |             :prop="col.prop" | ||||||
|  |             :label="col.title" | ||||||
|  |             :sortable="col.sorter ? 'custom' : false" | ||||||
|  |             :formatter="col.formatter" | ||||||
|  |             :min-width="col.minWidth" | ||||||
|  |         > | ||||||
|  |           <template v-if="col.render" #default="{ row }"> | ||||||
|  |             <component :is="col.render(row)"/> | ||||||
|  |           </template> | ||||||
|  |         </el-table-column> | ||||||
|  |       </el-table> | ||||||
|  |  | ||||||
|  |       <el-pagination | ||||||
|  |           @size-change="handleSizeChange" | ||||||
|  |           @current-change="handleCurrentChange" | ||||||
|  |           :current-page="pagination.currentPage" | ||||||
|  |           :page-sizes="[10, 20, 50, 100]" | ||||||
|  |           :page-size="pagination.pageSize" | ||||||
|  |           layout="total, sizes, prev, pager, next, jumper" | ||||||
|  |           :total="pagination.total" | ||||||
|  |           background | ||||||
|  |           style="margin-top: 20px; text-align: right;" | ||||||
|  |       ></el-pagination> | ||||||
|  |     </el-card> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import {ref, reactive, onMounted, watch, computed} from 'vue'; | ||||||
|  | import {ElMessage} from 'element-plus'; | ||||||
|  |  | ||||||
|  | const props = defineProps({ | ||||||
|  |   fetchData: { | ||||||
|  |     type: Function, | ||||||
|  |     required: true, | ||||||
|  |   }, | ||||||
|  |   columnsConfig: { | ||||||
|  |     type: Array, | ||||||
|  |     required: true, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const data = ref([]); | ||||||
|  | const loading = ref(false); | ||||||
|  | const pagination = reactive({ | ||||||
|  |   currentPage: 1, | ||||||
|  |   pageSize: 10, | ||||||
|  |   total: 0, | ||||||
|  | }); | ||||||
|  | const filters = reactive({}); | ||||||
|  | const sortOrder = reactive({ | ||||||
|  |   prop: undefined, | ||||||
|  |   order: undefined, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const filterableColumns = computed(() => { | ||||||
|  |   return props.columnsConfig.filter(col => col.filterType); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const tableColumns = computed(() => { | ||||||
|  |   return props.columnsConfig.map(col => { | ||||||
|  |     const newCol = {...col}; | ||||||
|  |     newCol.prop = Array.isArray(col.dataIndex) ? col.dataIndex.join('.') : col.dataIndex; | ||||||
|  |  | ||||||
|  |     // 添加智能默认 formatter | ||||||
|  |     if (!newCol.formatter) { | ||||||
|  |       newCol.formatter = (row, column, cellValue) => { | ||||||
|  |         if (typeof cellValue === 'object' && cellValue !== null) { | ||||||
|  |           try { | ||||||
|  |             return JSON.stringify(cellValue, null, 2); // 格式化为可读的JSON字符串 | ||||||
|  |           } catch (e) { | ||||||
|  |             console.warn('Failed to stringify object for display:', cellValue, e); | ||||||
|  |             return '[Object]'; // 无法序列化时显示简短提示 | ||||||
|  |           } | ||||||
|  |         } else if (Array.isArray(cellValue)) { | ||||||
|  |             return cellValue.join(', '); // 数组也默认用逗号连接 | ||||||
|  |         } | ||||||
|  |         return cellValue; | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return newCol; | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const loadData = async () => { | ||||||
|  |   loading.value = true; | ||||||
|  |   try { | ||||||
|  |     const params = { | ||||||
|  |       page: pagination.currentPage, | ||||||
|  |       page_size: pagination.pageSize, // Changed from pageSize to page_size | ||||||
|  |       ...filters, | ||||||
|  |       orderBy: sortOrder.prop, | ||||||
|  |       order: sortOrder.order === 'ascending' ? 'asc' : (sortOrder.order === 'descending' ? 'desc' : undefined), | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Custom function to format Date objects to YYYY-MM-DDTHH:mm:ssZ | ||||||
|  |     const formatToRFC3339WithOffset = (date) => { | ||||||
|  |       if (!date) return ''; // Handle null or undefined dates | ||||||
|  |       const year = date.getUTCFullYear(); | ||||||
|  |       const month = (date.getUTCMonth() + 1).toString().padStart(2, '0'); | ||||||
|  |       const day = date.getUTCDate().toString().padStart(2, '0'); | ||||||
|  |       const hours = date.getUTCHours().toString().padStart(2, '0'); | ||||||
|  |       const minutes = date.getUTCMinutes().toString().padStart(2, '0'); | ||||||
|  |       const seconds = date.getUTCSeconds().toString().padStart(2, '0'); | ||||||
|  |       return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}Z`; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // 将日期范围筛选转换为 start_time 和 end_time,并确保是 RFC3339 UTC 格式 (不带毫秒) | ||||||
|  |     filterableColumns.value.forEach(col => { | ||||||
|  |       if (col.filterType === 'dateRange' && filters[col.dataIndex] && filters[col.dataIndex].length === 2) { | ||||||
|  |         // filters[col.dataIndex] will now contain Date objects directly from el-date-picker | ||||||
|  |         const startDateObj = filters[col.dataIndex][0]; | ||||||
|  |         const endDateObj = filters[col.dataIndex][1]; | ||||||
|  |  | ||||||
|  |         params[`start_time`] = formatToRFC3339WithOffset(startDateObj); | ||||||
|  |         params[`end_time`] = formatToRFC3339WithOffset(endDateObj); | ||||||
|  |         delete params[col.dataIndex]; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     console.log('Sending parameters to fetchData:', params); | ||||||
|  |  | ||||||
|  |     const result = await props.fetchData(params); | ||||||
|  |     data.value = result.list; | ||||||
|  |     pagination.total = result.total; | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('Failed to fetch data:', error); | ||||||
|  |     ElMessage.error('获取数据失败,请稍后再试。'); | ||||||
|  |   } finally { | ||||||
|  |     loading.value = false; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const handleSizeChange = (val) => { | ||||||
|  |   pagination.pageSize = val; | ||||||
|  |   pagination.currentPage = 1; | ||||||
|  |   loadData(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const handleCurrentChange = (val) => { | ||||||
|  |   pagination.currentPage = val; | ||||||
|  |   loadData(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const handleSortChange = ({prop, order}) => { | ||||||
|  |   sortOrder.prop = prop; | ||||||
|  |   sortOrder.order = order; | ||||||
|  |   loadData(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const handleFilterChange = (key, value) => { | ||||||
|  |   filters[key] = value; | ||||||
|  |   pagination.currentPage = 1; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const resetFilters = () => { | ||||||
|  |   for (const key in filters) { | ||||||
|  |     delete filters[key]; | ||||||
|  |   } | ||||||
|  |   sortOrder.prop = undefined; | ||||||
|  |   sortOrder.order = undefined; | ||||||
|  |   pagination.currentPage = 1; | ||||||
|  |   loadData(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | onMounted(() => { | ||||||
|  |   loadData(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .generic-monitor-list { | ||||||
|  |   padding: 20px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .filter-form { | ||||||
|  |   margin-bottom: 20px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .filter-form .el-form-item { | ||||||
|  |   min-width: 220px; /* 增加最小宽度 */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .el-card { | ||||||
|  |   border: none; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										157
									
								
								src/components/PenForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								src/components/PenForm.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,157 @@ | |||||||
|  | <template> | ||||||
|  |   <el-dialog | ||||||
|  |     :model-value="visible" | ||||||
|  |     :title="title" | ||||||
|  |     @close="handleClose" | ||||||
|  |     :close-on-click-modal="false" | ||||||
|  |     width="500px" | ||||||
|  |   > | ||||||
|  |     <el-form | ||||||
|  |       ref="formRef" | ||||||
|  |       :model="formData" | ||||||
|  |       :rules="rules" | ||||||
|  |       label-width="100px" | ||||||
|  |       @submit.prevent | ||||||
|  |     > | ||||||
|  |       <el-form-item label="猪栏编号" prop="pen_number"> | ||||||
|  |         <el-input v-model="formData.pen_number" placeholder="例如:A01-01" /> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="容量" prop="capacity"> | ||||||
|  |         <el-input-number v-model="formData.capacity" :min="1" controls-position="right" style="width: 100%" /> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item v-if="isEdit" label="状态" prop="status"> | ||||||
|  |         <el-select v-model="formData.status" placeholder="请选择状态" style="width: 100%"> | ||||||
|  |           <el-option v-for="item in penStatusOptions" :key="item" :label="item" :value="item" /> | ||||||
|  |         </el-select> | ||||||
|  |       </el-form-item> | ||||||
|  |     </el-form> | ||||||
|  |  | ||||||
|  |     <template #footer> | ||||||
|  |       <span class="dialog-footer"> | ||||||
|  |         <el-button @click="handleClose">取消</el-button> | ||||||
|  |         <el-button type="primary" @click="handleSubmit" :loading="loading">确定</el-button> | ||||||
|  |       </span> | ||||||
|  |     </template> | ||||||
|  |   </el-dialog> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { ref, reactive, watch, computed, nextTick } from 'vue'; | ||||||
|  | import { ElMessage } from 'element-plus'; | ||||||
|  | import { createPen, updatePen } from '@/api/pen.js'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'PenForm', | ||||||
|  |   props: { | ||||||
|  |     visible: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false | ||||||
|  |     }, | ||||||
|  |     penData: { | ||||||
|  |       type: Object, | ||||||
|  |       required: true | ||||||
|  |     }, | ||||||
|  |     isEdit: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   emits: ['update:visible', 'success', 'cancel'], | ||||||
|  |   setup(props, { emit }) { | ||||||
|  |     const formRef = ref(null); | ||||||
|  |     const loading = ref(false); | ||||||
|  |     const penStatusOptions = ["空闲", "使用中", "病猪栏", "康复栏", "清洗消毒", "维修中"]; | ||||||
|  |  | ||||||
|  |     const initialFormData = () => ({ | ||||||
|  |       id: null, | ||||||
|  |       pen_number: '', | ||||||
|  |       capacity: 10, | ||||||
|  |       status: '空闲', | ||||||
|  |       house_id: null | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const formData = reactive(initialFormData()); | ||||||
|  |  | ||||||
|  |     const rules = { | ||||||
|  |       pen_number: [ | ||||||
|  |         { required: true, message: '请输入猪栏编号', trigger: 'blur' } | ||||||
|  |       ], | ||||||
|  |       capacity: [ | ||||||
|  |         { required: true, message: '请输入容量', trigger: 'blur' }, | ||||||
|  |         { type: 'integer', min: 1, message: '容量必须是大于0的整数', trigger: 'blur' } | ||||||
|  |       ], | ||||||
|  |       status: [ | ||||||
|  |         { required: true, message: '请选择状态', trigger: 'change' } | ||||||
|  |       ] | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const title = computed(() => (props.isEdit ? '编辑猪栏' : '添加猪栏')); | ||||||
|  |  | ||||||
|  |     const handleClose = () => { | ||||||
|  |       emit('update:visible', false); | ||||||
|  |       emit('cancel'); | ||||||
|  |       nextTick(() => { | ||||||
|  |         formRef.value?.resetFields(); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const handleSubmit = async () => { | ||||||
|  |       if (!formRef.value) return; | ||||||
|  |       await formRef.value.validate(async (valid) => { | ||||||
|  |         if (valid) { | ||||||
|  |           loading.value = true; | ||||||
|  |           try { | ||||||
|  |             let response; | ||||||
|  |             if (props.isEdit) { | ||||||
|  |               const { id, ...updateData } = formData; | ||||||
|  |               response = await updatePen(id, updateData); | ||||||
|  |             } else { | ||||||
|  |               const { id, status, ...createData } = formData; | ||||||
|  |               response = await createPen(createData); | ||||||
|  |             } | ||||||
|  |             emit('success', response.data); | ||||||
|  |             handleClose(); | ||||||
|  |           } catch (error) { | ||||||
|  |             ElMessage.error((props.isEdit ? '更新' : '创建') + '猪栏失败: ' + (error.message || '未知错误')); | ||||||
|  |           } finally { | ||||||
|  |             loading.value = false; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     watch(() => props.visible, (newVal) => { | ||||||
|  |       if (newVal) { | ||||||
|  |         Object.assign(formData, initialFormData()); | ||||||
|  |         if (props.isEdit && props.penData) { | ||||||
|  |           Object.assign(formData, props.penData); | ||||||
|  |         } else if (props.penData) { | ||||||
|  |           formData.house_id = props.penData.house_id; | ||||||
|  |         } | ||||||
|  |         nextTick(() => { | ||||||
|  |           formRef.value?.clearValidate(); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       formRef, | ||||||
|  |       loading, | ||||||
|  |       formData, | ||||||
|  |       rules, | ||||||
|  |       title, | ||||||
|  |       penStatusOptions, | ||||||
|  |       handleClose, | ||||||
|  |       handleSubmit | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .dialog-footer { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: flex-end; | ||||||
|  |   gap: 10px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										216
									
								
								src/components/PigBatchForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								src/components/PigBatchForm.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,216 @@ | |||||||
|  | <template> | ||||||
|  |   <el-dialog | ||||||
|  |       :model-value="visible" | ||||||
|  |       :title="title" | ||||||
|  |       @close="handleClose" | ||||||
|  |       :close-on-click-modal="false" | ||||||
|  |       width="600px" | ||||||
|  |   > | ||||||
|  |     <el-form | ||||||
|  |         ref="formRef" | ||||||
|  |         :model="formData" | ||||||
|  |         :rules="rules" | ||||||
|  |         label-width="120px" | ||||||
|  |         @submit.prevent | ||||||
|  |     > | ||||||
|  |       <el-form-item label="批次编号" prop="batch_number"> | ||||||
|  |         <el-input v-model="formData.batch_number" placeholder="请输入批次编号"/> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="初始数量" prop="initial_count"> | ||||||
|  |         <el-input-number v-model="formData.initial_count" :min="1" controls-position="right" style="width: 100%"/> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="来源类型" prop="origin_type"> | ||||||
|  |         <el-select v-model="formData.origin_type" placeholder="请选择来源类型" style="width: 100%"> | ||||||
|  |           <el-option v-for="item in originTypeOptions" :key="item" :label="item" :value="item"/> | ||||||
|  |         </el-select> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="开始日期" prop="start_date"> | ||||||
|  |         <el-date-picker | ||||||
|  |             v-model="formData.start_date" | ||||||
|  |             type="datetime" | ||||||
|  |             placeholder="选择开始日期和时间" | ||||||
|  |             value-format="YYYY-MM-DD HH:mm:ss" | ||||||
|  |             style="width: 100%" | ||||||
|  |         /> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="批次状态" prop="status"> | ||||||
|  |         <el-select v-model="formData.status" placeholder="请选择批次状态" style="width: 100%"> | ||||||
|  |           <el-option v-for="item in batchStatusOptions" :key="item" :label="item" :value="item"/> | ||||||
|  |         </el-select> | ||||||
|  |       </el-form-item> | ||||||
|  |     </el-form> | ||||||
|  |  | ||||||
|  |     <template #footer> | ||||||
|  |       <span class="dialog-footer"> | ||||||
|  |         <el-button @click="handleClose">取消</el-button> | ||||||
|  |         <el-button type="primary" @click="handleSubmit" :loading="loading">确定</el-button> | ||||||
|  |       </span> | ||||||
|  |     </template> | ||||||
|  |   </el-dialog> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import {ref, reactive, watch, computed, nextTick} from 'vue'; | ||||||
|  | import {ElMessage} from 'element-plus'; | ||||||
|  | import {createPigBatch, updatePigBatch} from '@/api/pigBatch.js'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'PigBatchForm', | ||||||
|  |   props: { | ||||||
|  |     visible: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false | ||||||
|  |     }, | ||||||
|  |     batchData: { | ||||||
|  |       type: Object, | ||||||
|  |       default: () => ({}) | ||||||
|  |     }, | ||||||
|  |     isEdit: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   emits: ['update:visible', 'success', 'cancel'], | ||||||
|  |   setup(props, {emit}) { | ||||||
|  |     const formRef = ref(null); | ||||||
|  |     const loading = ref(false); | ||||||
|  |  | ||||||
|  |     const originTypeOptions = ["自繁", "外购"]; | ||||||
|  |     const batchStatusOptions = ["保育", "生长", "育肥", "待售", "已出售", "已归档"]; | ||||||
|  |  | ||||||
|  |     const initialFormData = () => ({ | ||||||
|  |       id: null, | ||||||
|  |       batch_number: '', | ||||||
|  |       initial_count: 1, | ||||||
|  |       origin_type: '自繁', | ||||||
|  |       start_date: '', | ||||||
|  |       status: '保育', | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const formData = reactive(initialFormData()); | ||||||
|  |  | ||||||
|  |     const rules = { | ||||||
|  |       batch_number: [ | ||||||
|  |         {required: true, message: '请输入批次编号', trigger: 'blur'} | ||||||
|  |       ], | ||||||
|  |       initial_count: [ | ||||||
|  |         {required: true, message: '请输入初始数量', trigger: 'blur'}, | ||||||
|  |         {type: 'integer', min: 1, message: '初始数量必须是大于0的整数', trigger: 'blur'} | ||||||
|  |       ], | ||||||
|  |       origin_type: [ | ||||||
|  |         {required: true, message: '请选择来源类型', trigger: 'change'} | ||||||
|  |       ], | ||||||
|  |       start_date: [ | ||||||
|  |         {required: true, message: '请选择开始日期', trigger: 'change'} | ||||||
|  |       ], | ||||||
|  |       status: [ | ||||||
|  |         {required: true, message: '请选择批次状态', trigger: 'change'} | ||||||
|  |       ] | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const title = computed(() => (props.isEdit ? '编辑猪群' : '添加猪群')); | ||||||
|  |  | ||||||
|  |     // ✅ 修复:东八区 RFC3339 转换函数 | ||||||
|  |     const toRFC3339 = (dateStr) => { | ||||||
|  |       if (!dateStr) return null; | ||||||
|  |       // "2025-10-23 14:30:00" → Date 对象(东八区) | ||||||
|  |       const date = new Date(dateStr.replace(/ /, 'T') + '+08:00'); | ||||||
|  |       if (isNaN(date.getTime())) return null; | ||||||
|  |       return date.toISOString().slice(0, -1) + '+08:00'; // "2025-10-23T14:30:00+08:00" | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // ✅ 修复:RFC3339 转显示格式 | ||||||
|  |     const fromRFC3339 = (rfc3339Str) => { | ||||||
|  |       if (!rfc3339Str) return ''; | ||||||
|  |       const date = new Date(rfc3339Str); | ||||||
|  |       if (isNaN(date.getTime())) return ''; | ||||||
|  |       const year = date.getFullYear(); | ||||||
|  |       const month = String(date.getMonth() + 1).padStart(2, '0'); | ||||||
|  |       const day = String(date.getDate()).padStart(2, '0'); | ||||||
|  |       const hours = String(date.getHours()).padStart(2, '0'); | ||||||
|  |       const minutes = String(date.getMinutes()).padStart(2, '0'); | ||||||
|  |       const seconds = String(date.getSeconds()).padStart(2, '0'); | ||||||
|  |       return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const handleClose = () => { | ||||||
|  |       emit('update:visible', false); | ||||||
|  |       emit('cancel'); | ||||||
|  |       nextTick(() => { | ||||||
|  |         formRef.value?.resetFields(); | ||||||
|  |         Object.assign(formData, initialFormData()); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const handleSubmit = async () => { | ||||||
|  |       if (!formRef.value) return; | ||||||
|  |       await formRef.value.validate(async (valid) => { | ||||||
|  |         if (valid) { | ||||||
|  |           loading.value = true; | ||||||
|  |           try { | ||||||
|  |             let response; | ||||||
|  |             const submitData = {...formData}; | ||||||
|  |             delete submitData.id; | ||||||
|  |  | ||||||
|  |             // ✅ 修复:正确转东八区 RFC3339 | ||||||
|  |             submitData.start_date = toRFC3339(formData.start_date); | ||||||
|  |             console.log('提交的日期:', submitData.start_date); // 调试用 | ||||||
|  |  | ||||||
|  |             if (props.isEdit) { | ||||||
|  |               response = await updatePigBatch(props.batchData.id, submitData); | ||||||
|  |             } else { | ||||||
|  |               response = await createPigBatch(submitData); | ||||||
|  |             } | ||||||
|  |             ElMessage.success((props.isEdit ? '更新' : '创建') + '猪群成功'); | ||||||
|  |             emit('success', response.data); | ||||||
|  |             handleClose(); | ||||||
|  |           } catch (error) { | ||||||
|  |             console.error('提交错误:', error); | ||||||
|  |             ElMessage.error((props.isEdit ? '更新' : '创建') + '猪群失败: ' + (error.message || '未知错误')); | ||||||
|  |           } finally { | ||||||
|  |             loading.value = false; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     watch(() => props.visible, (newVal) => { | ||||||
|  |       if (newVal) { | ||||||
|  |         Object.assign(formData, initialFormData()); | ||||||
|  |         if (props.isEdit && props.batchData) { | ||||||
|  |           // ✅ 修复:正确转换编辑时的日期 | ||||||
|  |           formData.id = props.batchData.id; | ||||||
|  |           formData.batch_number = props.batchData.batch_number || ''; | ||||||
|  |           formData.initial_count = props.batchData.initial_count || 1; | ||||||
|  |           formData.origin_type = props.batchData.origin_type || '自繁'; | ||||||
|  |           formData.status = props.batchData.status || '保育'; | ||||||
|  |           formData.start_date = fromRFC3339(props.batchData.start_date); | ||||||
|  |         } | ||||||
|  |         nextTick(() => { | ||||||
|  |           formRef.value?.clearValidate(); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       formRef, | ||||||
|  |       loading, | ||||||
|  |       formData, | ||||||
|  |       rules, | ||||||
|  |       title, | ||||||
|  |       originTypeOptions, | ||||||
|  |       batchStatusOptions, | ||||||
|  |       handleClose, | ||||||
|  |       handleSubmit | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .dialog-footer { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: flex-end; | ||||||
|  |   gap: 10px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										300
									
								
								src/components/PigBatchList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								src/components/PigBatchList.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,300 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="pig-batch-list"> | ||||||
|  |     <div v-for="batch in pigBatches" :key="batch.id" class="pig-batch-item"> | ||||||
|  |       <div class="batch-header" @click="toggleExpand(batch)"> | ||||||
|  |         <div class="batch-info"> | ||||||
|  |           <div class="batch-info-line"> | ||||||
|  |             <span>批次编号: {{ batch.batch_number }}</span> | ||||||
|  |             <span>状态: {{ batch.status }}</span> | ||||||
|  |             <span>初始数量: {{ batch.initial_count }}</span> | ||||||
|  |             <span v-if="batch.currentTotalQuantity !== undefined && batch.currentTotalQuantity !== null">当前总数: {{ | ||||||
|  |                 batch.currentTotalQuantity | ||||||
|  |               }}</span> | ||||||
|  |             <span v-if="batch.origin_type">批次来源: {{ batch.origin_type }}</span> | ||||||
|  |           </div> | ||||||
|  |           <div class="batch-info-line"> | ||||||
|  |             <span v-if="batch.start_date">批次开始日期: {{ formatRFC3339(batch.start_date) }}</span> | ||||||
|  |             <span v-if="batch.end_date"> | ||||||
|  |               批次结束日期: | ||||||
|  |               <template v-if="formatRFC3339(batch.end_date) === '0001-01-01 08:00:00'"> | ||||||
|  |                 正在饲养中 | ||||||
|  |               </template> | ||||||
|  |               <template v-else> | ||||||
|  |                 {{ formatRFC3339(batch.end_date) }} | ||||||
|  |               </template> | ||||||
|  |             </span> | ||||||
|  |             <span v-if="batch.unassigned_pig_count !== undefined && batch.unassigned_pig_count > 0" class="red-text"> | ||||||
|  |               未分配数量: {{ batch.unassigned_pig_count }} | ||||||
|  |             </span> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="batch-actions"> | ||||||
|  |           <el-dropdown trigger="click" class="batch-dropdown"> | ||||||
|  |             <el-button type="primary" size="small"> | ||||||
|  |               管理猪群<el-icon class="el-icon--right"><arrow-down /></el-icon> | ||||||
|  |             </el-button> | ||||||
|  |             <template #dropdown> | ||||||
|  |               <el-dropdown-menu> | ||||||
|  |                 <el-dropdown-item @click="showAddPenDialog(batch)" :disabled="!batch.is_active">增加猪栏</el-dropdown-item> | ||||||
|  |                 <el-dropdown-item @click="emitEditBatch(batch)">编辑</el-dropdown-item> | ||||||
|  |                 <el-dropdown-item @click="emitDeleteBatch(batch)" divided>删除</el-dropdown-item> | ||||||
|  |               </el-dropdown-menu> | ||||||
|  |             </template> | ||||||
|  |           </el-dropdown> | ||||||
|  |           <el-dropdown trigger="click" class="batch-dropdown"> | ||||||
|  |             <el-button type="success" size="small"> | ||||||
|  |               调栏<el-icon class="el-icon--right"><arrow-down /></el-icon> | ||||||
|  |             </el-button> | ||||||
|  |             <template #dropdown> | ||||||
|  |               <el-dropdown-menu> | ||||||
|  |                 <el-dropdown-item  | ||||||
|  |                   @click="emitTransferPigs(batch)" | ||||||
|  |                   :disabled="!batch.is_active || !batch.pens || batch.pens.length < 2" | ||||||
|  |                 >群内调栏</el-dropdown-item> | ||||||
|  |                 <el-dropdown-item  | ||||||
|  |                   @click="emitTransferPigsAcrossBatches(batch)" | ||||||
|  |                   :disabled="!batch.is_active || !batch.pens || batch.pens.length === 0" | ||||||
|  |                 >跨群调栏</el-dropdown-item> | ||||||
|  |               </el-dropdown-menu> | ||||||
|  |             </template> | ||||||
|  |           </el-dropdown> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <div v-if="batch.isExpanded" class="batch-content"> | ||||||
|  |         <div v-if="batch.pens && batch.pens.length > 0" class="pig-pen-list"> | ||||||
|  |           <PigBatchPenCard | ||||||
|  |               v-for="pen in batch.pens" | ||||||
|  |               :key="pen.id" | ||||||
|  |               :pen="pen" | ||||||
|  |               :isBatchActive="batch.is_active" | ||||||
|  |               :batchUnassignedPigCount="batch.unassigned_pig_count" | ||||||
|  |               @allocate-pigs="showAllocatePigsDialog($event, batch)" | ||||||
|  |               @remove="emitRemovePen" | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |         <div v-else class="no-pens-message"> | ||||||
|  |           <p>该猪群下没有猪栏信息。</p> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <!-- 增加猪栏对话框 --> | ||||||
|  |     <el-dialog title="选择猪栏" v-model="addPenDialogVisible" width="30%"> | ||||||
|  |       <el-select v-model="selectedPenId" placeholder="请选择猪栏" style="width: 100%;"> | ||||||
|  |         <el-option | ||||||
|  |             v-for="pen in availablePens" | ||||||
|  |             :key="pen.id" | ||||||
|  |             :label="pen.label" | ||||||
|  |             :value="pen.id"> | ||||||
|  |         </el-option> | ||||||
|  |       </el-select> | ||||||
|  |       <template #footer> | ||||||
|  |         <span class="dialog-footer"> | ||||||
|  |           <el-button @click="addPenDialogVisible = false">取 消</el-button> | ||||||
|  |           <el-button type="primary" @click="assignPen">确 定</el-button> | ||||||
|  |         </span> | ||||||
|  |       </template> | ||||||
|  |     </el-dialog> | ||||||
|  |  | ||||||
|  |     <!-- 分配猪只对话框 --> | ||||||
|  |     <AllocatePigsDialog | ||||||
|  |         v-model:visible="allocatePigsDialogVisible" | ||||||
|  |         :unassigned-pig-count="currentBatch ? currentBatch.unassigned_pig_count : 0" | ||||||
|  |         :pen-id="selectedPenForAllocation ? selectedPenForAllocation.id : 0" | ||||||
|  |         @confirm="handleAllocatePigs" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import PigBatchPenCard from './PigBatchPenCard.vue'; | ||||||
|  | import AllocatePigsDialog from './AllocatePigsDialog.vue'; | ||||||
|  | import {getAllPens, getAllPigHouses, movePigsIntoPen} from '../api/pigBatch'; | ||||||
|  | import {formatRFC3339} from '../utils/format'; // 导入格式化函数 | ||||||
|  | import { ArrowDown } from '@element-plus/icons-vue'; // 导入 ArrowDown 图标 | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'PigBatchList', | ||||||
|  |   components: { | ||||||
|  |     PigBatchPenCard, | ||||||
|  |     AllocatePigsDialog, | ||||||
|  |     ArrowDown // 注册 ArrowDown 图标 | ||||||
|  |   }, | ||||||
|  |   props: { | ||||||
|  |     pigBatches: { | ||||||
|  |       type: Array, | ||||||
|  |       required: true | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   emits: ['edit-batch', 'delete-batch', 'add-pen', 'remove-pen', 'assign-pen-to-batch', 'reload-data', 'transfer-pigs', 'transfer-pigs-across-batches'], | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       addPenDialogVisible: false, | ||||||
|  |       availablePens: [], | ||||||
|  |       selectedPenId: null, | ||||||
|  |       currentBatch: null, // To store the batch for which we are adding a pen | ||||||
|  |       allocatePigsDialogVisible: false, | ||||||
|  |       selectedPenForAllocation: null | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     formatRFC3339, | ||||||
|  |     toggleExpand(batch) { | ||||||
|  |       batch.isExpanded = !batch.isExpanded; | ||||||
|  |     }, | ||||||
|  |     async fetchAvailablePens() { | ||||||
|  |       try { | ||||||
|  |         const [pensResponse, housesResponse] = await Promise.all([ | ||||||
|  |           getAllPens(), | ||||||
|  |           getAllPigHouses() | ||||||
|  |         ]); | ||||||
|  |  | ||||||
|  |         const pens = pensResponse.data; | ||||||
|  |         const houses = housesResponse.data; | ||||||
|  |  | ||||||
|  |         // Create a map for quick lookup of house names by ID | ||||||
|  |         const houseMap = new Map(houses.map(house => [house.id, house.name])); | ||||||
|  |  | ||||||
|  |         // Filter for pens that are not assigned to any batch | ||||||
|  |         const unassignedPens = pens.filter(pen => !pen.pig_batch_id); | ||||||
|  |  | ||||||
|  |         this.availablePens = unassignedPens.map(pen => ({ | ||||||
|  |           id: pen.id, | ||||||
|  |           label: `${pen.pen_number} (${houseMap.get(pen.house_id) || '未知猪舍'})` | ||||||
|  |         })); | ||||||
|  |       } catch (error) { | ||||||
|  |         console.error("Error fetching pens or houses:", error); | ||||||
|  |         this.$message.error("获取猪栏或猪舍信息失败"); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     showAddPenDialog(batch) { | ||||||
|  |       this.currentBatch = batch; | ||||||
|  |       this.selectedPenId = null; // Reset selection | ||||||
|  |       this.fetchAvailablePens(); | ||||||
|  |       this.addPenDialogVisible = true; | ||||||
|  |     }, | ||||||
|  |     assignPen() { | ||||||
|  |       if (this.selectedPenId && this.currentBatch) { | ||||||
|  |         this.$emit('assign-pen-to-batch', { | ||||||
|  |           batchId: this.currentBatch.id, | ||||||
|  |           penId: this.selectedPenId | ||||||
|  |         }); | ||||||
|  |         this.addPenDialogVisible = false; | ||||||
|  |       } else { | ||||||
|  |         this.$message.warning("请选择一个猪栏"); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     showAllocatePigsDialog(pen, batch) { | ||||||
|  |       this.currentBatch = batch; | ||||||
|  |       this.selectedPenForAllocation = pen; | ||||||
|  |       this.allocatePigsDialogVisible = true; | ||||||
|  |     }, | ||||||
|  |     async handleAllocatePigs({penId, quantity}) { | ||||||
|  |       try { | ||||||
|  |         await movePigsIntoPen(this.currentBatch.id, {toPenID: penId, quantity}); | ||||||
|  |         this.$message.success('猪只分配成功'); | ||||||
|  |         this.allocatePigsDialogVisible = false; | ||||||
|  |         this.$emit('reload-data'); // 通知父组件重新加载数据 | ||||||
|  |       } catch (error) { | ||||||
|  |         console.error('Error allocating pigs:', error); | ||||||
|  |         this.$message.error('分配猪只失败'); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     // 猪群操作 | ||||||
|  |     emitEditBatch(batch) { | ||||||
|  |       this.$emit('edit-batch', batch); | ||||||
|  |     }, | ||||||
|  |     emitDeleteBatch(batch) { | ||||||
|  |       this.$emit('delete-batch', batch); | ||||||
|  |     }, | ||||||
|  |     emitTransferPigs(batch) { | ||||||
|  |       this.$emit('transfer-pigs', batch); | ||||||
|  |     }, | ||||||
|  |     emitTransferPigsAcrossBatches(batch) { | ||||||
|  |       this.$emit('transfer-pigs-across-batches', batch); | ||||||
|  |     }, | ||||||
|  |     // 猪栏操作 | ||||||
|  |     emitRemovePen(pen) { | ||||||
|  |       this.$emit('remove-pen', pen); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .pig-batch-list { | ||||||
|  |   width: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .pig-batch-item { | ||||||
|  |   border: 1px solid #eee; | ||||||
|  |   border-radius: 4px; | ||||||
|  |   margin-bottom: 16px; | ||||||
|  |   overflow: hidden; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .batch-header { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: space-between; | ||||||
|  |   align-items: center; | ||||||
|  |   padding: 16px; | ||||||
|  |   cursor: pointer; | ||||||
|  |   background-color: #f9f9f9; | ||||||
|  |   transition: background-color 0.3s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .batch-header:hover { | ||||||
|  |   background-color: #f0f0f0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .batch-info { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; /* 垂直堆叠子元素 */ | ||||||
|  |   gap: 8px; /* 行间距 */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .batch-info-line { | ||||||
|  |   display: flex; | ||||||
|  |   flex-wrap: wrap; | ||||||
|  |   gap: 0 20px; /* 列间距 */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .batch-info-line:first-child span:first-child { | ||||||
|  |   font-weight: bold; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .batch-info span { | ||||||
|  |   font-size: 14px; | ||||||
|  |   color: #606266; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .batch-actions { | ||||||
|  |   display: flex; | ||||||
|  |   gap: 10px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .batch-dropdown { | ||||||
|  |   margin-left: 10px; /* 为下拉菜单添加左边距 */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .batch-content { | ||||||
|  |   padding: 16px; | ||||||
|  |   border-top: 1px solid #eee; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .pig-pen-list { | ||||||
|  |   display: flex; | ||||||
|  |   flex-wrap: wrap; | ||||||
|  |   gap: 16px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .no-pens-message { | ||||||
|  |   color: #909399; | ||||||
|  |   text-align: center; | ||||||
|  |   padding: 20px 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .batch-info-line .red-text { | ||||||
|  |   color: red !important; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										135
									
								
								src/components/PigBatchPenCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/components/PigBatchPenCard.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="pig-pen-info-card"> | ||||||
|  |     <div class="info-section"> | ||||||
|  |       <div class="title"> | ||||||
|  |         猪栏: {{ pen.pen_number }} <el-tag size="small" :type="statusType">{{ pen.status || '未知' }}</el-tag> | ||||||
|  |       </div> | ||||||
|  |       <div class="info-item">猪舍: {{ pen.house_name || '未知' }}</div> | ||||||
|  |       <div class="info-item border-left">容量: {{ pen.capacity }}</div> | ||||||
|  |       <div class="info-item">批次: {{ pen.batch_number || '未分配' }}</div> | ||||||
|  |       <div class="info-item border-left">存栏: <span :class="{'over-capacity': pen.current_pig_count > pen.capacity}">{{ pen.current_pig_count || 0 }}</span></div> | ||||||
|  |     </div> | ||||||
|  |     <div class="actions-section"> | ||||||
|  |       <el-button size="small" @click="emitAllocatePigs" :disabled="!isBatchActive || batchUnassignedPigCount <= 0">分配猪只</el-button> | ||||||
|  |       <el-button size="small" type="danger" @click="emitRemove" :disabled="!isBatchActive || pen.current_pig_count > 0">移除</el-button> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { computed } from 'vue'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'PigBatchPenCard', | ||||||
|  |   props: { | ||||||
|  |     pen: { | ||||||
|  |       type: Object, | ||||||
|  |       required: true | ||||||
|  |     }, | ||||||
|  |     isBatchActive: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: true // 默认活跃,以防万一没有传递 | ||||||
|  |     }, | ||||||
|  |     batchUnassignedPigCount: { | ||||||
|  |       type: Number, | ||||||
|  |       default: 0 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   emits: ['remove', 'allocate-pigs'], | ||||||
|  |   setup(props, { emit }) { | ||||||
|  |     const statusType = computed(() => { | ||||||
|  |       switch (props.pen.status) { | ||||||
|  |         case '使用中': | ||||||
|  |           return 'success'; | ||||||
|  |         case '病猪栏': | ||||||
|  |         case '维修中': | ||||||
|  |           return 'danger'; | ||||||
|  |         case '清洗消毒': | ||||||
|  |           return 'warning'; | ||||||
|  |         case '空闲': | ||||||
|  |         default: | ||||||
|  |           return 'info'; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const emitAllocatePigs = () => { | ||||||
|  |       emit('allocate-pigs', props.pen); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const emitRemove = () => { | ||||||
|  |       emit('remove', props.pen); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       statusType, | ||||||
|  |       emitAllocatePigs, | ||||||
|  |       emitRemove | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .pig-pen-info-card { | ||||||
|  |   border: 2px solid #ccc; | ||||||
|  |   border-radius: 8px; | ||||||
|  |   padding: 16px; | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |   justify-content: space-between; | ||||||
|  |   width: 220px; /* 适当加宽以容纳更多信息 */ | ||||||
|  |   height: auto; /* 让高度自适应内容 */ | ||||||
|  |   min-height: 240px; /* 设置最小高度 */ | ||||||
|  |   background-color: #fff; | ||||||
|  |   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | ||||||
|  |   transition: box-shadow 0.3s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .pig-pen-info-card:hover { | ||||||
|  |   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .info-section { | ||||||
|  |   display: grid; /* 使用grid布局创建两列 */ | ||||||
|  |   grid-template-columns: 1fr 1fr; /* 两列等宽 */ | ||||||
|  |   gap: 8px 10px; /* 行间距 8px, 列间距 10px */ | ||||||
|  |   margin-bottom: 10px; | ||||||
|  |   flex-grow: 1; /* 允许信息部分填充可用空间 */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .info-section .title { | ||||||
|  |   font-weight: bold; | ||||||
|  |   font-size: 1.1em; | ||||||
|  |   color: #303133; | ||||||
|  |   grid-column: 1 / -1; /* 标题横跨两列 */ | ||||||
|  |   margin-bottom: 5px; /* 标题下方增加一点间距 */ | ||||||
|  |   display: flex; /* Add flexbox for alignment */ | ||||||
|  |   align-items: center; /* Vertically align items */ | ||||||
|  |   gap: 8px; /* Space between title text and tag */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .info-section .info-item { | ||||||
|  |   font-size: 0.9em; | ||||||
|  |   color: #606266; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .info-item.border-left { | ||||||
|  |   border-left: 1px solid #eee; /* 添加左边框作为分隔线 */ | ||||||
|  |   padding-left: 10px; /* 增加左内边距,使内容不紧贴边框 */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .actions-section { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |   gap: 8px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .actions-section .el-button { | ||||||
|  |   width: 100%; | ||||||
|  |   margin: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .over-capacity { | ||||||
|  |   color: red; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										145
									
								
								src/components/PigHouseForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/components/PigHouseForm.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | |||||||
|  | <template> | ||||||
|  |   <el-dialog | ||||||
|  |     :model-value="visible" | ||||||
|  |     :title="title" | ||||||
|  |     @close="handleClose" | ||||||
|  |     :close-on-click-modal="false" | ||||||
|  |     width="500px" | ||||||
|  |   > | ||||||
|  |     <el-form | ||||||
|  |       ref="formRef" | ||||||
|  |       :model="formData" | ||||||
|  |       :rules="rules" | ||||||
|  |       label-width="100px" | ||||||
|  |       @submit.prevent | ||||||
|  |     > | ||||||
|  |       <el-form-item label="猪舍名称" prop="name"> | ||||||
|  |         <el-input v-model="formData.name" placeholder="请输入猪舍名称" /> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="描述" prop="description"> | ||||||
|  |         <el-input v-model="formData.description" type="textarea" placeholder="请输入描述信息" /> | ||||||
|  |       </el-form-item> | ||||||
|  |     </el-form> | ||||||
|  |  | ||||||
|  |     <template #footer> | ||||||
|  |       <span class="dialog-footer"> | ||||||
|  |         <el-button @click="handleClose">取消</el-button> | ||||||
|  |         <el-button type="primary" @click="handleSubmit" :loading="loading">确定</el-button> | ||||||
|  |       </span> | ||||||
|  |     </template> | ||||||
|  |   </el-dialog> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { ref, reactive, watch, computed, nextTick } from 'vue'; | ||||||
|  | import { ElMessage } from 'element-plus'; | ||||||
|  | import { createPigHouse, updatePigHouse } from '@/api/pigHouse.js'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'PigHouseForm', | ||||||
|  |   props: { | ||||||
|  |     visible: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false | ||||||
|  |     }, | ||||||
|  |     houseData: { | ||||||
|  |       type: Object, | ||||||
|  |       default: () => ({}) | ||||||
|  |     }, | ||||||
|  |     isEdit: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   emits: ['update:visible', 'success', 'cancel'], | ||||||
|  |   setup(props, { emit }) { | ||||||
|  |     const formRef = ref(null); | ||||||
|  |     const loading = ref(false); | ||||||
|  |  | ||||||
|  |     const initialFormData = () => ({ | ||||||
|  |       id: null, | ||||||
|  |       name: '', | ||||||
|  |       description: '' | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const formData = reactive(initialFormData()); | ||||||
|  |  | ||||||
|  |     const rules = { | ||||||
|  |       name: [ | ||||||
|  |         { required: true, message: '请输入猪舍名称', trigger: 'blur' } | ||||||
|  |       ] | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const title = computed(() => (props.isEdit ? '编辑猪舍' : '添加猪舍')); | ||||||
|  |  | ||||||
|  |     const handleClose = () => { | ||||||
|  |       emit('update:visible', false); | ||||||
|  |       emit('cancel'); | ||||||
|  |       Object.assign(formData, initialFormData()); | ||||||
|  |       nextTick(() => { | ||||||
|  |         formRef.value?.resetFields(); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const handleSubmit = async () => { | ||||||
|  |       if (!formRef.value) return; | ||||||
|  |       await formRef.value.validate(async (valid) => { | ||||||
|  |         if (valid) { | ||||||
|  |           loading.value = true; | ||||||
|  |           try { | ||||||
|  |             const submitData = { name: formData.name, description: formData.description }; | ||||||
|  |             let response; | ||||||
|  |             if (props.isEdit) { | ||||||
|  |               response = await updatePigHouse(formData.id, submitData); | ||||||
|  |             } else { | ||||||
|  |               response = await createPigHouse(submitData); | ||||||
|  |             } | ||||||
|  |             emit('success', response.data); | ||||||
|  |             handleClose(); | ||||||
|  |           } catch (error) { | ||||||
|  |             ElMessage.error((props.isEdit ? '更新' : '创建') + '猪舍失败: ' + (error.message || '未知错误')); | ||||||
|  |           } finally { | ||||||
|  |             loading.value = false; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     watch(() => props.houseData, (newVal) => { | ||||||
|  |       Object.assign(formData, initialFormData()); | ||||||
|  |       if (props.isEdit && newVal && newVal.id) { | ||||||
|  |         formData.id = newVal.id; | ||||||
|  |         formData.name = newVal.name; | ||||||
|  |         formData.description = newVal.description; | ||||||
|  |       } | ||||||
|  |     }, { immediate: true, deep: true }); | ||||||
|  |  | ||||||
|  |     watch(() => props.visible, (newVal) => { | ||||||
|  |         if (newVal && !props.isEdit) { | ||||||
|  |             Object.assign(formData, initialFormData()); | ||||||
|  |             nextTick(() => { | ||||||
|  |                 formRef.value?.clearValidate(); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       formRef, | ||||||
|  |       loading, | ||||||
|  |       formData, | ||||||
|  |       rules, | ||||||
|  |       title, | ||||||
|  |       handleClose, | ||||||
|  |       handleSubmit | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .dialog-footer { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: flex-end; | ||||||
|  |   gap: 10px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										151
									
								
								src/components/PigHouseList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/components/PigHouseList.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="pig-house-list"> | ||||||
|  |     <div v-for="house in enrichedPigHouses" :key="house.id" class="pig-house-item"> | ||||||
|  |       <div class="house-header" @click="emitToggleExpand(house.id)"> | ||||||
|  |         <div class="house-info"> | ||||||
|  |           <span>猪舍: {{ house.name }}</span> | ||||||
|  |           <span v-if="house.description">描述: {{ house.description }}</span> | ||||||
|  |         </div> | ||||||
|  |         <div class="house-actions"> | ||||||
|  |           <el-button size="small" type="primary" @click.stop="emitAddPen(house)">增加猪栏</el-button> | ||||||
|  |           <el-button size="small" @click.stop="emitEditHouse(house)">编辑</el-button> | ||||||
|  |           <el-button size="small" type="danger" @click.stop="emitDeleteHouse(house)">删除</el-button> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <div v-if="house.isExpanded" class="house-content"> | ||||||
|  |         <div v-if="house.pens && house.pens.length > 0" class="pig-pen-list"> | ||||||
|  |           <PigPenInfoCard | ||||||
|  |             v-for="pen in house.pens" | ||||||
|  |             :key="pen.id" | ||||||
|  |             :pen="pen" | ||||||
|  |             @edit="emitEditPen" | ||||||
|  |             @delete="emitDeletePen" | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |         <div v-else class="no-pens-message"> | ||||||
|  |           <p>该猪舍下没有猪栏信息。</p> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import PigPenInfoCard from './PigPenInfoCard.vue'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'PigHouseList', | ||||||
|  |   components: { | ||||||
|  |     PigPenInfoCard | ||||||
|  |   }, | ||||||
|  |   props: { | ||||||
|  |     pigHouses: { | ||||||
|  |       type: Array, | ||||||
|  |       required: true | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   emits: ['edit-house', 'delete-house', 'add-pen', 'edit-pen', 'delete-pen', 'toggle-house-expand'], | ||||||
|  |   computed: { | ||||||
|  |     enrichedPigHouses() { | ||||||
|  |       return this.pigHouses.map(house => { | ||||||
|  |         const pensWithHouseInfo = house.pens ? house.pens.map(pen => ({ | ||||||
|  |           ...pen, | ||||||
|  |           house_name: house.name, | ||||||
|  |           house_id: house.id | ||||||
|  |         })) : []; | ||||||
|  |         return { | ||||||
|  |           ...house, | ||||||
|  |           pens: pensWithHouseInfo | ||||||
|  |         }; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     emitToggleExpand(houseId) { | ||||||
|  |       this.$emit('toggle-house-expand', houseId); | ||||||
|  |     }, | ||||||
|  |     // 猪舍操作 | ||||||
|  |     emitAddPen(house) { | ||||||
|  |       this.$emit('add-pen', house); | ||||||
|  |     }, | ||||||
|  |     emitEditHouse(house) { | ||||||
|  |       this.$emit('edit-house', house); | ||||||
|  |     }, | ||||||
|  |     emitDeleteHouse(house) { | ||||||
|  |       this.$emit('delete-house', house); | ||||||
|  |     }, | ||||||
|  |     // 猪栏操作 | ||||||
|  |     emitEditPen(pen) { | ||||||
|  |       this.$emit('edit-pen', pen); | ||||||
|  |     }, | ||||||
|  |     emitDeletePen(pen) { | ||||||
|  |       this.$emit('delete-pen', pen); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .pig-house-list { | ||||||
|  |   width: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .pig-house-item { | ||||||
|  |   border: 1px solid #eee; | ||||||
|  |   border-radius: 4px; | ||||||
|  |   margin-bottom: 16px; | ||||||
|  |   overflow: hidden; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .house-header { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: space-between; | ||||||
|  |   align-items: center; | ||||||
|  |   padding: 16px; | ||||||
|  |   cursor: pointer; | ||||||
|  |   background-color: #f9f9f9; | ||||||
|  |   transition: background-color 0.3s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .house-header:hover { | ||||||
|  |   background-color: #f0f0f0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .house-info { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |   gap: 8px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .house-info span:first-child { | ||||||
|  |   font-weight: bold; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .house-info span { | ||||||
|  |   margin-right: 20px; | ||||||
|  |   font-size: 14px; | ||||||
|  |   color: #606266; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .house-actions { | ||||||
|  |   display: flex; | ||||||
|  |   gap: 10px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .house-content { | ||||||
|  |   padding: 16px; | ||||||
|  |   border-top: 1px solid #eee; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .pig-pen-list { | ||||||
|  |   display: flex; | ||||||
|  |   flex-wrap: wrap; | ||||||
|  |   gap: 16px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .no-pens-message { | ||||||
|  |   color: #909399; | ||||||
|  |   text-align: center; | ||||||
|  |   padding: 20px 0; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										127
									
								
								src/components/PigPenInfoCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								src/components/PigPenInfoCard.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="pig-pen-info-card"> | ||||||
|  |     <div class="info-section"> | ||||||
|  |       <div class="title"> | ||||||
|  |         猪栏: {{ pen.pen_number }} <el-tag size="small" :type="statusType">{{ pen.status || '未知' }}</el-tag> | ||||||
|  |       </div> | ||||||
|  |       <div class="info-item">猪舍: {{ pen.house_name || '未知' }}</div> | ||||||
|  |       <div class="info-item border-left">容量: {{ pen.capacity }}</div> | ||||||
|  |       <div class="info-item">批次: {{ pen.batch_number || '未分配' }}</div> | ||||||
|  |       <div class="info-item border-left">存栏: <span :class="{'over-capacity': pen.current_pig_count > pen.capacity}">{{ pen.current_pig_count || 0 }}</span></div> | ||||||
|  |     </div> | ||||||
|  |     <div class="actions-section"> | ||||||
|  |       <el-button size="small" @click="emitEdit">编辑</el-button> | ||||||
|  |       <el-button size="small" type="danger" @click="emitDelete">删除</el-button> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { computed } from 'vue'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'PigPenInfoCard', | ||||||
|  |   props: { | ||||||
|  |     pen: { | ||||||
|  |       type: Object, | ||||||
|  |       required: true | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   emits: ['edit', 'delete'], | ||||||
|  |   setup(props, { emit }) { | ||||||
|  |     const statusType = computed(() => { | ||||||
|  |       switch (props.pen.status) { | ||||||
|  |         case '使用中': | ||||||
|  |           return 'success'; | ||||||
|  |         case '病猪栏': | ||||||
|  |         case '维修中': | ||||||
|  |           return 'danger'; | ||||||
|  |         case '清洗消毒': | ||||||
|  |           return 'warning'; | ||||||
|  |         case '空闲': | ||||||
|  |         default: | ||||||
|  |           return 'info'; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const emitEdit = () => { | ||||||
|  |       emit('edit', props.pen); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const emitDelete = () => { | ||||||
|  |       emit('delete', props.pen); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       statusType, | ||||||
|  |       emitEdit, | ||||||
|  |       emitDelete | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .pig-pen-info-card { | ||||||
|  |   border: 2px solid #ccc; | ||||||
|  |   border-radius: 8px; | ||||||
|  |   padding: 16px; | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |   justify-content: space-between; | ||||||
|  |   width: 220px; /* 适当加宽以容纳更多信息 */ | ||||||
|  |   height: auto; /* 让高度自适应内容 */ | ||||||
|  |   min-height: 240px; /* 设置最小高度 */ | ||||||
|  |   background-color: #fff; | ||||||
|  |   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | ||||||
|  |   transition: box-shadow 0.3s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .pig-pen-info-card:hover { | ||||||
|  |   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .info-section { | ||||||
|  |   display: grid; /* 使用grid布局创建两列 */ | ||||||
|  |   grid-template-columns: 1fr 1fr; /* 两列等宽 */ | ||||||
|  |   gap: 8px 10px; /* 行间距 8px, 列间距 10px */ | ||||||
|  |   margin-bottom: 10px; | ||||||
|  |   flex-grow: 1; /* 允许信息部分填充可用空间 */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .info-section .title { | ||||||
|  |   font-weight: bold; | ||||||
|  |   font-size: 1.1em; | ||||||
|  |   color: #303133; | ||||||
|  |   grid-column: 1 / -1; /* 标题横跨两列 */ | ||||||
|  |   margin-bottom: 5px; /* 标题下方增加一点间距 */ | ||||||
|  |   display: flex; /* Add flexbox for alignment */ | ||||||
|  |   align-items: center; /* Vertically align items */ | ||||||
|  |   gap: 8px; /* Space between title text and tag */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .info-section .info-item { | ||||||
|  |   font-size: 0.9em; | ||||||
|  |   color: #606266; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .info-item.border-left { | ||||||
|  |   border-left: 1px solid #eee; /* 添加左边框作为分隔线 */ | ||||||
|  |   padding-left: 10px; /* 增加左内边距,使内容不紧贴边框 */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .actions-section { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |   gap: 8px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .actions-section .el-button { | ||||||
|  |   width: 100%; | ||||||
|  |   margin: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .over-capacity { | ||||||
|  |   color: red; | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -1,15 +1,15 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="plan-detail"> |   <div class="plan-detail"> | ||||||
|     <div v-if="loading" class="loading"> |     <div v-if="loading" class="loading"> | ||||||
|       <el-skeleton animated /> |       <el-skeleton animated/> | ||||||
|     </div> |     </div> | ||||||
|     <div v-else-if="error" class="error"> |     <div v-else-if="error" class="error"> | ||||||
|       <el-alert |       <el-alert | ||||||
|         :title="'加载计划内容失败 (ID: ' + planId + ')'" |           :title="'加载计划内容失败 (ID: ' + planId + ')'" | ||||||
|         :description="error" |           :description="error" | ||||||
|         type="error" |           type="error" | ||||||
|         show-icon |           show-icon | ||||||
|         @close="error = null" |           @close="error = null" | ||||||
|       /> |       /> | ||||||
|       <el-button type="primary" @click="fetchPlan" class="retry-btn">重新加载</el-button> |       <el-button type="primary" @click="fetchPlan" class="retry-btn">重新加载</el-button> | ||||||
|     </div> |     </div> | ||||||
| @@ -20,25 +20,35 @@ | |||||||
|             <span>{{ plan.name }} - 内容</span> |             <span>{{ plan.name }} - 内容</span> | ||||||
|             <div> |             <div> | ||||||
|               <template v-if="!isSubPlan"> |               <template v-if="!isSubPlan"> | ||||||
|                 <el-button class="button" type="primary" @click="savePlanContent" v-if="isEditingContent">保存</el-button> |                 <el-button class="button" type="primary" @click="savePlanContent" v-if="isEditingContent" | ||||||
|                 <el-button class="button" type="danger" @click="cancelEdit" v-if="isEditingContent">取消</el-button> |                            :disabled="plan.plan_type === '系统任务'">保存 | ||||||
|                 <el-button class="button" @click="enterEditMode" v-else>编辑内容</el-button> |                 </el-button> | ||||||
|  |                 <el-button class="button" type="danger" @click="cancelEdit" v-if="isEditingContent" | ||||||
|  |                            :disabled="plan.plan_type === '系统任务'">取消 | ||||||
|  |                 </el-button> | ||||||
|  |                 <el-button class="button" @click="enterEditMode" v-else :disabled="plan.plan_type === '系统任务'"> | ||||||
|  |                   编辑内容 | ||||||
|  |                 </el-button> | ||||||
|               </template> |               </template> | ||||||
|  |  | ||||||
|               <!-- Dynamic Add Buttons --> |               <!-- Dynamic Add Buttons --> | ||||||
|               <template v-if="isEditingContent"> |               <template v-if="isEditingContent"> | ||||||
|                 <el-button |                 <el-button | ||||||
|                   v-if="plan.content_type === 'sub_plans' || !plan.content_type" |                     v-if="plan.content_type === 'sub_plans' || !plan.content_type" | ||||||
|                   type="primary" |                     type="primary" | ||||||
|                   size="small" |                     size="small" | ||||||
|                   @click="showAddSubPlanDialog" |                     @click="showAddSubPlanDialog" | ||||||
|                 >增加子计划</el-button> |                     :disabled="plan.plan_type === '系统任务'" | ||||||
|  |                 >增加子计划 | ||||||
|  |                 </el-button> | ||||||
|                 <el-button |                 <el-button | ||||||
|                   v-if="plan.content_type === 'tasks' || !plan.content_type" |                     v-if="plan.content_type === 'tasks' || !plan.content_type" | ||||||
|                   type="primary" |                     type="primary" | ||||||
|                   size="small" |                     size="small" | ||||||
|                   @click="showTaskEditorDialog()" |                     @click="showTaskEditorDialog()" | ||||||
|                 >增加子任务</el-button> |                     :disabled="plan.plan_type === '系统任务'" | ||||||
|  |                 >增加子任务 | ||||||
|  |                 </el-button> | ||||||
|               </template> |               </template> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
| @@ -49,10 +59,10 @@ | |||||||
|           <h4>任务列表</h4> |           <h4>任务列表</h4> | ||||||
|           <el-timeline v-if="plan.tasks.length > 0"> |           <el-timeline v-if="plan.tasks.length > 0"> | ||||||
|             <el-timeline-item |             <el-timeline-item | ||||||
|               v-for="(task, index) in plan.tasks" |                 v-for="(task, index) in plan.tasks" | ||||||
|               :key="task.id || 'new-task-' + index" |                 :key="task.id || 'new-task-' + index" | ||||||
|               :timestamp="'执行顺序: ' + (task.execution_order !== undefined ? task.execution_order : index + 1)" |                 :timestamp="'执行顺序: ' + (task.execution_order !== undefined ? task.execution_order : index + 1)" | ||||||
|               placement="top" |                 placement="top" | ||||||
|             > |             > | ||||||
|               <el-card> |               <el-card> | ||||||
|                 <h5>{{ task.name }} ({{ task.type === 'waiting' ? '延时任务' : '未知任务' }})</h5> |                 <h5>{{ task.name }} ({{ task.type === 'waiting' ? '延时任务' : '未知任务' }})</h5> | ||||||
| @@ -61,8 +71,12 @@ | |||||||
|                   延时: {{ task.parameters.delay_duration }} 秒 |                   延时: {{ task.parameters.delay_duration }} 秒 | ||||||
|                 </p> |                 </p> | ||||||
|                 <el-button-group v-if="isEditingContent"> |                 <el-button-group v-if="isEditingContent"> | ||||||
|                   <el-button type="primary" size="small" @click="editTask(task)">编辑</el-button> |                   <el-button type="primary" size="small" @click="editTask(task)" | ||||||
|                   <el-button type="danger" size="small" @click="deleteTask(task)">删除</el-button> |                              :disabled="plan.plan_type === '系统任务'">编辑 | ||||||
|  |                   </el-button> | ||||||
|  |                   <el-button type="danger" size="small" @click="deleteTask(task)" | ||||||
|  |                              :disabled="plan.plan_type === '系统任务'">删除 | ||||||
|  |                   </el-button> | ||||||
|                 </el-button-group> |                 </el-button-group> | ||||||
|               </el-card> |               </el-card> | ||||||
|             </el-timeline-item> |             </el-timeline-item> | ||||||
| @@ -74,13 +88,16 @@ | |||||||
|         <div v-else-if="plan.content_type === 'sub_plans'"> |         <div v-else-if="plan.content_type === 'sub_plans'"> | ||||||
|           <h4>子计划列表</h4> |           <h4>子计划列表</h4> | ||||||
|           <div v-if="plan.sub_plans.length > 0"> |           <div v-if="plan.sub_plans.length > 0"> | ||||||
|             <div v-for="(subPlan, index) in plan.sub_plans" :key="subPlan.id || 'new-subplan-' + index" class="sub-plan-wrapper"> |             <div v-for="(subPlan, index) in plan.sub_plans" :key="subPlan.id || 'new-subplan-' + index" | ||||||
|  |                  class="sub-plan-wrapper"> | ||||||
|               <el-card> |               <el-card> | ||||||
|                 <div class="sub-plan-card-content"> |                 <div class="sub-plan-card-content"> | ||||||
|                   <!-- Pass child_plan_id to recursive PlanDetail --> |                   <!-- Pass child_plan_id to recursive PlanDetail --> | ||||||
|                   <plan-detail :plan-id="subPlan.child_plan_id" :is-sub-plan="true" /> |                   <plan-detail :plan-id="subPlan.child_plan_id" :is-sub-plan="true"/> | ||||||
|                   <el-button-group v-if="isEditingContent" class="sub-plan-actions"> |                   <el-button-group v-if="isEditingContent" class="sub-plan-actions"> | ||||||
|                     <el-button type="danger" size="small" @click="deleteSubPlan(subPlan)">删除</el-button> |                     <el-button type="danger" size="small" @click="deleteSubPlan(subPlan)" | ||||||
|  |                                :disabled="plan.plan_type === '系统任务'">删除 | ||||||
|  |                     </el-button> | ||||||
|                   </el-button-group> |                   </el-button-group> | ||||||
|                 </div> |                 </div> | ||||||
|               </el-card> |               </el-card> | ||||||
| @@ -100,22 +117,22 @@ | |||||||
|  |  | ||||||
|     <!-- Add Sub-plan Dialog --> |     <!-- Add Sub-plan Dialog --> | ||||||
|     <el-dialog |     <el-dialog | ||||||
|       v-model="addSubPlanDialogVisible" |         v-model="addSubPlanDialogVisible" | ||||||
|       title="选择子计划" |         title="选择子计划" | ||||||
|       width="600px" |         width="600px" | ||||||
|       @close="resetAddSubPlanDialog" |         @close="resetAddSubPlanDialog" | ||||||
|     > |     > | ||||||
|       <el-select |       <el-select | ||||||
|         v-model="selectedSubPlanId" |           v-model="selectedSubPlanId" | ||||||
|         placeholder="请选择一个计划作为子计划" |           placeholder="请选择一个计划作为子计划" | ||||||
|         filterable |           filterable | ||||||
|         style="width: 100%;" |           style="width: 100%;" | ||||||
|       > |       > | ||||||
|         <el-option |         <el-option | ||||||
|           v-for="item in availablePlans" |             v-for="item in availablePlans" | ||||||
|           :key="item.id" |             :key="item.id" | ||||||
|           :label="item.name" |             :label="item.name" | ||||||
|           :value="item.id" |             :value="item.id" | ||||||
|         ></el-option> |         ></el-option> | ||||||
|       </el-select> |       </el-select> | ||||||
|       <template #footer> |       <template #footer> | ||||||
| @@ -128,31 +145,35 @@ | |||||||
|  |  | ||||||
|     <!-- Task Editor Dialog (for Add and Edit) --> |     <!-- Task Editor Dialog (for Add and Edit) --> | ||||||
|     <el-dialog |     <el-dialog | ||||||
|       v-model="taskEditorDialogVisible" |         v-model="taskEditorDialogVisible" | ||||||
|       :title="isEditingTask ? '编辑子任务' : '增加子任务'" |         :title="isEditingTask ? '编辑子任务' : '增加子任务'" | ||||||
|       width="600px" |         width="600px" | ||||||
|       @close="resetTaskEditorDialog" |         @close="resetTaskEditorDialog" | ||||||
|     > |     > | ||||||
|       <el-form :model="currentTaskForm" ref="taskFormRef" :rules="taskFormRules" label-width="100px"> |       <el-form :model="currentTaskForm" ref="taskFormRef" :rules="taskFormRules" label-width="100px"> | ||||||
|         <el-form-item label="任务类型" prop="type"> |         <el-form-item label="任务类型" prop="type"> | ||||||
|           <el-select v-model="currentTaskForm.type" placeholder="请选择任务类型" style="width: 100%;" :disabled="isEditingTask"> |           <el-select v-model="currentTaskForm.type" placeholder="请选择任务类型" style="width: 100%;" | ||||||
|  |                      :disabled="isEditingTask || plan.plan_type === '系统任务'"> | ||||||
|             <!-- Only Delay Task for now --> |             <!-- Only Delay Task for now --> | ||||||
|             <el-option label="延时任务" value="delay_task"></el-option> |             <el-option label="延时任务" value="delay_task"></el-option> | ||||||
|           </el-select> |           </el-select> | ||||||
|         </el-form-item> |         </el-form-item> | ||||||
|         <el-form-item label="任务名称" prop="name"> |         <el-form-item label="任务名称" prop="name"> | ||||||
|           <el-input v-model="currentTaskForm.name" placeholder="请输入任务名称"></el-input> |           <el-input v-model="currentTaskForm.name" placeholder="请输入任务名称" | ||||||
|  |                     :disabled="plan.plan_type === '系统任务'"></el-input> | ||||||
|         </el-form-item> |         </el-form-item> | ||||||
|         <el-form-item label="任务描述" prop="description"> |         <el-form-item label="任务描述" prop="description"> | ||||||
|           <el-input type="textarea" v-model="currentTaskForm.description" placeholder="请输入任务描述"></el-input> |           <el-input type="textarea" v-model="currentTaskForm.description" placeholder="请输入任务描述" | ||||||
|  |                     :disabled="plan.plan_type === '系统任务'"></el-input> | ||||||
|         </el-form-item> |         </el-form-item> | ||||||
|         <!-- Dynamic task component for specific parameters --> |         <!-- Dynamic task component for specific parameters --> | ||||||
|         <template v-if="currentTaskForm.type === 'delay_task'"> |         <template v-if="currentTaskForm.type === 'delay_task'"> | ||||||
|           <DelayTaskEditor |           <DelayTaskEditor | ||||||
|             :parameters="currentTaskForm.parameters" |               :parameters="currentTaskForm.parameters" | ||||||
|             @update:parameters="val => currentTaskForm.parameters = val" |               @update:parameters="val => currentTaskForm.parameters = val" | ||||||
|             prop-path="parameters.delay_duration" |               prop-path="parameters.delay_duration" | ||||||
|             :is-editing="true"  |               :is-editing="true" | ||||||
|  |               :disabled="plan.plan_type === '系统任务'" | ||||||
|           /> |           /> | ||||||
|         </template> |         </template> | ||||||
|         <!-- More task types can be rendered here --> |         <!-- More task types can be rendered here --> | ||||||
| @@ -160,7 +181,7 @@ | |||||||
|       <template #footer> |       <template #footer> | ||||||
|         <span class="dialog-footer"> |         <span class="dialog-footer"> | ||||||
|           <el-button @click="taskEditorDialogVisible = false">取消</el-button> |           <el-button @click="taskEditorDialogVisible = false">取消</el-button> | ||||||
|           <el-button type="primary" @click="confirmTaskEdit">确定</el-button> |           <el-button type="primary" @click="confirmTaskEdit" :disabled="plan.plan_type === '系统任务'">确定</el-button> | ||||||
|         </span> |         </span> | ||||||
|       </template> |       </template> | ||||||
|     </el-dialog> |     </el-dialog> | ||||||
| @@ -169,8 +190,8 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import apiClient from '../api/index.js'; | import apiClient from '../api/index.js'; | ||||||
| import { ElMessage, ElMessageBox } from 'element-plus'; | import {ElMessage, ElMessageBox} from 'element-plus'; | ||||||
| import { ArrowDown } from '@element-plus/icons-vue';  | import {ArrowDown} from '@element-plus/icons-vue'; | ||||||
| import DelayTaskEditor from './tasks/DelayTask.vue'; | import DelayTaskEditor from './tasks/DelayTask.vue'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
| @@ -224,15 +245,15 @@ export default { | |||||||
|         parameters: {}, |         parameters: {}, | ||||||
|       }, |       }, | ||||||
|       taskFormRules: { |       taskFormRules: { | ||||||
|         type: [{ required: true, message: '请选择任务类型', trigger: 'change' }], |         type: [{required: true, message: '请选择任务类型', trigger: 'change'}], | ||||||
|         name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }], |         name: [{required: true, message: '请输入任务名称', trigger: 'blur'}], | ||||||
|         // Rule for delay_duration will be added/removed dynamically |         // Rule for delay_duration will be added/removed dynamically | ||||||
|       }, |       }, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     delayDurationRules() { |     delayDurationRules() { | ||||||
|       return [{ required: true, message: '请输入延时时间', trigger: 'blur' }]; |       return [{required: true, message: '请输入延时时间', trigger: 'blur'}]; | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
| @@ -265,7 +286,7 @@ export default { | |||||||
|       this.loading = true; |       this.loading = true; | ||||||
|       this.error = null; |       this.error = null; | ||||||
|       try { |       try { | ||||||
|         const response = await apiClient.plans.get(this.planId); |         const response = await apiClient.plans.getPlanById(this.planId); | ||||||
|         this.plan = { |         this.plan = { | ||||||
|           ...response.data, |           ...response.data, | ||||||
|           sub_plans: response.data.sub_plans || [], |           sub_plans: response.data.sub_plans || [], | ||||||
| @@ -303,24 +324,24 @@ export default { | |||||||
|           execute_num: this.plan.execute_num, |           execute_num: this.plan.execute_num, | ||||||
|           cron_expression: this.plan.cron_expression, |           cron_expression: this.plan.cron_expression, | ||||||
|           sub_plan_ids: this.plan.content_type === 'sub_plans' |           sub_plan_ids: this.plan.content_type === 'sub_plans' | ||||||
|             ? this.plan.sub_plans.map(sp => sp.child_plan_id)  |               ? this.plan.sub_plans.map(sp => sp.child_plan_id) | ||||||
|             : [], |               : [], | ||||||
|           tasks: this.plan.content_type === 'tasks' |           tasks: this.plan.content_type === 'tasks' | ||||||
|             ? this.plan.tasks.map((task, index) => ({ |               ? this.plan.tasks.map((task, index) => ({ | ||||||
|                 name: task.name, |                 name: task.name, | ||||||
|                 description: task.description, |                 description: task.description, | ||||||
|                 type: task.type, |                 type: task.type, | ||||||
|                 execution_order: index + 1, |                 execution_order: index + 1, | ||||||
|                 parameters: task.parameters || {}, |                 parameters: task.parameters || {}, | ||||||
|               })) |               })) | ||||||
|             : [], |               : [], | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         delete submitData.execute_count; |         delete submitData.execute_count; | ||||||
|         delete submitData.status; |         delete submitData.status; | ||||||
|  |  | ||||||
|         console.log("PlanDetail: Submitting data", submitData); |         console.log("PlanDetail: Submitting data", submitData); | ||||||
|         await apiClient.plans.update(this.planId, submitData); |         await apiClient.plans.updatePlan(this.planId, submitData); | ||||||
|         ElMessage.success('计划内容已保存'); |         ElMessage.success('计划内容已保存'); | ||||||
|         this.isEditingContent = false; |         this.isEditingContent = false; | ||||||
|         this.fetchPlan(); |         this.fetchPlan(); | ||||||
| @@ -355,9 +376,9 @@ export default { | |||||||
|     }, |     }, | ||||||
|     async fetchAvailablePlans() { |     async fetchAvailablePlans() { | ||||||
|       try { |       try { | ||||||
|         const response = await apiClient.plans.list(); |         const response = await apiClient.plans.getPlans({plan_type: '自定义任务', page: 1, page_size: 1000}); | ||||||
|         this.availablePlans = response.data.plans.filter(p => |         this.availablePlans = response.data.plans.filter(p => | ||||||
|           p.id !== this.planId |             p.id !== this.planId | ||||||
|         ); |         ); | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|         ElMessage.error('加载可用计划失败: ' + (error.message || '未知错误')); |         ElMessage.error('加载可用计划失败: ' + (error.message || '未知错误')); | ||||||
| @@ -409,17 +430,17 @@ export default { | |||||||
|       console.log("PlanDetail: Showing task editor dialog."); |       console.log("PlanDetail: Showing task editor dialog."); | ||||||
|       if (this.plan.sub_plans.length > 0 && !task) { |       if (this.plan.sub_plans.length > 0 && !task) { | ||||||
|         ElMessageBox.confirm('当前计划包含子计划,添加任务将清空现有子计划。是否继续?', '警告', { |         ElMessageBox.confirm('当前计划包含子计划,添加任务将清空现有子计划。是否继续?', '警告', { | ||||||
|             confirmButtonText: '确定', |           confirmButtonText: '确定', | ||||||
|             cancelButtonText: '取消', |           cancelButtonText: '取消', | ||||||
|             type: 'warning' |           type: 'warning' | ||||||
|           }).then(() => { |         }).then(() => { | ||||||
|             this.plan.sub_plans = [];  |           this.plan.sub_plans = []; | ||||||
|             this.taskEditorDialogVisible = true; |           this.taskEditorDialogVisible = true; | ||||||
|             this.prepareTaskForm(task); |           this.prepareTaskForm(task); | ||||||
|           }).catch(() => { |         }).catch(() => { | ||||||
|             // User cancelled |           // User cancelled | ||||||
|           }); |         }); | ||||||
|           return; |         return; | ||||||
|       } |       } | ||||||
|       this.taskEditorDialogVisible = true; |       this.taskEditorDialogVisible = true; | ||||||
|       this.prepareTaskForm(task); |       this.prepareTaskForm(task); | ||||||
| @@ -444,7 +465,7 @@ export default { | |||||||
|         this.isEditingTask = false; |         this.isEditingTask = false; | ||||||
|         this.editingTaskOriginalId = null; |         this.editingTaskOriginalId = null; | ||||||
|         // For new tasks, ensure delay_duration is reactive from start |         // For new tasks, ensure delay_duration is reactive from start | ||||||
|         this.currentTaskForm.parameters = { delay_duration: null };  |         this.currentTaskForm.parameters = {delay_duration: null}; | ||||||
|         console.log("PlanDetail: Prepared currentTaskForm for adding:", JSON.parse(JSON.stringify(this.currentTaskForm))); |         console.log("PlanDetail: Prepared currentTaskForm for adding:", JSON.parse(JSON.stringify(this.currentTaskForm))); | ||||||
|       } |       } | ||||||
|       // Manually trigger watch for type to ensure rules and default parameters are set |       // Manually trigger watch for type to ensure rules and default parameters are set | ||||||
| @@ -476,7 +497,7 @@ export default { | |||||||
|                 name: this.currentTaskForm.name, |                 name: this.currentTaskForm.name, | ||||||
|                 description: this.currentTaskForm.description, |                 description: this.currentTaskForm.description, | ||||||
|                 type: this.currentTaskForm.type === 'delay_task' ? 'waiting' : this.currentTaskForm.type, |                 type: this.currentTaskForm.type === 'delay_task' ? 'waiting' : this.currentTaskForm.type, | ||||||
|                 parameters: { ...this.currentTaskForm.parameters }, // Deep copy parameters to ensure new reference |                 parameters: {...this.currentTaskForm.parameters}, // Deep copy parameters to ensure new reference | ||||||
|               }; |               }; | ||||||
|               this.plan.tasks.splice(index, 1, updatedTask); // Replace the old task with the new one |               this.plan.tasks.splice(index, 1, updatedTask); // Replace the old task with the new one | ||||||
|               ElMessage.success(`子任务 "${this.currentTaskForm.name}" 已更新`); |               ElMessage.success(`子任务 "${this.currentTaskForm.name}" 已更新`); | ||||||
| @@ -491,7 +512,7 @@ export default { | |||||||
|               type: this.currentTaskForm.type === 'delay_task' ? 'waiting' : this.currentTaskForm.type, |               type: this.currentTaskForm.type === 'delay_task' ? 'waiting' : this.currentTaskForm.type, | ||||||
|               name: this.currentTaskForm.name, |               name: this.currentTaskForm.name, | ||||||
|               description: this.currentTaskForm.description, |               description: this.currentTaskForm.description, | ||||||
|               parameters: { ...this.currentTaskForm.parameters }, // Deep copy parameters to ensure new reference |               parameters: {...this.currentTaskForm.parameters}, // Deep copy parameters to ensure new reference | ||||||
|             }; |             }; | ||||||
|             this.plan.tasks = [...this.plan.tasks, newTask]; // Create a new array reference |             this.plan.tasks = [...this.plan.tasks, newTask]; // Create a new array reference | ||||||
|             ElMessage.success(`子任务 "${newTask.name}" 已添加`); |             ElMessage.success(`子任务 "${newTask.name}" 已添加`); | ||||||
| @@ -540,35 +561,43 @@ export default { | |||||||
| .plan-detail { | .plan-detail { | ||||||
|   margin-top: 10px; |   margin-top: 10px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .loading, .error { | .loading, .error { | ||||||
|   padding: 20px; |   padding: 20px; | ||||||
|   text-align: center; |   text-align: center; | ||||||
| } | } | ||||||
|  |  | ||||||
| .retry-btn { | .retry-btn { | ||||||
|   margin-top: 15px; |   margin-top: 15px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .sub-plan-wrapper { | .sub-plan-wrapper { | ||||||
|   margin-bottom: 10px; |   margin-bottom: 10px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .sub-plan-container { | .sub-plan-container { | ||||||
|   margin-left: 20px; |   margin-left: 20px; | ||||||
|   margin-top: 10px; |   margin-top: 10px; | ||||||
|   border-left: 2px solid #ebeef5; |   border-left: 2px solid #ebeef5; | ||||||
|   padding-left: 10px; |   padding-left: 10px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .sub-plan-card-content { | .sub-plan-card-content { | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   gap: 10px; |   gap: 10px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .sub-plan-actions { | .sub-plan-actions { | ||||||
|   align-self: flex-end; |   align-self: flex-end; | ||||||
| } | } | ||||||
|  |  | ||||||
| .card-header { | .card-header { | ||||||
|   display: flex; |   display: flex; | ||||||
|   justify-content: space-between; |   justify-content: space-between; | ||||||
|   align-items: center; |   align-items: center; | ||||||
| } | } | ||||||
|  |  | ||||||
| /* 调整子计划卡片内部的header,避免重复样式 */ | /* 调整子计划卡片内部的header,避免重复样式 */ | ||||||
| .sub-plan-container .card-header { | .sub-plan-container .card-header { | ||||||
|   padding: 0; |   padding: 0; | ||||||
|   | |||||||
| @@ -145,7 +145,7 @@ export default { | |||||||
|     // 处理执行方式变更 |     // 处理执行方式变更 | ||||||
|     const handleExecutionTypeChange = (value) => { |     const handleExecutionTypeChange = (value) => { | ||||||
|       // 如果切换为手动执行,清空执行次数和cron表达式 |       // 如果切换为手动执行,清空执行次数和cron表达式 | ||||||
|       if (value === 'manual') { |       if (value === '手动') { | ||||||
|         formData.execute_num = 0; |         formData.execute_num = 0; | ||||||
|         formData.cron_expression = ''; |         formData.cron_expression = ''; | ||||||
|       } else { |       } else { | ||||||
| @@ -178,7 +178,7 @@ export default { | |||||||
|             } |             } | ||||||
|              |              | ||||||
|             // 如果是手动执行,清除执行次数和cron表达式 |             // 如果是手动执行,清除执行次数和cron表达式 | ||||||
|             if (formData.execution_type === 'manual') { |             if (formData.execution_type === '手动') { | ||||||
|               submitData.execute_num = 0; |               submitData.execute_num = 0; | ||||||
|               submitData.cron_expression = ''; |               submitData.cron_expression = ''; | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -1,400 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="plan-list"> |  | ||||||
|     <el-card> |  | ||||||
|       <template #header> |  | ||||||
|         <div class="card-header"> |  | ||||||
|           <div class="title-container"> |  | ||||||
|             <h2 class="page-title">计划管理</h2> |  | ||||||
|             <el-button type="text" @click="loadPlans" class="refresh-btn" title="刷新计划列表"> |  | ||||||
|               <el-icon :size="20"><Refresh /></el-icon> |  | ||||||
|             </el-button> |  | ||||||
|           </div> |  | ||||||
|           <el-button type="primary" @click="addPlan">添加计划</el-button> |  | ||||||
|         </div> |  | ||||||
|       </template> |  | ||||||
|        |  | ||||||
|       <!-- 加载状态 --> |  | ||||||
|       <div v-if="loading" class="loading"> |  | ||||||
|         <el-skeleton animated /> |  | ||||||
|       </div> |  | ||||||
|        |  | ||||||
|       <!-- 错误状态 --> |  | ||||||
|       <div v-else-if="error" class="error"> |  | ||||||
|         <el-alert |  | ||||||
|           title="获取计划数据失败" |  | ||||||
|           :description="error" |  | ||||||
|           type="error" |  | ||||||
|           show-icon |  | ||||||
|           closable |  | ||||||
|           @close="error = null" |  | ||||||
|         /> |  | ||||||
|         <el-button type="primary" @click="loadPlans" class="retry-btn">重新加载</el-button> |  | ||||||
|       </div> |  | ||||||
|        |  | ||||||
|       <el-table  |  | ||||||
|         v-else |  | ||||||
|         :data="plans" |  | ||||||
|         style="width: 100%"  |  | ||||||
|         class="plan-list-table" |  | ||||||
|         :fit="true" |  | ||||||
|         :scrollbar-always-on="true" |  | ||||||
|         @sort-change="handleSortChange"> |  | ||||||
|         <el-table-column prop="id" label="计划ID" min-width="100" sortable="custom" /> |  | ||||||
|         <el-table-column prop="name" label="计划名称" min-width="120" sortable="custom" /> |  | ||||||
|         <el-table-column prop="description" label="计划描述" min-width="150" /> |  | ||||||
|         <el-table-column prop="execution_type" label="执行类型" min-width="150" sortable="custom"> |  | ||||||
|           <template #default="scope"> |  | ||||||
|             <el-tag v-if="scope.row.execution_type === 'manual'">手动</el-tag> |  | ||||||
|             <el-tag v-else-if="scope.row.execute_num === 0" type="success">自动(无限执行)</el-tag> |  | ||||||
|             <el-tag v-else type="warning">自动({{ scope.row.execute_num }}次)</el-tag> |  | ||||||
|           </template> |  | ||||||
|         </el-table-column> |  | ||||||
|         <el-table-column prop="execute_count" label="已执行次数" min-width="120" sortable="custom" /> |  | ||||||
|         <el-table-column prop="status" label="状态" min-width="100" sortable="custom"> |  | ||||||
|           <template #default="scope"> |  | ||||||
|             <el-tag v-if="scope.row.status === 0" type="danger">禁用计划</el-tag> |  | ||||||
|             <el-tag v-else-if="scope.row.status === 1" type="success">启用计划</el-tag> |  | ||||||
|             <el-tag v-else-if="scope.row.status === 3" type="danger">执行失败</el-tag> |  | ||||||
|             <el-tag v-else type="info">执行完毕</el-tag> |  | ||||||
|           </template> |  | ||||||
|         </el-table-column> |  | ||||||
|         <el-table-column prop="cron_expression" label="下次执行时间" min-width="150" sortable="custom"> |  | ||||||
|           <template #default="scope"> |  | ||||||
|             {{ formatNextExecutionTime(scope.row.cron_expression) }} |  | ||||||
|           </template> |  | ||||||
|         </el-table-column> |  | ||||||
|         <el-table-column label="操作" width="280"> |  | ||||||
|           <template #default="scope"> |  | ||||||
|             <el-button size="small" @click="editPlan(scope.row)">编辑</el-button> |  | ||||||
|             <el-button size="small" @click="showDetails(scope.row)">详情</el-button> |  | ||||||
|             <el-button  |  | ||||||
|               size="small"  |  | ||||||
|               :type="scope.row.status === 1 ? 'warning' : 'primary'" |  | ||||||
|               @click="scope.row.status === 1 ? stopPlan(scope.row) : startPlan(scope.row)" |  | ||||||
|               :loading="stoppingPlanId === scope.row.id || startingPlanId === scope.row.id" |  | ||||||
|             > |  | ||||||
|               {{ scope.row.status === 1 ? '停止' : '启动' }} |  | ||||||
|             </el-button> |  | ||||||
|             <el-button size="small" type="danger" @click="deletePlan(scope.row)">删除</el-button> |  | ||||||
|           </template> |  | ||||||
|         </el-table-column> |  | ||||||
|       </el-table> |  | ||||||
|        |  | ||||||
|     </el-card> |  | ||||||
|  |  | ||||||
|     <!-- 计划表单 --> |  | ||||||
|     <PlanForm  |  | ||||||
|       v-model:visible="dialogVisible"  |  | ||||||
|       :plan-data="currentPlan"  |  | ||||||
|       :is-edit="isEdit" |  | ||||||
|       @success="handlePlanSuccess" |  | ||||||
|       @cancel="handlePlanCancel" |  | ||||||
|     /> |  | ||||||
|  |  | ||||||
|     <!-- 计划详情 --> |  | ||||||
|     <el-dialog |  | ||||||
|       v-model="detailsVisible" |  | ||||||
|       title="计划详情" |  | ||||||
|       width="70%" |  | ||||||
|       top="5vh" |  | ||||||
|     > |  | ||||||
|       <plan-detail |  | ||||||
|         v-if="detailsVisible"  |  | ||||||
|         :plan-id="selectedPlanIdForDetails" |  | ||||||
|       /> |  | ||||||
|       <template #footer> |  | ||||||
|         <span class="dialog-footer"> |  | ||||||
|           <el-button @click="detailsVisible = false">关闭</el-button> |  | ||||||
|         </span> |  | ||||||
|       </template> |  | ||||||
|     </el-dialog> |  | ||||||
|  |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import { Refresh } from '@element-plus/icons-vue'; |  | ||||||
| import apiClient from '../api/index.js'; |  | ||||||
| import PlanForm from './PlanForm.vue'; |  | ||||||
| import PlanDetail from './PlanDetail.vue'; // 导入新组件 |  | ||||||
| import cronParser from 'cron-parser'; |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   name: 'PlanList', |  | ||||||
|   components: { |  | ||||||
|     PlanForm, |  | ||||||
|     PlanDetail, // 注册新组件 |  | ||||||
|     Refresh |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       plans: [], |  | ||||||
|       originalPlans: [], // Store the original unsorted list |  | ||||||
|       dialogVisible: false, |  | ||||||
|       detailsVisible: false, // 控制详情对话框 |  | ||||||
|       isEdit: false, |  | ||||||
|       loading: false, |  | ||||||
|       error: null, |  | ||||||
|       currentPlan: { |  | ||||||
|         id: null, |  | ||||||
|         name: '', |  | ||||||
|         description: '', |  | ||||||
|         execution_type: 'automatic', |  | ||||||
|         execute_num: 0, |  | ||||||
|         cron_expression: '' |  | ||||||
|       }, |  | ||||||
|       selectedPlanIdForDetails: null, // 当前要查看详情的计划ID |  | ||||||
|       startingPlanId: null, |  | ||||||
|       stoppingPlanId: null |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   async mounted() { |  | ||||||
|     await this.loadPlans(); |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     // 加载计划列表 |  | ||||||
|     async loadPlans() { |  | ||||||
|       this.loading = true; |  | ||||||
|       this.error = null; |  | ||||||
|        |  | ||||||
|       try { |  | ||||||
|         const response = await apiClient.plans.list(); |  | ||||||
|         let fetchedPlans = response.data?.plans || []; |  | ||||||
|         // Default sort by ID ascending |  | ||||||
|         fetchedPlans.sort((a, b) => a.id - b.id); |  | ||||||
|         this.plans = fetchedPlans; |  | ||||||
|         this.originalPlans = [...this.plans]; // Keep a copy of the original order |  | ||||||
|       } catch (err) { |  | ||||||
|         this.error = err.message || '未知错误'; |  | ||||||
|         console.error('加载计划列表失败:', err); |  | ||||||
|       } finally { |  | ||||||
|         this.loading = false; |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     // 处理表格排序 |  | ||||||
|     handleSortChange({ prop, order }) { |  | ||||||
|       if (!order) { |  | ||||||
|         // 恢复原始顺序 |  | ||||||
|         this.plans = [...this.originalPlans]; |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const sortFactor = order === 'ascending' ? 1 : -1; |  | ||||||
|  |  | ||||||
|       this.plans.sort((a, b) => { |  | ||||||
|         let valA = a[prop]; |  | ||||||
|         let valB = b[prop]; |  | ||||||
|  |  | ||||||
|         // '下次执行时间' 列的特殊排序逻辑 |  | ||||||
|         if (prop === 'cron_expression') { |  | ||||||
|           try { |  | ||||||
|             const parser = cronParser.default || cronParser; |  | ||||||
|             valA = valA ? parser.parse(valA).next().toDate().getTime() : 0; |  | ||||||
|           } catch (e) { |  | ||||||
|             valA = 0; // 无效的 cron 表达式排在前面 |  | ||||||
|           } |  | ||||||
|           try { |  | ||||||
|             const parser = cronParser.default || cronParser; |  | ||||||
|             valB = valB ? parser.parse(valB).next().toDate().getTime() : 0; |  | ||||||
|           } catch (e) { |  | ||||||
|             valB = 0; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (valA < valB) { |  | ||||||
|           return -1 * sortFactor; |  | ||||||
|         } |  | ||||||
|         if (valA > valB) { |  | ||||||
|           return 1 * sortFactor; |  | ||||||
|         } |  | ||||||
|         return 0; |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|      |  | ||||||
|     // 格式化下次执行时间 |  | ||||||
|     formatNextExecutionTime(cronExpression) { |  | ||||||
|       if (!cronExpression) { |  | ||||||
|         return '-'; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       try { |  | ||||||
|         // 正确使用cron-parser库 |  | ||||||
|         const parser = cronParser.default || cronParser; |  | ||||||
|         const interval = parser.parse(cronExpression); |  | ||||||
|         const next = interval.next().toDate(); |  | ||||||
|         return next.toLocaleString('zh-CN'); |  | ||||||
|       } catch (err) { |  | ||||||
|         console.error('解析cron表达式失败:', err); |  | ||||||
|         return '无效的表达式'; |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|      |  | ||||||
|     addPlan() { |  | ||||||
|       this.currentPlan = { |  | ||||||
|         id: null, |  | ||||||
|         name: '', |  | ||||||
|         description: '', |  | ||||||
|         execution_type: 'automatic', |  | ||||||
|         execute_num: 0, |  | ||||||
|         cron_expression: '' |  | ||||||
|       }; |  | ||||||
|       this.isEdit = false; |  | ||||||
|       this.dialogVisible = true; |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     showDetails(plan) { |  | ||||||
|       this.selectedPlanIdForDetails = plan.id; |  | ||||||
|       this.detailsVisible = true; |  | ||||||
|     }, |  | ||||||
|      |  | ||||||
|     editPlan(plan) { |  | ||||||
|       this.currentPlan = { ...plan }; |  | ||||||
|       this.isEdit = true; |  | ||||||
|       this.dialogVisible = true; |  | ||||||
|     }, |  | ||||||
|      |  | ||||||
|     async deletePlan(plan) { |  | ||||||
|       try { |  | ||||||
|         await this.$confirm('确认删除该计划吗?', '提示', { |  | ||||||
|           confirmButtonText: '确定', |  | ||||||
|           cancelButtonText: '取消', |  | ||||||
|           type: 'warning' |  | ||||||
|         }); |  | ||||||
|          |  | ||||||
|         await apiClient.plans.delete(plan.id); |  | ||||||
|         this.$message.success('删除成功'); |  | ||||||
|         await this.loadPlans(); |  | ||||||
|       } catch (err) { |  | ||||||
|         if (err !== 'cancel') { |  | ||||||
|           this.$message.error('删除失败: ' + (err.message || '未知错误')); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|      |  | ||||||
|     async startPlan(plan) { |  | ||||||
|       try { |  | ||||||
|         this.startingPlanId = plan.id; |  | ||||||
|         await apiClient.plans.start(plan.id); |  | ||||||
|         this.$message.success('计划启动成功'); |  | ||||||
|         await this.loadPlans(); |  | ||||||
|       } catch (err) { |  | ||||||
|         this.$message.error('启动失败: ' + (err.message || '未知错误')); |  | ||||||
|       } finally { |  | ||||||
|         this.startingPlanId = null; |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     async stopPlan(plan) { |  | ||||||
|       try { |  | ||||||
|         this.stoppingPlanId = plan.id; |  | ||||||
|         await apiClient.plans.stop(plan.id); |  | ||||||
|         this.$message.success('计划停止成功'); |  | ||||||
|         await this.loadPlans(); |  | ||||||
|       } catch (err) { |  | ||||||
|         this.$message.error('停止失败: ' + (err.message || '未知错误')); |  | ||||||
|       } finally { |  | ||||||
|         this.stoppingPlanId = null; |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|      |  | ||||||
|     // 处理计划表单提交成功 |  | ||||||
|     async handlePlanSuccess(planData) { |  | ||||||
|       try { |  | ||||||
|         if (this.isEdit) { |  | ||||||
|           // 编辑计划 |  | ||||||
|           await apiClient.plans.update(planData.id, planData); |  | ||||||
|           this.$message.success('计划更新成功'); |  | ||||||
|         } else { |  | ||||||
|           // 添加新计划 |  | ||||||
|           const planRequest = { |  | ||||||
|             ...planData, |  | ||||||
|             content_type: 'tasks' // 默认使用任务类型 |  | ||||||
|           }; |  | ||||||
|            |  | ||||||
|           await apiClient.plans.create(planRequest); |  | ||||||
|           this.$message.success('计划添加成功'); |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         await this.loadPlans(); |  | ||||||
|       } catch (err) { |  | ||||||
|         this.$message.error('保存失败: ' + (err.message || '未知错误')); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|      |  | ||||||
|     // 处理计划表单取消 |  | ||||||
|     handlePlanCancel() { |  | ||||||
|       this.dialogVisible = false; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style scoped> |  | ||||||
| .plan-list { |  | ||||||
|   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; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .loading { |  | ||||||
|   padding: 20px 0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .error { |  | ||||||
|   padding: 20px 0; |  | ||||||
|   text-align: center; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .retry-btn { |  | ||||||
|   margin-top: 15px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .plan-list-table :deep(tbody)::after { |  | ||||||
|   content: ""; |  | ||||||
|   display: block; |  | ||||||
|   height: 20px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @media (max-width: 768px) { |  | ||||||
|   .plan-list { |  | ||||||
|     padding: 10px; |  | ||||||
|   } |  | ||||||
|   .card-header { |  | ||||||
|     flex-direction: column; |  | ||||||
|     gap: 15px; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
							
								
								
									
										204
									
								
								src/components/TransferPigsAcrossBatchesModal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								src/components/TransferPigsAcrossBatchesModal.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,204 @@ | |||||||
|  | <template> | ||||||
|  |   <el-dialog | ||||||
|  |     title="跨群调栏" | ||||||
|  |     :model-value="visible" | ||||||
|  |     @update:model-value="$emit('update:visible', $event)" | ||||||
|  |     width="40%" | ||||||
|  |     @close="resetForm" | ||||||
|  |   > | ||||||
|  |     <el-form ref="form" :model="form" :rules="rules" label-width="120px"> | ||||||
|  |       <el-form-item label="源猪群批次"> | ||||||
|  |         <span>{{ batch.batch_number }}</span> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="源猪栏" prop="fromPenID"> | ||||||
|  |         <el-select v-model="form.fromPenID" placeholder="请选择源猪栏" style="width: 100%;"> | ||||||
|  |           <el-option | ||||||
|  |             v-for="pen in sourcePens" | ||||||
|  |             :key="pen.id" | ||||||
|  |             :label="`${pen.pen_number} (存栏: ${pen.current_pig_count})`" | ||||||
|  |             :value="pen.id" | ||||||
|  |           ></el-option> | ||||||
|  |         </el-select> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="目标猪群批次" prop="destBatchID"> | ||||||
|  |         <el-select v-model="form.destBatchID" placeholder="请选择目标猪群" style="width: 100%;" @change="onDestBatchChange"> | ||||||
|  |           <el-option | ||||||
|  |             v-for="b in availableBatches" | ||||||
|  |             :key="b.id" | ||||||
|  |             :label="b.batch_number" | ||||||
|  |             :value="b.id" | ||||||
|  |           ></el-option> | ||||||
|  |         </el-select> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="目标猪栏" prop="toPenID"> | ||||||
|  |         <el-select v-model="form.toPenID" placeholder="请选择目标猪栏" style="width: 100%;"> | ||||||
|  |           <el-option | ||||||
|  |             v-for="pen in destinationPens" | ||||||
|  |             :key="pen.id" | ||||||
|  |             :label="`${pen.pen_number} (容量: ${pen.capacity})`" | ||||||
|  |             :value="pen.id" | ||||||
|  |           ></el-option> | ||||||
|  |         </el-select> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="调栏数量" prop="quantity"> | ||||||
|  |         <el-input-number v-model="form.quantity" :min="1" :max="maxTransferQuantity" /> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="备注" prop="remarks"> | ||||||
|  |         <el-input v-model="form.remarks" type="textarea" :rows="2" placeholder="请输入备注" /> | ||||||
|  |       </el-form-item> | ||||||
|  |     </el-form> | ||||||
|  |     <template #footer> | ||||||
|  |       <span class="dialog-footer"> | ||||||
|  |         <el-button @click="$emit('update:visible', false)">取 消</el-button> | ||||||
|  |         <el-button type="primary" @click="handleConfirm">确 定</el-button> | ||||||
|  |       </span> | ||||||
|  |     </template> | ||||||
|  |   </el-dialog> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { getPigBatches, transferPigsAcrossBatches } from '@/api/pigBatch'; | ||||||
|  | import { getPens } from '@/api/pen'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'TransferPigsAcrossBatchesModal', | ||||||
|  |   props: { | ||||||
|  |     visible: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: true | ||||||
|  |     }, | ||||||
|  |     batch: { | ||||||
|  |       type: Object, | ||||||
|  |       required: true // 源猪群批次信息 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   emits: ['update:visible', 'success'], | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       form: { | ||||||
|  |         fromPenID: null, | ||||||
|  |         destBatchID: null, | ||||||
|  |         toPenID: null, | ||||||
|  |         quantity: 1, | ||||||
|  |         remarks: '' | ||||||
|  |       }, | ||||||
|  |       rules: { | ||||||
|  |         fromPenID: [{ required: true, message: '请选择源猪栏', trigger: 'change' }], | ||||||
|  |         destBatchID: [{ required: true, message: '请选择目标猪群批次', trigger: 'change' }], | ||||||
|  |         toPenID: [{ required: true, message: '请选择目标猪栏', trigger: 'change' }], | ||||||
|  |         quantity: [ | ||||||
|  |           { required: true, message: '请输入调栏数量', trigger: 'blur' }, | ||||||
|  |           { type: 'integer', message: '请输入整数', trigger: 'blur' }, | ||||||
|  |           { validator: this.validateQuantity, trigger: 'blur' } | ||||||
|  |         ] | ||||||
|  |       }, | ||||||
|  |       sourcePens: [], // 源猪群的猪栏列表 | ||||||
|  |       availableBatches: [], // 可用的目标猪群列表 | ||||||
|  |       destinationPens: [], // 目标猪群的猪栏列表 | ||||||
|  |       maxTransferQuantity: 1 // 最大可调栏数量 | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     visible(newVal) { | ||||||
|  |       if (newVal) { | ||||||
|  |         this.initData(); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     'form.fromPenID': function(newVal) { | ||||||
|  |       if (newVal) { | ||||||
|  |         const selectedPen = this.sourcePens.find(pen => pen.id === newVal); | ||||||
|  |         this.maxTransferQuantity = selectedPen ? selectedPen.current_pig_count : 1; | ||||||
|  |         // 如果当前数量大于最大值,重置数量 | ||||||
|  |         if (this.form.quantity > this.maxTransferQuantity) { | ||||||
|  |           this.form.quantity = this.maxTransferQuantity; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     async initData() { | ||||||
|  |       this.resetForm(); | ||||||
|  |       this.sourcePens = this.batch.pens.filter(pen => pen.current_pig_count > 0); // 过滤掉没有猪的猪栏 | ||||||
|  |       await this.fetchAvailableBatchesAndPens(); | ||||||
|  |     }, | ||||||
|  |     async fetchAvailableBatchesAndPens() { | ||||||
|  |       try { | ||||||
|  |         const [batchesResponse, pensResponse] = await Promise.all([ | ||||||
|  |           getPigBatches({ is_active: true }), // 获取所有活跃猪群 | ||||||
|  |           getPens() // 获取所有猪栏 | ||||||
|  |         ]); | ||||||
|  |  | ||||||
|  |         const allBatches = batchesResponse.data || []; | ||||||
|  |         const allPens = pensResponse.data || []; | ||||||
|  |  | ||||||
|  |         // 过滤掉源猪群自身,以及非活跃的猪群 | ||||||
|  |         this.availableBatches = allBatches.filter(b => b.id !== this.batch.id && b.is_active); | ||||||
|  |  | ||||||
|  |         // 将所有猪栏按批次ID分组,方便后续查找 | ||||||
|  |         const pensByBatch = allPens.reduce((acc, pen) => { | ||||||
|  |           if (pen.pig_batch_id) { | ||||||
|  |             if (!acc[pen.pig_batch_id]) { | ||||||
|  |               acc[pen.pig_batch_id] = []; | ||||||
|  |             } | ||||||
|  |             acc[pen.pig_batch_id].push(pen); | ||||||
|  |           } | ||||||
|  |           return acc; | ||||||
|  |         }, {}); | ||||||
|  |         this.pensByBatch = pensByBatch; // 存储起来,以便 onDestBatchChange 使用 | ||||||
|  |  | ||||||
|  |       } catch (error) { | ||||||
|  |         console.error("Error fetching batches or pens:", error); | ||||||
|  |         this.$message.error("获取猪群或猪栏信息失败"); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     onDestBatchChange(batchId) { | ||||||
|  |       this.form.toPenID = null; // 重置目标猪栏选择 | ||||||
|  |       // 允许选择目标批次下的所有猪栏,包括已满的 | ||||||
|  |       this.destinationPens = (this.pensByBatch[batchId] || []); | ||||||
|  |     }, | ||||||
|  |     validateQuantity(rule, value, callback) { | ||||||
|  |       if (value > this.maxTransferQuantity) { | ||||||
|  |         callback(new Error(`调栏数量不能超过源猪栏存栏数量 (${this.maxTransferQuantity})`)); | ||||||
|  |       } else { | ||||||
|  |         callback(); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     handleConfirm() { | ||||||
|  |       this.$refs.form.validate(async valid => { | ||||||
|  |         if (valid) { | ||||||
|  |           try { | ||||||
|  |             await transferPigsAcrossBatches(this.batch.id, { | ||||||
|  |               fromPenID: this.form.fromPenID, | ||||||
|  |               destBatchID: this.form.destBatchID, | ||||||
|  |               toPenID: this.form.toPenID, | ||||||
|  |               quantity: this.form.quantity, | ||||||
|  |               remarks: this.form.remarks | ||||||
|  |             }); | ||||||
|  |             this.$message.success('跨群调栏成功'); | ||||||
|  |             this.$emit('success'); | ||||||
|  |             this.$emit('update:visible', false); | ||||||
|  |           } catch (error) { | ||||||
|  |             console.error('Error transferring pigs across batches:', error); | ||||||
|  |             this.$message.error('跨群调栏失败: ' + (error.response?.data?.message || error.message || '未知错误')); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     resetForm() { | ||||||
|  |       this.$refs.form?.resetFields(); | ||||||
|  |       this.form.fromPenID = null; | ||||||
|  |       this.form.destBatchID = null; | ||||||
|  |       this.form.toPenID = null; | ||||||
|  |       this.form.quantity = 1; | ||||||
|  |       this.form.remarks = ''; | ||||||
|  |       this.sourcePens = []; | ||||||
|  |       this.availableBatches = []; | ||||||
|  |       this.destinationPens = []; | ||||||
|  |       this.maxTransferQuantity = 1; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | </style> | ||||||
							
								
								
									
										161
									
								
								src/components/TransferPigsModal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/components/TransferPigsModal.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | |||||||
|  | <template> | ||||||
|  |   <el-dialog | ||||||
|  |     title="群内调栏" | ||||||
|  |     :model-value="visible" | ||||||
|  |     @update:model-value="$emit('update:visible', $event)" | ||||||
|  |     width="500px" | ||||||
|  |     :before-close="handleClose" | ||||||
|  |   > | ||||||
|  |     <el-form ref="formRef" :model="form" :rules="rules" label-width="100px"> | ||||||
|  |       <el-form-item label="调出猪栏" prop="fromPenID"> | ||||||
|  |         <el-select v-model="form.fromPenID" placeholder="请选择调出猪栏" style="width: 100%;" @change="handleFromPenChange"> | ||||||
|  |           <el-option | ||||||
|  |             v-for="pen in sourcePens" | ||||||
|  |             :key="pen.id" | ||||||
|  |             :label="`${pen.pen_number} (存栏: ${pen.current_pig_count})`" | ||||||
|  |             :value="pen.id" | ||||||
|  |             :disabled="pen.current_pig_count === 0" | ||||||
|  |           /> | ||||||
|  |         </el-select> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="调入猪栏" prop="toPenID"> | ||||||
|  |         <el-select v-model="form.toPenID" placeholder="请选择调入猪栏" style="width: 100%;" :disabled="!form.fromPenID"> | ||||||
|  |           <el-option | ||||||
|  |             v-for="pen in destinationPens" | ||||||
|  |             :key="pen.id" | ||||||
|  |             :label="`${pen.pen_number} (存栏: ${pen.current_pig_count})`" | ||||||
|  |             :value="pen.id" | ||||||
|  |           /> | ||||||
|  |         </el-select> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="调栏数量" prop="quantity"> | ||||||
|  |         <el-input-number | ||||||
|  |           v-model="form.quantity" | ||||||
|  |           :min="1" | ||||||
|  |           :max="maxQuantity" | ||||||
|  |           :disabled="!form.fromPenID" | ||||||
|  |           placeholder="请输入数量" | ||||||
|  |           style="width: 100%;" | ||||||
|  |         /> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="备注" prop="remarks"> | ||||||
|  |         <el-input v-model="form.remarks" type="textarea" placeholder="请输入备注" /> | ||||||
|  |       </el-form-item> | ||||||
|  |     </el-form> | ||||||
|  |     <template #footer> | ||||||
|  |       <span class="dialog-footer"> | ||||||
|  |         <el-button @click="handleClose">取 消</el-button> | ||||||
|  |         <el-button type="primary" @click="handleSubmit" :loading="loading">确 定</el-button> | ||||||
|  |       </span> | ||||||
|  |     </template> | ||||||
|  |   </el-dialog> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { transferPigsWithinBatch } from '@/api/pigBatch.js'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'TransferPigsModal', | ||||||
|  |   props: { | ||||||
|  |     visible: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|  |     batch: { | ||||||
|  |       type: Object, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   emits: ['update:visible', 'success'], | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       loading: false, | ||||||
|  |       form: { | ||||||
|  |         fromPenID: null, | ||||||
|  |         toPenID: null, | ||||||
|  |         quantity: 1, | ||||||
|  |         remarks: '', | ||||||
|  |       }, | ||||||
|  |       rules: { | ||||||
|  |         fromPenID: [{ required: true, message: '请选择调出猪栏', trigger: 'change' }], | ||||||
|  |         toPenID: [{ required: true, message: '请选择调入猪栏', trigger: 'change' }], | ||||||
|  |         quantity: [{ required: true, message: '请输入调栏数量', trigger: 'blur' }], | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     sourcePens() { | ||||||
|  |       return this.batch.pens || []; | ||||||
|  |     }, | ||||||
|  |     destinationPens() { | ||||||
|  |       if (!this.form.fromPenID) return []; | ||||||
|  |       return this.batch.pens.filter(pen => pen.id !== this.form.fromPenID); | ||||||
|  |     }, | ||||||
|  |     maxQuantity() { | ||||||
|  |       if (!this.form.fromPenID) return 1; | ||||||
|  |       const selectedPen = this.sourcePens.find(p => p.id === this.form.fromPenID); | ||||||
|  |       return selectedPen ? selectedPen.current_pig_count : 1; | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     visible(newVal) { | ||||||
|  |       if (newVal) { | ||||||
|  |         this.resetForm(); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     handleFromPenChange(penId) { | ||||||
|  |       this.form.toPenID = null; | ||||||
|  |       this.form.quantity = 1; | ||||||
|  |       const selectedPen = this.sourcePens.find(p => p.id === penId); | ||||||
|  |       if (selectedPen && selectedPen.current_pig_count === 0) { | ||||||
|  |         this.$message.warning('该猪栏没有猪,无法调出。'); | ||||||
|  |         this.form.fromPenID = null; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     handleSubmit() { | ||||||
|  |       this.$refs.formRef.validate(async (valid) => { | ||||||
|  |         if (valid) { | ||||||
|  |           if (this.form.quantity > this.maxQuantity) { | ||||||
|  |             this.$message.error('调栏数量不能超过当前存栏量'); | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |           this.loading = true; | ||||||
|  |           try { | ||||||
|  |             await transferPigsWithinBatch(this.batch.id, this.form); | ||||||
|  |             this.$message.success('调栏成功'); | ||||||
|  |             this.$emit('success'); | ||||||
|  |             this.handleClose(); | ||||||
|  |           } catch (error) { | ||||||
|  |             this.$message.error('调栏失败: ' + (error.response?.data?.message || error.message)); | ||||||
|  |             console.error('Failed to transfer pigs:', error); | ||||||
|  |           } finally { | ||||||
|  |             this.loading = false; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     handleClose() { | ||||||
|  |       this.$emit('update:visible', false); | ||||||
|  |     }, | ||||||
|  |     resetForm() { | ||||||
|  |       if (this.$refs.formRef) { | ||||||
|  |         this.$refs.formRef.resetFields(); | ||||||
|  |       } | ||||||
|  |       this.form = { | ||||||
|  |         fromPenID: null, | ||||||
|  |         toPenID: null, | ||||||
|  |         quantity: 1, | ||||||
|  |         remarks: '', | ||||||
|  |       }; | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .dialog-footer { | ||||||
|  |   text-align: right; | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -15,19 +15,130 @@ | |||||||
|         :collapse="isCollapse" |         :collapse="isCollapse" | ||||||
|         :collapse-transition="false" |         :collapse-transition="false" | ||||||
|         router |         router | ||||||
|  |         :default-openeds="['/device-management', '/monitor', '/pms']" | ||||||
|       > |       > | ||||||
|         <el-menu-item index="/"> |         <el-menu-item index="/"> | ||||||
|           <el-icon><House /></el-icon> |           <el-icon><House /></el-icon> | ||||||
|           <template #title>首页</template> |           <template #title>首页</template> | ||||||
|         </el-menu-item> |         </el-menu-item> | ||||||
|         <el-menu-item index="/devices"> |  | ||||||
|           <el-icon><Monitor /></el-icon> |         <!-- 设备管理二级菜单 --> | ||||||
|           <template #title>设备管理</template> |         <el-sub-menu index="/device-management"> | ||||||
|         </el-menu-item> |           <template #title> | ||||||
|  |             <el-icon><Setting /></el-icon> | ||||||
|  |             <span>设备</span> | ||||||
|  |           </template> | ||||||
|  |           <el-menu-item index="/devices"> | ||||||
|  |             <el-icon><Monitor /></el-icon> | ||||||
|  |             <template #title>设备管理</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/device-templates"> | ||||||
|  |             <el-icon><Tickets /></el-icon> | ||||||
|  |             <template #title>设备模板管理</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |         </el-sub-menu> | ||||||
|  |  | ||||||
|  |         <!-- 猪场管理二级菜单 --> | ||||||
|  |         <el-sub-menu index="/pms"> | ||||||
|  |           <template #title> | ||||||
|  |             <el-icon><OfficeBuilding /></el-icon> | ||||||
|  |             <span>猪场管理</span> | ||||||
|  |           </template> | ||||||
|  |           <el-menu-item index="/pms/farm-management"> | ||||||
|  |             <el-icon><Tickets /></el-icon> | ||||||
|  |             <template #title>栏舍管理</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/pms/batch-management"> | ||||||
|  |             <el-icon><Management /></el-icon> | ||||||
|  |             <template #title>猪群管理</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |         </el-sub-menu> | ||||||
|  |  | ||||||
|         <el-menu-item index="/plans"> |         <el-menu-item index="/plans"> | ||||||
|           <el-icon><Calendar /></el-icon> |           <el-icon><Calendar /></el-icon> | ||||||
|           <template #title>计划管理</template> |           <template #title>计划管理</template> | ||||||
|         </el-menu-item> |         </el-menu-item> | ||||||
|  |  | ||||||
|  |         <!-- 系统监控二级菜单 --> | ||||||
|  |         <el-sub-menu index="/monitor"> | ||||||
|  |           <template #title> | ||||||
|  |             <el-icon><DataAnalysis /></el-icon> | ||||||
|  |             <span>系统监控</span> | ||||||
|  |           </template> | ||||||
|  |           <el-menu-item index="/monitor/device-command-logs"> | ||||||
|  |             <el-icon><Document /></el-icon> | ||||||
|  |             <template #title>设备命令日志</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/feed-usage-records"> | ||||||
|  |             <el-icon><Food /></el-icon> | ||||||
|  |             <template #title>饲料使用记录</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/medication-logs"> | ||||||
|  |             <el-icon><FirstAidKit /></el-icon> | ||||||
|  |             <template #title>用药记录</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/notifications"> | ||||||
|  |             <el-icon><Bell /></el-icon> | ||||||
|  |             <template #title>通知记录</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/pending-collections"> | ||||||
|  |             <el-icon><Clock /></el-icon> | ||||||
|  |             <template #title>待采集请求</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/pig-batch-logs"> | ||||||
|  |             <el-icon><Files /></el-icon> | ||||||
|  |             <template #title>猪批次日志</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/pig-purchases"> | ||||||
|  |             <el-icon><ShoppingCart /></el-icon> | ||||||
|  |             <template #title>猪只采购记录</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/pig-sales"> | ||||||
|  |             <el-icon><SoldOut /></el-icon> | ||||||
|  |             <template #title>猪只售卖记录</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/pig-sick-logs"> | ||||||
|  |             <el-icon><Warning /></el-icon> | ||||||
|  |             <template #title>病猪日志</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/pig-transfer-logs"> | ||||||
|  |             <el-icon><Switch /></el-icon> | ||||||
|  |             <template #title>猪只迁移日志</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/plan-execution-logs"> | ||||||
|  |             <el-icon><List /></el-icon> | ||||||
|  |             <template #title>计划执行日志</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/raw-material-purchases"> | ||||||
|  |             <el-icon><Shop /></el-icon> | ||||||
|  |             <template #title>原料采购记录</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/raw-material-stock-logs"> | ||||||
|  |             <el-icon><Coin /></el-icon> | ||||||
|  |             <template #title>原料库存日志</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/sensor-data"> | ||||||
|  |             <el-icon><DataLine /></el-icon> | ||||||
|  |             <template #title>传感器数据</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/task-execution-logs"> | ||||||
|  |             <el-icon><Finished /></el-icon> | ||||||
|  |             <template #title>任务执行日志</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/user-action-logs"> | ||||||
|  |             <el-icon><User /></el-icon> | ||||||
|  |             <template #title>用户操作日志</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/weighing-batches"> | ||||||
|  |             <el-icon><ScaleToOriginal /></el-icon> | ||||||
|  |             <template #title>批次称重记录</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |           <el-menu-item index="/monitor/weighing-records"> | ||||||
|  |             <el-icon><ScaleToOriginal /></el-icon> | ||||||
|  |             <template #title>单次称重记录</template> | ||||||
|  |           </el-menu-item> | ||||||
|  |         </el-sub-menu> | ||||||
|  |  | ||||||
|       </el-menu> |       </el-menu> | ||||||
|     </el-aside> |     </el-aside> | ||||||
|  |  | ||||||
| @@ -45,12 +156,12 @@ | |||||||
|           <div class="user-info"> |           <div class="user-info"> | ||||||
|             <el-dropdown> |             <el-dropdown> | ||||||
|               <span class="el-dropdown-link"> |               <span class="el-dropdown-link"> | ||||||
|                 管理员 <el-icon><ArrowDown /></el-icon> |                 {{ username }} <el-icon><ArrowDown /></el-icon> | ||||||
|               </span> |               </span> | ||||||
|               <template #dropdown> |               <template #dropdown> | ||||||
|                 <el-dropdown-menu> |                 <el-dropdown-menu> | ||||||
|                   <el-dropdown-item>个人信息</el-dropdown-item> |                   <el-dropdown-item>个人信息</el-dropdown-item> | ||||||
|                   <el-dropdown-item>退出登录</el-dropdown-item> |                   <el-dropdown-item @click="logout">退出登录</el-dropdown-item> | ||||||
|                 </el-dropdown-menu> |                 </el-dropdown-menu> | ||||||
|               </template> |               </template> | ||||||
|             </el-dropdown> |             </el-dropdown> | ||||||
| @@ -72,50 +183,93 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { ref, computed } from 'vue'; | import { ref, computed, onMounted, onUnmounted } from 'vue'; | ||||||
| import { useRoute } from 'vue-router'; | import { useRoute, useRouter } from 'vue-router'; | ||||||
| import { House, Monitor, Calendar, ArrowDown, Menu, Fold, Expand } from '@element-plus/icons-vue'; | import {  | ||||||
|  |   House, Monitor, Calendar, ArrowDown, Menu, Fold, Expand, Setting, Tickets, DataAnalysis, Document, Food, | ||||||
|  |   FirstAidKit, Clock, Files, ShoppingCart, SoldOut, Warning, Switch, List, Shop, Coin, DataLine, Finished, User, ScaleToOriginal, OfficeBuilding, Management, Bell | ||||||
|  | } from '@element-plus/icons-vue'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   name: 'MainLayout', |   name: 'MainLayout', | ||||||
|   components: { |   components: { | ||||||
|     House, |     House, Monitor, Calendar, ArrowDown, Menu, Fold, Expand, Setting, Tickets, DataAnalysis, Document, Food, | ||||||
|     Monitor, |     FirstAidKit, Clock, Files, ShoppingCart, SoldOut, Warning, Switch, List, Shop, Coin, DataLine, Finished, User, ScaleToOriginal, OfficeBuilding, Management, Bell | ||||||
|     Calendar, |  | ||||||
|     ArrowDown, |  | ||||||
|     Menu, |  | ||||||
|     Fold, |  | ||||||
|     Expand |  | ||||||
|   }, |   }, | ||||||
|   setup() { |   setup() { | ||||||
|     const route = useRoute(); |     const route = useRoute(); | ||||||
|  |     const router = useRouter(); | ||||||
|     const isCollapse = ref(false); |     const isCollapse = ref(false); | ||||||
|  |     const username = ref(localStorage.getItem('username') || '管理员'); | ||||||
|  |  | ||||||
|  |     const handleStorageChange = () => { | ||||||
|  |       username.value = localStorage.getItem('username') || '管理员'; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     onMounted(() => { | ||||||
|  |       window.addEventListener('storage', handleStorageChange); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     onUnmounted(() => { | ||||||
|  |       window.removeEventListener('storage', handleStorageChange); | ||||||
|  |     }); | ||||||
|      |      | ||||||
|     // 切换侧边栏折叠状态 |  | ||||||
|     const toggleCollapse = () => { |     const toggleCollapse = () => { | ||||||
|       isCollapse.value = !isCollapse.value; |       isCollapse.value = !isCollapse.value; | ||||||
|     }; |     }; | ||||||
|      |      | ||||||
|     // 当前激活的菜单项 |  | ||||||
|     const activeMenu = computed(() => { |     const activeMenu = computed(() => { | ||||||
|  |       const path = route.path; | ||||||
|  |       if (path.startsWith('/monitor') || path.startsWith('/pms') || path.startsWith('/devices') || path.startsWith('/device-templates')) { | ||||||
|  |         return path; | ||||||
|  |       } | ||||||
|       return route.path; |       return route.path; | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     // 当前页面标题 |  | ||||||
|     const currentPageTitle = computed(() => { |     const currentPageTitle = computed(() => { | ||||||
|       const routeMap = { |       const routeMap = { | ||||||
|         '/': '系统首页', |         '/': '系统首页', | ||||||
|         '/devices': '设备管理', |         '/devices': '设备管理', | ||||||
|         '/plans': '计划管理' |         '/device-templates': '设备模板管理', | ||||||
|  |         '/pms/farm-management': '栏舍管理', | ||||||
|  |         '/pms/batch-management': '猪群管理', | ||||||
|  |         '/plans': '计划管理', | ||||||
|  |         '/monitor/device-command-logs': '设备命令日志', | ||||||
|  |         '/monitor/feed-usage-records': '饲料使用记录', | ||||||
|  |         '/monitor/medication-logs': '用药记录', | ||||||
|  |         '/monitor/notifications': '通知记录', | ||||||
|  |         '/monitor/pending-collections': '待采集请求', | ||||||
|  |         '/monitor/pig-batch-logs': '猪批次日志', | ||||||
|  |         '/monitor/pig-purchases': '猪只采购记录', | ||||||
|  |         '/monitor/pig-sales': '猪只售卖记录', | ||||||
|  |         '/monitor/pig-sick-logs': '病猪日志', | ||||||
|  |         '/monitor/pig-transfer-logs': '猪只迁移日志', | ||||||
|  |         '/monitor/plan-execution-logs': '计划执行日志', | ||||||
|  |         '/monitor/raw-material-purchases': '原料采购记录', | ||||||
|  |         '/monitor/raw-material-stock-logs': '原料库存日志', | ||||||
|  |         '/monitor/sensor-data': '传感器数据', | ||||||
|  |         '/monitor/task-execution-logs': '任务执行日志', | ||||||
|  |         '/monitor/user-action-logs': '用户操作日志', | ||||||
|  |         '/monitor/weighing-batches': '批次称重记录', | ||||||
|  |         '/monitor/weighing-records': '单次称重记录', | ||||||
|       }; |       }; | ||||||
|       return routeMap[route.path] || '猪场管理系统'; |       return routeMap[route.path] || '猪场管理系统'; | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     const logout = () => { | ||||||
|  |       localStorage.removeItem('jwt_token'); | ||||||
|  |       localStorage.removeItem('username'); | ||||||
|  |       username.value = '管理员'; | ||||||
|  |       router.push('/login'); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|       isCollapse, |       isCollapse, | ||||||
|       activeMenu, |       activeMenu, | ||||||
|       currentPageTitle, |       currentPageTitle, | ||||||
|       toggleCollapse |       toggleCollapse, | ||||||
|  |       logout, | ||||||
|  |       username | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										94
									
								
								src/main.js
									
									
									
									
									
								
							
							
						
						
									
										94
									
								
								src/main.js
									
									
									
									
									
								
							| @@ -1,40 +1,102 @@ | |||||||
| import { createApp } from 'vue'; | import {createApp} from 'vue'; | ||||||
| import { createRouter, createWebHistory } from 'vue-router'; | import {createRouter, createWebHistory} from 'vue-router'; | ||||||
| import ElementPlus from 'element-plus'; | import ElementPlus from 'element-plus'; | ||||||
| import 'element-plus/dist/index.css'; | import 'element-plus/dist/index.css'; | ||||||
|  | import zhCn from 'element-plus/dist/locale/zh-cn.mjs'; // 导入 Element Plus 中文语言包 | ||||||
|  |  | ||||||
| import App from './App.vue'; | import App from './App.vue'; | ||||||
| import Home from './components/Home.vue'; | import Home from './views/home/Home.vue'; | ||||||
| import DeviceList from './components/DeviceList.vue'; | import DeviceList from './views/device/DeviceList.vue'; | ||||||
| import PlanList from './components/PlanList.vue'; | import PlanList from './views/plan/PlanList.vue'; | ||||||
|  | import LoginForm from './views/home/LoginForm.vue'; | ||||||
|  | import DeviceTemplateList from './views/device/DeviceTemplateList.vue'; | ||||||
|  | import PigFarmManagementView from './views/pms/PigFarmManagementView.vue'; // 导入栏舍管理视图 | ||||||
|  | import PigBatchManagementView from './views/pms/PigBatchManagementView.vue'; // 导入猪群管理视图 | ||||||
|  |  | ||||||
|  | // --- 统一导入所有监控视图 --- | ||||||
|  | import DeviceCommandLogView from './views/monitor/DeviceCommandLogView.vue'; | ||||||
|  | import FeedUsageRecordsView from './views/monitor/FeedUsageRecordsView.vue'; | ||||||
|  | import MedicationLogsView from './views/monitor/MedicationLogsView.vue'; | ||||||
|  | import NotificationLogView from './views/monitor/NotificationLogView.vue'; | ||||||
|  | import PendingCollectionsView from './views/monitor/PendingCollectionsView.vue'; | ||||||
|  | import PigBatchLogsView from './views/monitor/PigBatchLogsView.vue'; | ||||||
|  | import PigPurchasesView from './views/monitor/PigPurchasesView.vue'; | ||||||
|  | import PigSalesView from './views/monitor/PigSalesView.vue'; | ||||||
|  | import PigSickLogsView from './views/monitor/PigSickLogsView.vue'; | ||||||
|  | import PigTransferLogsView from './views/monitor/PigTransferLogsView.vue'; | ||||||
|  | import PlanExecutionLogsView from './views/monitor/PlanExecutionLogsView.vue'; | ||||||
|  | import RawMaterialPurchasesView from './views/monitor/RawMaterialPurchasesView.vue'; | ||||||
|  | import RawMaterialStockLogsView from './views/monitor/RawMaterialStockLogsView.vue'; | ||||||
|  | import SensorDataView from './views/monitor/SensorDataView.vue'; | ||||||
|  | import TaskExecutionLogsView from './views/monitor/TaskExecutionLogsView.vue'; | ||||||
|  | import UserActionLogsView from './views/monitor/UserActionLogsView.vue'; | ||||||
|  | import WeighingBatchesView from './views/monitor/WeighingBatchesView.vue'; | ||||||
|  | import WeighingRecordsView from './views/monitor/WeighingRecordsView.vue'; | ||||||
|  | // --------------------------- | ||||||
|  |  | ||||||
| // 导入全局样式 | // 导入全局样式 | ||||||
| import './assets/styles/main.css'; | import './assets/styles/main.css'; | ||||||
|  |  | ||||||
| // 配置路由 | // 配置路由 | ||||||
| const routes = [ | const routes = [ | ||||||
|   { path: '/', component: Home }, |     {path: '/', component: Home, meta: {requiresAuth: true}}, | ||||||
|   { path: '/devices', component: DeviceList }, |     {path: '/devices', component: DeviceList, meta: {requiresAuth: true}}, | ||||||
|   { path: '/plans', component: PlanList } |     {path: '/device-templates', component: DeviceTemplateList, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/plans', component: PlanList, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/login', component: LoginForm}, | ||||||
|  |     {path: '/pms/farm-management', name: 'PigFarmManagement', component: PigFarmManagementView, meta: { requiresAuth: true, title: '栏舍管理' }}, | ||||||
|  |     {path: '/pms/batch-management', name: 'PigBatchManagement', component: PigBatchManagementView, meta: { requiresAuth: true, title: '猪群管理' }}, | ||||||
|  |  | ||||||
|  |     // --- 统一注册所有监控路由 --- | ||||||
|  |     {path: '/monitor/device-command-logs', component: DeviceCommandLogView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/feed-usage-records', component: FeedUsageRecordsView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/medication-logs', component: MedicationLogsView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/notifications', component: NotificationLogView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/pending-collections', component: PendingCollectionsView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/pig-batch-logs', component: PigBatchLogsView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/pig-purchases', component: PigPurchasesView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/pig-sales', component: PigSalesView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/pig-sick-logs', component: PigSickLogsView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/pig-transfer-logs', component: PigTransferLogsView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/plan-execution-logs', component: PlanExecutionLogsView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/raw-material-purchases', component: RawMaterialPurchasesView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/raw-material-stock-logs', component: RawMaterialStockLogsView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/sensor-data', component: SensorDataView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/task-execution-logs', component: TaskExecutionLogsView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/user-action-logs', component: UserActionLogsView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/weighing-batches', component: WeighingBatchesView, meta: {requiresAuth: true}}, | ||||||
|  |     {path: '/monitor/weighing-records', component: WeighingRecordsView, meta: {requiresAuth: true}}, | ||||||
|  |     // --------------------------- | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| const router = createRouter({ | const router = createRouter({ | ||||||
|   history: createWebHistory(), |     history: createWebHistory(), | ||||||
|   routes |     routes | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // 全局路由守卫 | ||||||
|  | router.beforeEach((to, from, next) => { | ||||||
|  |     const loggedIn = localStorage.getItem('jwt_token'); | ||||||
|  |  | ||||||
|  |     if (to.matched.some(record => record.meta.requiresAuth) && !loggedIn) { | ||||||
|  |         // 如果路由需要认证但用户未登录,则重定向到登录页 | ||||||
|  |         next('/login'); | ||||||
|  |     } else if (to.path === '/login' && loggedIn) { | ||||||
|  |         // 如果用户已登录但试图访问登录页,则重定向到首页 | ||||||
|  |         next('/'); | ||||||
|  |     } else { | ||||||
|  |         next(); // 正常放行 | ||||||
|  |     } | ||||||
| }); | }); | ||||||
|  |  | ||||||
| // 创建Vue应用实例 | // 创建Vue应用实例 | ||||||
| const app = createApp(App); | const app = createApp(App); | ||||||
|  |  | ||||||
| // 使用Element Plus组件库 | // 全局配置 Element Plus 为中文 | ||||||
| app.use(ElementPlus); | app.use(ElementPlus, {locale: zhCn}); | ||||||
|  |  | ||||||
| // 使用路由 | // 使用路由 | ||||||
| app.use(router); | app.use(router); | ||||||
|  |  | ||||||
| // 初始化服务(示例) |  | ||||||
| // app.config.globalProperties.$api = ApiService; |  | ||||||
| // app.config.globalProperties.$utils = Utils; |  | ||||||
|  |  | ||||||
| // 挂载应用 | // 挂载应用 | ||||||
| app.mount('#app'); | app.mount('#app'); | ||||||
| @@ -1,79 +1,118 @@ | |||||||
| import apiClient from '../api/index.js'; | import { AreaControllerApi, DeviceApi } from '../api/device.js'; | ||||||
|  |  | ||||||
| class DeviceService { | class DeviceService { | ||||||
|   /** |     /** | ||||||
|    * 获取设备列表 |      * 获取所有设备和区域主控的列表,并将其合并为树形结构所需的数据 | ||||||
|    * @returns {Promise<Array>} 设备列表 |      * @returns {Promise<Array>} 合并后的设备列表 | ||||||
|    */ |      */ | ||||||
|   async getDevices() { |     async getDevices() { | ||||||
|     try { |         try { | ||||||
|       const response = await apiClient.devices.list(); |             // 1. 并行发起两个独立的API请求:一个获取区域主控,一个获取普通设备 | ||||||
|       return response.data || []; |             const [areaControllersResponse, devicesResponse] = await Promise.all([ | ||||||
|     } catch (error) { |                 AreaControllerApi.list(), // 调用 GET /api/v1/area-controllers | ||||||
|       console.error('获取设备列表失败:', error); |                 DeviceApi.list()        // 调用 GET /api/v1/devices | ||||||
|       throw error; |             ]); | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |             // 2. 处理区域主控数据,并添加前端所需的'type'标识 | ||||||
|    * 创建新设备 |             const areaControllers = (areaControllersResponse.data || []).map(controller => ({ | ||||||
|    * @param {Object} device 设备信息 |                 ...controller, | ||||||
|    * @returns {Promise<Object>} 创建的设备信息 |                 type: 'area_controller' | ||||||
|    */ |             })); | ||||||
|   async createDevice(device) { |  | ||||||
|     try { |  | ||||||
|       const response = await apiClient.devices.create(device); |  | ||||||
|       return response.data; |  | ||||||
|     } catch (error) { |  | ||||||
|       console.error('创建设备失败:', error); |  | ||||||
|       throw error; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |             // 3. 处理普通设备数据,并添加'type'和'parent_id'以构建树形结构 | ||||||
|    * 获取设备详情 |             const devices = (devicesResponse.data || []).map(device => ({ | ||||||
|    * @param {number} deviceId 设备ID |                 ...device, | ||||||
|    * @returns {Promise<Object>} 设备详情 |                 type: 'device', | ||||||
|    */ |                 parent_id: device.area_controller_id | ||||||
|   async getDevice(deviceId) { |             })); | ||||||
|     try { |  | ||||||
|       const response = await apiClient.devices.get(deviceId); |  | ||||||
|       return response.data; |  | ||||||
|     } catch (error) { |  | ||||||
|       console.error('获取设备详情失败:', error); |  | ||||||
|       throw error; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |             // 4. 合并两份数据,形成完整的列表,返回给UI组件 | ||||||
|    * 更新设备信息 |             return [...areaControllers, ...devices]; | ||||||
|    * @param {number} deviceId 设备ID |         } catch (error) { | ||||||
|    * @param {Object} device 更新的设备信息 |             console.error('获取设备列表失败:', error); | ||||||
|    * @returns {Promise<Object>} 更新后的设备信息 |             throw error; | ||||||
|    */ |         } | ||||||
|   async updateDevice(deviceId, device) { |  | ||||||
|     try { |  | ||||||
|       const response = await apiClient.devices.update(deviceId, device); |  | ||||||
|       return response.data; |  | ||||||
|     } catch (error) { |  | ||||||
|       console.error('更新设备失败:', error); |  | ||||||
|       throw error; |  | ||||||
|     } |     } | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |     /** | ||||||
|    * 删除设备 |      * 创建新设备或区域主控 | ||||||
|    * @param {number} deviceId 设备ID |      * @param {Object} data - 设备或区域主控信息,包含type字段 | ||||||
|    * @returns {Promise<void>} |      * @returns {Promise<Object>} 创建结果 | ||||||
|    */ |      */ | ||||||
|   async deleteDevice(deviceId) { |     async createDevice(data) { | ||||||
|     try { |         try { | ||||||
|       await apiClient.devices.delete(deviceId); |             if (data.type === 'area_controller') { | ||||||
|     } catch (error) { |                 const response = await AreaControllerApi.create(data); | ||||||
|       console.error('删除设备失败:', error); |                 return {...response.data, type: 'area_controller'}; | ||||||
|       throw error; |             } else { | ||||||
|  |                 const response = await DeviceApi.create(data); | ||||||
|  |                 return {...response.data, type: 'device', parent_id: response.data.area_controller_id}; | ||||||
|  |             } | ||||||
|  |         } catch (error) { | ||||||
|  |             console.error('创建设备失败:', error); | ||||||
|  |             throw error; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 获取设备或区域主控详情 | ||||||
|  |      * @param {number} id - ID | ||||||
|  |      * @param {string} type - 类型 ('area_controller' 或 'device') | ||||||
|  |      * @returns {Promise<Object>} 详情 | ||||||
|  |      */ | ||||||
|  |     async getDevice(id, type) { | ||||||
|  |         try { | ||||||
|  |             if (type === 'area_controller') { | ||||||
|  |                 const response = await AreaControllerApi.getById(id); | ||||||
|  |                 return {...response.data, type: 'area_controller'}; | ||||||
|  |             } else { | ||||||
|  |                 const response = await DeviceApi.getById(id); | ||||||
|  |                 return {...response.data, type: 'device', parent_id: response.data.area_controller_id}; | ||||||
|  |             } | ||||||
|  |         } catch (error) { | ||||||
|  |             console.error('获取设备详情失败:', error); | ||||||
|  |             throw error; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 更新设备或区域主控信息 | ||||||
|  |      * @param {number} id - ID | ||||||
|  |      * @param {Object} data - 更新的设备或区域主控信息,包含type字段 | ||||||
|  |      * @returns {Promise<Object>} 更新后的信息 | ||||||
|  |      */ | ||||||
|  |     async updateDevice(id, data) { | ||||||
|  |         try { | ||||||
|  |             if (data.type === 'area_controller') { | ||||||
|  |                 const response = await AreaControllerApi.update(id, data); | ||||||
|  |                 return {...response.data, type: 'area_controller'}; | ||||||
|  |             } else { | ||||||
|  |                 const response = await DeviceApi.update(id, data); | ||||||
|  |                 return {...response.data, type: 'device', parent_id: response.data.area_controller_id}; | ||||||
|  |             } | ||||||
|  |         } catch (error) { | ||||||
|  |             console.error('更新设备失败:', error); | ||||||
|  |             throw error; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 删除设备或区域主控 | ||||||
|  |      * @param {Object} device - 包含id和type属性的设备或区域主控对象 | ||||||
|  |      * @returns {Promise<void>} | ||||||
|  |      */ | ||||||
|  |     async deleteDevice(device) { | ||||||
|  |         try { | ||||||
|  |             if (device.type === 'area_controller') { | ||||||
|  |                 await AreaControllerApi.delete(device.id); | ||||||
|  |             } else { | ||||||
|  |                 await DeviceApi.delete(device.id); | ||||||
|  |             } | ||||||
|  |         } catch (error) { | ||||||
|  |             console.error('删除设备失败:', error); | ||||||
|  |             throw error; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // 导出设备服务实例 | // 导出设备服务实例 | ||||||
|   | |||||||
							
								
								
									
										79
									
								
								src/services/deviceTemplateService.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/services/deviceTemplateService.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | |||||||
|  | import apiClient from '../api/index.js'; | ||||||
|  |  | ||||||
|  | class DeviceTemplateService { | ||||||
|  |   /** | ||||||
|  |    * 获取设备模板列表 | ||||||
|  |    * @returns {Promise<Array>} 设备模板列表 | ||||||
|  |    */ | ||||||
|  |   async getDeviceTemplates() { | ||||||
|  |     try { | ||||||
|  |       const response = await apiClient.deviceTemplates.getDeviceTemplates(); // 更正此处 | ||||||
|  |       return response || []; | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('获取设备模板列表失败:', error); | ||||||
|  |       throw error; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 创建新设备模板 | ||||||
|  |    * @param {Object} deviceTemplateData 设备模板数据 | ||||||
|  |    * @returns {Promise<Object>} 创建的设备模板信息 | ||||||
|  |    */ | ||||||
|  |   async createDeviceTemplate(deviceTemplateData) { | ||||||
|  |     try { | ||||||
|  |       const response = await apiClient.deviceTemplates.createDeviceTemplate(deviceTemplateData); | ||||||
|  |       return response.data; | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('创建设备模板失败:', error); | ||||||
|  |       throw error; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 获取设备模板详情 | ||||||
|  |    * @param {number} id 设备模板ID | ||||||
|  |    * @returns {Promise<Object>} 设备模板详情 | ||||||
|  |    */ | ||||||
|  |   async getDeviceTemplate(id) { | ||||||
|  |     try { | ||||||
|  |       const response = await apiClient.deviceTemplates.getDeviceTemplateById(id); | ||||||
|  |       return response.data; | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('获取设备模板详情失败:', error); | ||||||
|  |       throw error; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 更新设备模板信息 | ||||||
|  |    * @param {number} id 设备模板ID | ||||||
|  |    * @param {Object} deviceTemplateData 更新的设备模板信息 | ||||||
|  |    * @returns {Promise<Object>} 更新后的设备模板信息 | ||||||
|  |    */ | ||||||
|  |   async updateDeviceTemplate(id, deviceTemplateData) { | ||||||
|  |     try { | ||||||
|  |       const response = await apiClient.deviceTemplates.updateDeviceTemplate(id, deviceTemplateData); | ||||||
|  |       return response.data; | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('更新设备模板失败:', error); | ||||||
|  |       throw error; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * 删除设备模板 | ||||||
|  |    * @param {number} id 设备模板ID | ||||||
|  |    * @returns {Promise<void>} | ||||||
|  |    */ | ||||||
|  |   async deleteDeviceTemplate(id) { | ||||||
|  |     try { | ||||||
|  |       await apiClient.deviceTemplates.deleteDeviceTemplate(id); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('删除设备模板失败:', error); | ||||||
|  |       throw error; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default new DeviceTemplateService(); | ||||||
							
								
								
									
										22
									
								
								src/utils/format.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/utils/format.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | /** | ||||||
|  |  * 将 RFC3339 格式的日期时间字符串格式化为 'YYYY-MM-DD HH:mm:ss' | ||||||
|  |  * @param {string | null | undefined} rfc3339String - 后端返回的日期时间字符串 | ||||||
|  |  * @returns {string} - 格式化后的字符串,如果输入无效则返回空字符串或提示 | ||||||
|  |  */ | ||||||
|  | export function formatRFC3339(rfc3339String) { | ||||||
|  |   if (!rfc3339String) { | ||||||
|  |     return '--'; // 或者返回空字符串 '' | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   try { | ||||||
|  |     // 例如: "2025-10-17T14:57:01.570169+08:00" | ||||||
|  |     // 替换 'T' 为空格,并截取前 19 个字符即可得到 'YYYY-MM-DD HH:mm:ss' | ||||||
|  |     return rfc3339String.replace('T', ' ').substring(0, 19); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('Error formatting date:', rfc3339String, error); | ||||||
|  |     return rfc3339String; // 格式化失败时返回原始值 | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 你未来还可以添加其他全局格式化函数 | ||||||
|  | // export function formatCurrency(number) { ... } | ||||||
| @@ -14,11 +14,11 @@ const http = axios.create({ | |||||||
| // 请求拦截器 | // 请求拦截器 | ||||||
| http.interceptors.request.use( | http.interceptors.request.use( | ||||||
|   config => { |   config => { | ||||||
|     // 可以在这里添加认证token等 |     // 在这里添加认证token | ||||||
|     // const token = localStorage.getItem('access_token'); |     const token = localStorage.getItem('jwt_token'); | ||||||
|     // if (token) { |     if (token) { | ||||||
|     //   config.headers.Authorization = `Bearer ${token}`; |       config.headers.Authorization = `Bearer ${token}`; | ||||||
|     // } |     } | ||||||
|     return config; |     return config; | ||||||
|   }, |   }, | ||||||
|   error => { |   error => { | ||||||
| @@ -49,6 +49,12 @@ http.interceptors.response.use( | |||||||
|     if (error.response) { |     if (error.response) { | ||||||
|       // 服务器返回错误状态码 |       // 服务器返回错误状态码 | ||||||
|       console.error('API Error:', error.response.status, error.response.data); |       console.error('API Error:', error.response.status, error.response.data); | ||||||
|  |       // 如果是401未授权,可以考虑重定向到登录页 | ||||||
|  |       if (error.response.status === 401) { | ||||||
|  |         // 清除token并重定向到登录页 | ||||||
|  |         localStorage.removeItem('jwt_token'); | ||||||
|  |         window.location.href = '/login'; | ||||||
|  |       } | ||||||
|     } else if (error.request) { |     } else if (error.request) { | ||||||
|       // 请求发出但没有收到响应 |       // 请求发出但没有收到响应 | ||||||
|       console.error('Network Error:', error.message); |       console.error('Network Error:', error.message); | ||||||
|   | |||||||
| @@ -46,19 +46,19 @@ | |||||||
|         :scrollbar-always-on="true" |         :scrollbar-always-on="true" | ||||||
|         @sort-change="handleSortChange"> |         @sort-change="handleSortChange"> | ||||||
|         <el-table-column width="40"></el-table-column> |         <el-table-column width="40"></el-table-column> | ||||||
|         <el-table-column prop="id" label="设备ID" min-width="100" sortable="custom" /> |         <el-table-column prop="id" label="ID" min-width="100" sortable="custom" /> | ||||||
|         <el-table-column prop="name" label="设备名称" min-width="120" sortable="custom" /> |         <el-table-column prop="name" label="名称" min-width="120" sortable="custom" /> | ||||||
|         <el-table-column prop="type" label="设备类型" min-width="100"> |         <el-table-column prop="type" label="类型" min-width="100"> | ||||||
|           <template #default="scope"> |           <template #default="scope"> | ||||||
|             {{ formatDeviceType(scope.row.type) }} |             {{ formatDeviceType(scope.row.type) }} | ||||||
|           </template> |           </template> | ||||||
|         </el-table-column> |         </el-table-column> | ||||||
|         <el-table-column prop="sub_type" label="设备子类型" min-width="100"> |         <el-table-column prop="device_template_name" label="设备模板" min-width="120"> | ||||||
|           <template #default="scope"> |           <template #default="scope"> | ||||||
|             {{ formatDeviceSubType(scope.row.sub_type) }} |             {{ scope.row.type === 'device' ? scope.row.device_template_name || '-' : '-' }} | ||||||
|           </template> |           </template> | ||||||
|         </el-table-column> |         </el-table-column> | ||||||
|         <el-table-column prop="location" label="设备地址描述" min-width="150" /> |         <el-table-column prop="location" label="地址描述" min-width="150" /> | ||||||
|         <el-table-column label="操作" min-width="120" align="center"> |         <el-table-column label="操作" min-width="120" align="center"> | ||||||
|           <template #default="scope"> |           <template #default="scope"> | ||||||
|             <el-button size="small" @click="editDevice(scope.row)">编辑</el-button> |             <el-button size="small" @click="editDevice(scope.row)">编辑</el-button> | ||||||
| @@ -81,8 +81,8 @@ | |||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
| import { Refresh } from '@element-plus/icons-vue'; | import { Refresh } from '@element-plus/icons-vue'; | ||||||
| import deviceService from '../services/deviceService.js'; | import deviceService from '../../services/deviceService.js'; | ||||||
| import DeviceForm from './DeviceForm.vue'; | import DeviceForm from '../../components/DeviceForm.vue'; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|   name: 'DeviceList', |   name: 'DeviceList', | ||||||
| @@ -159,7 +159,11 @@ export default { | |||||||
|       return areaControllers.map(controller => { |       return areaControllers.map(controller => { | ||||||
|         const children = devices.filter(device =>  |         const children = devices.filter(device =>  | ||||||
|           device.type === 'device' && device.parent_id === controller.id |           device.type === 'device' && device.parent_id === controller.id | ||||||
|         ); |         ).map(childDevice => { | ||||||
|  |           // 对于作为子设备的普通设备,确保它们没有 'children' 属性,并显式设置 hasChildren 为 false。 | ||||||
|  |           const { children, ...rest } = childDevice;  | ||||||
|  |           return { ...rest, hasChildren: false }; // 显式添加 hasChildren: false | ||||||
|  |         }); | ||||||
|          |          | ||||||
|         return { |         return { | ||||||
|           ...controller, |           ...controller, | ||||||
| @@ -177,22 +181,9 @@ export default { | |||||||
|       return typeMap[type] || type || '-'; |       return typeMap[type] || type || '-'; | ||||||
|     }, |     }, | ||||||
|      |      | ||||||
|     // 格式化设备子类型显示 |  | ||||||
|     formatDeviceSubType(subType) { |  | ||||||
|       const subTypeMap = { |  | ||||||
|         '': '-', |  | ||||||
|         'temperature': '温度传感器', |  | ||||||
|         'humidity': '湿度传感器', |  | ||||||
|         'ammonia': '氨气传感器', |  | ||||||
|         'feed_valve': '饲料阀门', |  | ||||||
|         'fan': '风扇', |  | ||||||
|         'water_curtain': '水帘' |  | ||||||
|       }; |  | ||||||
|       return subTypeMap[subType] || subType || '-'; |  | ||||||
|     }, |  | ||||||
|      |  | ||||||
|     addDevice() { |     addDevice() { | ||||||
|       this.currentDevice = {}; |       // 默认添加普通设备,如果需要添加区域主控,可以在DeviceForm中选择或通过其他入口触发 | ||||||
|  |       this.currentDevice = { type: 'device' };  | ||||||
|       this.isEdit = false; |       this.isEdit = false; | ||||||
|       this.dialogVisible = true; |       this.dialogVisible = true; | ||||||
|     }, |     }, | ||||||
| @@ -222,7 +213,7 @@ export default { | |||||||
|           type: 'warning' |           type: 'warning' | ||||||
|         }); |         }); | ||||||
|          |          | ||||||
|         await deviceService.deleteDevice(device.id); |         await deviceService.deleteDevice(device); | ||||||
|         this.$message.success('删除成功'); |         this.$message.success('删除成功'); | ||||||
|         await this.loadDevices(); |         await this.loadDevices(); | ||||||
|       } catch (err) { |       } catch (err) { | ||||||
| @@ -240,7 +231,9 @@ export default { | |||||||
|      |      | ||||||
|     tableRowClassName({ row, rowIndex }) { |     tableRowClassName({ row, rowIndex }) { | ||||||
|       if (row.type === 'area_controller') { |       if (row.type === 'area_controller') { | ||||||
|         return 'current-row'; |         return 'is-area-controller-row'; | ||||||
|  |       } else if (row.type === 'device') { | ||||||
|  |         return 'is-device-row'; | ||||||
|       } |       } | ||||||
|       return ''; |       return ''; | ||||||
|     } |     } | ||||||
| @@ -305,10 +298,15 @@ export default { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /* 确保区域主控设备始终高亮显示 */ | /* 确保区域主控设备始终高亮显示 */ | ||||||
| :deep(.current-row) { | :deep(.is-area-controller-row) { | ||||||
|   background-color: #f5f7fa !important; |   background-color: #f5f7fa !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /* 隐藏普通设备行的展开图标 */ | ||||||
|  | :deep(.is-device-row) .el-table__expand-icon { | ||||||
|  |   visibility: hidden; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @media (max-width: 768px) { | @media (max-width: 768px) { | ||||||
|   .device-list { |   .device-list { | ||||||
|     padding: 10px; |     padding: 10px; | ||||||
							
								
								
									
										215
									
								
								src/views/device/DeviceTemplateList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								src/views/device/DeviceTemplateList.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,215 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="device-template-list"> | ||||||
|  |     <el-card> | ||||||
|  |       <template #header> | ||||||
|  |         <div class="card-header"> | ||||||
|  |           <div class="title-container"> | ||||||
|  |             <h2 class="page-title">设备模板管理</h2> | ||||||
|  |             <el-button type="text" @click="loadDeviceTemplates" class="refresh-btn" title="刷新设备模板列表"> | ||||||
|  |               <el-icon :size="20"><Refresh /></el-icon> | ||||||
|  |             </el-button> | ||||||
|  |           </div> | ||||||
|  |           <el-button type="primary" @click="addTemplate">新增模板</el-button> | ||||||
|  |         </div> | ||||||
|  |       </template> | ||||||
|  |        | ||||||
|  |       <!-- 加载状态 --> | ||||||
|  |       <div v-if="loading" class="loading"> | ||||||
|  |         <el-skeleton animated /> | ||||||
|  |       </div> | ||||||
|  |        | ||||||
|  |       <!-- 错误状态 --> | ||||||
|  |       <div v-else-if="error" class="error"> | ||||||
|  |         <el-alert | ||||||
|  |           title="获取设备模板数据失败" | ||||||
|  |           :description="error" | ||||||
|  |           type="error" | ||||||
|  |           show-icon | ||||||
|  |           closable | ||||||
|  |           @close="error = null" | ||||||
|  |         /> | ||||||
|  |         <el-button type="primary" @click="loadDeviceTemplates" class="retry-btn">重新加载</el-button> | ||||||
|  |       </div> | ||||||
|  |        | ||||||
|  |       <!-- 设备模板列表 --> | ||||||
|  |       <el-table  | ||||||
|  |         v-else  | ||||||
|  |         :data="tableData"  | ||||||
|  |         style="width: 100%" | ||||||
|  |         :fit="true" | ||||||
|  |         table-layout="auto" | ||||||
|  |         row-key="id" | ||||||
|  |         :highlight-current-row="false" | ||||||
|  |         :scrollbar-always-on="true" | ||||||
|  |       > | ||||||
|  |         <el-table-column prop="id" label="ID" min-width="80" /> | ||||||
|  |         <el-table-column prop="name" label="名称" min-width="150" /> | ||||||
|  |         <el-table-column prop="manufacturer" label="制造商" min-width="120" /> | ||||||
|  |         <el-table-column prop="description" label="描述" min-width="200" /> | ||||||
|  |         <el-table-column prop="category" label="类别" min-width="100"> | ||||||
|  |           <template #default="scope"> | ||||||
|  |             {{ formatCategory(scope.row.category) }} | ||||||
|  |           </template> | ||||||
|  |         </el-table-column> | ||||||
|  |         <!-- 移除创建时间和更新时间列 --> | ||||||
|  |         <!-- <el-table-column prop="created_at" label="创建时间" min-width="160" /> --> | ||||||
|  |         <!-- <el-table-column prop="updated_at" label="更新时间" min-width="160" /> --> | ||||||
|  |         <el-table-column label="操作" min-width="150" align="center"> | ||||||
|  |           <template #default="scope"> | ||||||
|  |             <el-button size="small" @click="editTemplate(scope.row)">编辑</el-button> | ||||||
|  |             <el-button size="small" type="danger" @click="deleteTemplate(scope.row)">删除</el-button> | ||||||
|  |           </template> | ||||||
|  |         </el-table-column> | ||||||
|  |       </el-table> | ||||||
|  |     </el-card> | ||||||
|  |  | ||||||
|  |     <!-- 设备模板表单对话框 --> | ||||||
|  |     <DeviceTemplateForm  | ||||||
|  |       v-model:visible="dialogVisible"  | ||||||
|  |       :template-data="currentTemplate"  | ||||||
|  |       :is-edit="isEdit" | ||||||
|  |       @success="onTemplateSuccess" | ||||||
|  |       @cancel="dialogVisible = false" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { Refresh } from '@element-plus/icons-vue'; | ||||||
|  | import deviceTemplateService from '../../services/deviceTemplateService.js'; | ||||||
|  | import DeviceTemplateForm from '../../components/DeviceTemplateForm.vue'; | ||||||
|  | import { ElMessage, ElMessageBox } from 'element-plus'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'DeviceTemplateList', | ||||||
|  |   components: { | ||||||
|  |     DeviceTemplateForm, | ||||||
|  |     Refresh | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       tableData: [], | ||||||
|  |       loading: false, | ||||||
|  |       error: null, | ||||||
|  |       dialogVisible: false, | ||||||
|  |       currentTemplate: {}, | ||||||
|  |       isEdit: false | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   async mounted() { | ||||||
|  |     await this.loadDeviceTemplates(); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     async loadDeviceTemplates() { | ||||||
|  |       this.loading = true; | ||||||
|  |       this.error = null; | ||||||
|  |       try { | ||||||
|  |         const response = await deviceTemplateService.getDeviceTemplates(); | ||||||
|  |         // 确保只将数组部分赋值给 tableData | ||||||
|  |         this.tableData = response.data || []; | ||||||
|  |       } catch (err) { | ||||||
|  |         this.error = err.message || '未知错误'; | ||||||
|  |         console.error('加载设备模板列表失败:', err); | ||||||
|  |       } finally { | ||||||
|  |         this.loading = false; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     formatCategory(category) { | ||||||
|  |       const categoryMap = { | ||||||
|  |         'actuator': '执行器', | ||||||
|  |         'sensor': '传感器' | ||||||
|  |       }; | ||||||
|  |       return categoryMap[category] || category || '-'; | ||||||
|  |     }, | ||||||
|  |     addTemplate() { | ||||||
|  |       this.currentTemplate = {}; | ||||||
|  |       this.isEdit = false; | ||||||
|  |       this.dialogVisible = true; | ||||||
|  |     }, | ||||||
|  |     editTemplate(template) { | ||||||
|  |       // 深度拷贝,避免直接修改表格数据 | ||||||
|  |       this.currentTemplate = JSON.parse(JSON.stringify(template)); | ||||||
|  |       this.isEdit = true; | ||||||
|  |       this.dialogVisible = true; | ||||||
|  |     }, | ||||||
|  |     async deleteTemplate(template) { | ||||||
|  |       try { | ||||||
|  |         await ElMessageBox.confirm( | ||||||
|  |           `确认删除设备模板 "${template.name}" 吗?`, | ||||||
|  |           '提示', | ||||||
|  |           { | ||||||
|  |             confirmButtonText: '确定', | ||||||
|  |             cancelButtonText: '取消', | ||||||
|  |             type: 'warning', | ||||||
|  |           } | ||||||
|  |         ); | ||||||
|  |         await deviceTemplateService.deleteDeviceTemplate(template.id); | ||||||
|  |         ElMessage.success('删除成功'); | ||||||
|  |         await this.loadDeviceTemplates(); | ||||||
|  |       } catch (err) { | ||||||
|  |         if (err !== 'cancel') { | ||||||
|  |           ElMessage.error('删除失败: ' + (err.message || '未知错误')); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     onTemplateSuccess() { | ||||||
|  |       ElMessage.success(this.isEdit ? '设备模板更新成功' : '设备模板添加成功'); | ||||||
|  |       this.dialogVisible = false; | ||||||
|  |       this.loadDeviceTemplates(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .device-template-list { | ||||||
|  |   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; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .loading { | ||||||
|  |   padding: 20px 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .error { | ||||||
|  |   padding: 20px 0; | ||||||
|  |   text-align: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .retry-btn { | ||||||
|  |   margin-top: 15px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										117
									
								
								src/views/home/LoginForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src/views/home/LoginForm.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="login-container"> | ||||||
|  |     <el-card class="login-card"> | ||||||
|  |       <template #header> | ||||||
|  |         <div class="card-header"> | ||||||
|  |           <span>系统登录</span> | ||||||
|  |         </div> | ||||||
|  |       </template> | ||||||
|  |       <el-form | ||||||
|  |         ref="loginFormRef" | ||||||
|  |         :model="loginForm" | ||||||
|  |         :rules="loginRules" | ||||||
|  |         label-width="80px" | ||||||
|  |         class="login-form" | ||||||
|  |         @keyup.enter="handleLogin" | ||||||
|  |       > | ||||||
|  |         <el-form-item label="用户名" prop="identifier"> | ||||||
|  |           <el-input v-model="loginForm.identifier" placeholder="请输入用户名/邮箱/手机号"></el-input> | ||||||
|  |         </el-form-item> | ||||||
|  |         <el-form-item label="密码" prop="password"> | ||||||
|  |           <el-input type="password" v-model="loginForm.password" placeholder="请输入密码" show-password></el-input> | ||||||
|  |         </el-form-item> | ||||||
|  |         <el-form-item> | ||||||
|  |           <el-button type="primary" @click="handleLogin" :loading="loading" style="width: 100%;">登录</el-button> | ||||||
|  |         </el-form-item> | ||||||
|  |       </el-form> | ||||||
|  |     </el-card> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { ref, reactive } from 'vue'; | ||||||
|  | import { useRouter } from 'vue-router'; | ||||||
|  | import { ElMessage } from 'element-plus'; | ||||||
|  | import apiClient from '../../api'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'LoginForm', | ||||||
|  |   setup() { | ||||||
|  |     const router = useRouter(); | ||||||
|  |     const loginFormRef = ref(null); | ||||||
|  |     const loading = ref(false); | ||||||
|  |  | ||||||
|  |     const loginForm = reactive({ | ||||||
|  |       identifier: '', | ||||||
|  |       password: '', | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const loginRules = reactive({ | ||||||
|  |       identifier: [ | ||||||
|  |         { required: true, message: '请输入用户名/邮箱/手机号', trigger: 'blur' }, | ||||||
|  |       ], | ||||||
|  |       password: [ | ||||||
|  |         { required: true, message: '请输入密码', trigger: 'blur' }, | ||||||
|  |       ], | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const handleLogin = async () => { | ||||||
|  |       if (!loginFormRef.value) return; | ||||||
|  |       loginFormRef.value.validate(async (valid) => { | ||||||
|  |         if (valid) { | ||||||
|  |           loading.value = true; | ||||||
|  |           try { | ||||||
|  |             const response = await apiClient.users.login(loginForm); | ||||||
|  |             if (response.code === 2000 && response.data && response.data.token) { | ||||||
|  |               localStorage.setItem('jwt_token', response.data.token); | ||||||
|  |               localStorage.setItem('username', response.data.username); // 存储用户名 | ||||||
|  |               ElMessage.success('登录成功!'); | ||||||
|  |               router.push('/'); // 登录成功后跳转到首页 | ||||||
|  |             } else { | ||||||
|  |               ElMessage.error(response.message || '登录失败,请检查用户名或密码!'); | ||||||
|  |             } | ||||||
|  |           } catch (error) { | ||||||
|  |             console.error('登录请求失败:', error); | ||||||
|  |             ElMessage.error('登录请求失败,请稍后再试!'); | ||||||
|  |           } finally { | ||||||
|  |             loading.value = false; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       loginFormRef, | ||||||
|  |       loginForm, | ||||||
|  |       loginRules, | ||||||
|  |       loading, | ||||||
|  |       handleLogin, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .login-container { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: center; | ||||||
|  |   align-items: center; | ||||||
|  |   min-height: 100vh; | ||||||
|  |   background-color: #f0f2f5; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .login-card { | ||||||
|  |   width: 400px; | ||||||
|  |   max-width: 90%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .card-header { | ||||||
|  |   text-align: center; | ||||||
|  |   font-size: 1.2em; | ||||||
|  |   font-weight: bold; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .login-form { | ||||||
|  |   padding: 20px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										72
									
								
								src/views/monitor/DeviceCommandLogView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/views/monitor/DeviceCommandLogView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="device-command-log-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |         :fetchData="fetchDeviceCommandLogs" | ||||||
|  |         :columnsConfig="deviceCommandLogColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import {getDeviceCommandLogs} from '../../api/monitor.js'; | ||||||
|  | import {formatRFC3339} from '../../utils/format.js'; // 导入全局格式化函数 | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchDeviceCommandLogs = async (params) => { | ||||||
|  |   // 调用真实的 API 函数 | ||||||
|  |   // getDeviceCommandLogs 需要返回一个 { list: [], total: 0 } 格式的对象 | ||||||
|  |   return await getDeviceCommandLogs(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列 | ||||||
|  | const deviceCommandLogColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '设备ID', | ||||||
|  |     dataIndex: 'device_id', | ||||||
|  |     key: 'device_id', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'number', // 设置筛选类型为数字 | ||||||
|  |     minWidth: 100, // 设置最小宽度 | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '消息ID', | ||||||
|  |     dataIndex: 'message_id', | ||||||
|  |     key: 'message_id', | ||||||
|  |     // 后端不支持此字段筛选,暂时移除 | ||||||
|  |     // filterType: 'text',  | ||||||
|  |     minWidth: 300, // UUID较长,设置较宽的最小宽度 | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '发送时间', | ||||||
|  |     dataIndex: 'sent_at', | ||||||
|  |     key: 'sent_at', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', // 设置筛选类型为日期范围 | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), // 使用全局格式化函数 | ||||||
|  |     minWidth: 180, // 设置最小宽度 | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '接收成功', | ||||||
|  |     dataIndex: 'received_success', | ||||||
|  |     key: 'received_success', | ||||||
|  |     filterType: 'boolean', // 设置筛选类型为布尔值 | ||||||
|  |     formatter: (row, column, cellValue) => (cellValue ? '是' : '否'), | ||||||
|  |     minWidth: 100, // 设置最小宽度 | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '确认时间', | ||||||
|  |     dataIndex: 'acknowledged_at', | ||||||
|  |     key: 'acknowledged_at', | ||||||
|  |     sorter: true, | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), // 使用全局格式化函数 | ||||||
|  |     minWidth: 180, // 设置最小宽度 | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .device-command-log-view { | ||||||
|  |   /* 视图容器样式 */ | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										92
									
								
								src/views/monitor/FeedUsageRecordsView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/views/monitor/FeedUsageRecordsView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="feed-usage-record-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchFeedUsageRecords" | ||||||
|  |       :columnsConfig="feedUsageRecordColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getFeedUsageRecords } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchFeedUsageRecords = async (params) => { | ||||||
|  |   return await getFeedUsageRecords(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列配置 | ||||||
|  | // 根据 swagger, 支持的筛选参数有: feed_formula_id, operator_id, pen_id, start_time, end_time | ||||||
|  | const feedUsageRecordColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '记录ID', | ||||||
|  |     dataIndex: 'id', | ||||||
|  |     key: 'id', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '用量(kg)', | ||||||
|  |     dataIndex: 'amount', | ||||||
|  |     key: 'amount', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '饲料配方', | ||||||
|  |     dataIndex: ['feed_formula', 'name'], // 访问嵌套属性 | ||||||
|  |     key: 'feed_formula_name', | ||||||
|  |     minWidth: 150, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '配方ID (筛选)', | ||||||
|  |     dataIndex: 'feed_formula_id', | ||||||
|  |     key: 'feed_formula_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     minWidth: 150, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '猪栏', | ||||||
|  |     dataIndex: ['pen', 'name'], // 访问嵌套属性 | ||||||
|  |     key: 'pen_name', | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '猪栏ID (筛选)', | ||||||
|  |     dataIndex: 'pen_id', | ||||||
|  |     key: 'pen_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     minWidth: 150, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '操作员ID', | ||||||
|  |     dataIndex: 'operator_id', | ||||||
|  |     key: 'operator_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '记录时间', | ||||||
|  |     dataIndex: 'recorded_at', | ||||||
|  |     key: 'recorded_at', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '备注', | ||||||
|  |     dataIndex: 'remarks', | ||||||
|  |     key: 'remarks', | ||||||
|  |     minWidth: 200, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .feed-usage-record-view { | ||||||
|  |   /* 视图容器样式 */ | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										94
									
								
								src/views/monitor/MedicationLogsView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/views/monitor/MedicationLogsView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="medication-logs-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchMedicationLogs" | ||||||
|  |       :columnsConfig="medicationLogColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getMedicationLogs } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchMedicationLogs = async (params) => { | ||||||
|  |   return await getMedicationLogs(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列,依据 swagger.json | ||||||
|  | const medicationLogColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '记录ID', | ||||||
|  |     dataIndex: 'id', | ||||||
|  |     key: 'id', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '药品名称', | ||||||
|  |     dataIndex: ['medication', 'name'], // 嵌套属性 | ||||||
|  |     key: 'medication_name', | ||||||
|  |     minWidth: 150, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '用量', | ||||||
|  |     dataIndex: 'dosage_used', | ||||||
|  |     key: 'dosage_used', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '目标数量', | ||||||
|  |     dataIndex: 'target_count', | ||||||
|  |     key: 'target_count', | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '原因', | ||||||
|  |     dataIndex: 'reason', | ||||||
|  |     key: 'reason', | ||||||
|  |     filterType: 'select', | ||||||
|  |     filterOptions: [ | ||||||
|  |       { text: '预防', value: '预防' }, | ||||||
|  |       { text: '治疗', value: '治疗' }, | ||||||
|  |       { text: '保健', value: '保健' }, | ||||||
|  |     ], | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '发生时间', | ||||||
|  |     dataIndex: 'happened_at', | ||||||
|  |     key: 'happened_at', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '猪批次ID', | ||||||
|  |     dataIndex: 'pig_batch_id', | ||||||
|  |     key: 'pig_batch_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '操作员ID', | ||||||
|  |     dataIndex: 'operator_id', | ||||||
|  |     key: 'operator_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '描述', | ||||||
|  |     dataIndex: 'description', | ||||||
|  |     key: 'description', | ||||||
|  |     minWidth: 200, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 视图容器样式 */ | ||||||
|  | </style> | ||||||
							
								
								
									
										111
									
								
								src/views/monitor/NotificationLogView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								src/views/monitor/NotificationLogView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="notification-log-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchNotifications" | ||||||
|  |       :columnsConfig="notificationLogColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getNotifications, ZapcoreLevel } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchNotifications = async (params) => { | ||||||
|  |   return await getNotifications(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列 | ||||||
|  | const notificationLogColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '用户ID', | ||||||
|  |     dataIndex: 'user_id', | ||||||
|  |     key: 'user_id', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'number', | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '通知渠道', | ||||||
|  |     dataIndex: 'notifier_type', | ||||||
|  |     key: 'notifier_type', | ||||||
|  |     filterType: 'select', | ||||||
|  |     filterOptions: [ | ||||||
|  |       { value: '邮件', text: '邮件' }, | ||||||
|  |       { value: '企业微信', text: '企业微信' }, | ||||||
|  |       { value: '飞书', text: '飞书' }, | ||||||
|  |       { value: '日志', text: '日志' }, | ||||||
|  |     ], | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '目标地址', | ||||||
|  |     dataIndex: 'to_address', | ||||||
|  |     key: 'to_address', | ||||||
|  |     minWidth: 200, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '标题', | ||||||
|  |     dataIndex: 'title', | ||||||
|  |     key: 'title', | ||||||
|  |     minWidth: 200, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '消息', | ||||||
|  |     dataIndex: 'message', | ||||||
|  |     key: 'message', | ||||||
|  |     minWidth: 300, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '日志级别', | ||||||
|  |     dataIndex: 'level', | ||||||
|  |     key: 'level', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'select', | ||||||
|  |     filterOptions: Object.entries(ZapcoreLevel).map(([text, value]) => ({ value, text })), | ||||||
|  |     minWidth: 110, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '告警时间', | ||||||
|  |     dataIndex: 'alarm_timestamp', | ||||||
|  |     key: 'alarm_timestamp', | ||||||
|  |     sorter: true, | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '状态', | ||||||
|  |     dataIndex: 'status', | ||||||
|  |     key: 'status', | ||||||
|  |     filterType: 'select', | ||||||
|  |     filterOptions: [ | ||||||
|  |       { value: '发送成功', text: '发送成功' }, | ||||||
|  |       { value: '发送失败', text: '发送失败' }, | ||||||
|  |       { value: '已跳过', text: '已跳过' }, | ||||||
|  |     ], | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '错误信息', | ||||||
|  |     dataIndex: 'error_message', | ||||||
|  |     key: 'error_message', | ||||||
|  |     minWidth: 200, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '创建时间', | ||||||
|  |     dataIndex: 'created_at', | ||||||
|  |     key: 'created_at', | ||||||
|  |     sorter: true, | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .notification-log-view { | ||||||
|  |   /* 视图容器样式 */ | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										70
									
								
								src/views/monitor/PendingCollectionsView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/views/monitor/PendingCollectionsView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="pending-collections-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchPendingCollections" | ||||||
|  |       :columnsConfig="pendingCollectionColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getPendingCollections } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchPendingCollections = async (params) => { | ||||||
|  |   return await getPendingCollections(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列,依据 swagger.json | ||||||
|  | const pendingCollectionColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '关联ID', | ||||||
|  |     dataIndex: 'correlation_id', | ||||||
|  |     key: 'correlation_id', | ||||||
|  |     minWidth: 300, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '设备ID', | ||||||
|  |     dataIndex: 'device_id', | ||||||
|  |     key: 'device_id', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'number', | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '状态', | ||||||
|  |     dataIndex: 'status', | ||||||
|  |     key: 'status', | ||||||
|  |     filterType: 'select', | ||||||
|  |     filterOptions: [ | ||||||
|  |       { text: '等待中', value: '等待中' }, | ||||||
|  |       { text: '已完成', value: '已完成' }, | ||||||
|  |       { text: '已超时', value: '已超时' }, | ||||||
|  |     ], | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '创建时间', | ||||||
|  |     dataIndex: 'created_at', | ||||||
|  |     key: 'created_at', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '完成时间', | ||||||
|  |     dataIndex: 'fulfilled_at', | ||||||
|  |     key: 'fulfilled_at', | ||||||
|  |     sorter: true, | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 视图容器样式 */ | ||||||
|  | </style> | ||||||
							
								
								
									
										99
									
								
								src/views/monitor/PigBatchLogsView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/views/monitor/PigBatchLogsView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="pig-batch-logs-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchPigBatchLogs" | ||||||
|  |       :columnsConfig="pigBatchLogColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getPigBatchLogs } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchPigBatchLogs = async (params) => { | ||||||
|  |   return await getPigBatchLogs(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列,依据 swagger.json | ||||||
|  | const pigBatchLogColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '记录ID', | ||||||
|  |     dataIndex: 'id', | ||||||
|  |     key: 'id', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '猪批次ID', | ||||||
|  |     dataIndex: 'pig_batch_id', | ||||||
|  |     key: 'pig_batch_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '变更类型', | ||||||
|  |     dataIndex: 'change_type', | ||||||
|  |     key: 'change_type', | ||||||
|  |     filterType: 'select', | ||||||
|  |     filterOptions: [ | ||||||
|  |       { text: '死亡', value: '死亡' }, | ||||||
|  |       { text: '淘汰', value: '淘汰' }, | ||||||
|  |       { text: '销售', value: '销售' }, | ||||||
|  |       { text: '购买', value: '购买' }, | ||||||
|  |       { text: '转入', value: '转入' }, | ||||||
|  |       { text: '转出', value: '转出' }, | ||||||
|  |       { text: '盘点校正', value: '盘点校正' }, | ||||||
|  |     ], | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '变更数量', | ||||||
|  |     dataIndex: 'change_count', | ||||||
|  |     key: 'change_count', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '变更前数量', | ||||||
|  |     dataIndex: 'before_count', | ||||||
|  |     key: 'before_count', | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '变更后数量', | ||||||
|  |     dataIndex: 'after_count', | ||||||
|  |     key: 'after_count', | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '发生时间', | ||||||
|  |     dataIndex: 'happened_at', | ||||||
|  |     key: 'happened_at', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '操作员ID', | ||||||
|  |     dataIndex: 'operator_id', | ||||||
|  |     key: 'operator_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '原因', | ||||||
|  |     dataIndex: 'reason', | ||||||
|  |     key: 'reason', | ||||||
|  |     minWidth: 200, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 视图容器样式 */ | ||||||
|  | </style> | ||||||
							
								
								
									
										92
									
								
								src/views/monitor/PigPurchasesView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/views/monitor/PigPurchasesView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="pig-purchases-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchPigPurchases" | ||||||
|  |       :columnsConfig="pigPurchaseColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getPigPurchases } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchPigPurchases = async (params) => { | ||||||
|  |   return await getPigPurchases(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列,依据 swagger.json | ||||||
|  | const pigPurchaseColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '采购ID', | ||||||
|  |     dataIndex: 'id', | ||||||
|  |     key: 'id', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '猪批次ID', | ||||||
|  |     dataIndex: 'pig_batch_id', | ||||||
|  |     key: 'pig_batch_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '供应商', | ||||||
|  |     dataIndex: 'supplier', | ||||||
|  |     key: 'supplier', | ||||||
|  |     filterType: 'text', | ||||||
|  |     minWidth: 150, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '采购数量', | ||||||
|  |     dataIndex: 'quantity', | ||||||
|  |     key: 'quantity', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '单价', | ||||||
|  |     dataIndex: 'unit_price', | ||||||
|  |     key: 'unit_price', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '总价', | ||||||
|  |     dataIndex: 'total_price', | ||||||
|  |     key: 'total_price', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '采购日期', | ||||||
|  |     dataIndex: 'purchase_date', | ||||||
|  |     key: 'purchase_date', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '操作员ID', | ||||||
|  |     dataIndex: 'operator_id', | ||||||
|  |     key: 'operator_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '备注', | ||||||
|  |     dataIndex: 'remarks', | ||||||
|  |     key: 'remarks', | ||||||
|  |     minWidth: 200, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 视图容器样式 */ | ||||||
|  | </style> | ||||||
							
								
								
									
										92
									
								
								src/views/monitor/PigSalesView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/views/monitor/PigSalesView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="pig-sales-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchPigSales" | ||||||
|  |       :columnsConfig="pigSaleColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getPigSales } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchPigSales = async (params) => { | ||||||
|  |   return await getPigSales(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列,依据 swagger.json | ||||||
|  | const pigSaleColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '售卖ID', | ||||||
|  |     dataIndex: 'id', | ||||||
|  |     key: 'id', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '猪批次ID', | ||||||
|  |     dataIndex: 'pig_batch_id', | ||||||
|  |     key: 'pig_batch_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '买方', | ||||||
|  |     dataIndex: 'buyer', | ||||||
|  |     key: 'buyer', | ||||||
|  |     filterType: 'text', | ||||||
|  |     minWidth: 150, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '售卖数量', | ||||||
|  |     dataIndex: 'quantity', | ||||||
|  |     key: 'quantity', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '单价', | ||||||
|  |     dataIndex: 'unit_price', | ||||||
|  |     key: 'unit_price', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '总价', | ||||||
|  |     dataIndex: 'total_price', | ||||||
|  |     key: 'total_price', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '售卖日期', | ||||||
|  |     dataIndex: 'sale_date', | ||||||
|  |     key: 'sale_date', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '操作员ID', | ||||||
|  |     dataIndex: 'operator_id', | ||||||
|  |     key: 'operator_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '备注', | ||||||
|  |     dataIndex: 'remarks', | ||||||
|  |     key: 'remarks', | ||||||
|  |     minWidth: 200, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 视图容器样式 */ | ||||||
|  | </style> | ||||||
							
								
								
									
										106
									
								
								src/views/monitor/PigSickLogsView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/views/monitor/PigSickLogsView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="pig-sick-logs-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchPigSickLogs" | ||||||
|  |       :columnsConfig="pigSickLogColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getPigSickLogs } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchPigSickLogs = async (params) => { | ||||||
|  |   return await getPigSickLogs(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列,依据 swagger.json | ||||||
|  | const pigSickLogColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '记录ID', | ||||||
|  |     dataIndex: 'id', | ||||||
|  |     key: 'id', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '猪批次ID', | ||||||
|  |     dataIndex: 'pig_batch_id', | ||||||
|  |     key: 'pig_batch_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '猪栏ID', | ||||||
|  |     dataIndex: 'pen_id', | ||||||
|  |     key: 'pen_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '原因', | ||||||
|  |     dataIndex: 'reason', | ||||||
|  |     key: 'reason', | ||||||
|  |     filterType: 'select', | ||||||
|  |     filterOptions: [ | ||||||
|  |       { text: '患病', value: '患病' }, | ||||||
|  |       { text: '康复', value: '康复' }, | ||||||
|  |       { text: '死亡', value: '死亡' }, | ||||||
|  |       { text: '淘汰', value: '淘汰' }, | ||||||
|  |       { text: '转入', value: '转入' }, | ||||||
|  |       { text: '转出', value: '转出' }, | ||||||
|  |       { text: '其他', value: '其他' }, | ||||||
|  |     ], | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '治疗地点', | ||||||
|  |     dataIndex: 'treatment_location', | ||||||
|  |     key: 'treatment_location', | ||||||
|  |     filterType: 'select', | ||||||
|  |     filterOptions: [ | ||||||
|  |         { text: '原地治疗', value: '原地治疗' }, | ||||||
|  |         { text: '病猪栏治疗', value: '病猪栏治疗' }, | ||||||
|  |     ], | ||||||
|  |     minWidth: 130, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '变更数量', | ||||||
|  |     dataIndex: 'change_count', | ||||||
|  |     key: 'change_count', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '发生时间', | ||||||
|  |     dataIndex: 'happened_at', | ||||||
|  |     key: 'happened_at', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '操作员ID', | ||||||
|  |     dataIndex: 'operator_id', | ||||||
|  |     key: 'operator_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '备注', | ||||||
|  |     dataIndex: 'remarks', | ||||||
|  |     key: 'remarks', | ||||||
|  |     minWidth: 200, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 视图容器样式 */ | ||||||
|  | </style> | ||||||
							
								
								
									
										102
									
								
								src/views/monitor/PigTransferLogsView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/views/monitor/PigTransferLogsView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="pig-transfer-logs-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchPigTransferLogs" | ||||||
|  |       :columnsConfig="pigTransferLogColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getPigTransferLogs } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchPigTransferLogs = async (params) => { | ||||||
|  |   return await getPigTransferLogs(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列,依据 swagger.json | ||||||
|  | const pigTransferLogColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '记录ID', | ||||||
|  |     dataIndex: 'id', | ||||||
|  |     key: 'id', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '猪批次ID', | ||||||
|  |     dataIndex: 'pig_batch_id', | ||||||
|  |     key: 'pig_batch_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '猪栏ID', | ||||||
|  |     dataIndex: 'pen_id', | ||||||
|  |     key: 'pen_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '迁移类型', | ||||||
|  |     dataIndex: 'type', | ||||||
|  |     key: 'type', | ||||||
|  |     filterType: 'select', | ||||||
|  |     filterOptions: [ | ||||||
|  |       { text: '群内调栏', value: '群内调栏' }, | ||||||
|  |       { text: '跨群调栏', value: '跨群调栏' }, | ||||||
|  |       { text: '销售', value: '销售' }, | ||||||
|  |       { text: '死亡', value: '死亡' }, | ||||||
|  |       { text: '淘汰', value: '淘汰' }, | ||||||
|  |       { text: '新购入', value: '新购入' }, | ||||||
|  |       { text: '产房转入', value: '产房转入' }, | ||||||
|  |     ], | ||||||
|  |     minWidth: 130, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '数量', | ||||||
|  |     dataIndex: 'quantity', | ||||||
|  |     key: 'quantity', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '迁移时间', | ||||||
|  |     dataIndex: 'transfer_time', | ||||||
|  |     key: 'transfer_time', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '操作员ID', | ||||||
|  |     dataIndex: 'operator_id', | ||||||
|  |     key: 'operator_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '关联ID', | ||||||
|  |     dataIndex: 'correlation_id', | ||||||
|  |     key: 'correlation_id', | ||||||
|  |     filterType: 'text', | ||||||
|  |     minWidth: 280, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '备注', | ||||||
|  |     dataIndex: 'remarks', | ||||||
|  |     key: 'remarks', | ||||||
|  |     minWidth: 200, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 视图容器样式 */ | ||||||
|  | </style> | ||||||
							
								
								
									
										85
									
								
								src/views/monitor/PlanExecutionLogsView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/views/monitor/PlanExecutionLogsView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="plan-execution-logs-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchPlanExecutionLogs" | ||||||
|  |       :columnsConfig="planExecutionLogColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getPlanExecutionLogs } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchPlanExecutionLogs = async (params) => { | ||||||
|  |   return await getPlanExecutionLogs(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列,依据 swagger.json | ||||||
|  | const planExecutionLogColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '记录ID', | ||||||
|  |     dataIndex: 'id', | ||||||
|  |     key: 'id', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '计划ID', | ||||||
|  |     dataIndex: 'plan_id', | ||||||
|  |     key: 'plan_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '计划名称', | ||||||
|  |     dataIndex: 'plan_name', | ||||||
|  |     key: 'plan_name', | ||||||
|  |     minWidth: 150, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '状态', | ||||||
|  |     dataIndex: 'status', | ||||||
|  |     key: 'status', | ||||||
|  |     filterType: 'select', | ||||||
|  |     filterOptions: [ | ||||||
|  |       { text: '已开始', value: '已开始' }, | ||||||
|  |       { text: '已完成', value: '已完成' }, | ||||||
|  |       { text: '失败', value: '失败' }, | ||||||
|  |       { text: '已取消', value: '已取消' }, | ||||||
|  |       { text: '等待中', value: '等待中' }, | ||||||
|  |     ], | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '开始时间', | ||||||
|  |     dataIndex: 'started_at', | ||||||
|  |     key: 'started_at', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '结束时间', | ||||||
|  |     dataIndex: 'ended_at', | ||||||
|  |     key: 'ended_at', | ||||||
|  |     sorter: true, | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '错误信息', | ||||||
|  |     dataIndex: 'error', | ||||||
|  |     key: 'error', | ||||||
|  |     minWidth: 250, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 视图容器样式 */ | ||||||
|  | </style> | ||||||
							
								
								
									
										85
									
								
								src/views/monitor/RawMaterialPurchasesView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/views/monitor/RawMaterialPurchasesView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="raw-material-purchases-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchRawMaterialPurchases" | ||||||
|  |       :columnsConfig="rawMaterialPurchaseColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getRawMaterialPurchases } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchRawMaterialPurchases = async (params) => { | ||||||
|  |   return await getRawMaterialPurchases(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列,依据 swagger.json | ||||||
|  | const rawMaterialPurchaseColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '采购ID', | ||||||
|  |     dataIndex: 'id', | ||||||
|  |     key: 'id', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '原料名称', | ||||||
|  |     dataIndex: ['raw_material', 'name'], // 嵌套属性 | ||||||
|  |     key: 'raw_material_name', | ||||||
|  |     minWidth: 150, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '原料ID', | ||||||
|  |     dataIndex: 'raw_material_id', | ||||||
|  |     key: 'raw_material_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '供应商', | ||||||
|  |     dataIndex: 'supplier', | ||||||
|  |     key: 'supplier', | ||||||
|  |     filterType: 'text', | ||||||
|  |     minWidth: 150, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '数量', | ||||||
|  |     dataIndex: 'amount', | ||||||
|  |     key: 'amount', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '单价', | ||||||
|  |     dataIndex: 'unit_price', | ||||||
|  |     key: 'unit_price', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '总价', | ||||||
|  |     dataIndex: 'total_price', | ||||||
|  |     key: 'total_price', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '采购日期', | ||||||
|  |     dataIndex: 'purchase_date', | ||||||
|  |     key: 'purchase_date', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 视图容器样式 */ | ||||||
|  | </style> | ||||||
							
								
								
									
										87
									
								
								src/views/monitor/RawMaterialStockLogsView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/views/monitor/RawMaterialStockLogsView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="raw-material-stock-logs-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchRawMaterialStockLogs" | ||||||
|  |       :columnsConfig="rawMaterialStockLogColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getRawMaterialStockLogs } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchRawMaterialStockLogs = async (params) => { | ||||||
|  |   return await getRawMaterialStockLogs(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列,依据 swagger.json | ||||||
|  | const rawMaterialStockLogColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '记录ID', | ||||||
|  |     dataIndex: 'id', | ||||||
|  |     key: 'id', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '原料ID', | ||||||
|  |     dataIndex: 'raw_material_id', | ||||||
|  |     key: 'raw_material_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '来源类型', | ||||||
|  |     dataIndex: 'source_type', | ||||||
|  |     key: 'source_type', | ||||||
|  |     filterType: 'select', | ||||||
|  |     filterOptions: [ | ||||||
|  |       { text: '采购入库', value: '采购入库' }, | ||||||
|  |       { text: '饲喂出库', value: '饲喂出库' }, | ||||||
|  |       { text: '变质出库', value: '变质出库' }, | ||||||
|  |       { text: '售卖出库', value: '售卖出库' }, | ||||||
|  |       { text: '杂用领取', value: '杂用领取' }, | ||||||
|  |       { text: '手动盘点', value: '手动盘点' }, | ||||||
|  |     ], | ||||||
|  |     minWidth: 130, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '来源ID', | ||||||
|  |     dataIndex: 'source_id', | ||||||
|  |     key: 'source_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '变更数量', | ||||||
|  |     dataIndex: 'change_amount', | ||||||
|  |     key: 'change_amount', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '发生时间', | ||||||
|  |     dataIndex: 'happened_at', | ||||||
|  |     key: 'happened_at', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '备注', | ||||||
|  |     dataIndex: 'remarks', | ||||||
|  |     key: 'remarks', | ||||||
|  |     minWidth: 200, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 视图容器样式 */ | ||||||
|  | </style> | ||||||
							
								
								
									
										71
									
								
								src/views/monitor/SensorDataView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/views/monitor/SensorDataView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="sensor-data-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchSensorData" | ||||||
|  |       :columnsConfig="sensorDataColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getSensorData } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchSensorData = async (params) => { | ||||||
|  |   return await getSensorData(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列,依据 swagger.json | ||||||
|  | const sensorDataColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '设备ID', | ||||||
|  |     dataIndex: 'device_id', | ||||||
|  |     key: 'device_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '传感器类型', | ||||||
|  |     dataIndex: 'sensor_type', | ||||||
|  |     key: 'sensor_type', | ||||||
|  |     filterType: 'select', | ||||||
|  |     filterOptions: [ | ||||||
|  |       { text: '信号强度', value: '信号强度' }, | ||||||
|  |       { text: '电池电量', value: '电池电量' }, | ||||||
|  |       { text: '温度', value: '温度' }, | ||||||
|  |       { text: '湿度', value: '湿度' }, | ||||||
|  |       { text: '重量', value: '重量' }, | ||||||
|  |     ], | ||||||
|  |     minWidth: 150, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '数据', | ||||||
|  |     dataIndex: 'data', | ||||||
|  |     key: 'data', | ||||||
|  |     // 移除 formatter,由 GenericMonitorList 自动处理对象和数组显示 | ||||||
|  |     minWidth: 150, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '采集时间', | ||||||
|  |     dataIndex: 'time', | ||||||
|  |     key: 'time', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '区域主控ID', | ||||||
|  |     dataIndex: 'regional_controller_id', | ||||||
|  |     key: 'regional_controller_id', | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 视图容器样式 */ | ||||||
|  | </style> | ||||||
							
								
								
									
										93
									
								
								src/views/monitor/TaskExecutionLogsView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/views/monitor/TaskExecutionLogsView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="task-execution-logs-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchTaskExecutionLogs" | ||||||
|  |       :columnsConfig="taskExecutionLogColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getTaskExecutionLogs } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchTaskExecutionLogs = async (params) => { | ||||||
|  |   return await getTaskExecutionLogs(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列,依据 swagger.json | ||||||
|  | const taskExecutionLogColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '记录ID', | ||||||
|  |     dataIndex: 'id', | ||||||
|  |     key: 'id', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '任务ID', | ||||||
|  |     dataIndex: 'task_id', | ||||||
|  |     key: 'task_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '任务名称', | ||||||
|  |     dataIndex: ['task', 'name'], // 嵌套属性 | ||||||
|  |     key: 'task_name', | ||||||
|  |     minWidth: 150, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '计划执行ID', | ||||||
|  |     dataIndex: 'plan_execution_log_id', | ||||||
|  |     key: 'plan_execution_log_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '状态', | ||||||
|  |     dataIndex: 'status', | ||||||
|  |     key: 'status', | ||||||
|  |     filterType: 'select', | ||||||
|  |     filterOptions: [ | ||||||
|  |       { text: '已开始', value: '已开始' }, | ||||||
|  |       { text: '已完成', value: '已完成' }, | ||||||
|  |       { text: '失败', value: '失败' }, | ||||||
|  |       { text: '已取消', value: '已取消' }, | ||||||
|  |       { text: '等待中', value: '等待中' }, | ||||||
|  |     ], | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '开始时间', | ||||||
|  |     dataIndex: 'started_at', | ||||||
|  |     key: 'started_at', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '结束时间', | ||||||
|  |     dataIndex: 'ended_at', | ||||||
|  |     key: 'ended_at', | ||||||
|  |     sorter: true, | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '输出', | ||||||
|  |     dataIndex: 'output', | ||||||
|  |     key: 'output', | ||||||
|  |     minWidth: 250, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 视图容器样式 */ | ||||||
|  | </style> | ||||||
							
								
								
									
										100
									
								
								src/views/monitor/UserActionLogsView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/views/monitor/UserActionLogsView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="user-action-logs-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchUserActionLogs" | ||||||
|  |       :columnsConfig="userActionLogColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getUserActionLogs } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchUserActionLogs = async (params) => { | ||||||
|  |   return await getUserActionLogs(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列,依据 swagger.json | ||||||
|  | const userActionLogColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '记录ID', | ||||||
|  |     dataIndex: 'id', | ||||||
|  |     key: 'id', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '用户名', | ||||||
|  |     dataIndex: 'username', | ||||||
|  |     key: 'username', | ||||||
|  |     filterType: 'text', | ||||||
|  |     minWidth: 130, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '用户ID', | ||||||
|  |     dataIndex: 'user_id', | ||||||
|  |     key: 'user_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '操作类型', | ||||||
|  |     dataIndex: 'action_type', | ||||||
|  |     key: 'action_type', | ||||||
|  |     filterType: 'text', | ||||||
|  |     minWidth: 130, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '状态', | ||||||
|  |     dataIndex: 'status', | ||||||
|  |     key: 'status', | ||||||
|  |     filterType: 'select', | ||||||
|  |     filterOptions: [ | ||||||
|  |       { text: '成功', value: '成功' }, | ||||||
|  |       { text: '失败', value: '失败' }, | ||||||
|  |     ], | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: 'HTTP方法', | ||||||
|  |     dataIndex: 'http_method', | ||||||
|  |     key: 'http_method', | ||||||
|  |     minWidth: 110, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: 'HTTP路径', | ||||||
|  |     dataIndex: 'http_path', | ||||||
|  |     key: 'http_path', | ||||||
|  |     minWidth: 250, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '来源IP', | ||||||
|  |     dataIndex: 'source_ip', | ||||||
|  |     key: 'source_ip', | ||||||
|  |     minWidth: 150, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '操作时间', | ||||||
|  |     dataIndex: 'time', | ||||||
|  |     key: 'time', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '描述', | ||||||
|  |     dataIndex: 'description', | ||||||
|  |     key: 'description', | ||||||
|  |     minWidth: 250, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 视图容器样式 */ | ||||||
|  | </style> | ||||||
							
								
								
									
										65
									
								
								src/views/monitor/WeighingBatchesView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/views/monitor/WeighingBatchesView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="weighing-batches-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchWeighingBatches" | ||||||
|  |       :columnsConfig="weighingBatchColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getWeighingBatches } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchWeighingBatches = async (params) => { | ||||||
|  |   return await getWeighingBatches(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列,依据 swagger.json | ||||||
|  | const weighingBatchColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '记录ID', | ||||||
|  |     dataIndex: 'id', | ||||||
|  |     key: 'id', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '猪批次ID', | ||||||
|  |     dataIndex: 'pig_batch_id', | ||||||
|  |     key: 'pig_batch_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '称重时间', | ||||||
|  |     dataIndex: 'weighing_time', | ||||||
|  |     key: 'weighing_time', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '描述', | ||||||
|  |     dataIndex: 'description', | ||||||
|  |     key: 'description', | ||||||
|  |     minWidth: 250, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '创建时间', | ||||||
|  |     dataIndex: 'created_at', | ||||||
|  |     key: 'created_at', | ||||||
|  |     sorter: true, | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 视图容器样式 */ | ||||||
|  | </style> | ||||||
							
								
								
									
										79
									
								
								src/views/monitor/WeighingRecordsView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/views/monitor/WeighingRecordsView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="weighing-records-view"> | ||||||
|  |     <GenericMonitorList | ||||||
|  |       :fetchData="fetchWeighingRecords" | ||||||
|  |       :columnsConfig="weighingRecordColumns" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import GenericMonitorList from '../../components/GenericMonitorList.vue'; | ||||||
|  | import { getWeighingRecords } from '../../api/monitor.js'; | ||||||
|  | import { formatRFC3339 } from '../../utils/format.js'; | ||||||
|  |  | ||||||
|  | // 适配通用组件的 fetchData prop | ||||||
|  | const fetchWeighingRecords = async (params) => { | ||||||
|  |   return await getWeighingRecords(params); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // 定义表格的列,依据 swagger.json | ||||||
|  | const weighingRecordColumns = [ | ||||||
|  |   { | ||||||
|  |     title: '记录ID', | ||||||
|  |     dataIndex: 'id', | ||||||
|  |     key: 'id', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '批次称重ID', | ||||||
|  |     dataIndex: 'weighing_batch_id', | ||||||
|  |     key: 'weighing_batch_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '猪栏ID', | ||||||
|  |     dataIndex: 'pen_id', | ||||||
|  |     key: 'pen_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '重量', | ||||||
|  |     dataIndex: 'weight', | ||||||
|  |     key: 'weight', | ||||||
|  |     sorter: true, | ||||||
|  |     minWidth: 100, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '称重时间', | ||||||
|  |     dataIndex: 'weighing_time', | ||||||
|  |     key: 'weighing_time', | ||||||
|  |     sorter: true, | ||||||
|  |     filterType: 'dateRange', | ||||||
|  |     formatter: (row, column, cellValue) => formatRFC3339(cellValue), | ||||||
|  |     minWidth: 180, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '操作员ID', | ||||||
|  |     dataIndex: 'operator_id', | ||||||
|  |     key: 'operator_id', | ||||||
|  |     filterType: 'number', | ||||||
|  |     minWidth: 120, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     title: '备注', | ||||||
|  |     dataIndex: 'remark', | ||||||
|  |     key: 'remark', | ||||||
|  |     minWidth: 200, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 视图容器样式 */ | ||||||
|  | </style> | ||||||
| @@ -1,39 +1,426 @@ | |||||||
| <template> | <template> | ||||||
|   <el-table :data="planList"> |   <div class="plan-list"> | ||||||
|     <el-table-column prop="executeType" label="执行类型"> |     <el-card> | ||||||
|       <template #default="{ row }"> |       <template #header> | ||||||
|         <el-tag |         <div class="card-header"> | ||||||
|           v-if="row.executeType === 'manual'" |           <div class="title-container"> | ||||||
|           type="" |             <h2 class="page-title">计划管理</h2> | ||||||
|           class="full-width-tag" |             <el-button type="text" @click="loadPlans" class="refresh-btn" title="刷新计划列表"> | ||||||
|         > |               <el-icon :size="20"> | ||||||
|           手动 |                 <Refresh/> | ||||||
|         </el-tag> |               </el-icon> | ||||||
|         <el-tag |             </el-button> | ||||||
|           v-else-if="row.executeType === 'auto-infinite'" |           </div> | ||||||
|           type="success" |  | ||||||
|           class="full-width-tag" |           <div class="filter-and-add"> | ||||||
|         > |             <el-select v-model="planTypeFilter" placeholder="选择计划类型" @change="loadPlans" | ||||||
|           自动(无限执行) |                        style="width: 150px; margin-right: 10px;"> | ||||||
|         </el-tag> |               <el-option label="所有任务" value="所有任务"></el-option> | ||||||
|         <el-tag |               <el-option label="自定义任务" value="自定义任务"></el-option> | ||||||
|           v-else-if="row.executeType === 'auto-limited'" |               <el-option label="系统任务" value="系统任务"></el-option> | ||||||
|           type="warning" |             </el-select> | ||||||
|           class="full-width-tag" |             <el-button type="primary" @click="addPlan">添加计划</el-button> | ||||||
|         > |           </div> | ||||||
|           自动({{ row.executeTimes }}次) |         </div> | ||||||
|         </el-tag> |  | ||||||
|       </template> |       </template> | ||||||
|     </el-table-column> |  | ||||||
|   </el-table> |       <!-- 加载状态 --> | ||||||
|  |       <div v-if="loading" class="loading"> | ||||||
|  |         <el-skeleton animated/> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <!-- 错误状态 --> | ||||||
|  |       <div v-else-if="error" class="error"> | ||||||
|  |         <el-alert | ||||||
|  |             title="获取计划数据失败" | ||||||
|  |             :description="error" | ||||||
|  |             type="error" | ||||||
|  |             show-icon | ||||||
|  |             closable | ||||||
|  |             @close="error = null" | ||||||
|  |         /> | ||||||
|  |         <el-button type="primary" @click="loadPlans" class="retry-btn">重新加载</el-button> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <el-table | ||||||
|  |           v-else | ||||||
|  |           :data="plans" | ||||||
|  |           style="width: 100%" | ||||||
|  |           class="plan-list-table" | ||||||
|  |           :fit="true" | ||||||
|  |           :scrollbar-always-on="true" | ||||||
|  |           @sort-change="handleSortChange"> | ||||||
|  |         <el-table-column prop="id" label="计划ID" min-width="100" sortable="custom"/> | ||||||
|  |         <el-table-column prop="name" label="计划名称" min-width="120" sortable="custom"/> | ||||||
|  |         <el-table-column prop="description" label="计划描述" min-width="150"/> | ||||||
|  |         <el-table-column prop="execution_type" label="执行类型" min-width="150" sortable="custom"> | ||||||
|  |           <template #default="scope"> | ||||||
|  |             <el-tag v-if="scope.row.execution_type === '手动'">手动</el-tag> | ||||||
|  |             <el-tag v-else-if="scope.row.execute_num === 0" type="success">自动(无限执行)</el-tag> | ||||||
|  |             <el-tag v-else type="warning">自动({{ scope.row.execute_num }}次)</el-tag> | ||||||
|  |           </template> | ||||||
|  |         </el-table-column> | ||||||
|  |         <el-table-column prop="execute_count" label="已执行次数" min-width="120" sortable="custom"/> | ||||||
|  |         <el-table-column prop="status" label="状态" min-width="100" sortable="custom"> | ||||||
|  |           <template #default="scope"> | ||||||
|  |             <el-tag v-if="scope.row.status === 0" type="danger">禁用计划</el-tag> | ||||||
|  |             <el-tag v-else-if="scope.row.status === 1" type="success">启用计划</el-tag> | ||||||
|  |             <el-tag v-else-if="scope.row.status === 3" type="danger">执行失败</el-tag> | ||||||
|  |             <el-tag v-else type="info">执行完毕</el-tag> | ||||||
|  |           </template> | ||||||
|  |         </el-table-column> | ||||||
|  |         <el-table-column prop="cron_expression" label="下次执行时间" min-width="150" sortable="custom"> | ||||||
|  |           <template #default="scope"> | ||||||
|  |             {{ formatNextExecutionTime(scope.row.cron_expression) }} | ||||||
|  |           </template> | ||||||
|  |         </el-table-column> | ||||||
|  |         <el-table-column label="操作" width="280"> | ||||||
|  |           <template #default="scope"> | ||||||
|  |             <el-button size="small" @click="editPlan(scope.row)" :disabled="scope.row.plan_type === '系统任务'">编辑 | ||||||
|  |             </el-button> | ||||||
|  |             <el-button size="small" @click="showDetails(scope.row)">详情</el-button> | ||||||
|  |             <el-button | ||||||
|  |                 size="small" | ||||||
|  |                 :type="scope.row.status === 1 ? 'warning' : 'primary'" | ||||||
|  |                 @click="scope.row.status === 1 ? stopPlan(scope.row) : startPlan(scope.row)" | ||||||
|  |                 :loading="stoppingPlanId === scope.row.id || startingPlanId === scope.row.id" | ||||||
|  |                 :disabled="scope.row.plan_type === '系统任务'" | ||||||
|  |             > | ||||||
|  |               {{ scope.row.status === 1 ? '停止' : '启动' }} | ||||||
|  |             </el-button> | ||||||
|  |             <el-button size="small" type="danger" @click="deletePlan(scope.row)" | ||||||
|  |                        :disabled="scope.row.plan_type === '系统任务'">删除 | ||||||
|  |             </el-button> | ||||||
|  |           </template> | ||||||
|  |         </el-table-column> | ||||||
|  |       </el-table> | ||||||
|  |  | ||||||
|  |     </el-card> | ||||||
|  |  | ||||||
|  |     <!-- 计划表单 --> | ||||||
|  |     <PlanForm | ||||||
|  |         v-model:visible="dialogVisible" | ||||||
|  |         :plan-data="currentPlan" | ||||||
|  |         :is-edit="isEdit" | ||||||
|  |         @success="handlePlanSuccess" | ||||||
|  |         @cancel="handlePlanCancel" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <!-- 计划详情 --> | ||||||
|  |     <el-dialog | ||||||
|  |         v-model="detailsVisible" | ||||||
|  |         title="计划详情" | ||||||
|  |         width="70%" | ||||||
|  |         top="5vh" | ||||||
|  |     > | ||||||
|  |       <plan-detail | ||||||
|  |           v-if="detailsVisible" | ||||||
|  |           :plan-id="selectedPlanIdForDetails" | ||||||
|  |       /> | ||||||
|  |       <template #footer> | ||||||
|  |         <span class="dialog-footer"> | ||||||
|  |           <el-button @click="detailsVisible = false">关闭</el-button> | ||||||
|  |         </span> | ||||||
|  |       </template> | ||||||
|  |     </el-dialog> | ||||||
|  |  | ||||||
|  |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import {Refresh} from '@element-plus/icons-vue'; | ||||||
|  | import apiClient from '../../api'; | ||||||
|  | import PlanForm from '../../components/PlanForm.vue'; | ||||||
|  | import PlanDetail from '../../components/PlanDetail.vue'; // 导入新组件 | ||||||
|  | import cronParser from 'cron-parser'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'PlanList', | ||||||
|  |   components: { | ||||||
|  |     PlanForm, | ||||||
|  |     PlanDetail, // 注册新组件 | ||||||
|  |     Refresh | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       plans: [], | ||||||
|  |       originalPlans: [], // Store the original unsorted list | ||||||
|  |       dialogVisible: false, | ||||||
|  |       detailsVisible: false, // 控制详情对话框 | ||||||
|  |       isEdit: false, | ||||||
|  |       loading: false, | ||||||
|  |       error: null, | ||||||
|  |       currentPlan: { | ||||||
|  |         id: null, | ||||||
|  |         name: '', | ||||||
|  |         description: '', | ||||||
|  |         execution_type: 'automatic', | ||||||
|  |         execute_num: 0, | ||||||
|  |         cron_expression: '' | ||||||
|  |       }, | ||||||
|  |       selectedPlanIdForDetails: null, // 当前要查看详情的计划ID | ||||||
|  |       startingPlanId: null, | ||||||
|  |       stoppingPlanId: null, | ||||||
|  |       planTypeFilter: '自定义任务', // 新增:计划类型筛选,默认自定义任务 | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   async mounted() { | ||||||
|  |     await this.loadPlans(); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     // 加载计划列表 | ||||||
|  |     async loadPlans() { | ||||||
|  |       this.loading = true; | ||||||
|  |       this.error = null; | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         const response = await apiClient.plans.getPlans({plan_type: this.planTypeFilter, page: 1, page_size: 1000}); // 传递 plan_typeFilter | ||||||
|  |         let fetchedPlans = response.data?.plans || []; | ||||||
|  |         // Default sort by ID ascending | ||||||
|  |         fetchedPlans.sort((a, b) => a.id - b.id); | ||||||
|  |         this.plans = fetchedPlans; | ||||||
|  |         this.originalPlans = [...this.plans]; // Keep a copy of the original order | ||||||
|  |       } catch (err) { | ||||||
|  |         this.error = err.message || '未知错误'; | ||||||
|  |         console.error('加载计划列表失败:', err); | ||||||
|  |       } finally { | ||||||
|  |         this.loading = false; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     // 处理表格排序 | ||||||
|  |     handleSortChange({prop, order}) { | ||||||
|  |       if (!order) { | ||||||
|  |         // 恢复原始顺序 | ||||||
|  |         this.plans = [...this.originalPlans]; | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const sortFactor = order === 'ascending' ? 1 : -1; | ||||||
|  |  | ||||||
|  |       this.plans.sort((a, b) => { | ||||||
|  |         let valA = a[prop]; | ||||||
|  |         let valB = b[prop]; | ||||||
|  |  | ||||||
|  |         // '下次执行时间' 列的特殊排序逻辑 | ||||||
|  |         if (prop === 'cron_expression') { | ||||||
|  |           try { | ||||||
|  |             const parser = cronParser.default || cronParser; | ||||||
|  |             valA = valA ? parser.parse(valA).next().toDate().getTime() : 0; | ||||||
|  |           } catch (e) { | ||||||
|  |             valA = 0; // 无效的 cron 表达式排在前面 | ||||||
|  |           } | ||||||
|  |           try { | ||||||
|  |             const parser = cronParser.default || cronParser; | ||||||
|  |             valB = valB ? parser.parse(valB).next().toDate().getTime() : 0; | ||||||
|  |           } catch (e) { | ||||||
|  |             valB = 0; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (valA < valB) { | ||||||
|  |           return -1 * sortFactor; | ||||||
|  |         } | ||||||
|  |         if (valA > valB) { | ||||||
|  |           return 1 * sortFactor; | ||||||
|  |         } | ||||||
|  |         return 0; | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     // 格式化下次执行时间 | ||||||
|  |     formatNextExecutionTime(cronExpression) { | ||||||
|  |       if (!cronExpression) { | ||||||
|  |         return '-'; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         // 正确使用cron-parser库 | ||||||
|  |         const parser = cronParser.default || cronParser; | ||||||
|  |         const interval = parser.parse(cronExpression); | ||||||
|  |         const next = interval.next().toDate(); | ||||||
|  |         return next.toLocaleString('zh-CN'); | ||||||
|  |       } catch (err) { | ||||||
|  |         console.error('解析cron表达式失败:', err); | ||||||
|  |         return '无效的表达式'; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     addPlan() { | ||||||
|  |       this.currentPlan = { | ||||||
|  |         id: null, | ||||||
|  |         name: '', | ||||||
|  |         description: '', | ||||||
|  |         execution_type: 'automatic', | ||||||
|  |         execute_num: 0, | ||||||
|  |         cron_expression: '' | ||||||
|  |       }; | ||||||
|  |       this.isEdit = false; | ||||||
|  |       this.dialogVisible = true; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     showDetails(plan) { | ||||||
|  |       this.selectedPlanIdForDetails = plan.id; | ||||||
|  |       this.detailsVisible = true; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     editPlan(plan) { | ||||||
|  |       this.currentPlan = {...plan}; | ||||||
|  |       this.isEdit = true; | ||||||
|  |       this.dialogVisible = true; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     async deletePlan(plan) { | ||||||
|  |       try { | ||||||
|  |         await this.$confirm('确认删除该计划吗?', '提示', { | ||||||
|  |           confirmButtonText: '确定', | ||||||
|  |           cancelButtonText: '取消', | ||||||
|  |           type: 'warning' | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         await apiClient.plans.deletePlan(plan.id); | ||||||
|  |         this.$message.success('删除成功'); | ||||||
|  |         await this.loadPlans(); | ||||||
|  |       } catch (err) { | ||||||
|  |         if (err !== 'cancel') { | ||||||
|  |           this.$message.error('删除失败: ' + (err.message || '未知错误')); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     async startPlan(plan) { | ||||||
|  |       try { | ||||||
|  |         this.startingPlanId = plan.id; | ||||||
|  |         await apiClient.plans.startPlan(plan.id); | ||||||
|  |         this.$message.success('计划启动成功'); | ||||||
|  |         await this.loadPlans(); | ||||||
|  |       } catch (err) { | ||||||
|  |         this.$message.error('启动失败: ' + (err.message || '未知错误')); | ||||||
|  |       } finally { | ||||||
|  |         this.startingPlanId = null; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     async stopPlan(plan) { | ||||||
|  |       try { | ||||||
|  |         this.stoppingPlanId = plan.id; | ||||||
|  |         await apiClient.plans.stopPlan(plan.id); | ||||||
|  |         this.$message.success('计划停止成功'); | ||||||
|  |         await this.loadPlans(); | ||||||
|  |       } catch (err) { | ||||||
|  |         this.$message.error('停止失败: ' + (err.message || '未知错误')); | ||||||
|  |       } finally { | ||||||
|  |         this.stoppingPlanId = null; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     // 处理计划表单提交成功 | ||||||
|  |     async handlePlanSuccess(planData) { | ||||||
|  |       try { | ||||||
|  |         if (this.isEdit) { | ||||||
|  |           // 编辑计划 | ||||||
|  |           await apiClient.plans.updatePlan(planData.id, planData); | ||||||
|  |           this.$message.success('计划更新成功'); | ||||||
|  |         } else { | ||||||
|  |           // 添加新计划 | ||||||
|  |           const planRequest = { | ||||||
|  |             ...planData, | ||||||
|  |             content_type: 'tasks' // 默认使用任务类型 | ||||||
|  |           }; | ||||||
|  |  | ||||||
|  |           await apiClient.plans.createPlan(planRequest); | ||||||
|  |           this.$message.success('计划添加成功'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await this.loadPlans(); | ||||||
|  |       } catch (err) { | ||||||
|  |         this.$message.error('保存失败: ' + (err.message || '未知错误')); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     // 处理计划表单取消 | ||||||
|  |     handlePlanCancel() { | ||||||
|  |       this.dialogVisible = false; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     // 监听 planTypeFilter 变化,重新加载计划列表 | ||||||
|  |     planTypeFilter: 'loadPlans' | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
| <style scoped> | <style scoped> | ||||||
| .full-width-tag { | .plan-list { | ||||||
|   /* 移除默认的宽度限制和文本溢出处理 */ |   padding: 20px; | ||||||
|   max-width: none !important; |   max-width: 1200px; | ||||||
|   overflow: visible !important; |   margin: 0 auto; | ||||||
|   white-space: normal !important; | } | ||||||
|   word-wrap: break-word !important; |  | ||||||
|  | .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; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .loading { | ||||||
|  |   padding: 20px 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .error { | ||||||
|  |   padding: 20px 0; | ||||||
|  |   text-align: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .retry-btn { | ||||||
|  |   margin-top: 15px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .plan-list-table :deep(tbody)::after { | ||||||
|  |   content: ""; | ||||||
|  |   display: block; | ||||||
|  |   height: 20px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .filter-and-add { | ||||||
|  |   display: flex; | ||||||
|  |   align-items: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media (max-width: 768px) { | ||||||
|  |   .plan-list { | ||||||
|  |     padding: 10px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .card-header { | ||||||
|  |     flex-direction: column; | ||||||
|  |     gap: 15px; | ||||||
|  |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
							
								
								
									
										385
									
								
								src/views/pms/PigBatchManagementView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										385
									
								
								src/views/pms/PigBatchManagementView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,385 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="pig-batch-management"> | ||||||
|  |     <el-card> | ||||||
|  |       <template #header> | ||||||
|  |         <div class="card-header"> | ||||||
|  |           <div class="title-container"> | ||||||
|  |             <h2 class="page-title">猪群管理</h2> | ||||||
|  |             <el-button type="text" @click="loadData" class="refresh-btn" title="刷新列表"> | ||||||
|  |               <el-icon :size="20"><Refresh /></el-icon> | ||||||
|  |             </el-button> | ||||||
|  |           </div> | ||||||
|  |           <el-button type="primary" @click="handleAddBatch">添加猪群</el-button> | ||||||
|  |         </div> | ||||||
|  |       </template> | ||||||
|  |  | ||||||
|  |       <!-- 加载状态 --> | ||||||
|  |       <div v-if="loading" class="loading"> | ||||||
|  |         <el-skeleton animated /> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <!-- 错误状态 --> | ||||||
|  |       <div v-else-if="error" class="error"> | ||||||
|  |         <el-alert | ||||||
|  |           title="获取猪群数据失败" | ||||||
|  |           :description="error" | ||||||
|  |           type="error" | ||||||
|  |           show-icon | ||||||
|  |           closable | ||||||
|  |           @close="error = null" | ||||||
|  |         /> | ||||||
|  |         <el-button type="primary" @click="loadData" class="retry-btn">重新加载</el-button> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <!-- 数据展示或空状态 --> | ||||||
|  |       <div v-else> | ||||||
|  |         <PigBatchList  | ||||||
|  |           v-if="pigBatchesData.length > 0"  | ||||||
|  |           :pigBatches="pigBatchesData"  | ||||||
|  |           @edit-batch="handleEditBatch"  | ||||||
|  |           @delete-batch="handleDeleteBatch"  | ||||||
|  |           @add-pen="handleAddPen" | ||||||
|  |           @modify-pig-count-pen="handleModifyPigCountPen" | ||||||
|  |           @remove-pen="handleRemovePen" | ||||||
|  |           @assign-pen-to-batch="handleAssignPenToBatch" | ||||||
|  |           @transfer-pigs="handleTransferPigs" | ||||||
|  |           @transfer-pigs-across-batches="handleTransferPigsAcrossBatches" | ||||||
|  |           @reload-data="loadData" | ||||||
|  |         /> | ||||||
|  |         <el-empty v-else description="暂无数据" /> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |     </el-card> | ||||||
|  |  | ||||||
|  |     <!-- 猪群表单对话框 --> | ||||||
|  |     <PigBatchForm | ||||||
|  |       v-model:visible="batchDialogVisible" | ||||||
|  |       :batch-data="currentBatch" | ||||||
|  |       :is-edit="isEditBatch" | ||||||
|  |       @success="handleBatchSuccess" | ||||||
|  |       @cancel="batchDialogVisible = false" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <!-- 猪栏表单对话框 (用于在猪群管理中添加/编辑猪栏) --> | ||||||
|  |     <PenForm  | ||||||
|  |       v-model:visible="penDialogVisible" | ||||||
|  |       :pen-data="currentPen" | ||||||
|  |       :is-edit="isEditPen" | ||||||
|  |       @success="handlePenSuccess" | ||||||
|  |       @cancel="penDialogVisible = false" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <!-- 群内调栏对话框 --> | ||||||
|  |     <TransferPigsModal | ||||||
|  |       v-if="transferDialogVisible" | ||||||
|  |       v-model:visible="transferDialogVisible" | ||||||
|  |       :batch="currentBatchForTransfer" | ||||||
|  |       @success="handleTransferSuccess" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <!-- 跨群调栏对话框 --> | ||||||
|  |     <TransferPigsAcrossBatchesModal | ||||||
|  |       v-model:visible="transferAcrossBatchesDialogVisible" | ||||||
|  |       :batch="currentBatchForTransferAcrossBatches" | ||||||
|  |       @success="handleTransferAcrossBatchesSuccess" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { getPigBatches, deletePigBatch, assignPensToBatch, removePenFromBatch } from '@/api/pigBatch.js'; | ||||||
|  | import { getPens } from '@/api/pen.js'; | ||||||
|  | import { getPigHouses } from '@/api/pigHouse.js'; | ||||||
|  | import PigBatchList from '@/components/PigBatchList.vue'; | ||||||
|  | import PigBatchForm from '@/components/PigBatchForm.vue'; | ||||||
|  | import PenForm from '@/components/PenForm.vue'; | ||||||
|  | import TransferPigsModal from '@/components/TransferPigsModal.vue'; | ||||||
|  | import TransferPigsAcrossBatchesModal from '@/components/TransferPigsAcrossBatchesModal.vue'; | ||||||
|  | import { Refresh } from '@element-plus/icons-vue'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'PigBatchManagementView', | ||||||
|  |   components: { | ||||||
|  |     PigBatchList, | ||||||
|  |     PigBatchForm, | ||||||
|  |     PenForm, | ||||||
|  |     TransferPigsModal, | ||||||
|  |     TransferPigsAcrossBatchesModal, | ||||||
|  |     Refresh | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       pigBatchesData: [], | ||||||
|  |       loading: false, | ||||||
|  |       error: null, | ||||||
|  |       // 猪群表单状态 | ||||||
|  |       batchDialogVisible: false, | ||||||
|  |       isEditBatch: false, | ||||||
|  |       currentBatch: {}, | ||||||
|  |       // 猪栏表单状态 | ||||||
|  |       penDialogVisible: false, | ||||||
|  |       isEditPen: false, | ||||||
|  |       currentPen: {}, | ||||||
|  |       // 调栏模态框状态 | ||||||
|  |       transferDialogVisible: false, | ||||||
|  |       currentBatchForTransfer: {}, | ||||||
|  |       // 跨群调栏模态框状态 | ||||||
|  |       transferAcrossBatchesDialogVisible: false, | ||||||
|  |       currentBatchForTransferAcrossBatches: {}, | ||||||
|  |       // 辅助映射 | ||||||
|  |       houseMap: new Map(), // 用于猪栏显示猪舍名称 | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   async mounted() { | ||||||
|  |     await this.loadData(); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     async loadData() { | ||||||
|  |       this.loading = true; | ||||||
|  |       this.error = null; | ||||||
|  |       try { | ||||||
|  |         const [batchesResponse, pensResponse, housesResponse] = await Promise.all([ | ||||||
|  |           getPigBatches(), | ||||||
|  |           getPens(), | ||||||
|  |           getPigHouses(), | ||||||
|  |         ]); | ||||||
|  |          | ||||||
|  |         const batches = Array.isArray(batchesResponse.data) ? batchesResponse.data : []; | ||||||
|  |         const pens = Array.isArray(pensResponse.data) ? pensResponse.data : []; | ||||||
|  |         const houses = Array.isArray(housesResponse.data) ? housesResponse.data : []; | ||||||
|  |  | ||||||
|  |         // 更新 batchMap 和 houseMap | ||||||
|  |         this.houseMap = new Map(houses.map(h => [h.id, h.name])); | ||||||
|  |         this.batchMap = new Map(batches.map(batch => [batch.id, batch.batch_number])); | ||||||
|  |         this.pigBatchesData = this.assembleData(batches, pens); | ||||||
|  |  | ||||||
|  |       } catch (err) { | ||||||
|  |         this.error = err.message || '无法获取数据'; | ||||||
|  |         console.error('Failed to fetch pig batch data:', err); | ||||||
|  |       } finally { | ||||||
|  |         this.loading = false; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     assembleData(batches, pens) { | ||||||
|  |       const batchMap = new Map(); | ||||||
|  |       batches.forEach(batch => { | ||||||
|  |         const existingBatch = this.pigBatchesData.find(b => b.id === batch.id); | ||||||
|  |         batchMap.set(batch.id, { ...batch, pens: [], isExpanded: existingBatch ? existingBatch.isExpanded : false }); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       for (const pen of pens) { | ||||||
|  |         if (pen.pig_batch_id && batchMap.has(pen.pig_batch_id)) { | ||||||
|  |           const penWithDetails = { | ||||||
|  |             ...pen, | ||||||
|  |             house_name: this.houseMap.get(pen.house_id) || '未知猪舍', | ||||||
|  |             batch_number: batchMap.get(pen.pig_batch_id)?.batch_number || null | ||||||
|  |           }; | ||||||
|  |           batchMap.get(pen.pig_batch_id).pens.push(penWithDetails); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       // Calculate unassigned_pig_count for each batch using currentTotalQuantity and currentTotalPigsInPens | ||||||
|  |       const assembledBatches = Array.from(batchMap.values()); | ||||||
|  |       assembledBatches.forEach(batch => { | ||||||
|  |         // Use currentTotalQuantity and currentTotalPigsInPens directly from the batch object | ||||||
|  |         batch.unassigned_pig_count = (batch.currentTotalQuantity || 0) - (batch.currentTotalPigsInPens || 0); | ||||||
|  |       }); | ||||||
|  |       return assembledBatches; | ||||||
|  |     }, | ||||||
|  |     // --- 猪群操作 --- | ||||||
|  |     handleAddBatch() { | ||||||
|  |       this.currentBatch = {}; | ||||||
|  |       this.isEditBatch = false; | ||||||
|  |       this.batchDialogVisible = true; | ||||||
|  |     }, | ||||||
|  |     handleEditBatch(batch) { | ||||||
|  |       this.currentBatch = { ...batch }; | ||||||
|  |       this.isEditBatch = true; | ||||||
|  |       this.batchDialogVisible = true; | ||||||
|  |     }, | ||||||
|  |     async handleDeleteBatch(batch) { | ||||||
|  |       try { | ||||||
|  |         await this.$confirm(`确认删除猪群 "${batch.batch_number}" 吗?`, '提示', { | ||||||
|  |           confirmButtonText: '确定', | ||||||
|  |           cancelButtonText: '取消', | ||||||
|  |           type: 'warning' | ||||||
|  |         }); | ||||||
|  |         await deletePigBatch(batch.id); | ||||||
|  |         this.$message.success('删除成功'); | ||||||
|  |         // 本地更新数据 | ||||||
|  |         this.pigBatchesData = this.pigBatchesData.filter(b => b.id !== batch.id); | ||||||
|  |       } catch (err) { | ||||||
|  |         if (err !== 'cancel') { | ||||||
|  |           this.$message.error('删除失败: ' + (err.message || '未知错误')); | ||||||
|  |           console.error('Failed to delete pig batch:', err); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     handleBatchSuccess(newBatchData) { | ||||||
|  |       this.batchDialogVisible = false; | ||||||
|  |       // 为新批次数据添加 pens 和 isExpanded 状态 | ||||||
|  |       const processedBatchData = { ...newBatchData, pens: [], isExpanded: false }; | ||||||
|  |  | ||||||
|  |       if (this.isEditBatch) { | ||||||
|  |         // 更新现有猪群 | ||||||
|  |         const index = this.pigBatchesData.findIndex(b => b.id === processedBatchData.id); | ||||||
|  |         if (index !== -1) { | ||||||
|  |           // 保留 pens 和 isExpanded 状态 | ||||||
|  |           const existingBatch = this.pigBatchesData[index]; | ||||||
|  |           this.pigBatchesData[index] = { ...processedBatchData, pens: existingBatch.pens, isExpanded: existingBatch.isExpanded }; | ||||||
|  |         } | ||||||
|  |         this.$message.success('猪群更新成功'); | ||||||
|  |       } else { | ||||||
|  |         // 添加新猪群 | ||||||
|  |         this.pigBatchesData.push(processedBatchData); | ||||||
|  |         this.$message.success('猪群添加成功'); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     // --- 猪栏操作 (在猪群管理中) --- | ||||||
|  |     handleAddPen(house) { | ||||||
|  |       this.currentPen = { ...house }; // 修正:这里应该是传入house对象,而不是pen对象 | ||||||
|  |       this.isEditPen = false; | ||||||
|  |       this.penDialogVisible = true; | ||||||
|  |     }, | ||||||
|  |     handleModifyPigCountPen(pen) { | ||||||
|  |       this.currentPen = { ...pen }; | ||||||
|  |       this.isEditPen = true; | ||||||
|  |       this.penDialogVisible = true; | ||||||
|  |     }, | ||||||
|  |     async handleRemovePen(pen) { | ||||||
|  |       try { | ||||||
|  |         await this.$confirm(`确认将猪栏 "${pen.pen_number}" 从猪群中移除吗?`, '提示', { | ||||||
|  |           confirmButtonText: '确定', | ||||||
|  |           cancelButtonText: '取消', | ||||||
|  |           type: 'warning' | ||||||
|  |         }); | ||||||
|  |         await removePenFromBatch(pen.id, pen.pig_batch_id); | ||||||
|  |         this.$message.success('猪栏已成功从猪群中移除'); | ||||||
|  |         await this.loadData(); // Refresh data to show updated state | ||||||
|  |       } catch (err) { | ||||||
|  |         if (err !== 'cancel') { | ||||||
|  |           this.$message.error('移除失败: ' + (err.message || '未知错误')); | ||||||
|  |           console.error('Failed to remove pen from batch:', err); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     handlePenSuccess(newPenData) { | ||||||
|  |       this.penDialogVisible = false; | ||||||
|  |       // 为新猪栏数据添加 house_name 和 batch_number | ||||||
|  |       const penWithDetails = { | ||||||
|  |         ...newPenData, | ||||||
|  |         house_name: this.houseMap.get(newPenData.house_id) || '未知猪舍', | ||||||
|  |         batch_number: this.pigBatchesData.find(b => b.id === newPenData.pig_batch_id)?.batch_number || null | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       const batch = this.pigBatchesData.find(b => b.id === penWithDetails.pig_batch_id); | ||||||
|  |       if (batch) { | ||||||
|  |         if (this.isEditPen) { | ||||||
|  |           // 更新现有猪栏 | ||||||
|  |           const index = batch.pens.findIndex(p => p.id === penWithDetails.id); | ||||||
|  |           if (index !== -1) { | ||||||
|  |             batch.pens[index] = penWithDetails; | ||||||
|  |           } | ||||||
|  |           this.$message.success('猪栏更新成功'); | ||||||
|  |         } else { | ||||||
|  |           // 添加新猪栏 | ||||||
|  |           batch.pens.push(penWithDetails); | ||||||
|  |           this.$message.success('猪栏添加成功'); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     async handleAssignPenToBatch({ batchId, penId }) { | ||||||
|  |       try { | ||||||
|  |         await assignPensToBatch(batchId, { penIds: [penId] }); | ||||||
|  |         this.$message.success('猪栏分配成功'); | ||||||
|  |         await this.loadData(); // Refresh data to show assigned pen | ||||||
|  |       } catch (error) { | ||||||
|  |         this.$message.error('分配猪栏失败: ' + (error.response?.data?.message || error.message || '未知错误')); | ||||||
|  |         console.error('Failed to assign pen to batch:', error); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     // --- 调栏操作 --- | ||||||
|  |     handleTransferPigs(batch) { | ||||||
|  |       this.currentBatchForTransfer = batch; | ||||||
|  |       this.transferDialogVisible = true; | ||||||
|  |     }, | ||||||
|  |     handleTransferSuccess() { | ||||||
|  |       this.transferDialogVisible = false; | ||||||
|  |       this.loadData(); // 重新加载数据以反映变化 | ||||||
|  |     }, | ||||||
|  |     handleTransferPigsAcrossBatches(batch) { | ||||||
|  |       console.log('handleTransferPigsAcrossBatches called with batch:', batch); | ||||||
|  |       this.currentBatchForTransferAcrossBatches = batch; | ||||||
|  |       this.transferAcrossBatchesDialogVisible = true; | ||||||
|  |       console.log('transferAcrossBatchesDialogVisible set to:', this.transferAcrossBatchesDialogVisible); | ||||||
|  |     }, | ||||||
|  |     handleTransferAcrossBatchesSuccess() { | ||||||
|  |       this.transferAcrossBatchesDialogVisible = false; | ||||||
|  |       this.loadData(); // 重新加载数据以反映变化 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* 样式与 PigFarmManagementView 保持一致 */ | ||||||
|  | .pig-batch-management { | ||||||
|  |   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; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .loading { | ||||||
|  |   padding: 20px 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .error { | ||||||
|  |   padding: 20px 0; | ||||||
|  |   text-align: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .retry-btn { | ||||||
|  |   margin-top: 15px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media (max-width: 768px) { | ||||||
|  |   .pig-batch-management { | ||||||
|  |     padding: 10px; | ||||||
|  |   } | ||||||
|  |   .card-header { | ||||||
|  |     flex-direction: column; | ||||||
|  |     gap: 15px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										330
									
								
								src/views/pms/PigFarmManagementView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										330
									
								
								src/views/pms/PigFarmManagementView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,330 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="pig-farm-management"> | ||||||
|  |     <el-card> | ||||||
|  |       <template #header> | ||||||
|  |         <div class="card-header"> | ||||||
|  |           <div class="title-container"> | ||||||
|  |             <h2 class="page-title">栏舍管理</h2> | ||||||
|  |             <el-button type="text" @click="loadData" class="refresh-btn" title="刷新列表"> | ||||||
|  |               <el-icon :size="20"> | ||||||
|  |                 <Refresh/> | ||||||
|  |               </el-icon> | ||||||
|  |             </el-button> | ||||||
|  |           </div> | ||||||
|  |           <el-button type="primary" @click="handleAddHouse">添加猪舍</el-button> | ||||||
|  |         </div> | ||||||
|  |       </template> | ||||||
|  |  | ||||||
|  |       <!-- 加载状态 --> | ||||||
|  |       <div v-if="loading" class="loading"> | ||||||
|  |         <el-skeleton animated/> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <!-- 错误状态 --> | ||||||
|  |       <div v-else-if="error" class="error"> | ||||||
|  |         <el-alert | ||||||
|  |             title="获取栏舍数据失败" | ||||||
|  |             :description="error" | ||||||
|  |             type="error" | ||||||
|  |             show-icon | ||||||
|  |             closable | ||||||
|  |             @close="error = null" | ||||||
|  |         /> | ||||||
|  |         <el-button type="primary" @click="loadData" class="retry-btn">重新加载</el-button> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <!-- 数据展示或空状态 --> | ||||||
|  |       <div v-else> | ||||||
|  |         <PigHouseList | ||||||
|  |             v-if="pigHousesData.length > 0" | ||||||
|  |             :pigHouses="pigHousesData" | ||||||
|  |             @edit-house="handleEditHouse" | ||||||
|  |             @delete-house="handleDeleteHouse" | ||||||
|  |             @add-pen="handleAddPen" | ||||||
|  |             @edit-pen="handleEditPen" | ||||||
|  |             @delete-pen="handleDeletePen" | ||||||
|  |             @toggle-house-expand="handleToggleHouseExpand" | ||||||
|  |         /> | ||||||
|  |         <el-empty v-else description="暂无数据"/> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |     </el-card> | ||||||
|  |  | ||||||
|  |     <!-- 猪舍表单对话框 --> | ||||||
|  |     <PigHouseForm | ||||||
|  |         v-model:visible="houseDialogVisible" | ||||||
|  |         :house-data="currentHouse" | ||||||
|  |         :is-edit="isEditHouse" | ||||||
|  |         @success="handleHouseSuccess" | ||||||
|  |         @cancel="houseDialogVisible = false" | ||||||
|  |     /> | ||||||
|  |  | ||||||
|  |     <!-- 猪栏表单对话框 --> | ||||||
|  |     <PenForm | ||||||
|  |         v-model:visible="penDialogVisible" | ||||||
|  |         :pen-data="currentPen" | ||||||
|  |         :is-edit="isEditPen" | ||||||
|  |         @success="handlePenSuccess" | ||||||
|  |         @cancel="penDialogVisible = false" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import {getPigHouses, deletePigHouse} from '@/api/pigHouse.js'; | ||||||
|  | import {getPens, deletePen} from '@/api/pen.js'; | ||||||
|  | import {getPigBatches} from '@/api/pigBatch.js'; | ||||||
|  | import PigHouseList from '@/components/PigHouseList.vue'; | ||||||
|  | import PigHouseForm from '@/components/PigHouseForm.vue'; | ||||||
|  | import PenForm from '@/components/PenForm.vue'; | ||||||
|  | import {Refresh} from '@element-plus/icons-vue'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   name: 'PigFarmManagementView', | ||||||
|  |   components: { | ||||||
|  |     PigHouseList, | ||||||
|  |     PigHouseForm, | ||||||
|  |     PenForm, | ||||||
|  |     Refresh | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       pigHousesData: [], | ||||||
|  |       loading: false, | ||||||
|  |       error: null, | ||||||
|  |       // 猪舍表单状态 | ||||||
|  |       houseDialogVisible: false, | ||||||
|  |       isEditHouse: false, | ||||||
|  |       currentHouse: {}, | ||||||
|  |       // 猪栏表单状态 | ||||||
|  |       penDialogVisible: false, | ||||||
|  |       isEditPen: false, | ||||||
|  |       currentPen: {}, | ||||||
|  |       // 批次映射,用于本地组装猪栏的批次名称 | ||||||
|  |       batchMap: new Map(), | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   async mounted() { | ||||||
|  |     await this.loadData(); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     async loadData() { | ||||||
|  |       this.loading = true; | ||||||
|  |       this.error = null; | ||||||
|  |       try { | ||||||
|  |         const [pigHousesResponse, pensResponse, batchesResponse] = await Promise.all([ | ||||||
|  |           getPigHouses(), | ||||||
|  |           getPens(), | ||||||
|  |           getPigBatches() | ||||||
|  |         ]); | ||||||
|  |  | ||||||
|  |         const houses = Array.isArray(pigHousesResponse.data) ? pigHousesResponse.data : []; | ||||||
|  |         const pens = Array.isArray(pensResponse.data) ? pensResponse.data : []; | ||||||
|  |         const batches = Array.isArray(batchesResponse.data) ? batchesResponse.data : []; | ||||||
|  |  | ||||||
|  |         // 更新 batchMap | ||||||
|  |         this.batchMap = new Map(batches.map(batch => [batch.id, batch.batch_number])); | ||||||
|  |  | ||||||
|  |         this.pigHousesData = this.assembleData(houses, pens, batches); | ||||||
|  |       } catch (err) { | ||||||
|  |         this.error = err.message || '无法获取数据'; | ||||||
|  |         console.error('Failed to fetch pig farm data:', err); | ||||||
|  |       } finally { | ||||||
|  |         this.loading = false; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     assembleData(houses, pens) { | ||||||
|  |       // 此时 houses 和 pens 已确保是数组,batches 已经更新到 this.batchMap | ||||||
|  |       const houseMap = new Map(); | ||||||
|  |       houses.forEach(house => { | ||||||
|  |         // 确保保留 isExpanded 状态 | ||||||
|  |         const existingHouse = this.pigHousesData.find(h => h.id === house.id); | ||||||
|  |         houseMap.set(house.id, {...house, pens: [], isExpanded: existingHouse ? existingHouse.isExpanded : false}); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       for (const pen of pens) { | ||||||
|  |         if (houseMap.has(pen.house_id)) { | ||||||
|  |           const penWithBatch = { | ||||||
|  |             ...pen, | ||||||
|  |             batch_number: this.batchMap.get(pen.pig_batch_id) || null | ||||||
|  |           }; | ||||||
|  |           houseMap.get(pen.house_id).pens.push(penWithBatch); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       return Array.from(houseMap.values()); | ||||||
|  |     }, | ||||||
|  |     // --- 猪舍操作 --- | ||||||
|  |     handleAddHouse() { | ||||||
|  |       this.currentHouse = {}; | ||||||
|  |       this.isEditHouse = false; | ||||||
|  |       this.houseDialogVisible = true; | ||||||
|  |     }, | ||||||
|  |     handleEditHouse(house) { | ||||||
|  |       this.currentHouse = {...house}; | ||||||
|  |       this.isEditHouse = true; | ||||||
|  |       this.houseDialogVisible = true; | ||||||
|  |     }, | ||||||
|  |     async handleDeleteHouse(house) { | ||||||
|  |       try { | ||||||
|  |         await this.$confirm(`确认删除猪舍 "${house.name}" 吗?`, '提示', { | ||||||
|  |           confirmButtonText: '确定', | ||||||
|  |           cancelButtonText: '取消', | ||||||
|  |           type: 'warning' | ||||||
|  |         }); | ||||||
|  |         await deletePigHouse(house.id); | ||||||
|  |         this.$message.success('删除成功'); | ||||||
|  |         // 本地更新数据,不重新加载全部 | ||||||
|  |         this.pigHousesData = this.pigHousesData.filter(h => h.id !== house.id); | ||||||
|  |       } catch (err) { | ||||||
|  |         if (err !== 'cancel') { | ||||||
|  |           this.$message.error('删除失败: ' + (err.message || '未知错误')); | ||||||
|  |           console.error('Failed to delete pig house:', err); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     handleHouseSuccess(newHouseData) { | ||||||
|  |       this.houseDialogVisible = false; | ||||||
|  |       if (this.isEditHouse) { | ||||||
|  |         // 更新现有猪舍 | ||||||
|  |         const index = this.pigHousesData.findIndex(h => h.id === newHouseData.id); | ||||||
|  |         if (index !== -1) { | ||||||
|  |           // 保留 pens 和 isExpanded 状态 | ||||||
|  |           const existingHouse = this.pigHousesData[index]; | ||||||
|  |           this.pigHousesData[index] = {...newHouseData, pens: existingHouse.pens, isExpanded: existingHouse.isExpanded}; | ||||||
|  |         } | ||||||
|  |         this.$message.success('猪舍更新成功'); | ||||||
|  |       } else { | ||||||
|  |         // 添加新猪舍 | ||||||
|  |         this.pigHousesData.push({...newHouseData, pens: [], isExpanded: false}); | ||||||
|  |         this.$message.success('猪舍添加成功'); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     // --- 猪栏操作 --- | ||||||
|  |     handleAddPen(house) { | ||||||
|  |       this.currentPen = {house_id: house.id}; | ||||||
|  |       this.isEditPen = false; | ||||||
|  |       this.penDialogVisible = true; | ||||||
|  |     }, | ||||||
|  |     handleEditPen(pen) { | ||||||
|  |       this.currentPen = {...pen}; | ||||||
|  |       this.isEditPen = true; | ||||||
|  |       this.penDialogVisible = true; | ||||||
|  |     }, | ||||||
|  |     async handleDeletePen(pen) { | ||||||
|  |       try { | ||||||
|  |         await this.$confirm(`确认删除猪栏 "${pen.pen_number}" 吗?`, '提示', { | ||||||
|  |           confirmButtonText: '确定', | ||||||
|  |           cancelButtonText: '取消', | ||||||
|  |           type: 'warning' | ||||||
|  |         }); | ||||||
|  |         await deletePen(pen.id); | ||||||
|  |         this.$message.success('删除成功'); | ||||||
|  |         // 本地更新数据,不重新加载全部 | ||||||
|  |         const house = this.pigHousesData.find(h => h.id === pen.house_id); | ||||||
|  |         if (house) { | ||||||
|  |           house.pens = house.pens.filter(p => p.id !== pen.id); | ||||||
|  |         } | ||||||
|  |       } catch (err) { | ||||||
|  |         if (err !== 'cancel') { | ||||||
|  |           this.$message.error('删除失败: ' + (err.message || '未知错误')); | ||||||
|  |           console.error('Failed to delete pen:', err); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     handlePenSuccess(newPenData) { | ||||||
|  |       this.penDialogVisible = false; | ||||||
|  |       // 为新猪栏数据添加 batch_number | ||||||
|  |       const penWithBatch = { | ||||||
|  |         ...newPenData, | ||||||
|  |         batch_number: this.batchMap.get(newPenData.pig_batch_id) || null | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       const house = this.pigHousesData.find(h => h.id === penWithBatch.house_id); | ||||||
|  |       if (house) { | ||||||
|  |         if (this.isEditPen) { | ||||||
|  |           // 更新现有猪栏 | ||||||
|  |           const index = house.pens.findIndex(p => p.id === penWithBatch.id); | ||||||
|  |           if (index !== -1) { | ||||||
|  |             house.pens[index] = penWithBatch; | ||||||
|  |           } | ||||||
|  |           this.$message.success('猪栏更新成功'); | ||||||
|  |         } else { | ||||||
|  |           // 添加新猪栏 | ||||||
|  |           house.pens.push(penWithBatch); | ||||||
|  |           this.$message.success('猪栏添加成功'); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     handleToggleHouseExpand(houseId) { | ||||||
|  |       const house = this.pigHousesData.find(h => h.id === houseId); | ||||||
|  |       if (house) { | ||||||
|  |         house.isExpanded = !house.isExpanded; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .pig-farm-management { | ||||||
|  |   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; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .loading { | ||||||
|  |   padding: 20px 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .error { | ||||||
|  |   padding: 20px 0; | ||||||
|  |   text-align: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .retry-btn { | ||||||
|  |   margin-top: 15px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media (max-width: 768px) { | ||||||
|  |   .pig-farm-management { | ||||||
|  |     padding: 10px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .card-header { | ||||||
|  |     flex-direction: column; | ||||||
|  |     gap: 15px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| const { VueLoaderPlugin } = require('vue-loader'); | const { VueLoaderPlugin } = require('vue-loader'); | ||||||
| const path = require('path'); | const path = require('path'); | ||||||
| const HtmlWebpackPlugin = require('html-webpack-plugin'); | const HtmlWebpackPlugin = require('html-webpack-plugin'); | ||||||
|  | const webpack = require('webpack'); // 引入 webpack 模块 | ||||||
|  |  | ||||||
| module.exports = (env, argv) => { | module.exports = (env, argv) => { | ||||||
|   const isDevelopment = argv.mode === 'development'; |   const isDevelopment = argv.mode === 'development'; | ||||||
| @@ -38,6 +39,12 @@ module.exports = (env, argv) => { | |||||||
|       new HtmlWebpackPlugin({ |       new HtmlWebpackPlugin({ | ||||||
|         template: './index.html', |         template: './index.html', | ||||||
|         filename: 'index.html' |         filename: 'index.html' | ||||||
|  |       }), | ||||||
|  |       // 添加 Vue 特性标志的定义 | ||||||
|  |       new webpack.DefinePlugin({ | ||||||
|  |         __VUE_OPTIONS_API__: JSON.stringify(true), | ||||||
|  |         __VUE_PROD_DEVTOOLS__: JSON.stringify(isDevelopment), | ||||||
|  |         __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: JSON.stringify(isDevelopment) | ||||||
|       }) |       }) | ||||||
|     ], |     ], | ||||||
|     devServer: { |     devServer: { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user