Compare commits
	
		
			19 Commits
		
	
	
		
			52cf8c58ed
			...
			main-old
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3743b5ddcd | |||
| 6fe73d8ffe | |||
| 91f160b07e | |||
| a1950872fc | |||
| c499571c11 | |||
| cc7ea94e41 | |||
| 40a19b831a | |||
| 9944340d17 | |||
| 4a70c1e839 | |||
| e75b3ee148 | |||
| cbcba09d40 | |||
| 4805e422f7 | |||
| 64c86de71e | |||
| 3ecbf3b5af | |||
| 1a14aec19b | |||
| 008677467b | |||
| 2b4dd3e74d | |||
| 8468a96398 | |||
| e27aec0ca2 | 
@@ -32,6 +32,6 @@ websocket:
 | 
				
			|||||||
# 心跳配置
 | 
					# 心跳配置
 | 
				
			||||||
heartbeat:
 | 
					heartbeat:
 | 
				
			||||||
  # 心跳间隔(秒)
 | 
					  # 心跳间隔(秒)
 | 
				
			||||||
  interval: 5
 | 
					  interval: 30
 | 
				
			||||||
  # 请求并发数
 | 
					  # 请求并发数
 | 
				
			||||||
  concurrency: 5
 | 
					  concurrency: 5
 | 
				
			||||||
							
								
								
									
										21
									
								
								frontend/dist/assets/index.40048162.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								frontend/dist/assets/index.40048162.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								frontend/dist/assets/index.4965a25a.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								frontend/dist/assets/index.4965a25a.css
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								frontend/dist/assets/index.bcc76856.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/dist/assets/index.bcc76856.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										21
									
								
								frontend/dist/assets/index.cb9d3828.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								frontend/dist/assets/index.cb9d3828.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										4
									
								
								frontend/dist/index.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								frontend/dist/index.html
									
									
									
									
										vendored
									
									
								
							@@ -4,8 +4,8 @@
 | 
				
			|||||||
    <meta charset="UTF-8">
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
    <title>猪场管理系统</title>
 | 
					    <title>猪场管理系统</title>
 | 
				
			||||||
  <script type="module" crossorigin src="/assets/index.40048162.js"></script>
 | 
					  <script type="module" crossorigin src="/assets/index.cb9d3828.js"></script>
 | 
				
			||||||
  <link rel="stylesheet" href="/assets/index.4965a25a.css">
 | 
					  <link rel="stylesheet" href="/assets/index.bcc76856.css">
 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
<body>
 | 
					<body>
 | 
				
			||||||
    <div id="app"></div>
 | 
					    <div id="app"></div>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										303
									
								
								frontend/src/components/FeedPlanForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								frontend/src/components/FeedPlanForm.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,303 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="feed-plan-form">
 | 
				
			||||||
 | 
					    <div class="form-header">
 | 
				
			||||||
 | 
					      <h2>{{ isEditMode ? '编辑饲喂计划' : '新建饲喂计划' }}</h2>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <form @submit.prevent="handleSubmit">
 | 
				
			||||||
 | 
					      <div class="form-group">
 | 
				
			||||||
 | 
					        <label for="name">计划名称 *</label>
 | 
				
			||||||
 | 
					        <input 
 | 
				
			||||||
 | 
					          type="text" 
 | 
				
			||||||
 | 
					          id="name" 
 | 
				
			||||||
 | 
					          v-model="form.name" 
 | 
				
			||||||
 | 
					          required
 | 
				
			||||||
 | 
					          :disabled="loading"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <div class="form-group">
 | 
				
			||||||
 | 
					        <label for="description">计划描述</label>
 | 
				
			||||||
 | 
					        <textarea 
 | 
				
			||||||
 | 
					          id="description" 
 | 
				
			||||||
 | 
					          v-model="form.description" 
 | 
				
			||||||
 | 
					          rows="3"
 | 
				
			||||||
 | 
					          :disabled="loading"
 | 
				
			||||||
 | 
					        ></textarea>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <div class="form-group">
 | 
				
			||||||
 | 
					        <label>计划类型 *</label>
 | 
				
			||||||
 | 
					        <div class="radio-group">
 | 
				
			||||||
 | 
					          <label class="radio-item">
 | 
				
			||||||
 | 
					            <input 
 | 
				
			||||||
 | 
					              type="radio" 
 | 
				
			||||||
 | 
					              v-model="form.type" 
 | 
				
			||||||
 | 
					              value="manual" 
 | 
				
			||||||
 | 
					              :disabled="loading"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					            手动触发
 | 
				
			||||||
 | 
					          </label>
 | 
				
			||||||
 | 
					          <label class="radio-item">
 | 
				
			||||||
 | 
					            <input 
 | 
				
			||||||
 | 
					              type="radio" 
 | 
				
			||||||
 | 
					              v-model="form.type" 
 | 
				
			||||||
 | 
					              value="auto" 
 | 
				
			||||||
 | 
					              :disabled="loading"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					            自动触发
 | 
				
			||||||
 | 
					          </label>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <div class="form-group">
 | 
				
			||||||
 | 
					        <label>
 | 
				
			||||||
 | 
					          <input 
 | 
				
			||||||
 | 
					            type="checkbox" 
 | 
				
			||||||
 | 
					            v-model="form.enabled" 
 | 
				
			||||||
 | 
					            :disabled="loading"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					          启用计划
 | 
				
			||||||
 | 
					        </label>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <div v-if="form.type === 'auto'" class="form-group">
 | 
				
			||||||
 | 
					        <label for="schedule_cron">定时表达式</label>
 | 
				
			||||||
 | 
					        <input 
 | 
				
			||||||
 | 
					          type="text" 
 | 
				
			||||||
 | 
					          id="schedule_cron" 
 | 
				
			||||||
 | 
					          v-model="form.schedule_cron" 
 | 
				
			||||||
 | 
					          placeholder="例如: 0 0 7 * * *"
 | 
				
			||||||
 | 
					          :disabled="loading"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					        <div class="help-text">Cron表达式,用于设置自动执行时间</div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <div class="form-group">
 | 
				
			||||||
 | 
					        <label for="execution_limit">执行次数限制</label>
 | 
				
			||||||
 | 
					        <input 
 | 
				
			||||||
 | 
					          type="number" 
 | 
				
			||||||
 | 
					          id="execution_limit" 
 | 
				
			||||||
 | 
					          v-model.number="form.execution_limit" 
 | 
				
			||||||
 | 
					          min="0"
 | 
				
			||||||
 | 
					          :disabled="loading"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					        <div class="help-text">0表示无限制</div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <div class="form-actions">
 | 
				
			||||||
 | 
					        <button 
 | 
				
			||||||
 | 
					          type="button" 
 | 
				
			||||||
 | 
					          class="btn btn-secondary" 
 | 
				
			||||||
 | 
					          @click="$emit('cancel')"
 | 
				
			||||||
 | 
					          :disabled="loading"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          取消
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					        <button 
 | 
				
			||||||
 | 
					          type="submit" 
 | 
				
			||||||
 | 
					          class="btn btn-primary" 
 | 
				
			||||||
 | 
					          :disabled="loading || isSubmitting"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {{ loading || isSubmitting ? '处理中...' : (isEditMode ? '更新计划' : '创建计划') }}
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'FeedPlanForm',
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    // 编辑模式下的初始数据
 | 
				
			||||||
 | 
					    initialData: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      default: () => ({
 | 
				
			||||||
 | 
					        name: '',
 | 
				
			||||||
 | 
					        description: '',
 | 
				
			||||||
 | 
					        type: 'manual',
 | 
				
			||||||
 | 
					        enabled: true,
 | 
				
			||||||
 | 
					        schedule_cron: '',
 | 
				
			||||||
 | 
					        execution_limit: 0
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    // 是否为编辑模式
 | 
				
			||||||
 | 
					    isEditMode: {
 | 
				
			||||||
 | 
					      type: Boolean,
 | 
				
			||||||
 | 
					      default: false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    // 提交时的加载状态
 | 
				
			||||||
 | 
					    loading: {
 | 
				
			||||||
 | 
					      type: Boolean,
 | 
				
			||||||
 | 
					      default: false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      form: { ...this.initialData },
 | 
				
			||||||
 | 
					      isSubmitting: false // 防止重复提交
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  watch: {
 | 
				
			||||||
 | 
					    // 监听初始数据变化,更新表单
 | 
				
			||||||
 | 
					    initialData: {
 | 
				
			||||||
 | 
					      handler(newVal) {
 | 
				
			||||||
 | 
					        this.form = { ...newVal }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      deep: true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    async handleSubmit() {
 | 
				
			||||||
 | 
					      // 防止重复提交
 | 
				
			||||||
 | 
					      if (this.isSubmitting || this.loading) {
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      this.isSubmitting = true
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        // 表单验证
 | 
				
			||||||
 | 
					        if (!this.form.name.trim()) {
 | 
				
			||||||
 | 
					          alert('请输入计划名称')
 | 
				
			||||||
 | 
					          return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (this.form.type === 'auto' && this.form.schedule_cron && !this.isValidCron(this.form.schedule_cron)) {
 | 
				
			||||||
 | 
					          alert('请输入有效的Cron表达式')
 | 
				
			||||||
 | 
					          return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // 触发提交事件
 | 
				
			||||||
 | 
					        this.$emit('submit', { ...this.form })
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        // 在下一个tick重置提交状态,确保事件已经触发
 | 
				
			||||||
 | 
					        this.$nextTick(() => {
 | 
				
			||||||
 | 
					          this.isSubmitting = false
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 简单的Cron表达式验证
 | 
				
			||||||
 | 
					    isValidCron(cron) {
 | 
				
			||||||
 | 
					      // 这里可以添加更复杂的验证逻辑
 | 
				
			||||||
 | 
					      // 现在只是简单检查格式
 | 
				
			||||||
 | 
					      return typeof cron === 'string' && cron.trim().length > 0
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.feed-plan-form {
 | 
				
			||||||
 | 
					  background: white;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  padding: 30px;
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-header h2 {
 | 
				
			||||||
 | 
					  margin-top: 0;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-group {
 | 
				
			||||||
 | 
					  margin-bottom: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-group label {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  margin-bottom: 5px;
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-group input[type="text"],
 | 
				
			||||||
 | 
					.form-group input[type="number"],
 | 
				
			||||||
 | 
					.form-group textarea {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  padding: 10px;
 | 
				
			||||||
 | 
					  border: 1px solid #ddd;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-group input[type="text"]:focus,
 | 
				
			||||||
 | 
					.form-group input[type="number"]:focus,
 | 
				
			||||||
 | 
					.form-group textarea:focus {
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
 | 
					  border-color: #007bff;
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-group input:disabled,
 | 
				
			||||||
 | 
					.form-group textarea:disabled {
 | 
				
			||||||
 | 
					  background-color: #f8f9fa;
 | 
				
			||||||
 | 
					  cursor: not-allowed;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.radio-group {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  gap: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.radio-item {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 5px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.help-text {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					  margin-top: 5px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.form-actions {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: flex-end;
 | 
				
			||||||
 | 
					  gap: 15px;
 | 
				
			||||||
 | 
					  margin-top: 30px;
 | 
				
			||||||
 | 
					  padding-top: 20px;
 | 
				
			||||||
 | 
					  border-top: 1px solid #eee;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn {
 | 
				
			||||||
 | 
					  padding: 10px 20px;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  transition: background-color 0.3s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn:disabled {
 | 
				
			||||||
 | 
					  cursor: not-allowed;
 | 
				
			||||||
 | 
					  opacity: 0.6;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-primary {
 | 
				
			||||||
 | 
					  background-color: #007bff;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-primary:hover:not(:disabled) {
 | 
				
			||||||
 | 
					  background-color: #0069d9;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-secondary {
 | 
				
			||||||
 | 
					  background-color: #6c757d;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-secondary:hover:not(:disabled) {
 | 
				
			||||||
 | 
					  background-color: #5a6268;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@@ -12,6 +12,7 @@
 | 
				
			|||||||
      <ul>
 | 
					      <ul>
 | 
				
			||||||
        <li><router-link to="/dashboard" class="active">控制台</router-link></li>
 | 
					        <li><router-link to="/dashboard" class="active">控制台</router-link></li>
 | 
				
			||||||
        <li><router-link to="/device">设备管理</router-link></li>
 | 
					        <li><router-link to="/device">设备管理</router-link></li>
 | 
				
			||||||
 | 
					        <li><router-link to="/feed/plan">饲喂计划</router-link></li>
 | 
				
			||||||
      </ul>
 | 
					      </ul>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,14 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </header>
 | 
					    </header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <nav class="nav">
 | 
				
			||||||
 | 
					      <ul>
 | 
				
			||||||
 | 
					        <li><router-link to="/dashboard">控制台</router-link></li>
 | 
				
			||||||
 | 
					        <li><router-link to="/device" class="active">设备管理</router-link></li>
 | 
				
			||||||
 | 
					        <li><router-link to="/feed/plan">饲喂计划</router-link></li>
 | 
				
			||||||
 | 
					      </ul>
 | 
				
			||||||
 | 
					    </nav>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <main class="main-content">
 | 
					    <main class="main-content">
 | 
				
			||||||
      <div class="toolbar">
 | 
					      <div class="toolbar">
 | 
				
			||||||
        <button class="btn btn-primary" @click="openAddDeviceModal">添加设备</button>
 | 
					        <button class="btn btn-primary" @click="openAddDeviceModal">添加设备</button>
 | 
				
			||||||
@@ -481,6 +489,39 @@ export default {
 | 
				
			|||||||
  background-color: #f5f7fa;
 | 
					  background-color: #f5f7fa;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav {
 | 
				
			||||||
 | 
					  background-color: #343a40;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  margin-bottom: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav ul {
 | 
				
			||||||
 | 
					  list-style-type: none;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav li {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav a {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  padding: 15px 20px;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					  transition: background-color 0.3s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav a:hover {
 | 
				
			||||||
 | 
					  background-color: #495057;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav a.active {
 | 
				
			||||||
 | 
					  background-color: #007bff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.header {
 | 
					.header {
 | 
				
			||||||
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | 
					  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | 
				
			||||||
  color: white;
 | 
					  color: white;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										514
									
								
								frontend/src/pages/FeedPlan.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										514
									
								
								frontend/src/pages/FeedPlan.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,514 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="feed-plan-management">
 | 
				
			||||||
 | 
					    <div class="header">
 | 
				
			||||||
 | 
					      <h1>饲喂计划管理</h1>
 | 
				
			||||||
 | 
					      <div class="user-info">
 | 
				
			||||||
 | 
					        <span>欢迎, {{ username }}</span>
 | 
				
			||||||
 | 
					        <button class="logout-btn" @click="logout">退出</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <nav class="nav">
 | 
				
			||||||
 | 
					      <ul>
 | 
				
			||||||
 | 
					        <li><router-link to="/dashboard">控制台</router-link></li>
 | 
				
			||||||
 | 
					        <li><router-link to="/device">设备管理</router-link></li>
 | 
				
			||||||
 | 
					        <li><router-link to="/feed/plan" class="active">饲喂计划</router-link></li>
 | 
				
			||||||
 | 
					      </ul>
 | 
				
			||||||
 | 
					    </nav>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <main class="main-content">
 | 
				
			||||||
 | 
					      <div class="toolbar">
 | 
				
			||||||
 | 
					        <button class="btn btn-primary" @click="createPlan">创建计划</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="plan-list">
 | 
				
			||||||
 | 
					        <div v-if="loading" class="loading">
 | 
				
			||||||
 | 
					          加载中...
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div v-else-if="plans.length === 0" class="no-plans">
 | 
				
			||||||
 | 
					          暂无饲喂计划
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div v-else class="plans-container">
 | 
				
			||||||
 | 
					          <div 
 | 
				
			||||||
 | 
					            v-for="plan in plans" 
 | 
				
			||||||
 | 
					            :key="plan.id" 
 | 
				
			||||||
 | 
					            class="plan-card"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <div class="plan-header">
 | 
				
			||||||
 | 
					              <h3>{{ plan.name }}</h3>
 | 
				
			||||||
 | 
					              <span :class="['plan-status', { 'enabled': plan.enabled, 'disabled': !plan.enabled }]">
 | 
				
			||||||
 | 
					                {{ plan.enabled ? '已启用' : '已禁用' }}
 | 
				
			||||||
 | 
					              </span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <div class="plan-details">
 | 
				
			||||||
 | 
					              <p class="plan-description">{{ plan.description || '暂无描述' }}</p>
 | 
				
			||||||
 | 
					              <div class="plan-meta">
 | 
				
			||||||
 | 
					                <span class="plan-type">{{ plan.type === 'manual' ? '手动触发' : '自动触发' }}</span>
 | 
				
			||||||
 | 
					                <span v-if="plan.schedule_cron" class="plan-cron">定时: {{ plan.schedule_cron }}</span>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <div class="plan-actions">
 | 
				
			||||||
 | 
					              <button class="action-btn detail-btn" @click="viewDetail(plan.id)">详情</button>
 | 
				
			||||||
 | 
					              <button class="action-btn edit-btn" @click="editPlan(plan)">编辑</button>
 | 
				
			||||||
 | 
					              <button class="action-btn delete-btn" @click="deletePlan(plan.id)">删除</button>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </main>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <!-- 计划表单模态框 -->
 | 
				
			||||||
 | 
					    <div v-if="showModal" class="modal-overlay" @click="closeModal">
 | 
				
			||||||
 | 
					      <div class="modal-content" @click.stop>
 | 
				
			||||||
 | 
					        <FeedPlanForm
 | 
				
			||||||
 | 
					          :initial-data="currentPlan"
 | 
				
			||||||
 | 
					          :is-edit-mode="modalType === 'edit'"
 | 
				
			||||||
 | 
					          :loading="submitting"
 | 
				
			||||||
 | 
					          @submit="submitForm"
 | 
				
			||||||
 | 
					          @cancel="closeModal"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import FeedPlanForm from '../components/FeedPlanForm.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'FeedPlan',
 | 
				
			||||||
 | 
					  components: {
 | 
				
			||||||
 | 
					    FeedPlanForm
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      username: '',
 | 
				
			||||||
 | 
					      plans: [],
 | 
				
			||||||
 | 
					      loading: true,
 | 
				
			||||||
 | 
					      // 控制模态框显示
 | 
				
			||||||
 | 
					      showModal: false,
 | 
				
			||||||
 | 
					      // 当前操作类型: 'create' 或 'edit'
 | 
				
			||||||
 | 
					      modalType: 'create',
 | 
				
			||||||
 | 
					      // 当前编辑的计划
 | 
				
			||||||
 | 
					      currentPlan: null,
 | 
				
			||||||
 | 
					      // 提交时的加载状态
 | 
				
			||||||
 | 
					      submitting: false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  mounted() {
 | 
				
			||||||
 | 
					    this.username = localStorage.getItem('username') || '管理员'
 | 
				
			||||||
 | 
					    this.loadPlans()
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    // 加载饲喂计划列表
 | 
				
			||||||
 | 
					    async loadPlans() {
 | 
				
			||||||
 | 
					      this.loading = true
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const response = await fetch('/api/v1/feed/plan/list', {
 | 
				
			||||||
 | 
					          method: 'GET',
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					            'Authorization': 'Bearer ' + localStorage.getItem('authToken')
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        const data = await response.json()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (response.ok && data.code === 0) {
 | 
				
			||||||
 | 
					          this.plans = data.data.plans || []
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          console.error('获取饲喂计划列表失败:', data.message)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error('获取饲喂计划列表失败:', error)
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.loading = false
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 查看详情
 | 
				
			||||||
 | 
					    viewDetail(planId) {
 | 
				
			||||||
 | 
					      this.$router.push(`/feed/plan/detail/${planId}`)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 创建计划
 | 
				
			||||||
 | 
					    createPlan() {
 | 
				
			||||||
 | 
					      this.modalType = 'create'
 | 
				
			||||||
 | 
					      this.currentPlan = {
 | 
				
			||||||
 | 
					        name: '',
 | 
				
			||||||
 | 
					        description: '',
 | 
				
			||||||
 | 
					        type: 'manual',
 | 
				
			||||||
 | 
					        enabled: true,
 | 
				
			||||||
 | 
					        schedule_cron: '',
 | 
				
			||||||
 | 
					        execution_limit: 0
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.showModal = true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 编辑计划
 | 
				
			||||||
 | 
					    editPlan(plan) {
 | 
				
			||||||
 | 
					      this.modalType = 'edit'
 | 
				
			||||||
 | 
					      // 深拷贝计划数据,避免直接修改原数据
 | 
				
			||||||
 | 
					      this.currentPlan = JSON.parse(JSON.stringify(plan))
 | 
				
			||||||
 | 
					      this.showModal = true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 关闭模态框
 | 
				
			||||||
 | 
					    closeModal() {
 | 
				
			||||||
 | 
					      this.showModal = false
 | 
				
			||||||
 | 
					      this.currentPlan = null
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 提交表单
 | 
				
			||||||
 | 
					    async submitForm(formData) {
 | 
				
			||||||
 | 
					      // 防止重复提交
 | 
				
			||||||
 | 
					      if (this.submitting) {
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      this.submitting = true
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        let url, method
 | 
				
			||||||
 | 
					        if (this.modalType === 'create') {
 | 
				
			||||||
 | 
					          url = '/api/v1/feed/plan/create'
 | 
				
			||||||
 | 
					          method = 'POST'
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          url = '/api/v1/feed/plan/update'
 | 
				
			||||||
 | 
					          method = 'POST'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        const response = await fetch(url, {
 | 
				
			||||||
 | 
					          method: method,
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					            'Authorization': 'Bearer ' + localStorage.getItem('authToken')
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          body: JSON.stringify(formData)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        const data = await response.json()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (response.ok && data.code === 0) {
 | 
				
			||||||
 | 
					          // 提交成功,关闭模态框并重新加载列表
 | 
				
			||||||
 | 
					          this.closeModal()
 | 
				
			||||||
 | 
					          await this.loadPlans()
 | 
				
			||||||
 | 
					          alert(this.modalType === 'create' ? '创建计划成功' : '更新计划成功')
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          alert((this.modalType === 'create' ? '创建计划失败: ' : '更新计划失败: ') + (data.message || '未知错误'))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error(this.modalType === 'create' ? '创建计划失败:' : '更新计划失败:', error)
 | 
				
			||||||
 | 
					        alert((this.modalType === 'create' ? '创建计划失败: ' : '更新计划失败: ') + error.message)
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.submitting = false
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 删除计划
 | 
				
			||||||
 | 
					    async deletePlan(planId) {
 | 
				
			||||||
 | 
					      if (!confirm('确定要删除这个饲喂计划吗?')) {
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const response = await fetch('/api/v1/feed/plan/delete', {
 | 
				
			||||||
 | 
					          method: 'POST',
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					            'Authorization': 'Bearer ' + localStorage.getItem('authToken')
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          body: JSON.stringify({ id: planId })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        const data = await response.json()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (response.ok && data.code === 0) {
 | 
				
			||||||
 | 
					          // 删除成功,重新加载列表
 | 
				
			||||||
 | 
					          await this.loadPlans()
 | 
				
			||||||
 | 
					          this.$message?.success('删除成功') || alert('删除成功')
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.$message?.error('删除失败: ' + data.message) || alert('删除失败: ' + data.message)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error('删除饲喂计划失败:', error)
 | 
				
			||||||
 | 
					        this.$message?.error('删除饲喂计划失败: ' + error.message) || alert('删除饲喂计划失败: ' + error.message)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 退出登录
 | 
				
			||||||
 | 
					    logout() {
 | 
				
			||||||
 | 
					      localStorage.removeItem('authToken')
 | 
				
			||||||
 | 
					      localStorage.removeItem('username')
 | 
				
			||||||
 | 
					      this.$router.push('/')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.feed-plan-management {
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.header {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  margin-bottom: 30px;
 | 
				
			||||||
 | 
					  padding-bottom: 20px;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #eee;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.header h1 {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.user-info {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 15px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.logout-btn {
 | 
				
			||||||
 | 
					  padding: 8px 16px;
 | 
				
			||||||
 | 
					  background-color: #dc3545;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  transition: background-color 0.3s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.logout-btn:hover {
 | 
				
			||||||
 | 
					  background-color: #c82333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav {
 | 
				
			||||||
 | 
					  background-color: #343a40;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  margin-bottom: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav ul {
 | 
				
			||||||
 | 
					  list-style-type: none;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav li {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav a {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  padding: 15px 20px;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					  transition: background-color 0.3s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav a:hover {
 | 
				
			||||||
 | 
					  background-color: #495057;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav a.active {
 | 
				
			||||||
 | 
					  background-color: #007bff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toolbar {
 | 
				
			||||||
 | 
					  margin-bottom: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn {
 | 
				
			||||||
 | 
					  padding: 10px 20px;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  transition: background-color 0.3s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-primary {
 | 
				
			||||||
 | 
					  background-color: #007bff;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-primary:hover {
 | 
				
			||||||
 | 
					  background-color: #0069d9;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-list {
 | 
				
			||||||
 | 
					  min-height: 400px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.loading, .no-plans {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  padding: 50px;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plans-container {
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
 | 
				
			||||||
 | 
					  gap: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-card {
 | 
				
			||||||
 | 
					  border: 1px solid #ddd;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					  background-color: white;
 | 
				
			||||||
 | 
					  transition: box-shadow 0.3s, transform 0.3s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-card:hover {
 | 
				
			||||||
 | 
					  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
 | 
				
			||||||
 | 
					  transform: translateY(-2px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-header {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  margin-bottom: 15px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-header h3 {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					  font-size: 18px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-status {
 | 
				
			||||||
 | 
					  padding: 4px 8px;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-status.enabled {
 | 
				
			||||||
 | 
					  background-color: #d4edda;
 | 
				
			||||||
 | 
					  color: #155724;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-status.disabled {
 | 
				
			||||||
 | 
					  background-color: #f8d7da;
 | 
				
			||||||
 | 
					  color: #721c24;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-details {
 | 
				
			||||||
 | 
					  margin-bottom: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-description {
 | 
				
			||||||
 | 
					  margin: 0 0 15px 0;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					  line-height: 1.5;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-meta {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
 | 
					  gap: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-type, .plan-cron {
 | 
				
			||||||
 | 
					  padding: 4px 8px;
 | 
				
			||||||
 | 
					  background-color: #e9ecef;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  color: #495057;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-actions {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  gap: 10px;
 | 
				
			||||||
 | 
					  justify-content: flex-end;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.action-btn {
 | 
				
			||||||
 | 
					  padding: 6px 12px;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  font-size: 13px;
 | 
				
			||||||
 | 
					  transition: background-color 0.3s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.detail-btn {
 | 
				
			||||||
 | 
					  background-color: #17a2b8;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.detail-btn:hover {
 | 
				
			||||||
 | 
					  background-color: #138496;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.edit-btn {
 | 
				
			||||||
 | 
					  background-color: #ffc107;
 | 
				
			||||||
 | 
					  color: #212529;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.edit-btn:hover {
 | 
				
			||||||
 | 
					  background-color: #e0a800;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.delete-btn {
 | 
				
			||||||
 | 
					  background-color: #dc3545;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.delete-btn:hover {
 | 
				
			||||||
 | 
					  background-color: #c82333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					  .plans-container {
 | 
				
			||||||
 | 
					    grid-template-columns: 1fr;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .header {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 15px;
 | 
				
			||||||
 | 
					    align-items: flex-start;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .nav ul {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 模态框样式 */
 | 
				
			||||||
 | 
					.modal-overlay {
 | 
				
			||||||
 | 
					  position: fixed;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  bottom: 0;
 | 
				
			||||||
 | 
					  background-color: rgba(0, 0, 0, 0.5);
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  z-index: 1000;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.modal-content {
 | 
				
			||||||
 | 
					  max-width: 600px;
 | 
				
			||||||
 | 
					  width: 90%;
 | 
				
			||||||
 | 
					  max-height: 90vh;
 | 
				
			||||||
 | 
					  overflow-y: auto;
 | 
				
			||||||
 | 
					  background: white;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										652
									
								
								frontend/src/pages/FeedPlanDetail.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										652
									
								
								frontend/src/pages/FeedPlanDetail.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,652 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="feed-plan-detail">
 | 
				
			||||||
 | 
					    <div class="header">
 | 
				
			||||||
 | 
					      <h1>饲喂计划详情</h1>
 | 
				
			||||||
 | 
					      <div class="user-info">
 | 
				
			||||||
 | 
					        <span>欢迎, {{ username }}</span>
 | 
				
			||||||
 | 
					        <button class="logout-btn" @click="logout">退出</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <nav class="nav">
 | 
				
			||||||
 | 
					      <ul>
 | 
				
			||||||
 | 
					        <li><router-link to="/dashboard">控制台</router-link></li>
 | 
				
			||||||
 | 
					        <li><router-link to="/device">设备管理</router-link></li>
 | 
				
			||||||
 | 
					        <li><router-link to="/feed/plan">饲喂计划</router-link></li>
 | 
				
			||||||
 | 
					        <li><router-link to="/feed/plan/detail" class="active">计划详情</router-link></li>
 | 
				
			||||||
 | 
					      </ul>
 | 
				
			||||||
 | 
					    </nav>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <main class="main-content">
 | 
				
			||||||
 | 
					      <div v-if="loading" class="loading">
 | 
				
			||||||
 | 
					        加载中...
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <div v-else-if="error" class="error">
 | 
				
			||||||
 | 
					        {{ error }}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <div v-else-if="plan" class="plan-detail-container">
 | 
				
			||||||
 | 
					        <div class="plan-header">
 | 
				
			||||||
 | 
					          <h2>{{ plan.name }}</h2>
 | 
				
			||||||
 | 
					          <span :class="['plan-status', { 'enabled': plan.enabled, 'disabled': !plan.enabled }]">
 | 
				
			||||||
 | 
					            {{ plan.enabled ? '已启用' : '已禁用' }}
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div class="plan-info">
 | 
				
			||||||
 | 
					          <div class="info-item">
 | 
				
			||||||
 | 
					            <label>计划描述:</label>
 | 
				
			||||||
 | 
					            <span>{{ plan.description || '无描述' }}</span>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div class="info-item">
 | 
				
			||||||
 | 
					            <label>计划类型:</label>
 | 
				
			||||||
 | 
					            <span>{{ plan.type === 'manual' ? '手动触发' : '自动触发' }}</span>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div v-if="plan.schedule_cron" class="info-item">
 | 
				
			||||||
 | 
					            <label>定时表达式:</label>
 | 
				
			||||||
 | 
					            <span>{{ plan.schedule_cron }}</span>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div class="info-item">
 | 
				
			||||||
 | 
					            <label>执行次数限制:</label>
 | 
				
			||||||
 | 
					            <span>{{ plan.execution_limit > 0 ? plan.execution_limit : '无限制' }}</span>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <!-- 移除主计划中的父计划ID和顺序显示 -->
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div class="plan-steps">
 | 
				
			||||||
 | 
					          <h3>计划步骤</h3>
 | 
				
			||||||
 | 
					          <div v-if="plan.steps && plan.steps.length > 0" class="steps-list">
 | 
				
			||||||
 | 
					            <div 
 | 
				
			||||||
 | 
					              v-for="(step, index) in plan.steps" 
 | 
				
			||||||
 | 
					              :key="step.id" 
 | 
				
			||||||
 | 
					              class="step-item"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div class="step-header">
 | 
				
			||||||
 | 
					                <span class="step-number">步骤 {{ index + 1 }}</span>
 | 
				
			||||||
 | 
					                <span v-if="step.schedule_cron" class="step-cron">定时: {{ step.schedule_cron }}</span>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              
 | 
				
			||||||
 | 
					              <div class="step-details">
 | 
				
			||||||
 | 
					                <div class="detail-item">
 | 
				
			||||||
 | 
					                  <label>设备ID:</label>
 | 
				
			||||||
 | 
					                  <span>{{ step.device_id }}</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                <div class="detail-item">
 | 
				
			||||||
 | 
					                  <label>目标值:</label>
 | 
				
			||||||
 | 
					                  <span>{{ step.target_value }}</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                <div class="detail-item">
 | 
				
			||||||
 | 
					                  <label>动作:</label>
 | 
				
			||||||
 | 
					                  <span>{{ step.action }}</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                <div class="detail-item">
 | 
				
			||||||
 | 
					                  <label>执行次数限制:</label>
 | 
				
			||||||
 | 
					                  <span>{{ step.execution_limit > 0 ? step.execution_limit : '无限制' }}</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div v-else class="no-steps">
 | 
				
			||||||
 | 
					            该计划暂无步骤
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div v-if="plan.sub_plans && plan.sub_plans.length > 0" class="sub-plans">
 | 
				
			||||||
 | 
					          <h3>子计划</h3>
 | 
				
			||||||
 | 
					          <div class="sub-plans-list">
 | 
				
			||||||
 | 
					            <div 
 | 
				
			||||||
 | 
					              v-for="subPlan in plan.sub_plans" 
 | 
				
			||||||
 | 
					              :key="subPlan.id" 
 | 
				
			||||||
 | 
					              class="sub-plan-item"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div class="sub-plan-header">
 | 
				
			||||||
 | 
					                <h4>{{ subPlan.name }}</h4>
 | 
				
			||||||
 | 
					                <span :class="['plan-status', { 'enabled': subPlan.enabled, 'disabled': !subPlan.enabled }]">
 | 
				
			||||||
 | 
					                  {{ subPlan.enabled ? '已启用' : '已禁用' }}
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              
 | 
				
			||||||
 | 
					              <div class="sub-plan-info">
 | 
				
			||||||
 | 
					                <div class="info-item">
 | 
				
			||||||
 | 
					                  <label>描述:</label>
 | 
				
			||||||
 | 
					                  <span>{{ subPlan.description || '无描述' }}</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                <div class="info-item">
 | 
				
			||||||
 | 
					                  <label>类型:</label>
 | 
				
			||||||
 | 
					                  <span>{{ subPlan.type === 'manual' ? '手动触发' : '自动触发' }}</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                <div v-if="subPlan.schedule_cron" class="info-item">
 | 
				
			||||||
 | 
					                  <label>定时表达式:</label>
 | 
				
			||||||
 | 
					                  <span>{{ subPlan.schedule_cron }}</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                <div class="info-item">
 | 
				
			||||||
 | 
					                  <label>顺序:</label>
 | 
				
			||||||
 | 
					                  <span>{{ (subPlan.order_in_parent || 0) + 1 }}</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                <div v-if="subPlan.parent_id" class="info-item">
 | 
				
			||||||
 | 
					                  <label>父计划:</label>
 | 
				
			||||||
 | 
					                  <span>{{ getParentPlanName(subPlan.parent_id) }}(id:{{ subPlan.parent_id }})</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              
 | 
				
			||||||
 | 
					              <div class="sub-plan-steps">
 | 
				
			||||||
 | 
					                <h5>子计划步骤</h5>
 | 
				
			||||||
 | 
					                <div v-if="subPlan.steps && subPlan.steps.length > 0" class="steps-list">
 | 
				
			||||||
 | 
					                  <div 
 | 
				
			||||||
 | 
					                    v-for="(step, index) in subPlan.steps" 
 | 
				
			||||||
 | 
					                    :key="step.id" 
 | 
				
			||||||
 | 
					                    class="step-item"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <div class="step-header">
 | 
				
			||||||
 | 
					                      <span class="step-number">步骤 {{ index + 1 }}</span>
 | 
				
			||||||
 | 
					                      <span v-if="step.schedule_cron" class="step-cron">定时: {{ step.schedule_cron }}</span>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    <div class="step-details">
 | 
				
			||||||
 | 
					                      <div class="detail-item">
 | 
				
			||||||
 | 
					                        <label>设备ID:</label>
 | 
				
			||||||
 | 
					                        <span>{{ step.device_id }}</span>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      
 | 
				
			||||||
 | 
					                      <div class="detail-item">
 | 
				
			||||||
 | 
					                        <label>目标值:</label>
 | 
				
			||||||
 | 
					                        <span>{{ step.target_value }}</span>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      
 | 
				
			||||||
 | 
					                      <div class="detail-item">
 | 
				
			||||||
 | 
					                        <label>动作:</label>
 | 
				
			||||||
 | 
					                        <span>{{ step.action }}</span>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      
 | 
				
			||||||
 | 
					                      <div class="detail-item">
 | 
				
			||||||
 | 
					                        <label>执行次数限制:</label>
 | 
				
			||||||
 | 
					                        <span>{{ step.execution_limit > 0 ? step.execution_limit : '无限制' }}</span>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div v-else class="no-steps">
 | 
				
			||||||
 | 
					                  该子计划暂无步骤
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div class="actions">
 | 
				
			||||||
 | 
					          <button class="btn btn-secondary" @click="goBack">返回列表</button>
 | 
				
			||||||
 | 
					          <button class="btn btn-primary" @click="editPlan">编辑计划</button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </main>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: 'FeedPlanDetail',
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      username: '',
 | 
				
			||||||
 | 
					      plan: null,
 | 
				
			||||||
 | 
					      loading: true,
 | 
				
			||||||
 | 
					      error: null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  mounted() {
 | 
				
			||||||
 | 
					    this.username = localStorage.getItem('username') || '管理员'
 | 
				
			||||||
 | 
					    this.loadPlanDetail()
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    // 加载计划详情
 | 
				
			||||||
 | 
					    async loadPlanDetail() {
 | 
				
			||||||
 | 
					      this.loading = true
 | 
				
			||||||
 | 
					      this.error = null
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const planId = this.$route.query.id || this.$route.params.id
 | 
				
			||||||
 | 
					        if (!planId) {
 | 
				
			||||||
 | 
					          this.error = '无效的计划ID'
 | 
				
			||||||
 | 
					          return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        const response = await fetch(`/api/v1/feed/plan/detail?id=${planId}`, {
 | 
				
			||||||
 | 
					          method: 'GET',
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					            'Authorization': 'Bearer ' + localStorage.getItem('authToken')
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        const data = await response.json()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (response.ok && data.code === 0) {
 | 
				
			||||||
 | 
					          this.plan = data.data
 | 
				
			||||||
 | 
					          // 加载子计划的详细信息(包括步骤)
 | 
				
			||||||
 | 
					          await this.loadSubPlanDetails()
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.error = data.message || '获取计划详情失败'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error('获取计划详情失败:', error)
 | 
				
			||||||
 | 
					        this.error = '获取计划详情失败: ' + error.message
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        this.loading = false
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 加载子计划详情
 | 
				
			||||||
 | 
					    async loadSubPlanDetails() {
 | 
				
			||||||
 | 
					      if (!this.plan || !this.plan.sub_plans || this.plan.sub_plans.length === 0) {
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // 遍历所有子计划
 | 
				
			||||||
 | 
					      for (let i = 0; i < this.plan.sub_plans.length; i++) {
 | 
				
			||||||
 | 
					        const subPlan = this.plan.sub_plans[i]
 | 
				
			||||||
 | 
					        // 如果子计划没有步骤或步骤为空,则加载详细信息
 | 
				
			||||||
 | 
					        if (!subPlan.steps || subPlan.steps.length === 0) {
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            const response = await fetch(`/api/v1/feed/plan/detail?id=${subPlan.id}`, {
 | 
				
			||||||
 | 
					              method: 'GET',
 | 
				
			||||||
 | 
					              headers: {
 | 
				
			||||||
 | 
					                'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					                'Authorization': 'Bearer ' + localStorage.getItem('authToken')
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            const data = await response.json()
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (response.ok && data.code === 0) {
 | 
				
			||||||
 | 
					              // 用详细信息替换原来的简略信息
 | 
				
			||||||
 | 
					              this.plan.sub_plans[i] = data.data
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          } catch (error) {
 | 
				
			||||||
 | 
					            console.error(`获取子计划 ${subPlan.id} 详情失败:`, error)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 返回列表
 | 
				
			||||||
 | 
					    goBack() {
 | 
				
			||||||
 | 
					      this.$router.push('/feed/plan')
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 获取父计划名称
 | 
				
			||||||
 | 
					    getParentPlanName(parentId) {
 | 
				
			||||||
 | 
					      // 如果父计划就是当前主计划
 | 
				
			||||||
 | 
					      if (this.plan && this.plan.id === parentId) {
 | 
				
			||||||
 | 
					        return this.plan.name
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // 检查是否在子计划中
 | 
				
			||||||
 | 
					      if (this.plan && this.plan.sub_plans) {
 | 
				
			||||||
 | 
					        const parentPlan = this.plan.sub_plans.find(plan => plan.id === parentId)
 | 
				
			||||||
 | 
					        if (parentPlan) {
 | 
				
			||||||
 | 
					          return parentPlan.name
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // 默认返回"未知父计划"
 | 
				
			||||||
 | 
					      return '未知父计划'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 编辑计划
 | 
				
			||||||
 | 
					    editPlan() {
 | 
				
			||||||
 | 
					      // TODO: 实现编辑计划逻辑
 | 
				
			||||||
 | 
					      alert('编辑计划功能待实现')
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 退出登录
 | 
				
			||||||
 | 
					    logout() {
 | 
				
			||||||
 | 
					      localStorage.removeItem('authToken')
 | 
				
			||||||
 | 
					      localStorage.removeItem('username')
 | 
				
			||||||
 | 
					      this.$router.push('/')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.feed-plan-detail {
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.header {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  margin-bottom: 30px;
 | 
				
			||||||
 | 
					  padding-bottom: 20px;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #eee;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.header h1 {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.user-info {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 15px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.logout-btn {
 | 
				
			||||||
 | 
					  padding: 8px 16px;
 | 
				
			||||||
 | 
					  background-color: #dc3545;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  transition: background-color 0.3s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.logout-btn:hover {
 | 
				
			||||||
 | 
					  background-color: #c82333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav {
 | 
				
			||||||
 | 
					  background-color: #343a40;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  margin-bottom: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav ul {
 | 
				
			||||||
 | 
					  list-style-type: none;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav li {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav a {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  padding: 15px 20px;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					  transition: background-color 0.3s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav a:hover {
 | 
				
			||||||
 | 
					  background-color: #495057;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.nav a.active {
 | 
				
			||||||
 | 
					  background-color: #007bff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.loading, .error {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  padding: 50px;
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.loading {
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.error {
 | 
				
			||||||
 | 
					  color: #dc3545;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-detail-container {
 | 
				
			||||||
 | 
					  background-color: white;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					  padding: 30px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-header {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  margin-bottom: 30px;
 | 
				
			||||||
 | 
					  padding-bottom: 20px;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #eee;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-header h2 {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-status {
 | 
				
			||||||
 | 
					  padding: 6px 12px;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-status.enabled {
 | 
				
			||||||
 | 
					  background-color: #d4edda;
 | 
				
			||||||
 | 
					  color: #155724;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-status.disabled {
 | 
				
			||||||
 | 
					  background-color: #f8d7da;
 | 
				
			||||||
 | 
					  color: #721c24;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-info {
 | 
				
			||||||
 | 
					  margin-bottom: 30px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.info-item {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  margin-bottom: 15px;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.info-item label {
 | 
				
			||||||
 | 
					  width: 150px;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.info-item span {
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-steps, .sub-plans {
 | 
				
			||||||
 | 
					  margin-bottom: 30px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plan-steps h3, .sub-plans h3 {
 | 
				
			||||||
 | 
					  margin-top: 0;
 | 
				
			||||||
 | 
					  margin-bottom: 20px;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					  padding-bottom: 10px;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #eee;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.steps-list {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  gap: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.step-item {
 | 
				
			||||||
 | 
					  border: 1px solid #ddd;
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  background-color: #f8f9fa;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.step-header {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  margin-bottom: 15px;
 | 
				
			||||||
 | 
					  padding-bottom: 10px;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #eee;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.step-number {
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					  color: #007bff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.step-cron {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					  background-color: #e9ecef;
 | 
				
			||||||
 | 
					  padding: 2px 6px;
 | 
				
			||||||
 | 
					  border-radius: 3px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.step-details {
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
 | 
				
			||||||
 | 
					  gap: 15px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.detail-item {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.detail-item label {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					  margin-bottom: 3px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.detail-item span {
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.no-steps {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  padding: 30px;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					  font-style: italic;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.sub-plans-list {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  gap: 25px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.sub-plan-item {
 | 
				
			||||||
 | 
					  border: 1px solid #ddd;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  background-color: #fff;
 | 
				
			||||||
 | 
					  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.sub-plan-header {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  margin-bottom: 15px;
 | 
				
			||||||
 | 
					  padding-bottom: 10px;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #eee;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.sub-plan-header h4 {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.sub-plan-info {
 | 
				
			||||||
 | 
					  margin-bottom: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.sub-plan-info .info-item {
 | 
				
			||||||
 | 
					  margin-bottom: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.sub-plan-steps h5 {
 | 
				
			||||||
 | 
					  margin-top: 0;
 | 
				
			||||||
 | 
					  margin-bottom: 15px;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.actions {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: flex-end;
 | 
				
			||||||
 | 
					  gap: 15px;
 | 
				
			||||||
 | 
					  padding-top: 20px;
 | 
				
			||||||
 | 
					  border-top: 1px solid #eee;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn {
 | 
				
			||||||
 | 
					  padding: 10px 20px;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  transition: background-color 0.3s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-primary {
 | 
				
			||||||
 | 
					  background-color: #007bff;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-primary:hover {
 | 
				
			||||||
 | 
					  background-color: #0069d9;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-secondary {
 | 
				
			||||||
 | 
					  background-color: #6c757d;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-secondary:hover {
 | 
				
			||||||
 | 
					  background-color: #5a6268;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					  .header {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 15px;
 | 
				
			||||||
 | 
					    align-items: flex-start;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .nav ul {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .info-item {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    align-items: flex-start;
 | 
				
			||||||
 | 
					    gap: 5px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .info-item label {
 | 
				
			||||||
 | 
					    width: auto;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .step-details {
 | 
				
			||||||
 | 
					    grid-template-columns: 1fr;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  .actions {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@@ -2,6 +2,8 @@ import { createRouter, createWebHistory } from 'vue-router'
 | 
				
			|||||||
import Login from '../pages/Login.vue'
 | 
					import Login from '../pages/Login.vue'
 | 
				
			||||||
import Dashboard from '../pages/Dashboard.vue'
 | 
					import Dashboard from '../pages/Dashboard.vue'
 | 
				
			||||||
import Device from '../pages/Device.vue'
 | 
					import Device from '../pages/Device.vue'
 | 
				
			||||||
 | 
					import FeedPlan from '../pages/FeedPlan.vue'
 | 
				
			||||||
 | 
					import FeedPlanDetail from '../pages/FeedPlanDetail.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const routes = [
 | 
					const routes = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
@@ -20,6 +22,19 @@ const routes = [
 | 
				
			|||||||
    name: 'Device',
 | 
					    name: 'Device',
 | 
				
			||||||
    component: Device,
 | 
					    component: Device,
 | 
				
			||||||
    meta: { requiresAuth: true }
 | 
					    meta: { requiresAuth: true }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: '/feed/plan',
 | 
				
			||||||
 | 
					    name: 'FeedPlan',
 | 
				
			||||||
 | 
					    component: FeedPlan,
 | 
				
			||||||
 | 
					    meta: { requiresAuth: true }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: '/feed/plan/detail/:id',
 | 
				
			||||||
 | 
					    name: 'FeedPlanDetail',
 | 
				
			||||||
 | 
					    component: FeedPlanDetail,
 | 
				
			||||||
 | 
					    meta: { requiresAuth: true },
 | 
				
			||||||
 | 
					    props: true
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,15 @@
 | 
				
			|||||||
import { defineConfig } from 'vite'
 | 
					import { defineConfig } from 'vite'
 | 
				
			||||||
import vue from '@vitejs/plugin-vue'
 | 
					import vue from '@vitejs/plugin-vue'
 | 
				
			||||||
 | 
					import { resolve } from 'path'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// https://vitejs.dev/config/
 | 
					// https://vitejs.dev/config/
 | 
				
			||||||
export default defineConfig({
 | 
					export default defineConfig({
 | 
				
			||||||
  plugins: [vue()],
 | 
					  plugins: [vue()],
 | 
				
			||||||
 | 
					  resolve: {
 | 
				
			||||||
 | 
					    alias: {
 | 
				
			||||||
 | 
					      '@': resolve(__dirname, 'src')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  server: {
 | 
					  server: {
 | 
				
			||||||
    port: 3000,
 | 
					    port: 3000,
 | 
				
			||||||
    proxy: {
 | 
					    proxy: {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,6 +13,7 @@ import (
 | 
				
			|||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/api/middleware"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/api/middleware"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/config"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/config"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/controller/device"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/controller/device"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/controller/feed"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/controller/operation"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/controller/operation"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/controller/remote"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/controller/remote"
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/controller/user"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/controller/user"
 | 
				
			||||||
@@ -44,6 +45,9 @@ type API struct {
 | 
				
			|||||||
	// deviceController 设备控制控制器
 | 
						// deviceController 设备控制控制器
 | 
				
			||||||
	deviceController *device.Controller
 | 
						deviceController *device.Controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// feedController 饲喂管理控制器
 | 
				
			||||||
 | 
						feedController *feed.Controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// remoteController 远程控制控制器
 | 
						// remoteController 远程控制控制器
 | 
				
			||||||
	remoteController *remote.Controller
 | 
						remoteController *remote.Controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -65,7 +69,16 @@ type API struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// NewAPI 创建并返回一个新的API实例
 | 
					// NewAPI 创建并返回一个新的API实例
 | 
				
			||||||
// 初始化Gin引擎和相关配置
 | 
					// 初始化Gin引擎和相关配置
 | 
				
			||||||
func NewAPI(cfg *config.Config, userRepo repository.UserRepo, operationHistoryRepo repository.OperationHistoryRepo, deviceControlRepo repository.DeviceControlRepo, deviceRepo repository.DeviceRepo, websocketManager *websocket.Manager, heartbeatService *service.HeartbeatService, deviceStatusPool *service.DeviceStatusPool) *API {
 | 
					func NewAPI(cfg *config.Config,
 | 
				
			||||||
 | 
						userRepo repository.UserRepo,
 | 
				
			||||||
 | 
						operationHistoryRepo repository.OperationHistoryRepo,
 | 
				
			||||||
 | 
						deviceControlRepo repository.DeviceControlRepo,
 | 
				
			||||||
 | 
						deviceRepo repository.DeviceRepo,
 | 
				
			||||||
 | 
						feedRepo repository.FeedPlanRepo,
 | 
				
			||||||
 | 
						websocketManager *websocket.Manager,
 | 
				
			||||||
 | 
						heartbeatService *service.HeartbeatService,
 | 
				
			||||||
 | 
						deviceStatusPool *service.DeviceStatusPool,
 | 
				
			||||||
 | 
					) *API {
 | 
				
			||||||
	// 设置Gin为发布模式
 | 
						// 设置Gin为发布模式
 | 
				
			||||||
	gin.SetMode(gin.DebugMode)
 | 
						gin.SetMode(gin.DebugMode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -98,6 +111,9 @@ func NewAPI(cfg *config.Config, userRepo repository.UserRepo, operationHistoryRe
 | 
				
			|||||||
	// 创建设备控制控制器
 | 
						// 创建设备控制控制器
 | 
				
			||||||
	deviceController := device.NewController(deviceControlRepo, deviceRepo, websocketManager, heartbeatService, deviceStatusPool)
 | 
						deviceController := device.NewController(deviceControlRepo, deviceRepo, websocketManager, heartbeatService, deviceStatusPool)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 创建饲喂管理控制器
 | 
				
			||||||
 | 
						feedController := feed.NewController(feedRepo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 创建远程控制控制器
 | 
						// 创建远程控制控制器
 | 
				
			||||||
	remoteController := remote.NewController(websocketManager)
 | 
						remoteController := remote.NewController(websocketManager)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -110,6 +126,7 @@ func NewAPI(cfg *config.Config, userRepo repository.UserRepo, operationHistoryRe
 | 
				
			|||||||
		userController:      userController,
 | 
							userController:      userController,
 | 
				
			||||||
		operationController: operationController,
 | 
							operationController: operationController,
 | 
				
			||||||
		deviceController:    deviceController,
 | 
							deviceController:    deviceController,
 | 
				
			||||||
 | 
							feedController:      feedController,
 | 
				
			||||||
		remoteController:    remoteController,
 | 
							remoteController:    remoteController,
 | 
				
			||||||
		authMiddleware:      authMiddleware,
 | 
							authMiddleware:      authMiddleware,
 | 
				
			||||||
		websocketManager:    websocketManager,
 | 
							websocketManager:    websocketManager,
 | 
				
			||||||
@@ -222,6 +239,16 @@ func (a *API) setupRoutes() {
 | 
				
			|||||||
			deviceGroup.GET("/status", a.deviceController.GetDeviceStatus)
 | 
								deviceGroup.GET("/status", a.deviceController.GetDeviceStatus)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 饲喂相关路由
 | 
				
			||||||
 | 
							feedGroup := protectedGroup.Group("/feed")
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								feedGroup.GET("/plan/list", a.feedController.ListPlans)
 | 
				
			||||||
 | 
								feedGroup.GET("/plan/detail", a.feedController.Detail)
 | 
				
			||||||
 | 
								feedGroup.POST("/plan/create", a.feedController.Create)
 | 
				
			||||||
 | 
								feedGroup.POST("/plan/update", a.feedController.Update)
 | 
				
			||||||
 | 
								feedGroup.POST("/plan/delete", a.feedController.Delete)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 远程控制相关路由
 | 
							// 远程控制相关路由
 | 
				
			||||||
		remoteGroup := protectedGroup.Group("/remote")
 | 
							remoteGroup := protectedGroup.Group("/remote")
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,14 +3,471 @@
 | 
				
			|||||||
// 通过任务执行器执行具体控制任务
 | 
					// 通过任务执行器执行具体控制任务
 | 
				
			||||||
package feed
 | 
					package feed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// FeedController 饲料控制器
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/controller"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/logs"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/model"
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/storage/repository"
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Controller 饲料控制器
 | 
				
			||||||
// 管理饲料制备和分配设备的控制逻辑
 | 
					// 管理饲料制备和分配设备的控制逻辑
 | 
				
			||||||
type FeedController struct {
 | 
					type Controller struct {
 | 
				
			||||||
	// TODO: 定义饲料控制器结构
 | 
						feedPlanRepo repository.FeedPlanRepo
 | 
				
			||||||
 | 
						logger       *logs.Logger
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewFeedController 创建并返回一个新的饲料控制器实例
 | 
					// NewController 创建并返回一个新的饲料控制器实例
 | 
				
			||||||
func NewFeedController() *FeedController {
 | 
					func NewController(feedPlanRepo repository.FeedPlanRepo) *Controller {
 | 
				
			||||||
	// TODO: 实现饲料控制器初始化
 | 
						// TODO: 实现饲料控制器初始化
 | 
				
			||||||
 | 
						return &Controller{
 | 
				
			||||||
 | 
							feedPlanRepo: feedPlanRepo,
 | 
				
			||||||
 | 
							logger:       logs.NewLogger(),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateRequest 创建计划请求结构体
 | 
				
			||||||
 | 
					type CreateRequest struct {
 | 
				
			||||||
 | 
						// Name 计划名称
 | 
				
			||||||
 | 
						Name string `json:"name"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Description 计划描述
 | 
				
			||||||
 | 
						Description string `json:"description"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Type 计划类型(手动触发/自动触发)
 | 
				
			||||||
 | 
						Type model.FeedingPlanType `json:"type"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Enabled 是否启用
 | 
				
			||||||
 | 
						Enabled bool `json:"enabled"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ScheduleCron 定时任务表达式(仅当Type为auto时有效)
 | 
				
			||||||
 | 
						ScheduleCron *string `json:"schedule_cron,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ExecutionLimit 执行次数限制(0表示无限制,仅当Type为auto时有效)
 | 
				
			||||||
 | 
						ExecutionLimit int `json:"execution_limit"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ParentID 父计划ID(用于支持子计划结构)
 | 
				
			||||||
 | 
						ParentID *uint `json:"parent_id,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// OrderInParent 在父计划中的执行顺序
 | 
				
			||||||
 | 
						OrderInParent *int `json:"order_in_parent,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// IsMaster 是否为主计划(主计划可以包含子计划)
 | 
				
			||||||
 | 
						IsMaster bool `json:"is_master"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Steps 计划步骤列表
 | 
				
			||||||
 | 
						Steps []FeedingPlanStep `json:"steps"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// SubPlans 子计划列表
 | 
				
			||||||
 | 
						SubPlans []CreateRequest `json:"sub_plans"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Create 创建饲料计划
 | 
				
			||||||
 | 
					func (c *Controller) Create(ctx *gin.Context) {
 | 
				
			||||||
 | 
						var req CreateRequest
 | 
				
			||||||
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "请求参数错误: "+err.Error())
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 校验计划结构
 | 
				
			||||||
 | 
						if err := c.validatePlanStructure(&req); err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "计划结构错误: "+err.Error())
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 转换请求结构体为模型
 | 
				
			||||||
 | 
						plan := c.convertToCreateModel(&req)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 调用仓库创建计划
 | 
				
			||||||
 | 
						if err := c.feedPlanRepo.CreateFeedingPlan(plan); err != nil {
 | 
				
			||||||
 | 
							c.logger.Error("创建计划失败: " + err.Error())
 | 
				
			||||||
 | 
							controller.SendErrorResponse(ctx, controller.InternalServerErrorCode, "创建计划失败")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						controller.SendSuccessResponse(ctx, "创建计划成功", nil)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// validatePlanStructure 校验计划结构,不允许计划同时包含步骤和子计划
 | 
				
			||||||
 | 
					func (c *Controller) validatePlanStructure(req *CreateRequest) error {
 | 
				
			||||||
 | 
						// 检查当前计划是否同时包含步骤和子计划
 | 
				
			||||||
 | 
						if len(req.Steps) > 0 && len(req.SubPlans) > 0 {
 | 
				
			||||||
 | 
							return fmt.Errorf("计划不能同时包含步骤和子计划")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 递归检查子计划
 | 
				
			||||||
 | 
						for _, subPlan := range req.SubPlans {
 | 
				
			||||||
 | 
							if err := c.validatePlanStructure(&subPlan); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// convertToCreateModel 将创建请求结构体转换为数据库模型
 | 
				
			||||||
 | 
					func (c *Controller) convertToCreateModel(req *CreateRequest) *model.FeedingPlan {
 | 
				
			||||||
 | 
						plan := &model.FeedingPlan{
 | 
				
			||||||
 | 
							Name:           req.Name,
 | 
				
			||||||
 | 
							Description:    req.Description,
 | 
				
			||||||
 | 
							Type:           req.Type,
 | 
				
			||||||
 | 
							Enabled:        req.Enabled,
 | 
				
			||||||
 | 
							ScheduleCron:   req.ScheduleCron,
 | 
				
			||||||
 | 
							ExecutionLimit: req.ExecutionLimit,
 | 
				
			||||||
 | 
							ParentID:       req.ParentID,
 | 
				
			||||||
 | 
							OrderInParent:  req.OrderInParent,
 | 
				
			||||||
 | 
							// 不需要显式设置ID字段,仓库层会处理
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 转换步骤
 | 
				
			||||||
 | 
						plan.Steps = make([]model.FeedingPlanStep, len(req.Steps))
 | 
				
			||||||
 | 
						for i, step := range req.Steps {
 | 
				
			||||||
 | 
							plan.Steps[i] = model.FeedingPlanStep{
 | 
				
			||||||
 | 
								// ID在创建时不需要设置
 | 
				
			||||||
 | 
								// PlanID会在创建过程中自动设置
 | 
				
			||||||
 | 
								StepOrder:      step.StepOrder,
 | 
				
			||||||
 | 
								DeviceID:       step.DeviceID,
 | 
				
			||||||
 | 
								TargetValue:    step.TargetValue,
 | 
				
			||||||
 | 
								Action:         step.Action,
 | 
				
			||||||
 | 
								ScheduleCron:   step.ScheduleCron,
 | 
				
			||||||
 | 
								ExecutionLimit: step.ExecutionLimit,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 转换子计划
 | 
				
			||||||
 | 
						plan.SubPlans = make([]model.FeedingPlan, len(req.SubPlans))
 | 
				
			||||||
 | 
						for i, subReq := range req.SubPlans {
 | 
				
			||||||
 | 
							plan.SubPlans[i] = *c.convertToCreateModel(&subReq)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return plan
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Delete 删除饲料计划
 | 
				
			||||||
 | 
					func (c *Controller) Delete(ctx *gin.Context) {
 | 
				
			||||||
 | 
						// 获取路径参数中的计划ID
 | 
				
			||||||
 | 
						var req struct {
 | 
				
			||||||
 | 
							ID uint `json:"id" binding:"required"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "请求参数错误: "+err.Error())
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 调用仓库删除计划
 | 
				
			||||||
 | 
						if err := c.feedPlanRepo.DeleteFeedingPlan(uint(req.ID)); err != nil {
 | 
				
			||||||
 | 
							c.logger.Error("删除计划失败: " + err.Error())
 | 
				
			||||||
 | 
							controller.SendErrorResponse(ctx, controller.InternalServerErrorCode, "删除计划失败")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						controller.SendSuccessResponse(ctx, "删除计划成功", nil)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ListPlansResponse struct {
 | 
				
			||||||
 | 
						Plans []ListPlanResponseItem `json:"plans"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ListPlanResponseItem struct {
 | 
				
			||||||
 | 
						// ID 计划ID
 | 
				
			||||||
 | 
						ID uint `json:"id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Name 计划名称
 | 
				
			||||||
 | 
						Name string `json:"name"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Description 计划描述
 | 
				
			||||||
 | 
						Description string `json:"description"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Type 计划类型
 | 
				
			||||||
 | 
						Type model.FeedingPlanType `json:"type"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Enabled 是否启用
 | 
				
			||||||
 | 
						Enabled bool `json:"enabled"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ScheduleCron 定时任务表达式
 | 
				
			||||||
 | 
						ScheduleCron *string `json:"schedule_cron,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListPlans 获取饲料计划列表
 | 
				
			||||||
 | 
					func (c *Controller) ListPlans(ctx *gin.Context) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						introductions, err := c.feedPlanRepo.ListAllPlanIntroduction()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.logger.Error("获取设备列表失败: " + err.Error())
 | 
				
			||||||
 | 
							controller.SendErrorResponse(ctx, controller.InternalServerErrorCode, "获取计划列表失败")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp := ListPlansResponse{
 | 
				
			||||||
 | 
							Plans: []ListPlanResponseItem{},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, introduction := range introductions {
 | 
				
			||||||
 | 
							resp.Plans = append(resp.Plans, ListPlanResponseItem{
 | 
				
			||||||
 | 
								ID:           introduction.ID,
 | 
				
			||||||
 | 
								Name:         introduction.Name,
 | 
				
			||||||
 | 
								Description:  introduction.Description,
 | 
				
			||||||
 | 
								Enabled:      introduction.Enabled,
 | 
				
			||||||
 | 
								Type:         introduction.Type,
 | 
				
			||||||
 | 
								ScheduleCron: introduction.ScheduleCron,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						controller.SendSuccessResponse(ctx, "success", resp)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdateRequest 更新计划请求结构体
 | 
				
			||||||
 | 
					type UpdateRequest struct {
 | 
				
			||||||
 | 
						// ID 计划ID
 | 
				
			||||||
 | 
						ID uint `json:"id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Name 计划名称
 | 
				
			||||||
 | 
						Name string `json:"name"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Description 计划描述
 | 
				
			||||||
 | 
						Description string `json:"description"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Type 计划类型(手动触发/自动触发)
 | 
				
			||||||
 | 
						Type model.FeedingPlanType `json:"type"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Enabled 是否启用
 | 
				
			||||||
 | 
						Enabled bool `json:"enabled"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ScheduleCron 定时任务表达式(仅当Type为auto时有效)
 | 
				
			||||||
 | 
						ScheduleCron *string `json:"schedule_cron,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ExecutionLimit 执行次数限制(0表示无限制,仅当Type为auto时有效)
 | 
				
			||||||
 | 
						ExecutionLimit int `json:"execution_limit"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ParentID 父计划ID(用于支持子计划结构)
 | 
				
			||||||
 | 
						ParentID *uint `json:"parent_id,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// OrderInParent 在父计划中的执行顺序
 | 
				
			||||||
 | 
						OrderInParent *int `json:"order_in_parent,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// IsMaster 是否为主计划(主计划可以包含子计划)
 | 
				
			||||||
 | 
						IsMaster bool `json:"is_master"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Steps 计划步骤列表
 | 
				
			||||||
 | 
						Steps []FeedingPlanStep `json:"steps"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// SubPlans 子计划列表
 | 
				
			||||||
 | 
						SubPlans []UpdateRequest `json:"sub_plans"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DetailResponse 喂料计划主表
 | 
				
			||||||
 | 
					type DetailResponse struct {
 | 
				
			||||||
 | 
						// ID 计划ID
 | 
				
			||||||
 | 
						ID uint `json:"id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Name 计划名称
 | 
				
			||||||
 | 
						Name string `json:"name"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Description 计划描述
 | 
				
			||||||
 | 
						Description string `json:"description"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Type 计划类型(手动触发/自动触发)
 | 
				
			||||||
 | 
						Type model.FeedingPlanType `json:"type"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Enabled 是否启用
 | 
				
			||||||
 | 
						Enabled bool `json:"enabled"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ScheduleCron 定时任务表达式(仅当Type为auto时有效)
 | 
				
			||||||
 | 
						ScheduleCron *string `json:"schedule_cron,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ExecutionLimit 执行次数限制(0表示无限制,仅当Type为auto时有效)
 | 
				
			||||||
 | 
						ExecutionLimit int `json:"execution_limit"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ParentID 父计划ID(用于支持子计划结构)
 | 
				
			||||||
 | 
						ParentID *uint `json:"parent_id,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// OrderInParent 在父计划中的执行顺序
 | 
				
			||||||
 | 
						OrderInParent *int `json:"order_in_parent,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Steps 计划步骤列表
 | 
				
			||||||
 | 
						Steps []FeedingPlanStep `json:"steps"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// SubPlans 子计划列表
 | 
				
			||||||
 | 
						SubPlans []DetailResponse `json:"sub_plans"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FeedingPlanStep 喂料计划步骤表,表示计划中的每个设备动作
 | 
				
			||||||
 | 
					type FeedingPlanStep struct {
 | 
				
			||||||
 | 
						// ID 步骤ID
 | 
				
			||||||
 | 
						ID uint `json:"id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// PlanID 关联的计划ID
 | 
				
			||||||
 | 
						PlanID uint `json:"plan_id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// StepOrder 步骤顺序
 | 
				
			||||||
 | 
						StepOrder int `json:"step_order"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// DeviceID 关联的设备ID
 | 
				
			||||||
 | 
						DeviceID uint `json:"device_id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TargetValue 目标值(达到该值后停止工作切换到下一个设备)
 | 
				
			||||||
 | 
						TargetValue float64 `json:"target_value"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Action 动作(如:打开设备)
 | 
				
			||||||
 | 
						Action string `json:"action"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ScheduleCron 步骤定时任务表达式(可选)
 | 
				
			||||||
 | 
						ScheduleCron *string `json:"schedule_cron,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ExecutionLimit 步骤执行次数限制(0表示无限制)
 | 
				
			||||||
 | 
						ExecutionLimit int `json:"execution_limit"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Detail 获取饲料计划列细节
 | 
				
			||||||
 | 
					func (c *Controller) Detail(ctx *gin.Context) {
 | 
				
			||||||
 | 
						// 获取查询参数中的计划ID
 | 
				
			||||||
 | 
						planIDStr := ctx.Query("id")
 | 
				
			||||||
 | 
						if planIDStr == "" {
 | 
				
			||||||
 | 
							controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "缺少计划ID参数")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						planID, err := strconv.ParseUint(planIDStr, 10, 32)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "无效的计划ID")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 从仓库中获取计划详情
 | 
				
			||||||
 | 
						plan, err := c.feedPlanRepo.FindFeedingPlanByID(uint(planID))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.logger.Error("获取计划详情失败: " + err.Error())
 | 
				
			||||||
 | 
							controller.SendErrorResponse(ctx, controller.InternalServerErrorCode, "获取计划详情失败")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 转换为响应结构体
 | 
				
			||||||
 | 
						resp := c.convertToDetailResponse(plan)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						controller.SendSuccessResponse(ctx, "success", resp)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Update 更新饲料计划
 | 
				
			||||||
 | 
					func (c *Controller) Update(ctx *gin.Context) {
 | 
				
			||||||
 | 
						var req UpdateRequest
 | 
				
			||||||
 | 
						if err := ctx.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "请求参数错误: "+err.Error())
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 校验计划结构
 | 
				
			||||||
 | 
						if err := c.validateUpdatePlanStructure(&req); err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "计划结构错误: "+err.Error())
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 转换请求结构体为模型
 | 
				
			||||||
 | 
						plan := c.convertToUpdateModel(&req)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 调用仓库更新计划
 | 
				
			||||||
 | 
						if err := c.feedPlanRepo.UpdateFeedingPlan(plan); err != nil {
 | 
				
			||||||
 | 
							c.logger.Error("更新计划失败: " + err.Error())
 | 
				
			||||||
 | 
							controller.SendErrorResponse(ctx, controller.InternalServerErrorCode, "更新计划失败")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						controller.SendSuccessResponse(ctx, "更新计划成功", nil)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// validateUpdatePlanStructure 校验更新计划结构,不允许计划同时包含步骤和子计划
 | 
				
			||||||
 | 
					func (c *Controller) validateUpdatePlanStructure(req *UpdateRequest) error {
 | 
				
			||||||
 | 
						// 检查当前计划是否同时包含步骤和子计划
 | 
				
			||||||
 | 
						if len(req.Steps) > 0 && len(req.SubPlans) > 0 {
 | 
				
			||||||
 | 
							return fmt.Errorf("计划不能同时包含步骤和子计划")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 递归检查子计划
 | 
				
			||||||
 | 
						for _, subPlan := range req.SubPlans {
 | 
				
			||||||
 | 
							if err := c.validateUpdatePlanStructure(&subPlan); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// convertToUpdateModel 将更新请求结构体转换为数据库模型
 | 
				
			||||||
 | 
					func (c *Controller) convertToUpdateModel(req *UpdateRequest) *model.FeedingPlan {
 | 
				
			||||||
 | 
						plan := &model.FeedingPlan{
 | 
				
			||||||
 | 
							ID:             req.ID,
 | 
				
			||||||
 | 
							Name:           req.Name,
 | 
				
			||||||
 | 
							Description:    req.Description,
 | 
				
			||||||
 | 
							Type:           req.Type,
 | 
				
			||||||
 | 
							Enabled:        req.Enabled,
 | 
				
			||||||
 | 
							ScheduleCron:   req.ScheduleCron,
 | 
				
			||||||
 | 
							ExecutionLimit: req.ExecutionLimit,
 | 
				
			||||||
 | 
							ParentID:       req.ParentID,
 | 
				
			||||||
 | 
							OrderInParent:  req.OrderInParent,
 | 
				
			||||||
 | 
							Steps:          make([]model.FeedingPlanStep, len(req.Steps)),
 | 
				
			||||||
 | 
							SubPlans:       make([]model.FeedingPlan, len(req.SubPlans)),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 转换步骤
 | 
				
			||||||
 | 
						for i, step := range req.Steps {
 | 
				
			||||||
 | 
							plan.Steps[i] = model.FeedingPlanStep{
 | 
				
			||||||
 | 
								ID:             step.ID,
 | 
				
			||||||
 | 
								PlanID:         step.PlanID,
 | 
				
			||||||
 | 
								StepOrder:      step.StepOrder,
 | 
				
			||||||
 | 
								DeviceID:       step.DeviceID,
 | 
				
			||||||
 | 
								TargetValue:    step.TargetValue,
 | 
				
			||||||
 | 
								Action:         step.Action,
 | 
				
			||||||
 | 
								ScheduleCron:   step.ScheduleCron,
 | 
				
			||||||
 | 
								ExecutionLimit: step.ExecutionLimit,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 转换子计划
 | 
				
			||||||
 | 
						for i, subReq := range req.SubPlans {
 | 
				
			||||||
 | 
							plan.SubPlans[i] = *c.convertToUpdateModel(&subReq)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return plan
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// convertToDetailResponse 将数据库模型转换为响应结构体
 | 
				
			||||||
 | 
					func (c *Controller) convertToDetailResponse(plan *model.FeedingPlan) *DetailResponse {
 | 
				
			||||||
 | 
						resp := &DetailResponse{
 | 
				
			||||||
 | 
							ID:             plan.ID,
 | 
				
			||||||
 | 
							Name:           plan.Name,
 | 
				
			||||||
 | 
							Description:    plan.Description,
 | 
				
			||||||
 | 
							Type:           plan.Type,
 | 
				
			||||||
 | 
							Enabled:        plan.Enabled,
 | 
				
			||||||
 | 
							ScheduleCron:   plan.ScheduleCron,
 | 
				
			||||||
 | 
							ExecutionLimit: plan.ExecutionLimit,
 | 
				
			||||||
 | 
							ParentID:       plan.ParentID,
 | 
				
			||||||
 | 
							OrderInParent:  plan.OrderInParent,
 | 
				
			||||||
 | 
							Steps:          make([]FeedingPlanStep, len(plan.Steps)),
 | 
				
			||||||
 | 
							SubPlans:       make([]DetailResponse, len(plan.SubPlans)),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 转换步骤
 | 
				
			||||||
 | 
						for i, step := range plan.Steps {
 | 
				
			||||||
 | 
							resp.Steps[i] = FeedingPlanStep{
 | 
				
			||||||
 | 
								ID:             step.ID,
 | 
				
			||||||
 | 
								PlanID:         step.PlanID,
 | 
				
			||||||
 | 
								StepOrder:      step.StepOrder,
 | 
				
			||||||
 | 
								DeviceID:       step.DeviceID,
 | 
				
			||||||
 | 
								TargetValue:    step.TargetValue,
 | 
				
			||||||
 | 
								Action:         step.Action,
 | 
				
			||||||
 | 
								ScheduleCron:   step.ScheduleCron,
 | 
				
			||||||
 | 
								ExecutionLimit: step.ExecutionLimit,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 转换子计划
 | 
				
			||||||
 | 
						for i, subPlan := range plan.SubPlans {
 | 
				
			||||||
 | 
							// 递归转换子计划
 | 
				
			||||||
 | 
							resp.SubPlans[i] = *c.convertToDetailResponse(&subPlan)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return resp
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -40,6 +40,9 @@ type Application struct {
 | 
				
			|||||||
	// DeviceRepo 设备仓库实例
 | 
						// DeviceRepo 设备仓库实例
 | 
				
			||||||
	DeviceRepo repository.DeviceRepo
 | 
						DeviceRepo repository.DeviceRepo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// FeedPlanRepo 投喂计划仓库实例
 | 
				
			||||||
 | 
						FeedPlanRepo repository.FeedPlanRepo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// WebsocketManager WebSocket管理器
 | 
						// WebsocketManager WebSocket管理器
 | 
				
			||||||
	WebsocketManager *websocket.Manager
 | 
						WebsocketManager *websocket.Manager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -97,6 +100,8 @@ func (app *Application) Start() error {
 | 
				
			|||||||
	// 初始化设备仓库
 | 
						// 初始化设备仓库
 | 
				
			||||||
	app.DeviceRepo = repository.NewDeviceRepo(app.Storage.GetDB())
 | 
						app.DeviceRepo = repository.NewDeviceRepo(app.Storage.GetDB())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						app.FeedPlanRepo = repository.NewFeedPlanRepo(app.Storage.GetDB())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 初始化设备状态池
 | 
						// 初始化设备状态池
 | 
				
			||||||
	app.DeviceStatusPool = service.NewDeviceStatusPool()
 | 
						app.DeviceStatusPool = service.NewDeviceStatusPool()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -109,7 +114,16 @@ func (app *Application) Start() error {
 | 
				
			|||||||
	app.HeartbeatService = service.NewHeartbeatService(app.WebsocketManager, app.DeviceStatusPool, app.DeviceRepo, app.Config)
 | 
						app.HeartbeatService = service.NewHeartbeatService(app.WebsocketManager, app.DeviceStatusPool, app.DeviceRepo, app.Config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 初始化API组件
 | 
						// 初始化API组件
 | 
				
			||||||
	app.API = api.NewAPI(app.Config, app.UserRepo, app.OperationHistoryRepo, app.DeviceControlRepo, app.DeviceRepo, app.WebsocketManager, app.HeartbeatService, app.DeviceStatusPool)
 | 
						app.API = api.NewAPI(app.Config,
 | 
				
			||||||
 | 
							app.UserRepo,
 | 
				
			||||||
 | 
							app.OperationHistoryRepo,
 | 
				
			||||||
 | 
							app.DeviceControlRepo,
 | 
				
			||||||
 | 
							app.DeviceRepo,
 | 
				
			||||||
 | 
							app.FeedPlanRepo,
 | 
				
			||||||
 | 
							app.WebsocketManager,
 | 
				
			||||||
 | 
							app.HeartbeatService,
 | 
				
			||||||
 | 
							app.DeviceStatusPool,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 初始化任务执行器组件(使用5个工作协程)
 | 
						// 初始化任务执行器组件(使用5个工作协程)
 | 
				
			||||||
	app.TaskExecutor = task.NewExecutor(5)
 | 
						app.TaskExecutor = task.NewExecutor(5)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										193
									
								
								internal/model/feed.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								internal/model/feed.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,193 @@
 | 
				
			|||||||
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FeedingPlanType 喂料计划类型枚举
 | 
				
			||||||
 | 
					type FeedingPlanType string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						// FeedingPlanTypeManual 手动触发
 | 
				
			||||||
 | 
						FeedingPlanTypeManual FeedingPlanType = "manual"
 | 
				
			||||||
 | 
						// FeedingPlanTypeAuto 自动触发
 | 
				
			||||||
 | 
						FeedingPlanTypeAuto FeedingPlanType = "auto"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FeedingPlan 喂料计划主表
 | 
				
			||||||
 | 
					type FeedingPlan struct {
 | 
				
			||||||
 | 
						// ID 计划ID
 | 
				
			||||||
 | 
						ID uint `gorm:"primaryKey;column:id" json:"id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Name 计划名称
 | 
				
			||||||
 | 
						Name string `gorm:"not null;column:name" json:"name"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Description 计划描述
 | 
				
			||||||
 | 
						Description string `gorm:"column:description" json:"description"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Type 计划类型(手动触发/自动触发)
 | 
				
			||||||
 | 
						Type FeedingPlanType `gorm:"not null;column:type" json:"type"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Enabled 是否启用
 | 
				
			||||||
 | 
						Enabled bool `gorm:"not null;default:true;column:enabled" json:"enabled"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ScheduleCron 定时任务表达式(仅当Type为auto时有效)
 | 
				
			||||||
 | 
						ScheduleCron *string `gorm:"column:schedule_cron" json:"schedule_cron,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ExecutionLimit 执行次数限制(0表示无限制,仅当Type为auto时有效)
 | 
				
			||||||
 | 
						ExecutionLimit int `gorm:"not null;default:0;column:execution_limit" json:"execution_limit"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ParentID 父计划ID(用于支持子计划结构)
 | 
				
			||||||
 | 
						ParentID *uint `gorm:"column:parent_id;index" json:"parent_id,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// OrderInParent 在父计划中的执行顺序
 | 
				
			||||||
 | 
						OrderInParent *int `gorm:"column:order_in_parent" json:"order_in_parent,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// CreatedAt 创建时间
 | 
				
			||||||
 | 
						CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// UpdatedAt 更新时间
 | 
				
			||||||
 | 
						UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// DeletedAt 删除时间(用于软删除)
 | 
				
			||||||
 | 
						DeletedAt gorm.DeletedAt `gorm:"index;column:deleted_at" json:"-"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Steps 计划步骤列表
 | 
				
			||||||
 | 
						Steps []FeedingPlanStep `gorm:"foreignKey:PlanID" json:"-"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// SubPlans 子计划列表
 | 
				
			||||||
 | 
						SubPlans []FeedingPlan `gorm:"foreignKey:ParentID" json:"-"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TableName 指定FeedingPlan模型对应的数据库表名
 | 
				
			||||||
 | 
					func (FeedingPlan) TableName() string {
 | 
				
			||||||
 | 
						return "feeding_plans"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FeedingPlanStep 喂料计划步骤表,表示计划中的每个设备动作
 | 
				
			||||||
 | 
					type FeedingPlanStep struct {
 | 
				
			||||||
 | 
						// ID 步骤ID
 | 
				
			||||||
 | 
						ID uint `gorm:"primaryKey;column:id" json:"id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// PlanID 关联的计划ID
 | 
				
			||||||
 | 
						PlanID uint `gorm:"not null;column:plan_id;index" json:"plan_id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// StepOrder 步骤顺序
 | 
				
			||||||
 | 
						StepOrder int `gorm:"not null;column:step_order" json:"step_order"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// DeviceID 关联的设备ID
 | 
				
			||||||
 | 
						DeviceID uint `gorm:"not null;column:device_id;index" json:"device_id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TargetValue 目标值(达到该值后停止工作切换到下一个设备)
 | 
				
			||||||
 | 
						TargetValue float64 `gorm:"not null;column:target_value" json:"target_value"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Action 动作(如:打开设备)
 | 
				
			||||||
 | 
						Action string `gorm:"not null;column:action" json:"action"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ScheduleCron 步骤定时任务表达式(可选)
 | 
				
			||||||
 | 
						ScheduleCron *string `gorm:"column:schedule_cron" json:"schedule_cron,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ExecutionLimit 步骤执行次数限制(0表示无限制)
 | 
				
			||||||
 | 
						ExecutionLimit int `gorm:"not null;default:0;column:execution_limit" json:"execution_limit"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// CreatedAt 创建时间
 | 
				
			||||||
 | 
						CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// UpdatedAt 更新时间
 | 
				
			||||||
 | 
						UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// DeletedAt 删除时间(用于软删除)
 | 
				
			||||||
 | 
						DeletedAt gorm.DeletedAt `gorm:"index;column:deleted_at" json:"-"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TableName 指定FeedingPlanStep模型对应的数据库表名
 | 
				
			||||||
 | 
					func (FeedingPlanStep) TableName() string {
 | 
				
			||||||
 | 
						return "feeding_plan_steps"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FeedingExecution 喂料执行记录表
 | 
				
			||||||
 | 
					type FeedingExecution struct {
 | 
				
			||||||
 | 
						// ID 执行记录ID
 | 
				
			||||||
 | 
						ID uint `gorm:"primaryKey;column:id" json:"id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// PlanID 关联的计划ID
 | 
				
			||||||
 | 
						PlanID uint `gorm:"not null;column:plan_id;index" json:"plan_id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// MasterPlanID 主计划ID(如果是子计划执行,记录主计划ID)
 | 
				
			||||||
 | 
						MasterPlanID *uint `gorm:"column:master_plan_id;index" json:"master_plan_id,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TriggerType 触发类型(手动/自动)
 | 
				
			||||||
 | 
						TriggerType FeedingPlanType `gorm:"not null;column:trigger_type" json:"trigger_type"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Status 执行状态(进行中/已完成/已取消/失败)
 | 
				
			||||||
 | 
						Status string `gorm:"not null;column:status" json:"status"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// StartedAt 开始执行时间
 | 
				
			||||||
 | 
						StartedAt *time.Time `gorm:"column:started_at" json:"started_at,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// FinishedAt 完成时间
 | 
				
			||||||
 | 
						FinishedAt *time.Time `gorm:"column:finished_at" json:"finished_at,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// CreatedAt 创建时间
 | 
				
			||||||
 | 
						CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// UpdatedAt 更新时间
 | 
				
			||||||
 | 
						UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// DeletedAt 删除时间(用于软删除)
 | 
				
			||||||
 | 
						DeletedAt gorm.DeletedAt `gorm:"index;column:deleted_at" json:"-"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Steps 执行步骤详情
 | 
				
			||||||
 | 
						Steps []FeedingExecutionStep `gorm:"foreignKey:ExecutionID" json:"-"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TableName 指定FeedingExecution模型对应的数据库表名
 | 
				
			||||||
 | 
					func (FeedingExecution) TableName() string {
 | 
				
			||||||
 | 
						return "feeding_executions"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FeedingExecutionStep 喂料执行步骤详情表
 | 
				
			||||||
 | 
					type FeedingExecutionStep struct {
 | 
				
			||||||
 | 
						// ID 执行步骤ID
 | 
				
			||||||
 | 
						ID uint `gorm:"primaryKey;column:id" json:"id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ExecutionID 关联的执行记录ID
 | 
				
			||||||
 | 
						ExecutionID uint `gorm:"not null;column:execution_id;index" json:"execution_id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// StepID 关联的计划步骤ID
 | 
				
			||||||
 | 
						StepID uint `gorm:"not null;column:step_id;index" json:"step_id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// DeviceID 关联的设备ID
 | 
				
			||||||
 | 
						DeviceID uint `gorm:"not null;column:device_id;index" json:"device_id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TargetValue 目标值
 | 
				
			||||||
 | 
						TargetValue float64 `gorm:"not null;column:target_value" json:"target_value"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ActualValue 实际值
 | 
				
			||||||
 | 
						ActualValue *float64 `gorm:"column:actual_value" json:"actual_value,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Status 步骤状态(待执行/执行中/已完成/失败)
 | 
				
			||||||
 | 
						Status string `gorm:"not null;column:status" json:"status"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// StartedAt 开始执行时间
 | 
				
			||||||
 | 
						StartedAt *time.Time `gorm:"column:started_at" json:"started_at,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// FinishedAt 完成时间
 | 
				
			||||||
 | 
						FinishedAt *time.Time `gorm:"column:finished_at" json:"finished_at,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// CreatedAt 创建时间
 | 
				
			||||||
 | 
						CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// UpdatedAt 更新时间
 | 
				
			||||||
 | 
						UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// DeletedAt 删除时间(用于软删除)
 | 
				
			||||||
 | 
						DeletedAt gorm.DeletedAt `gorm:"index;column:deleted_at" json:"-"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TableName 指定FeedingExecutionStep模型对应的数据库表名
 | 
				
			||||||
 | 
					func (FeedingExecutionStep) TableName() string {
 | 
				
			||||||
 | 
						return "feeding_execution_steps"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -19,6 +19,10 @@ var migrateModels = []interface{}{
 | 
				
			|||||||
	&model.OperationHistory{},
 | 
						&model.OperationHistory{},
 | 
				
			||||||
	&model.Device{},
 | 
						&model.Device{},
 | 
				
			||||||
	&model.DeviceControl{},
 | 
						&model.DeviceControl{},
 | 
				
			||||||
 | 
						&model.FeedingPlan{},
 | 
				
			||||||
 | 
						&model.FeedingPlanStep{},
 | 
				
			||||||
 | 
						&model.FeedingExecution{},
 | 
				
			||||||
 | 
						&model.FeedingExecutionStep{},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PostgresStorage 代表基于PostgreSQL的存储实现
 | 
					// PostgresStorage 代表基于PostgreSQL的存储实现
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										205
									
								
								internal/storage/repository/feed.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								internal/storage/repository/feed.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,205 @@
 | 
				
			|||||||
 | 
					package repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"sort"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/model"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FeedPlanRepo 饲喂管理接口
 | 
				
			||||||
 | 
					type FeedPlanRepo interface {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ListAllPlanIntroduction 获取所有计划简介
 | 
				
			||||||
 | 
						ListAllPlanIntroduction() ([]*model.FeedingPlan, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// FindFeedingPlanByID 根据ID获取计划详情
 | 
				
			||||||
 | 
						FindFeedingPlanByID(id uint) (*model.FeedingPlan, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// CreateFeedingPlan 创建饲料计划
 | 
				
			||||||
 | 
						CreateFeedingPlan(feedingPlan *model.FeedingPlan) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// DeleteFeedingPlan 删除饲料计划及其所有子计划和步骤
 | 
				
			||||||
 | 
						DeleteFeedingPlan(id uint) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// UpdateFeedingPlan 更新饲料计划,采用先删除再重新创建的方式
 | 
				
			||||||
 | 
						UpdateFeedingPlan(feedingPlan *model.FeedingPlan) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type feedPlanRepo struct {
 | 
				
			||||||
 | 
						db *gorm.DB
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewFeedPlanRepo(db *gorm.DB) FeedPlanRepo {
 | 
				
			||||||
 | 
						return &feedPlanRepo{
 | 
				
			||||||
 | 
							db: db,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListAllPlanIntroduction 获取所有计划简介
 | 
				
			||||||
 | 
					func (f *feedPlanRepo) ListAllPlanIntroduction() ([]*model.FeedingPlan, error) {
 | 
				
			||||||
 | 
						var plans []*model.FeedingPlan
 | 
				
			||||||
 | 
						err := f.db.Model(&model.FeedingPlan{}).
 | 
				
			||||||
 | 
							Select("id, name, description, type, enabled, schedule_cron").
 | 
				
			||||||
 | 
							Find(&plans).Error
 | 
				
			||||||
 | 
						return plans, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FindFeedingPlanByID 根据ID获取计划详情
 | 
				
			||||||
 | 
					func (f *feedPlanRepo) FindFeedingPlanByID(feedingPlanID uint) (*model.FeedingPlan, error) {
 | 
				
			||||||
 | 
						var plan model.FeedingPlan
 | 
				
			||||||
 | 
						err := f.db.Where("id = ?", feedingPlanID).
 | 
				
			||||||
 | 
							Preload("Steps").
 | 
				
			||||||
 | 
							Preload("SubPlans").
 | 
				
			||||||
 | 
							First(&plan).Error
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &plan, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreateFeedingPlan 创建饲料计划,包括步骤和子计划
 | 
				
			||||||
 | 
					func (f *feedPlanRepo) CreateFeedingPlan(feedingPlan *model.FeedingPlan) error {
 | 
				
			||||||
 | 
						// 清空所有ID,确保创建新记录
 | 
				
			||||||
 | 
						f.clearAllIDs(feedingPlan)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return f.db.Transaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							return f.createFeedingPlanWithTx(tx, feedingPlan)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UpdateFeedingPlan 更新饲料计划,采用先删除再重新创建的方式
 | 
				
			||||||
 | 
					func (f *feedPlanRepo) UpdateFeedingPlan(feedingPlan *model.FeedingPlan) error {
 | 
				
			||||||
 | 
						// 检查计划是否存在
 | 
				
			||||||
 | 
						_, err := f.FindFeedingPlanByID(feedingPlan.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return f.db.Transaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 先删除原有的计划
 | 
				
			||||||
 | 
							if err := f.deleteFeedingPlanWithTx(tx, feedingPlan.ID); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 清空所有ID,包括子计划和步骤的ID
 | 
				
			||||||
 | 
							f.clearAllIDs(feedingPlan)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 再重新创建更新后的计划
 | 
				
			||||||
 | 
							if err := f.createFeedingPlanWithTx(tx, feedingPlan); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeleteFeedingPlan 删除饲料计划及其所有子计划和步骤
 | 
				
			||||||
 | 
					func (f *feedPlanRepo) DeleteFeedingPlan(id uint) error {
 | 
				
			||||||
 | 
						return f.db.Transaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							// 递归删除计划及其所有子计划
 | 
				
			||||||
 | 
							if err := f.deleteFeedingPlanWithTx(tx, id); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// deleteFeedingPlanWithTx 在事务中递归删除饲料计划
 | 
				
			||||||
 | 
					func (f *feedPlanRepo) deleteFeedingPlanWithTx(tx *gorm.DB, id uint) error {
 | 
				
			||||||
 | 
						// 先查找计划及其子计划
 | 
				
			||||||
 | 
						var plan model.FeedingPlan
 | 
				
			||||||
 | 
						if err := tx.Where("id = ?", id).Preload("SubPlans").First(&plan).Error; err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 递归删除所有子计划
 | 
				
			||||||
 | 
						for _, subPlan := range plan.SubPlans {
 | 
				
			||||||
 | 
							if err := f.deleteFeedingPlanWithTx(tx, subPlan.ID); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 删除该计划的所有步骤
 | 
				
			||||||
 | 
						if err := tx.Where("plan_id = ?", id).Delete(&model.FeedingPlanStep{}).Error; err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 删除计划本身
 | 
				
			||||||
 | 
						if err := tx.Delete(&model.FeedingPlan{}, id).Error; err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// createFeedingPlanWithTx 在事务中递归创建饲料计划
 | 
				
			||||||
 | 
					func (f *feedPlanRepo) createFeedingPlanWithTx(tx *gorm.DB, feedingPlan *model.FeedingPlan) error {
 | 
				
			||||||
 | 
						// 先创建计划主体
 | 
				
			||||||
 | 
						if err := tx.Create(feedingPlan).Error; err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 处理步骤 - 先按现有顺序排序,再重新分配从0开始的连续编号
 | 
				
			||||||
 | 
						sort.Slice(feedingPlan.Steps, func(i, j int) bool {
 | 
				
			||||||
 | 
							return feedingPlan.Steps[i].StepOrder < feedingPlan.Steps[j].StepOrder
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 重新填充步骤编号
 | 
				
			||||||
 | 
						for i := range feedingPlan.Steps {
 | 
				
			||||||
 | 
							feedingPlan.Steps[i].StepOrder = i
 | 
				
			||||||
 | 
							feedingPlan.Steps[i].PlanID = feedingPlan.ID
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 如果有步骤,批量创建步骤
 | 
				
			||||||
 | 
						if len(feedingPlan.Steps) > 0 {
 | 
				
			||||||
 | 
							if err := tx.Create(&feedingPlan.Steps).Error; err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 处理子计划 - 重新填充子计划编号和父ID
 | 
				
			||||||
 | 
						sort.Slice(feedingPlan.SubPlans, func(i, j int) bool {
 | 
				
			||||||
 | 
							// 如果OrderInParent为nil,放在最后
 | 
				
			||||||
 | 
							if feedingPlan.SubPlans[i].OrderInParent == nil {
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if feedingPlan.SubPlans[j].OrderInParent == nil {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return *feedingPlan.SubPlans[i].OrderInParent < *feedingPlan.SubPlans[j].OrderInParent
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 重新填充子计划编号和父ID
 | 
				
			||||||
 | 
						for i := range feedingPlan.SubPlans {
 | 
				
			||||||
 | 
							order := i
 | 
				
			||||||
 | 
							feedingPlan.SubPlans[i].OrderInParent = &order
 | 
				
			||||||
 | 
							feedingPlan.SubPlans[i].ParentID = &feedingPlan.ID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 递归创建子计划
 | 
				
			||||||
 | 
							if err := f.createFeedingPlanWithTx(tx, &feedingPlan.SubPlans[i]); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// clearAllIDs 清空计划及其子计划和步骤的所有ID
 | 
				
			||||||
 | 
					func (f *feedPlanRepo) clearAllIDs(plan *model.FeedingPlan) {
 | 
				
			||||||
 | 
						// 清空计划ID
 | 
				
			||||||
 | 
						plan.ID = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 清空所有步骤的ID和关联的计划ID
 | 
				
			||||||
 | 
						for i := range plan.Steps {
 | 
				
			||||||
 | 
							plan.Steps[i].ID = 0
 | 
				
			||||||
 | 
							plan.Steps[i].PlanID = 0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 清空所有子计划的ID和关联的父计划ID,并递归清空子计划的ID
 | 
				
			||||||
 | 
						for i := range plan.SubPlans {
 | 
				
			||||||
 | 
							plan.SubPlans[i].ID = 0
 | 
				
			||||||
 | 
							plan.SubPlans[i].ParentID = nil
 | 
				
			||||||
 | 
							f.clearAllIDs(&plan.SubPlans[i])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -23,6 +23,12 @@ type Task interface {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// GetPriority 获取任务优先级
 | 
						// GetPriority 获取任务优先级
 | 
				
			||||||
	GetPriority() int
 | 
						GetPriority() int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Done 返回一个channel,当任务执行完毕时该channel会被关闭
 | 
				
			||||||
 | 
						Done() <-chan struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// IsDone 检查任务是否已完成
 | 
				
			||||||
 | 
						IsDone() bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// taskItem 任务队列中的元素
 | 
					// taskItem 任务队列中的元素
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user