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