Files
pig-farm-controller/frontend/src/pages/FeedPlan.vue

514 lines
11 KiB
Vue

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