1.0.40 项目台账页面

dev
yinq 2 weeks ago
parent e1fafe5866
commit 4cc39dfeae

@ -116,12 +116,6 @@ export const constantRoutes: RouteRecordRaw[] = [
name: 'ErpProjectPlanView',
meta: { title: '项目计划查看', activeMenu: '/oa/erp/erpProjectPlan' }
},
{
path: 'budgetInfo/edit',
component: () => import('@/views/oa/erp/budgetInfo/edit.vue'),
name: 'BudgetEdit',
meta: { title: '项目预算申请', activeMenu: '/oa/erp/budgetInfo' }
},
{
path: 'erpProjectPlan/gantt/:projectPlanId',
@ -140,6 +134,12 @@ export const constantRoutes: RouteRecordRaw[] = [
component: () => import('@/views/oa/erp/erpProjectChange/edit.vue'),
name: 'ErpProjectChangeEditById',
meta: { title: '项目变更编辑', activeMenu: '/oa/erp/erpProjectPlan' }
},
{
path: 'projectLedger/:projectId',
component: () => import('@/views/oa/erp/projectLedger/index.vue'),
name: 'ProjectLedger',
meta: { title: '项目台账', activeMenu: '/oa/erp/projectInfo' }
}
]
},
@ -162,7 +162,6 @@ export const constantRoutes: RouteRecordRaw[] = [
}
]
},
{
path: '/oa/erp',
component: Layout,

@ -96,7 +96,13 @@
<dict-tag :options="contract_flag" :value="scope.row.contractFlag" />
</template>
</el-table-column>
<el-table-column label="项目编号" align="center" prop="projectCode" width="130" v-if="columns[3].visible" />
<el-table-column label="项目编号" align="center" prop="projectCode" width="130" v-if="columns[3].visible">
<template #default="scope">
<el-link type="primary" underline @click="handleLedger(scope.row)">
{{ scope.row.projectCode }}
</el-link>
</template>
</el-table-column>
<el-table-column label="项目名称" align="center" prop="projectName" width="140" v-if="columns[4].visible" />
<el-table-column label="业务方向" align="center" prop="businessDirection" width="100" v-if="columns[5].visible">
<template #default="scope">
@ -318,6 +324,14 @@ const handleView = (row?: ProjectInfoVO) => {
});
};
/** 项目台账页面 */
const handleLedger = (row: ProjectInfoVO) => {
const _projectId = row?.projectId;
if (!_projectId) return;
proxy.$tab.closePage(route);
router.push(`/oa/erp/projectLedger/${_projectId}`);
};
/** 删除按钮操作 */
const handleDelete = async (row?: ProjectInfoVO) => {
const _projectIds = row?.projectId || ids.value;

@ -0,0 +1,513 @@
<template>
<div class="project-ledger-container">
<el-card shadow="never" class="main-card">
<template #header>
<div class="card-header">
<span class="header-title">项目台账</span>
<el-button size="default" type="primary" icon="ArrowLeft" @click="handleBack"></el-button>
</div>
</template>
<el-tabs v-model="activeTab" @tab-change="handleTabChange" class="custom-tabs">
<el-tab-pane label="项目基本信息" name="project">
<div v-loading="loadingProject" class="tab-content">
<el-descriptions :column="3" border v-if="projectInfo" class="project-descriptions">
<el-descriptions-item label="项目编号" :span="1">
<span class="value-text value-highlight">{{ projectInfo?.projectCode || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="项目名称" :span="2">
<span class="value-text value-highlight">{{ projectInfo?.projectName || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="有无合同">
<dict-tag :options="contract_flag" :value="projectInfo?.contractFlag" />
</el-descriptions-item>
<el-descriptions-item label="业务方向">
<dict-tag :options="business_direction" :value="projectInfo?.businessDirection" />
</el-descriptions-item>
<el-descriptions-item label="项目类别">
<dict-tag :options="project_category" :value="projectInfo?.projectCategory" />
</el-descriptions-item>
<el-descriptions-item label="项目状态">
<dict-tag :options="project_status" :value="projectInfo?.projectStatus" />
</el-descriptions-item>
<el-descriptions-item label="流程状态">
<dict-tag :options="wf_business_status" :value="projectInfo?.flowStatus" />
</el-descriptions-item>
<el-descriptions-item label="激活标识">
<dict-tag :options="active_flag" :value="projectInfo?.activeFlag" />
</el-descriptions-item>
<el-descriptions-item label="项目经理">{{ projectInfo?.managerName || '-' }}</el-descriptions-item>
<el-descriptions-item label="部门负责人">{{ projectInfo?.chargeName || '-' }}</el-descriptions-item>
<el-descriptions-item label="部门">{{ projectInfo?.deptName || '-' }}</el-descriptions-item>
<el-descriptions-item label="付款方式">{{ projectInfo?.paymentMethod || '-' }}</el-descriptions-item>
<el-descriptions-item label="金额" :span="1">
<span class="value-text value-amount">
{{ projectInfo?.amount ? formatNumber(projectInfo.amount) + ' 元' : '-' }}
</span>
</el-descriptions-item>
</el-descriptions>
<el-empty v-else description="暂无项目信息" class="empty-state" />
</div>
</el-tab-pane>
<el-tab-pane label="合同信息" name="contract">
<div v-loading="loadingContract" class="tab-content">
<div class="section-header">
<div class="header-left">
<i class="el-icon-document-copy section-icon"></i>
<span class="section-title">合同信息</span>
<el-tag v-if="contractList.length > 0" type="info" size="small" class="count-tag">
{{ contractList.length }}
</el-tag>
</div>
<div class="header-actions">
<el-button size="default" type="primary" plain icon="Link" @click="openContractPage"></el-button>
<el-button size="default" icon="Refresh" @click="loadContractList"></el-button>
</div>
</div>
<el-table :data="contractList" size="default" border stripe v-loading="loadingContract" class="data-table">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="合同编号" prop="contractCode" width="150" show-overflow-tooltip />
<el-table-column label="合同名称" prop="contractName" min-width="200" show-overflow-tooltip />
<el-table-column label="合同总价" prop="totalPrice" width="130" align="right">
<template #default="scope">
{{ scope.row.totalPrice ? formatNumber(scope.row.totalPrice) + ' 元' : '-' }}
</template>
</el-table-column>
<el-table-column label="业务方向" prop="businessDirection" width="120">
<template #default="scope">
<dict-tag :options="business_direction" :value="scope.row.businessDirection" />
</template>
</el-table-column>
<el-table-column label="合同状态" prop="contractStatus" width="120">
<template #default="scope">
<dict-tag :options="contract_status" :value="scope.row.contractStatus" />
</template>
</el-table-column>
<el-table-column label="流程状态" prop="flowStatus" width="120">
<template #default="scope">
<dict-tag :options="wf_business_status" :value="scope.row.flowStatus" />
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="scope">
<el-button size="default" type="primary" text @click="viewContract(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!loadingContract && contractList.length === 0" description="暂无合同信息" class="empty-state" />
</div>
</el-tab-pane>
<el-tab-pane label="预算信息" name="budget">
<div v-loading="loadingBudget" class="tab-content">
<div class="section-header">
<div class="header-left">
<i class="el-icon-coin section-icon"></i>
<span class="section-title">预算信息</span>
<el-tag v-if="budgetTotal > 0" type="info" size="small" class="count-tag">
{{ budgetTotal }}
</el-tag>
</div>
<div class="header-actions">
<el-button size="default" type="primary" plain icon="Link" @click="openBudgetPage"></el-button>
<el-button size="default" icon="Refresh" @click="loadBudgetList"></el-button>
</div>
</div>
<el-table :data="budgetList" size="default" border stripe v-loading="loadingBudget" class="data-table">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="预算ID" prop="budgetId" width="120" />
<el-table-column label="版本" prop="budgetVersion" width="80" align="center" />
<el-table-column label="预算状态" prop="budgetStatus" width="120" align="center">
<template #default="scope">
<dict-tag :options="budget_status" :value="scope.row.budgetStatus" />
</template>
</el-table-column>
<el-table-column label="合同额(元)" prop="contractAmount" width="130" align="right">
<template #default="scope">
{{ scope.row.contractAmount ? formatNumber(scope.row.contractAmount) : '-' }}
</template>
</el-table-column>
<el-table-column label="预算成本(元)" prop="budgetCost" width="130" align="right">
<template #default="scope">
{{ scope.row.budgetCost ? formatNumber(scope.row.budgetCost) : '-' }}
</template>
</el-table-column>
<el-table-column label="预算毛利率(%)" prop="budgetRate" width="130" align="right">
<template #default="scope">
{{ scope.row.budgetRate ? (scope.row.budgetRate / 100).toFixed(2) + '%' : '-' }}
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="scope">
<el-button size="default" type="primary" text @click="viewBudget(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!loadingBudget && budgetList.length === 0" description="暂无预算信息" class="empty-state" />
<pagination
v-show="budgetTotal > 0"
:total="budgetTotal"
v-model:page="budgetQuery.pageNum"
v-model:limit="budgetQuery.pageSize"
@pagination="loadBudgetList"
class="pagination-wrapper"
/>
</div>
</el-tab-pane>
<el-tab-pane label="项目计划" name="plan">
<div v-loading="loadingPlan" class="tab-content">
<div class="section-header">
<div class="header-left">
<i class="el-icon-calendar section-icon"></i>
<span class="section-title">项目计划</span>
<el-tag v-if="planTotal > 0" type="info" size="small" class="count-tag">
{{ planTotal }}
</el-tag>
</div>
<div class="header-actions">
<el-button size="default" type="primary" plain icon="Link" @click="openPlanPage"></el-button>
<el-button size="default" icon="Refresh" @click="loadPlanList"></el-button>
</div>
</div>
<el-table :data="planList" size="default" border stripe v-loading="loadingPlan" class="data-table">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="计划编号" prop="projectPlanCode" width="180" show-overflow-tooltip />
<el-table-column label="项目名称" prop="projectName" min-width="200" show-overflow-tooltip />
<el-table-column label="项目经理" prop="managerName" width="120" />
<el-table-column label="部门负责人" prop="chargeName" width="120" />
<el-table-column label="付款方式" prop="paymentMethod" width="120" />
<el-table-column label="项目计划状态" prop="projectPlanStatus" width="150" align="center">
<template #default="scope">
<dict-tag :options="project_plan_status" :value="scope.row.projectPlanStatus" />
</template>
</el-table-column>
<el-table-column label="流程状态" prop="flowStatus" width="120" align="center">
<template #default="scope">
<dict-tag :options="wf_business_status" :value="scope.row.flowStatus" />
</template>
</el-table-column>
<el-table-column label="关联合同" prop="contractName" min-width="180" show-overflow-tooltip />
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="scope">
<el-button size="default" type="primary" text @click="viewPlan(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!loadingPlan && planList.length === 0" description="暂无项目计划" class="empty-state" />
<pagination
v-show="planTotal > 0"
:total="planTotal"
v-model:page="planQuery.pageNum"
v-model:limit="planQuery.pageSize"
@pagination="loadPlanList"
class="pagination-wrapper"
/>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</template>
<script setup lang="ts" name="ProjectLedger">
import { computed, reactive, ref, toRefs, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { getProjectInfo } from '@/api/oa/erp/projectInfo';
import { listErpBudgetInfo } from '@/api/oa/erp/budgetInfo';
import { budgetInfoVO } from '@/api/oa/erp/budgetInfo/types';
import { listErpProjectPlan } from '@/api/oa/erp/erpProjectPlan';
import { ErpProjectPlanVO } from '@/api/oa/erp/erpProjectPlan/types';
import { ProjectInfoVO } from '@/api/oa/erp/projectInfo/types';
import { getErpProjectContractsList } from '@/api/oa/erp/projectContracts';
import { ProjectContractsVO } from '@/api/oa/erp/projectContracts/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const router = useRouter();
const { contract_flag, project_category, business_direction, project_status, wf_business_status, active_flag, contract_status, contract_category, project_plan_status, budget_status } =
toRefs<any>(proxy?.useDict('contract_flag', 'project_category', 'business_direction', 'project_status', 'wf_business_status', 'active_flag', 'contract_status', 'contract_category', 'project_plan_status', 'budget_status'));
const projectId = computed(() => route.params.projectId as string | number | undefined);
const projectInfo = ref<ProjectInfoVO | null>(null);
const contractList = ref<ProjectContractsVO[]>([]);
const activeTab = ref('project');
//
const loadedTabs = ref<Set<string>>(new Set(['project']));
const loadingProject = ref(false);
const loadingContract = ref(false);
const loadingBudget = ref(false);
const loadingPlan = ref(false);
const budgetList = ref<budgetInfoVO[]>([]);
const planList = ref<ErpProjectPlanVO[]>([]);
const budgetTotal = ref(0);
const planTotal = ref(0);
const budgetQuery = reactive({ pageNum: 1, pageSize: 5, projectId: undefined });
const planQuery = reactive({ pageNum: 1, pageSize: 5, projectId: undefined });
const handleBack = () => {
proxy?.$tab.closePage(route);
router.back();
};
const openContractPage = () => {
proxy?.$tab.openPage('/oa/erp/contractInfo', '合同管理');
};
const openBudgetPage = () => {
proxy?.$tab.openPage('/oa/erp/budgetInfo', '项目预算');
};
const openPlanPage = () => {
proxy?.$tab.openPage('/oa/erp/erpProjectPlan', '项目计划');
};
const viewBudget = (row: budgetInfoVO) => {
if (!row?.budgetId) return;
proxy?.$tab.openPage('/oa/erp/budgetInfo/edit', '项目预算申请', {
id: row.budgetId,
type: 'view'
});
};
const viewPlan = (row: ErpProjectPlanVO) => {
if (!row?.projectPlanId) return;
router.push(`/oa/erp/erpProjectPlan/view/${row.projectPlanId}`);
};
const viewContract = (row: ProjectContractsVO) => {
if (!row?.contractId) return;
proxy?.$tab.openPage('/oa/erp/contractInfo/edit', '合同信息', {
id: row.contractId,
type: 'view'
});
};
//
const formatNumber = (num: number) => {
if (!num) return '0';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
const loadContractList = async () => {
if (!projectId.value) {
contractList.value = [];
return;
}
loadingContract.value = true;
try {
const params = { projectId: projectId.value } as any;
const res = await getErpProjectContractsList(params);
// left join
contractList.value = res.data || [];
} catch (error) {
console.error('查询项目关联合同失败:', error);
contractList.value = [];
} finally {
loadingContract.value = false;
}
};
const loadBudgetList = async () => {
if (!projectId.value) {
budgetList.value = [];
budgetTotal.value = 0;
return;
}
loadingBudget.value = true;
try {
const res: any = await listErpBudgetInfo({
...budgetQuery,
projectId: projectId.value
});
budgetList.value = res.rows || [];
budgetTotal.value = res.total || 0;
} finally {
loadingBudget.value = false;
}
};
const loadPlanList = async () => {
if (!projectId.value) {
planList.value = [];
planTotal.value = 0;
return;
}
loadingPlan.value = true;
try {
const res: any = await listErpProjectPlan({
...planQuery,
projectId: projectId.value
});
planList.value = res.rows || [];
planTotal.value = res.total || 0;
} finally {
loadingPlan.value = false;
}
};
const loadProjectInfo = async () => {
if (!projectId.value) return;
loadingProject.value = true;
try {
const res = await getProjectInfo(projectId.value);
projectInfo.value = res.data;
} finally {
loadingProject.value = false;
}
};
//
const handleTabChange = (tabName: string) => {
if (!loadedTabs.value.has(tabName)) {
loadedTabs.value.add(tabName);
switch (tabName) {
case 'contract':
loadContractList();
break;
case 'budget':
budgetQuery.pageNum = 1;
budgetQuery.projectId = projectId.value;
loadBudgetList();
break;
case 'plan':
planQuery.pageNum = 1;
planQuery.projectId = projectId.value;
loadPlanList();
break;
}
}
};
const loadAll = async () => {
await loadProjectInfo();
budgetQuery.projectId = projectId.value;
planQuery.projectId = projectId.value;
};
watch(
() => projectId.value,
() => {
if (projectId.value) {
loadedTabs.value.clear();
loadedTabs.value.add('project');
loadAll();
}
},
{ immediate: true }
);
</script>
<style scoped lang="scss">
.project-ledger-container {
padding: 16px;
.main-card {
:deep(.el-card__header) {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.header-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
}
:deep(.el-card__body) {
padding: 0;
}
}
.custom-tabs {
:deep(.el-tabs__content) {
padding: 20px;
}
}
.tab-content {
padding: 0;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e4e7ed;
.header-left {
display: flex;
align-items: center;
gap: 10px;
.section-icon {
font-size: 16px;
color: #409eff;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
}
.header-actions {
display: flex;
gap: 8px;
}
}
.project-descriptions {
.value-text {
&.value-highlight {
font-weight: 600;
color: #409eff;
}
&.value-amount {
font-weight: 600;
color: #f56c6c;
}
}
}
.data-table {
:deep(.el-table__header) {
th {
font-weight: 600;
}
}
:deep(.el-table__body) {
tr:hover {
background-color: #f5f7fa;
}
}
}
.empty-state {
padding: 40px 0;
}
.pagination-wrapper {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
}
</style>
Loading…
Cancel
Save