Compare commits

..

2 Commits

Author SHA1 Message Date
cc7ea94e41 1. 实现前端删除饲喂计划
2. 修复后端delete接口bug
2025-09-10 16:25:52 +08:00
40a19b831a 增加饲喂计划列表展示界面 2025-09-10 16:14:05 +08:00
11 changed files with 487 additions and 29 deletions

21
frontend/dist/assets/index.0581bd6d.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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.40048162.js"></script>
<link rel="stylesheet" href="/assets/index.4965a25a.css">
<script type="module" crossorigin src="/assets/index.0581bd6d.js"></script>
<link rel="stylesheet" href="/assets/index.42c8d2d4.css">
</head>
<body>
<div id="app"></div>

View File

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

View File

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

View File

@@ -0,0 +1,403 @@
<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>
</template>
<script>
export default {
name: 'FeedPlan',
data() {
return {
username: '',
plans: [],
loading: true
}
},
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/${planId}`)
},
// 创建计划
createPlan() {
// TODO: 实现创建计划逻辑
alert('创建计划功能待实现')
},
// 编辑计划
editPlan(plan) {
// TODO: 实现编辑计划逻辑
alert(`编辑计划: ${plan.name}`)
},
// 删除计划
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;
}
}
</style>

View File

@@ -2,6 +2,7 @@ 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'
const routes = [
{
@@ -20,6 +21,12 @@ const routes = [
name: 'Device',
component: Device,
meta: { requiresAuth: true }
},
{
path: '/feed/plan',
name: 'FeedPlan',
component: FeedPlan,
meta: { requiresAuth: true }
}
]

View File

@@ -126,15 +126,17 @@ func (c *Controller) convertToCreateModel(req *CreateRequest) *model.FeedingPlan
// Delete 删除饲料计划
func (c *Controller) Delete(ctx *gin.Context) {
// 获取路径参数中的计划ID
planIDStr := ctx.Param("id")
planID, err := strconv.ParseUint(planIDStr, 10, 32)
if err != nil {
controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "无效的计划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(planID)); err != nil {
if err := c.feedPlanRepo.DeleteFeedingPlan(uint(req.ID)); err != nil {
c.logger.Error("删除计划失败: " + err.Error())
controller.SendErrorResponse(ctx, controller.InternalServerErrorCode, "删除计划失败")
return

View File

@@ -19,6 +19,10 @@ var migrateModels = []interface{}{
&model.OperationHistory{},
&model.Device{},
&model.DeviceControl{},
&model.FeedingPlan{},
&model.FeedingPlanStep{},
&model.FeedingExecution{},
&model.FeedingExecutionStep{},
}
// PostgresStorage 代表基于PostgreSQL的存储实现