Skip to content

第三方接口对接 ​

通过平台接口引擎安全调用第三方 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 + '&timestamp=' + 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 };
}

最终效果:

相关文档 ​

白泽 ProCode 开发者文档