| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-card class="header-card" shadow="never"> |
| | | <div class="header-row"> |
| | | <div class="header-title">项ç®è¯¦æ
</div> |
| | | <div class="header-actions"> |
| | | <el-button style="color: white;background: #002FA7;" @click="openProgressReport">è¿åº¦æ±æ¥</el-button> |
| | | <!-- <el-button type="danger" @click="openDiscussProgress">æ´½è°è¿åº¦</el-button> --> |
| | | <el-button @click="goBack">è¿å</el-button> |
| | | </div> |
| | | </div> |
| | | <el-steps v-if="steps.length > 0" :active="activeStep" align-center finish-status="success"> |
| | | <el-step v-for="(s, idx) in steps" :key="idx" :title="s" /> |
| | | </el-steps> |
| | | </el-card> |
| | | |
| | | <el-card class="content-card" shadow="never" v-loading="loading"> |
| | | <el-tabs v-model="activeTab"> |
| | | <el-tab-pane label="åºç¡èµæ" name="base"> |
| | | <el-descriptions :column="4" border> |
| | | <el-descriptions-item label="项ç®ID">{{ info.id ?? '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="åæ®ç¼å·">{{ info.no || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®åç§°">{{ info.title || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="客æ·åç§°">{{ info.clientName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="客æ·ID">{{ info.clientId ?? '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç¶é¡¹ç®">{{ parentProjectLabel }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®ç±»å">{{ projectTypeLabel }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç«é¡¹æ¥æ">{{ info.establishTime || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="é¡¹ç®æ¥æº">{{ info.source || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç«é¡¹äºº">{{ info.managerName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¸å¡å">{{ info.salesmanName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="é¨é¨">{{ info.departmentName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="åæ®ç¶æ"> |
| | | <dict-tag :options="bill_status" :value="String(info.status ?? '')" /> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å®¡æ ¸ç¶æ"> |
| | | <dict-tag :options="project_management" :value="String(info.reviewStatus ?? '')" /> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="计åç¶æ"> |
| | | <dict-tag :options="plan_status" :value="String(info.stage ?? '')" /> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="é¢è®¡å·¥æ(天)">{{ estimatedDays }}</el-descriptions-item> |
| | | <el-descriptions-item label="计åå¼å§æ¥æ">{{ info.planStartTime || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="计åå®ææ¥æ">{{ info.planEndTime || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®é
å¼å§æ¥æ">{{ info.actualStartTime || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®é
å®ææ¥æ">{{ info.actualEndTime || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="è®¢åæ¥æ">{{ info.orderDate || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®éé¢">{{ info.orderAmount ?? '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="夿³¨" :span="4">{{ info.remark || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <div class="attachment-block" v-if="attachments.length > 0"> |
| | | <div class="attachment-title">éä»¶</div> |
| | | <div class="attachment-list"> |
| | | <div v-for="(att, idx) in attachments" :key="att.id || att.url || idx" class="attachment-item"> |
| | | <el-icon><Document /></el-icon> |
| | | <span class="attachment-name">{{ att.name || att.fileName || att.url || 'éä»¶' }}</span> |
| | | <el-button link type="primary" size="small" @click="downloadAttachment(att)">ä¸è½½</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <el-divider content-position="left">产åä¿¡æ¯</el-divider> |
| | | <el-table :data="productRows" border show-summary :summary-method="summarizeProductTable"> |
| | | <el-table-column align="center" label="åºå·" type="index" width="60" /> |
| | | <el-table-column label="产å大类" prop="productCategory" show-overflow-tooltip /> |
| | | <el-table-column label="è§æ ¼åå·" prop="specificationModel" show-overflow-tooltip /> |
| | | <el-table-column label="åä½" prop="unit" width="90" /> |
| | | <el-table-column label="æ°é" prop="quantity" width="90" /> |
| | | <el-table-column label="ç¨ç(%)" prop="taxRate" width="90" /> |
| | | <el-table-column label="å«ç¨åä»·(å
)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" /> |
| | | <el-table-column label="å«ç¨æ»ä»·(å
)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" /> |
| | | <el-table-column label="ä¸å«ç¨æ»ä»·(å
)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" /> |
| | | <el-table-column label="å票类å" prop="invoiceType" width="110" /> |
| | | </el-table> |
| | | |
| | | <el-divider content-position="left">项ç®å¢é</el-divider> |
| | | <el-table :data="teamRows" border> |
| | | <el-table-column align="center" label="åºå·" type="index" width="60" /> |
| | | <el-table-column label="å§å" prop="userName" show-overflow-tooltip /> |
| | | <el-table-column label="项ç®ç»è§è²" prop="userRoleName" show-overflow-tooltip /> |
| | | <el-table-column label="è¿å
¥æ¥æ" prop="joinTime" width="140" /> |
| | | <el-table-column label="ç¦»å¼æ¥æ" prop="departTime" width="140" /> |
| | | <el-table-column label="èç³»æ¹å¼" prop="contact" show-overflow-tooltip /> |
| | | <el-table-column label="夿³¨" prop="remark" show-overflow-tooltip /> |
| | | </el-table> |
| | | |
| | | <el-divider content-position="left">æ¶è´§å°å</el-divider> |
| | | <el-descriptions :column="3" border> |
| | | <el-descriptions-item label="æ¶è´§äºº">{{ shippingAddress.consignee || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="èç³»æ¹å¼">{{ shippingAddress.contract || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å°å">{{ shippingAddress.address || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <el-divider content-position="left">è系信æ¯</el-divider> |
| | | <el-descriptions :column="4" border> |
| | | <el-descriptions-item label="è系人">{{ contractInfo.name || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ§å«">{{ contractInfo.sex || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="çæ¥">{{ contractInfo.birthday || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="é¨é¨">{{ contractInfo.department || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="èå¡">{{ contractInfo.job || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="ææºå·">{{ contractInfo.phoneNumber || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="é®ç®±">{{ contractInfo.email || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="QQ">{{ contractInfo.qq || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="åºå®çµè¯">{{ contractInfo.lineaFissa || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="微信">{{ contractInfo.wx || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç±è´¯">{{ contractInfo.origineEtnica || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ³äººä»£è¡¨">{{ contractInfo.rappresentanteLegale || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-tab-pane> |
| | | |
| | | <el-tab-pane label="项ç®ç±»å" name="plan"> |
| | | <el-descriptions :column="4" border> |
| | | <el-descriptions-item label="ç±»ååç§°">{{ projectPlan.name || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="夿³¨" :span="3">{{ projectPlan.description || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <div class="attachment-block" v-if="planAttachments.length > 0"> |
| | | <div class="attachment-title">ç±»åéä»¶</div> |
| | | <div class="attachment-list"> |
| | | <div v-for="(att, idx) in planAttachments" :key="att.id || att.url || idx" class="attachment-item"> |
| | | <el-icon><Document /></el-icon> |
| | | <span class="attachment-name">{{ att.name || att.fileName || att.url || 'éä»¶' }}</span> |
| | | <el-button link type="primary" size="small" @click="downloadAttachment(att)">ä¸è½½</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <el-table :data="planNodeRows" border style="margin-top: 14px;"> |
| | | <el-table-column align="center" label="æ¥éª¤" type="index" width="80" /> |
| | | <el-table-column label="é¶æ®µåç§°" prop="name" min-width="160" show-overflow-tooltip /> |
| | | <el-table-column label="è´è´£äºº" prop="leaderName" width="140" show-overflow-tooltip /> |
| | | <el-table-column label="é¢è®¡å·¥æ(天)" prop="estimatedDuration" width="140" /> |
| | | <el-table-column label="å·¥æ¶åä»·" prop="hourlyRate" width="120" /> |
| | | <el-table-column label="ä½ä¸å
容" prop="workContent" min-width="180" show-overflow-tooltip /> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | |
| | | <el-tab-pane label="项ç®é¶æ®µ" name="stage"> |
| | | <el-table :data="stageNodeRows" border style="margin-top: 14px;"> |
| | | <el-table-column align="center" label="åºå·" type="index" width="60" /> |
| | | <el-table-column label="é¶æ®µ" prop="stageName" min-width="160" show-overflow-tooltip /> |
| | | <el-table-column label="æè¿°" prop="description" min-width="220" show-overflow-tooltip /> |
| | | <el-table-column label="å®é
è´è´£äºº" prop="actualLeaderName" width="140" show-overflow-tooltip /> |
| | | <el-table-column label="è¿åº¦(%)" prop="progress" width="110" /> |
| | | <el-table-column label="计åå¼å§" prop="planStartTime" width="120" /> |
| | | <el-table-column label="计åç»æ" prop="planEndTime" width="120" /> |
| | | <el-table-column label="å®é
å¼å§" prop="actualStartTime" width="120" /> |
| | | <el-table-column label="å®é
ç»æ" prop="actualEndTime" width="120" /> |
| | | <el-table-column label="é¢è®¡å·¥æ(天)" prop="estimatedDuration" width="130" /> |
| | | <el-table-column label="éä»¶" width="90" align="center"> |
| | | <template #default="{ row }"> |
| | | <span>{{ row.attachmentCount }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column v-if="false" label="æä½" width="100" align="center" fixed="right" > |
| | | <template #default="{ row }"> |
| | | <el-button link type="danger" size="small" @click="handleDeleteStage(row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | </el-card> |
| | | </div> |
| | | |
| | | <ProgressReportDialog |
| | | v-model="progressReportVisible" |
| | | :project-id="info.id" |
| | | :project-info="info" |
| | | :plan-nodes="planNodeRows" |
| | | :default-plan-node-id="defaultPlanNodeId" |
| | | @submitted="handleProgressSubmitted" |
| | | /> |
| | | <DiscussProgressDialog |
| | | v-model="discussProgressVisible" |
| | | :project-id="info.id" |
| | | :plan-nodes="planNodeRows" |
| | | :default-plan-node-id="defaultPlanNodeId" |
| | | @submitted="handleDiscussSubmitted" |
| | | /> |
| | | </template> |
| | | |
| | | <script setup name="ProjectManagementDetail"> |
| | | import { computed, getCurrentInstance, onMounted, ref } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { Document } from '@element-plus/icons-vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { getProject, saveStage, listStage, deleteStage } from '@/api/projectManagement/project' |
| | | import { listPlan } from '@/api/projectManagement/projectType' |
| | | import ProgressReportDialog from '@/components/ProjectManagement/ProgressReportDialog.vue' |
| | | import DiscussProgressDialog from '@/components/ProjectManagement/DiscussProgressDialog.vue' |
| | | import useUserStore from '@/store/modules/user' |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status') |
| | | |
| | | const loading = ref(false) |
| | | const activeTab = ref('base') |
| | | const progressReportVisible = ref(false) |
| | | const discussProgressVisible = ref(false) |
| | | const userStore = useUserStore() |
| | | |
| | | const info = ref({}) |
| | | const shippingAddress = ref({}) |
| | | const contractInfo = ref({}) |
| | | const productRows = ref([]) |
| | | const teamRows = ref([]) |
| | | const attachments = ref([]) |
| | | const projectTypeMap = ref(new Map()) |
| | | const projectPlan = ref({}) |
| | | const planNodeRows = ref([]) |
| | | const planAttachments = ref([]) |
| | | const stageNodeRows = ref([]) |
| | | |
| | | const estimatedDays = computed(() => { |
| | | const raw = info.value?.estimatedDays |
| | | const n = Number(raw) |
| | | if (Number.isFinite(n) && n > 0) return n |
| | | const start = info.value?.planStartTime |
| | | const end = info.value?.planEndTime |
| | | if (!start || !end) return 0 |
| | | const startTime = new Date(`${start}T00:00:00`).getTime() |
| | | const endTime = new Date(`${end}T00:00:00`).getTime() |
| | | if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || endTime < startTime) return 0 |
| | | return Math.floor((endTime - startTime) / (24 * 60 * 60 * 1000)) + 1 |
| | | }) |
| | | |
| | | const projectTypeLabel = computed(() => { |
| | | const id = info.value?.projectManagementPlanId |
| | | if (id === undefined || id === null || id === '') return '-' |
| | | const p = projectTypeMap.value.get(Number(id)) |
| | | return p?.name || String(id) |
| | | }) |
| | | |
| | | const parentProjectLabel = computed(() => { |
| | | return ( |
| | | info.value?.parentTitle || |
| | | info.value?.projectManagementInfoParentName || |
| | | info.value?.projectManagementInfoParentId || |
| | | '-' |
| | | ) |
| | | }) |
| | | |
| | | const planStageEnum = computed(() => { |
| | | const list = Array.isArray(plan_status) ? plan_status : [] |
| | | return list.map(i => ({ value: String(i.value), label: i.label })) |
| | | }) |
| | | |
| | | const steps = computed(() => { |
| | | const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : [] |
| | | const sorted = [...nodes].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0)) |
| | | const labels = sorted.map(i => i.name || i.workContent).filter(Boolean) |
| | | return labels.length > 0 ? labels : planStageEnum.value.map(i => i.label) |
| | | }) |
| | | |
| | | const activeStep = computed(() => { |
| | | const statusOrStage = info.value?.stage ?? info.value?.status |
| | | const enumList = planStageEnum.value |
| | | // ä¼å
ä½¿ç¨ planStageEnum ç value å¹é
|
| | | const found = enumList.find(i => i.value === String(statusOrStage)) |
| | | const label = found?.label |
| | | // å¨é¡¹ç®ç±»åèç¹ä¸æ¥æ¾å¯¹åº label ç䏿 |
| | | const nodeLabels = steps.value |
| | | const idxByLabel = label ? nodeLabels.findIndex(l => String(l) === String(label)) : -1 |
| | | if (idxByLabel >= 0) return idxByLabel + 1 |
| | | // åéï¼å¦æ statusOrStage æ¯æ°åç´¢å¼ |
| | | const n = Number(statusOrStage) |
| | | if (Number.isFinite(n) && n > 0) return Math.min(n, nodeLabels.length) |
| | | return 0 |
| | | }) |
| | | |
| | | function goBack() { |
| | | router.back() |
| | | } |
| | | |
| | | function openProgressReport() { |
| | | progressReportVisible.value = true |
| | | } |
| | | |
| | | function openDiscussProgress() { |
| | | discussProgressVisible.value = true |
| | | } |
| | | |
| | | const defaultPlanNodeId = computed(() => { |
| | | const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : [] |
| | | if (nodes.length === 0) return undefined |
| | | const stageVal = info.value?.stage |
| | | const direct = nodes.find(n => String(n.id) === String(stageVal)) |
| | | if (direct?.id) return direct.id |
| | | const sorted = [...nodes].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0)) |
| | | const idx = Number(stageVal) |
| | | if (Number.isFinite(idx)) { |
| | | const byIndex = sorted[idx - 1] || sorted[idx] || sorted[0] |
| | | if (byIndex?.id) return byIndex.id |
| | | } |
| | | return sorted[0]?.id |
| | | }) |
| | | |
| | | async function handleProgressSubmitted(payload) { |
| | | try { |
| | | const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : [] |
| | | const node = nodes.find(n => String(n.id) === String(payload.planNodeId)) || {} |
| | | const description = payload.remark |
| | | ? `${payload.reportDate || ''} ${payload.remark}`.trim() |
| | | : `${payload.reportDate || ''} è¿åº¦æ±æ¥`.trim() |
| | | const req = { |
| | | id: null, |
| | | projectManagementPlanNodeId: payload.planNodeId, |
| | | projectManagementInfoId: info.value?.id, |
| | | description, |
| | | actualLeaderId: userStore.id || info.value?.managerId, |
| | | actualLeaderName: userStore.nickName || info.value?.managerName || info.value?.managerName, |
| | | estimatedDuration: Number(node.estimatedDuration ?? 0) || 0, |
| | | planStartTime: payload.planStartTime || info.value?.planStartTime, |
| | | planEndTime: payload.planEndTime || info.value?.planEndTime, |
| | | actualStartTime: payload.actualStartTime || null, |
| | | actualEndTime: payload.actualEndTime || null, |
| | | progress: Number(payload.totalProgress ?? payload.completionProgress ?? 0) || 0, |
| | | storageBlobDTOs: Array.isArray(payload.storageBlobDTOs) ? payload.storageBlobDTOs : [] |
| | | } |
| | | const res = await saveStage(req) |
| | | if (res?.code === 200) { |
| | | ElMessage.success('æäº¤æå') |
| | | await Promise.all([loadDetail(), loadStageList()]) |
| | | return |
| | | } |
| | | ElMessage.error(res?.msg || 'æäº¤å¤±è´¥') |
| | | } catch (e) { |
| | | ElMessage.error('æäº¤å¤±è´¥') |
| | | } |
| | | } |
| | | |
| | | async function handleDiscussSubmitted(payload) { |
| | | try { |
| | | const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : [] |
| | | const node = nodes.find(n => String(n.id) === String(payload.planNodeId)) || {} |
| | | const req = { |
| | | id: null, |
| | | projectManagementPlanNodeId: payload.planNodeId, |
| | | projectManagementInfoId: info.value?.id, |
| | | description: payload.remark, |
| | | actualLeaderId: userStore.id || info.value?.managerId, |
| | | actualLeaderName: userStore.nickName || info.value?.managerName || info.value?.managerName, |
| | | estimatedDuration: Number(node.estimatedDuration ?? 0) || 0, |
| | | planStartTime: info.value?.planStartTime, |
| | | planEndTime: info.value?.planEndTime, |
| | | actualStartTime: info.value?.actualStartTime || null, |
| | | actualEndTime: info.value?.actualEndTime || null, |
| | | progress: Number(info.value?.progress ?? 0) || 0, |
| | | attachmentIds: Array.isArray(payload.attachmentIds) ? payload.attachmentIds : [] |
| | | } |
| | | const res = await saveStage(req) |
| | | if (res?.code === 200) { |
| | | ElMessage.success('æäº¤æå') |
| | | await Promise.all([loadDetail(), loadStageList()]) |
| | | return |
| | | } |
| | | ElMessage.error(res?.msg || 'æäº¤å¤±è´¥') |
| | | } catch (e) { |
| | | ElMessage.error('æäº¤å¤±è´¥') |
| | | } |
| | | } |
| | | |
| | | function downloadAttachment(row) { |
| | | if (row?.url) { |
| | | try { |
| | | proxy.$download.byUrl(row.url, row.originalFilename); |
| | | return |
| | | } catch (e) {} |
| | | } |
| | | ElMessage.warning('éä»¶ææ ä¸è½½å°å') |
| | | } |
| | | |
| | | async function loadProjectTypeMap() { |
| | | try { |
| | | const res = await listPlan({ current: 1, size: 999 }) |
| | | const records = res?.data?.records || res?.records || res?.rows || [] |
| | | projectTypeMap.value = new Map((records || []).map(r => [Number(r.id), r])) |
| | | } catch { |
| | | projectTypeMap.value = new Map() |
| | | } |
| | | } |
| | | |
| | | function getPlanNodeName(planNodeId) { |
| | | const list = Array.isArray(planNodeRows.value) ? planNodeRows.value : [] |
| | | const node = list.find(n => String(n.id) === String(planNodeId)) |
| | | return node?.name || node?.workContent || String(planNodeId ?? '') |
| | | } |
| | | |
| | | async function loadStageList() { |
| | | const projectId = info.value?.id |
| | | if (!projectId) { |
| | | stageNodeRows.value = [] |
| | | return |
| | | } |
| | | try { |
| | | const res = await listStage(projectId) |
| | | const data = res?.data?.data ?? res?.data ?? res |
| | | const list = data?.records || data?.rows || data?.list || data || [] |
| | | const records = Array.isArray(list) ? list : [] |
| | | stageNodeRows.value = records.map(r => { |
| | | const attachmentList = Array.isArray(r.attachmentList) ? r.attachmentList : [] |
| | | const attachmentIds = Array.isArray(r.attachmentIds) ? r.attachmentIds : [] |
| | | return { |
| | | ...r, |
| | | stageName: getPlanNodeName(r.projectManagementPlanNodeId), |
| | | attachmentCount: attachmentList.length || attachmentIds.length || 0 |
| | | } |
| | | }) |
| | | } catch { |
| | | stageNodeRows.value = [] |
| | | } |
| | | } |
| | | |
| | | async function handleDeleteStage(row) { |
| | | const stageId = row?.id |
| | | if (!stageId) return |
| | | try { |
| | | await ElMessageBox.confirm('æ¯å¦ç¡®è®¤å é¤è¯¥é¡¹ç®é¶æ®µï¼', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }) |
| | | const res = await deleteStage(stageId) |
| | | if (res?.code === 200) { |
| | | ElMessage.success('å 餿å') |
| | | await loadStageList() |
| | | return |
| | | } |
| | | ElMessage.error(res?.msg || 'å é¤å¤±è´¥') |
| | | } catch {} |
| | | } |
| | | |
| | | function syncProjectPlan() { |
| | | const id = info.value?.projectManagementPlanId |
| | | if (id === undefined || id === null || id === '') { |
| | | projectPlan.value = {} |
| | | planNodeRows.value = [] |
| | | planAttachments.value = [] |
| | | return |
| | | } |
| | | const plan = projectTypeMap.value.get(Number(id)) || {} |
| | | projectPlan.value = plan || {} |
| | | planNodeRows.value = Array.isArray(plan?.planNodeList) ? plan.planNodeList : [] |
| | | planAttachments.value = Array.isArray(plan?.attachmentList) ? plan.attachmentList : [] |
| | | } |
| | | |
| | | async function loadDetail() { |
| | | const id = route.params?.id |
| | | if (!id) return |
| | | loading.value = true |
| | | try { |
| | | const res = await getProject(id) |
| | | const detail = res?.data?.data ?? res?.data ?? res |
| | | info.value = detail?.info || {} |
| | | shippingAddress.value = detail?.shippingAddress || {} |
| | | contractInfo.value = detail?.contractInfo || {} |
| | | productRows.value = Array.isArray(detail?.salesLedgerProductList) ? detail.salesLedgerProductList : [] |
| | | teamRows.value = Array.isArray(detail?.info?.teamList) ? detail.info.teamList : [] |
| | | attachments.value = Array.isArray(detail?.info?.attachmentList) ? detail.info.attachmentList : [] |
| | | syncProjectPlan() |
| | | await loadStageList() |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | await loadProjectTypeMap() |
| | | await loadDetail() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .section-bar { |
| | | width: 3px; |
| | | height: 14px; |
| | | background: #002FA7; |
| | | border-radius: 2px; |
| | | } |
| | | .header-card { |
| | | margin-bottom: 14px; |
| | | } |
| | | |
| | | .header-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .header-title { |
| | | font-weight: 600; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .content-card { |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .attachment-block { |
| | | margin-top: 14px; |
| | | } |
| | | |
| | | .attachment-title { |
| | | font-weight: 600; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .attachment-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .attachment-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .attachment-name { |
| | | max-width: 520px; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | </style> |
| | | |