<!--
|
进度汇报(→ POST /projectManagement/info/saveStage)
|
|
字段以管理端 projectDetail.vue 的 handleProgressSubmitted 组装出的请求体为准。
|
管理端 ProgressReportDialog 里还有「上次进度 / 负责人 / 部门」三个输入框,但它们
|
并不在请求体里(负责人取的是当前登录人,不是表单值),所以这里不做可编辑控件:
|
「上次进度」由已有汇报记录推算后只读展示,「负责人」显示将被记录的当前登录人,
|
避免出现填了不生效的字段。
|
-->
|
<template>
|
<view class="project-progress">
|
<PageHeader title="进度汇报"
|
@back="goBack" />
|
|
<view v-if="projectName"
|
class="project-banner">
|
<text class="banner-label">项目名称</text>
|
<text class="banner-value">{{ projectName }}</text>
|
</view>
|
|
<view class="form-container">
|
<up-form ref="formRef"
|
:model="form"
|
:rules="rules"
|
label-width="120"
|
input-align="right"
|
error-message-align="right">
|
<u-cell-group title="阶段信息"
|
class="form-section">
|
<up-form-item label="项目阶段"
|
prop="planNodeId"
|
required>
|
<FormPicker v-model="form.planNodeId"
|
type="select"
|
title="选择项目阶段"
|
:options="stageOptions"
|
placeholder="请选择项目阶段" />
|
</up-form-item>
|
<up-form-item label="计划开始时间"
|
prop="planStartTime"
|
required>
|
<FormPicker v-model="form.planStartTime"
|
type="date"
|
placeholder="请选择计划开始时间" />
|
</up-form-item>
|
<up-form-item label="计划完工时间"
|
prop="planEndTime"
|
required>
|
<FormPicker v-model="form.planEndTime"
|
type="date"
|
placeholder="请选择计划完工时间" />
|
</up-form-item>
|
<up-form-item label="实际开工日期"
|
prop="actualStartTime">
|
<FormPicker v-model="form.actualStartTime"
|
type="date"
|
placeholder="请选择实际开工日期" />
|
</up-form-item>
|
</u-cell-group>
|
|
<u-cell-group title="进度情况"
|
class="form-section">
|
<up-form-item label="本次进度日期"
|
prop="reportDate"
|
required>
|
<FormPicker v-model="form.reportDate"
|
type="date"
|
placeholder="请选择本次进度日期" />
|
</up-form-item>
|
<up-form-item label="上次进度(%)">
|
<up-input :model-value="String(lastProgress)"
|
disabled />
|
</up-form-item>
|
<up-form-item label="完成进度(%)"
|
prop="completionProgress"
|
required>
|
<view class="progress-input">
|
<up-input v-model="form.completionProgress"
|
type="number"
|
placeholder="请输入完成进度" />
|
<up-button type="primary"
|
size="small"
|
text="完成"
|
@click="markDone" />
|
</view>
|
</up-form-item>
|
<up-form-item label="累计进度(%)"
|
prop="totalProgress">
|
<up-input v-model="form.totalProgress"
|
type="number"
|
placeholder="留空则取完成进度" />
|
</up-form-item>
|
<up-form-item label="实际完工日期"
|
prop="actualEndTime">
|
<FormPicker v-model="form.actualEndTime"
|
type="date"
|
placeholder="请选择实际完工日期" />
|
</up-form-item>
|
</u-cell-group>
|
|
<u-cell-group title="汇报信息"
|
class="form-section">
|
<up-form-item label="负责人">
|
<up-input :model-value="leaderName || '当前登录人'"
|
disabled />
|
</up-form-item>
|
<up-form-item label="备注"
|
prop="remark">
|
<up-textarea v-model="form.remark"
|
placeholder="请输入备注"
|
:maxlength="200"
|
count
|
auto-height />
|
</up-form-item>
|
<view class="upload-block">
|
<CommonUpload v-model="attachmentFiles"
|
accept="all" />
|
</view>
|
</u-cell-group>
|
</up-form>
|
</view>
|
|
<FooterButtons :loading="loading"
|
confirm-text="提交汇报"
|
@cancel="goBack"
|
@confirm="handleSubmit" />
|
</view>
|
</template>
|
|
<script setup>
|
import { computed, ref } from "vue";
|
import { onLoad } from "@dcloudio/uni-app";
|
import PageHeader from "@/components/PageHeader.vue";
|
import FooterButtons from "@/components/FooterButtons.vue";
|
import FormPicker from "@/components/FormPicker.vue";
|
import CommonUpload from "@/components/CommonUpload.vue";
|
import useUserStore from "@/store/modules/user";
|
import { getProject, saveStage } from "@/api/projectManagement/project";
|
import {
|
buildStagePayload,
|
defaultPlanNodeId,
|
fetchPlanNodeList,
|
fetchProjectStages,
|
planNodeOptions,
|
} from "./_utils/stage";
|
|
const userStore = useUserStore();
|
|
const formRef = ref();
|
const loading = ref(false);
|
const projectId = ref("");
|
const info = ref({});
|
const projectName = ref("");
|
const planNodes = ref([]);
|
const lastProgress = ref(0);
|
const attachmentFiles = ref([]);
|
|
const leaderName = computed(() => userStore.nickName || "");
|
|
const stageOptions = computed(() => planNodeOptions(planNodes.value));
|
|
const form = ref({
|
planNodeId: "",
|
planStartTime: "",
|
planEndTime: "",
|
actualStartTime: "",
|
actualEndTime: "",
|
reportDate: "",
|
completionProgress: "",
|
totalProgress: "",
|
remark: "",
|
});
|
|
const rules = {
|
planNodeId: [{ required: true, message: "请选择项目阶段", trigger: "change" }],
|
planStartTime: [
|
{ required: true, message: "请选择计划开始时间", trigger: "change" },
|
],
|
planEndTime: [
|
{ required: true, message: "请选择计划完工时间", trigger: "change" },
|
],
|
reportDate: [
|
{ required: true, message: "请选择本次进度日期", trigger: "change" },
|
],
|
completionProgress: [
|
{ required: true, message: "请输入完成进度", trigger: "blur" },
|
],
|
};
|
|
const goBack = () => uni.navigateBack();
|
|
// 后端不接收「上次进度」,由已汇报记录里的最大 progress 推算,只作填写参考
|
const deriveLastProgress = stages =>
|
(Array.isArray(stages) ? stages : []).reduce((max, row) => {
|
const value = Number(row?.progress ?? 0) || 0;
|
return value > max ? value : max;
|
}, 0);
|
|
const markDone = () => {
|
form.value.completionProgress = "100";
|
form.value.totalProgress = "100";
|
if (!form.value.actualEndTime) form.value.actualEndTime = form.value.reportDate;
|
};
|
|
const loadDetail = async () => {
|
try {
|
const res = await getProject(projectId.value);
|
const raw = res?.data?.data ?? res?.data ?? res;
|
info.value = raw?.info || {};
|
projectName.value = info.value.title || "";
|
|
const [nodes, stages] = await Promise.all([
|
fetchPlanNodeList(info.value.projectManagementPlanId),
|
fetchProjectStages(projectId.value),
|
]);
|
planNodes.value = nodes;
|
lastProgress.value = deriveLastProgress(stages);
|
|
form.value.planNodeId = defaultPlanNodeId(info.value.stage, nodes);
|
form.value.planStartTime = info.value.planStartTime || "";
|
form.value.planEndTime = info.value.planEndTime || "";
|
form.value.actualStartTime = info.value.actualStartTime || "";
|
form.value.actualEndTime = info.value.actualEndTime || "";
|
// 有历史汇报才预填累计进度;没有则留空,提交时退回「完成进度」
|
form.value.totalProgress = lastProgress.value
|
? String(lastProgress.value)
|
: "";
|
} catch (error) {
|
uni.showToast({ title: "获取项目详情失败", icon: "none" });
|
}
|
};
|
|
const handleSubmit = async () => {
|
const valid = await formRef.value.validate().catch(() => false);
|
if (!valid) return;
|
|
loading.value = true;
|
saveStage(
|
buildStagePayload({
|
projectId: projectId.value,
|
projectInfo: info.value,
|
planNodes: planNodes.value,
|
form: form.value,
|
attachments: attachmentFiles.value,
|
leaderId: userStore.id,
|
leaderName: userStore.nickName,
|
})
|
)
|
.then(() => {
|
uni.showToast({ title: "汇报成功", icon: "success" });
|
setTimeout(() => uni.navigateBack(), 300);
|
})
|
.catch(() => {
|
uni.showToast({ title: "汇报失败", icon: "none" });
|
})
|
.finally(() => {
|
loading.value = false;
|
});
|
};
|
|
onLoad(options => {
|
projectId.value = options?.id || "";
|
if (projectId.value) loadDetail();
|
});
|
</script>
|
|
<style scoped lang="scss">
|
@import "@/static/scss/form-common.scss";
|
|
.project-progress {
|
min-height: 100vh;
|
background: #f8f9fa;
|
padding-bottom: 100px;
|
}
|
|
.project-banner {
|
margin: 12px 12px 0;
|
padding: 14px 16px;
|
border-radius: 12px;
|
background: #ffffff;
|
display: flex;
|
flex-direction: column;
|
gap: 6px;
|
}
|
|
.banner-label {
|
font-size: 13px;
|
color: #909399;
|
}
|
|
.banner-value {
|
font-size: 16px;
|
font-weight: 600;
|
color: #22324d;
|
}
|
|
.form-container {
|
padding: 12px 12px 0;
|
}
|
|
.form-section {
|
margin-bottom: 12px;
|
border-radius: 12px;
|
overflow: hidden;
|
box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
|
}
|
|
.progress-input {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
width: 100%;
|
}
|
|
.upload-block {
|
padding: 12px;
|
}
|
|
:deep(.u-cell-group__title) {
|
padding: 14px 18px 10px !important;
|
font-size: 15px !important;
|
font-weight: 600 !important;
|
color: #22324d !important;
|
background: #f8fbff !important;
|
}
|
</style>
|