Files
fad_oa/ruoyi-ui/src/views/oa/project/dispatch/index.vue

1080 lines
55 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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 countpageSize=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>