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