增加创建和更新计划基本信息的界面

This commit is contained in:
2025-09-10 20:04:38 +08:00
parent 6fe73d8ffe
commit 3743b5ddcd
9 changed files with 453 additions and 29 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.cb9d3828.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 charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>猪场管理系统</title> <title>猪场管理系统</title>
<script type="module" crossorigin src="/assets/index.48fb8fe6.js"></script> <script type="module" crossorigin src="/assets/index.cb9d3828.js"></script>
<link rel="stylesheet" href="/assets/index.2ad61d14.css"> <link rel="stylesheet" href="/assets/index.bcc76856.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

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

View File

@@ -60,17 +60,43 @@
</div> </div>
</div> </div>
</main> </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> </div>
</template> </template>
<script> <script>
import FeedPlanForm from '../components/FeedPlanForm.vue'
export default { export default {
name: 'FeedPlan', name: 'FeedPlan',
components: {
FeedPlanForm
},
data() { data() {
return { return {
username: '', username: '',
plans: [], plans: [],
loading: true loading: true,
// 控制模态框显示
showModal: false,
// 当前操作类型: 'create' 或 'edit'
modalType: 'create',
// 当前编辑的计划
currentPlan: null,
// 提交时的加载状态
submitting: false
} }
}, },
@@ -113,14 +139,75 @@ export default {
// 创建计划 // 创建计划
createPlan() { createPlan() {
// TODO: 实现创建计划逻辑 this.modalType = 'create'
alert('创建计划功能待实现') this.currentPlan = {
name: '',
description: '',
type: 'manual',
enabled: true,
schedule_cron: '',
execution_limit: 0
}
this.showModal = true
}, },
// 编辑计划 // 编辑计划
editPlan(plan) { editPlan(plan) {
// TODO: 实现编辑计划逻辑 this.modalType = 'edit'
alert(`编辑计划: ${plan.name}`) // 深拷贝计划数据,避免直接修改原数据
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
}
}, },
// 删除计划 // 删除计划
@@ -400,4 +487,28 @@ export default {
flex-direction: column; 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> </style>

View File

@@ -1,9 +1,15 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: { server: {
port: 3000, port: 3000,
proxy: { proxy: {

View File

@@ -121,6 +121,7 @@ func (c *Controller) convertToCreateModel(req *CreateRequest) *model.FeedingPlan
ExecutionLimit: req.ExecutionLimit, ExecutionLimit: req.ExecutionLimit,
ParentID: req.ParentID, ParentID: req.ParentID,
OrderInParent: req.OrderInParent, OrderInParent: req.OrderInParent,
// 不需要显式设置ID字段仓库层会处理
} }
// 转换步骤 // 转换步骤

View File

@@ -60,6 +60,9 @@ func (f *feedPlanRepo) FindFeedingPlanByID(feedingPlanID uint) (*model.FeedingPl
// CreateFeedingPlan 创建饲料计划,包括步骤和子计划 // CreateFeedingPlan 创建饲料计划,包括步骤和子计划
func (f *feedPlanRepo) CreateFeedingPlan(feedingPlan *model.FeedingPlan) error { func (f *feedPlanRepo) CreateFeedingPlan(feedingPlan *model.FeedingPlan) error {
// 清空所有ID确保创建新记录
f.clearAllIDs(feedingPlan)
return f.db.Transaction(func(tx *gorm.DB) error { return f.db.Transaction(func(tx *gorm.DB) error {
return f.createFeedingPlanWithTx(tx, feedingPlan) return f.createFeedingPlanWithTx(tx, feedingPlan)
}) })