feat: 创建项目阶段与ui布局

This commit is contained in:
2025-11-19 14:32:21 +08:00
parent f13ef3232b
commit dd3fd18132
3 changed files with 479 additions and 10 deletions

View File

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

View File

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

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