|
|
@@ -347,7 +347,13 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref } from "vue";
|
|
|
+import { onMounted, ref, watch } from "vue";
|
|
|
+import { ElMessage } from "element-plus";
|
|
|
+import {
|
|
|
+ getAbnormalReports,
|
|
|
+ getOverviewList,
|
|
|
+ getOverviewStats,
|
|
|
+} from "@/api/project";
|
|
|
|
|
|
defineOptions({
|
|
|
name: "ProjectOverview",
|
|
|
@@ -372,264 +378,323 @@ const reportDialogVisible = ref(false);
|
|
|
const reportDialogType = ref("warning");
|
|
|
const reportDialogList = ref([]);
|
|
|
|
|
|
-const warningReportList = [
|
|
|
- {
|
|
|
- id: 1,
|
|
|
- projectName: "垦利10-1油田开发项目",
|
|
|
- reportName: "可行性研究-环境质量现状调查",
|
|
|
- startTime: "2026-06-05",
|
|
|
- duration: "60天",
|
|
|
- planEndTime: "2026-08-05",
|
|
|
- days: 2,
|
|
|
- unit: "樟树市邦厉有限公司",
|
|
|
- owner: "苏小苏",
|
|
|
- },
|
|
|
- {
|
|
|
- id: 2,
|
|
|
- projectName: "渤中19-6凝析气田开发项目",
|
|
|
- reportName: "详细设计/建造安装-通航安全影响咨询",
|
|
|
- startTime: "2026-05-01",
|
|
|
- duration: "90天",
|
|
|
- planEndTime: "2026-08-20",
|
|
|
- days: 22,
|
|
|
- unit: "天津海蓝工程咨询有限公司",
|
|
|
- owner: "李明",
|
|
|
- },
|
|
|
-];
|
|
|
+const ABNORMAL_TYPE_MAP = {
|
|
|
+ warning: 1,
|
|
|
+ delay: 2,
|
|
|
+};
|
|
|
|
|
|
-const delayReportList = [
|
|
|
- {
|
|
|
- id: 1,
|
|
|
- projectName: "锦州25-1油田开发项目",
|
|
|
- reportName: "可行性研究-通航安全影响咨询",
|
|
|
- startTime: "2026-05-01",
|
|
|
- duration: "90天",
|
|
|
- planEndTime: "2026-07-30",
|
|
|
- days: 3,
|
|
|
- unit: "天津海蓝工程咨询有限公司",
|
|
|
- owner: "王强",
|
|
|
- },
|
|
|
-];
|
|
|
+const mapAbnormalReports = (list = []) => {
|
|
|
+ const result = [];
|
|
|
+ list.forEach((project) => {
|
|
|
+ (project.reports || []).forEach((report) => {
|
|
|
+ (report.nodes || []).forEach((node) => {
|
|
|
+ result.push({
|
|
|
+ id: node.id,
|
|
|
+ projectName: project.projectName || "",
|
|
|
+ reportName: report.reportName || "",
|
|
|
+ startTime: node.startDate || "",
|
|
|
+ duration:
|
|
|
+ node.duration != null && node.duration !== ""
|
|
|
+ ? `${node.duration}天`
|
|
|
+ : "",
|
|
|
+ planEndTime: node.plannedEndDate || "",
|
|
|
+ days: node.daysDiff ?? 0,
|
|
|
+ unit: node.reportUnit || "",
|
|
|
+ owner: node.personInCharge || "",
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+ return result;
|
|
|
+};
|
|
|
|
|
|
-const onSummaryNumClick = (card) => {
|
|
|
+const onSummaryNumClick = async (card) => {
|
|
|
if (card.key !== "warning" && card.key !== "delay") return;
|
|
|
+ const type = ABNORMAL_TYPE_MAP[card.key];
|
|
|
reportDialogType.value = card.key;
|
|
|
- reportDialogList.value =
|
|
|
- card.key === "warning" ? warningReportList : delayReportList;
|
|
|
- reportDialogVisible.value = true;
|
|
|
+ try {
|
|
|
+ const res = await getAbnormalReports(type);
|
|
|
+ if (res?.code !== 200) {
|
|
|
+ ElMessage.error(res?.msg || "获取异常报告失败");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ reportDialogList.value = mapAbnormalReports(
|
|
|
+ Array.isArray(res.data) ? res.data : []
|
|
|
+ );
|
|
|
+ reportDialogVisible.value = true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error);
|
|
|
+ ElMessage.error("获取异常报告失败,请稍后重试");
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
-const summaryCards = [
|
|
|
+const PHASE_NAME_MAP = {
|
|
|
+ 1: "可行性研究",
|
|
|
+ 3: "基本设计",
|
|
|
+ 5: "详细设计/建造安装",
|
|
|
+};
|
|
|
+
|
|
|
+const PHASE_ORDER = [1, 3, 5];
|
|
|
+
|
|
|
+const getPhaseCounts = (phaseCounts = []) => {
|
|
|
+ const countMap = {};
|
|
|
+ phaseCounts.forEach((item) => {
|
|
|
+ countMap[item.phaseType] = item.count ?? 0;
|
|
|
+ });
|
|
|
+ return PHASE_ORDER.map((phaseType) => ({
|
|
|
+ name: PHASE_NAME_MAP[phaseType],
|
|
|
+ count: countMap[phaseType] ?? 0,
|
|
|
+ }));
|
|
|
+};
|
|
|
+
|
|
|
+const createEmptySummaryCards = () => [
|
|
|
{
|
|
|
key: "total",
|
|
|
label: "项目总数",
|
|
|
- value: "24",
|
|
|
+ value: "0",
|
|
|
unit: "个项目",
|
|
|
color: "#2382E7",
|
|
|
icon: "total_number_of_projects",
|
|
|
- phases: [
|
|
|
- { name: "可行性研究", count: 8 },
|
|
|
- { name: "基本设计", count: 8 },
|
|
|
- { name: "详细设计/建造安装", count: 8 },
|
|
|
- ],
|
|
|
+ phases: getPhaseCounts([]),
|
|
|
},
|
|
|
{
|
|
|
key: "warning",
|
|
|
label: "预警报告",
|
|
|
tip: "(计划日期内仍未完成)",
|
|
|
- value: "2",
|
|
|
+ value: "0",
|
|
|
unit: "个项目",
|
|
|
color: "#FF8E38",
|
|
|
icon: "early_warning_report",
|
|
|
- phases: [
|
|
|
- { name: "可行性研究", count: 1 },
|
|
|
- { name: "基本设计", count: 0 },
|
|
|
- { name: "详细设计/建造安装", count: 1 },
|
|
|
- ],
|
|
|
+ phases: getPhaseCounts([]),
|
|
|
},
|
|
|
{
|
|
|
key: "delay",
|
|
|
label: "滞后报告",
|
|
|
tip: "(计划日期内仍未完成)",
|
|
|
- value: "1",
|
|
|
+ value: "0",
|
|
|
unit: "个项目",
|
|
|
color: "#F54848",
|
|
|
icon: "late_report",
|
|
|
- phases: [
|
|
|
- { name: "可行性研究", count: 1 },
|
|
|
- { name: "基本设计", count: 0 },
|
|
|
- { name: "详细设计/建造安装", count: 0 },
|
|
|
- ],
|
|
|
+ phases: getPhaseCounts([]),
|
|
|
},
|
|
|
{
|
|
|
key: "rate",
|
|
|
label: "报告完成率",
|
|
|
- value: "58",
|
|
|
+ value: "0",
|
|
|
unit: "%",
|
|
|
color: "#10B837",
|
|
|
icon: "report_completion_rate",
|
|
|
subTitle: "年度计划完成率",
|
|
|
subReport: {
|
|
|
- prefix: "2026年报告:",
|
|
|
- value: "58",
|
|
|
- total: "100",
|
|
|
+ prefix: `${new Date().getFullYear()}年报告:`,
|
|
|
+ value: "0",
|
|
|
+ total: "0",
|
|
|
},
|
|
|
},
|
|
|
];
|
|
|
|
|
|
-const createNormalPopover = (name, planTime = "2026-07-30") => ({
|
|
|
- name,
|
|
|
- planTime,
|
|
|
- completeTime: planTime,
|
|
|
-});
|
|
|
-
|
|
|
-const projectList = [
|
|
|
- {
|
|
|
- id: 1,
|
|
|
- name: "垦利10-1油田开发项目",
|
|
|
- progress: 20,
|
|
|
- timeline: {
|
|
|
- plan: [
|
|
|
- { type: "plan", flex: 1 },
|
|
|
- { type: "empty", flex: 13 },
|
|
|
- ],
|
|
|
- actual: [
|
|
|
- {
|
|
|
- type: "warning",
|
|
|
- flex: 1,
|
|
|
- label: "环境质量现状调查",
|
|
|
- popover: {
|
|
|
- name: "环境质量现状调查",
|
|
|
- planTime: "2026-08-05",
|
|
|
- remainDays: 2,
|
|
|
- },
|
|
|
- },
|
|
|
- { type: "empty", flex: 13 },
|
|
|
- ],
|
|
|
+const summaryCards = ref(createEmptySummaryCards());
|
|
|
+
|
|
|
+const applyOverviewStats = (data = {}) => {
|
|
|
+ const projectTotal = data.projectTotal || {};
|
|
|
+ const warning = data.warning || {};
|
|
|
+ const delayed = data.delayed || {};
|
|
|
+ const completion = data.completion || {};
|
|
|
+ const year = new Date().getFullYear();
|
|
|
+
|
|
|
+ summaryCards.value = [
|
|
|
+ {
|
|
|
+ key: "total",
|
|
|
+ label: "项目总数",
|
|
|
+ value: String(projectTotal.total ?? 0),
|
|
|
+ unit: "个项目",
|
|
|
+ color: "#2382E7",
|
|
|
+ icon: "total_number_of_projects",
|
|
|
+ phases: getPhaseCounts(projectTotal.phaseCounts),
|
|
|
},
|
|
|
- warning: 1,
|
|
|
- delay: 0,
|
|
|
- },
|
|
|
- {
|
|
|
- id: 2,
|
|
|
- name: "锦州25-1油田开发项目",
|
|
|
- progress: 43,
|
|
|
- timeline: {
|
|
|
- plan: [
|
|
|
- { type: "done", flex: 5 },
|
|
|
- { type: "plan", flex: 1 },
|
|
|
- { type: "empty", flex: 8 },
|
|
|
- ],
|
|
|
- actual: [
|
|
|
- {
|
|
|
- type: "actual",
|
|
|
- flex: 1,
|
|
|
- label: "环境质量现状调查",
|
|
|
- popover: createNormalPopover("环境质量现状调查"),
|
|
|
- },
|
|
|
- {
|
|
|
- type: "actual",
|
|
|
- flex: 1,
|
|
|
- label: "渔业资源现状调查",
|
|
|
- popover: createNormalPopover("渔业资源现状调查"),
|
|
|
- },
|
|
|
- {
|
|
|
- type: "actual",
|
|
|
- flex: 1,
|
|
|
- label: "环境风险预评价",
|
|
|
- popover: createNormalPopover("环境风险预评价"),
|
|
|
- },
|
|
|
- {
|
|
|
- type: "actual",
|
|
|
- flex: 1,
|
|
|
- label: "桌面路由方案",
|
|
|
- popover: createNormalPopover("桌面路由方案"),
|
|
|
- },
|
|
|
- {
|
|
|
- type: "delay",
|
|
|
- flex: 1,
|
|
|
- label: "通航安全影响咨询",
|
|
|
- popover: {
|
|
|
- name: "通航安全影响咨询",
|
|
|
- planTime: "2026-07-30",
|
|
|
- overdueDays: 3,
|
|
|
- },
|
|
|
- },
|
|
|
- {
|
|
|
- type: "actual",
|
|
|
- flex: 1,
|
|
|
- label: "安全预评价",
|
|
|
- popover: createNormalPopover("安全预评价"),
|
|
|
- },
|
|
|
- { type: "empty", flex: 8 },
|
|
|
- ],
|
|
|
+ {
|
|
|
+ key: "warning",
|
|
|
+ label: "预警报告",
|
|
|
+ tip: "(计划日期内仍未完成)",
|
|
|
+ value: String(warning.total ?? 0),
|
|
|
+ unit: "个项目",
|
|
|
+ color: "#FF8E38",
|
|
|
+ icon: "early_warning_report",
|
|
|
+ phases: getPhaseCounts(warning.phaseCounts),
|
|
|
},
|
|
|
- warning: 0,
|
|
|
- delay: 1,
|
|
|
- },
|
|
|
- {
|
|
|
- id: 3,
|
|
|
- name: "渤中19-6凝析气田开发项目",
|
|
|
- progress: 100,
|
|
|
- timeline: {
|
|
|
- plan: [
|
|
|
- { type: "done", flex: 1 },
|
|
|
- { type: "done", flex: 1 },
|
|
|
- { type: "done", flex: 1 },
|
|
|
- { type: "done", flex: 1 },
|
|
|
- { type: "done", flex: 1 },
|
|
|
- { type: "done", flex: 1 },
|
|
|
- { type: "done", flex: 1 },
|
|
|
- { type: "done", flex: 1 },
|
|
|
- { type: "done", flex: 1 },
|
|
|
- { type: "done", flex: 1 },
|
|
|
- { type: "done", flex: 1 },
|
|
|
- { type: "done", flex: 1 },
|
|
|
- { type: "done", flex: 1 },
|
|
|
- { type: "done", flex: 1 },
|
|
|
- ],
|
|
|
- actual: [
|
|
|
- "环境质量现状调查",
|
|
|
- "环境影响评价",
|
|
|
- "可行性研究报告",
|
|
|
- "项目建议书",
|
|
|
- "核准批复",
|
|
|
- "总图设计",
|
|
|
- "工艺设计",
|
|
|
- "结构设计",
|
|
|
- "公用工程设计",
|
|
|
- "基本设计审查",
|
|
|
- "详细设计",
|
|
|
- "设备采购",
|
|
|
- "建造安装",
|
|
|
- "竣工验收",
|
|
|
- ].map((label) => ({
|
|
|
- type: "actual",
|
|
|
- flex: 1,
|
|
|
- label,
|
|
|
- popover: createNormalPopover(label),
|
|
|
- })),
|
|
|
+ {
|
|
|
+ key: "delay",
|
|
|
+ label: "滞后报告",
|
|
|
+ tip: "(计划日期内仍未完成)",
|
|
|
+ value: String(delayed.total ?? 0),
|
|
|
+ unit: "个项目",
|
|
|
+ color: "#F54848",
|
|
|
+ icon: "late_report",
|
|
|
+ phases: getPhaseCounts(delayed.phaseCounts),
|
|
|
},
|
|
|
- warning: 0,
|
|
|
- delay: 0,
|
|
|
- },
|
|
|
- {
|
|
|
- id: 4,
|
|
|
- name: "曹妃甸6-4油田开发项目",
|
|
|
- progress: 0,
|
|
|
- timeline: { plan: [], actual: [] },
|
|
|
- warning: 0,
|
|
|
- delay: 0,
|
|
|
- },
|
|
|
- {
|
|
|
- id: 5,
|
|
|
- name: "旅大5-2油田开发项目",
|
|
|
- progress: 0,
|
|
|
- timeline: { plan: [], actual: [] },
|
|
|
- warning: 0,
|
|
|
- delay: 0,
|
|
|
- },
|
|
|
-];
|
|
|
+ {
|
|
|
+ key: "rate",
|
|
|
+ label: "报告完成率",
|
|
|
+ value: String(completion.rate ?? 0),
|
|
|
+ unit: "%",
|
|
|
+ color: "#10B837",
|
|
|
+ icon: "report_completion_rate",
|
|
|
+ subTitle: "年度计划完成率",
|
|
|
+ subReport: {
|
|
|
+ prefix: `${year}年报告:`,
|
|
|
+ value: String(completion.yearCompleted ?? 0),
|
|
|
+ total: String(completion.yearTotal ?? 0),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ];
|
|
|
+};
|
|
|
+
|
|
|
+const loadOverviewStats = async () => {
|
|
|
+ try {
|
|
|
+ const res = await getOverviewStats();
|
|
|
+ if (res?.code !== 200) {
|
|
|
+ ElMessage.error(res?.msg || "获取概览统计失败");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ applyOverviewStats(res.data || {});
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error);
|
|
|
+ ElMessage.error("获取概览统计失败,请稍后重试");
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ loadOverviewStats();
|
|
|
+ loadOverviewList();
|
|
|
+});
|
|
|
+
|
|
|
+watch(listFilter, () => {
|
|
|
+ loadOverviewList();
|
|
|
+});
|
|
|
+
|
|
|
+const STATUS_FILTER_MAP = {
|
|
|
+ all: 0,
|
|
|
+ warning: 1,
|
|
|
+ delay: 2,
|
|
|
+};
|
|
|
+
|
|
|
+const buildNodePopover = (node, reportName) => {
|
|
|
+ const popover = {
|
|
|
+ name: reportName || "",
|
|
|
+ planTime: node.plannedEndDate || "",
|
|
|
+ };
|
|
|
+ if (Number(node.status) === 3) {
|
|
|
+ popover.remainDays = node.daysDiff ?? null;
|
|
|
+ } else if (Number(node.status) === 4) {
|
|
|
+ popover.overdueDays = node.daysDiff ?? null;
|
|
|
+ } else if (Number(node.status) === 2) {
|
|
|
+ popover.completeTime = node.actualEndDate || node.plannedEndDate || "";
|
|
|
+ }
|
|
|
+ return popover;
|
|
|
+};
|
|
|
+
|
|
|
+const PHASE_FLEX_MAP = {
|
|
|
+ 1: 5,
|
|
|
+ 3: 5,
|
|
|
+ 5: 4,
|
|
|
+};
|
|
|
+
|
|
|
+const collectPhaseNodes = (phase) => {
|
|
|
+ const nodes = [];
|
|
|
+ (phase?.reports || []).forEach((report) => {
|
|
|
+ (report.nodes || []).forEach((node) => {
|
|
|
+ nodes.push({
|
|
|
+ ...node,
|
|
|
+ reportName: report.reportName || "",
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+ return nodes;
|
|
|
+};
|
|
|
+
|
|
|
+const buildNodeActualSeg = (node, flex) => {
|
|
|
+ const status = Number(node.status);
|
|
|
+ let type = "empty";
|
|
|
+ if (status === 1 || status === 2) type = "actual";
|
|
|
+ else if (status === 3) type = "warning";
|
|
|
+ else if (status === 4) type = "delay";
|
|
|
+
|
|
|
+ const seg = {
|
|
|
+ type,
|
|
|
+ flex,
|
|
|
+ label: type === "empty" ? "" : node.reportName,
|
|
|
+ };
|
|
|
+ if (type !== "empty") {
|
|
|
+ seg.popover = buildNodePopover(node, node.reportName);
|
|
|
+ }
|
|
|
+ return seg;
|
|
|
+};
|
|
|
+
|
|
|
+const buildTimeline = (phases = []) => {
|
|
|
+ const phaseMap = {};
|
|
|
+ phases.forEach((phase) => {
|
|
|
+ phaseMap[Number(phase.phaseType)] = phase;
|
|
|
+ });
|
|
|
+
|
|
|
+ const hasAnyNode = PHASE_ORDER.some(
|
|
|
+ (phaseType) => collectPhaseNodes(phaseMap[phaseType]).length > 0
|
|
|
+ );
|
|
|
+ if (!hasAnyNode) {
|
|
|
+ return { plan: [], actual: [] };
|
|
|
+ }
|
|
|
+
|
|
|
+ const plan = [];
|
|
|
+ const actual = [];
|
|
|
+
|
|
|
+ PHASE_ORDER.forEach((phaseType) => {
|
|
|
+ const phaseFlex = PHASE_FLEX_MAP[phaseType];
|
|
|
+ const nodes = collectPhaseNodes(phaseMap[phaseType]);
|
|
|
+
|
|
|
+ if (!nodes.length) {
|
|
|
+ plan.push({ type: "empty", flex: phaseFlex });
|
|
|
+ actual.push({ type: "empty", flex: phaseFlex });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const unit = phaseFlex / nodes.length;
|
|
|
+ nodes.forEach((node) => {
|
|
|
+ plan.push({
|
|
|
+ type: Number(node.status) === 2 ? "done" : "plan",
|
|
|
+ flex: unit,
|
|
|
+ });
|
|
|
+ actual.push(buildNodeActualSeg(node, unit));
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ return { plan, actual };
|
|
|
+};
|
|
|
+
|
|
|
+const mapOverviewListItem = (item) => ({
|
|
|
+ id: item.id,
|
|
|
+ name: item.projectName || "",
|
|
|
+ progress: item.overallProgress ?? 0,
|
|
|
+ timeline: buildTimeline(item.phases || []),
|
|
|
+ warning: item.warningCount ?? 0,
|
|
|
+ delay: item.delayedCount ?? 0,
|
|
|
+});
|
|
|
+
|
|
|
+const projectList = ref([]);
|
|
|
+
|
|
|
+const loadOverviewList = async () => {
|
|
|
+ try {
|
|
|
+ const status = STATUS_FILTER_MAP[listFilter.value] ?? 0;
|
|
|
+ const res = await getOverviewList(status);
|
|
|
+ if (res?.code !== 200) {
|
|
|
+ ElMessage.error(res?.msg || "获取项目列表失败");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const list = Array.isArray(res.data) ? res.data : [];
|
|
|
+ projectList.value = list.map(mapOverviewListItem);
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error);
|
|
|
+ ElMessage.error("获取项目列表失败,请稍后重试");
|
|
|
+ }
|
|
|
+};
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
@@ -1521,5 +1586,14 @@ const projectList = [
|
|
|
border: 1px solid #e4eaf2;
|
|
|
border-radius: 2px;
|
|
|
cursor: pointer;
|
|
|
+
|
|
|
+ &:hover,
|
|
|
+ &:focus,
|
|
|
+ &:focus-visible {
|
|
|
+ color: #58657c;
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1px solid #e4eaf2;
|
|
|
+ outline: none;
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|