zhangwencui
2026-10-29 3ce715a4e66955eac86dcd2f5c8afcfdf8becb6d
src/components/ProjectManagement/ProgressReportDialog.vue
@@ -1,10 +1,11 @@
<template>
  <el-dialog v-model="visible" title="进度汇报" width="900px" top="8vh" append-to-body destroy-on-close @close="handleClose">
  <el-dialog v-model="visible" title="进度汇报" width="960px" top="6vh" append-to-body destroy-on-close @close="handleClose">
    <el-form ref="formRef" :model="form" :rules="rules" label-position="top" label-width="120px">
      <el-form-item label="项目阶段" prop="planNodeId">
        <el-select v-model="form.planNodeId" placeholder="请选择" clearable style="width: 100%">
        <el-select v-model="form.planNodeId" placeholder="请选择" style="width: 100%" @change="handleStageChange">
          <el-option v-for="opt in stageOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
        </el-select>
        <div v-if="stageTip" class="stage-tip">{{ stageTip }}</div>
      </el-form-item>
      <el-row :gutter="20">
        <el-col :span="8">
@@ -63,8 +64,8 @@
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="上次进度(%)" prop="lastProgress">
            <el-input-number v-model="form.lastProgress" :min="0" :max="100" controls-position="right" style="width: 100%" />
          <el-form-item label="上次进度(%)">
            <el-input-number v-model="form.lastProgress" :min="0" :max="100" controls-position="right" style="width: 100%" disabled />
          </el-form-item>
        </el-col>
        <el-col :span="8">
@@ -115,6 +116,70 @@
        </el-col>
      </el-row>
      <div v-if="isTeamStage" class="team-block">
        <div class="team-header">
          <span class="team-title">项目团队</span>
          <el-button type="primary" size="small" @click="addTeamRow">新增成员</el-button>
        </div>
        <el-table :data="teamRows" border size="small">
          <el-table-column label="姓名" width="150">
            <template #default="{ row }">
              <el-select v-model="row.userId" placeholder="请选择" filterable clearable style="width: 100%">
                <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
              </el-select>
            </template>
          </el-table-column>
          <el-table-column label="项目组角色" width="150">
            <template #default="{ row }">
              <el-select v-model="row.userRoleId" placeholder="请选择" filterable clearable style="width: 100%">
                <el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" />
              </el-select>
            </template>
          </el-table-column>
          <el-table-column label="进入日期" width="150">
            <template #default="{ row }">
              <el-date-picker
                v-model="row.joinTime"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择"
                style="width: 100%"
                clearable
              />
            </template>
          </el-table-column>
          <el-table-column label="离开日期" width="150">
            <template #default="{ row }">
              <el-date-picker
                v-model="row.departTime"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择"
                style="width: 100%"
                clearable
              />
            </template>
          </el-table-column>
          <el-table-column label="联系方式" min-width="150">
            <template #default="{ row }">
              <el-input v-model="row.contact" placeholder="请输入" clearable />
            </template>
          </el-table-column>
          <el-table-column label="备注" min-width="140">
            <template #default="{ row }">
              <el-input v-model="row.remark" placeholder="请输入" clearable />
            </template>
          </el-table-column>
          <el-table-column label="操作" width="70" align="center">
            <template #default="{ $index }">
              <el-button link type="danger" size="small" @click="removeTeamRow($index)">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
      <el-form-item label="附件" prop="attachmentIds">
        <FileUpload v-model:file-list="form.storageBlobDTOs" />
      </el-form-item>
@@ -130,16 +195,18 @@
</template>
<script setup name="ProgressReportDialog">
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { getToken } from '@/utils/auth'
import FileUpload from "@/components/AttachmentUpload/file/index.vue";
import { computed, ref, watch } from 'vue'
import { ElMessageBox } from 'element-plus'
import FileUpload from '@/components/AttachmentUpload/file/index.vue'
import { findRoleListPage } from '@/api/projectManagement/role'
import { userListAll } from '@/api/publicApi'
const props = defineProps({
  modelValue: { type: Boolean, default: false },
  projectId: { type: [Number, String], default: undefined },
  projectInfo: { type: Object, default: () => ({}) },
  planNodes: { type: Array, default: () => [] },
  stageRecords: { type: Array, default: () => [] },
  defaultPlanNodeId: { type: [Number, String], default: undefined }
})
@@ -151,6 +218,9 @@
})
const formRef = ref()
const userOptions = ref([])
const roleOptions = ref([])
const teamRows = ref([])
const form = ref({
  planNodeId: undefined,
@@ -176,40 +246,176 @@
  completionProgress: [{ required: true, message: '请输入', trigger: 'change' }]
}
const stageOptions = computed(() => {
const sortedNodes = computed(() => {
  const list = Array.isArray(props.planNodes) ? props.planNodes : []
  const sorted = [...list].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
  return sorted
  return [...list].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
})
const stageOptions = computed(() =>
  sortedNodes.value
    .map(n => ({
      label: n.name || n.workContent || n.title || String(n.id ?? ''),
      value: n.id
    }))
    .filter(i => i.value !== undefined && i.value !== null && i.value !== '')
)
const stageNameOf = nodeId => {
  const node = sortedNodes.value.find(n => String(n.id) === String(nodeId))
  return node?.name || node?.workContent || ''
}
// 各阶段最近一条汇报记录(记录按阶段、时间升序返回)
const latestProgressMap = computed(() => {
  const map = {}
  const records = Array.isArray(props.stageRecords) ? props.stageRecords : []
  records.forEach(r => {
    const key = String(r.projectManagementPlanNodeId ?? '')
    map[key] = Number(r.progress) || 0
  })
  return map
})
// 当前应汇报的下一阶段:第一个未完成(进度 < 100)的阶段
const expectedNodeId = computed(() => {
  const nodes = sortedNodes.value
  if (nodes.length === 0) return undefined
  const next = nodes.find(n => (latestProgressMap.value[String(n.id)] ?? 0) < 100)
  return (next || nodes[nodes.length - 1])?.id
})
const isTeamStage = computed(() => stageNameOf(form.value.planNodeId).includes('团队'))
const stageTip = computed(() => {
  if (!form.value.planNodeId) return ''
  const progress = latestProgressMap.value[String(form.value.planNodeId)] ?? 0
  if (progress >= 100) return '该阶段已完成,仍可继续补充汇报'
  if (expectedNodeId.value && String(form.value.planNodeId) !== String(expectedNodeId.value)) {
    return '当前选择的不是应汇报的下一阶段'
  }
  return ''
})
function todayStr() {
  const d = new Date()
  const pad = n => String(n).padStart(2, '0')
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}
function latestRecordOf(nodeId) {
  const records = Array.isArray(props.stageRecords) ? props.stageRecords : []
  const matched = records.filter(r => String(r.projectManagementPlanNodeId) === String(nodeId))
  return matched.length ? matched[matched.length - 1] : null
}
function applyStageBase(nodeId) {
  const record = latestRecordOf(nodeId)
  form.value.lastProgress = Number(record?.progress ?? 0) || 0
  form.value.completionProgress = 0
  form.value.totalProgress = Number(record?.progress ?? 0) || 0
  form.value.planStartTime = record?.planStartTime || props.projectInfo?.planStartTime || ''
  form.value.planEndTime = record?.planEndTime || props.projectInfo?.planEndTime || ''
  form.value.actualStartTime = record?.actualStartTime || props.projectInfo?.actualStartTime || ''
  form.value.actualEndTime = record?.actualEndTime || ''
}
async function handleStageChange(val) {
  applyStageBase(val)
  if (!val) return
  const expected = expectedNodeId.value
  if (expected && String(val) !== String(expected)) {
    try {
      await ElMessageBox.confirm(
        `当前应汇报的“${stageNameOf(expected)}”阶段还未完成,选择其他阶段将跳过中间阶段,是否继续?`,
        '提示',
        { confirmButtonText: '继续', cancelButtonText: '返回', type: 'warning' }
      )
    } catch {
      form.value.planNodeId = expected
      applyStageBase(expected)
    }
  }
}
function resetFromProject() {
  const info = props.projectInfo || {}
  form.value = {
    planNodeId: props.defaultPlanNodeId ?? stageOptions.value[0]?.value,
    planStartTime: info.planStartTime || '',
    planEndTime: info.planEndTime || '',
    actualStartTime: info.actualStartTime || '',
    actualEndTime: info.actualEndTime || '',
    reportDate: '',
    lastProgress: Number(info.lastProgress ?? 0) || 0,
    planNodeId: props.defaultPlanNodeId ?? expectedNodeId.value ?? stageOptions.value[0]?.value,
    planStartTime: '',
    planEndTime: '',
    actualStartTime: '',
    actualEndTime: '',
    reportDate: todayStr(),
    lastProgress: 0,
    completionProgress: 0,
    totalProgress: Number(info.totalProgress ?? info.progress ?? 0) || 0,
    totalProgress: 0,
    managerName: info.managerName || '',
    departmentName: info.departmentName || '',
    remark: '',
    storageBlobDTOs: []
  }
  applyStageBase(form.value.planNodeId)
  form.value.reportDate = todayStr()
  loadTeamRows()
}
function loadTeamRows() {
  const list = Array.isArray(props.projectInfo?.teamList) ? props.projectInfo.teamList : []
  teamRows.value = list.map(t => ({
    userId: t.userId,
    userRoleId: t.userRoleId,
    joinTime: t.joinTime || '',
    departTime: t.departTime || '',
    contact: t.contact || '',
    remark: t.remark || ''
  }))
  if (teamRows.value.length === 0) addTeamRow()
}
function addTeamRow() {
  teamRows.value.push({ userId: undefined, userRoleId: undefined, joinTime: '', departTime: '', contact: '', remark: '' })
}
function removeTeamRow(index) {
  teamRows.value.splice(index, 1)
  if (teamRows.value.length === 0) addTeamRow()
}
async function loadOptions() {
  try {
    const res = await userListAll()
    const list = res?.data || res?.rows || res || []
    userOptions.value = (Array.isArray(list) ? list : []).map(u => ({
      label: u.nickName || u.userName || u.username || u.name,
      value: u.userId || u.id
    }))
  } catch {
    userOptions.value = []
  }
  try {
    const res = await findRoleListPage({ pageNum: 1, pageSize: 999 })
    const records = res?.data?.records || res?.rows || res?.records || []
    roleOptions.value = records.map(item => ({ label: item.roleName || item.name, value: item.id }))
  } catch {
    roleOptions.value = []
  }
}
watch(
  () => props.modelValue,
  v => {
    if (v) resetFromProject()
    if (v) {
      loadOptions()
      resetFromProject()
    }
  }
)
// 累计进度跟随本次完成进度
watch(
  () => form.value.completionProgress,
  v => {
    form.value.totalProgress = Number(v) || 0
  }
)
@@ -220,15 +426,39 @@
function markDone() {
  form.value.completionProgress = 100
  form.value.totalProgress = 100
  if (!form.value.actualEndTime) form.value.actualEndTime = form.value.reportDate || ''
  if (!form.value.actualEndTime) form.value.actualEndTime = form.value.reportDate || todayStr()
}
function buildTeamList() {
  const findLabel = (list, value) => (list || []).find(i => String(i.value) === String(value))?.label || ''
  return teamRows.value
    .filter(r => r.userId !== undefined && r.userId !== null && r.userId !== '')
    .map(r => ({
      userId: Number(r.userId),
      userName: findLabel(userOptions.value, r.userId),
      userRoleId: r.userRoleId ?? '',
      userRoleName: findLabel(roleOptions.value, r.userRoleId),
      joinTime: r.joinTime || null,
      departTime: r.departTime || null,
      contact: r.contact || '',
      remark: r.remark || ''
    }))
}
async function submit() {
  await formRef.value?.validate?.()
  emit('submitted', {
  try {
    await formRef.value?.validate?.()
  } catch {
    return
  }
  const payload = {
    projectId: props.projectId,
    ...form.value
  })
  }
  if (isTeamStage.value) {
    payload.teamList = buildTeamList()
  }
  emit('submitted', payload)
  visible.value = false
}
</script>
@@ -239,4 +469,28 @@
  justify-content: flex-end;
  gap: 10px;
}
.stage-tip {
  margin-top: 6px;
  font-size: 12px;
  color: #e6a23c;
  line-height: 1.4;
}
.team-block {
  margin-bottom: 18px;
}
.team-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
}
.team-title {
  font-weight: 600;
  color: #303133;
  font-size: 14px;
}
</style>