Compare commits
	
		
			4 Commits
		
	
	
		
			cc7ea94e41
			...
			6fe73d8ffe
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6fe73d8ffe | |||
| 91f160b07e | |||
| a1950872fc | |||
| c499571c11 | 
@@ -32,6 +32,6 @@ websocket:
 | 
				
			|||||||
# 心跳配置
 | 
					# 心跳配置
 | 
				
			||||||
heartbeat:
 | 
					heartbeat:
 | 
				
			||||||
  # 心跳间隔(秒)
 | 
					  # 心跳间隔(秒)
 | 
				
			||||||
  interval: 5
 | 
					  interval: 30
 | 
				
			||||||
  # 请求并发数
 | 
					  # 请求并发数
 | 
				
			||||||
  concurrency: 5
 | 
					  concurrency: 5
 | 
				
			||||||
							
								
								
									
										21
									
								
								frontend/dist/assets/index.0581bd6d.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								frontend/dist/assets/index.0581bd6d.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										21
									
								
								frontend/dist/assets/index.48fb8fe6.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								frontend/dist/assets/index.48fb8fe6.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.0581bd6d.js"></script>
 | 
					  <script type="module" crossorigin src="/assets/index.48fb8fe6.js"></script>
 | 
				
			||||||
  <link rel="stylesheet" href="/assets/index.42c8d2d4.css">
 | 
					  <link rel="stylesheet" href="/assets/index.2ad61d14.css">
 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
<body>
 | 
					<body>
 | 
				
			||||||
    <div id="app"></div>
 | 
					    <div id="app"></div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -108,7 +108,7 @@ export default {
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    // 查看详情
 | 
					    // 查看详情
 | 
				
			||||||
    viewDetail(planId) {
 | 
					    viewDetail(planId) {
 | 
				
			||||||
      this.$router.push(`/feed/plan/${planId}`)
 | 
					      this.$router.push(`/feed/plan/detail/${planId}`)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // 创建计划
 | 
					    // 创建计划
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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>
 | 
				
			||||||
@@ -3,6 +3,7 @@ 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 FeedPlan from '../pages/FeedPlan.vue'
 | 
				
			||||||
 | 
					import FeedPlanDetail from '../pages/FeedPlanDetail.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const routes = [
 | 
					const routes = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
@@ -27,6 +28,13 @@ const routes = [
 | 
				
			|||||||
    name: 'FeedPlan',
 | 
					    name: 'FeedPlan',
 | 
				
			||||||
    component: FeedPlan,
 | 
					    component: FeedPlan,
 | 
				
			||||||
    meta: { requiresAuth: true }
 | 
					    meta: { requiresAuth: true }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: '/feed/plan/detail/:id',
 | 
				
			||||||
 | 
					    name: 'FeedPlanDetail',
 | 
				
			||||||
 | 
					    component: FeedPlanDetail,
 | 
				
			||||||
 | 
					    meta: { requiresAuth: true },
 | 
				
			||||||
 | 
					    props: true
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@
 | 
				
			|||||||
package feed
 | 
					package feed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.huangwc.com/pig/pig-farm-controller/internal/controller"
 | 
						"git.huangwc.com/pig/pig-farm-controller/internal/controller"
 | 
				
			||||||
@@ -44,25 +45,25 @@ type CreateRequest struct {
 | 
				
			|||||||
	Enabled bool `json:"enabled"`
 | 
						Enabled bool `json:"enabled"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ScheduleCron 定时任务表达式(仅当Type为auto时有效)
 | 
						// ScheduleCron 定时任务表达式(仅当Type为auto时有效)
 | 
				
			||||||
	ScheduleCron *string `json:"scheduleCron"`
 | 
						ScheduleCron *string `json:"schedule_cron,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ExecutionLimit 执行次数限制(0表示无限制,仅当Type为auto时有效)
 | 
						// ExecutionLimit 执行次数限制(0表示无限制,仅当Type为auto时有效)
 | 
				
			||||||
	ExecutionLimit int `json:"executionLimit"`
 | 
						ExecutionLimit int `json:"execution_limit"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ParentID 父计划ID(用于支持子计划结构)
 | 
						// ParentID 父计划ID(用于支持子计划结构)
 | 
				
			||||||
	ParentID *uint `json:"parentID"`
 | 
						ParentID *uint `json:"parent_id,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// OrderInParent 在父计划中的执行顺序
 | 
						// OrderInParent 在父计划中的执行顺序
 | 
				
			||||||
	OrderInParent *int `json:"orderInParent"`
 | 
						OrderInParent *int `json:"order_in_parent,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// IsMaster 是否为主计划(主计划可以包含子计划)
 | 
						// IsMaster 是否为主计划(主计划可以包含子计划)
 | 
				
			||||||
	IsMaster bool `json:"isMaster"`
 | 
						IsMaster bool `json:"is_master"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Steps 计划步骤列表
 | 
						// Steps 计划步骤列表
 | 
				
			||||||
	Steps []FeedingPlanStep `json:"steps"`
 | 
						Steps []FeedingPlanStep `json:"steps"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// SubPlans 子计划列表
 | 
						// SubPlans 子计划列表
 | 
				
			||||||
	SubPlans []CreateRequest `json:"subPlans"`
 | 
						SubPlans []CreateRequest `json:"sub_plans"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Create 创建饲料计划
 | 
					// Create 创建饲料计划
 | 
				
			||||||
@@ -73,6 +74,12 @@ func (c *Controller) Create(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 校验计划结构
 | 
				
			||||||
 | 
						if err := c.validatePlanStructure(&req); err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "计划结构错误: "+err.Error())
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 转换请求结构体为模型
 | 
						// 转换请求结构体为模型
 | 
				
			||||||
	plan := c.convertToCreateModel(&req)
 | 
						plan := c.convertToCreateModel(&req)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -86,6 +93,23 @@ func (c *Controller) Create(ctx *gin.Context) {
 | 
				
			|||||||
	controller.SendSuccessResponse(ctx, "创建计划成功", nil)
 | 
						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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// convertToCreateModel 将创建请求结构体转换为数据库模型
 | 
					// convertToCreateModel 将创建请求结构体转换为数据库模型
 | 
				
			||||||
func (c *Controller) convertToCreateModel(req *CreateRequest) *model.FeedingPlan {
 | 
					func (c *Controller) convertToCreateModel(req *CreateRequest) *model.FeedingPlan {
 | 
				
			||||||
	plan := &model.FeedingPlan{
 | 
						plan := &model.FeedingPlan{
 | 
				
			||||||
@@ -183,11 +207,12 @@ func (c *Controller) ListPlans(ctx *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	for _, introduction := range introductions {
 | 
						for _, introduction := range introductions {
 | 
				
			||||||
		resp.Plans = append(resp.Plans, ListPlanResponseItem{
 | 
							resp.Plans = append(resp.Plans, ListPlanResponseItem{
 | 
				
			||||||
			ID:          introduction.ID,
 | 
								ID:           introduction.ID,
 | 
				
			||||||
			Name:        introduction.Name,
 | 
								Name:         introduction.Name,
 | 
				
			||||||
			Description: introduction.Description,
 | 
								Description:  introduction.Description,
 | 
				
			||||||
			Enabled:     introduction.Enabled,
 | 
								Enabled:      introduction.Enabled,
 | 
				
			||||||
			Type:        introduction.Type,
 | 
								Type:         introduction.Type,
 | 
				
			||||||
 | 
								ScheduleCron: introduction.ScheduleCron,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -212,25 +237,25 @@ type UpdateRequest struct {
 | 
				
			|||||||
	Enabled bool `json:"enabled"`
 | 
						Enabled bool `json:"enabled"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ScheduleCron 定时任务表达式(仅当Type为auto时有效)
 | 
						// ScheduleCron 定时任务表达式(仅当Type为auto时有效)
 | 
				
			||||||
	ScheduleCron *string `json:"scheduleCron"`
 | 
						ScheduleCron *string `json:"schedule_cron,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ExecutionLimit 执行次数限制(0表示无限制,仅当Type为auto时有效)
 | 
						// ExecutionLimit 执行次数限制(0表示无限制,仅当Type为auto时有效)
 | 
				
			||||||
	ExecutionLimit int `json:"executionLimit"`
 | 
						ExecutionLimit int `json:"execution_limit"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ParentID 父计划ID(用于支持子计划结构)
 | 
						// ParentID 父计划ID(用于支持子计划结构)
 | 
				
			||||||
	ParentID *uint `json:"parentID"`
 | 
						ParentID *uint `json:"parent_id,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// OrderInParent 在父计划中的执行顺序
 | 
						// OrderInParent 在父计划中的执行顺序
 | 
				
			||||||
	OrderInParent *int `json:"orderInParent"`
 | 
						OrderInParent *int `json:"order_in_parent,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// IsMaster 是否为主计划(主计划可以包含子计划)
 | 
						// IsMaster 是否为主计划(主计划可以包含子计划)
 | 
				
			||||||
	IsMaster bool `json:"isMaster"`
 | 
						IsMaster bool `json:"is_master"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Steps 计划步骤列表
 | 
						// Steps 计划步骤列表
 | 
				
			||||||
	Steps []FeedingPlanStep `json:"steps"`
 | 
						Steps []FeedingPlanStep `json:"steps"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// SubPlans 子计划列表
 | 
						// SubPlans 子计划列表
 | 
				
			||||||
	SubPlans []UpdateRequest `json:"subPlans"`
 | 
						SubPlans []UpdateRequest `json:"sub_plans"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// DetailResponse 喂料计划主表
 | 
					// DetailResponse 喂料计划主表
 | 
				
			||||||
@@ -251,22 +276,22 @@ type DetailResponse struct {
 | 
				
			|||||||
	Enabled bool `json:"enabled"`
 | 
						Enabled bool `json:"enabled"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ScheduleCron 定时任务表达式(仅当Type为auto时有效)
 | 
						// ScheduleCron 定时任务表达式(仅当Type为auto时有效)
 | 
				
			||||||
	ScheduleCron *string `json:"scheduleCron"`
 | 
						ScheduleCron *string `json:"schedule_cron,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ExecutionLimit 执行次数限制(0表示无限制,仅当Type为auto时有效)
 | 
						// ExecutionLimit 执行次数限制(0表示无限制,仅当Type为auto时有效)
 | 
				
			||||||
	ExecutionLimit int `json:"executionLimit"`
 | 
						ExecutionLimit int `json:"execution_limit"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ParentID 父计划ID(用于支持子计划结构)
 | 
						// ParentID 父计划ID(用于支持子计划结构)
 | 
				
			||||||
	ParentID *uint `json:"parentID"`
 | 
						ParentID *uint `json:"parent_id,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// OrderInParent 在父计划中的执行顺序
 | 
						// OrderInParent 在父计划中的执行顺序
 | 
				
			||||||
	OrderInParent *int `json:"orderInParent"`
 | 
						OrderInParent *int `json:"order_in_parent,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Steps 计划步骤列表
 | 
						// Steps 计划步骤列表
 | 
				
			||||||
	Steps []FeedingPlanStep `json:"steps"`
 | 
						Steps []FeedingPlanStep `json:"steps"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// SubPlans 子计划列表
 | 
						// SubPlans 子计划列表
 | 
				
			||||||
	SubPlans []DetailResponse `json:"subPlans"`
 | 
						SubPlans []DetailResponse `json:"sub_plans"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// FeedingPlanStep 喂料计划步骤表,表示计划中的每个设备动作
 | 
					// FeedingPlanStep 喂料计划步骤表,表示计划中的每个设备动作
 | 
				
			||||||
@@ -275,31 +300,36 @@ type FeedingPlanStep struct {
 | 
				
			|||||||
	ID uint `json:"id"`
 | 
						ID uint `json:"id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// PlanID 关联的计划ID
 | 
						// PlanID 关联的计划ID
 | 
				
			||||||
	PlanID uint `json:"planID"`
 | 
						PlanID uint `json:"plan_id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// StepOrder 步骤顺序
 | 
						// StepOrder 步骤顺序
 | 
				
			||||||
	StepOrder int `json:"stepOrder"`
 | 
						StepOrder int `json:"step_order"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// DeviceID 关联的设备ID
 | 
						// DeviceID 关联的设备ID
 | 
				
			||||||
	DeviceID uint `json:"deviceID"`
 | 
						DeviceID uint `json:"device_id"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// TargetValue 目标值(达到该值后停止工作切换到下一个设备)
 | 
						// TargetValue 目标值(达到该值后停止工作切换到下一个设备)
 | 
				
			||||||
	TargetValue float64 `json:"targetValue"`
 | 
						TargetValue float64 `json:"target_value"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Action 动作(如:打开设备)
 | 
						// Action 动作(如:打开设备)
 | 
				
			||||||
	Action string `json:"action"`
 | 
						Action string `json:"action"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ScheduleCron 步骤定时任务表达式(可选)
 | 
						// ScheduleCron 步骤定时任务表达式(可选)
 | 
				
			||||||
	ScheduleCron *string `json:"scheduleCron"`
 | 
						ScheduleCron *string `json:"schedule_cron,omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// ExecutionLimit 步骤执行次数限制(0表示无限制)
 | 
						// ExecutionLimit 步骤执行次数限制(0表示无限制)
 | 
				
			||||||
	ExecutionLimit int `json:"executionLimit"`
 | 
						ExecutionLimit int `json:"execution_limit"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Detail 获取饲料计划列细节
 | 
					// Detail 获取饲料计划列细节
 | 
				
			||||||
func (c *Controller) Detail(ctx *gin.Context) {
 | 
					func (c *Controller) Detail(ctx *gin.Context) {
 | 
				
			||||||
	// 获取路径参数中的计划ID
 | 
						// 获取查询参数中的计划ID
 | 
				
			||||||
	planIDStr := ctx.Param("id")
 | 
						planIDStr := ctx.Query("id")
 | 
				
			||||||
 | 
						if planIDStr == "" {
 | 
				
			||||||
 | 
							controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "缺少计划ID参数")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	planID, err := strconv.ParseUint(planIDStr, 10, 32)
 | 
						planID, err := strconv.ParseUint(planIDStr, 10, 32)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "无效的计划ID")
 | 
							controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "无效的计划ID")
 | 
				
			||||||
@@ -328,6 +358,12 @@ func (c *Controller) Update(ctx *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 校验计划结构
 | 
				
			||||||
 | 
						if err := c.validateUpdatePlanStructure(&req); err != nil {
 | 
				
			||||||
 | 
							controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "计划结构错误: "+err.Error())
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 转换请求结构体为模型
 | 
						// 转换请求结构体为模型
 | 
				
			||||||
	plan := c.convertToUpdateModel(&req)
 | 
						plan := c.convertToUpdateModel(&req)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -341,6 +377,23 @@ func (c *Controller) Update(ctx *gin.Context) {
 | 
				
			|||||||
	controller.SendSuccessResponse(ctx, "更新计划成功", nil)
 | 
						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 将更新请求结构体转换为数据库模型
 | 
					// convertToUpdateModel 将更新请求结构体转换为数据库模型
 | 
				
			||||||
func (c *Controller) convertToUpdateModel(req *UpdateRequest) *model.FeedingPlan {
 | 
					func (c *Controller) convertToUpdateModel(req *UpdateRequest) *model.FeedingPlan {
 | 
				
			||||||
	plan := &model.FeedingPlan{
 | 
						plan := &model.FeedingPlan{
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user