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

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
}
]