<!--
|
项目详情
|
|
六个 Tab:基础资料 / 项目阶段 / 诉讼信息 / 中标信息 / 销售合同 / 全流程时间轴。
|
诉讼、中标、销售合同没有「按项目查」的专用接口,管理端也是拉全量后按
|
项目 id / 项目名称匹配(见 projectDetail.vue 的 matchProject),这里照做。
|
|
「全流程时间轴」是前端聚合:阶段汇报 + 诉讼立案 + 招投标 按日期倒序拼接,
|
后端没有 timeline 接口;将来后端提供了只需换数据源,UI 不用动。
|
-->
|
<template>
|
<view class="project-detail">
|
<PageHeader title="项目详情"
|
@back="goBack">
|
<template #right>
|
<text class="header-action"
|
@click="goEdit">编辑</text>
|
</template>
|
</PageHeader>
|
|
<view v-if="!loading">
|
<view class="detail-card">
|
<view class="hero">
|
<text class="hero-title">{{ info.title || "-" }}</text>
|
<up-tag :text="dictLabel(auditOptions, info.reviewStatus)"
|
:type="dictTagType(auditOptions, info.reviewStatus)"
|
size="mini"
|
shape="circle" />
|
</view>
|
<view class="hero-meta">
|
<text class="hero-meta-item">单据编号:{{ info.no || "-" }}</text>
|
<text class="hero-meta-item">项目金额:{{ formatAmount(info.orderAmount) }}</text>
|
</view>
|
</view>
|
|
<view v-if="planNodes.length > 0"
|
class="steps-card">
|
<up-steps :current="activeStep"
|
direction="row"
|
activeColor="#2979ff"
|
inactiveColor="#c0c4cc">
|
<up-steps-item v-for="(node, index) in planNodes"
|
:key="node.id || index"
|
:title="node.name || node.workContent || ''" />
|
</up-steps>
|
</view>
|
|
<view class="tabs-wrap">
|
<up-tabs v-model="activeTab"
|
:list="TABS"
|
:scrollable="true"
|
itemStyle="padding: 0 14px; height: 84rpx;" />
|
</view>
|
|
<!-- 基础资料 -->
|
<view v-if="activeTab === 0">
|
<u-cell-group title="基本信息"
|
class="detail-section">
|
<u-cell title="单据编号"
|
:value="info.no || '-'" />
|
<u-cell title="项目名称"
|
:value="info.title || '-'" />
|
<u-cell title="客户名称"
|
:value="info.clientName || '-'" />
|
<u-cell title="父项目"
|
:value="detail.info.parentProjectName || '-'" />
|
<u-cell title="项目类型"
|
:value="projectTypeLabel" />
|
<u-cell title="立项日期"
|
:value="info.establishTime || '-'" />
|
<u-cell title="项目来源"
|
:value="info.source || '-'" />
|
<u-cell title="立项人"
|
:value="info.managerName || '-'" />
|
<u-cell title="业务员"
|
:value="info.salesmanName || '-'" />
|
<u-cell title="部门"
|
:value="info.departmentName || '-'" />
|
<u-cell title="单据状态"
|
:value="dictLabel(billOptions, info.status) || '-'" />
|
<u-cell title="审核状态"
|
:value="dictLabel(auditOptions, info.reviewStatus) || '-'" />
|
<u-cell title="计划状态"
|
:value="dictLabel(stageOptions, info.stage) || '-'" />
|
<u-cell title="预计工期(天)"
|
:value="estimatedDays === null ? '-' : String(estimatedDays)" />
|
<u-cell title="计划开始日期"
|
:value="info.planStartTime || '-'" />
|
<u-cell title="计划完成日期"
|
:value="info.planEndTime || '-'" />
|
<u-cell title="实际开始日期"
|
:value="info.actualStartTime || '-'" />
|
<u-cell title="实际完成日期"
|
:value="info.actualEndTime || '-'" />
|
<u-cell title="订单日期"
|
:value="info.orderDate || '-'" />
|
<u-cell title="项目金额"
|
:value="formatAmount(info.orderAmount)" />
|
<u-cell title="备注"
|
:value="info.remark || '-'" />
|
</u-cell-group>
|
|
<u-cell-group title="产品信息"
|
class="detail-section">
|
<view v-if="productList.length === 0"
|
class="empty-text">
|
<text>暂无产品信息</text>
|
</view>
|
<view v-for="(row, index) in productList"
|
v-else
|
:key="row.id || index"
|
class="sub-card">
|
<view class="sub-card-header">
|
<text class="sub-card-title">{{ row.productCategory || "产品" }} {{ index + 1 }}</text>
|
<text class="sub-card-amount">{{ formatAmount(row.taxInclusiveTotalPrice) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">规格型号</text>
|
<text class="detail-value">{{ row.specificationModel || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">单位</text>
|
<text class="detail-value">{{ row.unit || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">数量</text>
|
<text class="detail-value">{{ row.quantity ?? "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">税率(%)</text>
|
<text class="detail-value">{{ row.taxRate ?? "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">含税单价</text>
|
<text class="detail-value">{{ formatAmount(row.taxInclusiveUnitPrice) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">含税总价</text>
|
<text class="detail-value">{{ formatAmount(row.taxInclusiveTotalPrice) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">不含税总价</text>
|
<text class="detail-value">{{ formatAmount(row.taxExclusiveTotalPrice) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">发票类型</text>
|
<text class="detail-value">{{ row.invoiceType || "-" }}</text>
|
</view>
|
</view>
|
</u-cell-group>
|
|
<u-cell-group title="项目团队"
|
class="detail-section">
|
<view v-if="teamList.length === 0"
|
class="empty-text">
|
<text>暂无项目成员</text>
|
</view>
|
<view v-for="(row, index) in teamList"
|
v-else
|
:key="row.id || index"
|
class="sub-card">
|
<view class="sub-card-header">
|
<text class="sub-card-title">{{ row.userName || "-" }}</text>
|
<text class="sub-card-amount">{{ row.userRoleName || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">进入日期</text>
|
<text class="detail-value">{{ row.joinTime || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">离开日期</text>
|
<text class="detail-value">{{ row.departTime || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">联系方式</text>
|
<text class="detail-value">{{ row.contact || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">备注</text>
|
<text class="detail-value">{{ row.remark || "-" }}</text>
|
</view>
|
</view>
|
</u-cell-group>
|
|
<u-cell-group title="收货地址"
|
class="detail-section">
|
<u-cell title="收货人"
|
:value="detail.shippingAddress.consignee || '-'" />
|
<u-cell title="联系方式"
|
:value="detail.shippingAddress.contract || '-'" />
|
<u-cell title="地址"
|
:value="detail.shippingAddress.address || '-'" />
|
</u-cell-group>
|
|
<u-cell-group title="联系信息"
|
class="detail-section">
|
<u-cell title="联系人"
|
:value="detail.contractInfo.name || '-'" />
|
<u-cell title="性别"
|
:value="detail.contractInfo.sex || '-'" />
|
<u-cell title="生日"
|
:value="detail.contractInfo.birthday || '-'" />
|
<u-cell title="部门"
|
:value="detail.contractInfo.department || '-'" />
|
<u-cell title="职务"
|
:value="detail.contractInfo.job || '-'" />
|
<u-cell title="手机号"
|
:value="detail.contractInfo.phoneNumber || '-'" />
|
<u-cell title="邮箱"
|
:value="detail.contractInfo.email || '-'" />
|
<u-cell title="QQ"
|
:value="detail.contractInfo.qq || '-'" />
|
<u-cell title="微信号"
|
:value="detail.contractInfo.wx || '-'" />
|
<u-cell title="企业微信"
|
:value="detail.contractInfo.workWechat || '-'" />
|
<u-cell title="地址"
|
:value="detail.contractInfo.address || '-'" />
|
<u-cell title="备注"
|
:value="detail.contractInfo.remark || '-'" />
|
</u-cell-group>
|
|
<u-cell-group title="附件"
|
class="detail-section">
|
<view v-if="attachments.length === 0"
|
class="empty-text">
|
<text>暂无附件</text>
|
</view>
|
<view v-for="(file, index) in attachments"
|
v-else
|
:key="file.id || index"
|
class="file-item"
|
@click="previewFile(file)">
|
<up-icon name="attach"
|
size="16"
|
color="#2979ff"></up-icon>
|
<text class="file-name">{{ file.name || `附件${index + 1}` }}</text>
|
<up-icon name="arrow-right"
|
size="14"
|
color="#c0c4cc"></up-icon>
|
</view>
|
</u-cell-group>
|
</view>
|
|
<!-- 项目阶段 -->
|
<view v-if="activeTab === 1">
|
<u-cell-group title="计划节点(项目类型模板)"
|
class="detail-section">
|
<view v-if="planNodes.length === 0"
|
class="empty-text">
|
<text>该项目类型未配置计划节点</text>
|
</view>
|
<view v-for="(node, index) in planNodes"
|
v-else
|
:key="node.id || index"
|
class="sub-card">
|
<view class="sub-card-header">
|
<text class="sub-card-title">{{ node.name || node.workContent || "-" }}</text>
|
<text class="sub-card-amount">工期 {{ node.estimatedDuration ?? "-" }} 天</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">负责人</text>
|
<text class="detail-value">{{ node.leaderName || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">作业内容</text>
|
<text class="detail-value">{{ node.workContent || "-" }}</text>
|
</view>
|
</view>
|
</u-cell-group>
|
|
<u-cell-group title="阶段汇报记录"
|
class="detail-section">
|
<view v-if="stageList.length === 0"
|
class="empty-text">
|
<text>暂无阶段汇报</text>
|
</view>
|
<view v-for="(row, index) in stageList"
|
v-else
|
:key="row.id || index"
|
class="sub-card">
|
<view class="sub-card-header">
|
<text class="sub-card-title">{{ row.stageName || "-" }}</text>
|
<text class="sub-card-amount">{{ row.progress ?? 0 }}%</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">描述</text>
|
<text class="detail-value">{{ row.description || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">实际负责人</text>
|
<text class="detail-value">{{ row.actualLeaderName || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">计划起止</text>
|
<text class="detail-value">{{ row.planStartTime || "-" }} ~ {{ row.planEndTime || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">实际起止</text>
|
<text class="detail-value">{{ row.actualStartTime || "-" }} ~ {{ row.actualEndTime || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">预计工期</text>
|
<text class="detail-value">{{ row.estimatedDuration ?? "-" }} 天</text>
|
</view>
|
</view>
|
</u-cell-group>
|
</view>
|
|
<!-- 诉讼信息 -->
|
<view v-if="activeTab === 2">
|
<view v-if="lawsuitList.length === 0"
|
class="no-data">
|
<text>暂无诉讼信息</text>
|
</view>
|
<view v-else
|
class="list-wrap">
|
<view v-for="row in lawsuitList"
|
:key="row.id"
|
class="list-card">
|
<view class="list-card-header">
|
<text class="list-card-title">{{ row.caseNo || "-" }}</text>
|
<up-tag :text="row.lawsuitStatus || '-'"
|
:type="lawsuitTagType(row.lawsuitStatus)"
|
size="mini"
|
shape="circle" />
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">合同编号</text>
|
<text class="detail-value">{{ row.contractNo || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">立案时间</text>
|
<text class="detail-value">{{ formatDate(row.filingDate) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">开庭时间</text>
|
<text class="detail-value">{{ formatDate(row.hearingDate) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">标的金额</text>
|
<text class="detail-value">{{ formatAmount(row.claimAmount) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">承办律师</text>
|
<text class="detail-value">{{ row.lawyer || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">判决摘要</text>
|
<text class="detail-value">{{ row.judgmentSummary || "-" }}</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<!-- 中标信息 -->
|
<view v-if="activeTab === 3">
|
<view v-if="biddingList.length === 0"
|
class="no-data">
|
<text>暂无招投标信息</text>
|
</view>
|
<view v-else
|
class="list-wrap">
|
<view v-for="row in biddingList"
|
:key="row.id"
|
class="list-card">
|
<view class="list-card-header">
|
<text class="list-card-title">{{ row.bidDocName || "-" }}</text>
|
<up-tag :text="row.bidResult || '-'"
|
:type="biddingTagType(row.bidResult)"
|
size="mini"
|
shape="circle" />
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">招标单位</text>
|
<text class="detail-value">{{ row.tenderUnit || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">投标单位</text>
|
<text class="detail-value">{{ row.bidUnit || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">招标日期</text>
|
<text class="detail-value">{{ formatDate(row.tenderDate) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">投标日期</text>
|
<text class="detail-value">{{ formatDate(row.bidDate) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">标书款</text>
|
<text class="detail-value">{{ formatAmount(row.bidDocFee) }}({{ row.bidDocFeeStatus || "-" }})</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">投标保证金</text>
|
<text class="detail-value">{{ formatAmount(row.bidDeposit) }}</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<!-- 销售合同 -->
|
<view v-if="activeTab === 4">
|
<view v-if="contractList.length === 0"
|
class="no-data">
|
<text>暂无销售合同</text>
|
</view>
|
<view v-else
|
class="list-wrap">
|
<view v-for="row in contractList"
|
:key="row.id"
|
class="list-card">
|
<view class="list-card-header">
|
<text class="list-card-title">{{ row.salesContractNo || "-" }}</text>
|
<text class="list-card-meta">{{ formatDate(row.executionDate) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">客户名称</text>
|
<text class="detail-value">{{ row.customerName || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">业务员</text>
|
<text class="detail-value">{{ row.salesman || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">合同金额</text>
|
<text class="detail-value">{{ formatAmount(row.contractAmount) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">实际合同金额</text>
|
<text class="detail-value">{{ formatAmount(row.netContractAmount) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">交付日期</text>
|
<text class="detail-value">{{ formatDate(row.deliveryDate) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">录入人</text>
|
<text class="detail-value">{{ row.entryPersonName || "-" }}</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<!-- 全流程时间轴(前端聚合,非后端接口) -->
|
<view v-if="activeTab === 5">
|
<view v-if="timeline.length === 0"
|
class="no-data">
|
<text>暂无流程记录</text>
|
</view>
|
<view v-else
|
class="timeline-wrap">
|
<up-steps :current="-1"
|
direction="column"
|
activeColor="#2979ff"
|
inactiveColor="#2979ff">
|
<up-steps-item v-for="(item, index) in timeline"
|
:key="index"
|
:title="item.title"
|
:desc="item.desc" />
|
</up-steps>
|
</view>
|
</view>
|
</view>
|
|
<view v-else
|
class="loading-tip">
|
<text>加载中...</text>
|
</view>
|
|
<view v-if="!loading"
|
class="footer-btns">
|
<up-button class="footer-btn"
|
text="返回"
|
@click="goBack" />
|
<up-button class="footer-btn"
|
type="primary"
|
text="进度汇报"
|
@click="goProgress" />
|
</view>
|
</view>
|
</template>
|
|
<script setup>
|
import { computed, ref } from "vue";
|
import { onLoad, onShow } from "@dcloudio/uni-app";
|
import PageHeader from "@/components/PageHeader.vue";
|
import { useDict } from "@/utils/dict";
|
import { unwrapList } from "@/utils/list";
|
import { openFile } from "@/utils/file";
|
import { parseDate } from "@/utils/warning";
|
import { getProject } from "@/api/projectManagement/project";
|
import { listPlan } from "@/api/projectManagement/projectType";
|
import { lawsuitListPage } from "@/api/salesManagement/lawsuitManagement";
|
import { biddingListPage } from "@/api/salesManagement/biddingManagement";
|
import { ledgerListPage } from "@/api/salesManagement/salesLedger";
|
import { toDetail } from "./_utils/mapper";
|
import { dictLabel, dictTagType, toPickerOptions } from "./_utils/dict";
|
import {
|
fetchPlanNodeList,
|
fetchProjectStages,
|
stageNameFromNodes,
|
} from "./_utils/stage";
|
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
const TABS = [
|
{ name: "基础资料" },
|
{ name: "项目阶段" },
|
{ name: "诉讼信息" },
|
{ name: "中标信息" },
|
{ name: "销售合同" },
|
{ name: "全流程时间轴" },
|
];
|
|
const LAWSUIT_TAG_TYPE = {
|
立案中: "warning",
|
审理中: "primary",
|
已判决: "success",
|
已撤诉: "info",
|
已和解: "success",
|
};
|
|
const BIDDING_TAG_TYPE = {
|
废标: "info",
|
流标: "info",
|
未中标: "warning",
|
中标待出通知书: "primary",
|
已出中标通知书: "success",
|
已签合同: "success",
|
};
|
|
const { bill_status, project_management, plan_status } = useDict(
|
"bill_status",
|
"project_management",
|
"plan_status"
|
);
|
|
const billOptions = computed(() => toPickerOptions(bill_status.value));
|
const auditOptions = computed(() => toPickerOptions(project_management.value));
|
const stageOptions = computed(() => toPickerOptions(plan_status.value));
|
|
const loading = ref(true);
|
const projectId = ref("");
|
const activeTab = ref(0);
|
const detail = ref({});
|
const planNodes = ref([]);
|
const stageList = ref([]);
|
const projectTypeName = ref("");
|
const lawsuitList = ref([]);
|
const biddingList = ref([]);
|
const contractList = ref([]);
|
|
const info = computed(() => detail.value.info || {});
|
const productList = computed(() => detail.value.productList || []);
|
const teamList = computed(() => detail.value.teamList || []);
|
const attachments = computed(() => detail.value.attachments || []);
|
|
const projectTypeLabel = computed(
|
() => projectTypeName.value || String(info.value.projectManagementPlanId ?? "-")
|
);
|
|
const estimatedDays = computed(() => {
|
const raw = Number(info.value.estimatedDays);
|
if (Number.isFinite(raw) && raw > 0) return raw;
|
const start = parseDate(info.value.planStartTime);
|
const end = parseDate(info.value.planEndTime);
|
if (!start || !end || end < start) return null;
|
return Math.floor((end.getTime() - start.getTime()) / MS_PER_DAY) + 1;
|
});
|
|
// 计划节点已按 sort 排序,计划状态按序号或节点 id 命中
|
const activeStep = computed(() => {
|
const nodes = planNodes.value;
|
const stage = info.value.stage;
|
if (!nodes.length) return 0;
|
const byId = nodes.findIndex(n => String(n.id) === String(stage));
|
if (byId >= 0) return byId;
|
const byLabel = nodes.findIndex(
|
n => String(n.name || n.workContent) === String(stage)
|
);
|
if (byLabel >= 0) return byLabel;
|
const index = Number(stage);
|
if (Number.isFinite(index) && index > 0) return Math.min(index - 1, nodes.length - 1);
|
return 0;
|
});
|
|
// 全流程时间轴:阶段 + 诉讼 + 招投标 按日期倒序聚合(前端拼接,后端无 timeline 接口)
|
const timeline = computed(() => {
|
const events = [];
|
stageList.value.forEach(row => {
|
events.push({
|
date: row.planStartTime || row.actualStartTime || "",
|
title: `【项目阶段】${row.stageName || "阶段汇报"}`,
|
desc: row.description || "",
|
});
|
});
|
lawsuitList.value.forEach(row => {
|
events.push({
|
date: row.filingDate || "",
|
title: `【诉讼】${row.caseNo || ""} ${row.lawsuitStatus || ""}`.trim(),
|
desc: row.judgmentSummary || row.progressRemark || "",
|
});
|
});
|
biddingList.value.forEach(row => {
|
events.push({
|
date: row.bidDate || "",
|
title: `【招投标】${row.bidDocName || ""} ${row.bidResult || ""}`.trim(),
|
desc: row.tenderUnit ? `招标单位:${row.tenderUnit}` : "",
|
});
|
});
|
return events
|
.filter(item => item.title)
|
.sort((a, b) => String(b.date).localeCompare(String(a.date)));
|
});
|
|
const goBack = () => uni.navigateBack();
|
|
const goEdit = () => {
|
uni.navigateTo({
|
url: `/pages/projectManagement/project/edit?id=${projectId.value}`,
|
});
|
};
|
|
const goProgress = () => {
|
uni.navigateTo({
|
url: `/pages/projectManagement/project/progress?id=${projectId.value}`,
|
});
|
};
|
|
const formatAmount = amount =>
|
amount === null || amount === undefined || amount === ""
|
? "-"
|
: `¥${Number(amount).toFixed(2)}`;
|
|
const formatDate = value => (value ? String(value).substring(0, 10) : "-");
|
|
const lawsuitTagType = status => LAWSUIT_TAG_TYPE[status] || "info";
|
const biddingTagType = status => BIDDING_TAG_TYPE[status] || "info";
|
|
const previewFile = file => openFile(file?.url || file?.previewURL);
|
|
const loadProjectTypeName = async () => {
|
const planId = info.value.projectManagementPlanId;
|
if (planId === undefined || planId === null || planId === "") return;
|
try {
|
const res = await listPlan({ current: 1, size: 999 });
|
const matched = unwrapList(res).find(
|
item => String(item.id) === String(planId)
|
);
|
projectTypeName.value = matched?.name || "";
|
} catch (error) {
|
projectTypeName.value = "";
|
}
|
};
|
|
// 诉讼 / 招投标没有按项目查的接口,拉全量后按项目 id 或名称匹配(对齐管理端 matchProject)
|
const matchProject = (record, id, title) => {
|
if (!record) return false;
|
if (
|
record.projectId !== undefined &&
|
record.projectId !== null &&
|
record.projectId !== ""
|
) {
|
if (String(record.projectId) === String(id)) return true;
|
}
|
const name = String(record.projectName || "").trim();
|
const target = String(title || "").trim();
|
if (name && target) {
|
return name === target || name.includes(target) || target.includes(name);
|
}
|
return false;
|
};
|
|
const loadRelated = async (id, title) => {
|
if (!id) return;
|
|
try {
|
const res = await lawsuitListPage({ projectName: title, current: -1, size: -1 });
|
let matched = unwrapList(res).filter(r => matchProject(r, id, title));
|
if (matched.length === 0) {
|
const all = await lawsuitListPage({ current: -1, size: -1 });
|
matched = unwrapList(all).filter(r => matchProject(r, id, title));
|
}
|
lawsuitList.value = matched;
|
} catch (error) {
|
lawsuitList.value = [];
|
}
|
|
try {
|
const res = await biddingListPage({ projectName: title, current: -1, size: -1 });
|
let matched = unwrapList(res).filter(r => matchProject(r, id, title));
|
if (matched.length === 0) {
|
const all = await biddingListPage({ current: -1, size: -1 });
|
matched = unwrapList(all).filter(r => matchProject(r, id, title));
|
}
|
biddingList.value = matched;
|
} catch (error) {
|
biddingList.value = [];
|
}
|
|
try {
|
const res = await ledgerListPage({ projectId: id, current: -1, size: -1 });
|
contractList.value = unwrapList(res).sort((a, b) =>
|
String(b.executionDate || "").localeCompare(String(a.executionDate || ""))
|
);
|
} catch (error) {
|
contractList.value = [];
|
}
|
};
|
|
const loadDetail = async () => {
|
loading.value = true;
|
try {
|
const res = await getProject(projectId.value);
|
const raw = res?.data?.data ?? res?.data ?? res;
|
detail.value = toDetail(raw || {});
|
|
const id = info.value.id || projectId.value;
|
const title = info.value.title || "";
|
|
const [nodes, stages] = await Promise.all([
|
fetchPlanNodeList(info.value.projectManagementPlanId),
|
fetchProjectStages(id),
|
]);
|
planNodes.value = nodes;
|
stageList.value = stages.map(row => ({
|
...row,
|
stageName: stageNameFromNodes(nodes, row.projectManagementPlanNodeId),
|
}));
|
|
await Promise.all([loadProjectTypeName(), loadRelated(id, title)]);
|
} catch (error) {
|
uni.showToast({ title: "获取项目详情失败", icon: "none" });
|
} finally {
|
loading.value = false;
|
}
|
};
|
|
onLoad(options => {
|
projectId.value = options?.id || "";
|
if (!projectId.value) loading.value = false;
|
});
|
|
// 从「进度汇报」返回时阶段列表会变,用 onShow 重拉(onShow 在 onLoad 后也会触发)
|
onShow(() => {
|
if (projectId.value) loadDetail();
|
});
|
</script>
|
|
<style scoped lang="scss">
|
@import "@/styles/sales-common.scss";
|
|
.project-detail {
|
min-height: 100vh;
|
background: #f8f9fa;
|
padding-bottom: 90px;
|
}
|
|
.header-action {
|
font-size: 14px;
|
color: #2979ff;
|
}
|
|
.detail-card {
|
padding: 12px;
|
}
|
|
.hero {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 18px;
|
border-radius: 16px;
|
background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%);
|
box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08);
|
}
|
|
.hero-title {
|
font-size: 18px;
|
font-weight: 600;
|
color: #1f2d3d;
|
max-width: 70%;
|
word-break: break-all;
|
}
|
|
.hero-meta {
|
display: flex;
|
flex-direction: column;
|
gap: 4px;
|
padding: 12px 18px 0;
|
}
|
|
.hero-meta-item {
|
font-size: 12px;
|
color: #909399;
|
}
|
|
.steps-card {
|
margin: 12px;
|
padding: 16px 8px;
|
border-radius: 12px;
|
background: #ffffff;
|
box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
|
}
|
|
.tabs-wrap {
|
margin: 12px 12px 0;
|
border-radius: 12px;
|
overflow: hidden;
|
background: #ffffff;
|
box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
|
}
|
|
.detail-section {
|
margin: 12px;
|
border-radius: 12px;
|
overflow: hidden;
|
box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
|
}
|
|
.sub-card {
|
padding: 12px 16px;
|
border-bottom: 1px solid #f2f4f8;
|
}
|
|
.sub-card:last-child {
|
border-bottom: none;
|
}
|
|
.sub-card-header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
margin-bottom: 8px;
|
}
|
|
.sub-card-title {
|
font-size: 14px;
|
font-weight: 600;
|
color: #22324d;
|
}
|
|
.sub-card-amount {
|
font-size: 13px;
|
color: #2979ff;
|
}
|
|
.list-wrap {
|
padding: 12px;
|
display: flex;
|
flex-direction: column;
|
gap: 12px;
|
}
|
|
.list-card {
|
background: #ffffff;
|
border-radius: 12px;
|
padding: 14px 16px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
}
|
|
.list-card-header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
margin-bottom: 8px;
|
}
|
|
.list-card-title {
|
font-size: 14px;
|
font-weight: 600;
|
color: #22324d;
|
max-width: 70%;
|
word-break: break-all;
|
}
|
|
.list-card-meta {
|
font-size: 12px;
|
color: #909399;
|
}
|
|
.timeline-wrap {
|
margin: 12px;
|
padding: 20px 16px;
|
border-radius: 12px;
|
background: #ffffff;
|
box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
|
}
|
|
.file-item {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
padding: 12px 16px;
|
border-bottom: 1px solid #f2f4f8;
|
}
|
|
.file-item:last-child {
|
border-bottom: none;
|
}
|
|
.file-name {
|
flex: 1;
|
font-size: 14px;
|
color: #333;
|
word-break: break-all;
|
}
|
|
.empty-text {
|
padding: 16px;
|
font-size: 14px;
|
color: #909399;
|
}
|
|
.loading-tip {
|
padding: 80px 0;
|
text-align: center;
|
font-size: 14px;
|
color: #909399;
|
}
|
|
.footer-btns {
|
position: fixed;
|
left: 0;
|
right: 0;
|
bottom: 0;
|
display: flex;
|
gap: 12px;
|
padding: 12px 16px;
|
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
background: #ffffff;
|
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
|
z-index: 1000;
|
}
|
|
.footer-btn {
|
flex: 1;
|
}
|
|
: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>
|