| src/api/projectManagement/project.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/components/ProjectManagement/DiscussProgressDialog.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/components/ProjectManagement/ProgressReportDialog.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/router/index.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/projectManagement/Management/components/formDia.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/projectManagement/Management/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/projectManagement/Management/projectDetail.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/api/projectManagement/project.js
@@ -71,6 +71,29 @@ }) } // é¶æ®µ export function saveStage(data) { return request({ url: '/projectManagement/info/saveStage', method: 'post', data: data }) } export function listStage(projectId) { return request({ url: `/projectManagement/info/listStage/${projectId}`, method: 'post' }) } export function deleteStage(stageId) { return request({ url: `/projectManagement/info/deleteStage/${stageId}`, method: 'post' }) } export function listPlan(data) { return request({ url: '/projectManagement/plan/listPage', src/components/ProjectManagement/DiscussProgressDialog.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,141 @@ <template> <el-dialog v-model="visible" title="æ´½è°è¿åº¦" width="700px" top="10vh" append-to-body destroy-on-close @close="handleClose"> <el-form ref="formRef" :model="form" :rules="rules" label-position="top" label-width="120px"> <el-form-item label="项ç®é¶æ®µ" prop="planNodeId"> <el-select v-model="form.planNodeId" placeholder="è¯·éæ©" clearable style="width: 100%"> <el-option v-for="opt in stageOptions" :key="opt.value" :label="opt.label" :value="opt.value" /> </el-select> </el-form-item> <el-form-item label="夿³¨" prop="remark"> <el-input v-model="form.remark" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请è¾å ¥" /> </el-form-item> <el-form-item label="éä»¶" prop="attachmentIds"> <el-upload v-model:file-list="fileList" :action="upload.url" :headers="upload.headers" multiple name="files" :on-success="handleUploadSuccess" :on-error="handleUploadError" :on-remove="handleRemove" > <el-button type="primary">ä¸ä¼ æä»¶</el-button> </el-upload> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button @click="visible = false">åæ¶</el-button> <el-button type="danger" @click="submit">æäº¤</el-button> </div> </template> </el-dialog> </template> <script setup name="DiscussProgressDialog"> import { computed, reactive, ref, watch } from 'vue' import { ElMessage } from 'element-plus' import { getToken } from '@/utils/auth' const props = defineProps({ modelValue: { type: Boolean, default: false }, projectId: { type: [Number, String], default: undefined }, planNodes: { type: Array, default: () => [] }, defaultPlanNodeId: { type: [Number, String], default: undefined } }) const emit = defineEmits(['update:modelValue', 'submitted']) const visible = computed({ get: () => props.modelValue, set: v => emit('update:modelValue', v) }) const upload = reactive({ url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload', headers: { Authorization: 'Bearer ' + getToken() } }) const formRef = ref() const fileList = ref([]) const form = ref({ planNodeId: undefined, remark: '', attachmentIds: [] }) const rules = { planNodeId: [{ required: true, message: 'è¯·éæ©', trigger: 'change' }], remark: [{ required: true, message: '请è¾å ¥', trigger: 'blur' }] } const stageOptions = computed(() => { const list = Array.isArray(props.planNodes) ? props.planNodes : [] const sorted = [...list].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0)) return sorted .map(n => ({ label: n.name || n.workContent || n.title || String(n.id ?? ''), value: n.id })) .filter(i => i.value !== undefined && i.value !== null && i.value !== '') }) watch( () => props.modelValue, v => { if (v) { form.value = { planNodeId: props.defaultPlanNodeId ?? stageOptions.value[0]?.value, remark: '', attachmentIds: [] } fileList.value = [] } } ) function handleClose() { formRef.value?.resetFields?.() } function handleUploadError() { ElMessage.error('ä¸ä¼ æä»¶å¤±è´¥') } function handleUploadSuccess(res, file) { if (res?.code !== 200) { ElMessage.error(res?.msg || 'ä¸ä¼ 失败') return } const attachmentId = res?.data?.id ?? res?.data?.tempId ?? '' if (!attachmentId) return form.value.attachmentIds.push(attachmentId) try { file.attachmentId = attachmentId } catch (e) {} ElMessage.success('ä¸ä¼ æå') } function handleRemove(file) { const attachmentId = file?.attachmentId if (!attachmentId) return form.value.attachmentIds = (form.value.attachmentIds || []).filter(id => id !== attachmentId) } async function submit() { await formRef.value?.validate?.() emit('submitted', { projectId: props.projectId, ...form.value }) visible.value = false } </script> <style scoped lang="scss"> .dialog-footer { display: flex; justify-content: flex-end; gap: 10px; } </style> src/components/ProjectManagement/ProgressReportDialog.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,282 @@ <template> <el-dialog v-model="visible" title="è¿åº¦æ±æ¥" width="900px" top="8vh" append-to-body destroy-on-close @close="handleClose"> <el-form ref="formRef" :model="form" :rules="rules" label-position="top" label-width="120px"> <el-form-item label="项ç®é¶æ®µ" prop="planNodeId"> <el-select v-model="form.planNodeId" placeholder="è¯·éæ©" clearable style="width: 100%"> <el-option v-for="opt in stageOptions" :key="opt.value" :label="opt.label" :value="opt.value" /> </el-select> </el-form-item> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="计åå¼å§æ¶é´" prop="planStartTime"> <el-date-picker v-model="form.planStartTime" type="date" value-format="YYYY-MM-DD" format="YYYY-MM-DD" placeholder="è¯·éæ©" style="width: 100%" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="计åå®å·¥æ¶é´" prop="planEndTime"> <el-date-picker v-model="form.planEndTime" type="date" value-format="YYYY-MM-DD" format="YYYY-MM-DD" placeholder="è¯·éæ©" style="width: 100%" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="å®é å¼å·¥æ¥æ" prop="actualStartTime"> <el-date-picker v-model="form.actualStartTime" type="date" value-format="YYYY-MM-DD" format="YYYY-MM-DD" placeholder="è¯·éæ©" style="width: 100%" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="æ¬æ¬¡è¿åº¦æ¥æ" prop="reportDate"> <el-date-picker v-model="form.reportDate" type="date" value-format="YYYY-MM-DD" format="YYYY-MM-DD" placeholder="è¯·éæ©" style="width: 100%" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="䏿¬¡è¿åº¦(%)" prop="lastProgress"> <el-input-number v-model="form.lastProgress" :min="0" :max="100" controls-position="right" style="width: 100%" /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="宿è¿åº¦(%)" prop="completionProgress"> <div style="display: flex; gap: 8px; width: 100%;"> <el-input-number v-model="form.completionProgress" :min="0" :max="100" controls-position="right" style="flex: 1" /> <el-button type="danger" @click="markDone">宿</el-button> </div> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="累计è¿åº¦(%)" prop="totalProgress"> <el-input-number v-model="form.totalProgress" :min="0" :max="100" controls-position="right" style="width: 100%" /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="å®é å®å·¥æ¥æ" prop="actualEndTime"> <el-date-picker v-model="form.actualEndTime" type="date" value-format="YYYY-MM-DD" format="YYYY-MM-DD" placeholder="è¯·éæ©" style="width: 100%" clearable /> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="è´è´£äºº" prop="managerName"> <el-input v-model="form.managerName" placeholder="请è¾å ¥" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="8"> <el-form-item label="é¨é¨" prop="departmentName"> <el-input v-model="form.departmentName" placeholder="请è¾å ¥" clearable /> </el-form-item> </el-col> <el-col :span="16"> <el-form-item label="夿³¨" prop="remark"> <el-input v-model="form.remark" placeholder="请è¾å ¥" clearable /> </el-form-item> </el-col> </el-row> <el-form-item label="éä»¶" prop="attachmentIds"> <el-upload v-model:file-list="fileList" :action="upload.url" :headers="upload.headers" multiple name="files" :on-success="handleUploadSuccess" :on-error="handleUploadError" :on-remove="handleRemove" > <el-button type="primary">ä¸ä¼ æä»¶</el-button> </el-upload> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button @click="visible = false">åæ¶</el-button> <el-button type="primary" @click="submit">ç¡®å®</el-button> </div> </template> </el-dialog> </template> <script setup name="ProgressReportDialog"> import { computed, reactive, ref, watch } from 'vue' import { ElMessage } from 'element-plus' import { getToken } from '@/utils/auth' const props = defineProps({ modelValue: { type: Boolean, default: false }, projectId: { type: [Number, String], default: undefined }, projectInfo: { type: Object, default: () => ({}) }, planNodes: { type: Array, default: () => [] }, defaultPlanNodeId: { type: [Number, String], default: undefined } }) const emit = defineEmits(['update:modelValue', 'submitted']) const visible = computed({ get: () => props.modelValue, set: v => emit('update:modelValue', v) }) const formRef = ref() const fileList = ref([]) const upload = reactive({ url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload', headers: { Authorization: 'Bearer ' + getToken() } }) const form = ref({ planNodeId: undefined, planStartTime: '', planEndTime: '', actualStartTime: '', actualEndTime: '', reportDate: '', lastProgress: 0, completionProgress: 0, totalProgress: 0, managerName: '', departmentName: '', remark: '', attachmentIds: [] }) 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: 'change' }] } const stageOptions = computed(() => { const list = Array.isArray(props.planNodes) ? props.planNodes : [] const sorted = [...list].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0)) return sorted .map(n => ({ label: n.name || n.workContent || n.title || String(n.id ?? ''), value: n.id })) .filter(i => i.value !== undefined && i.value !== null && i.value !== '') }) function resetFromProject() { const info = props.projectInfo || {} form.value = { planNodeId: props.defaultPlanNodeId ?? stageOptions.value[0]?.value, planStartTime: info.planStartTime || '', planEndTime: info.planEndTime || '', actualStartTime: info.actualStartTime || '', actualEndTime: info.actualEndTime || '', reportDate: '', lastProgress: Number(info.lastProgress ?? 0) || 0, completionProgress: 0, totalProgress: Number(info.totalProgress ?? info.progress ?? 0) || 0, managerName: info.managerName || '', departmentName: info.departmentName || '', remark: '', attachmentIds: [] } fileList.value = [] } watch( () => props.modelValue, v => { if (v) resetFromProject() } ) function handleClose() { formRef.value?.resetFields?.() } function markDone() { form.value.completionProgress = 100 form.value.totalProgress = 100 if (!form.value.actualEndTime) form.value.actualEndTime = form.value.reportDate || '' } function handleUploadError() { ElMessage.error('ä¸ä¼ æä»¶å¤±è´¥') } function handleUploadSuccess(res, file) { if (res?.code !== 200) { ElMessage.error(res?.msg || 'ä¸ä¼ 失败') return } const attachmentId = res?.data?.id ?? res?.data?.tempId ?? '' if (!attachmentId) return form.value.attachmentIds.push(attachmentId) try { file.attachmentId = attachmentId } catch (e) {} ElMessage.success('ä¸ä¼ æå') } function handleRemove(file) { const attachmentId = file?.attachmentId if (!attachmentId) return form.value.attachmentIds = (form.value.attachmentIds || []).filter(id => id !== attachmentId) } async function submit() { await formRef.value?.validate?.() emit('submitted', { projectId: props.projectId, ...form.value }) visible.value = false } </script> <style scoped lang="scss"> .dialog-footer { display: flex; justify-content: flex-end; gap: 10px; } </style> src/router/index.js
@@ -106,6 +106,19 @@ }, ], }, { path: "/projectManagement/Management/detail", component: Layout, hidden: true, children: [ { path: ":id", component: () => import("@/views/projectManagement/Management/projectDetail.vue"), name: "ProjectManagementDetail", meta: { title: "项ç®è¯¦æ ", activeMenu: "/projectManagement/Management" }, }, ], }, ]; // å¨æè·¯ç±ï¼åºäºç¨æ·æé卿å»å è½½ src/views/projectManagement/Management/components/formDia.vue
@@ -1474,7 +1474,7 @@ .section-bar { width: 3px; height: 14px; background: #e61e1e; background: #002FA7; border-radius: 2px; } src/views/projectManagement/Management/index.vue
@@ -35,10 +35,10 @@ </el-dropdown-menu> </template> </el-dropdown> --> <el-button @click="handleSubmit">æäº¤</el-button> <el-button @click="handleAudit">å®¡æ ¸</el-button> <el-button @click="handleReverseAudit">åå®¡æ ¸</el-button> <el-button @click="handleDelete">å é¤</el-button> <el-button :loading="submitLoading" @click="handleSubmit">æäº¤</el-button> <el-button :loading="auditLoading" @click="handleAudit">å®¡æ ¸</el-button> <el-button :loading="reverseAuditLoading" @click="handleReverseAudit">åå®¡æ ¸</el-button> <el-button :loading="deleteLoading" @click="handleDelete">å é¤</el-button> </div> <PIMTable @@ -58,38 +58,63 @@ </template> <template #action="{ row }"> <el-button link type="primary" @click="handleEdit(row)">ç¼è¾</el-button> <el-button link type="primary" @click="handleProgressReport(row)">è¿åº¦æ±æ¥</el-button> <el-button link type="primary" @click="handleDiscussProgress(row)">æ´½è°è¿å±</el-button> <el-button link type="primary" :loading="progressBtnLoadingId===row.id" @click="handleProgressReport(row)">è¿åº¦æ±æ¥</el-button> <el-button link type="primary" @click="handleDetail(row)">详æ </el-button> </template> </PIMTable> </div> <FormDia ref="formDiaRef" @completed="getList" /> <ProgressReportDialog v-model="progressReportVisible" :project-id="progressProjectId" :project-info="progressProjectInfo" :plan-nodes="progressPlanNodes" :default-plan-node-id="progressDefaultPlanNodeId" @submitted="handleProgressSubmitted" /> </div> </template> <script setup name="ProjectManagement"> import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue' import { useRouter } from 'vue-router' import SearchPanel from '@/components/SearchPanel/index.vue' import PIMTable from '@/components/PIMTable/PIMTable.vue' import FormDia from './components/formDia.vue' import ProgressReportDialog from '@/components/ProjectManagement/ProgressReportDialog.vue' import { listProject, delProject, submitProject, auditProject, reverseAuditProject reverseAuditProject, getProject, saveStage } from '@/api/projectManagement/project' import { listPlan } from '@/api/projectManagement/projectType' import { ElMessage, ElMessageBox } from 'element-plus' import useUserStore from '@/store/modules/user' const { proxy } = getCurrentInstance() const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status') const router = useRouter() const userStore = useUserStore() const loading = ref(false) const ids = ref([]) const tableData = ref([]) const formDiaRef = ref() const progressReportVisible = ref(false) const progressProjectId = ref(undefined) const progressProjectInfo = ref({}) const progressPlanNodes = ref([]) const progressDefaultPlanNodeId = ref(undefined) const progressBtnLoadingId = ref(null) const submitLoading = ref(false) const auditLoading = ref(false) const reverseAuditLoading = ref(false) const deleteLoading = ref(false) const data = reactive({ queryParams: { @@ -208,102 +233,174 @@ formDiaRef.value?.openDialog({ operationType: 'add' }) } function handleDelete() { async function handleDelete() { const delIds = ids.value if (delIds.length === 0) { ElMessage.warning('è¯·éæ©è¦å é¤çæ°æ®é¡¹') return } ElMessageBox.confirm('æ¯å¦ç¡®è®¤å 餿鿰æ®é¡¹?', 'è¦å', { confirmButtonText: 'ç¡®å®', cancelButtonText: 'åæ¶', type: 'warning' }) .then(() => delProject(delIds)) .then(() => { getList() ElMessage.success('å 餿å') try { await ElMessageBox.confirm('æ¯å¦ç¡®è®¤å 餿鿰æ®é¡¹?', 'è¦å', { confirmButtonText: 'ç¡®å®', cancelButtonText: 'åæ¶', type: 'warning' }) .catch(() => {}) deleteLoading.value = true await delProject(delIds) getList() ElMessage.success('å 餿å') } catch {} finally { deleteLoading.value = false } } function handleSubmit() { async function handleSubmit() { const submitIds = ids.value if (submitIds.length === 0) { ElMessage.warning('è¯·éæ©è¦æäº¤çæ°æ®é¡¹') return } ElMessageBox.confirm('æ¯å¦ç¡®è®¤æäº¤æéæ°æ®é¡¹?', 'æç¤º', { confirmButtonText: 'ç¡®å®', cancelButtonText: 'åæ¶', type: 'warning' }) .then(async () => { await Promise.all(submitIds.map(id => submitProject({ id }))) try { await ElMessageBox.confirm('æ¯å¦ç¡®è®¤æäº¤æéæ°æ®é¡¹?', 'æç¤º', { confirmButtonText: 'ç¡®å®', cancelButtonText: 'åæ¶', type: 'warning' }) .then(() => { getList() ElMessage.success('æäº¤æå') }) .catch(() => {}) submitLoading.value = true await Promise.all(submitIds.map(id => submitProject({ id }))) getList() ElMessage.success('æäº¤æå') } catch {} finally { submitLoading.value = false } } function handleAudit() { async function handleAudit() { const auditIds = ids.value if (auditIds.length === 0) { ElMessage.warning('è¯·éæ©è¦å®¡æ ¸çæ°æ®é¡¹') return } ElMessageBox.confirm('æ¯å¦ç¡®è®¤å®¡æ ¸æéæ°æ®é¡¹?', 'æç¤º', { confirmButtonText: 'ç¡®å®', cancelButtonText: 'åæ¶', type: 'warning' }) .then(async () => { await Promise.all(auditIds.map(id => auditProject({ id }))) try { await ElMessageBox.confirm('æ¯å¦ç¡®è®¤å®¡æ ¸æéæ°æ®é¡¹?', 'æç¤º', { confirmButtonText: 'ç¡®å®', cancelButtonText: 'åæ¶', type: 'warning' }) .then(() => { getList() ElMessage.success('å®¡æ ¸æå') }) .catch(() => {}) auditLoading.value = true await Promise.all(auditIds.map(id => auditProject({ id }))) getList() ElMessage.success('å®¡æ ¸æå') } catch {} finally { auditLoading.value = false } } function handleReverseAudit() { async function handleReverseAudit() { const reverseAuditIds = ids.value if (reverseAuditIds.length === 0) { ElMessage.warning('è¯·éæ©è¦åå®¡æ ¸çæ°æ®é¡¹') return } ElMessageBox.confirm('æ¯å¦ç¡®è®¤åå®¡æ ¸æéæ°æ®é¡¹?', 'æç¤º', { confirmButtonText: 'ç¡®å®', cancelButtonText: 'åæ¶', type: 'warning' }) .then(async () => { await Promise.all(reverseAuditIds.map(id => reverseAuditProject({ id }))) try { await ElMessageBox.confirm('æ¯å¦ç¡®è®¤åå®¡æ ¸æéæ°æ®é¡¹?', 'æç¤º', { confirmButtonText: 'ç¡®å®', cancelButtonText: 'åæ¶', type: 'warning' }) .then(() => { getList() ElMessage.success('åå®¡æ ¸æå') }) .catch(() => {}) reverseAuditLoading.value = true await Promise.all(reverseAuditIds.map(id => reverseAuditProject({ id }))) getList() ElMessage.success('åå®¡æ ¸æå') } catch {} finally { reverseAuditLoading.value = false } } function handleGenerateBill(command) { ElMessage.info(`çæåæ®: ${command}`) } function handleProgressReport(row) { formDiaRef.value?.openDialog({ operationType: 'view', row }) function computeDefaultPlanNodeId(stageVal, nodes) { const list = Array.isArray(nodes) ? nodes : [] if (list.length === 0) return undefined const direct = list.find(n => String(n.id) === String(stageVal)) if (direct?.id) return direct.id const sorted = [...list].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 } function handleDiscussProgress(row) { formDiaRef.value?.openDialog({ operationType: 'view', row }) async function handleProgressReport(row) { if (!row?.id) return try { progressBtnLoadingId.value = row.id const res = await getProject(row.id) const detail = res?.data?.data ?? res?.data ?? res const info = detail?.info || {} progressProjectId.value = info.id progressProjectInfo.value = info const planId = info.projectManagementPlanId if (planId) { const planRes = await listPlan({ current: 1, size: 999 }) const records = planRes?.data?.records || planRes?.records || planRes?.rows || [] const plan = (records || []).find(p => String(p.id) === String(planId)) || {} progressPlanNodes.value = Array.isArray(plan?.planNodeList) ? plan.planNodeList : [] } else { progressPlanNodes.value = [] } progressDefaultPlanNodeId.value = computeDefaultPlanNodeId(info.stage, progressPlanNodes.value) progressReportVisible.value = true } catch (e) { ElMessage.error('è·å项ç®è¯¦æ 失败') } finally { progressBtnLoadingId.value = null } } async function handleProgressSubmitted(payload) { try { const nodes = Array.isArray(progressPlanNodes.value) ? progressPlanNodes.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: progressProjectId.value, description, actualLeaderId: userStore.id || progressProjectInfo.value?.managerId, actualLeaderName: userStore.nickName || progressProjectInfo.value?.managerName, estimatedDuration: Number(node.estimatedDuration ?? 0) || 0, planStartTime: payload.planStartTime || progressProjectInfo.value?.planStartTime, planEndTime: payload.planEndTime || progressProjectInfo.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('æäº¤æå') getList() return } ElMessage.error(res?.msg || 'æäº¤å¤±è´¥') } catch (e) { ElMessage.error('æäº¤å¤±è´¥') } } function handleDetail(row) { formDiaRef.value?.openDialog({ operationType: 'view', row }) if (!row?.id) return router.push(`/projectManagement/Management/detail/${row.id}`) } function handleEdit(row) { 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="æ³äººä»£è¡¨">{{ 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>