1080 lines
55 KiB
Vue
1080 lines
55 KiB
Vue
<template>
|
||
<div class="dispatch-layout">
|
||
|
||
<!-- ══════════════ 左栏:项目列表 ══════════════ -->
|
||
<div class="panel-left">
|
||
<div class="left-header">
|
||
<span class="left-title">项目列表</span>
|
||
<span class="left-count">{{ projectList.length }} 个</span>
|
||
</div>
|
||
<div class="left-search">
|
||
<el-input v-model="searchText" placeholder="编号 / 代号 / 名称" prefix-icon="el-icon-search" size="small" clearable @input="filterProjects" />
|
||
</div>
|
||
<div class="left-list" v-loading="projectLoading">
|
||
<div
|
||
v-for="p in filteredList"
|
||
:key="p.projectId"
|
||
class="project-item"
|
||
:class="{ 'is-active': activeProject && activeProject.projectId === p.projectId }"
|
||
@click="selectProject(p)"
|
||
>
|
||
<div class="pi-top">
|
||
<span class="pi-name" :title="p.projectName">{{ p.projectName }}</span>
|
||
<span class="pi-status" :class="statusClass(p.projectStatus)">{{ statusLabel(p.projectStatus) }}</span>
|
||
</div>
|
||
<div class="pi-row">
|
||
<span class="pi-tag" v-if="p.projectNum">{{ p.projectNum }}</span>
|
||
<span class="pi-tag accent" v-if="p.projectCode">{{ p.projectCode }}</span>
|
||
</div>
|
||
<div class="pi-row">
|
||
<i class="el-icon-user" style="color:#c0c4cc;font-size:11px;margin-right:3px" />
|
||
<span class="pi-sub">{{ p.functionary || '—' }}</span>
|
||
<span class="pi-sub" style="margin-left:auto" v-if="p.beginTime">{{ parseTime(p.beginTime, '{y}-{m}-{d}') }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="!projectLoading && filteredList.length === 0" class="left-empty">暂无项目</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══════════════ 中栏:模块 Tab ══════════════ -->
|
||
<div class="panel-mid" v-if="activeProject">
|
||
<div
|
||
v-for="m in MODULES"
|
||
:key="m.key"
|
||
class="mod-tab"
|
||
:class="{ 'mod-active': activeTab === m.key }"
|
||
@click="switchTab(m.key)"
|
||
>
|
||
<i :class="m.icon" class="mod-icon" />
|
||
<span class="mod-label">{{ m.label }}</span>
|
||
<span v-if="badgeCounts[m.key] > 0" class="mod-badge">{{ badgeCounts[m.key] }}</span>
|
||
<span v-else-if="badgeCounts[m.key] === 0" class="mod-badge mod-badge-zero">0</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══════════════ 右栏:内容区 ══════════════ -->
|
||
<div class="panel-right" v-if="activeProject" v-loading="moduleLoading[activeTab]">
|
||
|
||
<!-- ── 当前项目固定头 ── -->
|
||
<div class="project-header-bar">
|
||
<i class="el-icon-office-building project-header-icon" />
|
||
<span class="project-header-name">{{ activeProject.projectName }}</span>
|
||
<span v-if="activeProject.projectNum" class="project-header-tag">{{ activeProject.projectNum }}</span>
|
||
<span v-if="activeProject.projectCode" class="project-header-tag accent">{{ activeProject.projectCode }}</span>
|
||
<span class="project-header-status" :class="statusClass(activeProject.projectStatus)">{{ statusLabel(activeProject.projectStatus) }}</span>
|
||
<span v-if="activeProject.functionary" class="project-header-sub"><i class="el-icon-user" /> {{ activeProject.functionary }}</span>
|
||
</div>
|
||
|
||
<!-- ── 内容滚动区 ── -->
|
||
<div class="panel-right-body">
|
||
|
||
<!-- ── 基本信息 ── -->
|
||
<template v-if="activeTab === 'info'">
|
||
<div class="section-title">基本信息</div>
|
||
<el-descriptions :column="3" border size="small">
|
||
<el-descriptions-item label="项目名称" :span="2">{{ activeProject.projectName }}</el-descriptions-item>
|
||
<el-descriptions-item label="项目状态">
|
||
<span :class="'status-dot ' + statusClass(activeProject.projectStatus)">{{ statusLabel(activeProject.projectStatus) }}</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="项目编号">{{ activeProject.projectNum || '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="项目代号">{{ activeProject.projectCode || '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="项目类型">{{ activeProject.projectType || '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="负责人">{{ activeProject.functionary || '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="开始时间">{{ parseTime(activeProject.beginTime, '{y}-{m}-{d}') || '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="计划完成">{{ parseTime(activeProject.finishTime, '{y}-{m}-{d}') || '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="项目地址" :span="2">{{ activeProject.address || '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="项目资金">{{ activeProject.funds ? activeProject.funds + ' 元' : '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="交付说明" :span="3">{{ activeProject.delivery || '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="项目简介" :span="3">{{ activeProject.introduction || '—' }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
<!-- 快速统计 -->
|
||
<div class="section-title" style="margin-top:20px">关联统计</div>
|
||
<el-row :gutter="12">
|
||
<el-col v-for="m in MODULES.filter(x => x.key !== 'info')" :key="m.key" :span="4">
|
||
<div class="stat-mini" @click="switchTab(m.key)">
|
||
<i :class="m.icon" class="stat-mini-icon" />
|
||
<div class="stat-mini-num">{{ badgeCounts[m.key] !== undefined ? badgeCounts[m.key] : '—' }}</div>
|
||
<div class="stat-mini-label">{{ m.label }}</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
</template>
|
||
|
||
<!-- ── 合同 ── -->
|
||
<template v-else-if="activeTab === 'contract'">
|
||
<div class="section-title">合同记录</div>
|
||
<el-table :data="moduleData.contract" size="small" border stripe>
|
||
<el-table-column label="合同编号" prop="contractNum" width="140" />
|
||
<el-table-column label="合同名称" prop="contractName" min-width="160" show-overflow-tooltip />
|
||
<el-table-column label="合同金额(元)" prop="contractPrice" width="120" align="right">
|
||
<template slot-scope="{ row }"><span class="money">{{ row.contractPrice }}</span></template>
|
||
</el-table-column>
|
||
<el-table-column label="签订时间" prop="signTime" width="110">
|
||
<template slot-scope="{ row }">{{ parseTime(row.signTime, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="有效期" prop="validity" width="110" />
|
||
<el-table-column label="合同类型" prop="contractType" width="90" align="center" />
|
||
<el-table-column label="状态" prop="contractStatus" width="80" align="center" />
|
||
<el-table-column label="甲方单位" prop="firstName" width="120" show-overflow-tooltip />
|
||
<el-table-column label="甲方负责人" prop="firstPerson" width="90" align="center" />
|
||
<el-table-column label="甲方电话" prop="firstPhone" width="120" />
|
||
<el-table-column label="乙方单位" prop="secondName" width="120" show-overflow-tooltip />
|
||
<el-table-column label="乙方负责人" prop="secondPerson" width="90" align="center" />
|
||
<el-table-column label="乙方电话" prop="secondPhone" width="120" />
|
||
<el-table-column label="创建人" prop="createBy" width="80" align="center" />
|
||
<el-table-column label="创建时间" prop="createTime" width="110">
|
||
<template slot-scope="{ row }">{{ parseTime(row.createTime, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="附件" width="80" align="center" fixed="right">
|
||
<template slot-scope="{ row }">
|
||
<el-button v-if="row.accessory" type="text" size="mini" icon="el-icon-download" @click="$download.oss(row.accessory)">下载</el-button>
|
||
<span v-else style="color:#c0c4cc">无</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- ── 收付款记录 ── -->
|
||
<template v-else-if="activeTab === 'finance'">
|
||
<div class="section-title">收付款记录</div>
|
||
<el-table :data="moduleData.finance" size="small" border stripe>
|
||
<el-table-column label="账务名称" prop="financeTitle" min-width="150" show-overflow-tooltip />
|
||
<el-table-column label="收/付款类型" width="90" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span :style="{ color: String(row.financeType) === '0' ? '#67C23A' : '#F56C6C' }">
|
||
{{ String(row.financeType) === '0' ? '收款' : String(row.financeType) === '1' ? '付款' : row.financeType || '—' }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="付款方式" width="100" align="center">
|
||
<template slot-scope="{ row }">{{ payTypeLabel(row.payType) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="付款金额(元)" width="120" align="right">
|
||
<template slot-scope="{ row }"><span class="money">{{ sumDetailPrice(row) }}</span></template>
|
||
</el-table-column>
|
||
<el-table-column label="开票比例" prop="makeRatio" width="90" align="center" />
|
||
<el-table-column label="交易时间" prop="financeTime" width="110">
|
||
<template slot-scope="{ row }">{{ parseTime(row.financeTime, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="经手人/付款方" prop="financeParties" width="120" show-overflow-tooltip />
|
||
<el-table-column label="收款账户" prop="receiveAccountName" width="120" show-overflow-tooltip />
|
||
<el-table-column label="签约公司" prop="signingCompany" width="120" show-overflow-tooltip />
|
||
<el-table-column label="状态" width="80" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span :style="{ color: row.status === 1 ? '#67C23A' : '#E6A23C' }">{{ row.status === 1 ? '已完成' : '待处理' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- ── 回款计划 ── -->
|
||
<template v-else-if="activeTab === 'payment'">
|
||
<div class="section-title">回款计划</div>
|
||
<el-table :data="moduleData.payment" size="small" border stripe>
|
||
<el-table-column label="计划开始" prop="startTime" width="110">
|
||
<template slot-scope="{ row }">{{ parseTime(row.startTime, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="计划结束" prop="endTime" width="110">
|
||
<template slot-scope="{ row }">{{ parseTime(row.endTime, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="最早结束时间" prop="earliestEndTime" width="120">
|
||
<template slot-scope="{ row }">{{ parseTime(row.earliestEndTime, '{y}-{m}-{d}') || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="金额(元)" prop="amount" width="120" align="right">
|
||
<template slot-scope="{ row }"><span class="money">{{ row.amount }}</span></template>
|
||
</el-table-column>
|
||
<el-table-column label="完成状态" width="90" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span :style="{ color: row.complete === 1 ? '#67C23A' : '#E6A23C' }">{{ row.complete === 1 ? '已完成' : '待回款' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="完成资格" width="90" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span :style="{ color: row.qualified === 1 ? '#67C23A' : '#909399' }">{{ row.qualified === 1 ? '已具备' : '未具备' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="是否作废" width="80" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span :style="{ color: row.isVoid === 1 ? '#F56C6C' : '#67C23A' }">{{ row.isVoid === 1 ? '已作废' : '有效' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- ── 成本 ── -->
|
||
<template v-else-if="activeTab === 'cost'">
|
||
<div class="section-title">成本明细</div>
|
||
<el-table :data="moduleData.cost" size="small" border stripe>
|
||
<el-table-column label="成本类型" width="100" align="center">
|
||
<template slot-scope="{ row }">{{ costTypeLabel(row.costType) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="金额(元)" prop="cost" width="130" align="right">
|
||
<template slot-scope="{ row }"><span class="money">{{ row.cost }}</span></template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" prop="remark" min-width="200" show-overflow-tooltip />
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- ── 报销 ── -->
|
||
<template v-else-if="activeTab === 'claim'">
|
||
<div class="section-title">报销记录</div>
|
||
<el-table :data="moduleData.claim" size="small" border stripe>
|
||
<el-table-column label="出发时间" prop="startTime" width="110">
|
||
<template slot-scope="{ row }">{{ parseTime(row.startTime, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="返回时间" prop="endTime" width="110">
|
||
<template slot-scope="{ row }">{{ parseTime(row.endTime, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="天数" prop="tripDays" width="60" align="center" />
|
||
<el-table-column label="报销金额(元)" prop="cost" width="120" align="right">
|
||
<template slot-scope="{ row }"><span class="money">{{ row.cost }}</span></template>
|
||
</el-table-column>
|
||
<el-table-column label="目的地" prop="address" min-width="120" show-overflow-tooltip />
|
||
<el-table-column label="票据数" prop="detailNumber" width="70" align="center" />
|
||
<el-table-column label="审批状态" width="90" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span :style="{ color: row.status === 1 ? '#67C23A' : row.status === 2 ? '#F56C6C' : '#E6A23C' }">
|
||
{{ row.status === 1 ? '已通过' : row.status === 2 ? '已驳回' : '审批中' }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="完成时间" prop="completedTime" width="110">
|
||
<template slot-scope="{ row }">{{ parseTime(row.completedTime, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- ── 奖金分配 ── -->
|
||
<template v-else-if="activeTab === 'bonus'">
|
||
<div class="section-title">奖金池分配</div>
|
||
<el-table :data="moduleData.bonus" size="small" border stripe>
|
||
<el-table-column label="奖金池ID" prop="poolId" width="100" align="center" />
|
||
<el-table-column label="分配金额" prop="allocatedAmount" width="130" align="right">
|
||
<template slot-scope="{ row }"><span class="money">{{ row.allocatedAmount }}</span></template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" prop="remark" min-width="200" show-overflow-tooltip />
|
||
<el-table-column label="创建时间" prop="createTime" width="150">
|
||
<template slot-scope="{ row }">{{ parseTime(row.createTime, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- ── 项目进度 ── -->
|
||
<template v-else-if="activeTab === 'progress'">
|
||
<div class="section-title">项目进度</div>
|
||
<div class="progress-tree">
|
||
<div v-if="!moduleData.progress || moduleData.progress.length === 0" class="empty-tip">暂无进度数据</div>
|
||
<template v-for="top in progressTree">
|
||
<div :key="'t-' + top.progressId" class="pt-group">
|
||
<div class="pt-top">
|
||
<i class="el-icon-folder" style="color:#409EFF;margin-right:6px" />
|
||
<span class="pt-top-name">{{ top.progressName }}</span>
|
||
<span class="pt-status" :class="'ps-' + top.status">{{ progressStatusLabel(top.status) }}</span>
|
||
</div>
|
||
<div v-for="sub in top.children" :key="'s-' + sub.progressId" class="pt-sub">
|
||
<i class="el-icon-caret-right" style="color:#909399;margin-right:4px;font-size:11px" />
|
||
<span class="pt-sub-name">{{ sub.progressName }}</span>
|
||
<span class="pt-status" :class="'ps-' + sub.status">{{ progressStatusLabel(sub.status) }}</span>
|
||
<span v-if="sub.amount" class="pt-amount">¥{{ sub.amount }}</span>
|
||
<span v-if="sub.timeRemark" class="pt-time-remark">{{ sub.timeRemark }}</span>
|
||
<!-- leaf tasks -->
|
||
<div v-for="leaf in sub.children" :key="'l-' + leaf.progressId" class="pt-leaf">
|
||
<i class="el-icon-document" style="color:#c0c4cc;margin-right:4px;font-size:11px" />
|
||
<span>{{ leaf.progressName }}</span>
|
||
<span class="pt-status" :class="'ps-' + leaf.status">{{ progressStatusLabel(leaf.status) }}</span>
|
||
<span v-if="leaf.amount" class="pt-amount">¥{{ leaf.amount }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ── 任务 ── -->
|
||
<template v-else-if="activeTab === 'task'">
|
||
<div class="section-title">任务列表</div>
|
||
<el-table :data="moduleData.task" size="small" border stripe>
|
||
<el-table-column label="任务标题" prop="taskTitle" min-width="160" show-overflow-tooltip />
|
||
<el-table-column label="工作类型" width="100" align="center">
|
||
<template slot-scope="{ row }">{{ workTypeLabel(row.taskType) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="优先级" width="70" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span :style="{ color: String(row.taskGrade) === '2' ? '#F56C6C' : String(row.taskGrade) === '1' ? '#E6A23C' : '#909399' }">
|
||
{{ String(row.taskGrade) === '2' ? '紧急' : String(row.taskGrade) === '1' ? '中等' : '一般' }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="80" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span :style="{ color: Number(row.state) === 2 ? '#67C23A' : Number(row.state) === 1 ? '#E6A23C' : '#409EFF' }">
|
||
{{ Number(row.state) === 2 ? '已完成' : Number(row.state) === 1 ? '待验收' : '进行中' }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="执行人" prop="workerNickName" width="80" align="center" />
|
||
<el-table-column label="协作人员" prop="collaborator" width="100" show-overflow-tooltip />
|
||
<el-table-column label="部门" prop="deptName" width="100" show-overflow-tooltip />
|
||
<el-table-column label="开始" prop="beginTime" width="100">
|
||
<template slot-scope="{ row }">{{ parseTime(row.beginTime, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="截止" prop="finishTime" width="100">
|
||
<template slot-scope="{ row }">{{ parseTime(row.finishTime, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="完成时间" prop="completedTime" width="100">
|
||
<template slot-scope="{ row }">{{ parseTime(row.completedTime, '{y}-{m}-{d}') || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="逾期天数" prop="overDays" width="80" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span v-if="row.overDays > 0" style="color:#F56C6C">{{ row.overDays }}天</span>
|
||
<span v-else style="color:#67C23A">—</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="延期次数" prop="postponements" width="80" align="center" />
|
||
<el-table-column label="发起人" prop="createUserNickName" width="80" align="center" />
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- ── 采购需求 ── -->
|
||
<template v-else-if="activeTab === 'require'">
|
||
<div class="section-title">采购需求</div>
|
||
<el-table :data="moduleData.require" size="small" border stripe>
|
||
<el-table-column label="标题" prop="title" min-width="160" show-overflow-tooltip />
|
||
<el-table-column label="需求描述" prop="description" min-width="160" show-overflow-tooltip />
|
||
<el-table-column label="需求方" prop="requesterNickName" width="90" align="center" />
|
||
<el-table-column label="负责人" prop="ownerNickName" width="90" align="center" />
|
||
<el-table-column label="状态" width="90" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span :style="{ color: row.status === 2 ? '#67C23A' : row.status === 1 ? '#409EFF' : '#E6A23C' }">
|
||
{{ row.status === 2 ? '已完成' : row.status === 1 ? '进行中' : '待处理' }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="截止日期" prop="deadline" width="110">
|
||
<template slot-scope="{ row }">{{ parseTime(row.deadline, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="所属项目" prop="projectName" min-width="140" show-overflow-tooltip />
|
||
<el-table-column label="创建人" prop="createBy" width="90" align="center" />
|
||
<el-table-column label="创建时间" prop="createTime" width="110">
|
||
<template slot-scope="{ row }">{{ parseTime(row.createTime, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="最后更新人" prop="updateBy" width="90" align="center" />
|
||
<el-table-column label="更新时间" prop="updateTime" width="110">
|
||
<template slot-scope="{ row }">{{ parseTime(row.updateTime, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- ── 发货单 ── -->
|
||
<template v-else-if="activeTab === 'delivery'">
|
||
<div class="section-title">发货单</div>
|
||
<el-table :data="moduleData.delivery" size="small" border stripe>
|
||
<el-table-column label="发货单号" prop="deliveryOrderNo" width="200" show-overflow-tooltip />
|
||
<el-table-column label="状态" width="80" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span :style="{ color: row.status === 1 ? '#67C23A' : '#E6A23C' }">{{ row.status === 1 ? '已完成' : '进行中' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="合同号" prop="contractNo" width="110" show-overflow-tooltip />
|
||
<el-table-column label="用户合同号" prop="userContractNo" width="120" show-overflow-tooltip />
|
||
<el-table-column label="采购合同号" prop="procurementNo" width="120" show-overflow-tooltip />
|
||
<el-table-column label="送达目的地" prop="deliveryDestination" min-width="120" show-overflow-tooltip />
|
||
<el-table-column label="送货地址" prop="deliveryAddress" min-width="120" show-overflow-tooltip />
|
||
<el-table-column label="收货单位" prop="receivingCompany" min-width="120" show-overflow-tooltip />
|
||
<el-table-column label="收货人" prop="receiver" width="90" align="center" />
|
||
<el-table-column label="实际收货人" prop="actualReceiver" width="90" align="center" />
|
||
<el-table-column label="收货电话" prop="receiverPhone" width="120" />
|
||
<el-table-column label="供应商" prop="supplierFullname" min-width="120" show-overflow-tooltip />
|
||
<el-table-column label="供应商联系人" prop="supplierContact" width="100" align="center" />
|
||
<el-table-column label="供应商电话" prop="supplierPhone" width="120" />
|
||
<el-table-column label="施工单位" prop="constructionCompany" min-width="120" show-overflow-tooltip />
|
||
<el-table-column label="送货备注" prop="deliveryRemark" min-width="120" show-overflow-tooltip />
|
||
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
|
||
<el-table-column label="附件" width="80" align="center" fixed="right">
|
||
<template slot-scope="{ row }">
|
||
<el-button v-if="row.accessory" type="text" size="mini" icon="el-icon-download" @click="$download.oss(row.accessory)">下载</el-button>
|
||
<span v-else style="color:#c0c4cc">无</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- ── 快递 ── -->
|
||
<template v-else-if="activeTab === 'express'">
|
||
<div class="section-title">快递记录</div>
|
||
<el-table :data="moduleData.express" size="small" border stripe>
|
||
<el-table-column label="快递单号" prop="expressCode" width="160" />
|
||
<el-table-column label="快递类型" prop="expressType" width="90" align="center" />
|
||
<el-table-column label="状态" width="90" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span :style="{ color: row.status === 2 ? '#67C23A' : row.status === 1 ? '#409EFF' : '#909399' }">{{ expressStatusLabel(row.status) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="供应商" prop="supplyName" width="120" show-overflow-tooltip />
|
||
<el-table-column label="供应商电话" prop="supplyPhone" width="120" />
|
||
<el-table-column label="负责人" prop="ownerName" width="90" align="center" />
|
||
<el-table-column label="负责人电话" prop="ownerPhone" width="120" />
|
||
<el-table-column label="计划到货" prop="planDate" width="110">
|
||
<template slot-scope="{ row }">{{ parseTime(row.planDate, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="接收时间" prop="acceptTime" width="110">
|
||
<template slot-scope="{ row }">{{ parseTime(row.acceptTime, '{y}-{m}-{d}') || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="物流状态" prop="firstStatusName" width="100" show-overflow-tooltip />
|
||
<el-table-column label="当前节点" prop="lastStatus" min-width="140" show-overflow-tooltip />
|
||
<el-table-column label="关联发货记录" prop="detailName" min-width="120" show-overflow-tooltip />
|
||
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- ── 流程卡 ── -->
|
||
<template v-else-if="activeTab === 'processCard'">
|
||
<div class="section-title">制造流程卡</div>
|
||
<el-table :data="moduleData.processCard" size="small" border stripe>
|
||
<el-table-column label="设备名称" prop="equipmentName" min-width="160" show-overflow-tooltip />
|
||
<el-table-column label="数量" prop="equipmentQuantity" width="70" align="center" />
|
||
<el-table-column label="制造负责人" prop="manufacturingLeader" width="110" align="center" />
|
||
<el-table-column label="运营负责人" prop="operationLeader" width="110" align="center" />
|
||
<el-table-column label="计划交货日期" prop="plannedDeliveryDate" width="120">
|
||
<template slot-scope="{ row }">{{ parseTime(row.plannedDeliveryDate, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- ── 工作报告 ── -->
|
||
<template v-else-if="activeTab === 'report'">
|
||
<div class="section-title">工作报告</div>
|
||
<el-table :data="moduleData.report" size="small" border stripe>
|
||
<el-table-column label="报工人" prop="nickName" width="90" align="center" />
|
||
<el-table-column label="部门" prop="deptName" width="100" show-overflow-tooltip />
|
||
<el-table-column label="工作地点" prop="workPlace" width="120" show-overflow-tooltip />
|
||
<el-table-column label="国内/国外" width="90" align="center">
|
||
<template slot-scope="{ row }">{{ row.workTypeName || (row.workType === 0 ? '国内' : row.workType === 1 ? '国外' : '—') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="是否出差" width="80" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span :style="{ color: row.isTrip === 1 ? '#E6A23C' : '#909399' }">{{ row.isTrip === 1 ? '出差' : '未出差' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="国内工时" prop="inWorkNum" width="80" align="center" />
|
||
<el-table-column label="国外工时" prop="outWorkNum" width="80" align="center" />
|
||
<el-table-column label="报工内容" prop="content" min-width="200" show-overflow-tooltip>
|
||
<template slot-scope="{ row }"><span v-html="row.content" class="html-preview" /></template>
|
||
</el-table-column>
|
||
<el-table-column label="报工时间" prop="createTime" width="150">
|
||
<template slot-scope="{ row }">{{ parseTime(row.createTime, '{y}-{m}-{d} {h}:{i}') }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- ── 汇报摘要 ── -->
|
||
<template v-else-if="activeTab === 'summary'">
|
||
<div class="section-title">汇报摘要</div>
|
||
<el-table :data="moduleData.summary" size="small" border stripe>
|
||
<el-table-column label="汇报标题" prop="reportTitle" min-width="160" show-overflow-tooltip />
|
||
<el-table-column label="汇报日期" prop="reportDate" width="110">
|
||
<template slot-scope="{ row }">{{ parseTime(row.reportDate, '{y}-{m}-{d}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="汇报人" prop="reporter" width="90" align="center" />
|
||
<el-table-column label="状态" width="80" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span :style="{ color: row.status === 1 ? '#67C23A' : '#409EFF' }">{{ row.status === 1 ? '已完成' : '进行中' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="最近更新" prop="lastUpdateTime" width="150">
|
||
<template slot-scope="{ row }">{{ parseTime(row.lastUpdateTime, '{y}-{m}-{d} {h}:{i}') || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- ── 文件操作记录 ── -->
|
||
<template v-else-if="activeTab === 'files'">
|
||
<div class="section-title">文件操作记录</div>
|
||
<el-table :data="moduleData.files" size="small" border stripe>
|
||
<el-table-column label="文件名" prop="fileName" min-width="180" show-overflow-tooltip />
|
||
<el-table-column label="一级节点" prop="firstLevelNode" width="110" show-overflow-tooltip />
|
||
<el-table-column label="二级节点" prop="secondLevelNode" width="110" show-overflow-tooltip />
|
||
<el-table-column label="操作类型" width="80" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span :style="{ color: row.type === 1 ? '#409EFF' : '#F56C6C' }">{{ row.type === 1 ? '上传' : '删除' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作人" prop="operatorName" width="90" align="center" />
|
||
<el-table-column label="操作时间" prop="createTime" width="150">
|
||
<template slot-scope="{ row }">{{ parseTime(row.createTime, '{y}-{m}-{d} {h}:{i}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
|
||
<el-table-column label="下载" width="80" align="center" fixed="right">
|
||
<template slot-scope="{ row }">
|
||
<el-button v-if="row.fileId" type="text" size="mini" icon="el-icon-download" @click="$download.oss(row.fileId)">下载</el-button>
|
||
<span v-else style="color:#c0c4cc">—</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- ── 问题反馈 ── -->
|
||
<template v-else-if="activeTab === 'feedback'">
|
||
<div class="section-title">问题反馈</div>
|
||
<el-table :data="moduleData.feedback" size="small" border stripe>
|
||
<el-table-column label="标题" prop="title" min-width="160" show-overflow-tooltip />
|
||
<el-table-column label="类型" prop="type" width="90" align="center">
|
||
<template slot-scope="{ row }">{{ row.type || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="90" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span :style="{ color: row.status === 1 ? '#67C23A' : '#E6A23C' }">{{ row.status === 1 ? '已解决' : '待处理' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="反馈内容" prop="content" min-width="200" show-overflow-tooltip />
|
||
<el-table-column label="提交人" prop="createBy" width="90" align="center" />
|
||
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
|
||
<el-table-column label="创建时间" prop="createTime" width="150">
|
||
<template slot-scope="{ row }">{{ parseTime(row.createTime, '{y}-{m}-{d} {h}:{i}') }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- ── 操作历史 ── -->
|
||
<template v-else-if="activeTab === 'oplog'">
|
||
<div class="section-title">操作历史</div>
|
||
<el-table :data="moduleData.oplog" size="small" border stripe>
|
||
<el-table-column label="操作时间" prop="operateTime" width="150">
|
||
<template slot-scope="{ row }">{{ parseTime(row.operateTime, '{y}-{m}-{d} {h}:{i}') }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作对象" width="90" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span class="badge-tag" :style="{ color: targetTypeColor(row.targetType), borderColor: targetTypeColor(row.targetType) }">{{ targetTypeLabel(row.targetType) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="对象名称" prop="targetName" width="130" show-overflow-tooltip />
|
||
<el-table-column label="操作类型" width="90" align="center">
|
||
<template slot-scope="{ row }">
|
||
<span class="badge-tag" :style="{ color: opTypeColor(row.operationType), borderColor: opTypeColor(row.operationType) }">{{ opTypeLabel(row.operationType) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作描述" prop="operationDesc" min-width="200" show-overflow-tooltip />
|
||
<el-table-column label="操作人" prop="operator" width="90" align="center" />
|
||
</el-table>
|
||
</template>
|
||
|
||
<!-- 分页 -->
|
||
<div class="right-pagination" v-if="activeTab !== 'info' && activeTab !== 'progress'">
|
||
<pagination
|
||
v-show="modulePaging[activeTab] && modulePaging[activeTab].total > 0"
|
||
:total="modulePaging[activeTab] ? modulePaging[activeTab].total : 0"
|
||
:page.sync="modulePaging[activeTab].pageNum"
|
||
:limit.sync="modulePaging[activeTab].pageSize"
|
||
layout="total, prev, pager, next"
|
||
@pagination="reloadCurrentTab"
|
||
/>
|
||
</div>
|
||
|
||
</div><!-- /panel-right-body -->
|
||
</div>
|
||
|
||
<!-- 未选中项目时的占位 -->
|
||
<div class="panel-right panel-empty" v-else>
|
||
<i class="el-icon-office-building" style="font-size:48px;color:#dcdfe6" />
|
||
<p>请从左侧选择一个项目</p>
|
||
</div>
|
||
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { listProject, getProject } from '@/api/oa/project'
|
||
import { listOaContract } from '@/api/oa/oaContract'
|
||
import { listFinancePro } from '@/api/oa/finance'
|
||
import request from '@/utils/request'
|
||
// /oa/cost/list1 支持 projectId 过滤(list 是全项目汇总,不能用)
|
||
function listCostByProject (params) {
|
||
return request({ url: '/oa/cost/list1', method: 'get', params })
|
||
}
|
||
import { getPaymentProgress } from '@/api/oa/finance/progress'
|
||
import { listOaClaim } from '@/api/oa/claim'
|
||
import { listBonusProjectRel } from '@/api/oa/bonusProjectRel'
|
||
import { listProgress } from '@/api/oa/progress'
|
||
import { listTask } from '@/api/oa/task'
|
||
import { listRequirements } from '@/api/oa/requirement'
|
||
import { listDeliveryOrder } from '@/api/oa/workshop/deliveryOrder'
|
||
import { listExpress } from '@/api/oa/express'
|
||
import { listProcessCard } from '@/api/oa/workshop/processCard'
|
||
import { listProjectReport } from '@/api/oa/projectReport'
|
||
import { listReportSummary } from '@/api/oa/reportSummary'
|
||
import { listFileOperationRecord } from '@/api/oa/fileOperationRecord'
|
||
import { listFeedback } from '@/api/oa/feedback'
|
||
import { listOperationLog } from '@/api/oa/projectOperationLog'
|
||
|
||
const MODULES = [
|
||
{ key: 'info', label: '基本信息', icon: 'el-icon-office-building' },
|
||
{ key: 'contract', label: '合同', icon: 'el-icon-document' },
|
||
{ key: 'finance', label: '收付款记录', icon: 'el-icon-bank-card' },
|
||
{ key: 'payment', label: '回款计划', icon: 'el-icon-money' },
|
||
{ key: 'cost', label: '成本', icon: 'el-icon-coin' },
|
||
{ key: 'claim', label: '报销', icon: 'el-icon-tickets' },
|
||
{ key: 'bonus', label: '奖金分配', icon: 'el-icon-present' },
|
||
{ key: 'progress', label: '项目进度', icon: 'el-icon-data-line' },
|
||
{ key: 'task', label: '任务', icon: 'el-icon-s-check' },
|
||
{ key: 'require', label: '采购需求', icon: 'el-icon-shopping-cart-2' },
|
||
{ key: 'delivery', label: '发货单', icon: 'el-icon-truck' },
|
||
{ key: 'express', label: '快递', icon: 'el-icon-box' },
|
||
{ key: 'processCard', label: '生产', icon: 'el-icon-printer' },
|
||
{ key: 'report', label: '工作报告', icon: 'el-icon-edit-outline' },
|
||
{ key: 'summary', label: '汇报摘要', icon: 'el-icon-collection' },
|
||
{ key: 'files', label: '文件记录', icon: 'el-icon-folder' },
|
||
{ key: 'feedback', label: '问题反馈', icon: 'el-icon-warning-outline' },
|
||
{ key: 'oplog', label: '操作历史', icon: 'el-icon-s-order' }
|
||
]
|
||
|
||
const TARGET_TYPES = [
|
||
{ value: 1, label: '项目进度', color: '#409EFF' },
|
||
{ value: 2, label: '进度步骤', color: '#67C23A' },
|
||
{ value: 3, label: '任务', color: '#E6A23C' },
|
||
{ value: 4, label: '延期申请', color: '#F56C6C' }
|
||
]
|
||
const OP_TYPES = [
|
||
{ value: 1, label: '新增', color: '#67C23A' },
|
||
{ value: 2, label: '修改', color: '#409EFF' },
|
||
{ value: 3, label: '删除', color: '#F56C6C' },
|
||
{ value: 4, label: '状态变更', color: '#E6A23C' },
|
||
{ value: 5, label: '完成', color: '#67C23A' },
|
||
{ value: 6, label: '申请延期', color: '#E6A23C' },
|
||
{ value: 7, label: '审批通过', color: '#67C23A' },
|
||
{ value: 8, label: '审批驳回', color: '#F56C6C' }
|
||
]
|
||
|
||
// 每个 tab 的数据加载函数工厂
|
||
function makePaging () { return { pageNum: 1, pageSize: 20, total: 0 } }
|
||
|
||
export default {
|
||
name: 'ProjectDispatch',
|
||
data () {
|
||
return {
|
||
MODULES,
|
||
projectLoading: false,
|
||
projectList: [],
|
||
filteredList: [],
|
||
searchText: '',
|
||
activeProject: null,
|
||
activeTab: 'info',
|
||
moduleData: {},
|
||
moduleLoading: {},
|
||
badgeCounts: {},
|
||
modulePaging: Object.fromEntries(MODULES.map(m => [m.key, makePaging()]))
|
||
}
|
||
},
|
||
computed: {
|
||
progressTree () {
|
||
const rows = this.moduleData.progress || []
|
||
const map = {}
|
||
rows.forEach(r => { map[r.progressId] = { ...r, children: [] } })
|
||
const roots = []
|
||
rows.forEach(r => {
|
||
if (r.parentId && map[r.parentId]) {
|
||
map[r.parentId].children.push(map[r.progressId])
|
||
} else {
|
||
roots.push(map[r.progressId])
|
||
}
|
||
})
|
||
return roots
|
||
}
|
||
},
|
||
created () {
|
||
this.loadProjects()
|
||
},
|
||
methods: {
|
||
// ── 项目列表 ──
|
||
loadProjects () {
|
||
this.projectLoading = true
|
||
listProject({ pageNum: 1, pageSize: 999 }).then(res => {
|
||
this.projectList = res.rows || []
|
||
this.filteredList = this.projectList
|
||
}).finally(() => { this.projectLoading = false })
|
||
},
|
||
filterProjects () {
|
||
const q = (this.searchText || '').trim().toLowerCase()
|
||
if (!q) { this.filteredList = this.projectList; return }
|
||
this.filteredList = this.projectList.filter(p =>
|
||
(p.projectName || '').toLowerCase().includes(q) ||
|
||
(p.projectNum || '').toLowerCase().includes(q) ||
|
||
(p.projectCode || '').toLowerCase().includes(q)
|
||
)
|
||
},
|
||
selectProject (p) {
|
||
if (this.activeProject && this.activeProject.projectId === p.projectId) return
|
||
this.activeProject = p
|
||
this.activeTab = 'info'
|
||
this.moduleData = {}
|
||
this.modulePaging = Object.fromEntries(MODULES.map(m => [m.key, makePaging()]))
|
||
this.badgeCounts = {}
|
||
this.loadAllBadges()
|
||
},
|
||
|
||
// ── Tab 切换 ──
|
||
switchTab (key) {
|
||
this.activeTab = key
|
||
if (key === 'info') return
|
||
if (this.moduleData[key] === undefined) {
|
||
this.fetchModule(key)
|
||
}
|
||
},
|
||
reloadCurrentTab () {
|
||
this.fetchModule(this.activeTab)
|
||
},
|
||
|
||
// ── 数据加载 ──
|
||
fetchModule (key) {
|
||
const pid = this.activeProject.projectId
|
||
const pg = this.modulePaging[key]
|
||
this.$set(this.moduleLoading, key, true)
|
||
|
||
const done = (rows, total) => {
|
||
this.$set(this.moduleData, key, rows || [])
|
||
if (pg) pg.total = total || (rows || []).length
|
||
this.$set(this.moduleLoading, key, false)
|
||
this.$set(this.badgeCounts, key, total || (rows || []).length)
|
||
}
|
||
const fail = () => { this.$set(this.moduleLoading, key, false) }
|
||
const params = { projectId: pid, pageNum: pg.pageNum, pageSize: pg.pageSize }
|
||
|
||
const loaders = {
|
||
contract: () => listOaContract(params).then(r => done(r.rows, r.total)).catch(fail),
|
||
finance: () => listFinancePro(params).then(r => done(r.rows, r.total)).catch(fail),
|
||
payment: () => getPaymentProgress(params).then(r => done(r.rows, r.total)).catch(fail),
|
||
cost: () => listCostByProject(params).then(r => done(r.rows, r.total)).catch(fail),
|
||
claim: () => listOaClaim(params).then(r => done(r.rows, r.total)).catch(fail),
|
||
bonus: () => listBonusProjectRel(params).then(r => done(r.rows, r.total)).catch(fail),
|
||
progress: () => listProgress(pid).then(r => { const rows = Array.isArray(r) ? r : (r.rows || r.data || []); done(rows, rows.length) }).catch(fail),
|
||
task: () => listTask(params).then(r => done(r.rows, r.total)).catch(fail),
|
||
require: () => listRequirements(params).then(r => done(r.rows, r.total)).catch(fail),
|
||
delivery: () => listDeliveryOrder(params).then(r => done(r.rows, r.total)).catch(fail),
|
||
express: () => listExpress(params).then(r => done(r.rows, r.total)).catch(fail),
|
||
processCard: () => listProcessCard(params).then(r => done(r.rows, r.total)).catch(fail),
|
||
report: () => listProjectReport(params).then(r => done(r.rows, r.total)).catch(fail),
|
||
summary: () => listReportSummary(params).then(r => done(r.rows, r.total)).catch(fail),
|
||
files: () => listFileOperationRecord(params).then(r => done(r.rows, r.total)).catch(fail),
|
||
feedback: () => listFeedback(params).then(r => done(r.rows, r.total)).catch(fail),
|
||
oplog: () => listOperationLog(params).then(r => done(r.rows, r.total)).catch(fail)
|
||
}
|
||
if (loaders[key]) loaders[key]()
|
||
},
|
||
|
||
// 并发加载所有 badge count(pageSize=1 只取 total)
|
||
loadAllBadges () {
|
||
const pid = this.activeProject.projectId
|
||
MODULES.filter(m => m.key !== 'info').forEach(m => {
|
||
this.$set(this.badgeCounts, m.key, undefined)
|
||
})
|
||
// use small pageSize for count
|
||
const cp = { projectId: pid, pageNum: 1, pageSize: 1 }
|
||
const setCount = (key, res) => {
|
||
this.$set(this.badgeCounts, key, res.total !== undefined ? res.total : (res.rows || []).length)
|
||
}
|
||
listOaContract(cp).then(r => setCount('contract', r)).catch(() => {})
|
||
listFinancePro(cp).then(r => setCount('finance', r)).catch(() => {})
|
||
getPaymentProgress(cp).then(r => setCount('payment', r)).catch(() => {})
|
||
listCostByProject(cp).then(r => setCount('cost', r)).catch(() => {})
|
||
listOaClaim(cp).then(r => setCount('claim', r)).catch(() => {})
|
||
listBonusProjectRel(cp).then(r => setCount('bonus', r)).catch(() => {})
|
||
listProgress(pid).then(r => { const rows = Array.isArray(r) ? r : (r.rows || r.data || []); this.$set(this.badgeCounts, 'progress', rows.length) }).catch(() => {})
|
||
listTask(cp).then(r => setCount('task', r)).catch(() => {})
|
||
listRequirements(cp).then(r => setCount('require', r)).catch(() => {})
|
||
listDeliveryOrder(cp).then(r => setCount('delivery', r)).catch(() => {})
|
||
listExpress(cp).then(r => setCount('express', r)).catch(() => {})
|
||
listProcessCard(cp).then(r => setCount('processCard', r)).catch(() => {})
|
||
listProjectReport(cp).then(r => setCount('report', r)).catch(() => {})
|
||
listReportSummary(cp).then(r => setCount('summary', r)).catch(() => {})
|
||
listFileOperationRecord(cp).then(r => setCount('files', r)).catch(() => {})
|
||
listFeedback(cp).then(r => setCount('feedback', r)).catch(() => {})
|
||
listOperationLog(cp).then(r => setCount('oplog', r)).catch(() => {})
|
||
},
|
||
|
||
// ── 辅助 ──
|
||
// sys_project_status 字典: '0'=进行中(primary), '1'=进度完成(success)
|
||
statusLabel (s) {
|
||
if (s === '0' || s === 0) return '进行中'
|
||
if (s === '1' || s === 1) return '进度完成'
|
||
return s || '—'
|
||
},
|
||
statusClass (s) {
|
||
if (s === '0' || s === 0) return 'st-active'
|
||
if (s === '1' || s === 1) return 'st-done'
|
||
return 'st-cancel'
|
||
},
|
||
costTypeLabel (t) {
|
||
return t === 1 ? '材料' : t === 2 ? '人工' : t === 3 ? '设备' : t === 4 ? '其他' : '—'
|
||
},
|
||
progressStatusLabel (s) {
|
||
return s === 1 ? '进行中' : s === 2 ? '已完成' : s === 0 ? '未开始' : '—'
|
||
},
|
||
expressStatusLabel (s) {
|
||
return s === 0 ? '待发货' : s === 1 ? '运输中' : s === 2 ? '已签收' : '—'
|
||
},
|
||
targetTypeLabel (v) { return (TARGET_TYPES.find(t => t.value === v) || {}).label || v },
|
||
targetTypeColor (v) { return (TARGET_TYPES.find(t => t.value === v) || {}).color || '#909399' },
|
||
opTypeLabel (v) { return (OP_TYPES.find(t => t.value === v) || {}).label || v },
|
||
opTypeColor (v) { return (OP_TYPES.find(t => t.value === v) || {}).color || '#909399' },
|
||
// sys_work_type 字典
|
||
workTypeLabel (v) {
|
||
const map = { 1: '需求沟通', 2: '方案策划', 3: '采购备货', 4: '收货验参', 5: '施工安装', 6: '质量管控', 7: '项目初验', 8: '项目终验', 9: '项目结算', 101: '客户接待' }
|
||
return map[Number(v)] || (v != null ? String(v) : '—')
|
||
},
|
||
// sys_pay_type 字典: 1=对公转账, 2=现金交易, 3=现金转账, 4=银行承兑
|
||
payTypeLabel (v) {
|
||
const map = { '1': '对公转账', '2': '现金交易', '3': '现金转账', '4': '银行承兑' }
|
||
return map[String(v)] || (v != null ? String(v) : '—')
|
||
},
|
||
// 计算 detailList 中所有 price 的加和
|
||
sumDetailPrice (row) {
|
||
const list = row.detailList
|
||
if (!Array.isArray(list) || list.length === 0) return row.makePrice || '—'
|
||
const total = list.reduce((acc, item) => acc + (parseFloat(item.price) || 0), 0)
|
||
return total.toFixed(2)
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* ══ 整体布局 ══ */
|
||
.dispatch-layout {
|
||
display: flex;
|
||
height: calc(100vh - 84px);
|
||
background: #f0f2f5;
|
||
overflow: hidden;
|
||
gap: 0;
|
||
}
|
||
|
||
/* ══ 左栏 ══ */
|
||
.panel-left {
|
||
width: 240px;
|
||
flex-shrink: 0;
|
||
background: #fff;
|
||
border-right: 1px solid #dcdfe6;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
.left-header {
|
||
padding: 14px 14px 8px;
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 6px;
|
||
flex-shrink: 0;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
.left-title { font-size: 14px; font-weight: 700; color: #303133; }
|
||
.left-count { font-size: 12px; color: #909399; }
|
||
.left-search { padding: 10px 10px 6px; flex-shrink: 0; }
|
||
.left-list { flex: 1; overflow-y: auto; }
|
||
.left-empty { text-align: center; color: #c0c4cc; padding: 40px 0; font-size: 13px; }
|
||
|
||
.project-item {
|
||
padding: 10px 12px;
|
||
cursor: pointer;
|
||
border-bottom: 1px solid #f5f5f5;
|
||
transition: background .15s;
|
||
}
|
||
.project-item:hover { background: #f5f7fa; }
|
||
.project-item.is-active { background: #ecf5ff; border-right: 3px solid #409EFF; }
|
||
|
||
.pi-top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 4px;
|
||
}
|
||
.pi-name {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
flex: 1;
|
||
margin-right: 6px;
|
||
}
|
||
.pi-status {
|
||
font-size: 11px;
|
||
padding: 1px 5px;
|
||
border-radius: 2px;
|
||
flex-shrink: 0;
|
||
border: 1px solid;
|
||
}
|
||
.st-active { color: #409EFF; border-color: #b3d8ff; background: #ecf5ff; }
|
||
.st-done { color: #67C23A; border-color: #c2e7b0; background: #f0f9eb; }
|
||
.st-pause { color: #E6A23C; border-color: #faecd8; background: #fdf6ec; }
|
||
.st-cancel { color: #909399; border-color: #e4e7ed; background: #f4f4f5; }
|
||
|
||
.pi-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
margin-top: 3px;
|
||
}
|
||
.pi-tag {
|
||
font-size: 11px;
|
||
color: #606266;
|
||
background: #f4f4f5;
|
||
border-radius: 2px;
|
||
padding: 1px 5px;
|
||
}
|
||
.pi-tag.accent { color: #409EFF; background: #ecf5ff; }
|
||
.pi-sub { font-size: 11px; color: #909399; }
|
||
|
||
/* ══ 中栏(模块 Tab) ══ */
|
||
.panel-mid {
|
||
width: 120px;
|
||
flex-shrink: 0;
|
||
background: #fff;
|
||
border-right: 1px solid #dcdfe6;
|
||
overflow-y: auto;
|
||
padding: 8px 0;
|
||
}
|
||
.mod-tab {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 9px 12px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: #606266;
|
||
position: relative;
|
||
transition: background .15s;
|
||
}
|
||
.mod-tab:hover { background: #f5f7fa; color: #303133; }
|
||
.mod-active { background: #ecf5ff; color: #409EFF; font-weight: 600; border-left: 3px solid #409EFF; }
|
||
.mod-icon { font-size: 13px; flex-shrink: 0; }
|
||
.mod-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.mod-badge {
|
||
font-size: 10px;
|
||
background: #F56C6C;
|
||
color: #fff;
|
||
border-radius: 8px;
|
||
padding: 0 4px;
|
||
min-width: 16px;
|
||
text-align: center;
|
||
line-height: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
.mod-badge-zero {
|
||
background: #e4e7ed;
|
||
color: #909399;
|
||
}
|
||
|
||
/* ══ 右栏(内容区) ══ */
|
||
.panel-right {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
background: #f0f2f5;
|
||
}
|
||
|
||
/* 当前项目固定头 */
|
||
.project-header-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 18px;
|
||
background: #fff;
|
||
border-bottom: 1px solid #dcdfe6;
|
||
flex-shrink: 0;
|
||
flex-wrap: wrap;
|
||
}
|
||
.project-header-icon { font-size: 15px; color: #409EFF; flex-shrink: 0; }
|
||
.project-header-name { font-size: 14px; font-weight: 700; color: #303133; margin-right: 4px; }
|
||
.project-header-tag { font-size: 11px; color: #606266; background: #f4f4f5; border-radius: 2px; padding: 1px 6px; flex-shrink: 0; }
|
||
.project-header-tag.accent { color: #409EFF; background: #ecf5ff; }
|
||
.project-header-status { font-size: 11px; padding: 1px 6px; border-radius: 2px; border: 1px solid; flex-shrink: 0; }
|
||
.project-header-sub { font-size: 12px; color: #909399; margin-left: 4px; }
|
||
|
||
/* 内容滚动区 */
|
||
.panel-right-body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 16px 18px;
|
||
}
|
||
.panel-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #c0c4cc;
|
||
font-size: 14px;
|
||
gap: 12px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: #303133;
|
||
margin-bottom: 12px;
|
||
padding-left: 8px;
|
||
border-left: 3px solid #409EFF;
|
||
}
|
||
|
||
/* ══ 统计卡片(基本信息页底部) ══ */
|
||
.stat-mini {
|
||
background: #fff;
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 4px;
|
||
padding: 12px 10px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
margin-bottom: 10px;
|
||
transition: border-color .2s;
|
||
}
|
||
.stat-mini:hover { border-color: #409EFF; }
|
||
.stat-mini-icon { font-size: 18px; color: #909399; }
|
||
.stat-mini-num { font-size: 18px; font-weight: 700; color: #303133; margin: 4px 0; }
|
||
.stat-mini-label { font-size: 11px; color: #909399; }
|
||
|
||
/* ══ 进度树 ══ */
|
||
.progress-tree { background: #fff; border: 1px solid #dcdfe6; border-radius: 4px; padding: 12px 16px; }
|
||
.empty-tip { text-align: center; color: #c0c4cc; padding: 30px 0; font-size: 13px; }
|
||
|
||
.pt-group { margin-bottom: 12px; border-bottom: 1px solid #f0f0f0; padding-bottom: 10px; }
|
||
.pt-top { display: flex; align-items: center; padding: 6px 0; font-size: 13px; font-weight: 600; color: #303133; }
|
||
.pt-top-name { flex: 1; }
|
||
|
||
.pt-sub { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; padding: 4px 0 4px 20px; font-size: 12px; color: #606266; }
|
||
.pt-sub-name { font-weight: 500; color: #303133; }
|
||
|
||
.pt-leaf { display: flex; align-items: center; gap: 6px; padding: 3px 0 3px 40px; font-size: 12px; color: #909399; width: 100%; }
|
||
|
||
.pt-status { font-size: 11px; padding: 1px 5px; border-radius: 2px; flex-shrink: 0; }
|
||
.ps-0 { color: #909399; background: #f4f4f5; }
|
||
.ps-1 { color: #409EFF; background: #ecf5ff; }
|
||
.ps-2 { color: #67C23A; background: #f0f9eb; }
|
||
|
||
.pt-amount { font-size: 11px; color: #E6A23C; background: #fdf6ec; padding: 1px 5px; border-radius: 2px; }
|
||
.pt-time-remark { font-size: 11px; color: #909399; }
|
||
|
||
/* ══ 通用 ══ */
|
||
.money { color: #303133; font-weight: 600; }
|
||
.html-preview { font-size: 12px; line-height: 1.4; }
|
||
.right-pagination { margin-top: 12px; display: flex; justify-content: flex-end; }
|
||
|
||
.badge-tag {
|
||
display: inline-block;
|
||
padding: 0 5px;
|
||
height: 18px;
|
||
line-height: 16px;
|
||
border-radius: 2px;
|
||
border: 1px solid;
|
||
font-size: 11px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* el-table 放在灰色背景上,加白色外壳 */
|
||
.el-table { background: #fff; }
|
||
</style>
|