From 4f8ccd4b18baa9301c2e389bc0e53d499be54802 Mon Sep 17 00:00:00 2001
From: ZN <zhang_12370@163.com>
Date: 星期三, 11 三月 2026 18:01:39 +0800
Subject: [PATCH] Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-management into dev_New
---
src/views/projectManagement/Management/projectDetail.vue | 538 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 538 insertions(+), 0 deletions(-)
diff --git a/src/views/projectManagement/Management/projectDetail.vue b/src/views/projectManagement/Management/projectDetail.vue
new file mode 100644
index 0000000..c54a389
--- /dev/null
+++ b/src/views/projectManagement/Management/projectDetail.vue
@@ -0,0 +1,538 @@
+<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="娉曚汉浠h〃">{{ 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,
+ 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('鎻愪氦澶辫触')
+ }
+}
+
+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(att) {
+ if (att?.url) {
+ try {
+ proxy.$download.resource(att.url)
+ return
+ } catch (e) {}
+ }
+ if (att?.name) {
+ try {
+ proxy.$download.name(att.name, false)
+ 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>
+
--
Gitblit v1.9.3