feat: 创建项目阶段与ui布局
This commit is contained in:
@@ -163,6 +163,7 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project> impl
|
||||
for (StageDetail stageDetail : details) {
|
||||
ProjectRequestVo.RequestDescription req2 = new ProjectRequestVo.RequestDescription();
|
||||
BeanUtil.copyProperties(stageDetail, req2);
|
||||
req2.setDays(CommonTimeFormatUtil.calculateDaysBetween(req2.getStartTime(), req2.getEndTime()));
|
||||
if (req.getDetailId().equals(req2.getParentDetailId())) {
|
||||
if (ObjectUtil.isEmpty(req.getChildren())) {
|
||||
req.setChildren(new ArrayList<>());
|
||||
|
||||
@@ -1,23 +1,163 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
<a-spin :spinning="loading">
|
||||
<section v-if="projectDetail">
|
||||
<pre>{{ projectDetail }}</pre>
|
||||
</section>
|
||||
<!-- 项目名称标题 -->
|
||||
<div class="project-title">
|
||||
<h2>{{ parsedQuery?.projectName || '项目名称' }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- 项目信息部分 -->
|
||||
<a-card :bordered="false" class="project-info-card">
|
||||
<a-form :model="projectInfo" layout="horizontal" :label-col="{ span: 7 }" :wrapper-col="{ span: 17 }" class="compact-form">
|
||||
<!-- 第一行:项目名称、项目编号 -->
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="项目名称:">
|
||||
<span>{{ projectInfo.projectName || '-' }}</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="项目编号:">
|
||||
<span>{{ projectInfo.projectCode || '-' }}</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<!-- 第二行:项目经理、产品需求、开发组 -->
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="项目经理:">
|
||||
<span>{{ projectInfo.projectManage || '-' }}</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="产品需求:">
|
||||
<span>{{ projectInfo.requireManage || '-' }}</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="开发组:">
|
||||
<span>{{ projectInfo.devGroup || '-' }}</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<!-- 第三行:开始时间、完成时间 -->
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="开始时间:">
|
||||
<span>{{ projectInfo.startTime || '-' }}</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="完成时间:">
|
||||
<span>{{ projectInfo.endTime || '-' }}</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
<div class="planned-hours">
|
||||
<span class="hours-label">计划工时:</span>
|
||||
<span class="hours-value">{{ plannedHours }}</span>
|
||||
<a-button type="primary" class="create-stage-btn" @click="formRef.onOpen(projectInfo.projectId)">创建项目阶段</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 表格信息部分 -->
|
||||
<div v-if="stageList.length > 0" class="stage-tables">
|
||||
<a-card
|
||||
v-for="(stage, stageIndex) in stageList"
|
||||
:key="stage.stageId"
|
||||
:bordered="false"
|
||||
class="stage-card"
|
||||
>
|
||||
<template #title>
|
||||
<div class="stage-title-wrapper">
|
||||
<span>第{{ getStageNumber(stage.stageSeq) }}阶段 {{ stage.stageName }}</span>
|
||||
<a-button type="primary" class="create-requirement-btn">创建需求</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<a-table
|
||||
:columns="tableColumns"
|
||||
:data-source="stage.descriptions"
|
||||
:pagination="false"
|
||||
:row-key="(record) => record.detailId"
|
||||
:defaultExpandAllRows="true"
|
||||
bordered
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'seq'">
|
||||
{{ record.seq }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'modelName'">
|
||||
{{ record.modelName }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'modelDetail'">
|
||||
{{ record.modelDetail }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'modelDescription'">
|
||||
{{ record.modelDescription }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'priority'">
|
||||
{{ record.priority }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'remark'">
|
||||
{{ record.remark || '无' }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'startTime'">
|
||||
{{ formatDate(record.startTime) }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'endTime'">
|
||||
{{ formatDate(record.endTime) }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'days'">
|
||||
{{ record.days }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<a-button type="link" size="small" class="action-btn">
|
||||
新增同级
|
||||
</a-button>
|
||||
<a-button type="link" size="small" class="action-btn">
|
||||
新增子级
|
||||
</a-button>
|
||||
<a-button type="link" size="small" class="action-btn">
|
||||
修改需求
|
||||
</a-button>
|
||||
<a-button type="link" size="small" class="action-btn">
|
||||
删除需求
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
<template #summary>
|
||||
<a-table-summary fixed>
|
||||
<a-table-summary-row>
|
||||
<a-table-summary-cell :index="0" :col-span="10">
|
||||
<span style="font-weight: bold">总计天数:{{ getStageTotalDays(stage.descriptions) }}</span>
|
||||
</a-table-summary-cell>
|
||||
</a-table-summary-row>
|
||||
</a-table-summary>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
<a-empty v-else description="暂无数据" />
|
||||
</a-spin>
|
||||
</a-card>
|
||||
<stage-add-dialog ref="formRef" @successful="fetchDetail"/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Description">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import projectApi from '@/api/pmm/projectApi'
|
||||
import { message } from 'ant-design-vue'
|
||||
import StageAddDialog from "@/views/pmm/project/request/stageAddDialog.vue";
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const loading = ref(false)
|
||||
const projectDetail = ref<Record<string, any> | null>(null)
|
||||
const stageList = ref<any[]>([])
|
||||
const plannedHours = ref(0)
|
||||
const formRef = ref()
|
||||
|
||||
const parsedQuery = computed(() => {
|
||||
const payload = route.query.item as string | undefined
|
||||
@@ -30,24 +170,148 @@
|
||||
}
|
||||
})
|
||||
|
||||
const projectInfo = computed(() => {
|
||||
return parsedQuery.value || {}
|
||||
})
|
||||
|
||||
const projectId = computed(() => parsedQuery.value?.projectId)
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'seq',
|
||||
key: 'seq',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '功能模块',
|
||||
dataIndex: 'modelName',
|
||||
key: 'modelName',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '功能明细',
|
||||
dataIndex: 'modelDetail',
|
||||
key: 'modelDetail',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'modelDescription',
|
||||
key: 'modelDescription',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
key: 'priority',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
key: 'remark',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '开始日期',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '结束日期',
|
||||
dataIndex: 'endTime',
|
||||
key: 'endTime',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '计划天数',
|
||||
dataIndex: 'days',
|
||||
key: 'days',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 300,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const monthStr = month < 10 ? '0' + month : String(month)
|
||||
const dayStr = day < 10 ? '0' + day : String(day)
|
||||
return `${year}-${monthStr}-${dayStr}`
|
||||
}
|
||||
|
||||
// 获取阶段序号中文
|
||||
const getStageNumber = (seq: string) => {
|
||||
const num = parseInt(seq)
|
||||
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
|
||||
if (num <= 10) {
|
||||
return numbers[num - 1] || seq
|
||||
}
|
||||
return seq
|
||||
}
|
||||
|
||||
// 计算阶段总天数(只统计一级,不包含子项)
|
||||
const getStageTotalDays = (descriptions: any[]) => {
|
||||
if (!descriptions || descriptions.length === 0) return 0
|
||||
let total = 0
|
||||
descriptions.forEach((item) => {
|
||||
if (item.days) {
|
||||
total += item.days
|
||||
}
|
||||
})
|
||||
return total
|
||||
}
|
||||
|
||||
// 计算计划工时(所有阶段的总天数 * 24)
|
||||
const calculatePlannedHours = (stages: any[]) => {
|
||||
let total = 0
|
||||
stages.forEach((stage) => {
|
||||
if (stage.descriptions) {
|
||||
total += getStageTotalDays(stage.descriptions)
|
||||
}
|
||||
})
|
||||
return total * 24
|
||||
}
|
||||
|
||||
// 获取项目需求明细
|
||||
const fetchDetail = () => {
|
||||
if (!projectId.value) {
|
||||
projectDetail.value = null
|
||||
stageList.value = []
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
projectApi
|
||||
.requestDetail({ projectId: projectId.value })
|
||||
.then((data) => {
|
||||
projectDetail.value = data
|
||||
if (Array.isArray(data)) {
|
||||
stageList.value = data
|
||||
plannedHours.value = calculatePlannedHours(data)
|
||||
} else {
|
||||
stageList.value = []
|
||||
message.warning('数据格式错误')
|
||||
}
|
||||
loading.value = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('获取项目详情失败', error)
|
||||
projectDetail.value = null
|
||||
})
|
||||
.then(() => {
|
||||
console.warn('获取项目需求明细失败', error)
|
||||
stageList.value = []
|
||||
message.error('获取项目需求明细失败')
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
@@ -58,5 +322,122 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.project-title {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.project-title h2 {
|
||||
margin: 0;
|
||||
color: #1890ff;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.project-info-card {
|
||||
margin-bottom: 24px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.project-info-card :deep(.ant-card-body) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-row) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-row:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-form-item-label) {
|
||||
padding-bottom: 0;
|
||||
width: 90px;
|
||||
flex: 0 0 90px;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-form-item-label > label) {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
font-size: 14px;
|
||||
width: 90px;
|
||||
text-align: right;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-form-item-control) {
|
||||
line-height: 28px;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-form-item-control-input) {
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.compact-form :deep(.ant-form-item-control-input-content) {
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.planned-hours {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.hours-label {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hours-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.create-stage-btn {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.stage-tables {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.stage-card {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stage-card :deep(.ant-card-head-title) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stage-title-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stage-title-wrapper > span {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.create-requirement-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
87
nl-vue/src/views/pmm/project/request/stageAddDialog.vue
Normal file
87
nl-vue/src/views/pmm/project/request/stageAddDialog.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<xn-form-container
|
||||
:title="formData.stageId ? '编辑项目阶段' : '增加项目阶段'"
|
||||
:width="700"
|
||||
v-model:open="open"
|
||||
:destroy-on-close="true"
|
||||
@close="onClose"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" :rules="formRules" layout="horizontal">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="阶段名称:" name="stageName">
|
||||
<a-input v-model:value="formData.stageName" placeholder="请输入阶段名称" allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="阶段序号:" name="stageSeq">
|
||||
<a-input-number
|
||||
v-model:value="formData.stageSeq"
|
||||
placeholder="请输入阶段序号"
|
||||
style="width: 100%"
|
||||
allow-clear
|
||||
:min="1"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-button style="margin-right: 8px" @click="onClose">关闭</a-button>
|
||||
<a-button type="primary" @click="onSubmit" :loading="submitLoading">保存</a-button>
|
||||
</template>
|
||||
</xn-form-container>
|
||||
</template>
|
||||
|
||||
<script setup name="projectStageForm">
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { required } from '@/utils/formRules'
|
||||
import projectStageApi from '@/api/pmm/projectStageApi'
|
||||
// 抽屉状态
|
||||
const open = ref(false)
|
||||
const emit = defineEmits({ successful: null })
|
||||
const formRef = ref()
|
||||
// 表单数据
|
||||
const formData = ref({})
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 打开抽屉
|
||||
const onOpen = (record) => {
|
||||
open.value = true
|
||||
if (record) {
|
||||
console.log(record)
|
||||
formData.value.projectId = record
|
||||
}
|
||||
}
|
||||
// 关闭抽屉
|
||||
const onClose = () => {
|
||||
formRef.value.resetFields()
|
||||
formData.value = {}
|
||||
open.value = false
|
||||
}
|
||||
// 默认要校验的
|
||||
const formRules = {}
|
||||
// 验证并提交数据
|
||||
const onSubmit = () => {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
submitLoading.value = true
|
||||
const formDataParam = cloneDeep(formData.value)
|
||||
projectStageApi
|
||||
.projectStageSubmitForm(formDataParam, formDataParam.stageId)
|
||||
.then(() => {
|
||||
onClose()
|
||||
emit('successful')
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
// 抛出函数
|
||||
defineExpose({
|
||||
onOpen
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user