514 lines
11 KiB
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> |