Appearance
第三方接口对接
通过平台接口引擎安全调用第三方 HTTP 接口,以提供给前端调用
功能说明
避免在浏览器写死第三方密钥;密钥放后端脚本/配置。
一、概述
1.1 为什么需要平台封装?
核心问题: 如果直接在前端(浏览器)调用第三方 API,API 密钥(Key/Token)会暴露在客户端代码中,存在严重安全隐患。
解决方案: 平台提供「出站 HttpService」能力——由后端脚本封装第三方调用,前端仅通过平台内部接口触发执行。密钥全程存储在后端,浏览器不可见。
1.2 核心定位
| 维度 | 说明 |
|---|---|
| 前端角色 | 仅负责触发平台内部接口(Execute),不直接接触第三方 |
| 平台角色 | 作为代理层,负责密钥管理、请求封装、响应转发 |
| 后端角色 | 通过出站 HttpService 调用实际第三方 API,密钥仅存于此 |
⚠️ 安全原则: 任何第三方密钥(API Key、App Secret、Access Token)绝对禁止出现在前端代码、浏览器控制台、URL 参数或前端配置中。
二、架构原理
plain
┌─────────────────┐ ┌──────────────────────┐ ┌─────────────────┐
│ 浏览器 │ │ 平台应用 │ │ 第三方服务 │
│ (前端页面) │────▶│ 接口引擎 + Execute │────▶│ (外部 HTTP API) │
│ │ │ │ │ │
│ ❌ 不存密钥 │ │ ✅ 密钥存储后端 │ │ 微信/支付宝/ │
│ ❌ 不调外部 │ │ ✅ 出站 HttpService │ │ 短信/地图/ │
│ │ │ ✅ 响应转发前端 │ │ 其他 SaaS │
└─────────────────┘ └──────────────────────┘ └─────────────────┘
│ │
│ ▼
│ ┌──────────────────┐
│ │ 密钥管理区 │
│ │ appsettings / │
│ │ 后端脚本变量 │
│ └──────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 前端可见: 接口 ID、请求参数、平台返回结果 │
│ 前端不可见: API Key、App Secret、第三方 URL、签名逻辑 │
└─────────────────────────────────────────────────────────────────────┘2.1 数据流说明
plain
1. 前端页面触发 Execute(调用平台内部接口)
│
▼
2. 平台接口引擎接收请求,加载对应接口配置
│
▼
3. 后端脚本执行(context 调外部 API)
├─ 从 appsettings / 脚本变量读取密钥
├─ 构造 HTTP 请求(Header/Body/签名)
└─ 通过出站 HttpService 发送请求
│
▼
4. 第三方服务返回结果
│
▼
5. 后端脚本处理响应(脱敏/格式化)
│
▼
6. 平台将结果返回前端,前端渲染展示三、核心概念
3.1 名词解释
| 名词 | 说明 |
|---|---|
| 出站 HttpService | 后端脚本中用于调用外部 HTTP 接口的服务组件,封装了 HTTP Client 能力 |
| 接口引擎 | 平台内置的接口定义与执行引擎,支持可视化配置接口参数、脚本逻辑 |
| Execute | 前端调用平台接口的触发动作,通常绑定在按钮、表单提交等交互上 |
| smart-link | 特殊类型的出站接口,用于扫码登录等场景(如微信扫码、钉钉扫码) |
| context | 后端脚本执行上下文,包含请求参数、用户会话、HttpService 实例等 |
| appsettings | 平台配置文件,用于存储环境相关的密钥、URL 等敏感配置 |
3.2 关键约定
| 约定项 | 说明 |
|---|---|
| 密钥存储 | 所有第三方密钥必须存储在 appsettings 或后端脚本变量中,禁止硬编码 |
| 请求代理 | 前端不直接请求第三方域名,所有请求必须经过平台接口引擎中转 |
| 响应脱敏 | 后端脚本应对第三方响应进行必要脱敏,避免将原始密钥/Token 透传给前端 |
| 超时配置 | 出站请求必须设置合理的超时时间(建议 10~30 秒),防止第三方挂起导致平台阻塞 |
| 错误处理 | 第三方异常应包装为平台标准错误格式,前端统一处理 |
名词解释
| 名词 | 说明 |
|---|---|
| 出站 HttpService | 后端脚本调用外部 HTTP |
| smart-link | 扫码登录等接口 |
示例代码
前端
<template>
<div class="remote-page dashboard-light hw-trace-page">
<div class="page-top">
<el-alert type="info" :closable="false" show-icon class="top-alert">
<template #title>
<span>数据来源于国家危废平台(实时查询)</span>
<span v-if="enterpriseName" class="enterprise-name"> · {{ enterpriseName }}</span>
</template>
</el-alert>
<el-card class="search-card" shadow="never">
<el-form :model="searchForm" inline class="search-form">
<el-form-item label="废物名称">
<el-input v-model="searchForm.name" placeholder="请输入废物名称" clearable style="width: 180px;" />
</el-form-item>
<el-form-item label="废物代码">
<el-input v-model="searchForm.code" placeholder="请输入废物代码" clearable style="width: 180px;" />
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="searchForm.startDate"
type="date"
placeholder="年/月/日"
style="width: 140px;"
value-format="YYYY-MM-DD"
/>
<span style="margin: 0 8px;">至</span>
<el-date-picker
v-model="searchForm.endDate"
type="date"
placeholder="年/月/日"
style="width: 140px;"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="全部" clearable style="width: 120px;">
<el-option label="全部" value="" />
<el-option v-for="item in statusOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="tableLoading" @click="handleSearch">查 询</el-button>
<el-button :disabled="tableLoading" @click="handleReset">重 置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="flow-card" shadow="never">
<template #header>
<div class="card-header">
<span class="header-title">| 危废处理全过程流程</span>
</div>
</template>
<div v-loading="flowLoading" class="flow-container">
<div class="flow-column">
<div
class="flow-box clickable"
:class="{ active: activeTab === 'produce' }"
@click="switchTab('produce')"
>
<div class="flow-title">产生</div>
<div class="flow-data">
<div class="data-row">
<span class="data-value blue">{{ formatFlowTon(flowStats.produce.ton) }}</span>
<span class="data-unit">吨</span>
<span class="data-value blue">{{ formatFlowBarrel(flowStats.produce.barrel) }}</span>
<span class="data-unit">桶</span>
</div>
</div>
</div>
</div>
<div class="flow-arrow">
<el-icon><arrow-right /></el-icon>
</div>
<div class="flow-column">
<div
class="flow-box clickable"
:class="{ active: activeTab === 'storage' }"
@click="switchTab('storage')"
>
<div class="flow-title">贮存入库</div>
<div class="flow-data">
<div class="data-row">
<span class="data-value blue">{{ formatFlowTon(flowStats.storage.ton) }}</span>
<span class="data-unit">吨</span>
<span class="data-value blue">{{ formatFlowBarrel(flowStats.storage.barrel) }}</span>
<span class="data-unit">桶</span>
</div>
<div class="data-row sub">
<span class="data-label">库存</span>
<span class="data-value blue">{{ formatFlowTon(flowStats.storage.inventoryTon) }}</span>
<span class="data-unit">吨</span>
</div>
</div>
</div>
<div
class="flow-box clickable"
:class="{ active: activeTab === 'immediate' }"
@click="switchTab('immediate')"
>
<div class="flow-title">立产立清</div>
<div class="flow-data">
<div class="data-row">
<span class="data-value blue">{{ formatFlowTon(flowStats.immediate.ton) }}</span>
<span class="data-unit">吨</span>
<span class="data-value blue">{{ formatFlowBarrel(flowStats.immediate.barrel) }}</span>
<span class="data-unit">桶</span>
</div>
<div class="data-row sub">
<span class="data-label">未转移</span>
<span class="data-value blue">{{ formatFlowTon(flowStats.immediate.pendingTon) }}</span>
<span class="data-unit">吨</span>
</div>
</div>
</div>
</div>
<div class="flow-arrow">
<el-icon><arrow-right /></el-icon>
</div>
<div class="flow-column">
<div class="flow-label">贮存出库</div>
<div
class="flow-box clickable"
:class="{ active: activeTab === 'selfDispose' }"
@click="switchTab('selfDispose')"
>
<div class="flow-title">自处置出库</div>
<div class="flow-data">
<div class="data-row">
<span class="data-value blue">{{ formatFlowTon(flowStats.storageOutSelfDispose.ton) }}</span>
<span class="data-unit">吨</span>
<span class="data-value blue">{{ formatFlowBarrel(flowStats.storageOutSelfDispose.barrel) }}</span>
<span class="data-unit">桶</span>
</div>
</div>
</div>
<div
class="flow-box primary clickable"
:class="{ active: activeTab === 'transfer' }"
@click="switchTab('transfer')"
>
<div class="flow-title">转移</div>
<div class="flow-data">
<div class="data-row">
<span class="data-value white">{{ formatFlowTon(flowStats.storageOutTransfer.ton) }}</span>
<span class="data-unit white">吨</span>
<span class="data-value white">{{ formatFlowBarrel(flowStats.storageOutTransfer.barrel) }}</span>
<span class="data-unit white">桶</span>
</div>
</div>
</div>
<div class="flow-label">立产立清出库</div>
<div class="flow-box clickable">
<div class="flow-title">自处置</div>
<div class="flow-data">
<div class="data-row">
<span class="data-value blue">{{ formatFlowTon(flowStats.immediateOutSelfDispose.ton) }}</span>
<span class="data-unit">吨</span>
<span class="data-value blue">{{ formatFlowBarrel(flowStats.immediateOutSelfDispose.barrel) }}</span>
<span class="data-unit">桶</span>
</div>
</div>
</div>
<div class="flow-box clickable">
<div class="flow-title">转移</div>
<div class="flow-data">
<div class="data-row">
<span class="data-value blue">{{ formatFlowTon(flowStats.immediateOutTransfer.ton) }}</span>
<span class="data-unit">吨</span>
<span class="data-value blue">{{ formatFlowBarrel(flowStats.immediateOutTransfer.barrel) }}</span>
<span class="data-unit">桶</span>
</div>
</div>
</div>
</div>
<div class="flow-arrow">
<el-icon><arrow-right /></el-icon>
</div>
<div class="flow-column transfer-column">
<div class="transfer-box">
<div class="transfer-title">
<span class="title-text">转移联单</span>
<span class="title-tip">(请到联单管理菜单中查询)</span>
</div>
<div class="transfer-content">
<div class="transfer-item">
<div class="item-header">未办结</div>
<div class="item-body">
<span class="data-value blue">{{ formatFlowTon(flowStats.manifestStorage.pending.ton) }}</span>
<span class="data-unit">吨</span>
<span class="data-value blue">{{ formatFlowBarrel(flowStats.manifestStorage.pending.barrel) }}</span>
<span class="data-unit">桶</span>
</div>
</div>
<div class="transfer-arrow">
<el-icon><arrow-right /></el-icon>
</div>
<div class="transfer-item">
<div class="item-header">已办结</div>
<div class="item-body">
<span class="data-value blue">{{ formatFlowTon(flowStats.manifestStorage.done.ton) }}</span>
<span class="data-unit">吨</span>
<span class="data-value blue">{{ formatFlowBarrel(flowStats.manifestStorage.done.barrel) }}</span>
<span class="data-unit">桶</span>
</div>
</div>
</div>
</div>
<div class="transfer-box">
<div class="transfer-title">
<span class="title-text">转移联单</span>
<span class="title-tip">(请到联单管理菜单中查询)</span>
</div>
<div class="transfer-content">
<div class="transfer-item">
<div class="item-header">未办结</div>
<div class="item-body">
<span class="data-value blue">{{ formatFlowTon(flowStats.manifestImmediate.pending.ton) }}</span>
<span class="data-unit">吨</span>
<span class="data-value blue">{{ formatFlowBarrel(flowStats.manifestImmediate.pending.barrel) }}</span>
<span class="data-unit">桶</span>
</div>
</div>
<div class="transfer-arrow">
<el-icon><arrow-right /></el-icon>
</div>
<div class="transfer-item">
<div class="item-header">已办结</div>
<div class="item-body">
<span class="data-value blue">{{ formatFlowTon(flowStats.manifestImmediate.done.ton) }}</span>
<span class="data-unit">吨</span>
<span class="data-value blue">{{ formatFlowBarrel(flowStats.manifestImmediate.done.barrel) }}</span>
<span class="data-unit">桶</span>
</div>
</div>
</div>
</div>
</div>
</div>
</el-card>
</div>
<el-card ref="tableCardRef" class="table-card" shadow="never">
<el-tabs v-model="activeTab" type="border-card" class="data-tabs">
<el-tab-pane label="产生台账" name="produce">
<div class="tab-pane-body">
<el-table v-loading="tableLoading" :height="tableBodyHeight" :data="produceData" border stripe class="full-width-table">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="produceTime" label="产生时间" min-width="160" />
<el-table-column prop="batchNo" label="批次号" min-width="140" />
<el-table-column prop="digitalCode" label="数字识别码" min-width="160" />
<el-table-column prop="wasteName" label="废物俗称" min-width="140" />
<el-table-column prop="wasteCode" label="废物代码" min-width="160">
<template #default="{ row }">
<el-tag type="primary" size="small">{{ row.wasteCode }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="quantity" label="产生量(t)" min-width="100" align="right" />
<el-table-column prop="destination" label="去向" min-width="100" />
<el-table-column prop="status" label="状态" min-width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">{{ row.status || '—' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="dataSource" label="数据来源" min-width="100" />
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="openDetail('produce', row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.produce.currentPage"
v-model:page-size="pagination.produce.pageSize"
:total="pagination.produce.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="(page) => handlePageChange('produce', page)"
@size-change="(size) => handleSizeChange('produce', size)"
/>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="贮存入库台账" name="storage">
<div class="tab-pane-body">
<el-table v-loading="tableLoading" :height="tableBodyHeight" :data="storageData" border stripe class="full-width-table">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="status" label="状态" min-width="90">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">{{ row.status || '—' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="inTime" label="入库时间" min-width="160" />
<el-table-column prop="wasteName" label="废物俗称" min-width="140" />
<el-table-column prop="wasteCode" label="废物代码" min-width="160">
<template #default="{ row }">
<el-tag type="primary" size="small">{{ row.wasteCode }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="inQuantity" label="入库量(t)" min-width="100" align="right" />
<el-table-column prop="remainQuantity" label="剩余量(t)" min-width="100" align="right" />
<el-table-column prop="batchNo" label="入库批次" min-width="140" />
<el-table-column prop="facility" label="贮存设施" min-width="160" />
<el-table-column prop="handler" label="贮存经办人" min-width="120" />
<el-table-column prop="operator" label="录入人" min-width="100" />
<el-table-column prop="dataSource" label="数据来源" min-width="100" />
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="openDetail('storage', row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.storage.currentPage"
v-model:page-size="pagination.storage.pageSize"
:total="pagination.storage.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="(page) => handlePageChange('storage', page)"
@size-change="(size) => handleSizeChange('storage', size)"
/>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="立产立清台账" name="immediate">
<div class="tab-pane-body">
<el-table v-loading="tableLoading" :height="tableBodyHeight" :data="immediateData" border stripe class="full-width-table">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="batchNo" label="批次号" min-width="140" />
<el-table-column prop="wasteName" label="废物俗称" min-width="140" />
<el-table-column prop="wasteCode" label="废物代码" min-width="160">
<template #default="{ row }">
<el-tag type="primary" size="small">{{ row.wasteCode }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="disposeQuantity" label="利用处置/转移量(t)" min-width="160" align="right" />
<el-table-column prop="produceTime" label="产生时间" min-width="160" />
<el-table-column prop="status" label="状态" min-width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">{{ row.status || '—' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="dataSource" label="数据来源" min-width="100" />
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="openDetail('immediate', row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.immediate.currentPage"
v-model:page-size="pagination.immediate.pageSize"
:total="pagination.immediate.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="(page) => handlePageChange('immediate', page)"
@size-change="(size) => handleSizeChange('immediate', size)"
/>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="自处置出库台账" name="selfDispose">
<div class="tab-pane-body">
<el-table v-loading="tableLoading" :height="tableBodyHeight" :data="selfDisposeData" border stripe class="full-width-table">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="outTime" label="出库时间" min-width="120" />
<el-table-column prop="wasteName" label="废物俗称" min-width="140" />
<el-table-column prop="wasteCode" label="废物代码" min-width="160">
<template #default="{ row }">
<el-tag type="primary" size="small">{{ row.wasteCode }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="batchNo" label="出库批次" min-width="140" />
<el-table-column prop="outQuantity" label="出库量(t)" min-width="100" align="right" />
<el-table-column prop="destination" label="废物去向" min-width="120" />
<el-table-column prop="status" label="状态" min-width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">{{ row.status || '—' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="dataSource" label="数据来源" min-width="100" />
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="openDetail('selfDispose', row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.selfDispose.currentPage"
v-model:page-size="pagination.selfDispose.pageSize"
:total="pagination.selfDispose.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="(page) => handlePageChange('selfDispose', page)"
@size-change="(size) => handleSizeChange('selfDispose', size)"
/>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="转移出库台账" name="transfer">
<div class="tab-pane-body">
<el-table v-loading="tableLoading" :height="tableBodyHeight" :data="transferData" border stripe class="full-width-table">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="outTime" label="出库时间" min-width="160" />
<el-table-column prop="wasteName" label="废物俗称" min-width="140" />
<el-table-column prop="wasteCode" label="废物代码" min-width="160">
<template #default="{ row }">
<el-tag type="primary" size="small">{{ row.wasteCode }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="outType" label="出库类型" min-width="120" />
<el-table-column prop="batchNo" label="出库批次" min-width="140" />
<el-table-column prop="unionNo" label="联单编号" min-width="160" />
<el-table-column prop="outQuantity" label="出库量(t)" min-width="100" align="right" />
<el-table-column prop="operator" label="录入人" min-width="100" />
<el-table-column prop="dataSource" label="数据来源" min-width="100" />
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="openDetail('transfer', row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.transfer.currentPage"
v-model:page-size="pagination.transfer.pageSize"
:total="pagination.transfer.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="(page) => handlePageChange('transfer', page)"
@size-change="(size) => handleSizeChange('transfer', size)"
/>
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
<el-dialog
v-model="detailVisible"
:title="detailTitle"
width="1200px"
class="ledger-detail-dialog"
destroy-on-close
align-center
>
<div v-for="section in detailSections" :key="section.title" class="detail-section-box">
<div class="detail-section-head">{{ section.title }}</div>
<div class="detail-section-body">
<template v-for="(row, rowIndex) in section.rows" :key="`${section.title}-${rowIndex}`">
<div v-if="row.kind === 'full'" class="detail-row detail-row-full">
<div class="detail-field-label">{{ row.cell.label }}</div>
<div class="detail-field-value">{{ row.cell.value }}</div>
</div>
<div v-else-if="row.kind === 'single'" class="detail-row detail-row-full">
<div class="detail-field-label">{{ row.cell.label }}</div>
<div class="detail-field-value">{{ row.cell.value }}</div>
</div>
<div v-else class="detail-row detail-row-triple">
<div
v-for="(cell, cellIndex) in row.cells"
:key="`${section.title}-${rowIndex}-${cellIndex}`"
class="detail-field"
:class="{ 'is-empty': cell.empty }"
>
<div class="detail-field-label">{{ cell.label }}</div>
<div class="detail-field-value">{{ cell.empty ? '' : cell.value }}</div>
</div>
</div>
</template>
</div>
<div v-else class="detail-grid">
<div
v-for="item in section.items || []"
:key="item.label"
class="detail-cell"
:class="{ 'span-3': item.span === 3 }"
>
<div class="detail-label">{{ item.label }}</div>
<div class="detail-value">{{ item.value }}</div>
</div>
</div>
</div>
<template #footer>
<el-button @click="detailVisible = false">关 闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import { App as AppService } from '@/services/App'
import { ArrowRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const App = new AppService()
type TabName = 'produce' | 'storage' | 'immediate' | 'selfDispose' | 'transfer'
type RawRecord = Record<string, unknown>
type LedgerRow = Record<string, unknown> & { _raw?: RawRecord }
type DetailItem = { label: string; value: string; span?: 3 }
type DetailCell = { label: string; value: string; empty?: boolean }
type DetailRow =
| { kind: 'single'; cell: DetailCell }
| { kind: 'triple'; cells: [DetailCell, DetailCell, DetailCell] }
| { kind: 'full'; cell: DetailCell }
type DetailSection = {
title: string
rows?: DetailRow[]
items?: DetailItem[]
}
type FlowMetric = { ton: number; barrel: number }
type ManifestMetric = { pending: FlowMetric; done: FlowMetric }
function createEmptyFlowMetric(): FlowMetric {
return { ton: 0, barrel: 0 }
}
function createEmptyFlowStats() {
return {
produce: createEmptyFlowMetric(),
storage: { ...createEmptyFlowMetric(), inventoryTon: 0 },
immediate: { ...createEmptyFlowMetric(), pendingTon: 0 },
storageOutSelfDispose: createEmptyFlowMetric(),
storageOutTransfer: createEmptyFlowMetric(),
immediateOutSelfDispose: createEmptyFlowMetric(),
immediateOutTransfer: createEmptyFlowMetric(),
manifestStorage: { pending: createEmptyFlowMetric(), done: createEmptyFlowMetric() },
manifestImmediate: { pending: createEmptyFlowMetric(), done: createEmptyFlowMetric() },
}
}
const API = {
enterprise: '/api/code/v1/cswm/enterprise',
ledgerQuery: '/api/code/v1/cswm/ledger/query',
}
const TAB_LX: Record<TabName, string> = {
produce: 'cs',
storage: 'rk',
immediate: 'lclq',
selfDispose: 'zxlycz',
transfer: 'wwlycz',
}
const STATUS_OPTIONS: Record<TabName, string[]> = {
produce: ['待转移', '贮存中', '已转移', '已完成'],
storage: ['贮存中', '已出库'],
immediate: ['进行中', '已完成'],
selfDispose: ['进行中', '已完成'],
transfer: ['待出库', '已出库', '已作废'],
}
const searchForm = reactive({
name: '',
code: '',
startDate: '',
endDate: '',
status: '',
})
const statusOptions = computed(() => STATUS_OPTIONS[activeTab.value])
const activeTab = ref<TabName>('produce')
const tableLoading = ref(false)
const flowLoading = ref(false)
const enterpriseName = ref('')
const pagination = reactive({
produce: { currentPage: 1, pageSize: 10, total: 0 },
storage: { currentPage: 1, pageSize: 10, total: 0 },
immediate: { currentPage: 1, pageSize: 10, total: 0 },
selfDispose: { currentPage: 1, pageSize: 10, total: 0 },
transfer: { currentPage: 1, pageSize: 10, total: 0 },
})
const flowStats = reactive(createEmptyFlowStats())
const produceData = ref<LedgerRow[]>([])
const storageData = ref<LedgerRow[]>([])
const immediateData = ref<LedgerRow[]>([])
const selfDisposeData = ref<LedgerRow[]>([])
const transferData = ref<LedgerRow[]>([])
const detailVisible = ref(false)
const detailTitle = ref('')
const detailSections = ref<DetailSection[]>([])
const tableCardRef = ref<{ $el: HTMLElement } | null>(null)
const tableBodyHeight = ref(320)
const TAB_BODY_PADDING = 24
const PAGINATION_HEIGHT = 52
let tableResizeObserver: ResizeObserver | null = null
function updateTableBodyHeight(): void {
nextTick(() => {
const root = tableCardRef.value?.$el
if (!root) {
return
}
const cardBody = root.querySelector('.el-card__body') as HTMLElement | null
const tabsHeader = root.querySelector('.el-tabs__header') as HTMLElement | null
if (!cardBody || !tabsHeader) {
return
}
const available = cardBody.clientHeight - tabsHeader.offsetHeight - PAGINATION_HEIGHT - TAB_BODY_PADDING
tableBodyHeight.value = Math.max(available, 180)
})
}
function bindTableResizeObserver(): void {
const root = tableCardRef.value?.$el
if (!root || tableResizeObserver) {
return
}
tableResizeObserver = new ResizeObserver(() => {
updateTableBodyHeight()
})
tableResizeObserver.observe(root)
window.addEventListener('resize', updateTableBodyHeight)
updateTableBodyHeight()
}
function unbindTableResizeObserver(): void {
tableResizeObserver?.disconnect()
tableResizeObserver = null
window.removeEventListener('resize', updateTableBodyHeight)
}
const tabDataRefs: Record<TabName, typeof produceData> = {
produce: produceData,
storage: storageData,
immediate: immediateData,
selfDispose: selfDisposeData,
transfer: transferData,
}
function defaultDateRange(): { start: string; end: string } {
const now = new Date()
const y = now.getFullYear()
const m = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
return { start: `${y}-${m}-01`, end: `${y}-${m}-${day}` }
}
function applyDefaultDates(): void {
const range = defaultDateRange()
searchForm.startDate = range.start
searchForm.endDate = range.end
}
function formatFlowTon(value: number): string {
if (!Number.isFinite(value)) {
return '0.0000'
}
return value.toFixed(4)
}
function formatFlowBarrel(value: number): string {
if (!Number.isFinite(value)) {
return '0'
}
return String(Math.round(value))
}
function parseFlowNum(value: unknown): number {
const num = parseFloat(String(value ?? ''))
return Number.isFinite(num) ? num : 0
}
function parseFlowBarrel(value: unknown): number {
const num = parseInt(String(value ?? ''), 10)
return Number.isFinite(num) ? num : 0
}
function pickRawTon(raw: RawRecord, keys: string[]): number {
for (const key of keys) {
if (raw[key] != null && String(raw[key]) !== '') {
return parseFlowNum(raw[key])
}
}
return 0
}
function sumFlowMetric(rows: LedgerRow[], tonKeys: string[], barrelKey = 'rqbzsl'): FlowMetric {
const metric = createEmptyFlowMetric()
for (const row of rows) {
const raw = getRaw(row)
metric.ton += pickRawTon(raw, tonKeys)
metric.barrel += parseFlowBarrel(raw[barrelKey])
}
return metric
}
function hasUnionNo(row: LedgerRow): boolean {
const raw = getRaw(row)
const unionNo = String(raw.ldbh || row.unionNo || '').trim()
return unionNo !== ''
}
function isSelfDisposeRow(row: LedgerRow): boolean {
const raw = getRaw(row)
const text = String(raw.qx || raw.zxlyczfs || raw.cklx || raw.cklxmc || row.destination || '')
return text.includes('自行') || text === '3' || text.toUpperCase().includes('ZX')
}
function isManifestDone(row: LedgerRow): boolean {
const raw = getRaw(row)
const status = String(raw.ztmc || raw.zt || row.status || '')
if (status.includes('未办结') || status.includes('进行中') || status.includes('待')) {
return false
}
return status.includes('办结') || status.includes('完成') || status.includes('已转移')
}
function splitManifestRows(rows: LedgerRow[], tonKeys: string[]): ManifestMetric {
const result: ManifestMetric = {
pending: createEmptyFlowMetric(),
done: createEmptyFlowMetric(),
}
for (const row of rows) {
const raw = getRaw(row)
const ton = pickRawTon(raw, tonKeys)
const barrel = parseFlowBarrel(raw.rqbzsl)
const bucket = isManifestDone(row) ? result.done : result.pending
bucket.ton += ton
bucket.barrel += barrel
}
return result
}
function sumInventoryTon(rows: LedgerRow[]): number {
let total = 0
for (const row of rows) {
const raw = getRaw(row)
const remain = pickRawTon(raw, ['syll', 'kcl'])
if (remain > 0) {
total += remain
continue
}
if (row.remainQuantity != null && String(row.remainQuantity) !== '') {
total += parseFlowNum(row.remainQuantity)
}
}
return total
}
function analyzeImmediateRows(rows: LedgerRow[]) {
const total = createEmptyFlowMetric()
let pendingTon = 0
const selfDispose = createEmptyFlowMetric()
const transfer = createEmptyFlowMetric()
for (const row of rows) {
const raw = getRaw(row)
const ton = pickRawTon(raw, ['zxlyczl', 'csl', 'lyczl', 'zyl']) || parseFlowNum(row.disposeQuantity)
const barrel = parseFlowBarrel(raw.rqbzsl)
total.ton += ton
total.barrel += barrel
if (isSelfDisposeRow(row)) {
selfDispose.ton += ton
selfDispose.barrel += barrel
continue
}
if (hasUnionNo(row)) {
transfer.ton += ton
transfer.barrel += barrel
continue
}
pendingTon += ton
}
return {
total,
pendingTon,
selfDispose,
transfer,
}
}
function applyFlowStats(data: ReturnType<typeof createEmptyFlowStats>): void {
flowStats.produce.ton = data.produce.ton
flowStats.produce.barrel = data.produce.barrel
flowStats.storage.ton = data.storage.ton
flowStats.storage.barrel = data.storage.barrel
flowStats.storage.inventoryTon = data.storage.inventoryTon
flowStats.immediate.ton = data.immediate.ton
flowStats.immediate.barrel = data.immediate.barrel
flowStats.immediate.pendingTon = data.immediate.pendingTon
flowStats.storageOutSelfDispose.ton = data.storageOutSelfDispose.ton
flowStats.storageOutSelfDispose.barrel = data.storageOutSelfDispose.barrel
flowStats.storageOutTransfer.ton = data.storageOutTransfer.ton
flowStats.storageOutTransfer.barrel = data.storageOutTransfer.barrel
flowStats.immediateOutSelfDispose.ton = data.immediateOutSelfDispose.ton
flowStats.immediateOutSelfDispose.barrel = data.immediateOutSelfDispose.barrel
flowStats.immediateOutTransfer.ton = data.immediateOutTransfer.ton
flowStats.immediateOutTransfer.barrel = data.immediateOutTransfer.barrel
flowStats.manifestStorage.pending.ton = data.manifestStorage.pending.ton
flowStats.manifestStorage.pending.barrel = data.manifestStorage.pending.barrel
flowStats.manifestStorage.done.ton = data.manifestStorage.done.ton
flowStats.manifestStorage.done.barrel = data.manifestStorage.done.barrel
flowStats.manifestImmediate.pending.ton = data.manifestImmediate.pending.ton
flowStats.manifestImmediate.pending.barrel = data.manifestImmediate.pending.barrel
flowStats.manifestImmediate.done.ton = data.manifestImmediate.done.ton
flowStats.manifestImmediate.done.barrel = data.manifestImmediate.done.barrel
}
async function loadFlowStats(): Promise<void> {
if (!searchForm.startDate || !searchForm.endDate) {
return
}
flowLoading.value = true
try {
const [csRows, rkRows, lclqRows, zxlyczRows, wwlyczRows] = await Promise.all([
fetchAllLedgerRows('cs'),
fetchAllLedgerRows('rk'),
fetchAllLedgerRows('lclq'),
fetchAllLedgerRows('zxlycz'),
fetchAllLedgerRows('wwlycz'),
])
const nextStats = createEmptyFlowStats()
nextStats.produce = sumFlowMetric(filterRowsByStatus('produce', csRows), ['csl'])
const filteredRkRows = filterRowsByStatus('storage', rkRows)
nextStats.storage = {
...sumFlowMetric(filteredRkRows, ['rkl']),
inventoryTon: sumInventoryTon(filteredRkRows),
}
const filteredLclqRows = filterRowsByStatus('immediate', lclqRows)
const immediateStats = analyzeImmediateRows(filteredLclqRows)
nextStats.immediate = {
...immediateStats.total,
pendingTon: immediateStats.pendingTon,
}
nextStats.storageOutSelfDispose = sumFlowMetric(filterRowsByStatus('selfDispose', zxlyczRows), ['ckl', 'czl'])
const filteredTransferRows = filterRowsByStatus('transfer', wwlyczRows)
nextStats.storageOutTransfer = sumFlowMetric(filteredTransferRows, ['ckl'])
nextStats.immediateOutSelfDispose = immediateStats.selfDispose
nextStats.immediateOutTransfer = immediateStats.transfer
nextStats.manifestStorage = splitManifestRows(filteredTransferRows.filter(hasUnionNo), ['ckl'])
nextStats.manifestImmediate = splitManifestRows(filteredLclqRows.filter(hasUnionNo), ['zxlyczl', 'csl', 'lyczl'])
applyFlowStats(nextStats)
} catch (e) {
applyFlowStats(createEmptyFlowStats())
console.warn('流程汇总加载失败', e)
} finally {
flowLoading.value = false
updateTableBodyHeight()
}
}
async function fetchAllLedgerRows(lx: string): Promise<LedgerRow[]> {
const pageSize = 100
let page = 1
let allRows: LedgerRow[] = []
let total = 0
while (true) {
const res = await queryLedger(lx, page, pageSize)
if (String(res?.code) !== '1000') {
break
}
const rows = Array.isArray(res.rows) ? (res.rows as LedgerRow[]) : []
total = Number(res.total ?? rows.length)
allRows = allRows.concat(rows)
if (allRows.length >= total || rows.length === 0) {
break
}
page += 1
}
return allRows
}
function getRaw(row: LedgerRow): RawRecord {
return row._raw || {}
}
function pick(row: LedgerRow, rawKeys: string[], mappedKey?: string): string {
const raw = getRaw(row)
for (const key of rawKeys) {
const value = raw[key]
if (value != null && String(value) !== '') {
return String(value)
}
}
if (mappedKey) {
const value = row[mappedKey]
if (value != null && String(value) !== '') {
return String(value)
}
}
return '—'
}
function formatAmount(raw: RawRecord, amountKey: string, unitKey = 'jldw'): string {
const amount = raw[amountKey]
if (amount == null || String(amount) === '') {
return '—'
}
const unit = raw[unitKey] != null && String(raw[unitKey]) !== '' ? String(raw[unitKey]) : '吨'
return `${amount} ${unit}`
}
function formatWasteCode(raw: RawRecord, row?: LedgerRow): string {
const category = raw.fwlb != null ? String(raw.fwlb) : ''
const code = raw.fwdm != null ? String(raw.fwdm) : ''
if (category && code) {
return `${category} ${code}`
}
if (code) {
return code
}
if (category) {
return category
}
if (row?.wasteCode != null && String(row.wasteCode) !== '') {
return String(row.wasteCode)
}
return '—'
}
function mapRegType(value: string): string {
if (value === '0') {
return '正常产生'
}
if (value === '1') {
return '虚拟标签产生'
}
return value || '—'
}
function mapVirtualLabelType(value: string): string {
const map: Record<string, string> = {
'1': '采用罐车运输',
'2': '采用管道运输',
}
return map[value] || value || '—'
}
function mapDestination(value: string, row?: LedgerRow): string {
const map: Record<string, string> = {
'1': '暂存',
'2': '立产立清转移',
'3': '自行处置',
'4': '外委处置',
}
if (value && map[value]) {
return map[value]
}
if (value) {
return value
}
if (row?.destination != null && String(row.destination) !== '') {
return String(row.destination)
}
return '—'
}
function formatPackCount(raw: RawRecord): string {
const count = raw.rqbzsl
if (count == null || String(count) === '') {
return '-'
}
return `${count} 个`
}
function detailDash(value: string): string {
return value && value !== '—' ? value : '-'
}
function detailLabel(text: string): string {
return `${text}:`
}
function emptyDetailCell(): DetailCell {
return { label: '', value: '', empty: true }
}
function formatWasteCategory(raw: RawRecord, row?: LedgerRow): string {
const category = raw.fwlb != null ? String(raw.fwlb) : ''
const name = raw.gjwxfwmlmc != null ? String(raw.gjwxfwmlmc) : ''
if (category && name) {
return `${category}${name}`
}
if (name) {
return name
}
if (category) {
return category
}
return detailDash('')
}
function pickRaw(raw: RawRecord, keys: string[]): string {
for (const key of keys) {
const value = raw[key]
if (value != null && String(value) !== '') {
return String(value)
}
}
return ''
}
function formatDangerDesc(raw: RawRecord, row?: LedgerRow): string {
const desc = pickRaw(raw, ['wxfwms', 'fwms', 'gjwxfwmlmc'])
if (desc) {
return desc
}
if (row?.wasteName != null && String(row.wasteName) !== '') {
return String(row.wasteName)
}
return '-'
}
function formatContainerCapacity(raw: RawRecord): string {
const text = pickRaw(raw, ['rqrl', 'rqcr'])
if (!text) {
return '-'
}
if (/升|l/i.test(text)) {
return text
}
return `${text}L`
}
function mapFacilityType(value: string): string {
const map: Record<string, string> = {
'1': '贮存库',
'2': '贮存池',
'3': '贮存罐区',
}
return map[value] || value || '-'
}
function mapFormType(value: string): string {
const map: Record<string, string> = {
S: '固态',
SS: '半固态',
L: '液态',
G: '气态',
'1': '固态',
'2': '半固态',
'3': '液态',
'4': '气态',
}
return map[value] || value || '-'
}
function formatProduceAmount(raw: RawRecord, row?: LedgerRow): string {
const amount = formatAmount(raw, 'csl')
if (amount === '—') {
const fallback = pick(row || {}, [], 'quantity')
return fallback === '—' ? '-' : `${fallback} 吨`
}
return amount
}
function formatStorageInAmount(raw: RawRecord, row?: LedgerRow): string {
const amount = formatAmount(raw, 'rkl')
if (amount === '—') {
const fallback = pick(row || {}, [], 'inQuantity')
return fallback === '—' ? '-' : `${fallback} 吨`
}
return amount
}
function buildProduceDetail(row: LedgerRow): DetailSection[] {
const raw = getRaw(row)
const isVirtual = String(raw.sfxnbq) === '1'
const destination = mapDestination(String(raw.qx ?? ''), row)
const regRows: DetailRow[] = [
{
kind: 'single',
cell: { label: detailLabel('登记类型'), value: detailDash(mapRegType(String(raw.sfxnbq ?? ''))) },
},
]
if (isVirtual) {
regRows.push({
kind: 'single',
cell: { label: detailLabel('虚拟标签类型'), value: detailDash(mapVirtualLabelType(pick(row, ['xnbqlx', 'xnbqbs']))) },
})
}
const sections: DetailSection[] = [
{
title: '产生登记',
rows: regRows,
},
{
title: '废物信息',
rows: [
{
kind: 'triple',
cells: [
{ label: detailLabel('废物俗称'), value: detailDash(pick(row, ['fwmc'], 'wasteName')) },
{ label: detailLabel('废物代码'), value: detailDash(formatWasteCode(raw, row)) },
{ label: detailLabel('废物类别'), value: detailDash(formatWasteCategory(raw, row)) },
],
},
{
kind: 'triple',
cells: [
{ label: detailLabel('形态'), value: detailDash(mapFormType(pickRaw(raw, ['fwxt', 'xt']))) },
{ label: detailLabel('危险特性'), value: detailDash(pick(row, ['wxtx'])) },
{ label: detailLabel('有害成分'), value: detailDash(pick(row, ['yhcf'])) },
],
},
],
},
{
title: '产生信息',
rows: [
{
kind: 'triple',
cells: [
{ label: detailLabel('产生时间'), value: detailDash(pick(row, ['cssj'], 'produceTime')) },
{ label: detailLabel('产生量'), value: formatProduceAmount(raw, row) },
{ label: detailLabel('生产设施名称'), value: detailDash(pick(row, ['scssmc']) !== '—' ? pick(row, ['scssmc']) : pick(row, ['scsstybh'])) },
],
},
{
kind: 'triple',
cells: [
{ label: detailLabel('产生部门经办人'), value: detailDash(pick(row, ['csbmjbr'])) },
{ label: detailLabel('包装类型'), value: detailDash(pick(row, ['rqlx'])) },
{ label: detailLabel('包装数量'), value: formatPackCount(raw) },
],
},
{
kind: 'triple',
cells: [
{ label: detailLabel('去向'), value: detailDash(destination) },
{ label: detailLabel('容器材质'), value: detailDash(pick(row, ['rqcz', 'rqczhz'])) },
{ label: detailLabel('容器容量'), value: formatContainerCapacity(raw) },
],
},
],
},
]
if (destination === '立产立清转移') {
sections.push({
title: '立产立清转移',
rows: [
{
kind: 'single',
cell: { label: detailLabel('联单编号'), value: detailDash(pick(row, ['ldbh'])) },
},
],
})
}
return sections
}
function buildStorageDetail(row: LedgerRow): DetailSection[] {
const raw = getRaw(row)
return [
{
title: '废物信息',
rows: [
{
kind: 'triple',
cells: [
{ label: detailLabel('废物俗称'), value: detailDash(pick(row, ['fwmc'], 'wasteName')) },
{ label: detailLabel('废物代码'), value: detailDash(formatWasteCode(raw, row)) },
{ label: detailLabel('危险废物'), value: detailDash(formatDangerDesc(raw, row)) },
],
},
{
kind: 'triple',
cells: [
{ label: detailLabel('危险特性'), value: detailDash(pick(row, ['wxtx'])) },
{ label: detailLabel('形态'), value: detailDash(mapFormType(pickRaw(raw, ['fwxt', 'xt']))) },
{ label: detailLabel('废物类别'), value: detailDash(formatWasteCategory(raw, row)) },
],
},
{
kind: 'full',
cell: { label: detailLabel('有害成分'), value: detailDash(pick(row, ['yhcf'])) },
},
],
},
{
title: '入库信息',
rows: [
{
kind: 'triple',
cells: [
{ label: detailLabel('产生时间'), value: detailDash(pick(row, ['cssj', 'ywsj'])) },
{ label: detailLabel('入库时间'), value: detailDash(pick(row, ['rksj'], 'inTime')) },
{ label: detailLabel('入库量'), value: formatStorageInAmount(raw, row) },
],
},
{
kind: 'triple',
cells: [
{ label: detailLabel('贮存设施'), value: detailDash(pick(row, ['zcssmc'], 'facility') !== '—' ? pick(row, ['zcssmc'], 'facility') : pick(row, ['zcsstybh'])) },
{ label: detailLabel('设施类型'), value: detailDash(mapFacilityType(pickRaw(raw, ['zcsslx']))) },
{ label: detailLabel('贮存部门经办人'), value: detailDash(pick(row, ['zcbmjbr'], 'handler')) },
],
},
{
kind: 'triple',
cells: [
{ label: detailLabel('贮存库位'), value: detailDash(pick(row, ['zckw', 'zcsskw'])) },
{ label: detailLabel('包装类型'), value: detailDash(pick(row, ['rqlx'])) },
{ label: detailLabel('包装数量'), value: formatPackCount(raw) },
],
},
{
kind: 'triple',
cells: [
{ label: detailLabel('生产设施名称'), value: detailDash(pick(row, ['scssmc']) !== '—' ? pick(row, ['scssmc']) : pick(row, ['scsstybh'])) },
{ label: detailLabel('生产设施编码'), value: detailDash(pick(row, ['scsstybh'])) },
{ label: detailLabel('产生部门经办人'), value: detailDash(pick(row, ['csbmjbr'])) },
],
},
{
kind: 'triple',
cells: [
{ label: detailLabel('运送部门经办人'), value: detailDash(pick(row, ['ysbmjbr'])) },
{ label: detailLabel('备注'), value: detailDash(pick(row, ['bz', 'remark'])) },
emptyDetailCell(),
],
},
],
},
]
}
function buildImmediateDetail(row: LedgerRow): DetailSection[] {
const raw = getRaw(row)
const isVirtual = String(raw.sfxnbq) === '1'
const regItems: DetailItem[] = [
{ label: '登记类型', value: mapRegType(String(raw.sfxnbq ?? '')) },
]
if (isVirtual) {
regItems.push({ label: '虚拟标签类型', value: mapVirtualLabelType(pick(row, ['xnbqlx', 'xnbqbs'])) })
}
return [
{ title: '产生登记', items: regItems },
{
title: '废物信息',
items: [
{ label: '废物俗称', value: pick(row, ['fwmc'], 'wasteName') },
{ label: '废物代码', value: formatWasteCode(raw, row) },
{ label: '废物类别', value: pick(row, ['gjwxfwmlmc', 'fwlb']) },
{ label: '形态', value: pick(row, ['fwxt', 'xt']) },
{ label: '危险特性', value: pick(row, ['wxtx']) },
{ label: '有害成分', value: pick(row, ['yhcf']) },
],
},
{
title: '产生信息',
items: [
{ label: '产生时间', value: pick(row, ['cssj', 'zxlyczsj', 'ywsj'], 'produceTime') },
{ label: '产生量', value: formatAmount(raw, 'csl') !== '—' ? formatAmount(raw, 'csl') : pick(row, [], 'disposeQuantity') },
{ label: '生产设施名称', value: pick(row, ['scssmc']) !== '—' ? pick(row, ['scssmc']) : pick(row, ['scsstybh', 'zxlyczsstybh']) },
{ label: '产生部门经办人', value: pick(row, ['csbmjbr', 'zxlyczbmjbr']) },
{ label: '包装类型', value: pick(row, ['rqlx']) },
{ label: '包装数量', value: formatPackCount(raw) },
{ label: '废物去向', value: mapDestination(String(raw.qx ?? ''), row) !== '—' ? mapDestination(String(raw.qx ?? ''), row) : pick(row, ['zxlyczfs']) },
{ label: '容器材质', value: pick(row, ['rqcz', 'rqczhz']) },
{ label: '容器容量', value: pick(row, ['rqrl', 'rqcr']) },
],
},
{
title: '立产立清转移',
items: [
{ label: '联单编号', value: pick(row, ['ldbh'], 'unionNo') },
{ label: '利用处置/转移量', value: formatAmount(raw, 'zxlyczl') !== '—' ? formatAmount(raw, 'zxlyczl') : pick(row, [], 'disposeQuantity') },
{ label: '处置时间', value: pick(row, ['zxlyczwbsj', 'zxlyczsj']) },
],
},
]
}
function buildSelfDisposeDetail(row: LedgerRow): DetailSection[] {
const raw = getRaw(row)
return [
{
title: '基本信息',
items: [
{ label: '出库时间', value: pick(row, ['cksj', 'czsj', 'zxlyczsj'], 'outTime') },
{ label: '出库量', value: formatAmount(raw, 'ckl') !== '—' ? formatAmount(raw, 'ckl') : formatAmount(raw, 'czl') !== '—' ? formatAmount(raw, 'czl') : pick(row, [], 'outQuantity') },
{ label: '出库批次', value: pick(row, ['ckpcbm', 'pcbm'], 'batchNo') },
{ label: '废物去向', value: pick(row, [], 'destination') !== '—' ? pick(row, [], 'destination') : '自行处置' },
],
},
{
title: '废物信息',
items: [
{ label: '废物俗称', value: pick(row, ['fwmc'], 'wasteName') },
{ label: '废物代码', value: formatWasteCode(raw, row) },
{ label: '危险废物', value: pick(row, ['gjwxfwmlmc', 'wxfwms']), span: 3 },
{ label: '危险特性', value: pick(row, ['wxtx']) },
{ label: '形态', value: pick(row, ['fwxt', 'xt']) },
{ label: '废物类别', value: pick(row, ['fwlb', 'gjwxfwmlmc']) },
{ label: '有害成分', value: pick(row, ['yhcf']), span: 3 },
],
},
{
title: '处置信息',
items: [
{ label: '处置方式', value: pick(row, ['czfs', 'lyczfs', 'zxlyczfs']) },
{ label: '处置设施', value: pick(row, ['czssmc', 'zxlyczsstybh']) },
{ label: '处置时间', value: pick(row, ['czsj', 'cksj', 'zxlyczwbsj']) },
{ label: '处置经办人', value: pick(row, ['czbmjbr', 'zxlyczbmjbr', 'jbr']) },
],
},
]
}
function buildTransferDetail(row: LedgerRow): DetailSection[] {
const raw = getRaw(row)
return [
{
title: '出库信息',
items: [
{ label: '出库时间', value: pick(row, ['cksj'], 'outTime') },
{ label: '出库类型', value: pick(row, ['cklx', 'cklxmc'], 'outType') },
{ label: '出库量', value: formatAmount(raw, 'ckl') !== '—' ? formatAmount(raw, 'ckl') : pick(row, [], 'outQuantity') },
{ label: '出库批次', value: pick(row, ['ckpcbm'], 'batchNo') },
{ label: '联单编号', value: pick(row, ['ldbh'], 'unionNo') },
{ label: '录入人', value: pick(row, ['lrr', 'cjr'], 'operator') },
{ label: '运送部门经办人', value: pick(row, ['ysbmjbr']) },
{ label: '出库部门经办人', value: pick(row, ['ckbmjbr']) },
{ label: '数字识别码', value: pick(row, ['szsbm']) },
],
},
{
title: '废物信息',
items: [
{ label: '废物俗称', value: pick(row, ['fwmc'], 'wasteName') },
{ label: '废物代码', value: formatWasteCode(raw, row) },
{ label: '危险废物', value: pick(row, ['gjwxfwmlmc', 'wxfwms']), span: 3 },
{ label: '危险特性', value: pick(row, ['wxtx']) },
{ label: '形态', value: pick(row, ['fwxt', 'xt']) },
{ label: '废物类别', value: pick(row, ['fwlb', 'gjwxfwmlmc']) },
{ label: '有害成分', value: pick(row, ['yhcf']), span: 3 },
],
},
]
}
const DETAIL_BUILDERS: Record<TabName, (row: LedgerRow) => DetailSection[]> = {
produce: buildProduceDetail,
storage: buildStorageDetail,
immediate: buildImmediateDetail,
selfDispose: buildSelfDisposeDetail,
transfer: buildTransferDetail,
}
const DETAIL_TITLES: Record<TabName, string> = {
produce: '产生台账查看',
storage: '入库详情',
immediate: '立产立清台账',
selfDispose: '自处置出库台账详情',
transfer: '出库详情',
}
function openDetail(tab: TabName, row: LedgerRow): void {
detailTitle.value = DETAIL_TITLES[tab]
detailSections.value = DETAIL_BUILDERS[tab](row)
detailVisible.value = true
}
function resolveRowStatus(tabName: TabName, row: LedgerRow): string {
const raw = getRaw(row)
const direct = String(row.status || raw.ztmc || raw.zt || '').trim()
if (direct && !/^\d+$/.test(direct)) {
return direct
}
if (tabName === 'produce') {
const qxMap: Record<string, string> = {
'1': '待转移',
'2': '已转移',
'3': '已完成',
'4': '已转移',
}
const codeMap: Record<string, string> = {
'1': '待转移',
'2': '已转移',
'3': '已完成',
'4': '已转移',
'5': '贮存中',
}
return qxMap[String(raw.qx ?? '')] || codeMap[direct] || direct
}
if (tabName === 'storage') {
const codeMap: Record<string, string> = { '1': '贮存中', '2': '已出库', '3': '已作废' }
if (codeMap[direct]) {
return codeMap[direct]
}
const remain = pickRawTon(raw, ['syll', 'kcl'])
if (remain > 0) {
return '贮存中'
}
if (raw.cksj != null && String(raw.cksj) !== '') {
return '已出库'
}
return direct || '贮存中'
}
if (tabName === 'immediate' || tabName === 'selfDispose') {
const codeMap: Record<string, string> = { '1': '进行中', '2': '已完成', '3': '已作废' }
if (codeMap[direct]) {
return codeMap[direct]
}
if ((raw.jssj != null && String(raw.jssj) !== '') || (raw.wcsj != null && String(raw.wcsj) !== '')) {
return '已完成'
}
return direct || '进行中'
}
const codeMap: Record<string, string> = { '1': '待出库', '2': '已出库', '3': '已作废', '9': '已作废' }
if (codeMap[direct]) {
return codeMap[direct]
}
if (String(raw.zfbz) === '1') {
return '已作废'
}
if (raw.cksj != null && String(raw.cksj) !== '') {
return '已出库'
}
return direct || '待出库'
}
function enrichRows(tabName: TabName, rows: LedgerRow[]): LedgerRow[] {
return rows.map((row) => {
const status = resolveRowStatus(tabName, row)
const raw = getRaw(row)
const next: LedgerRow = { ...row, status }
if (tabName === 'produce') {
next.destination = mapDestination(String(raw.qx ?? ''), row)
}
return next
})
}
function matchRowStatus(tabName: TabName, row: LedgerRow, filterStatus: string): boolean {
if (!filterStatus) {
return true
}
return resolveRowStatus(tabName, row) === filterStatus
}
function filterRowsByStatus(tabName: TabName, rows: LedgerRow[]): LedgerRow[] {
const filterStatus = searchForm.status.trim()
if (!filterStatus) {
return rows
}
return rows.filter((row) => matchRowStatus(tabName, row, filterStatus))
}
function paginateRows<T>(rows: T[], page: number, pageSize: number): T[] {
const start = (page - 1) * pageSize
return rows.slice(start, start + pageSize)
}
function getStatusType(status: string | undefined): string {
const map: Record<string, string> = {
'待转移': 'warning',
'待出库': 'warning',
'已转移': 'success',
'贮存中': 'info',
'已完成': 'success',
'进行中': 'primary',
'已出库': 'danger',
'已作废': 'info',
'暂存': 'info',
}
return map[status || ''] || 'info'
}
async function loadEnterprise(): Promise<void> {
try {
const res = await App.Services.HttpService.post(API.enterprise, {}) as Record<string, unknown>
const data = res?.data as Record<string, unknown> | null
enterpriseName.value = String(data?.dwmc || data?.qymc || '')
} catch (e) {
console.warn('企业信息加载失败', e)
}
}
async function queryLedger(lx: string, page: number, pageSize: number): Promise<Record<string, unknown>> {
const body: Record<string, unknown> = {
lx,
ksrq: searchForm.startDate,
jsrq: searchForm.endDate,
page,
page_size: pageSize,
}
if (searchForm.name.trim()) {
body.fwmc = searchForm.name.trim()
}
if (searchForm.code.trim()) {
body.fwdm = searchForm.code.trim()
}
return App.Services.HttpService.post(API.ledgerQuery, body) as Promise<Record<string, unknown>>
}
function assignTabRows(tabName: TabName, rows: LedgerRow[], total: number): void {
tabDataRefs[tabName].value = rows
pagination[tabName].total = total
}
async function loadTab(tabName: TabName): Promise<void> {
if (!searchForm.startDate || !searchForm.endDate) {
ElMessage.warning('请选择查询日期范围')
return
}
tableLoading.value = true
try {
const lx = TAB_LX[tabName]
const pageConf = pagination[tabName]
const statusFilter = searchForm.status.trim()
if (statusFilter) {
const allRows = enrichRows(tabName, await fetchAllLedgerRows(lx))
const filtered = allRows.filter((row) => matchRowStatus(tabName, row, statusFilter))
const pageRows = paginateRows(filtered, pageConf.currentPage, pageConf.pageSize)
assignTabRows(tabName, pageRows, filtered.length)
return
}
const res = await queryLedger(lx, pageConf.currentPage, pageConf.pageSize)
if (String(res?.code) !== '1000') {
throw new Error(String(res?.message || '台账查询失败'))
}
const rows = enrichRows(tabName, Array.isArray(res.rows) ? (res.rows as LedgerRow[]) : [])
const total = Number(res.total ?? rows.length)
assignTabRows(tabName, rows, Number.isFinite(total) ? total : rows.length)
} catch (e) {
assignTabRows(tabName, [], 0)
ElMessage.error(e instanceof Error ? e.message : String(e))
} finally {
tableLoading.value = false
}
}
async function reloadAll(): Promise<void> {
await loadFlowStats()
await loadTab(activeTab.value)
}
const handleSearch = (): void => {
;(Object.keys(pagination) as TabName[]).forEach((tab) => {
pagination[tab].currentPage = 1
})
void reloadAll()
}
const handleReset = (): void => {
searchForm.name = ''
searchForm.code = ''
searchForm.status = ''
applyDefaultDates()
handleSearch()
}
const handlePageChange = (tabName: TabName, page: number): void => {
pagination[tabName].currentPage = page
void loadTab(tabName)
}
const handleSizeChange = (tabName: TabName, size: number): void => {
pagination[tabName].pageSize = size
pagination[tabName].currentPage = 1
void loadTab(tabName)
}
const switchTab = (tabName: TabName): void => {
activeTab.value = tabName
}
watch(activeTab, (tab) => {
void loadTab(tab)
updateTableBodyHeight()
})
onMounted(() => {
applyDefaultDates()
void loadEnterprise()
void reloadAll()
bindTableResizeObserver()
})
onBeforeUnmount(() => {
unbindTableResizeObserver()
})
</script>
<style scoped>
.hw-trace-page {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
max-width: 100%;
padding: 12px;
box-sizing: border-box;
background-color: #f5f7fa;
display: flex;
flex-direction: column;
overflow: hidden;
gap: 8px;
}
.page-top {
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.top-alert {
margin-bottom: 0;
}
.top-alert :deep(.el-alert__content) {
padding: 4px 0;
}
.enterprise-name {
color: #606266;
font-weight: 500;
}
.search-card,
.flow-card {
margin-bottom: 0;
}
.search-card :deep(.el-card__body),
.flow-card :deep(.el-card__body) {
padding: 10px 12px;
}
.flow-card :deep(.el-card__header) {
padding: 8px 12px;
}
.search-form {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.search-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.header-title {
font-size: 15px;
font-weight: bold;
color: #303133;
}
.flow-container {
display: flex;
align-items: flex-start;
padding: 4px 0 0;
overflow-x: auto;
}
.flow-column {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 132px;
}
.flow-box {
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
background: #fff;
transition: all 0.3s;
}
.flow-box.clickable {
cursor: pointer;
}
.flow-box.clickable:hover {
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
border-color: #409eff;
transform: translateY(-2px);
}
.flow-box.active {
box-shadow: 0 0 0 2px #2c5aa9;
border-color: #2c5aa9;
}
.flow-box.primary {
background: #409eff;
border-color: #409eff;
}
.flow-box.primary.clickable:hover {
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
border-color: #66b1ff;
}
.flow-title {
padding: 6px 10px;
text-align: center;
font-weight: bold;
font-size: 13px;
color: #303133;
border-bottom: 1px solid #ebeef5;
background: #f5f7fa;
}
.flow-box.primary .flow-title {
background: #409eff;
color: #fff;
border-bottom: 1px solid #66b1ff;
}
.flow-data {
padding: 6px 8px;
}
.data-row {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
flex-wrap: wrap;
font-family: Consolas, 'Courier New', monospace;
}
.data-row.sub {
margin-top: 4px;
justify-content: flex-start;
}
.data-label {
font-size: 12px;
color: #606266;
margin-right: 4px;
font-family: inherit;
}
.data-value {
font-weight: bold;
font-size: 13px;
}
.data-value.blue {
color: #409eff;
}
.data-value.white {
color: #fff;
}
.data-unit {
font-size: 11px;
color: #909399;
}
.data-unit.white {
color: #e6e6e6;
}
.flow-label {
text-align: center;
font-size: 12px;
color: #909399;
padding: 2px 0;
}
.flow-arrow {
display: flex;
align-items: center;
justify-content: center;
padding: 0 6px;
color: #e6a23c;
font-size: 18px;
margin-top: 24px;
}
.transfer-column {
min-width: 300px;
gap: 12px;
}
.transfer-box {
border: 1px dashed #dcdfe6;
border-radius: 4px;
padding: 8px;
background: #fafafa;
}
.transfer-title {
text-align: center;
margin-bottom: 6px;
}
.title-text {
font-weight: bold;
font-size: 13px;
color: #303133;
}
.title-tip {
font-size: 11px;
color: #909399;
margin-left: 4px;
}
.transfer-content {
display: flex;
align-items: center;
gap: 6px;
}
.transfer-item {
flex: 1;
border: 1px solid #ebeef5;
border-radius: 4px;
background: #fff;
overflow: hidden;
}
.item-header {
padding: 4px 6px;
text-align: center;
font-size: 12px;
font-weight: bold;
color: #409eff;
background: #ecf5ff;
border-bottom: 1px solid #ebeef5;
}
.item-body {
padding: 6px;
text-align: center;
font-size: 12px;
}
.transfer-arrow {
color: #c0c4cc;
font-size: 14px;
}
.table-card {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.table-card :deep(.el-card__body) {
flex: 1;
min-height: 0;
padding: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.data-tabs {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.data-tabs :deep(.el-tabs) {
height: 100%;
display: flex;
flex-direction: column;
}
.data-tabs :deep(.el-tabs__header) {
margin-bottom: 0;
background-color: #f5f7fa;
flex-shrink: 0;
}
.data-tabs :deep(.el-tabs__nav-scroll) {
padding: 0 8px;
}
.data-tabs :deep(.el-tabs__item) {
font-weight: 500;
transition: all 0.3s;
height: 36px;
line-height: 36px;
}
.data-tabs :deep(.el-tabs__item.is-active) {
font-weight: bold;
background-color: #fff;
}
.data-tabs :deep(.el-tabs__content) {
flex: 1;
min-height: 0;
padding: 0;
background: #fff;
overflow: hidden;
}
.data-tabs :deep(.el-tab-pane) {
height: 100%;
}
.tab-pane-body {
height: 100%;
box-sizing: border-box;
padding: 12px;
display: flex;
flex-direction: column;
min-height: 0;
}
.full-width-table {
width: 100% !important;
flex: 1;
min-height: 0;
}
.full-width-table :deep(.el-table__header-wrapper th) {
background-color: #f5f7fa;
font-weight: 600;
color: #303133;
}
.pagination-wrapper {
flex-shrink: 0;
display: flex;
justify-content: flex-end;
padding: 10px 0 0;
border-top: 1px solid #ebeef5;
margin-top: 10px;
}
.ledger-detail-dialog :deep(.el-dialog__body) {
max-height: 70vh;
overflow-y: auto;
padding-top: 8px;
}
.detail-section-box {
margin-bottom: 16px;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
background: #fff;
}
.detail-section-box + .detail-section-box {
margin-top: 0;
}
.detail-section-head {
padding: 12px 16px;
background: #fff;
border-left: 4px solid #2c5aa9;
border-bottom: 1px solid #ebeef5;
font-size: 14px;
font-weight: 600;
color: #303133;
}
.detail-section-body .detail-row {
display: flex;
border-bottom: 1px solid #ebeef5;
}
.detail-section-body .detail-row:last-child {
border-bottom: none;
}
.detail-row-triple .detail-field {
flex: 1;
display: flex;
min-width: 0;
border-right: 1px solid #ebeef5;
}
.detail-row-triple .detail-field:last-child {
border-right: none;
}
.detail-row-full .detail-field-label,
.detail-row-triple .detail-field-label {
width: 120px;
min-width: 120px;
background: #f5f7fa;
padding: 10px 16px;
font-size: 13px;
color: #606266;
border-right: 1px solid #ebeef5;
box-sizing: border-box;
}
.detail-row-full .detail-field-value,
.detail-row-triple .detail-field-value {
flex: 1;
padding: 10px 16px;
font-size: 13px;
color: #303133;
word-break: break-all;
}
.detail-row-triple .detail-field.is-empty .detail-field-label,
.detail-row-triple .detail-field.is-empty .detail-field-value {
min-height: 42px;
}
.detail-section + .detail-section {
margin-top: 20px;
}
.detail-section-title {
margin-bottom: 12px;
padding-left: 10px;
border-left: 3px solid #409eff;
font-size: 14px;
font-weight: 600;
color: #303133;
line-height: 1.2;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
border-top: 1px solid #ebeef5;
border-left: 1px solid #ebeef5;
}
.detail-cell {
display: grid;
grid-template-columns: 118px minmax(0, 1fr);
min-height: 42px;
border-right: 1px solid #ebeef5;
border-bottom: 1px solid #ebeef5;
background: #fff;
}
.detail-cell.span-3 {
grid-column: span 3;
}
.detail-label {
display: flex;
align-items: center;
padding: 10px 12px;
background: #f5f7fa;
color: #606266;
font-size: 13px;
border-right: 1px solid #ebeef5;
}
.detail-value {
display: flex;
align-items: center;
padding: 10px 12px;
color: #303133;
font-size: 13px;
word-break: break-all;
}
</style>后端
// 国家危废平台公共常量与请求(测试环境,写死在接口引擎)
var CSWM_BASE_URL = ''; //第三方平台地址
var CSWM_TOKEN = ''; //访问接口所需token
var CSWM_QYID = ''; //参数1
var CSWM_NONCE = ''; //参数2
var CSWM_SUCCESS = '1000'; //成功状态下返回值
var CSWM_WLWSBTYBH = '';
function cswmRandomNonce(maxLen) {
var chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
var len = maxLen || 32;
var s = '';
var i = 0;
for (i = 0; i < len; i++) {
s += chars.charAt(Math.floor(Math.random() * chars.length));
}
return s;
}
function cswmPost(path, payloadObj) {
var plain = JSON.stringify(payloadObj != null ? payloadObj : {});
var data = App.Helpers.Crypto.Sm4EncryptHex(plain, CSWM_TOKEN);
var nonce = CSWM_NONCE;
if (nonce == null || String(nonce) === '') {
nonce = cswmRandomNonce(32);
}
var timestamp = String(Math.floor(Date.now() / 1000));
var sign = App.Helpers.Crypto.Sm3HashUpper(
'data=' + data + '&nonce=' + nonce + '×tamp=' + timestamp
);
var reqBody = JSON.stringify({
data: data,
nonce: nonce,
timestamp: timestamp,
sign: sign
});
var headers = {
token: CSWM_TOKEN,
'Content-Type': 'application/json'
};
var respText = App.Services.HttpService.Post(CSWM_BASE_URL + path, reqBody, headers);
if (respText == null || String(respText) === '') {
throw new Error('国平台返回空响应');
}
return JSON.parse(String(respText));
}
function cswmText(v) {
if (v == null) {
return '';
}
return String(v);
}
function cswmParsePage(v, fallback) {
var n = parseInt(v != null ? String(v) : String(fallback), 10);
if (isNaN(n) || n < 1) {
return fallback;
}
return n;
}
function cswmValidateDateRange(ksrq, jsrq) {
var start = cswmText(ksrq).trim();
var end = cswmText(jsrq).trim();
if (start === '' || end === '') {
throw new Error('开始日期 ksrq 与结束日期 jsrq 不能为空');
}
var ds = new Date(start);
var de = new Date(end);
if (isNaN(ds.getTime()) || isNaN(de.getTime())) {
throw new Error('日期格式须为 yyyy-MM-dd');
}
if (de < ds) {
throw new Error('结束日期不能早于开始日期');
}
var diffDays = (de.getTime() - ds.getTime()) / (1000 * 60 * 60 * 24);
if (diffDays > 31) {
throw new Error('开始日期与结束日期间隔不得大于一个月');
}
return { ksrq: start, jsrq: end };
}
function cswmFailResponse(resp) {
App.Response.StatusCode = 400;
App.Response.Body = {
code: resp != null && resp.code != null ? String(resp.code) : '',
message: resp != null && resp.message != null ? String(resp.message) : '国平台调用失败',
total: 0,
rows: []
};
}
function cswmMapDestination(value) {
var map = {
'1': '暂存',
'2': '立产立清转移',
'3': '自行处置',
'4': '外委处置'
};
var text = cswmText(value);
return map[text] || text;
}
function cswmResolveProduceStatus(row) {
var st = cswmText(row.ztmc || row.zt);
if (st !== '' && !/^\d+$/.test(st)) {
return st;
}
var codeMap = { '1': '待转移', '2': '已转移', '3': '已完成', '4': '已转移', '5': '贮存中' };
if (codeMap[st]) {
return codeMap[st];
}
var qxMap = { '1': '待转移', '2': '已转移', '3': '已完成', '4': '已转移' };
return qxMap[cswmText(row.qx)] || st;
}
function cswmResolveStorageStatus(row) {
var st = cswmText(row.ztmc || row.zt);
if (st !== '' && !/^\d+$/.test(st)) {
return st;
}
var codeMap = { '1': '贮存中', '2': '已出库', '3': '已作废' };
if (codeMap[st]) {
return codeMap[st];
}
var remain = row.syll != null ? row.syll : row.kcl;
if (remain != null && parseFloat(String(remain)) > 0) {
return '贮存中';
}
if (row.cksj != null && cswmText(row.cksj) !== '') {
return '已出库';
}
return st || '贮存中';
}
function cswmResolveProcessStatus(row) {
var st = cswmText(row.ztmc || row.zt);
if (st !== '' && !/^\d+$/.test(st)) {
return st;
}
var codeMap = { '1': '进行中', '2': '已完成', '3': '已作废' };
if (codeMap[st]) {
return codeMap[st];
}
if ((row.jssj != null && cswmText(row.jssj) !== '') || (row.wcsj != null && cswmText(row.wcsj) !== '')) {
return '已完成';
}
return '进行中';
}
function cswmResolveTransferStatus(row) {
var st = cswmText(row.ztmc || row.zt);
if (st !== '' && !/^\d+$/.test(st)) {
return st;
}
var codeMap = { '1': '待出库', '2': '已出库', '3': '已作废', '9': '已作废' };
if (codeMap[st]) {
return codeMap[st];
}
if (String(row.zfbz) === '1') {
return '已作废';
}
if (row.cksj != null && cswmText(row.cksj) !== '') {
return '已出库';
}
return '待出库';
}
function cswmFilterRows(rows, fwmc, fwdm) {
var nameKey = cswmText(fwmc).trim().toLowerCase();
var codeKey = cswmText(fwdm).trim().toLowerCase();
if (nameKey === '' && codeKey === '') {
return rows;
}
var out = [];
var i = 0;
for (i = 0; i < rows.length; i++) {
var row = rows[i];
var wn = cswmText(row.wasteName).toLowerCase();
var wc = cswmText(row.wasteCode).toLowerCase();
var okName = nameKey === '' || wn.indexOf(nameKey) >= 0;
var okCode = codeKey === '' || wc.indexOf(codeKey) >= 0;
if (okName && okCode) {
out.push(row);
}
}
return out;
}
function cswmMapLedgerRow(lx, row) {
if (row == null) {
return null;
}
if (lx === 'cs') {
return {
produceTime: cswmText(row.cssj),
batchNo: cswmText(row.cspcbm),
digitalCode: cswmText(row.szsbm),
wasteName: cswmText(row.fwmc),
wasteCode: cswmText(row.fwdm),
quantity: cswmText(row.csl),
destination: cswmMapDestination(row.qx || row.fwqx || row.qxmc),
status: cswmResolveProduceStatus(row),
dataSource: '国家平台',
_raw: row
};
}
if (lx === 'rk') {
return {
status: cswmResolveStorageStatus(row),
inTime: cswmText(row.rksj),
wasteName: cswmText(row.fwmc),
wasteCode: cswmText(row.fwdm),
inQuantity: cswmText(row.rkl),
remainQuantity: cswmText(row.syll || row.kcl),
batchNo: cswmText(row.rkpcbm),
facility: cswmText(row.zcssmc || row.ssmc),
handler: cswmText(row.zcjbr || row.jbr),
operator: cswmText(row.lrr || row.cjr),
dataSource: '国家平台',
_raw: row
};
}
if (lx === 'lclq') {
return {
status: cswmResolveProcessStatus(row),
batchNo: cswmText(row.cspcbm || row.pcbm),
wasteName: cswmText(row.fwmc),
wasteCode: cswmText(row.fwdm),
disposeQuantity: cswmText(row.csl || row.lyczl || row.zyl),
produceTime: cswmText(row.cssj),
unionNo: cswmText(row.ldbh),
destination: cswmText(row.qx || row.fwqx),
dataSource: '国家平台',
_raw: row
};
}
if (lx === 'zxlycz') {
return {
status: cswmResolveProcessStatus(row),
outTime: cswmText(row.cksj || row.czsj),
wasteName: cswmText(row.fwmc),
wasteCode: cswmText(row.fwdm),
outQuantity: cswmText(row.ckl || row.czl),
batchNo: cswmText(row.ckpcbm || row.pcbm),
destination: '自行处置',
dataSource: '国家平台',
_raw: row
};
}
if (lx === 'wwlycz') {
return {
status: cswmResolveTransferStatus(row),
outTime: cswmText(row.cksj),
wasteName: cswmText(row.fwmc),
wasteCode: cswmText(row.fwdm),
outType: cswmText(row.cklx || row.cklxmc),
batchNo: cswmText(row.ckpcbm),
unionNo: cswmText(row.ldbh),
outQuantity: cswmText(row.ckl),
operator: cswmText(row.lrr || row.cjr),
dataSource: '国家平台',
_raw: row
};
}
return {
wasteName: cswmText(row.fwmc),
wasteCode: cswmText(row.fwdm),
dataSource: '国家平台',
_raw: row
};
}
function cswmMapLedgerRows(lx, list) {
var rows = [];
var i = 0;
if (list == null) {
return rows;
}
for (i = 0; i < list.length; i++) {
rows.push(cswmMapLedgerRow(lx, list[i]));
}
return rows;
}
try {
var resp = cswmPost('/iot/entInfo', {});
if (resp == null || String(resp.code) !== CSWM_SUCCESS) {
cswmFailResponse(resp);
} else {
App.Response.Body = {
code: String(resp.code),
message: cswmText(resp.message),
data: resp.data != null ? resp.data : null
};
}
} catch (e) {
App.IsException = true;
App.Helpers.Logger.Error(String(e));
App.Response.StatusCode = 500;
App.Response.Body = { message: String(e), data: null };
}最终效果:

