1.修复参数解析bug

2. 增加查看计划详情的界面
This commit is contained in:
2025-09-10 19:03:41 +08:00
parent c499571c11
commit a1950872fc
9 changed files with 664 additions and 54 deletions

View File

@@ -32,6 +32,6 @@ websocket:
# 心跳配置
heartbeat:
# 心跳间隔(秒)
interval: 5
interval: 30
# 请求并发数
concurrency: 5

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.9fd2dfb4.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>猪场管理系统</title>
<script type="module" crossorigin src="/assets/index.0581bd6d.js"></script>
<link rel="stylesheet" href="/assets/index.42c8d2d4.css">
<script type="module" crossorigin src="/assets/index.9fd2dfb4.js"></script>
<link rel="stylesheet" href="/assets/index.9a05129d.css">
</head>
<body>
<div id="app"></div>

View File

@@ -108,7 +108,7 @@ export default {
// 查看详情
viewDetail(planId) {
this.$router.push(`/feed/plan/${planId}`)
this.$router.push(`/feed/plan/detail/${planId}`)
},
// 创建计划

View File

@@ -0,0 +1,601 @@
<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>
<div v-if="plan.parent_id" class="info-item">
<label>父计划ID:</label>
<span>{{ plan.parent_id }}</span>
</div>
<div v-if="plan.order_in_parent !== null" class="info-item">
<label>父计划中顺序:</label>
<span>{{ plan.order_in_parent }}</span>
</div>
</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 }}</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
} else {
this.error = data.message || '获取计划详情失败'
}
} catch (error) {
console.error('获取计划详情失败:', error)
this.error = '获取计划详情失败: ' + error.message
} finally {
this.loading = false
}
},
// 返回列表
goBack() {
this.$router.push('/feed/plan')
},
// 编辑计划
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>

View File

@@ -3,6 +3,7 @@ import Login from '../pages/Login.vue'
import Dashboard from '../pages/Dashboard.vue'
import Device from '../pages/Device.vue'
import FeedPlan from '../pages/FeedPlan.vue'
import FeedPlanDetail from '../pages/FeedPlanDetail.vue'
const routes = [
{
@@ -27,6 +28,13 @@ const routes = [
name: 'FeedPlan',
component: FeedPlan,
meta: { requiresAuth: true }
},
{
path: '/feed/plan/detail/:id',
name: 'FeedPlanDetail',
component: FeedPlanDetail,
meta: { requiresAuth: true },
props: true
}
]

View File

@@ -44,25 +44,25 @@ type CreateRequest struct {
Enabled bool `json:"enabled"`
// ScheduleCron 定时任务表达式(仅当Type为auto时有效)
ScheduleCron *string `json:"scheduleCron"`
ScheduleCron *string `json:"schedule_cron,omitempty"`
// ExecutionLimit 执行次数限制(0表示无限制仅当Type为auto时有效)
ExecutionLimit int `json:"executionLimit"`
ExecutionLimit int `json:"execution_limit"`
// ParentID 父计划ID用于支持子计划结构
ParentID *uint `json:"parentID"`
ParentID *uint `json:"parent_id,omitempty"`
// OrderInParent 在父计划中的执行顺序
OrderInParent *int `json:"orderInParent"`
OrderInParent *int `json:"order_in_parent,omitempty"`
// IsMaster 是否为主计划(主计划可以包含子计划)
IsMaster bool `json:"isMaster"`
IsMaster bool `json:"is_master"`
// Steps 计划步骤列表
Steps []FeedingPlanStep `json:"steps"`
// SubPlans 子计划列表
SubPlans []CreateRequest `json:"subPlans"`
SubPlans []CreateRequest `json:"sub_plans"`
}
// Create 创建饲料计划
@@ -188,6 +188,7 @@ func (c *Controller) ListPlans(ctx *gin.Context) {
Description: introduction.Description,
Enabled: introduction.Enabled,
Type: introduction.Type,
ScheduleCron: introduction.ScheduleCron,
})
}
@@ -212,25 +213,25 @@ type UpdateRequest struct {
Enabled bool `json:"enabled"`
// ScheduleCron 定时任务表达式(仅当Type为auto时有效)
ScheduleCron *string `json:"scheduleCron"`
ScheduleCron *string `json:"schedule_cron,omitempty"`
// ExecutionLimit 执行次数限制(0表示无限制仅当Type为auto时有效)
ExecutionLimit int `json:"executionLimit"`
ExecutionLimit int `json:"execution_limit"`
// ParentID 父计划ID用于支持子计划结构
ParentID *uint `json:"parentID"`
ParentID *uint `json:"parent_id,omitempty"`
// OrderInParent 在父计划中的执行顺序
OrderInParent *int `json:"orderInParent"`
OrderInParent *int `json:"order_in_parent,omitempty"`
// IsMaster 是否为主计划(主计划可以包含子计划)
IsMaster bool `json:"isMaster"`
IsMaster bool `json:"is_master"`
// Steps 计划步骤列表
Steps []FeedingPlanStep `json:"steps"`
// SubPlans 子计划列表
SubPlans []UpdateRequest `json:"subPlans"`
SubPlans []UpdateRequest `json:"sub_plans"`
}
// DetailResponse 喂料计划主表
@@ -251,22 +252,22 @@ type DetailResponse struct {
Enabled bool `json:"enabled"`
// ScheduleCron 定时任务表达式(仅当Type为auto时有效)
ScheduleCron *string `json:"scheduleCron"`
ScheduleCron *string `json:"schedule_cron,omitempty"`
// ExecutionLimit 执行次数限制(0表示无限制仅当Type为auto时有效)
ExecutionLimit int `json:"executionLimit"`
ExecutionLimit int `json:"execution_limit"`
// ParentID 父计划ID用于支持子计划结构
ParentID *uint `json:"parentID"`
ParentID *uint `json:"parent_id,omitempty"`
// OrderInParent 在父计划中的执行顺序
OrderInParent *int `json:"orderInParent"`
OrderInParent *int `json:"order_in_parent,omitempty"`
// Steps 计划步骤列表
Steps []FeedingPlanStep `json:"steps"`
// SubPlans 子计划列表
SubPlans []DetailResponse `json:"subPlans"`
SubPlans []DetailResponse `json:"sub_plans"`
}
// FeedingPlanStep 喂料计划步骤表,表示计划中的每个设备动作
@@ -275,25 +276,25 @@ type FeedingPlanStep struct {
ID uint `json:"id"`
// PlanID 关联的计划ID
PlanID uint `json:"planID"`
PlanID uint `json:"plan_id"`
// StepOrder 步骤顺序
StepOrder int `json:"stepOrder"`
StepOrder int `json:"step_order"`
// DeviceID 关联的设备ID
DeviceID uint `json:"deviceID"`
DeviceID uint `json:"device_id"`
// TargetValue 目标值(达到该值后停止工作切换到下一个设备)
TargetValue float64 `json:"targetValue"`
TargetValue float64 `json:"target_value"`
// Action 动作(如:打开设备)
Action string `json:"action"`
// ScheduleCron 步骤定时任务表达式(可选)
ScheduleCron *string `json:"scheduleCron"`
ScheduleCron *string `json:"schedule_cron,omitempty"`
// ExecutionLimit 步骤执行次数限制(0表示无限制)
ExecutionLimit int `json:"executionLimit"`
ExecutionLimit int `json:"execution_limit"`
}
// Detail 获取饲料计划列细节