feat: 完成需求工单管理模块(全栈开发)

后端:
- 创建 Requirement 实体类(映射 requirement 表)
- 创建 RequirementMapper 接口(含自定义查询)
- 创建 RequirementService 业务逻辑层
  - 分页查询(支持多条件筛选)
  - 新增/更新/删除需求
  - 更新需求状态和进度
  - 自动设置默认租户ID
- 创建 RequirementController 控制器
  - RESTful API 设计
  - 状态管理和进度跟踪

前端:
- 创建 requirement.js API 文件(完整的 CRUD 接口)
- 创建 requirement.vue 管理页面
  - 多条件搜索(需求名称、项目、客户、状态)
  - 表格展示(含状态标签、优先级标签、进度条)
  - 新增/编辑表单(支持工时、日期、应收款管理)
  - 状态更新对话框
  - 进度更新对话框(滑块组件)
  - 项目和客户下拉选择
- 添加路由配置(/project/requirement)
This commit is contained in:
zhangjf 2026-02-15 18:06:20 +08:00
parent 1a47943b10
commit 9c00696baf
3 changed files with 723 additions and 0 deletions

View File

@ -0,0 +1,76 @@
import request from '../utils/request'
/**
* 获取需求工单列表
*/
export const getRequirementList = (params) => {
return request({
url: '/proj/api/v1/requirement/list',
method: 'get',
params
})
}
/**
* 获取需求工单详情
*/
export const getRequirementById = (requirementId) => {
return request({
url: `/proj/api/v1/requirement/${requirementId}`,
method: 'get'
})
}
/**
* 创建需求工单
*/
export const createRequirement = (data) => {
return request({
url: '/proj/api/v1/requirement',
method: 'post',
data
})
}
/**
* 更新需求工单
*/
export const updateRequirement = (requirementId, data) => {
return request({
url: `/proj/api/v1/requirement/${requirementId}`,
method: 'put',
data
})
}
/**
* 删除需求工单
*/
export const deleteRequirement = (requirementId) => {
return request({
url: `/proj/api/v1/requirement/${requirementId}`,
method: 'delete'
})
}
/**
* 更新需求状态
*/
export const updateRequirementStatus = (requirementId, status) => {
return request({
url: `/proj/api/v1/requirement/${requirementId}/status`,
method: 'put',
params: { status }
})
}
/**
* 更新需求进度
*/
export const updateRequirementProgress = (requirementId, progress) => {
return request({
url: `/proj/api/v1/requirement/${requirementId}/progress`,
method: 'put',
params: { progress }
})
}

View File

@ -86,6 +86,12 @@ const routes = [
name: 'Contract', name: 'Contract',
component: () => import('../views/project/contract.vue'), component: () => import('../views/project/contract.vue'),
meta: { title: '合同管理' } meta: { title: '合同管理' }
},
{
path: 'requirement',
name: 'Requirement',
component: () => import('../views/project/requirement.vue'),
meta: { title: '需求工单' }
} }
] ]
} }

View File

@ -0,0 +1,641 @@
<template>
<div class="requirement-container">
<el-card>
<!-- 搜索栏 -->
<el-form :inline="true" :model="searchForm">
<el-form-item label="需求名称">
<el-input v-model="searchForm.requirementName" placeholder="请输入需求名称" clearable />
</el-form-item>
<el-form-item label="项目">
<el-select v-model="searchForm.projectId" placeholder="请选择项目" clearable>
<el-option
v-for="project in projectOptions"
:key="project.projectId"
:label="project.projectName"
:value="project.projectId"
/>
</el-select>
</el-form-item>
<el-form-item label="客户">
<el-select v-model="searchForm.customerId" placeholder="请选择客户" clearable>
<el-option
v-for="customer in customerOptions"
:key="customer.customerId"
:label="customer.customerName"
:value="customer.customerId"
/>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="待开发" value="pending" />
<el-option label="开发中" value="developing" />
<el-option label="已交付" value="delivered" />
<el-option label="已完成" value="completed" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作栏 -->
<el-row style="margin-bottom: 15px;">
<el-button type="primary" @click="handleAdd">新增需求</el-button>
</el-row>
<!-- 表格 -->
<el-table :data="tableData" border v-loading="loading">
<el-table-column prop="requirementCode" label="需求编号" width="150" />
<el-table-column prop="requirementName" label="需求名称" width="200" />
<el-table-column prop="projectId" label="项目" width="150">
<template #default="{ row }">
{{ getProjectName(row.projectId) }}
</template>
</el-table-column>
<el-table-column prop="customerName" label="客户" width="150">
<template #default="{ row }">
{{ getCustomerName(row.customerId) }}
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" width="100">
<template #default="{ row }">
<el-tag v-if="row.priority === 'high'" type="danger"></el-tag>
<el-tag v-else-if="row.priority === 'normal'" type="warning"></el-tag>
<el-tag v-else type="info"></el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag v-if="row.status === 'pending'" type="info">待开发</el-tag>
<el-tag v-else-if="row.status === 'developing'" type="primary">开发中</el-tag>
<el-tag v-else-if="row.status === 'delivered'" type="warning">已交付</el-tag>
<el-tag v-else-if="row.status === 'completed'" type="success">已完成</el-tag>
</template>
</el-table-column>
<el-table-column prop="progress" label="进度" width="120">
<template #default="{ row }">
<el-progress :percentage="row.progress" :color="getProgressColor(row.progress)" />
</template>
</el-table-column>
<el-table-column prop="estimatedHours" label="预估工时(h)" width="120" />
<el-table-column prop="actualHours" label="实际工时(h)" width="120" />
<el-table-column prop="receivableAmount" label="应收款" width="120">
<template #default="{ row }">
{{ row.receivableAmount ? '¥' + row.receivableAmount : '-' }}
</template>
</el-table-column>
<el-table-column prop="createdTime" label="创建时间" width="180" />
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="info" size="small" @click="handleUpdateStatus(row)">更新状态</el-button>
<el-button type="warning" size="small" @click="handleUpdateProgress(row)">更新进度</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="page.current"
v-model:page-size="page.size"
:total="page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
style="margin-top: 20px; justify-content: flex-end;"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="form.requirementId ? '编辑需求' : '新增需求'"
width="800px"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="需求编号" prop="requirementCode">
<el-input v-model="form.requirementCode" placeholder="请输入需求编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="需求名称" prop="requirementName">
<el-input v-model="form.requirementName" placeholder="请输入需求名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目" prop="projectId">
<el-select v-model="form.projectId" placeholder="请选择项目" style="width: 100%;">
<el-option
v-for="project in projectOptions"
:key="project.projectId"
:label="project.projectName"
:value="project.projectId"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户" prop="customerId">
<el-select v-model="form.customerId" placeholder="请选择客户" style="width: 100%;">
<el-option
v-for="customer in customerOptions"
:key="customer.customerId"
:label="customer.customerName"
:value="customer.customerId"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="优先级" prop="priority">
<el-select v-model="form.priority" placeholder="请选择优先级" style="width: 100%;">
<el-option label="高" value="high" />
<el-option label="中" value="normal" />
<el-option label="低" value="low" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态" style="width: 100%;">
<el-option label="待开发" value="pending" />
<el-option label="开发中" value="developing" />
<el-option label="已交付" value="delivered" />
<el-option label="已完成" value="completed" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="需求描述" prop="description">
<el-input
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入需求描述"
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="预估工时(h)" prop="estimatedHours">
<el-input-number v-model="form.estimatedHours" :min="0" :precision="2" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实际工时(h)" prop="actualHours">
<el-input-number v-model="form.actualHours" :min="0" :precision="2" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="计划开始日期" prop="plannedStart">
<el-date-picker
v-model="form.plannedStart"
type="date"
placeholder="选择日期"
style="width: 100%;"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="计划结束日期" prop="plannedEnd">
<el-date-picker
v-model="form.plannedEnd"
type="date"
placeholder="选择日期"
style="width: 100%;"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="实际开始日期" prop="actualStart">
<el-date-picker
v-model="form.actualStart"
type="date"
placeholder="选择日期"
style="width: 100%;"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实际结束日期" prop="actualEnd">
<el-date-picker
v-model="form.actualEnd"
type="date"
placeholder="选择日期"
style="width: 100%;"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="应收款金额" prop="receivableAmount">
<el-input-number v-model="form.receivableAmount" :min="0" :precision="2" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="应收款日期" prop="receivableDate">
<el-date-picker
v-model="form.receivableDate"
type="date"
placeholder="选择日期"
style="width: 100%;"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="交付日期" prop="deliveryDate">
<el-date-picker
v-model="form.deliveryDate"
type="date"
placeholder="选择日期"
style="width: 100%;"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
<!-- 更新状态对话框 -->
<el-dialog v-model="statusDialogVisible" title="更新状态" width="400px">
<el-form>
<el-form-item label="状态">
<el-select v-model="statusForm.status" placeholder="请选择状态" style="width: 100%;">
<el-option label="待开发" value="pending" />
<el-option label="开发中" value="developing" />
<el-option label="已交付" value="delivered" />
<el-option label="已完成" value="completed" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="statusDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmitStatus">确定</el-button>
</template>
</el-dialog>
<!-- 更新进度对话框 -->
<el-dialog v-model="progressDialogVisible" title="更新进度" width="400px">
<el-form>
<el-form-item label="进度">
<el-slider v-model="progressForm.progress" :min="0" :max="100" show-input />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="progressDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmitProgress">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getRequirementList,
createRequirement,
updateRequirement,
deleteRequirement,
updateRequirementStatus,
updateRequirementProgress
} from '../../api/requirement'
import { getProjectList } from '../../api/project'
import { getCustomerList } from '../../api/customer'
//
const searchForm = reactive({
requirementName: '',
projectId: null,
customerId: null,
status: ''
})
//
const tableData = ref([])
const loading = ref(false)
//
const page = reactive({
current: 1,
size: 10,
total: 0
})
//
const projectOptions = ref([])
//
const customerOptions = ref([])
//
const dialogVisible = ref(false)
const formRef = ref(null)
//
const form = reactive({
requirementId: null,
requirementCode: '',
requirementName: '',
description: '',
projectId: null,
customerId: null,
priority: 'normal',
status: 'pending',
estimatedHours: 0,
actualHours: 0,
plannedStart: null,
plannedEnd: null,
actualStart: null,
actualEnd: null,
deliveryDate: null,
receivableAmount: 0,
receivableDate: null
})
//
const rules = {
requirementCode: [
{ required: true, message: '请输入需求编号', trigger: 'blur' }
],
requirementName: [
{ required: true, message: '请输入需求名称', trigger: 'blur' }
],
projectId: [
{ required: true, message: '请选择项目', trigger: 'change' }
],
customerId: [
{ required: true, message: '请选择客户', trigger: 'change' }
]
}
//
const statusDialogVisible = ref(false)
const statusForm = reactive({
requirementId: null,
status: ''
})
//
const progressDialogVisible = ref(false)
const progressForm = reactive({
requirementId: null,
progress: 0
})
//
const loadProjects = async () => {
try {
const res = await getProjectList({ current: 1, size: 1000 })
projectOptions.value = res.records || []
} catch (error) {
console.error('加载项目列表失败:', error)
}
}
//
const loadCustomers = async () => {
try {
const res = await getCustomerList({ current: 1, size: 1000 })
customerOptions.value = res.records || []
} catch (error) {
console.error('加载客户列表失败:', error)
}
}
//
const getProjectName = (projectId) => {
const project = projectOptions.value.find(p => p.projectId === projectId)
return project ? project.projectName : '-'
}
//
const getCustomerName = (customerId) => {
const customer = customerOptions.value.find(c => c.customerId === customerId)
return customer ? customer.customerName : '-'
}
//
const getProgressColor = (percentage) => {
if (percentage < 30) return '#f56c6c'
if (percentage < 70) return '#e6a23c'
return '#67c23a'
}
//
const fetchData = async () => {
loading.value = true
try {
const params = {
current: page.current,
size: page.size,
requirementName: searchForm.requirementName || undefined,
projectId: searchForm.projectId || undefined,
customerId: searchForm.customerId || undefined,
status: searchForm.status || undefined
}
const res = await getRequirementList(params)
tableData.value = res.records
page.total = res.total
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error(error.message || '加载数据失败')
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
page.current = 1
fetchData()
}
//
const handleReset = () => {
searchForm.requirementName = ''
searchForm.projectId = null
searchForm.customerId = null
searchForm.status = ''
page.current = 1
fetchData()
}
//
const handleAdd = () => {
Object.assign(form, {
requirementId: null,
requirementCode: '',
requirementName: '',
description: '',
projectId: null,
customerId: null,
priority: 'normal',
status: 'pending',
estimatedHours: 0,
actualHours: 0,
plannedStart: null,
plannedEnd: null,
actualStart: null,
actualEnd: null,
deliveryDate: null,
receivableAmount: 0,
receivableDate: null
})
dialogVisible.value = true
}
//
const handleEdit = (row) => {
Object.assign(form, {
requirementId: row.requirementId,
requirementCode: row.requirementCode,
requirementName: row.requirementName,
description: row.description,
projectId: row.projectId,
customerId: row.customerId,
priority: row.priority,
status: row.status,
estimatedHours: row.estimatedHours,
actualHours: row.actualHours,
plannedStart: row.plannedStart,
plannedEnd: row.plannedEnd,
actualStart: row.actualStart,
actualEnd: row.actualEnd,
deliveryDate: row.deliveryDate,
receivableAmount: row.receivableAmount,
receivableDate: row.receivableDate
})
dialogVisible.value = true
}
//
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
} catch (error) {
ElMessage.warning('请检查表单填写是否完整')
return
}
try {
if (form.requirementId) {
await updateRequirement(form.requirementId, form)
ElMessage.success('更新成功')
} else {
await createRequirement(form)
ElMessage.success('创建成功')
}
dialogVisible.value = false
await fetchData()
} catch (error) {
console.error('保存失败:', error)
ElMessage.error(error.message || '操作失败')
}
}
//
const handleUpdateStatus = (row) => {
statusForm.requirementId = row.requirementId
statusForm.status = row.status
statusDialogVisible.value = true
}
//
const handleSubmitStatus = async () => {
try {
await updateRequirementStatus(statusForm.requirementId, statusForm.status)
ElMessage.success('状态更新成功')
statusDialogVisible.value = false
await fetchData()
} catch (error) {
console.error('状态更新失败:', error)
ElMessage.error(error.message || '状态更新失败')
}
}
//
const handleUpdateProgress = (row) => {
progressForm.requirementId = row.requirementId
progressForm.progress = row.progress
progressDialogVisible.value = true
}
//
const handleSubmitProgress = async () => {
try {
await updateRequirementProgress(progressForm.requirementId, progressForm.progress)
ElMessage.success('进度更新成功')
progressDialogVisible.value = false
await fetchData()
} catch (error) {
console.error('进度更新失败:', error)
ElMessage.error(error.message || '进度更新失败')
}
}
//
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该需求吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await deleteRequirement(row.requirementId)
ElMessage.success('删除成功')
//
if (tableData.value.length === 1 && page.current > 1) {
page.current--
}
await fetchData()
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error)
ElMessage.error(error.message || '删除失败')
}
}
}
//
onMounted(() => {
loadProjects()
loadCustomers()
fetchData()
})
</script>
<style scoped>
.requirement-container {
padding: 20px;
}
</style>