| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <SearchPanel |
| | | v-model="queryParams" |
| | | :schema="searchSchema" |
| | | @search="handleQuery" |
| | | @reset="resetQuery" |
| | | > |
| | | <template #billStatus="{ item }"> |
| | | <el-select v-model="queryParams[item.prop]" placeholder="è¯·éæ©åæ®ç¶æ" clearable style="width: 100%"> |
| | | <el-option v-for="dict in bill_status" :key="dict.value" :label="dict.label" :value="dict.value" /> |
| | | </el-select> |
| | | </template> |
| | | <template #auditStatus="{ item }"> |
| | | <el-select v-model="queryParams[item.prop]" placeholder="è¯·éæ©è®¡åç¶æ" clearable style="width: 100%"> |
| | | <el-option v-for="dict in project_management" :key="dict.value" :label="dict.label" :value="dict.value" /> |
| | | </el-select> |
| | | </template> |
| | | <template #projectStage="{ item }"> |
| | | <el-select v-model="queryParams[item.prop]" placeholder="è¯·éæ©å®¡æ ¸ç¶æ" clearable style="width: 100%"> |
| | | <el-option v-for="dict in plan_status" :key="dict.value" :label="dict.label" :value="dict.value" /> |
| | | </el-select> |
| | | </template> |
| | | </SearchPanel> |
| | | |
| | | <div class="table-container"> |
| | | <div class="table-actions"> |
| | | <el-button style="background-color: #002FA7; color: #fff" @click="handleAdd">æ°å¢</el-button> |
| | | <!-- <el-dropdown split-button type="default" @command="handleGenerateBill" style="margin-left: 10px;"> |
| | | çæåæ® |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | | <el-dropdown-item command="1">çæåæ®1</el-dropdown-item> |
| | | <el-dropdown-item command="2">çæåæ®2</el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | </template> |
| | | </el-dropdown> --> |
| | | <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 |
| | | :column="columns" |
| | | :tableData="tableData" |
| | | :page="pagination" |
| | | :tableLoading="loading" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination="handlePagination" |
| | | > |
| | | <template #auditStatus="{ row }"> |
| | | <dict-tag :options="project_management" :value="row.auditStatus" /> |
| | | </template> |
| | | <template #projectStage="{ row }"> |
| | | <dict-tag :options="plan_status" :value="row.projectStage" /> |
| | | </template> |
| | | <template #action="{ row }"> |
| | | <el-button link type="primary" @click="handleEdit(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, |
| | | 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: { |
| | | projectNameOrCode: undefined, |
| | | customerName: undefined, |
| | | billStatus: undefined, |
| | | projectStage: undefined, |
| | | auditStatus: undefined, |
| | | salesperson: undefined, |
| | | pageNum: 1, |
| | | pageSize: 10 |
| | | }, |
| | | pagination: { |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | layout: 'total, sizes, prev, pager, next, jumper' |
| | | } |
| | | }) |
| | | |
| | | const { queryParams, pagination } = toRefs(data) |
| | | |
| | | const searchSchema = [ |
| | | { prop: 'projectNameOrCode', label: '项ç®åç§°/ç¼å·', type: 'input', placeholder: '请è¾å
¥é¡¹ç®åç§°/ç¼å·' }, |
| | | { prop: 'customerName', label: '客æ·åç§°', type: 'input', placeholder: '请è¾å
¥å®¢æ·åç§°' }, |
| | | { prop: 'billStatus', label: 'åæ®ç¶æ', slot: 'billStatus' }, |
| | | { prop: 'projectStage', label: '计åç¶æ', slot: 'projectStage' }, |
| | | { prop: 'auditStatus', label: 'å®¡æ ¸ç¶æ', slot: 'auditStatus' }, |
| | | { prop: 'salesperson', label: 'ä¸å¡äººå', type: 'input', placeholder: '请è¾å
¥ä¸å¡äººå' } |
| | | ] |
| | | |
| | | const columns = [ |
| | | { label: 'åæ®ç¼å·', prop: 'billNo', align: 'center', width: '150' }, |
| | | { label: '项ç®åç§°', prop: 'projectName', align: 'center' }, |
| | | { label: 'å®¡æ ¸ç¶æ', prop: 'auditStatus', align: 'center', dataType: 'slot', slot: 'auditStatus' }, |
| | | { label: '客æ·åç§°', prop: 'customerName', align: 'center' }, |
| | | { label: 'ç«é¡¹æ¥æ', prop: 'setupDate', align: 'center', width: '120' }, |
| | | { label: 'é¡¹ç®æ¥æº', prop: 'projectSource', align: 'center' }, |
| | | { label: '项ç®åç±»', prop: 'projectClassification', align: 'center' }, |
| | | { label: 'æä½', prop: 'action', align: 'center', width: '250', dataType: 'slot', slot: 'action', fixed: 'right' } |
| | | ] |
| | | |
| | | function getList() { |
| | | loading.value = true |
| | | const params = { |
| | | noOrName: queryParams.value.projectNameOrCode, |
| | | clientName: queryParams.value.customerName, |
| | | salesmanName: queryParams.value.salesperson, |
| | | reviewStatus: queryParams.value.auditStatus, |
| | | stage: queryParams.value.projectStage, |
| | | current: queryParams.value.pageNum, |
| | | size: queryParams.value.pageSize |
| | | } |
| | | listProject(params) |
| | | .then(response => { |
| | | const records = response?.data?.records || response?.rows || response?.records || [] |
| | | const billFilter = queryParams.value.billStatus |
| | | const filtered = billFilter === undefined || billFilter === null || billFilter === '' |
| | | ? records |
| | | : records.filter(r => String(r.billStatus ?? r.status) === String(billFilter)) |
| | | tableData.value = filtered.map(r => ({ |
| | | id: r.id, |
| | | billNo: r.no ?? r.billNo, |
| | | projectName: r.title ?? r.projectName, |
| | | billStatus: r.billStatus ?? r.status, |
| | | auditStatus: r.reviewStatus ?? r.auditStatus, |
| | | projectStage: r.stage ?? r.projectStage, |
| | | customerName: r.clientName ?? r.customerName, |
| | | parentProject: r.parentTitle ?? r.parentName ?? r.parentProject, |
| | | setupDate: r.establishTime ?? r.setupDate, |
| | | projectType: r.planName ?? r.projectType, |
| | | projectSource: r.source ?? r.projectSource, |
| | | projectClassification: r.departmentName ?? r.projectClassification, |
| | | raw: r |
| | | })) |
| | | pagination.value.total = response?.total || response?.data?.total || 0 |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false |
| | | }) |
| | | } |
| | | |
| | | function handleQuery() { |
| | | queryParams.value.pageNum = 1 |
| | | pagination.value.current = 1 |
| | | getList() |
| | | } |
| | | |
| | | function resetQuery() { |
| | | queryParams.value = { |
| | | projectNameOrCode: undefined, |
| | | customerName: undefined, |
| | | billStatus: undefined, |
| | | projectStage: undefined, |
| | | auditStatus: undefined, |
| | | salesperson: undefined, |
| | | pageNum: 1, |
| | | pageSize: 10 |
| | | } |
| | | handleQuery() |
| | | } |
| | | |
| | | function handleSelectionChange(selection) { |
| | | ids.value = selection.map(item => item.id) |
| | | } |
| | | |
| | | function handlePagination({ page, limit }) { |
| | | queryParams.value.pageNum = page |
| | | queryParams.value.pageSize = limit |
| | | pagination.value.current = page |
| | | pagination.value.size = limit |
| | | getList() |
| | | } |
| | | |
| | | function handleAdd() { |
| | | formDiaRef.value?.openDialog({ operationType: 'add' }) |
| | | } |
| | | |
| | | async function handleDelete() { |
| | | const delIds = ids.value |
| | | if (delIds.length === 0) { |
| | | ElMessage.warning('è¯·éæ©è¦å é¤çæ°æ®é¡¹') |
| | | return |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('æ¯å¦ç¡®è®¤å 餿鿰æ®é¡¹?', 'è¦å', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }) |
| | | deleteLoading.value = true |
| | | await delProject(delIds) |
| | | getList() |
| | | ElMessage.success('å 餿å') |
| | | } catch {} finally { |
| | | deleteLoading.value = false |
| | | } |
| | | } |
| | | |
| | | async function handleSubmit() { |
| | | const submitIds = ids.value |
| | | if (submitIds.length === 0) { |
| | | ElMessage.warning('è¯·éæ©è¦æäº¤çæ°æ®é¡¹') |
| | | return |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('æ¯å¦ç¡®è®¤æäº¤æéæ°æ®é¡¹?', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }) |
| | | submitLoading.value = true |
| | | await Promise.all(submitIds.map(id => submitProject({ id }))) |
| | | getList() |
| | | ElMessage.success('æäº¤æå') |
| | | } catch {} finally { |
| | | submitLoading.value = false |
| | | } |
| | | } |
| | | |
| | | async function handleAudit() { |
| | | const auditIds = ids.value |
| | | if (auditIds.length === 0) { |
| | | ElMessage.warning('è¯·éæ©è¦å®¡æ ¸çæ°æ®é¡¹') |
| | | return |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('æ¯å¦ç¡®è®¤å®¡æ ¸æéæ°æ®é¡¹?', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }) |
| | | auditLoading.value = true |
| | | await Promise.all(auditIds.map(id => auditProject({ id }))) |
| | | getList() |
| | | ElMessage.success('å®¡æ ¸æå') |
| | | } catch {} finally { |
| | | auditLoading.value = false |
| | | } |
| | | } |
| | | |
| | | async function handleReverseAudit() { |
| | | const reverseAuditIds = ids.value |
| | | if (reverseAuditIds.length === 0) { |
| | | ElMessage.warning('è¯·éæ©è¦åå®¡æ ¸çæ°æ®é¡¹') |
| | | return |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('æ¯å¦ç¡®è®¤åå®¡æ ¸æéæ°æ®é¡¹?', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }) |
| | | 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 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 |
| | | } |
| | | |
| | | 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) { |
| | | if (!row?.id) return |
| | | router.push(`/projectManagement/Management/detail/${row.id}`) |
| | | } |
| | | |
| | | function handleEdit(row) { |
| | | formDiaRef.value?.openDialog({ operationType: 'edit', row }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .app-container { |
| | | padding: 20px; |
| | | } |
| | | .table-container { |
| | | background-color: #fff; |
| | | padding: 20px; |
| | | border-radius: 4px; |
| | | } |
| | | .table-actions { |
| | | margin-bottom: 15px; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | </style> |