<template>
  <basic-container class="business-crud-page">
    <avue-crud
      :option="option"
      :table-loading="loading"
      :data="data"
      v-model:page="page"
      v-model="form"
      ref="crud"
      :permission="permissionList"
      :before-open="beforeOpen"
      @row-save="rowSave"
      @row-update="rowUpdate"
      @row-del="rowDel"
      @search-change="searchChange"
      @search-reset="searchReset"
      @selection-change="selectionChange"
      @current-change="currentChange"
      @size-change="sizeChange"
      @refresh-change="refreshChange"
      @on-load="onLoad"
    >
      <template #menu-left>
        <el-button
          type="primary"
          icon="el-icon-plus"
          plain
          v-if="canCreate"
          @click="$refs.crud.rowAdd()"
          >{{ config.createText || '新增' }}
        </el-button>
        <el-button
          type="primary"
          icon="el-icon-upload"
          plain
          v-if="config.importUrl && hasPermission(`${config.permission}_import`) && !readonlyScope"
          @click="handleImport"
          >{{ config.importText || '批量导入' }}
        </el-button>
        <el-button
          type="primary"
          icon="el-icon-download"
          plain
          v-if="config.templateUrl && hasPermission(`${config.permission}_template`)"
          @click="handleTemplate"
          >下载模板
        </el-button>
        <el-button
          type="primary"
          icon="el-icon-download"
          plain
          v-if="config.exportUrl && hasPermission(`${config.permission}_export`)"
          @click="handleExport"
          >批量导出
        </el-button>
        <el-button
          type="success"
          icon="el-icon-check"
          plain
          v-if="canBatchComplete"
          @click="handleBatchComplete"
          >批量完成
        </el-button>
        <el-button
          type="primary"
          icon="el-icon-truck"
          plain
          v-if="canRoadLoading"
          @click="handleRoadLoading"
          >{{ config.roadLoadingText || '公路配载' }}
        </el-button>
        <el-button
          type="danger"
          icon="el-icon-delete"
          plain
          v-if="
            config.batchDelete !== false &&
            hasPermission(`${config.permission}_delete`) &&
            !readonlyScope
          "
          @click="handleDelete"
          >批量删除
        </el-button>
      </template>

      <template #status="{ row }">
        <el-tag :type="statusTagType(row.status)">
          {{ displayStatus(row, 'status') }}
        </el-tag>
      </template>

      <template #businessStatus="{ row }">
        <el-tag :type="statusTagType(row.businessStatus)">
          {{ displayStatus(row, 'businessStatus') }}
        </el-tag>
      </template>

      <template #approvalStatus="{ row }">
        <el-tag :type="statusTagType(row.approvalStatus)">
          {{ displayStatus(row, 'approvalStatus') }}
        </el-tag>
      </template>

      <template #basicInfoTitle-form>
        <div class="dialog-section-title">基本信息</div>
      </template>

      <template #contractCategory-label>合同类型</template>

      <template #projectName-form>
        <el-select
          v-model="selectedProjectId"
          class="business-crud-page__field"
          placeholder="请选择项目名称"
          filterable
          clearable
          :loading="projectLoading"
          :disabled="dialogReadonly"
          @visible-change="visible => visible && loadProjectOptions()"
          @change="handleProjectChange"
        >
          <el-option
            v-for="item in projectOptions"
            :key="item.id"
            :label="item.projectName"
            :value="item.id"
          />
        </el-select>
      </template>

      <template #contractName-form>
        <el-select
          v-model="form.contractId"
          class="business-crud-page__field"
          placeholder="请选择客户合同"
          filterable
          clearable
          :loading="contractLoading"
          :disabled="dialogReadonly || !form.projectId"
          @visible-change="visible => visible && loadContractOptionsForProject()"
          @change="handleContractChange"
        >
          <el-option
            v-for="item in contractOptions"
            :key="item.id"
            :label="item.contractName"
            :value="item.id"
          />
        </el-select>
      </template>

      <template #shippingInfoTitle-form>
        <div
          class="dialog-section-title dialog-section-title--action business-crud-page__shipping-title"
        >
          <span>收发货信息</span>
          <el-button
            v-if="!dialogReadonly"
            type="primary"
            text
            :disabled="transportAddressSelectDisabled"
            @click="openTransportRouteDialog"
          >
            选择线路
          </el-button>
        </div>
      </template>

      <template #departureAddress-form>
        <div class="business-crud-page__route-address-row">
          <el-input
            v-model="form.departureName"
            readonly
            :placeholder="transportStationNamePlaceholder"
            :suffix-icon="transportStationMode ? Search : undefined"
            :disabled="transportAddressSelectDisabled"
            @click="openTransportPrimaryAddressPicker('departure')"
          />
          <el-input
            v-model="form.departureAddress"
            readonly
            :placeholder="transportAddressDetailPlaceholder"
            :suffix-icon="transportStationMode ? undefined : Location"
            :disabled="transportAddressSelectDisabled"
            @click="openTransportSecondaryAddressPicker('departure')"
          />
          <el-tooltip content="选择常用地址" placement="top">
            <el-button
              type="primary"
              :icon="List"
              link
              :disabled="transportAddressSelectDisabled"
              @click="openTransportAddressDialog('departure')"
            />
          </el-tooltip>
          <span class="business-crud-page__route-label">联系人</span>
          <el-input
            v-model="form.departureContact"
            placeholder="请输入"
            :disabled="dialogReadonly"
          />
          <span class="business-crud-page__route-label">联系方式</span>
          <el-input v-model="form.departurePhone" placeholder="请输入" :disabled="dialogReadonly" />
        </div>
      </template>

      <template #arrivalAddress-form>
        <div class="business-crud-page__route-address-row">
          <el-input
            v-model="form.arrivalName"
            readonly
            :placeholder="transportStationNamePlaceholder"
            :suffix-icon="transportStationMode ? Search : undefined"
            :disabled="transportAddressSelectDisabled"
            @click="openTransportPrimaryAddressPicker('arrival')"
          />
          <el-input
            v-model="form.arrivalAddress"
            readonly
            :placeholder="transportAddressDetailPlaceholder"
            :suffix-icon="transportStationMode ? undefined : Location"
            :disabled="transportAddressSelectDisabled"
            @click="openTransportSecondaryAddressPicker('arrival')"
          />
          <el-tooltip content="选择常用地址" placement="top">
            <el-button
              type="primary"
              :icon="List"
              link
              :disabled="transportAddressSelectDisabled"
              @click="openTransportAddressDialog('arrival')"
            />
          </el-tooltip>
          <span class="business-crud-page__route-label">联系人</span>
          <el-input v-model="form.arrivalContact" placeholder="请输入" :disabled="dialogReadonly" />
          <span class="business-crud-page__route-label">联系方式</span>
          <el-input v-model="form.arrivalPhone" placeholder="请输入" :disabled="dialogReadonly" />
        </div>
      </template>

      <template #goodsInfoTitle-form>
        <div
          class="dialog-section-title dialog-section-title--action business-crud-page__goods-title"
        >
          <span>货物信息</span>
          <div v-if="!dialogReadonly" class="business-crud-page__section-actions">
            <el-button type="primary" text @click="cargoImportBox = true">导入货物</el-button>
            <el-button type="primary" text @click="openCommonCargoDialog">常用货物</el-button>
          </div>
        </div>
      </template>

      <template #goodsJson-form>
        <div class="business-crud-page__cargo-wrap">
          <el-table
            :data="transportCargoRows"
            border
            class="business-crud-page__cargo-table"
            empty-text="暂无货物信息"
          >
            <el-table-column type="index" label="序号" width="70" align="center" fixed="left" />
            <el-table-column label="货物名称" min-width="150" align="center">
              <template #default="{ row }">
                <el-input v-model="row.cargoName" placeholder="请输入" :disabled="dialogReadonly" />
              </template>
            </el-table-column>
            <el-table-column min-width="220" align="center">
              <template #header>
                <span class="business-crud-page__required-column">货物类型</span>
              </template>
              <template #default="{ row }">
                <el-cascader
                  v-model="row.cargoTypePath"
                  :options="transportCargoTypeOptions"
                  :props="transportCargoTypeCascaderProps"
                  placeholder="请选择"
                  clearable
                  filterable
                  :disabled="dialogReadonly"
                  :filter-method="filterBillingCargoType"
                  @visible-change="visible => visible && ensureBillingCargoTypeOptions()"
                  @change="value => handleTransportCargoTypeChange(row, value)"
                />
              </template>
            </el-table-column>
            <el-table-column label="数量" min-width="120" align="center">
              <template #default="{ row }">
                <el-input
                  v-model="row.quantity"
                  placeholder="请输入"
                  :disabled="dialogReadonly"
                  @input="value => handleTransportCargoQuantityInput(row, value)"
                />
              </template>
            </el-table-column>
            <el-table-column label="数量单位" min-width="120" align="center">
              <template #default="{ row }">
                <el-select v-model="row.quantityUnit" clearable :disabled="dialogReadonly">
                  <el-option
                    v-for="item in quantityUnitOptions"
                    :key="item"
                    :label="item"
                    :value="item"
                  />
                </el-select>
              </template>
            </el-table-column>
            <el-table-column label="包装" min-width="120" align="center">
              <template #default="{ row }">
                <el-select v-model="row.packageType" clearable :disabled="dialogReadonly">
                  <el-option
                    v-for="item in packageOptions"
                    :key="item.value"
                    :label="item.label"
                    :value="item.value"
                  />
                </el-select>
              </template>
            </el-table-column>
            <el-table-column label="品牌" min-width="130" align="center">
              <template #default="{ row }">
                <el-input v-model="row.brand" placeholder="请输入" :disabled="dialogReadonly" />
              </template>
            </el-table-column>
            <el-table-column label="规格" min-width="130" align="center">
              <template #default="{ row }">
                <el-input
                  v-model="row.specification"
                  placeholder="请输入"
                  :disabled="dialogReadonly"
                />
              </template>
            </el-table-column>
            <el-table-column label="型号" min-width="130" align="center">
              <template #default="{ row }">
                <el-input v-model="row.model" placeholder="请输入" :disabled="dialogReadonly" />
              </template>
            </el-table-column>
            <el-table-column label="物料编码" min-width="150" align="center">
              <template #default="{ row }">
                <el-input
                  v-model="row.materialCode"
                  placeholder="请输入"
                  :disabled="dialogReadonly"
                />
              </template>
            </el-table-column>
            <el-table-column label="设备编码" min-width="150" align="center">
              <template #default="{ row }">
                <el-input
                  v-model="row.deviceCode"
                  placeholder="请输入"
                  :disabled="dialogReadonly"
                />
              </template>
            </el-table-column>
            <el-table-column label="备注" min-width="180" align="center">
              <template #default="{ row }">
                <el-input v-model="row.remark" placeholder="请输入" :disabled="dialogReadonly" />
              </template>
            </el-table-column>
            <el-table-column label="操作" width="140" align="center" fixed="right">
              <template #default="{ $index }">
                <el-button
                  v-if="!dialogReadonly"
                  type="primary"
                  text
                  @click="addTransportCargoRow($index)"
                >
                  新增
                </el-button>
                <el-button
                  v-if="!dialogReadonly"
                  type="primary"
                  text
                  @click="removeTransportCargoRow($index)"
                >
                  删除
                </el-button>
              </template>
            </el-table-column>
          </el-table>
          <div class="business-crud-page__cargo-total">
            运输数量合计：{{ transportCargoQuantityTotal }}
          </div>
        </div>
      </template>

      <template #organizationName-form>
        <el-cascader
          v-model="selectedOrganizationId"
          class="business-crud-page__field"
          placeholder="请选择所属组织"
          :options="organizationOptions"
          :props="organizationCascaderProps"
          clearable
          filterable
          :show-all-levels="false"
          :disabled="dialogReadonly"
          @visible-change="visible => visible && loadOrganizationOptions()"
          @change="handleOrganizationChange"
        />
      </template>

      <template #applyLimit-form>
        <el-input
          v-model="form.applyLimit"
          class="business-crud-page__field"
          placeholder="请输入申请临时额度"
          :disabled="dialogReadonly"
          @input="value => handleAmountInput('applyLimit', value)"
        />
      </template>

      <template #contractPeriod-form>
        <div class="business-crud-page__date-range">
          <el-date-picker
            v-model="form.startDate"
            class="business-crud-page__date"
            type="date"
            placeholder="YYYY-MM-DD"
            format="YYYY-MM-DD"
            value-format="YYYY-MM-DD"
            :disabled="dialogReadonly"
            @change="handleContractDateChange"
          />
          <span class="business-crud-page__date-separator">至</span>
          <el-date-picker
            v-model="form.endDate"
            class="business-crud-page__date"
            type="date"
            placeholder="YYYY-MM-DD"
            format="YYYY-MM-DD"
            value-format="YYYY-MM-DD"
            :disabled="dialogReadonly"
            @change="handleContractDateChange"
          />
        </div>
      </template>

      <template #copyCount-form>
        <div class="business-crud-page__inline-number">
          <el-input
            v-model="form.copyCount"
            placeholder="请输入"
            :disabled="dialogReadonly"
            @input="value => handleIntegerInput('copyCount', value)"
          />
          <span>份</span>
        </div>
      </template>

      <template #paymentDays-form>
        <div class="business-crud-page__inline-number">
          <el-input
            v-model="form.paymentDays"
            placeholder="请输入"
            :disabled="dialogReadonly"
            @input="value => handleIntegerInput('paymentDays', value)"
          />
          <span>天</span>
        </div>
      </template>

      <template #contractFileTitle-form>
        <div class="dialog-section-title">合同文件</div>
      </template>

      <template #contractFileJson-form>
        <div class="business-crud-page__attachment">
          <div class="business-crud-page__attachment-head">
            <vehicle-attachment-upload
              v-model="contractFileRows"
              :readonly="dialogReadonly"
              :file-types="attachmentFileTypes"
              :max-size="500"
              :show-tip="false"
              :show-file-list="false"
              button-text="上传附件"
              @change="handleContractFileChange"
            />
          </div>
          <el-table :data="contractFileRows" border class="business-crud-page__attachment-table">
            <el-table-column type="index" label="序号" width="70" align="center" />
            <el-table-column label="文件名" min-width="240" align="center" show-overflow-tooltip>
              <template #default="{ row }">{{ row.originalName || row.name }}</template>
            </el-table-column>
            <el-table-column label="文件大小" width="120" align="center">
              <template #default="{ row }">{{ formatFileSize(row.size) }}</template>
            </el-table-column>
            <el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
            <el-table-column prop="uploadTime" label="上传时间" width="170" align="center" />
            <el-table-column label="操作" width="120" align="center">
              <template #default="{ row, $index }">
                <el-button
                  v-if="!dialogReadonly"
                  type="primary"
                  text
                  @click="removeContractFile($index)"
                >
                  删除
                </el-button>
                <el-button v-else type="primary" text @click="downloadAttachment(row)">
                  下载
                </el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </template>

      <template #billingInfoTitle-form>
        <div class="dialog-section-title">计费信息</div>
      </template>

      <template #billingPlanJson-form>
        <div class="business-crud-page__billing">
          <div class="business-crud-page__billing-head">
            <div class="business-crud-page__billing-switch">
              <span>自动生成费用明细</span>
              <el-radio-group v-model="form.billingEnabled" :disabled="dialogReadonly">
                <el-radio :label="1">开启</el-radio>
                <el-radio :label="0">关闭</el-radio>
              </el-radio-group>
            </div>
            <el-button v-if="!dialogReadonly" type="primary" @click="openBillingPlan()">
              添加
            </el-button>
          </div>
          <el-table :data="billingPlanRows" border class="business-crud-page__billing-table">
            <el-table-column type="index" label="序号" width="80" align="center" />
            <el-table-column prop="planName" label="方案名称" min-width="220" align="center" />
            <el-table-column label="默认方案" width="140" align="center">
              <template #default="{ row }">{{ row.defaultPlan ? '是' : '' }}</template>
            </el-table-column>
            <el-table-column
              prop="remark"
              label="备注"
              min-width="260"
              align="center"
              show-overflow-tooltip
            />
            <el-table-column label="操作" width="180" align="center">
              <template #default="{ row, $index }">
                <el-button type="primary" text @click="openBillingPlan(row, $index)">
                  编辑
                </el-button>
                <el-button
                  v-if="!dialogReadonly"
                  type="primary"
                  text
                  @click="removeBillingPlan($index)"
                >
                  删除
                </el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </template>

      <template #settlementRuleTitle-form>
        <div class="dialog-section-title">结算生成规则</div>
      </template>

      <template #settlementRuleJson-form>
        <div class="business-crud-page__settlement-rule">
          <div class="business-crud-page__settlement-switch">
            <span>自动生成结算单</span>
            <el-radio-group v-model="settlementRuleForm.autoGenerate" :disabled="dialogReadonly">
              <el-radio :label="1">开启</el-radio>
              <el-radio :label="0">关闭</el-radio>
            </el-radio-group>
          </div>
          <el-form
            :model="settlementRuleForm"
            label-position="right"
            label-width="auto"
            class="business-crud-page__module-form business-crud-page__module-form--three"
          >
            <el-form-item label="账单起始日期" required>
              <el-date-picker
                v-model="settlementRuleForm.billStartDate"
                type="date"
                placeholder="请选择账单起始日期"
                format="YYYY-MM-DD"
                value-format="YYYY-MM-DD"
                :disabled="dialogReadonly"
              />
            </el-form-item>
            <el-form-item label="结算类型" required>
              <el-select
                v-model="settlementRuleForm.settlementType"
                placeholder="请选择结算类型"
                :disabled="dialogReadonly"
                @change="handleSettlementTypeChange"
              >
                <el-option
                  v-for="item in settlementTypeOptions"
                  :key="item"
                  :label="item"
                  :value="item"
                />
              </el-select>
            </el-form-item>
            <el-form-item v-if="showSettlementBillCycleType" label="账单周期类型" required>
              <el-select
                v-model="settlementRuleForm.billCycleType"
                placeholder="请选择账单周期类型"
                :disabled="dialogReadonly"
                @change="handleSettlementBillCycleTypeChange"
              >
                <el-option
                  v-for="item in billCycleTypeOptions"
                  :key="item"
                  :label="item"
                  :value="item"
                />
              </el-select>
            </el-form-item>
            <el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日">
              <el-select
                v-model="settlementRuleForm.billCutoffDay"
                placeholder="请选择账单截单日"
                :disabled="dialogReadonly"
              >
                <el-option
                  v-for="item in billCutoffDayOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
            <el-form-item v-if="showSettlementCycleDays" label="周期天数" required>
              <el-select
                v-model="settlementRuleForm.cycleDays"
                placeholder="请选择周期天数"
                :disabled="dialogReadonly"
              >
                <el-option
                  v-for="item in cycleDayOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-form>
        </div>
      </template>

      <template #reconciliationTitle-form>
        <div class="dialog-section-title">对账配置</div>
      </template>

      <template #reconciliationJson-form>
        <el-form
          :model="reconciliationForm"
          label-position="right"
          label-width="auto"
          class="business-crud-page__module-form business-crud-page__module-form--two"
        >
          <el-form-item label="是否跳过对账" required>
            <el-select
              v-model="reconciliationForm.skipReconciliation"
              placeholder="请选择是否跳过对账"
              :disabled="dialogReadonly"
            >
              <el-option label="是" :value="1" />
              <el-option label="否" :value="0" />
            </el-select>
          </el-form-item>
          <el-form-item label="对账模式" required>
            <el-select
              v-model="reconciliationForm.reconciliationMode"
              placeholder="请选择对账模式"
              :disabled="dialogReadonly"
            >
              <el-option
                v-for="item in reconciliationModeOptions"
                :key="item"
                :label="item"
                :value="item"
              />
            </el-select>
          </el-form-item>
        </el-form>
      </template>

      <template #attachmentTitle-form>
        <div class="dialog-section-title">{{ config.attachmentTitle || '其它附件' }}</div>
      </template>

      <template #attachmentsJson-form>
        <div class="business-crud-page__attachment">
          <div class="business-crud-page__attachment-head">
            <vehicle-attachment-upload
              v-model="attachmentRows"
              :readonly="dialogReadonly"
              :file-types="attachmentFileTypes"
              :max-size="500"
              :show-tip="false"
              :show-file-list="false"
              button-text="上传附件"
              @change="handleAttachmentChange"
            />
            <el-button
              type="primary"
              :disabled="!attachmentRows.length"
              @click="handleBatchDownload"
            >
              批量下载
            </el-button>
          </div>
          <el-table :data="attachmentRows" border class="business-crud-page__attachment-table">
            <el-table-column type="index" label="序号" width="70" align="center" />
            <el-table-column label="文件名" min-width="200" align="center" show-overflow-tooltip>
              <template #default="{ row }">{{ row.originalName || row.name }}</template>
            </el-table-column>
            <el-table-column label="附件描述" min-width="220" align="center">
              <template #default="{ row }">
                <el-input
                  v-model="row.description"
                  placeholder="请输入附件描述"
                  :disabled="dialogReadonly"
                />
              </template>
            </el-table-column>
            <el-table-column label="文件大小" width="110" align="center">
              <template #default="{ row }">{{ formatFileSize(row.size) }}</template>
            </el-table-column>
            <el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
            <el-table-column prop="uploadTime" label="上传时间" width="160" align="center" />
            <el-table-column label="操作" width="100" align="center">
              <template #default="{ row, $index }">
                <el-button
                  v-if="!dialogReadonly"
                  type="primary"
                  text
                  @click="removeAttachment($index)"
                >
                  删除
                </el-button>
                <el-button v-else type="primary" text @click="downloadAttachment(row)">
                  下载
                </el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </template>

      <template #changeRecordTitle-form>
        <div class="dialog-section-title">变更记录</div>
      </template>

      <template #changeRecordJson-form>
        <el-table :data="changeRecordRows" border class="business-crud-page__change-table">
          <el-table-column type="index" label="序号" width="70" align="center" />
          <el-table-column prop="changeDate" label="变更日期" min-width="150" align="center" />
          <el-table-column prop="handlerUserName" label="经办人" min-width="140" align="center" />
          <el-table-column prop="changeType" label="变更类型" min-width="160" align="center" />
          <el-table-column
            prop="changeReason"
            label="变更原因"
            min-width="240"
            align="center"
            show-overflow-tooltip
          />
          <el-table-column prop="statusName" label="状态" min-width="140" align="center" />
          <el-table-column label="操作" width="120" align="center">
            <template #default="{ row }">
              <el-button type="primary" text @click="handleChangeRecordFlow(row)"> 流程 </el-button>
            </template>
          </el-table-column>
        </el-table>
      </template>

      <template #menu-form-before>
        <el-button
          v-if="config.enableDraftSave && !dialogReadonly"
          type="primary"
          plain
          :loading="draftSaveLoading"
          @click="handleDraftSave"
        >
          保存
        </el-button>
      </template>

      <template #menu-form>
        <div v-if="showTransportPlanCreateActions" class="business-crud-page__create-actions">
          <el-button :disabled="Boolean(transportPlanCreateActionLoading)" @click="closeCrudDialog">
            关闭
          </el-button>
          <el-button
            type="primary"
            plain
            :loading="transportPlanCreateActionLoading === 'draft'"
            :disabled="Boolean(transportPlanCreateActionLoading)"
            @click="handleTransportPlanCreateAction('draft')"
          >
            暂存
          </el-button>
          <el-button
            type="primary"
            :loading="transportPlanCreateActionLoading === 'create'"
            :disabled="Boolean(transportPlanCreateActionLoading)"
            @click="handleTransportPlanCreateAction('create')"
          >
            确认创建
          </el-button>
        </div>
        <div v-if="showContractCreateActions" class="business-crud-page__contract-actions">
          <el-button
            type="primary"
            plain
            :loading="contractCreateActionLoading === 'draft'"
            :disabled="Boolean(contractCreateActionLoading)"
            @click="handleContractCreateAction('draft')"
          >
            保存
          </el-button>
          <el-button
            type="primary"
            :loading="contractCreateActionLoading === 'temporary'"
            :disabled="Boolean(contractCreateActionLoading)"
            @click="handleContractCreateAction('temporary')"
          >
            提交临时合同
          </el-button>
          <el-button
            type="primary"
            :loading="contractCreateActionLoading === 'formal'"
            :disabled="Boolean(contractCreateActionLoading)"
            @click="handleContractCreateAction('formal')"
          >
            提交正式合同
          </el-button>
        </div>
      </template>

      <template #contractStage="{ row }">
        <el-tag :type="stageTagType(row.contractStage)">
          {{ displayStatus(row, 'contractStage') }}
        </el-tag>
      </template>

      <template #menu="{ row, index }">
        <template v-if="config.customMenuActions">
          <el-button
            v-for="action in menuActions(row)"
            :key="action.key"
            :type="action.type || 'primary'"
            text
            @click="handleMenuAction(action, row, index)"
          >
            {{ action.label }}
          </el-button>
        </template>
        <template v-else>
          <el-button
            type="primary"
            text
            v-if="hasPermission(`${config.permission}_view`)"
            @click="$refs.crud.rowView(row, index)"
          >
            查看
          </el-button>
          <el-button
            type="primary"
            text
            v-if="canEdit(row)"
            @click="$refs.crud.rowEdit(row, index)"
          >
            编辑
          </el-button>
          <el-button type="primary" text v-if="canCopy(row)" @click="handleCopy(row)">
            复制
          </el-button>
          <el-button type="primary" text v-if="canEnable(row)" @click="handleAction('enable', row)">
            启用
          </el-button>
          <el-button
            type="primary"
            text
            v-if="canDisable(row)"
            @click="handleAction('disable', row)"
          >
            停用
          </el-button>
          <el-button
            type="primary"
            text
            v-if="canReassign(row)"
            @click="handleAction('reassign', row)"
          >
            重新派单
          </el-button>
          <el-button type="primary" text v-if="canCancel(row)" @click="handleAction('cancel', row)">
            取消
          </el-button>
          <el-button
            type="primary"
            text
            v-if="canComplete(row)"
            @click="handleAction('complete', row)"
          >
            完成
          </el-button>
          <el-button
            v-for="operation in customOperations(row)"
            :key="operation.action"
            :type="operation.type || 'primary'"
            text
            @click="handleCustomOperation(operation, row)"
          >
            {{ operation.label }}
          </el-button>
          <el-button type="primary" text v-if="canDelete(row)" @click="rowDel(row)">
            删除
          </el-button>
        </template>
      </template>
    </avue-crud>

    <empty-pagination
      :page="page"
      @size-change="sizeChange"
      @current-change="currentChange"
      @load="onLoad(page, query)"
    />

    <flow-design
      v-if="website.design.designMode"
      is-dialog
      v-model:is-display="flowBox"
      :process-instance-id="processInstanceId"
    />
    <el-dialog v-else title="流程图" append-to-body v-model="flowBox" :fullscreen="true">
      <iframe
        :src="flowUrl"
        width="100%"
        height="700"
        title="流程图"
        frameBorder="no"
        border="0"
        marginWidth="0"
        marginHeight="0"
        scrolling="no"
        allowTransparency="yes"
      />
      <template #footer>
        <el-button @click="flowBox = false">关闭</el-button>
      </template>
    </el-dialog>

    <el-dialog
      :title="billingPlanMode === 'edit' ? '编辑计费方案' : '添加计费方案'"
      append-to-body
      v-model="billingPlanBox"
      width="96%"
      class="business-crud-page__billing-dialog"
    >
      <el-form
        ref="billingPlanFormRef"
        :model="billingPlanForm"
        :rules="billingPlanRules"
        label-position="right"
        label-width="auto"
      >
        <div class="business-crud-page__billing-form-row">
          <el-form-item label="方案名称" prop="planName" required>
            <el-input
              v-model="billingPlanForm.planName"
              maxlength="100"
              :disabled="dialogReadonly"
            />
          </el-form-item>
          <el-form-item label="" class="business-crud-page__billing-default">
            <el-checkbox v-model="billingPlanForm.defaultPlan" :disabled="dialogReadonly">
              默认方案
            </el-checkbox>
          </el-form-item>
        </div>
        <el-form-item label="备注">
          <el-input
            v-model="billingPlanForm.remark"
            maxlength="100"
            show-word-limit
            :disabled="dialogReadonly"
          />
        </el-form-item>
      </el-form>

      <div class="business-crud-page__billing-rule-head">
        <el-button v-if="!dialogReadonly" type="primary" text @click="addBillingRule">
          +添加规则
        </el-button>
      </div>
      <div class="business-crud-page__billing-rule-wrap">
        <el-table
          :data="billingPlanForm.rules"
          border
          class="business-crud-page__billing-rule-table"
        >
          <el-table-column type="index" label="序号" width="70" align="center" fixed="left" />
          <el-table-column width="180" align="center">
            <template #header>
              <span class="business-crud-page__required-column">费用类型</span>
            </template>
            <template #default="{ row }">
              <el-select
                v-model="row.feeType"
                class="business-crud-page__billing-field"
                clearable
                filterable
                :disabled="dialogReadonly"
                :loading="billingFeeCategoryLoading"
                placeholder="请选择"
                @change="value => handleBillingFeeTypeChange(row, value)"
              >
                <el-option
                  v-for="item in billingFeeCategoryOptions"
                  :key="item.id || item.dictKey"
                  :label="item.dictValue"
                  :value="item.dictKey"
                />
              </el-select>
            </template>
          </el-table-column>
          <el-table-column width="210" align="center">
            <template #header>
              <span class="business-crud-page__required-column">费用项</span>
            </template>
            <template #default="{ row }">
              <el-select
                v-model="row.feeItem"
                class="business-crud-page__billing-field"
                clearable
                filterable
                :disabled="dialogReadonly || !row.feeType"
                :loading="billingFeeItemLoadingMap[resolveBillingFeeType(row.feeType)]"
                :placeholder="row.feeType ? '请选择' : '请先选择费用类型'"
              >
                <el-option
                  v-for="item in getBillingFeeItemOptions(row.feeType)"
                  :key="item.id || item.name || item.englishName"
                  :label="item.name || item.englishName"
                  :value="item.name || item.englishName"
                />
              </el-select>
            </template>
          </el-table-column>
          <el-table-column width="170" align="center">
            <template #header>
              <span class="business-crud-page__required-column">计费要素</span>
            </template>
            <template #default="{ row }">
              <el-select
                v-model="row.billingElement"
                :disabled="dialogReadonly"
                placeholder="请选择"
                @change="handleBillingElementChange(row)"
              >
                <el-option
                  v-for="item in billingElementOptions"
                  :key="item"
                  :label="item"
                  :value="item"
                />
              </el-select>
            </template>
          </el-table-column>
          <el-table-column width="180" align="center">
            <template #header>
              <span class="business-crud-page__required-column">计费类型</span>
            </template>
            <template #default="{ row }">
              <el-select
                v-model="row.billingType"
                :disabled="dialogReadonly || !row.billingElement"
                :placeholder="row.billingElement ? '请选择' : '请先选择计费要素'"
                @change="handleBillingTypeChange(row)"
              >
                <el-option
                  v-for="item in getBillingTypeOptions(row)"
                  :key="item"
                  :label="item"
                  :value="item"
                />
              </el-select>
            </template>
          </el-table-column>
          <el-table-column width="150" align="center">
            <template #header>
              <span class="business-crud-page__required-column">计费单位</span>
            </template>
            <template #default="{ row }">
              <el-select
                v-model="row.billingUnit"
                clearable
                filterable
                :disabled="dialogReadonly"
                :loading="billingUnitLoading"
                placeholder="请选择"
              >
                <el-option
                  v-for="item in billingUnitOptions"
                  :key="item.id || item.dictKey || item.dictValue"
                  :label="item.dictValue"
                  :value="item.dictValue"
                />
              </el-select>
            </template>
          </el-table-column>
          <el-table-column width="140" align="center">
            <template #header>
              <span class="business-crud-page__required-column">单价（元）</span>
            </template>
            <template #default="{ row }">
              <el-input
                v-model="row.unitPrice"
                :disabled="dialogReadonly"
                placeholder="请输入"
                @input="value => handleBillingDecimalInput(row, 'unitPrice', value)"
              />
            </template>
          </el-table-column>
          <el-table-column label="计费要素下限" width="260" align="center">
            <template #default="{ row }">
              <div class="business-crud-page__limit-list">
                <el-input
                  v-for="(item, rangeIndex) in getBillingLimitRanges(row)"
                  :key="rangeIndex"
                  v-model="item.lowerLimit"
                  :disabled="!canEditBillingLimit(row)"
                  :placeholder="canEditBillingLimit(row) ? '请输入下限' : '无需配置'"
                  @input="value => handleBillingLimitInput(row, rangeIndex, 'lowerLimit', value)"
                />
              </div>
            </template>
          </el-table-column>
          <el-table-column label="计费要素上限" width="260" align="center">
            <template #default="{ row }">
              <div class="business-crud-page__limit-list">
                <div
                  v-for="(item, rangeIndex) in getBillingLimitRanges(row)"
                  :key="rangeIndex"
                  class="business-crud-page__limit-row"
                >
                  <el-input
                    v-model="item.upperLimit"
                    :disabled="!canEditBillingLimit(row)"
                    :placeholder="canEditBillingLimit(row) ? '请输入上限' : '无需配置'"
                    @input="value => handleBillingLimitInput(row, rangeIndex, 'upperLimit', value)"
                  />
                  <el-button
                    v-if="canEditBillingLimit(row)"
                    type="primary"
                    text
                    @click="addBillingLimitRange(row, rangeIndex)"
                  >
                    添加
                  </el-button>
                  <el-button
                    v-if="canEditBillingLimit(row) && getBillingLimitRanges(row).length > 1"
                    type="primary"
                    text
                    @click="removeBillingLimitRange(row, rangeIndex)"
                  >
                    删除
                  </el-button>
                </div>
              </div>
            </template>
          </el-table-column>
          <el-table-column label="保底计费重量" width="240" align="center">
            <template #default="{ row }">
              <el-input
                v-model="row.minimumBillingWeight"
                :disabled="!canEditMinimumBillingWeight(row)"
                :placeholder="
                  canEditMinimumBillingWeight(row)
                    ? '实际计量值小于保底数值时按保底计费'
                    : '仅按重量、按吨·公里可填写'
                "
                @input="value => handleBillingDecimalInput(row, 'minimumBillingWeight', value)"
              />
            </template>
          </el-table-column>
          <el-table-column label="备注" width="180" align="center">
            <template #default="{ row }">
              <el-input
                v-model="row.remark"
                maxlength="100"
                show-word-limit
                :disabled="dialogReadonly"
                placeholder="请输入"
              />
            </template>
          </el-table-column>
          <el-table-column label="操作" width="230" align="center" fixed="right">
            <template #default="{ row, $index }">
              <el-button type="primary" text @click="openBillingMatch(row, $index)">
                设置匹配条件
              </el-button>
              <el-button v-if="!dialogReadonly" type="primary" text @click="copyBillingRule(row)">
                复制
              </el-button>
              <el-button
                v-if="!dialogReadonly"
                type="primary"
                text
                @click="removeBillingRule($index)"
              >
                删除
              </el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>

      <template #footer>
        <div class="business-crud-page__dialog-footer">
          <el-button type="primary" :disabled="dialogReadonly" @click="submitBillingPlan">
            提交
          </el-button>
          <el-button @click="billingPlanBox = false">取消</el-button>
        </div>
      </template>
    </el-dialog>

    <el-dialog title="设置匹配条件" append-to-body v-model="billingMatchBox" width="720px">
      <el-form
        :model="billingMatchForm"
        label-position="right"
        label-width="90px"
        class="business-crud-page__match-form"
      >
        <el-row :gutter="48">
          <el-col :span="12">
            <el-form-item label="起运地">
              <el-cascader
                v-model="billingMatchForm.originPath"
                :options="billingMatchRegionOptions"
                :props="billingMatchRegionCascaderProps"
                :placeholder="billingMatchForm.origin || '请选择起运地'"
                :loading="billingMatchRegionLoading"
                :disabled="dialogReadonly"
                clearable
                filterable
                @visible-change="visible => visible && ensureBillingMatchRegionOptions()"
                @change="value => handleBillingMatchRegionChange('origin', value)"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="目的地">
              <el-cascader
                v-model="billingMatchForm.destinationPath"
                :options="billingMatchRegionOptions"
                :props="billingMatchRegionCascaderProps"
                :placeholder="billingMatchForm.destination || '请选择目的地'"
                :loading="billingMatchRegionLoading"
                :disabled="dialogReadonly"
                clearable
                filterable
                @visible-change="visible => visible && ensureBillingMatchRegionOptions()"
                @change="value => handleBillingMatchRegionChange('destination', value)"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="运输方式">
              <el-select
                v-model="billingMatchForm.transportMode"
                :disabled="dialogReadonly"
                clearable
              >
                <el-option label="公路" value="公路" />
                <el-option label="铁路" value="铁路" />
                <el-option label="水路" value="水路" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="货物类型">
              <el-cascader
                v-model="billingMatchForm.cargoTypePath"
                :options="billingCargoTypeOptions"
                :props="billingCargoTypeCascaderProps"
                :placeholder="billingMatchForm.cargoType || '请选择货物类型'"
                :loading="billingCargoTypeLoading"
                :disabled="dialogReadonly"
                clearable
                filterable
                :filter-method="filterBillingCargoType"
                @visible-change="visible => visible && ensureBillingCargoTypeOptions()"
                @change="handleBillingMatchCargoTypeChange"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <el-button @click="billingMatchBox = false">取消</el-button>
        <el-button type="primary" :disabled="dialogReadonly" @click="saveBillingMatch">
          保存
        </el-button>
      </template>
    </el-dialog>

    <el-dialog
      title="选择线路"
      append-to-body
      v-model="transportRouteBox"
      width="1280px"
      @opened="loadTransportRouteList"
    >
      <div class="business-crud-page__dialog-search">
        <el-form :model="transportRouteQuery" label-position="right" label-width="86px">
          <el-row :gutter="16">
            <el-col :span="6">
              <el-form-item label="线路名称">
                <el-input v-model="transportRouteQuery.routeName" clearable placeholder="请输入" />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="发货地址">
                <el-input
                  v-model="transportRouteQuery.departureAddress"
                  clearable
                  placeholder="请输入"
                />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="收货地址">
                <el-input
                  v-model="transportRouteQuery.arrivalAddress"
                  clearable
                  placeholder="请输入"
                />
              </el-form-item>
            </el-col>
            <el-col :span="6" class="business-crud-page__dialog-search-actions">
              <el-button type="primary" @click="handleTransportRouteSearch">查询</el-button>
              <el-button @click="handleTransportRouteReset">重置</el-button>
            </el-col>
          </el-row>
        </el-form>
      </div>
      <el-table border height="320" :data="transportRouteRows" v-loading="transportRouteLoading">
        <el-table-column type="index" label="序号" width="70" align="center" />
        <el-table-column prop="routeName" label="线路名称" min-width="150" />
        <el-table-column prop="routeCode" label="线路编号" min-width="140" />
        <el-table-column prop="departureName" label="发货地" min-width="140" />
        <el-table-column
          prop="departureAddress"
          label="发货地址"
          min-width="220"
          show-overflow-tooltip
        />
        <el-table-column prop="departureContact" label="发货联系人" min-width="120" />
        <el-table-column prop="departurePhone" label="发货联系方式" min-width="140" />
        <el-table-column prop="arrivalName" label="收货地" min-width="140" />
        <el-table-column
          prop="arrivalAddress"
          label="收货地址"
          min-width="220"
          show-overflow-tooltip
        />
        <el-table-column prop="arrivalContact" label="收货联系人" min-width="120" />
        <el-table-column prop="arrivalPhone" label="收货联系方式" min-width="140" />
        <el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
        <el-table-column prop="deptName" label="组织" min-width="150" show-overflow-tooltip />
        <el-table-column
          prop="updateUserName"
          label="更新人"
          min-width="120"
          :formatter="formatTransportRouteUpdateUserName"
        />
        <el-table-column prop="updateTime" label="更新时间" min-width="160" />
        <el-table-column prop="createTime" label="创建时间" min-width="160" />
        <el-table-column label="操作" width="110" fixed="right">
          <template #default="{ row }">
            <el-button type="primary" text @click.stop="selectTransportRouteRow(row)">
              选择
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="business-crud-page__dialog-pagination">
        <el-pagination
          v-model:current-page="transportRoutePage.currentPage"
          v-model:page-size="transportRoutePage.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, prev, pager, next, sizes"
          :total="transportRoutePage.total"
          @size-change="handleTransportRouteSizeChange"
          @current-change="handleTransportRouteCurrentChange"
        />
      </div>
      <template #footer>
        <el-button @click="transportRouteBox = false">关闭</el-button>
      </template>
    </el-dialog>

    <el-dialog
      title="选择常用地址"
      append-to-body
      v-model="transportAddressBox"
      width="1280px"
      @opened="loadTransportAddressList"
    >
      <div class="business-crud-page__dialog-search">
        <el-form :model="transportAddressQuery" label-position="right" label-width="86px">
          <el-row :gutter="16">
            <el-col :span="6">
              <el-form-item label="地址名称">
                <el-input
                  v-model="transportAddressQuery.addressName"
                  clearable
                  placeholder="请输入"
                />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="地址类型">
                <el-select
                  v-model="transportAddressQuery.addressType"
                  clearable
                  filterable
                  placeholder="请选择"
                >
                  <el-option
                    v-for="item in addressTypeOptions"
                    :key="item.value"
                    :label="item.label"
                    :value="item.value"
                  />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="详细地址">
                <el-input
                  v-model="transportAddressQuery.detailAddress"
                  clearable
                  placeholder="请输入"
                />
              </el-form-item>
            </el-col>
            <el-col :span="6" class="business-crud-page__dialog-search-actions">
              <el-button type="primary" @click="handleTransportAddressSearch">查询</el-button>
              <el-button @click="handleTransportAddressReset">重置</el-button>
            </el-col>
          </el-row>
        </el-form>
      </div>
      <el-table
        border
        height="320"
        :data="transportAddressRows"
        v-loading="transportAddressLoading"
      >
        <el-table-column type="index" label="序号" width="70" align="center" />
        <el-table-column prop="addressName" label="地址名称" min-width="150" />
        <el-table-column prop="addressCode" label="地址编号" min-width="130" />
        <el-table-column prop="addressType" label="类型" min-width="120" />
        <el-table-column
          prop="siteCodeDisplay"
          label="站点编码"
          min-width="120"
          :formatter="formatTransportAddressSiteCode"
        />
        <el-table-column
          prop="detailAddress"
          label="详细地址"
          min-width="260"
          show-overflow-tooltip
        />
        <el-table-column prop="longitude" label="经度" min-width="120" />
        <el-table-column prop="latitude" label="纬度" min-width="120" />
        <el-table-column prop="regionName" label="行政区划" min-width="150" show-overflow-tooltip />
        <el-table-column prop="contactName" label="联系人" min-width="120" />
        <el-table-column prop="contactPhone" label="联系方式" min-width="140" />
        <el-table-column prop="deptName" label="组织" min-width="150" show-overflow-tooltip />
        <el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
        <el-table-column prop="updateTime" label="更新时间" min-width="160" />
        <el-table-column prop="createTime" label="创建时间" min-width="160" />
        <el-table-column label="操作" width="110" fixed="right">
          <template #default="{ row }">
            <el-button type="primary" text @click.stop="selectTransportAddressRow(row)">
              选择
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="business-crud-page__dialog-pagination">
        <el-pagination
          v-model:current-page="transportAddressPage.currentPage"
          v-model:page-size="transportAddressPage.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, prev, pager, next, sizes"
          :total="transportAddressPage.total"
          @size-change="handleTransportAddressSizeChange"
          @current-change="handleTransportAddressCurrentChange"
        />
      </div>
      <template #footer>
        <el-button @click="transportAddressBox = false">关闭</el-button>
      </template>
    </el-dialog>

    <el-dialog
      title="选择站点"
      append-to-body
      v-model="transportStationBox"
      width="1180px"
      @opened="loadTransportStationList"
    >
      <div class="business-crud-page__dialog-search">
        <el-form :model="transportStationQuery" label-position="right" label-width="86px">
          <el-row :gutter="16">
            <el-col :span="6">
              <el-form-item label="站点名称">
                <el-input v-model="transportStationQuery.name" clearable placeholder="请输入" />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="站点类型">
                <el-input :model-value="transportStationTypeLabel" disabled />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="详细地址">
                <el-input
                  v-model="transportStationQuery.detailAddress"
                  clearable
                  placeholder="请输入"
                />
              </el-form-item>
            </el-col>
            <el-col :span="6" class="business-crud-page__dialog-search-actions">
              <el-button type="primary" @click="handleTransportStationSearch">查询</el-button>
              <el-button @click="handleTransportStationReset">重置</el-button>
            </el-col>
          </el-row>
        </el-form>
      </div>
      <el-table
        border
        height="320"
        :data="transportStationRows"
        v-loading="transportStationLoading"
      >
        <el-table-column type="index" label="序号" width="70" align="center" />
        <el-table-column prop="stationName" label="站点名" min-width="160" />
        <el-table-column prop="stationType" label="类型" min-width="120" />
        <el-table-column prop="stationCode" label="站点编码" min-width="130" />
        <el-table-column
          prop="detailAddress"
          label="详细地址"
          min-width="260"
          show-overflow-tooltip
        />
        <el-table-column prop="longitude" label="经度" min-width="120" />
        <el-table-column prop="latitude" label="纬度" min-width="120" />
        <el-table-column prop="regionName" label="行政区划" min-width="160" show-overflow-tooltip />
        <el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
        <el-table-column prop="updateTime" label="更新时间" min-width="160" />
        <el-table-column prop="createTime" label="创建时间" min-width="160" />
        <el-table-column label="操作" width="110" fixed="right">
          <template #default="{ row }">
            <el-button type="primary" text @click.stop="selectTransportStationRow(row)">
              选择
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="business-crud-page__dialog-pagination">
        <el-pagination
          v-model:current-page="transportStationPage.currentPage"
          v-model:page-size="transportStationPage.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, prev, pager, next, sizes"
          :total="transportStationPage.total"
          @size-change="handleTransportStationSizeChange"
          @current-change="handleTransportStationCurrentChange"
        />
      </div>
      <template #footer>
        <el-button @click="transportStationBox = false">关闭</el-button>
      </template>
    </el-dialog>

    <el-dialog
      title="地图选择地址"
      append-to-body
      v-model="transportMapBox"
      width="920px"
      @opened="initTransportAmap"
    >
      <div class="business-crud-page__map-toolbar">
        <el-input
          v-model="transportMapKeyword"
          clearable
          placeholder="输入地址关键词"
          @keyup.enter="searchTransportMapKeyword"
        />
        <el-button
          type="primary"
          :icon="Search"
          :loading="transportMapLoading"
          @click="searchTransportMapKeyword"
        >
          搜索
        </el-button>
      </div>
      <div ref="transportAmap" class="business-crud-page__map"></div>
      <div class="business-crud-page__map-info">
        <span>{{ transportMapStatus }}</span>
        <span v-if="transportMapSelected.regionName"
          >行政区划：{{ transportMapSelected.regionName }}</span
        >
      </div>
      <template #footer>
        <el-button @click="transportMapBox = false">取消</el-button>
        <el-button
          type="primary"
          :disabled="!transportMapSelected.longitude"
          @click="confirmTransportMapPick"
        >
          确定
        </el-button>
      </template>
    </el-dialog>

    <el-dialog
      title="选择常用货物"
      append-to-body
      v-model="commonCargoBox"
      width="1280px"
      @opened="loadCommonCargoList"
    >
      <div class="business-crud-page__dialog-search">
        <el-form :model="commonCargoQuery" label-position="right" label-width="100px">
          <el-row :gutter="16">
            <el-col :span="6">
              <el-form-item label="货物名称">
                <el-input v-model="commonCargoQuery.cargoName" clearable placeholder="请输入" />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="货物类型">
                <el-cascader
                  v-model="commonCargoQuery.cargoTypePath"
                  :options="billingCargoTypeOptions"
                  :props="billingCargoTypeCascaderProps"
                  placeholder="请选择"
                  clearable
                  filterable
                  :filter-method="filterBillingCargoType"
                  @visible-change="visible => visible && ensureBillingCargoTypeOptions()"
                  @change="handleCommonCargoTypeChange"
                />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="规格型号">
                <el-input
                  v-model="commonCargoQuery.specificationModel"
                  clearable
                  placeholder="请输入"
                />
              </el-form-item>
            </el-col>
            <el-col :span="6" class="business-crud-page__dialog-search-actions">
              <el-button type="primary" @click="handleCommonCargoSearch">查询</el-button>
              <el-button @click="handleCommonCargoReset">重置</el-button>
            </el-col>
          </el-row>
        </el-form>
      </div>
      <el-table border height="320" :data="commonCargoRows" v-loading="commonCargoLoading">
        <el-table-column type="index" label="序号" width="70" align="center" />
        <el-table-column prop="cargoName" label="货物名称" min-width="150" />
        <el-table-column prop="cargoCode" label="货物编号" min-width="130" />
        <el-table-column prop="firstCargoTypeName" label="一级货物类型" min-width="150" />
        <el-table-column prop="secondCargoTypeName" label="二级货物类型" min-width="150" />
        <el-table-column prop="secondCargoTypeCode" label="二级货物编码" min-width="140" />
        <el-table-column prop="packageType" label="包装" min-width="100" />
        <el-table-column prop="brand" label="品牌" min-width="120" />
        <el-table-column prop="specification" label="规格" min-width="120" />
        <el-table-column prop="model" label="型号" min-width="120" />
        <el-table-column prop="cargoValue" label="单价" min-width="120" />
        <el-table-column prop="priceUnit" label="计价单位" min-width="120" />
        <el-table-column
          prop="cargoSize"
          label="尺寸"
          min-width="120"
          :formatter="formatCommonCargoSize"
        />
        <el-table-column
          prop="descriptionOne"
          label="其他说明1"
          min-width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="descriptionTwo"
          label="其他说明2"
          min-width="160"
          show-overflow-tooltip
        />
        <el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
        <el-table-column prop="deptName" label="组织" min-width="150" show-overflow-tooltip />
        <el-table-column prop="updateTime" label="更新时间" min-width="160" />
        <el-table-column prop="createTime" label="创建时间" min-width="160" />
        <el-table-column label="操作" width="110" fixed="right">
          <template #default="{ row }">
            <el-button type="primary" text @click.stop="selectCommonCargoRow(row)">
              选择
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="business-crud-page__dialog-pagination">
        <el-pagination
          v-model:current-page="commonCargoPage.currentPage"
          v-model:page-size="commonCargoPage.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, prev, pager, next, sizes"
          :total="commonCargoPage.total"
          @size-change="handleCommonCargoSizeChange"
          @current-change="handleCommonCargoCurrentChange"
        />
      </div>
      <template #footer>
        <el-button @click="commonCargoBox = false">关闭</el-button>
      </template>
    </el-dialog>

    <el-dialog title="导入货物" append-to-body v-model="cargoImportBox" width="560px">
      <el-upload
        drag
        action="#"
        accept=".xls,.xlsx"
        :auto-upload="false"
        :show-file-list="false"
        :disabled="cargoImportLoading"
        :on-change="handleCargoImportFileChange"
      >
        <i class="el-icon-upload"></i>
        <div class="el-upload__text">将 Excel 文件拖到此处，或点击上传</div>
        <template #tip>
          <div class="el-upload__tip">
            按货物信息表头读取：货物名称、货物类型、数量、数量单位、包装、品牌、规格、型号、物料编码、设备编码、备注
          </div>
        </template>
      </el-upload>
      <template #footer>
        <el-button @click="cargoImportBox = false">关闭</el-button>
      </template>
    </el-dialog>

    <el-dialog :title="`${config.title}导入`" append-to-body v-model="excelBox" width="555px">
      <avue-form :option="excelOptionConfig" v-model="excelForm">
        <template #excelTemplate>
          <el-button type="primary" @click="handleTemplate">
            点击下载<i class="el-icon-download el-icon--right"></i>
          </el-button>
        </template>
      </avue-form>
    </el-dialog>
  </basic-container>
</template>

<script>
import { exportBlob } from '@/api/common';
import { getList as getCommonAddressList } from '@/api/base/common-address';
import { getList as getAirportMasterList } from '@/api/base/airport-master';
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
import { getList as getFeeItemList } from '@/api/base/fee-item';
import { getList as getPortTerminalList } from '@/api/base/port-terminal';
import { getList as getRailwayStationList } from '@/api/base/railway-station';
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
import { getList as getContractList } from '@/api/business/contract-manage';
import { getList as getProjectList } from '@/api/business/project-apply';
import { getList as getCommonRouteList } from '@/api/business/common-route';
import { getDictionary } from '@/api/system/dictbiz';
import { getDeptTree } from '@/api/system/dept';
import { addressTypeOptions } from '@/option/base/common-address';
import { packageOptions } from '@/option/business/common';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel';
import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { List, Location, Search } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';

const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
let amapLoader;

const estimateMenuButtonCount = config => {
  const rowActions = (config.actions || []).filter(action => action !== 'batchComplete');
  return 3 + rowActions.length + (config.operations || []).length;
};

const extractRecords = res => {
  const data = res?.data?.data || res?.data || res;
  if (Array.isArray(data)) return data;
  if (Array.isArray(data?.records)) return data.records;
  if (Array.isArray(data?.data)) return data.data;
  if (Array.isArray(data?.data?.records)) return data.data.records;
  return [];
};

const defaultBillingRule = () => ({
  feeType: '',
  feeItem: '',
  billingElement: '',
  billingType: '',
  billingUnit: '',
  unitPrice: '',
  lowerLimit: '',
  upperLimit: '',
  limitRanges: [{ lowerLimit: '', upperLimit: '' }],
  minimumBillingWeight: '',
  remark: '',
  matchCondition: {
    origin: '',
    originCode: '',
    originPath: [],
    destination: '',
    destinationCode: '',
    destinationPath: [],
    transportMode: '',
    cargoType: '',
    cargoTypeCode: '',
    cargoTypePath: [],
  },
});

const defaultBillingPlan = () => ({
  planName: '',
  defaultPlan: false,
  remark: '',
  rules: [defaultBillingRule()],
});

const defaultSettlementRule = () => ({
  autoGenerate: 1,
  billStartDate: '',
  settlementType: '月结',
  billCycleType: '固定截单日',
  billCutoffDay: 25,
  cycleDays: '',
});

const defaultReconciliation = () => ({
  skipReconciliation: 1,
  reconciliationMode: '明细逐行核对',
});

const defaultTransportCargo = () => ({
  cargoName: '',
  cargoType: '',
  cargoTypeCode: '',
  cargoTypePath: [],
  quantity: '',
  quantityUnit: '',
  packageType: '',
  brand: '',
  specification: '',
  model: '',
  materialCode: '',
  deviceCode: '',
  remark: '',
});

export default {
  name: 'BusinessCrudPage',
  props: {
    api: {
      type: Object,
      required: true,
    },
    config: {
      type: Object,
      required: true,
    },
    crudOption: {
      type: Object,
      required: true,
    },
    crudExcelOption: {
      type: Object,
      default: () => ({ column: [] }),
    },
  },
  data() {
    return {
      List,
      Location,
      Search,
      form: {},
      suppressTransportTypeClear: false,
      query: {},
      loading: true,
      data: [],
      allDept: 0,
      dialogReadonly: false,
      draftSaveLoading: false,
      crudDialogType: '',
      defaultDialogButtons: null,
      contractCreateActionLoading: '',
      transportPlanCreateActionLoading: '',
      option: this.cloneOption(this.crudOption, estimateMenuButtonCount(this.config)),
      selectedProjectId: '',
      projectOptions: [],
      projectLoading: false,
      contractOptions: [],
      contractLoading: false,
      selectedOrganizationId: '',
      organizationOptions: [],
      organizationFlatOptions: [],
      organizationLoading: false,
      addressTypeOptions,
      flowBox: false,
      flowUrl: '',
      processInstanceId: '',
      attachmentRows: [],
      contractFileRows: [],
      transportCargoRows: [],
      transportRouteBox: false,
      transportRouteLoading: false,
      transportRouteQuery: {},
      transportRouteRows: [],
      transportRouteSelected: null,
      transportRoutePage: {
        pageSize: 10,
        currentPage: 1,
        total: 0,
      },
      transportAddressBox: false,
      transportAddressTarget: '',
      transportAddressLoading: false,
      transportAddressQuery: {},
      transportAddressRows: [],
      transportAddressSelected: null,
      transportAddressPage: {
        pageSize: 10,
        currentPage: 1,
        total: 0,
      },
      transportStationBox: false,
      transportStationTarget: '',
      transportStationLoading: false,
      transportStationQuery: {},
      transportStationRows: [],
      transportStationPage: {
        pageSize: 10,
        currentPage: 1,
        total: 0,
      },
      transportMapBox: false,
      transportMapTarget: '',
      transportMapLoading: false,
      transportMapKeyword: '',
      transportMapStatus: '可搜索地址或点击地图选点',
      transportMapSelected: {},
      transportAmap: null,
      transportAmapMarker: null,
      transportAmapGeocoder: null,
      commonCargoBox: false,
      commonCargoLoading: false,
      commonCargoQuery: {},
      commonCargoRows: [],
      commonCargoSelected: [],
      commonCargoPage: {
        pageSize: 10,
        currentPage: 1,
        total: 0,
      },
      cargoImportBox: false,
      cargoImportLoading: false,
      billingPlanRows: [],
      settlementRuleForm: defaultSettlementRule(),
      reconciliationForm: defaultReconciliation(),
      changeRecordRows: [],
      billingPlanBox: false,
      billingPlanMode: 'add',
      billingPlanIndex: -1,
      billingPlanForm: defaultBillingPlan(),
      billingFeeCategoryOptions: [],
      billingFeeCategoryLoading: false,
      billingFeeCategoryRequest: null,
      billingFeeItemOptionsMap: {},
      billingFeeItemLoadingMap: {},
      billingFeeItemRequestMap: {},
      billingUnitOptions: [],
      billingUnitLoading: false,
      billingUnitRequest: null,
      billingMatchBox: false,
      billingMatchRuleIndex: -1,
      billingMatchForm: defaultBillingRule().matchCondition,
      billingMatchRegionOptions: [],
      billingMatchRegionLoading: false,
      billingMatchRegionRequest: null,
      billingCargoTypeOptions: [],
      billingCargoTypeFlatOptions: [],
      billingCargoTypeLoading: false,
      billingCargoTypeRequest: null,
      billingPlanRules: {
        planName: [{ required: true, message: '请输入方案名称', trigger: 'blur' }],
      },
      billingElementOptions: [
        '按重量',
        '按体积',
        '按车辆',
        '按里程',
        '按吨·公里',
        '固定金额（整单一口价）',
        '按数量',
      ],
      billingElementTypeMap: {
        按重量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
        按体积: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
        按车辆: ['固定单价'],
        按里程: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
        '按吨·公里': ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
        '固定金额（整单一口价）': ['固定一口价'],
        按数量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
      },
      settlementTypeOptions: ['月结', '日结', '周结', '半月结', '固定天数周期结算'],
      billCycleTypeOptions: ['固定截单日', '自然月'],
      reconciliationModeOptions: ['明细逐行核对', '按账单汇总核对', '跳过自动核对'],
      packageOptions,
      quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
      attachmentFileTypes: [
        'pdf',
        'bmp',
        'jpeg',
        'png',
        'jpg',
        'doc',
        'docx',
        'ppt',
        'pptx',
        'xlsx',
        'xls',
        'eml',
        'msg',
        'zip',
      ],
      excelBox: false,
      excelForm: {},
      excelOptionConfig: this.cloneOption(this.crudExcelOption),
      page: {
        pageSize: 10,
        pageSizes: [10, 20, 50, 100],
        currentPage: 1,
        total: 0,
      },
      selectionList: [],
    };
  },
  created() {
    if (this.config.enableProjectSelect) {
      this.loadProjectOptions();
    }
    if (this.config.enableOrganizationSelect) {
      this.loadOrganizationOptions();
    }
    if (this.config.enableBillingPlan) {
      this.ensureBillingFeeCategoryOptions();
      this.ensureBillingUnitOptions();
    }
  },
  computed: {
    ...mapGetters(['permission', 'userInfo']),
    permissionList() {
      return {
        addBtn: this.canCreate,
      };
    },
    isAdmin() {
      const authority = this.userInfo && this.userInfo.authority;
      return Array.isArray(authority)
        ? authority.includes('admin')
        : String(authority || '').includes('admin');
    },
    canViewAllDept() {
      return this.isAdmin;
    },
    readonlyScope() {
      return this.config.enableAllDept && this.allDept === 1;
    },
    canCreate() {
      return this.hasPermission(`${this.config.permission}_add`) && !this.readonlyScope;
    },
    showContractCreateActions() {
      return (
        this.config.enableContractCreateActions &&
        this.crudDialogType === 'add' &&
        !this.dialogReadonly
      );
    },
    showTransportPlanCreateActions() {
      return (
        this.config.enableTransportPlanCreateActions &&
        this.crudDialogType === 'add' &&
        !this.dialogReadonly
      );
    },
    organizationCascaderProps() {
      return {
        label: 'label',
        value: 'id',
        children: 'children',
        checkStrictly: true,
        emitPath: false,
      };
    },
    billingMatchRegionCascaderProps() {
      return {
        label: 'title',
        value: 'id',
        children: 'children',
        leaf: 'leaf',
        emitPath: true,
      };
    },
    billingCargoTypeCascaderProps() {
      return {
        label: 'cargoName',
        value: 'id',
        children: 'children',
        disabled: (data, node) => node.level === 1,
        leaf: 'leaf',
        checkStrictly: true,
        emitPath: true,
      };
    },
    transportCargoTypeOptions() {
      return this.billingCargoTypeOptions.filter(item => item.children?.length);
    },
    transportCargoTypeCascaderProps() {
      return {
        label: 'cargoName',
        value: 'id',
        children: 'children',
        emitPath: true,
      };
    },
    showSettlementBillCycleType() {
      return this.settlementRuleForm.settlementType === '月结';
    },
    showSettlementBillCutoffDay() {
      return (
        this.showSettlementBillCycleType && this.settlementRuleForm.billCycleType === '固定截单日'
      );
    },
    showSettlementCycleDays() {
      return this.settlementRuleForm.settlementType === '固定天数周期结算';
    },
    canBatchComplete() {
      return (
        this.hasAction('batchComplete') &&
        this.hasPermission(`${this.config.permission}_batch_complete`) &&
        !this.readonlyScope
      );
    },
    canRoadLoading() {
      const permission =
        this.config.roadLoadingPermission || `${this.config.permission}_road_loading`;
      return this.config.roadLoading && this.hasPermission(permission) && !this.readonlyScope;
    },
    ids() {
      return this.selectionList.map(item => item.id).join(',');
    },
    billCutoffDayOptions() {
      return Array.from({ length: 31 }, (_, index) => {
        const value = index + 1;
        return {
          label: `${value}日`,
          value,
        };
      });
    },
    cycleDayOptions() {
      return Array.from({ length: 365 }, (_, index) => {
        const value = index + 1;
        return {
          label: `${value}天`,
          value,
        };
      });
    },
    transportCargoQuantityTotal() {
      if (!this.transportCargoRows.length) return '系统自动计算';
      const total = this.transportCargoRows.reduce((sum, row) => {
        const value = Number(row.quantity);
        return Number.isFinite(value) ? sum + value : sum;
      }, 0);
      return Number.isInteger(total) ? String(total) : total.toFixed(2);
    },
    transportAddressSelectDisabled() {
      return this.dialogReadonly || this.isBillingFieldEmpty(this.form.transportType);
    },
    transportMode() {
      return this.resolveTransportMode(this.form.transportType);
    },
    transportStationMode() {
      return ['water', 'rail', 'air'].includes(this.transportMode);
    },
    transportStationTypeLabel() {
      const labelMap = {
        water: '港口/码头',
        rail: '铁路车站',
        air: '空港机场',
      };
      return labelMap[this.transportMode] || '';
    },
    transportStationNamePlaceholder() {
      if (!this.transportStationMode) return '行政区划自动带出';
      const labelMap = {
        water: '请选择港口/码头',
        rail: '请选择车站',
        air: '请选择空港',
      };
      return labelMap[this.transportMode] || '请选择站点';
    },
    transportAddressDetailPlaceholder() {
      return this.transportStationMode ? '选择站点后带出详细地址' : '请输入';
    },
  },
  watch: {
    'form.transportType'(value, oldValue) {
      if (!this.config.enableTransportPlanForm || this.suppressTransportTypeClear) return;
      if (String(value ?? '') === String(oldValue ?? '')) return;
      this.handleTransportTypeChange(value);
    },
  },
  methods: {
    cloneOption(option, menuButtonCount) {
      const nextOption = {
        ...option,
        column: (option.column || []).map(column => ({ ...column })),
      };
      return applyTableMenuWidth(nextOption, menuButtonCount);
    },
    hasPermission(code) {
      return this.isAdmin || this.validData(this.permission && this.permission[code], false);
    },
    hasAction(action) {
      return (this.config.actions || []).includes(action);
    },
    statusValue(row) {
      return row[this.config.statusProp || 'status'];
    },
    inStatus(row, statusList) {
      if (!statusList || !statusList.length) {
        return true;
      }
      const status = this.statusValue(row);
      const statusText = this.config.statusTextProp ? row[this.config.statusTextProp] : '';
      return statusList.includes(status) || statusList.includes(statusText);
    },
    isReadonlyEditAllowed(row) {
      return this.inStatus(row, this.config.ignoreReadonlyEditStatus);
    },
    isReadonlyDeleteAllowed(row) {
      return this.inStatus(row, this.config.ignoreReadonlyDeleteStatus);
    },
    canEdit(row) {
      return (
        this.hasPermission(`${this.config.permission}_edit`) &&
        !this.readonlyScope &&
        (!row.readonly || this.isReadonlyEditAllowed(row)) &&
        this.inStatus(row, this.config.editStatus)
      );
    },
    canDelete(row) {
      return (
        this.hasPermission(`${this.config.permission}_delete`) &&
        !this.readonlyScope &&
        (!row.readonly || this.isReadonlyDeleteAllowed(row)) &&
        this.inStatus(row, this.config.deleteStatus) &&
        (!this.config.deleteStage || this.config.deleteStage.includes(row.contractStage))
      );
    },
    canCopy(row) {
      return (
        this.hasAction('copy') &&
        this.hasPermission(`${this.config.permission}_copy`) &&
        !this.readonlyScope &&
        !row.readonly
      );
    },
    canEnable(row) {
      return (
        this.hasAction('enable') &&
        this.hasPermission(`${this.config.permission}_enable`) &&
        !this.readonlyScope &&
        !row.readonly &&
        [0, 2].includes(row.status)
      );
    },
    canDisable(row) {
      return (
        this.hasAction('disable') &&
        this.hasPermission(`${this.config.permission}_disable`) &&
        !this.readonlyScope &&
        !row.readonly &&
        row.status === 1
      );
    },
    canCancel(row) {
      if (
        !this.hasAction('cancel') ||
        !this.hasPermission(`${this.config.permission}_cancel`) ||
        this.readonlyScope ||
        row.readonly
      ) {
        return false;
      }
      const status = this.statusValue(row);
      return this.config.permission === 'transport_plan'
        ? ['waiting_dispatch', 'dispatching'].includes(status)
        : ['pending', 'processing'].includes(status);
    },
    canComplete(row) {
      if (
        !this.hasAction('complete') ||
        !this.hasPermission(`${this.config.permission}_complete`) ||
        this.readonlyScope ||
        row.readonly
      ) {
        return false;
      }
      const status = this.statusValue(row);
      return this.config.permission === 'transport_plan'
        ? status === 'dispatching'
        : status === 'processing';
    },
    canReassign(row) {
      return (
        this.hasAction('reassign') &&
        this.hasPermission(`${this.config.permission}_reassign`) &&
        !this.readonlyScope &&
        !row.readonly &&
        this.statusValue(row) === 'pending'
      );
    },
    customOperations(row) {
      return (this.config.operations || []).filter(operation => this.canOperation(operation, row));
    },
    menuActions(row) {
      const actions = [];
      if (this.canCopy(row)) {
        actions.push({ key: 'copy', label: '复制', action: 'copy' });
      }
      if (this.canEdit(row)) {
        actions.push({
          key: 'edit',
          label: this.editLabel(row),
          action: 'edit',
        });
      }
      this.customOperations(row).forEach(operation => {
        actions.push({
          ...operation,
          key: `${operation.action}-${operation.label}`,
        });
      });
      if (this.canDelete(row)) {
        actions.push({ key: 'delete', label: '删除', action: 'delete', type: 'danger' });
      }
      return actions;
    },
    editLabel(row) {
      const status = this.statusValue(row);
      return this.config.editLabelStatusMap?.[status] || '编辑';
    },
    handleMenuAction(action, row, index) {
      if (action.action === 'copy') {
        this.handleCopy(row);
        return;
      }
      if (action.action === 'edit' || action.action === 'attachmentUpload') {
        this.$refs.crud.rowEdit(row, index);
        return;
      }
      if (action.action === 'delete') {
        this.rowDel(row);
        return;
      }
      this.handleCustomOperation(action, row);
    },
    canOperation(operation, row) {
      const permission = operation.permission || `${this.config.permission}_${operation.action}`;
      if (!this.hasPermission(permission) || this.readonlyScope || row.readonly) {
        return false;
      }
      const prop = operation.statusProp || this.config.statusProp || 'status';
      const statusList = operation.status || [];
      if (statusList.length && !statusList.includes(row[prop])) {
        return false;
      }
      if (operation.stage && !operation.stage.includes(row.contractStage)) {
        return false;
      }
      return true;
    },
    displayStatus(row, prop) {
      const textProp = prop === this.config.statusProp ? this.config.statusTextProp : '';
      if (textProp && row[textProp]) {
        return row[textProp];
      }
      const column = this.findColumn(this.option.column, prop);
      const item = column && (column.dicData || []).find(dic => dic.value === row[prop]);
      return item ? item.label : row[prop] || '未知';
    },
    syncContractNoDisabled(type) {
      if (!this.config.enableContractForm) return;
      const column = this.findColumn?.(this.option.column, 'contractNo');
      if (column) {
        column.disabled = type !== 'add';
      }
    },
    syncCustomCreateDialogButtons(type) {
      if (
        !this.config.enableContractCreateActions &&
        !this.config.enableTransportPlanCreateActions
      ) {
        return;
      }
      if (!this.defaultDialogButtons) {
        this.defaultDialogButtons = {
          saveBtn: this.option.saveBtn,
          cancelBtn: this.option.cancelBtn,
        };
      }
      const customCreate = type === 'add';
      this.option.saveBtn = customCreate ? false : this.defaultDialogButtons.saveBtn;
      this.option.cancelBtn = customCreate ? false : this.defaultDialogButtons.cancelBtn;
    },
    statusTagType(status) {
      if (
        [
          1,
          'pending',
          'waiting_dispatch',
          'processing',
          'dispatching',
          'approved',
          'change_approved',
        ].includes(status)
      ) {
        return 'success';
      }
      if ([2, 'cancelled', 'withdrawn', 'draft'].includes(status)) {
        return 'info';
      }
      if (['rejected', 'change_rejected'].includes(status)) {
        return 'danger';
      }
      if (status === 'completed') {
        return 'primary';
      }
      return 'warning';
    },
    stageTagType(stage) {
      if (stage === 'formal') {
        return 'success';
      }
      if (stage === 'temporary') {
        return 'warning';
      }
      if (stage === 'terminated') {
        return 'danger';
      }
      return 'info';
    },
    isMonthlySettlement(rule = this.settlementRuleForm) {
      return rule.settlementType === '月结';
    },
    isFixedBillCutoffSettlement(rule = this.settlementRuleForm) {
      return this.isMonthlySettlement(rule) && rule.billCycleType === '固定截单日';
    },
    isFixedCycleSettlement(rule = this.settlementRuleForm) {
      return rule.settlementType === '固定天数周期结算';
    },
    handleSettlementTypeChange(value) {
      if (value === '月结') {
        if (!this.settlementRuleForm.billCycleType) {
          this.settlementRuleForm.billCycleType = '固定截单日';
        }
        if (this.settlementRuleForm.billCycleType === '固定截单日') {
          this.settlementRuleForm.billCutoffDay = this.settlementRuleForm.billCutoffDay || 25;
        }
        this.settlementRuleForm.cycleDays = '';
        return;
      }
      this.settlementRuleForm.billCycleType = '';
      this.settlementRuleForm.billCutoffDay = '';
      if (value !== '固定天数周期结算') {
        this.settlementRuleForm.cycleDays = '';
      }
    },
    handleSettlementBillCycleTypeChange(value) {
      if (value === '固定截单日') {
        this.settlementRuleForm.billCutoffDay = this.settlementRuleForm.billCutoffDay || 25;
        return;
      }
      this.settlementRuleForm.billCutoffDay = '';
    },
    normalizeSettlementRule(rule = this.settlementRuleForm) {
      const nextRule = {
        ...defaultSettlementRule(),
        ...(rule || {}),
      };
      if (!this.isMonthlySettlement(nextRule)) {
        nextRule.billCycleType = '';
        nextRule.billCutoffDay = '';
      } else if (!this.isFixedBillCutoffSettlement(nextRule)) {
        nextRule.billCutoffDay = '';
      }
      if (!this.isFixedCycleSettlement(nextRule)) {
        nextRule.cycleDays = '';
      }
      return nextRule;
    },
    validateSettlementRule() {
      const rule = this.normalizeSettlementRule(this.settlementRuleForm);
      if (this.isBillingFieldEmpty(rule.billStartDate)) {
        this.$message.warning('请选择账单起始日期');
        return false;
      }
      if (this.isBillingFieldEmpty(rule.settlementType)) {
        this.$message.warning('请选择结算类型');
        return false;
      }
      if (this.isMonthlySettlement(rule) && this.isBillingFieldEmpty(rule.billCycleType)) {
        this.$message.warning('请选择账单周期类型');
        return false;
      }
      if (this.isFixedBillCutoffSettlement(rule)) {
        const billCutoffDay = Number(rule.billCutoffDay);
        if (!Number.isInteger(billCutoffDay) || billCutoffDay < 1 || billCutoffDay > 31) {
          this.$message.warning('账单截单日必须为1~31的整数');
          return false;
        }
      }
      if (this.isFixedCycleSettlement(rule)) {
        const cycleDays = Number(rule.cycleDays);
        if (!Number.isInteger(cycleDays) || cycleDays < 1 || cycleDays > 365) {
          this.$message.warning('周期天数必须为1~365的整数');
          return false;
        }
      }
      return true;
    },
    normalizeRow(row) {
      const submitRow = { ...row };
      if (this.config.enableTransportPlanForm) {
        if (!this.validateTransportPlanForm(submitRow)) return false;
        submitRow.goodsJson = JSON.stringify(this.transportCargoRows);
      }
      if (this.config.enableAttachmentTable) {
        submitRow.attachmentsJson = JSON.stringify(this.attachmentRows);
      }
      if (this.config.enableContractFileUpload) {
        submitRow.contractFileJson = JSON.stringify(this.contractFileRows);
      }
      if (this.config.enableBillingPlan) {
        submitRow.billingPlanJson = JSON.stringify(this.billingPlanRows);
      }
      if (this.config.enableSettlementRule) {
        if (!this.validateSettlementRule()) return false;
        submitRow.settlementRuleJson = JSON.stringify(this.normalizeSettlementRule());
      }
      if (this.config.enableReconciliation) {
        submitRow.reconciliationJson = JSON.stringify(this.reconciliationForm);
      }
      if (this.config.enableChangeRecord) {
        submitRow.changeRecordJson = JSON.stringify(this.changeRecordRows);
      }
      if (this.config.saveAsDraft) {
        submitRow.approvalStatus = 'draft';
      }
      delete submitRow.basicInfoTitle;
      delete submitRow.shippingInfoTitle;
      delete submitRow.goodsInfoTitle;
      delete submitRow.contractFileTitle;
      delete submitRow.billingInfoTitle;
      delete submitRow.settlementRuleTitle;
      delete submitRow.reconciliationTitle;
      delete submitRow.attachmentTitle;
      delete submitRow.changeRecordTitle;
      delete submitRow.contractPeriod;
      Object.keys(submitRow).forEach(key => {
        if (typeof submitRow[key] === 'string') {
          submitRow[key] = submitRow[key].trim();
        }
      });
      return submitRow;
    },
    validateTransportPlanForm(row) {
      const requiredFields = [
        ['projectName', '项目'],
        ['contractName', '客户合同'],
        ['planName', '计划名称'],
        ['transportType', '运输方式'],
        ['planStartDate', '计划开始日期'],
        ['planEndDate', '计划结束日期'],
        ['departureAddress', '发货地址'],
        ['arrivalAddress', '收货地址'],
      ];
      const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(row[prop]));
      if (emptyField) {
        this.$message.warning(`请选择或输入${emptyField[1]}`);
        return false;
      }
      if (row.planStartDate && row.planEndDate && row.planEndDate < row.planStartDate) {
        this.$message.warning('计划结束日期不能早于计划开始日期');
        return false;
      }
      if (row.remark && row.remark.length > 200) {
        this.$message.warning('备注不能超过200个字符');
        return false;
      }
      for (const [index, cargo] of this.transportCargoRows.entries()) {
        if (!cargo.cargoType) {
          this.$message.warning(`第${index + 1}行货物类型不能为空`);
          return false;
        }
      }
      return true;
    },
    stopSubmitLoading(loading) {
      if (typeof loading === 'function') {
        loading();
      }
    },
    rowSave(row, done, loading) {
      const submitRow = this.normalizeRow(row);
      if (!submitRow) {
        this.stopSubmitLoading(loading);
        return;
      }
      this.getSaveRequest()(submitRow).then(
        () => {
          this.onLoad(this.page);
          this.$message({ type: 'success', message: '操作成功!' });
          done();
        },
        error => {
          window.console.log(error);
          this.stopSubmitLoading(loading);
        }
      );
    },
    rowUpdate(row, index, done, loading) {
      const submitRow = this.normalizeRow(row);
      if (!submitRow) {
        this.stopSubmitLoading(loading);
        return;
      }
      this.getSaveRequest()(submitRow).then(
        () => {
          this.onLoad(this.page);
          this.$message({ type: 'success', message: '操作成功!' });
          done();
        },
        error => {
          window.console.log(error);
          this.stopSubmitLoading(loading);
        }
      );
    },
    getSaveRequest() {
      if (this.config.saveAsDraft && typeof this.api.saveDraft === 'function') {
        return this.api.saveDraft;
      }
      return this.api.submit;
    },
    buildContractCreateSubmitRow(action) {
      const contractStage = action === 'draft' ? 'draft' : 'temporary';
      const submitRow = this.normalizeRow({
        ...this.form,
        contractStage,
        approvalStatus: 'draft',
      });
      return submitRow;
    },
    validateContractCreateAction(action, row) {
      if (!row) return false;
      if (this.isBillingFieldEmpty(row.contractName)) {
        this.$message.warning('请输入合同名称');
        return false;
      }
      if (this.isBillingFieldEmpty(row.contractCategory)) {
        this.$message.warning('请选择合同类别');
        return false;
      }
      if (['temporary', 'formal'].includes(action) && this.isBillingFieldEmpty(row.signType)) {
        this.$message.warning('请选择签约类型');
        return false;
      }
      if (action !== 'formal') return true;
      const formalFields = [
        ['partyA', '甲方'],
        ['partyB', '乙方'],
        ['startDate', '合同期限'],
        ['signDate', '签订日期'],
      ];
      const emptyField = formalFields.find(([prop]) => this.isBillingFieldEmpty(row[prop]));
      if (emptyField) {
        this.$message.warning(`请选择或输入${emptyField[1]}`);
        return false;
      }
      return true;
    },
    extractSavedContract(res, fallback = {}) {
      const data = res?.data?.data || res?.data || {};
      if (data && typeof data === 'object' && !Array.isArray(data)) {
        return {
          ...fallback,
          ...data,
        };
      }
      return fallback;
    },
    getSavedContractId(savedContract) {
      const id = savedContract?.id || this.form.id;
      if (!id) {
        this.$message.warning('合同保存成功但未返回主键，无法继续流转');
        return '';
      }
      return id;
    },
    handleContractCreateAction(action) {
      if (!this.config.enableContractCreateActions || this.contractCreateActionLoading) return;
      const submitRow = this.buildContractCreateSubmitRow(action);
      if (!this.validateContractCreateAction(action, submitRow)) return;
      this.contractCreateActionLoading = action;
      this.api
        .saveDraft(submitRow)
        .then(res => {
          const savedContract = this.extractSavedContract(res, submitRow);
          this.form.id = savedContract.id || this.form.id;
          if (action === 'draft') {
            return savedContract;
          }
          const id = this.getSavedContractId(savedContract);
          if (!id) return Promise.reject(new Error('合同主键为空'));
          if (action === 'formal') {
            return this.api.submitFormal(id).then(() => savedContract);
          }
          return savedContract;
        })
        .then(() => {
          const messageMap = {
            draft: '保存成功',
            temporary: '临时合同提交成功',
            formal: '正式合同提交成功',
          };
          this.$message.success(messageMap[action]);
          this.onLoad(this.page, this.query);
          this.closeCrudDialog();
        })
        .catch(error => {
          if (error?.message !== '合同主键为空') {
            window.console.log(error);
          }
        })
        .finally(() => {
          this.contractCreateActionLoading = '';
        });
    },
    handleTransportPlanCreateAction(action) {
      if (!this.config.enableTransportPlanCreateActions || this.transportPlanCreateActionLoading) {
        return;
      }
      const statusProp = this.config.statusProp || 'businessStatus';
      const submitRow = this.normalizeRow({
        ...this.form,
        [statusProp]: action === 'draft' ? 'draft' : 'waiting_dispatch',
      });
      if (!submitRow) return;
      this.transportPlanCreateActionLoading = action;
      this.api
        .submit(submitRow)
        .then(() => {
          this.$message.success(action === 'draft' ? '暂存成功' : '创建成功');
          this.onLoad(this.page, this.query);
          this.closeCrudDialog();
        })
        .finally(() => {
          this.transportPlanCreateActionLoading = '';
        });
    },
    handleDraftSave() {
      const request =
        typeof this.api.saveDraft === 'function' ? this.api.saveDraft : this.api.submit;
      const submitRow = this.normalizeRow(this.form);
      if (!submitRow) return;
      this.draftSaveLoading = true;
      request(submitRow)
        .then(() => {
          this.$message.success('保存成功');
          this.onLoad(this.page, this.query);
          this.closeCrudDialog();
        })
        .finally(() => {
          this.draftSaveLoading = false;
        });
    },
    closeCrudDialog() {
      const crud = this.$refs.crud;
      if (crud && typeof crud.closeDialog === 'function') {
        crud.closeDialog();
      }
    },
    rowDel(row) {
      this.$confirm('确定将选择数据删除?', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      })
        .then(() => this.api.remove(row.id))
        .then(res => {
          this.afterRemove(res.data.data);
        });
    },
    handleDelete() {
      if (this.selectionList.length === 0) {
        this.$message.warning('请选择至少一条数据');
        return;
      }
      const selection = this.selectionList.filter(item => this.canDelete(item));
      if (!selection.length) {
        this.$message.warning('所选数据当前状态不可删除');
        return;
      }
      this.$confirm('确定将选择数据删除?', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      })
        .then(() => this.api.remove(selection.map(item => item.id).join(',')))
        .then(res => {
          this.afterRemove(res.data.data);
        });
    },
    afterRemove(result = {}) {
      this.onLoad(this.page);
      const successCount = result.successCount || 0;
      const skippedCount = result.skippedCount || 0;
      if (skippedCount) {
        this.$message.warning(`成功删除${successCount}条，因业务引用跳过${skippedCount}条`);
      } else {
        this.$message({ type: 'success', message: '删除成功!' });
      }
      this.selectionClear();
    },
    beforeOpen(done, type) {
      this.crudDialogType = type;
      this.syncCustomCreateDialogButtons(type);
      this.dialogReadonly = type === 'view';
      this.syncContractNoDisabled(type);
      if (type === 'add') {
        this.form = {
          ...(this.config.defaultForm || {}),
          ...this.form,
        };
        this.selectedProjectId = '';
        this.selectedOrganizationId = '';
        this.attachmentRows = [];
        this.contractFileRows = [];
        this.transportCargoRows = [];
        this.billingPlanRows = [];
        this.settlementRuleForm = defaultSettlementRule();
        this.reconciliationForm = defaultReconciliation();
        this.changeRecordRows = [];
        this.applyCurrentUserHandler();
        this.initTransportPlanFormRows();
        this.syncContractPeriod(false);
        this.syncBillingPlanJson();
        done();
        return;
      }
      if (['edit', 'view'].includes(type)) {
        this.api.getDetail(this.form.id).then(res => {
          this.applyFormDetail(res.data.data || {});
          done();
        });
        return;
      }
      done();
    },
    applyFormDetail(row) {
      this.suppressTransportTypeClear = true;
      this.form = row || {};
      this.$nextTick(() => {
        this.suppressTransportTypeClear = false;
      });
      this.selectedProjectId = this.form.projectId || '';
      this.selectedOrganizationId = this.form.organizationId || '';
      this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
      this.contractFileRows = this.parseJsonArray(this.form.contractFileJson);
      this.initTransportPlanFormRows();
      this.billingPlanRows = this.parseJsonArray(this.form.billingPlanJson);
      this.settlementRuleForm = this.normalizeSettlementRule({
        ...defaultSettlementRule(),
        ...this.parseJsonObject(this.form.settlementRuleJson),
      });
      this.reconciliationForm = {
        ...defaultReconciliation(),
        ...this.parseJsonObject(this.form.reconciliationJson),
      };
      this.changeRecordRows = this.parseJsonArray(this.form.changeRecordJson);
      this.applyCurrentUserHandler();
      this.syncContractPeriod(false);
      this.syncBillingPlanJson();
      if (this.config.enableOrganizationSelect) {
        this.syncCurrentOrganizationOption();
      }
      if (this.config.enableProjectSelect) {
        this.form.usedFundLimit = 0;
        this.form.remainingFundLimit = 0;
        this.syncCurrentProjectOption();
      }
      if (this.config.enableTransportPlanForm) {
        this.syncCurrentContractOption();
      }
    },
    parseJsonArray(value) {
      if (!value) return [];
      if (Array.isArray(value)) return value;
      try {
        const list = JSON.parse(value);
        return Array.isArray(list) ? list : [];
      } catch (error) {
        return [];
      }
    },
    parseJsonObject(value) {
      if (!value) return {};
      if (typeof value === 'object' && !Array.isArray(value)) return value;
      try {
        const data = JSON.parse(value);
        return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
      } catch (error) {
        return {};
      }
    },
    loadProjectOptions() {
      if (!this.config.enableProjectSelect || this.projectLoading) return;
      this.projectLoading = true;
      getProjectList(1, 9999, {})
        .then(res => {
          const records = res.data.data?.records || [];
          this.projectOptions = records;
          this.syncCurrentProjectOption();
        })
        .finally(() => {
          this.projectLoading = false;
        });
    },
    syncCurrentProjectOption() {
      if (!this.form.projectId || !this.form.projectName) return;
      const exists = this.projectOptions.some(
        item => String(item.id) === String(this.form.projectId)
      );
      if (!exists) {
        this.projectOptions.unshift({
          id: this.form.projectId,
          projectName: this.form.projectName,
          projectCode: this.form.projectCode,
          undertakeDeptName: this.form.undertakeDeptName,
          fundLimit: this.form.projectFundLimit,
        });
      }
    },
    handleProjectChange(projectId) {
      const project = this.projectOptions.find(item => String(item.id) === String(projectId));
      if (!project) {
        this.form.projectId = '';
        this.form.projectName = '';
        this.form.projectCode = '';
        this.form.undertakeDeptName = '';
        this.form.projectFundLimit = '';
        this.form.usedFundLimit = 0;
        this.form.remainingFundLimit = 0;
        if (this.config.enableTransportPlanForm) {
          this.form.transportType = '';
          this.form.contractId = '';
          this.form.contractName = '';
          this.contractOptions = [];
        }
        return;
      }
      this.form.projectId = project.id;
      this.form.projectName = project.projectName || '';
      this.form.projectCode = project.projectCode || '';
      this.form.undertakeDeptName = project.undertakeDeptName || '';
      this.form.projectFundLimit = project.fundLimit || project.projectFundLimit || 0;
      this.form.usedFundLimit = 0;
      this.form.remainingFundLimit = 0;
      if (this.config.enableTransportPlanForm) {
        this.form.customerName =
          project.customerName ||
          project.customerNames ||
          project.customer ||
          this.form.customerName ||
          '';
        this.form.transportType =
          project.transportType ||
          project.transportMode ||
          project.transportWay ||
          this.form.transportType ||
          '';
        this.loadContractOptionsForProject(true);
      }
      this.$refs.crud?.validateField?.('projectName');
    },
    loadContractOptionsForProject(autoPick = false) {
      if (!this.config.enableTransportPlanForm || !this.form.projectId || this.contractLoading) {
        return Promise.resolve([]);
      }
      this.contractLoading = true;
      return getContractList(1, 9999, {
        projectId: this.form.projectId,
        projectName: this.form.projectName,
      })
        .then(res => {
          this.contractOptions = extractRecords(res);
          this.syncCurrentContractOption();
          if (autoPick && this.contractOptions.length) {
            this.applyContract(this.contractOptions[0]);
          }
          return this.contractOptions;
        })
        .finally(() => {
          this.contractLoading = false;
        });
    },
    syncCurrentContractOption() {
      if (!this.form.contractId || !this.form.contractName) return;
      const exists = this.contractOptions.some(
        item => String(item.id) === String(this.form.contractId)
      );
      if (!exists) {
        this.contractOptions.unshift({
          id: this.form.contractId,
          contractName: this.form.contractName,
        });
      }
    },
    handleContractChange(contractId) {
      const contract = this.contractOptions.find(item => String(item.id) === String(contractId));
      if (!contract) {
        this.form.contractId = '';
        this.form.contractName = '';
        return;
      }
      this.applyContract(contract);
    },
    applyContract(contract = {}) {
      this.form.contractId = contract.id || '';
      this.form.contractName = contract.contractName || '';
      this.form.customerName =
        contract.customerName || contract.customerNames || this.form.customerName || '';
      this.$refs.crud?.validateField?.('contractName');
    },
    initTransportPlanFormRows() {
      if (!this.config.enableTransportPlanForm) return;
      this.transportCargoRows = this.parseJsonArray(this.form.goodsJson).map(row =>
        this.normalizeTransportCargoRow(row)
      );
      this.syncTransportCargoJson();
      this.syncCurrentContractOption();
    },
    normalizeTransportCargoRow(row = {}) {
      return {
        ...defaultTransportCargo(),
        ...row,
        cargoTypePath: this.normalizeBillingCargoTypePath(row.cargoTypePath),
        cargoType:
          row.cargoType || row.goodsType || row.secondCargoTypeName || row.firstCargoTypeName || '',
        cargoTypeCode:
          row.cargoTypeCode ||
          row.goodsTypeCode ||
          row.secondCargoTypeCode ||
          row.firstCargoTypeCode ||
          '',
        cargoName: row.cargoName || row.goodsName || '',
        packageType: row.packageType || row.package || '',
        specification: row.specification || row.spec || '',
      };
    },
    syncTransportCargoJson() {
      if (!this.config.enableTransportPlanForm) return;
      this.form.goodsJson = JSON.stringify(this.transportCargoRows);
    },
    addTransportCargoRow(index = -1, row = {}) {
      const nextRow = this.normalizeTransportCargoRow(row);
      if (index > -1) {
        this.transportCargoRows.splice(index + 1, 0, nextRow);
      } else {
        this.transportCargoRows.push(nextRow);
      }
      this.syncTransportCargoJson();
    },
    removeTransportCargoRow(index) {
      this.transportCargoRows.splice(index, 1);
      this.syncTransportCargoJson();
    },
    handleTransportCargoQuantityInput(row, value) {
      const text = String(value || '').replace(/[^\d.]/g, '');
      const parts = text.split('.');
      row.quantity =
        parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 3)}` : parts[0];
      this.syncTransportCargoJson();
    },
    handleTransportCargoTypeChange(row, value) {
      const path = this.normalizeBillingCargoTypePath(value);
      const cargoType = this.findBillingCargoTypeByPath(path);
      const labels = this.getBillingCargoTypePathLabels(path);
      row.cargoTypePath = path;
      row.cargoType = labels.length ? labels[labels.length - 1] : '';
      row.cargoTypeCode = cargoType?.cargoCode || cargoType?.code || cargoType?.id || '';
      this.syncTransportCargoJson();
    },
    handleTransportTypeChange(value) {
      this.form.transportTypeName = this.getTransportTypeLabel(value);
      this.clearTransportShippingInfo();
    },
    clearTransportShippingInfo() {
      [
        'departureName',
        'departureAddress',
        'departureAddressCode',
        'departureSiteCode',
        'departureLongitude',
        'departureLatitude',
        'departureContact',
        'departurePhone',
        'arrivalName',
        'arrivalAddress',
        'arrivalAddressCode',
        'arrivalSiteCode',
        'arrivalLongitude',
        'arrivalLatitude',
        'arrivalContact',
        'arrivalPhone',
      ].forEach(prop => {
        this.form[prop] = '';
      });
      this.transportRouteSelected = null;
      this.transportAddressSelected = null;
      this.transportMapSelected = {};
      this.transportRouteBox = false;
      this.transportAddressBox = false;
      this.transportStationBox = false;
      this.transportMapBox = false;
      this.transportAddressTarget = '';
      this.transportStationTarget = '';
      this.transportMapTarget = '';
      this.transportMapKeyword = '';
      this.transportMapStatus = '可搜索地址或点击地图选点';
      if (this.transportAmapMarker) {
        this.transportAmapMarker.setMap(null);
        this.transportAmapMarker = null;
      }
      this.$refs.crud?.clearValidate?.([
        'departureAddress',
        'arrivalAddress',
        'departureName',
        'arrivalName',
        'departureContact',
        'departurePhone',
        'arrivalContact',
        'arrivalPhone',
      ]);
    },
    getTransportTypeLabel(value) {
      const column = (this.option.column || []).find(item => item.prop === 'transportType');
      const dictionary = column?.dicData || [];
      const item = dictionary.find(
        dic =>
          String(dic.value ?? dic.dictKey ?? '') === String(value ?? '') ||
          String(dic.dictKey ?? '') === String(value ?? '')
      );
      return item?.label || item?.dictValue || '';
    },
    resolveTransportMode(value) {
      const text = [value, this.form.transportTypeName, this.getTransportTypeLabel(value)]
        .filter(Boolean)
        .join(' ')
        .toLowerCase();
      if (!text) return '';
      if (
        text.includes('公路') ||
        text.includes('道路') ||
        text.includes('road') ||
        text.includes('highway') ||
        text === 'gl'
      ) {
        return 'road';
      }
      if (
        text.includes('铁路') ||
        text.includes('rail') ||
        text.includes('train') ||
        text === 'tl'
      ) {
        return 'rail';
      }
      if (
        text.includes('水路') ||
        text.includes('水运') ||
        text.includes('海运') ||
        text.includes('港') ||
        text.includes('water') ||
        text.includes('ship') ||
        text.includes('sea') ||
        text === 'sl'
      ) {
        return 'water';
      }
      if (
        text.includes('航空') ||
        text.includes('空运') ||
        text.includes('空港') ||
        text.includes('air') ||
        text.includes('airport') ||
        text === 'hk'
      ) {
        return 'air';
      }
      return '';
    },
    openTransportPrimaryAddressPicker(target) {
      if (this.transportStationMode) {
        this.openTransportStationDialog(target);
      }
    },
    openTransportSecondaryAddressPicker(target) {
      if (this.transportStationMode) {
        this.openTransportStationDialog(target);
        return;
      }
      this.openTransportMapDialog(target);
    },
    ensureTransportTypeBeforeAddress() {
      if (
        !this.config.enableTransportPlanForm ||
        !this.isBillingFieldEmpty(this.form.transportType)
      ) {
        return true;
      }
      this.$message.warning('请先选择运输方式');
      return false;
    },
    openTransportRouteDialog() {
      if (!this.ensureTransportTypeBeforeAddress()) return;
      this.transportRouteSelected = null;
      this.transportRoutePage.currentPage = 1;
      this.transportRouteQuery = {};
      this.transportRouteBox = true;
    },
    loadTransportRouteList() {
      this.transportRouteLoading = true;
      getCommonRouteList(this.transportRoutePage.currentPage, this.transportRoutePage.pageSize, {
        ...this.transportRouteQuery,
        allDept: 0,
      })
        .then(res => {
          const data = res.data.data || {};
          this.transportRouteRows = data.records || [];
          this.transportRoutePage.total = data.total || 0;
        })
        .finally(() => {
          this.transportRouteLoading = false;
        });
    },
    handleTransportRouteSearch() {
      this.transportRoutePage.currentPage = 1;
      this.loadTransportRouteList();
    },
    handleTransportRouteReset() {
      this.transportRouteQuery = {};
      this.transportRoutePage.currentPage = 1;
      this.loadTransportRouteList();
    },
    handleTransportRouteSizeChange(pageSize) {
      this.transportRoutePage.pageSize = pageSize;
      this.transportRoutePage.currentPage = 1;
      this.loadTransportRouteList();
    },
    handleTransportRouteCurrentChange(currentPage) {
      this.transportRoutePage.currentPage = currentPage;
      this.loadTransportRouteList();
    },
    formatTransportRouteUpdateUserName(row) {
      return formatUpdateUserName(row);
    },
    selectTransportRouteRow(row) {
      this.transportRouteSelected = row;
      this.applyTransportRoute(row);
      this.transportRouteBox = false;
    },
    applyTransportRoute(route = {}) {
      this.applyTransportAddress('departure', {
        addressName: route.departureName,
        detailAddress: route.departureAddress,
        longitude: route.departureLongitude,
        latitude: route.departureLatitude,
        contactName: route.departureContact,
        contactPhone: route.departurePhone,
        addressCode: route.departureAddressCode,
        siteCode: route.departureSiteCode,
      });
      this.applyTransportAddress('arrival', {
        addressName: route.arrivalName,
        detailAddress: route.arrivalAddress,
        longitude: route.arrivalLongitude,
        latitude: route.arrivalLatitude,
        contactName: route.arrivalContact,
        contactPhone: route.arrivalPhone,
        addressCode: route.arrivalAddressCode,
        siteCode: route.arrivalSiteCode,
      });
    },
    openTransportStationDialog(target) {
      if (!this.ensureTransportTypeBeforeAddress()) return;
      if (!this.transportStationMode) return;
      this.transportStationTarget = target;
      this.transportStationQuery = {};
      this.transportStationPage.currentPage = 1;
      this.transportStationBox = true;
    },
    getTransportStationListRequest() {
      const requestMap = {
        water: getPortTerminalList,
        rail: getRailwayStationList,
        air: getAirportMasterList,
      };
      return requestMap[this.transportMode];
    },
    loadTransportStationList() {
      const request = this.getTransportStationListRequest();
      if (!request) return;
      this.transportStationLoading = true;
      request(
        this.transportStationPage.currentPage,
        this.transportStationPage.pageSize,
        this.buildTransportStationQueryParams()
      )
        .then(res => {
          const data = res.data.data || {};
          this.transportStationRows = (data.records || []).map(row =>
            this.normalizeTransportStationRow(row)
          );
          this.transportStationPage.total = data.total || 0;
        })
        .finally(() => {
          this.transportStationLoading = false;
        });
    },
    buildTransportStationQueryParams() {
      const query = this.transportStationQuery || {};
      return Object.keys(query).reduce((params, key) => {
        const value = query[key];
        if (value !== undefined && value !== null && value !== '') {
          params[key] = value;
        }
        return params;
      }, {});
    },
    handleTransportStationSearch() {
      this.transportStationPage.currentPage = 1;
      this.loadTransportStationList();
    },
    handleTransportStationReset() {
      this.transportStationQuery = {};
      this.transportStationPage.currentPage = 1;
      this.loadTransportStationList();
    },
    handleTransportStationSizeChange(pageSize) {
      this.transportStationPage.pageSize = pageSize;
      this.transportStationPage.currentPage = 1;
      this.loadTransportStationList();
    },
    handleTransportStationCurrentChange(currentPage) {
      this.transportStationPage.currentPage = currentPage;
      this.loadTransportStationList();
    },
    normalizeTransportStationRow(row = {}) {
      const regionName =
        row.regionName ||
        [row.provinceName, row.cityName, row.districtName].filter(Boolean).join('');
      return {
        ...row,
        stationName: row.name || row.stationName || row.addressName || '',
        stationType: row.category || this.transportStationTypeLabel,
        stationCode: row.code || row.tmisCode || row.iataCode || row.icaoCode || row.siteCode || '',
        detailAddress: row.detailAddress || row.address || '',
        regionName,
      };
    },
    selectTransportStationRow(row) {
      const station = this.normalizeTransportStationRow(row);
      this.applyTransportAddress(this.transportStationTarget, {
        addressName: station.stationName,
        addressType: this.transportStationTypeLabel,
        detailAddress: station.detailAddress,
        longitude: station.longitude,
        latitude: station.latitude,
        regionName: station.regionName,
        regionCode: station.regionCode,
        siteCode: station.stationCode,
        siteCodeDisplay: station.stationCode,
        remark: station.remark,
      });
      this.transportStationBox = false;
    },
    openTransportAddressDialog(target) {
      if (!this.ensureTransportTypeBeforeAddress()) return;
      this.transportAddressTarget = target;
      this.transportAddressSelected = null;
      this.transportAddressQuery = {};
      this.transportAddressPage.currentPage = 1;
      this.transportAddressBox = true;
    },
    loadTransportAddressList() {
      this.transportAddressLoading = true;
      getCommonAddressList(
        this.transportAddressPage.currentPage,
        this.transportAddressPage.pageSize,
        {
          ...this.transportAddressQuery,
          allDept: 0,
        }
      )
        .then(res => {
          const data = res.data.data || {};
          this.transportAddressRows = data.records || [];
          this.transportAddressPage.total = data.total || 0;
        })
        .finally(() => {
          this.transportAddressLoading = false;
        });
    },
    handleTransportAddressSearch() {
      this.transportAddressPage.currentPage = 1;
      this.loadTransportAddressList();
    },
    handleTransportAddressReset() {
      this.transportAddressQuery = {};
      this.transportAddressPage.currentPage = 1;
      this.loadTransportAddressList();
    },
    handleTransportAddressSizeChange(pageSize) {
      this.transportAddressPage.pageSize = pageSize;
      this.transportAddressPage.currentPage = 1;
      this.loadTransportAddressList();
    },
    handleTransportAddressCurrentChange(currentPage) {
      this.transportAddressPage.currentPage = currentPage;
      this.loadTransportAddressList();
    },
    formatTransportAddressSiteCode(row) {
      return row.addressType === '常规地址' ? '/' : row.siteCodeDisplay || row.siteCode || '';
    },
    selectTransportAddressRow(row) {
      this.transportAddressSelected = row;
      this.applyTransportAddress(this.transportAddressTarget, {
        ...row,
        preferRegionName: true,
      });
      this.transportAddressBox = false;
    },
    applyTransportAddress(target, address = {}) {
      const prefix = target === 'departure' ? 'departure' : 'arrival';
      this.form[`${prefix}Name`] = address.preferRegionName
        ? address.regionName || address.addressName || ''
        : address.addressName || address.regionName || '';
      this.form[`${prefix}Address`] = address.detailAddress || address.address || '';
      this.form[`${prefix}AddressCode`] = address.addressCode || '';
      this.form[`${prefix}SiteCode`] =
        address.addressType === '常规地址'
          ? '/'
          : address.siteCodeDisplay || address.siteCode || '';
      this.form[`${prefix}Longitude`] = address.longitude || '';
      this.form[`${prefix}Latitude`] = address.latitude || '';
      this.form[`${prefix}Contact`] = address.contactName || this.form[`${prefix}Contact`] || '';
      this.form[`${prefix}Phone`] = address.contactPhone || this.form[`${prefix}Phone`] || '';
      this.$refs.crud?.validateField?.(`${prefix}Address`);
    },
    openTransportMapDialog(target) {
      if (this.dialogReadonly) return;
      if (!this.ensureTransportTypeBeforeAddress()) return;
      const prefix = target === 'departure' ? 'departure' : 'arrival';
      this.transportMapTarget = prefix;
      this.transportMapKeyword = this.form[`${prefix}Address`] || this.form[`${prefix}Name`] || '';
      this.transportMapSelected = this.buildTransportMapSelection({
        lng: this.form[`${prefix}Longitude`],
        lat: this.form[`${prefix}Latitude`],
        address: this.form[`${prefix}Address`],
        regionName: this.form[`${prefix}Name`],
      });
      this.transportMapStatus = this.transportMapSelected.longitude
        ? '已加载当前选点，可重新选点'
        : '可搜索地址或点击地图选点';
      this.transportMapBox = true;
    },
    loadAmap() {
      if (window.AMap && window.AMap.Map) {
        return Promise.resolve();
      }
      if (!amapLoader) {
        amapLoader = new Promise((resolve, reject) => {
          window._AMapSecurityConfig = {
            securityJsCode: AMAP_SECURITY_CODE,
          };
          const script = document.createElement('script');
          script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`;
          script.async = true;
          script.onload = resolve;
          script.onerror = () => reject(new Error('高德地图组件加载失败'));
          document.body.appendChild(script);
        });
      }
      return amapLoader;
    },
    initTransportAmap() {
      this.loadAmap()
        .then(() => {
          this.$nextTick(() => {
            if (!this.transportAmap) {
              const center = this.toLngLat(
                this.transportMapSelected.longitude || 116.40769,
                this.transportMapSelected.latitude || 39.89945
              );
              this.transportAmap = new window.AMap.Map(this.$refs.transportAmap, {
                center,
                zoom: this.transportMapSelected.longitude ? 14 : 11,
              });
              window.AMap.plugin(['AMap.ToolBar', 'AMap.Geocoder'], () => {
                this.transportAmap.addControl(new window.AMap.ToolBar());
                if (!this.transportAmapGeocoder) {
                  this.transportAmapGeocoder = new window.AMap.Geocoder();
                }
              });
              this.transportAmap.on('click', event => this.pickTransportMapPoint(event.lnglat));
            } else if (typeof this.transportAmap.resize === 'function') {
              this.transportAmap.resize();
            }
            if (this.transportMapSelected.longitude) {
              this.renderTransportMapMarker(
                this.toLngLat(
                  this.transportMapSelected.longitude,
                  this.transportMapSelected.latitude
                )
              );
            }
          });
        })
        .catch(() => {
          this.$message.error('高德地图组件加载失败，请稍后重试或重新打开弹窗');
          this.transportMapBox = false;
        });
    },
    searchTransportMapKeyword() {
      const keyword = String(this.transportMapKeyword || '').trim();
      if (!keyword) {
        this.$message.warning('请输入地址关键词');
        return;
      }
      this.loadAmap()
        .then(() => {
          this.transportMapLoading = true;
          this.ensureTransportAmapGeocoder()
            .then(() => this.runAmapGeocode('location', keyword))
            .then(result => {
              const point = this.resolveMapPoint(result);
              if (!point) {
                this.transportMapStatus = '未找到匹配地址';
                this.$message.warning('地图搜索无匹配地址');
                return;
              }
              this.pickTransportMapPoint(point, keyword);
            })
            .catch(() => {
              this.transportMapStatus = '地图搜索失败';
              this.$message.error('地图搜索失败，请稍后重试');
            })
            .finally(() => {
              this.transportMapLoading = false;
            });
        })
        .catch(() => {
          this.$message.error('高德地图组件加载失败，请稍后重试或重新打开弹窗');
        });
    },
    pickTransportMapPoint(lnglat, keyword) {
      const longitude = this.getPointLng(lnglat);
      const latitude = this.getPointLat(lnglat);
      if (longitude === undefined || latitude === undefined) {
        this.$message.warning('选点坐标无效');
        return;
      }
      const point = this.toLngLat(longitude, latitude);
      this.renderTransportMapMarker(point);
      this.transportMapSelected = this.buildTransportMapSelection({
        lng: longitude,
        lat: latitude,
        address: keyword,
      });
      this.transportMapStatus = '正在反查地址...';
      this.ensureTransportAmapGeocoder()
        .then(() => this.runAmapGeocode('address', point))
        .then(result => {
          const address = this.resolveMapAddress(result);
          this.transportMapSelected = {
            ...this.transportMapSelected,
            detailAddress: address.detailAddress || this.transportMapSelected.detailAddress,
            regionName: address.regionName || this.transportMapSelected.regionName,
            regionCode: address.regionCode || this.transportMapSelected.regionCode,
          };
          this.transportMapKeyword =
            this.transportMapSelected.detailAddress || this.transportMapKeyword;
          this.transportMapStatus = this.transportMapSelected.detailAddress || '已选点，可确认回填';
        })
        .catch(() => {
          this.transportMapStatus = '反查地址失败';
          this.$message.error('反查地址失败，请重新选点');
        });
    },
    renderTransportMapMarker(point) {
      if (!this.transportAmap || !window.AMap) return;
      if (this.transportAmapMarker) {
        this.transportAmapMarker.setMap(null);
      }
      this.transportAmapMarker = new window.AMap.Marker({
        position: point,
      });
      this.transportAmapMarker.setMap(this.transportAmap);
      this.transportAmap.setCenter(point);
    },
    confirmTransportMapPick() {
      if (!this.transportMapSelected.longitude) {
        this.$message.warning('请先搜索或点击地图完成选点');
        return;
      }
      const prefix = this.transportMapTarget;
      this.form[`${prefix}Address`] =
        this.transportMapSelected.detailAddress || this.form[`${prefix}Address`];
      this.form[`${prefix}Name`] =
        this.transportMapSelected.regionName || this.form[`${prefix}Name`];
      this.form[`${prefix}Longitude`] = this.transportMapSelected.longitude;
      this.form[`${prefix}Latitude`] = this.transportMapSelected.latitude;
      this.transportMapBox = false;
      this.$refs.crud?.validateField?.(`${prefix}Address`);
    },
    buildTransportMapSelection({ lng, lat, address, regionName, regionCode }) {
      const longitude = this.formatCoordinate(lng);
      const latitude = this.formatCoordinate(lat);
      return {
        longitude,
        latitude,
        detailAddress: address || '',
        regionName: regionName || '',
        regionCode: regionCode || '',
      };
    },
    formatCoordinate(value) {
      if (value === undefined || value === null || value === '') return '';
      const numberValue = Number(value);
      return Number.isFinite(numberValue) ? numberValue.toFixed(6) : '';
    },
    toLngLat(lng, lat) {
      return new window.AMap.LngLat(Number(lng), Number(lat));
    },
    getPointLng(point) {
      if (!point) return undefined;
      if (typeof point.getLng === 'function') return point.getLng();
      return point.lng ?? point.lon;
    },
    getPointLat(point) {
      if (!point) return undefined;
      if (typeof point.getLat === 'function') return point.getLat();
      return point.lat;
    },
    ensureTransportAmapGeocoder() {
      if (this.transportAmapGeocoder) {
        return Promise.resolve(this.transportAmapGeocoder);
      }
      return new Promise((resolve, reject) => {
        if (!window.AMap || !window.AMap.plugin) {
          reject(new Error('高德地图组件未就绪'));
          return;
        }
        window.AMap.plugin(['AMap.Geocoder'], () => {
          try {
            this.transportAmapGeocoder = new window.AMap.Geocoder();
            resolve(this.transportAmapGeocoder);
          } catch (error) {
            reject(error);
          }
        });
      });
    },
    runAmapGeocode(action, input) {
      return new Promise((resolve, reject) => {
        const timer = window.setTimeout(() => {
          reject(new Error('高德地图请求超时'));
        }, 10000);
        const done = (status, result) => {
          window.clearTimeout(timer);
          if (status === 'complete' && result) {
            resolve(result);
            return;
          }
          reject(new Error('高德地图请求失败'));
        };
        try {
          if (action === 'location') {
            this.transportAmapGeocoder.getLocation(input, done);
          } else {
            this.transportAmapGeocoder.getAddress(input, done);
          }
        } catch (error) {
          window.clearTimeout(timer);
          reject(error);
        }
      });
    },
    resolveMapPoint(result) {
      if (!result) return null;
      const geocode = Array.isArray(result.geocodes) ? result.geocodes[0] : null;
      if (geocode && geocode.location) return geocode.location;
      if (result.location) return result.location;
      if (result.lnglat) return result.lnglat;
      if (result.getLng || result.lng || result.lon) return result;
      return null;
    },
    resolveMapAddress(result = {}) {
      const regeocode = result.regeocode || {};
      const component = regeocode.addressComponent || {};
      const city = Array.isArray(component.city) ? '' : component.city;
      const regionName = [component.province, city, component.district].filter(Boolean).join('');
      return {
        detailAddress: regeocode.formattedAddress || '',
        regionName,
        regionCode: component.adcode || '',
      };
    },
    openCommonCargoDialog() {
      this.commonCargoSelected = [];
      this.commonCargoQuery = {};
      this.commonCargoPage.currentPage = 1;
      this.commonCargoBox = true;
    },
    loadCommonCargoList() {
      this.commonCargoLoading = true;
      getCommonCargoList(this.commonCargoPage.currentPage, this.commonCargoPage.pageSize, {
        ...this.buildCommonCargoQueryParams(),
        allDept: 0,
      })
        .then(res => {
          const data = res.data.data || {};
          this.commonCargoRows = data.records || [];
          this.commonCargoPage.total = data.total || 0;
        })
        .finally(() => {
          this.commonCargoLoading = false;
        });
    },
    buildCommonCargoQueryParams() {
      const { cargoTypePath, ...query } = this.commonCargoQuery || {};
      return Object.keys(query).reduce((params, key) => {
        const value = query[key];
        if (value !== undefined && value !== null && value !== '') {
          params[key] = value;
        }
        return params;
      }, {});
    },
    handleCommonCargoSearch() {
      this.commonCargoPage.currentPage = 1;
      this.loadCommonCargoList();
    },
    handleCommonCargoReset() {
      this.commonCargoQuery = {};
      this.commonCargoPage.currentPage = 1;
      this.loadCommonCargoList();
    },
    handleCommonCargoSizeChange(pageSize) {
      this.commonCargoPage.pageSize = pageSize;
      this.commonCargoPage.currentPage = 1;
      this.loadCommonCargoList();
    },
    handleCommonCargoCurrentChange(currentPage) {
      this.commonCargoPage.currentPage = currentPage;
      this.loadCommonCargoList();
    },
    handleCommonCargoTypeChange(value) {
      const path = this.normalizeBillingCargoTypePath(value);
      const labels = this.getBillingCargoTypePathLabels(path);
      const firstCargoType = this.billingCargoTypeFlatOptions.find(
        item => String(item.id) === String(path[0] || '') && item.path?.length === 1
      );
      const secondCargoType = this.findBillingCargoTypeByPath(path);
      this.commonCargoQuery = {
        ...this.commonCargoQuery,
        cargoTypePath: path,
        firstCargoTypeName: labels[0] || '',
        firstCargoTypeCode: firstCargoType?.cargoCode || firstCargoType?.code || '',
        secondCargoTypeName: labels[1] || '',
        secondCargoTypeCode: secondCargoType?.cargoCode || secondCargoType?.code || '',
      };
      if (!path.length) {
        delete this.commonCargoQuery.firstCargoTypeName;
        delete this.commonCargoQuery.firstCargoTypeCode;
        delete this.commonCargoQuery.secondCargoTypeName;
        delete this.commonCargoQuery.secondCargoTypeCode;
      }
    },
    formatCommonCargoSize(row) {
      return row.cargoSize || row.size || row.dimension || row.dimensions || '';
    },
    async selectCommonCargoRow(row) {
      this.commonCargoSelected = [row];
      await this.ensureBillingCargoTypeOptions();
      this.addTransportCargoRow(-1, this.mapCommonCargoRow(row));
      this.commonCargoBox = false;
    },
    mapCommonCargoRow(row = {}) {
      const cargoTypePath = this.resolveCommonCargoTypePath(row);
      const cargoType = this.findBillingCargoTypeByPath(cargoTypePath);
      const labels = this.getBillingCargoTypePathLabels(cargoTypePath);
      return {
        cargoName: row.cargoName,
        cargoTypePath,
        cargoType: labels[1] || row.secondCargoTypeName || row.firstCargoTypeName,
        cargoTypeCode:
          cargoType?.cargoCode ||
          cargoType?.code ||
          row.secondCargoTypeCode ||
          row.firstCargoTypeCode,
        packageType: row.packageType,
        brand: row.brand,
        specification: row.specification,
        model: row.model,
        remark: row.remark || row.descriptionOne || '',
      };
    },
    async handleCargoImportFileChange(file) {
      const rawFile = file.raw;
      if (!rawFile) return;
      this.cargoImportLoading = true;
      try {
        const ExcelJSModule = await import('exceljs');
        const ExcelJS = ExcelJSModule.default || ExcelJSModule;
        const workbook = new ExcelJS.Workbook();
        await workbook.xlsx.load(await rawFile.arrayBuffer());
        const worksheet = workbook.worksheets[0];
        const headerRow = worksheet.getRow(1);
        const headerMap = {};
        headerRow.eachCell((cell, colNumber) => {
          headerMap[String(cell.value || '').trim()] = colNumber;
        });
        const fieldMap = {
          货物名称: 'cargoName',
          货物类型: 'cargoType',
          数量: 'quantity',
          数量单位: 'quantityUnit',
          包装: 'packageType',
          品牌: 'brand',
          规格: 'specification',
          型号: 'model',
          物料编码: 'materialCode',
          设备编码: 'deviceCode',
          备注: 'remark',
        };
        const rows = [];
        worksheet.eachRow((row, rowNumber) => {
          if (rowNumber === 1) return;
          const nextRow = defaultTransportCargo();
          Object.entries(fieldMap).forEach(([label, prop]) => {
            const columnIndex = headerMap[label];
            if (!columnIndex) return;
            nextRow[prop] = String(row.getCell(columnIndex).value || '').trim();
          });
          if (Object.values(nextRow).some(value => String(value || '').trim())) {
            rows.push(nextRow);
          }
        });
        if (!rows.length) {
          this.$message.warning('未读取到货物数据');
          return;
        }
        rows.forEach(row => this.addTransportCargoRow(-1, row));
        this.$message.success(`成功导入${rows.length}条货物`);
        this.cargoImportBox = false;
      } catch (error) {
        window.console.log(error);
        this.$message.error('货物导入失败，请检查文件格式');
      } finally {
        this.cargoImportLoading = false;
      }
    },
    loadOrganizationOptions() {
      if (!this.config.enableOrganizationSelect || this.organizationLoading) return;
      this.organizationLoading = true;
      getDeptTree(this.userInfo?.tenantId)
        .then(res => {
          this.organizationOptions = this.normalizeOrganizationOptions(res.data.data || []);
          this.organizationFlatOptions = this.flattenOrganizationOptions(this.organizationOptions);
          this.syncOrganizationSearchDic();
          this.syncCurrentOrganizationOption();
        })
        .finally(() => {
          this.organizationLoading = false;
        });
    },
    normalizeOrganizationOptions(tree = []) {
      return (tree || []).map(item => {
        const name = item.title || item.deptName || item.name || item.label || '';
        const children = this.normalizeOrganizationOptions(item.children || []);
        return {
          ...item,
          id: String(item.id),
          label: name,
          rawLabel: name,
          children: children.length ? children : undefined,
        };
      });
    },
    flattenOrganizationOptions(tree = []) {
      const result = [];
      tree.forEach(item => {
        result.push({
          id: String(item.id),
          label: item.label,
          rawLabel: item.rawLabel || item.label,
        });
        if (item.children && item.children.length) {
          result.push(...this.flattenOrganizationOptions(item.children));
        }
      });
      return result;
    },
    syncOrganizationSearchDic() {
      const column = this.findColumn?.(this.option.column, 'organizationName');
      if (column) {
        column.dicData = this.organizationOptions;
      }
    },
    syncCurrentOrganizationOption() {
      if (!this.form.organizationId || !this.form.organizationName) return;
      const exists = this.organizationFlatOptions.some(
        item => String(item.id) === String(this.form.organizationId)
      );
      if (!exists) {
        const current = {
          id: String(this.form.organizationId),
          label: this.form.organizationName,
          rawLabel: this.form.organizationName,
        };
        this.organizationOptions.unshift(current);
        this.organizationFlatOptions.unshift(current);
        this.syncOrganizationSearchDic();
      }
    },
    findOrganizationOption(organizationId) {
      const value = Array.isArray(organizationId)
        ? organizationId[organizationId.length - 1]
        : organizationId;
      return this.organizationFlatOptions.find(item => String(item.id) === String(value));
    },
    handleOrganizationChange(organizationId) {
      const organization = this.findOrganizationOption(organizationId);
      if (!organization) {
        this.form.organizationId = '';
        this.form.organizationName = '';
        return;
      }
      this.form.organizationId = organization.id;
      this.form.organizationName = organization.rawLabel || organization.label || '';
      this.$refs.crud?.validateField?.('organizationName');
    },
    handleAmountInput(prop, value) {
      const text = String(value || '').replace(/[^\d.]/g, '');
      const parts = text.split('.');
      this.form[prop] =
        parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
    },
    handleIntegerInput(prop, value) {
      this.form[prop] = String(value || '').replace(/\D/g, '');
    },
    handleContractDateChange() {
      this.syncContractPeriod(true);
    },
    syncContractPeriod(validate = true) {
      if (!this.config.enableContractPeriod) return;
      this.form.contractPeriod =
        this.form.startDate && this.form.endDate
          ? `${this.form.startDate}至${this.form.endDate}`
          : '';
      if (validate) {
        this.$refs.crud?.validateField?.('contractPeriod');
      }
    },
    applyCurrentUserHandler() {
      if (!this.config.enableCurrentUserHandler || this.form.handlerUserName) return;
      this.form.handlerUserName =
        this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || '';
    },
    handleAttachmentChange(list) {
      const userName = this.userInfo?.realName || this.userInfo?.userName || '';
      const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
      this.attachmentRows = (list || []).map(item => ({
        ...item,
        description: item.description || '',
        uploadUserName: item.uploadUserName || userName,
        uploadTime: item.uploadTime || uploadTime,
      }));
      this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
    },
    removeAttachment(index) {
      this.attachmentRows.splice(index, 1);
      this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
    },
    formatFileSize(size) {
      const number = Number(size);
      if (!number) return '';
      if (number < 1024) return `${number}B`;
      if (number < 1024 * 1024) return `${(number / 1024).toFixed(1)}KB`;
      return `${(number / 1024 / 1024).toFixed(1)}MB`;
    },
    downloadAttachment(row) {
      const url = row.url || row.link;
      if (!url) {
        this.$message.warning('附件地址为空');
        return;
      }
      downloadFileByUrl(url, row.originalName || row.name || '附件');
    },
    handleBatchDownload() {
      this.attachmentRows.forEach(row => this.downloadAttachment(row));
    },
    handleContractFileChange(list) {
      const userName = this.userInfo?.realName || this.userInfo?.userName || '';
      const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
      this.contractFileRows = (list || []).map(item => ({
        ...item,
        uploadUserName: item.uploadUserName || userName,
        uploadTime: item.uploadTime || uploadTime,
      }));
      this.form.contractFileJson = JSON.stringify(this.contractFileRows);
    },
    removeContractFile(index) {
      this.contractFileRows.splice(index, 1);
      this.form.contractFileJson = JSON.stringify(this.contractFileRows);
    },
    cloneData(data) {
      return JSON.parse(JSON.stringify(data));
    },
    syncBillingPlanJson() {
      if (!this.config.enableBillingPlan) return;
      if (
        this.form.billingEnabled === undefined ||
        this.form.billingEnabled === null ||
        this.form.billingEnabled === ''
      ) {
        this.form.billingEnabled = 1;
      }
      this.form.billingPlanJson = JSON.stringify(this.billingPlanRows);
    },
    ensureBillingMatchRegionOptions() {
      if (!this.config.enableBillingPlan && !this.config.enableTransportPlanForm) {
        return Promise.resolve([]);
      }
      if (this.billingMatchRegionOptions.length) {
        return Promise.resolve(this.billingMatchRegionOptions);
      }
      if (this.billingMatchRegionRequest) {
        return this.billingMatchRegionRequest;
      }
      this.billingMatchRegionLoading = true;
      this.billingMatchRegionRequest = getRegionLazyTree()
        .then(res => {
          const regionTree = this.buildBillingMatchRegionTree(res.data.data || []);
          const regions = this.extractBillingMatchChinaRegions(regionTree);
          this.billingMatchRegionOptions = regions
            .map(item => this.normalizeBillingMatchRegionNode(item, 1))
            .filter(Boolean);
          return this.billingMatchRegionOptions;
        })
        .finally(() => {
          this.billingMatchRegionLoading = false;
          this.billingMatchRegionRequest = null;
        });
      return this.billingMatchRegionRequest;
    },
    ensureBillingCargoTypeOptions() {
      if (!this.config.enableBillingPlan && !this.config.enableTransportPlanForm) {
        return Promise.resolve([]);
      }
      if (this.billingCargoTypeOptions.length) {
        return Promise.resolve(this.billingCargoTypeOptions);
      }
      if (this.billingCargoTypeRequest) {
        return this.billingCargoTypeRequest;
      }
      this.billingCargoTypeLoading = true;
      this.billingCargoTypeRequest = getCargoTypeList(1, 9999)
        .then(res => {
          this.billingCargoTypeOptions = this.buildBillingCargoTypeTree(extractRecords(res));
          this.billingCargoTypeFlatOptions = this.flattenBillingCargoTypeOptions(
            this.billingCargoTypeOptions
          );
          return this.billingCargoTypeOptions;
        })
        .finally(() => {
          this.billingCargoTypeLoading = false;
          this.billingCargoTypeRequest = null;
        });
      return this.billingCargoTypeRequest;
    },
    buildBillingCargoTypeTree(cargoTypes = []) {
      const flatCargoTypes = [];
      const collectCargoTypes = list => {
        (list || []).forEach(item => {
          flatCargoTypes.push({
            ...item,
            children: undefined,
          });
          if (item.children?.length) collectCargoTypes(item.children);
        });
      };
      collectCargoTypes(cargoTypes);

      const nodeMap = new Map();
      const codeMap = new Map();
      flatCargoTypes.forEach(item => {
        const id = item.id ?? item.cargoCode ?? item.code;
        if (id === undefined || id === null) return;
        const node = {
          ...item,
          id: String(id),
          cargoName: this.formatBillingCargoTypeLabel(item),
          cargoCode: item.cargoCode || item.code || '',
          parentId:
            item.parentId === undefined || item.parentId === null ? '' : String(item.parentId),
          parentCargoCode: item.parentCargoCode || item.parentCode || '',
          children: [],
        };
        nodeMap.set(node.id, node);
        if (node.cargoCode) {
          codeMap.set(String(node.cargoCode), node);
        }
      });

      const rootNodes = [];
      nodeMap.forEach(node => {
        const parent =
          nodeMap.get(node.parentId) ||
          codeMap.get(String(node.parentCargoCode || '')) ||
          codeMap.get(String(node.parentId || ''));
        if (parent && parent !== node && Number(node.typeLevel) !== 1) {
          parent.children.push(node);
        } else {
          rootNodes.push(node);
        }
      });
      return rootNodes.map(item => this.normalizeBillingCargoTypeNode(item, 1)).filter(Boolean);
    },
    normalizeBillingCargoTypeNode(cargoType, level = 1, parentPath = []) {
      if (level > 2) return null;
      const id = String(cargoType.id);
      const path = [...parentPath, id];
      const children =
        level === 1
          ? (cargoType.children || [])
              .map(item => this.normalizeBillingCargoTypeNode(item, level + 1, path))
              .filter(Boolean)
          : [];
      return {
        ...cargoType,
        id,
        cargoName: this.formatBillingCargoTypeLabel(cargoType),
        cargoCode: cargoType.cargoCode || cargoType.code || '',
        path,
        leaf: level === 2,
        children: children.length ? children : undefined,
      };
    },
    flattenBillingCargoTypeOptions(options = []) {
      const result = [];
      const collectOptions = list => {
        (list || []).forEach(item => {
          result.push(item);
          if (item.children?.length) collectOptions(item.children);
        });
      };
      collectOptions(options);
      return result;
    },
    buildBillingMatchRegionTree(regions = []) {
      const flatRegions = [];
      const collectRegions = list => {
        (list || []).forEach(item => {
          flatRegions.push({
            ...item,
            children: undefined,
          });
          if (item.children?.length) collectRegions(item.children);
        });
      };
      collectRegions(regions);

      const nodeMap = new Map();
      flatRegions.forEach(item => {
        const id = item.id ?? item.code ?? item.value;
        if (id === undefined || id === null) return;
        nodeMap.set(String(id), {
          ...item,
          id: String(id),
          parentId:
            item.parentId === undefined || item.parentId === null
              ? String(item.parentCode || '')
              : String(item.parentId),
          children: [],
        });
      });

      const rootNodes = [];
      nodeMap.forEach(node => {
        const parent = nodeMap.get(node.parentId);
        if (parent && parent !== node) {
          parent.children.push(node);
        } else {
          rootNodes.push(node);
        }
      });
      return rootNodes;
    },
    normalizeBillingMatchRegionNode(region, level = 1) {
      if (level > 2) return null;
      const children =
        level === 1
          ? (region.children || [])
              .map(item => this.normalizeBillingMatchRegionNode(item, level + 1))
              .filter(Boolean)
          : [];
      return {
        ...region,
        title: region.title || region.name || '',
        leaf: level === 2,
        children: children.length ? children : undefined,
      };
    },
    isBillingMatchChinaRegion(region = {}) {
      const title = String(region.title || region.name || '').trim();
      return title === '中国' || title === '中华人民共和国';
    },
    extractBillingMatchChinaRegions(regions) {
      const china = (regions || []).find(item => this.isBillingMatchChinaRegion(item));
      if (china?.children?.length) return china.children;
      if (regions.length === 1 && regions[0].children?.length) return regions[0].children;
      return regions;
    },
    getBillingMatchRegionPathLabels(path = []) {
      let options = this.billingMatchRegionOptions;
      const labels = [];
      (path || []).forEach(value => {
        const option = (options || []).find(item => String(item.id) === String(value));
        if (!option) return;
        labels.push(option.title || option.name || '');
        options = option.children || [];
      });
      return labels.filter(Boolean);
    },
    handleBillingMatchRegionChange(prop, value) {
      const pathProp = `${prop}Path`;
      const path = Array.isArray(value) && value.length >= 2 ? value.slice(0, 2) : [];
      const labels = this.getBillingMatchRegionPathLabels(path);
      this.billingMatchForm[pathProp] = path;
      if (!path.length) {
        this.billingMatchForm[prop] = '';
        this.billingMatchForm[`${prop}Code`] = '';
        return;
      }
      if (labels.length) {
        this.billingMatchForm[prop] = labels.join('');
      }
      this.billingMatchForm[`${prop}Code`] = String(path[path.length - 1]);
    },
    formatBillingCargoTypeLabel(item = {}) {
      return item.cargoName || item.name || item.typeName || item.label || '';
    },
    filterBillingCargoType(node, keyword) {
      const text = String(keyword || '').trim();
      if (!text) return true;
      const labels = node.pathLabels?.length ? node.pathLabels : [node.label];
      return (
        labels.some(label => String(label || '').includes(text)) ||
        String(node.text || '').includes(text)
      );
    },
    getBillingCargoTypePathLabels(path = []) {
      let options = this.billingCargoTypeOptions;
      const labels = [];
      (path || []).forEach(value => {
        const option = (options || []).find(item => String(item.id) === String(value));
        if (!option) return;
        labels.push(option.cargoName || '');
        options = option.children || [];
      });
      return labels.filter(Boolean);
    },
    findBillingCargoTypeByPath(path = []) {
      const value = Array.isArray(path) && path.length >= 2 ? String(path[1]) : '';
      if (!value) return null;
      return (
        this.billingCargoTypeFlatOptions.find(
          item => String(item.id) === value && item.path?.length >= 2
        ) || null
      );
    },
    findBillingCargoTypeByCodeOrName(condition = {}) {
      const code = String(condition.cargoTypeCode || '').trim();
      const name = String(condition.cargoType || '').trim();
      return (
        this.billingCargoTypeFlatOptions.find(item => {
          const cargoCode = String(item.cargoCode || item.code || item.id || '');
          return item.path?.length >= 2 && code && cargoCode === code;
        }) ||
        this.billingCargoTypeFlatOptions.find(item => {
          const cargoName = this.formatBillingCargoTypeLabel(item);
          return item.path?.length >= 2 && name && (cargoName === name || name.endsWith(cargoName));
        }) ||
        null
      );
    },
    resolveCommonCargoTypePath(row = {}) {
      const path = this.normalizeBillingCargoTypePath(row.cargoTypePath);
      if (path.length) return path;
      const cargoType =
        this.findBillingCargoTypeByCodeOrName({
          cargoTypeCode: row.secondCargoTypeCode || row.firstCargoTypeCode || row.cargoTypeCode,
          cargoType: row.secondCargoTypeName || row.firstCargoTypeName || row.cargoType,
        }) ||
        this.findBillingCargoTypeByCodeOrName({
          cargoTypeCode: row.firstCargoTypeCode || '',
          cargoType: row.firstCargoTypeName || '',
        });
      return cargoType?.path || [];
    },
    handleBillingMatchCargoTypeChange(value) {
      const path =
        Array.isArray(value) && value.length >= 2
          ? value.slice(0, 2).map(item => String(item))
          : [];
      const cargoType = this.findBillingCargoTypeByPath(path);
      const labels = this.getBillingCargoTypePathLabels(path);
      this.billingMatchForm.cargoTypePath = path;
      if (!path.length || !cargoType) {
        this.billingMatchForm.cargoType = '';
        this.billingMatchForm.cargoTypeCode = '';
        return;
      }
      this.billingMatchForm.cargoType =
        labels.length > 1
          ? labels.join('/')
          : labels[0] || this.formatBillingCargoTypeLabel(cargoType || {});
      this.billingMatchForm.cargoTypeCode =
        cargoType?.cargoCode || cargoType?.code || cargoType?.id || '';
    },
    normalizeBillingCargoTypePath(path) {
      return Array.isArray(path) && path.length >= 2
        ? path.slice(0, 2).map(value => String(value))
        : [];
    },
    resolveBillingMatchCargoTypePath(condition = {}) {
      const path = this.normalizeBillingCargoTypePath(condition.cargoTypePath);
      if (path.length) return path;
      const cargoType = this.findBillingCargoTypeByCodeOrName(condition);
      return cargoType?.path || [];
    },
    normalizeBillingMatchRegionPath(path) {
      return Array.isArray(path) && path.length >= 2
        ? path.slice(0, 2).map(value => String(value))
        : [];
    },
    normalizeBillingMatchCondition(condition = {}) {
      const defaults = defaultBillingRule().matchCondition;
      const nextCondition = {
        ...defaults,
        ...(condition || {}),
      };
      nextCondition.originPath = this.normalizeBillingMatchRegionPath(condition?.originPath);
      nextCondition.destinationPath = this.normalizeBillingMatchRegionPath(
        condition?.destinationPath
      );
      nextCondition.cargoTypePath = this.normalizeBillingCargoTypePath(condition?.cargoTypePath);
      return nextCondition;
    },
    ensureBillingFeeCategoryOptions() {
      if (!this.config.enableBillingPlan) return Promise.resolve([]);
      if (this.billingFeeCategoryOptions.length) {
        return Promise.resolve(this.billingFeeCategoryOptions);
      }
      if (this.billingFeeCategoryRequest) {
        return this.billingFeeCategoryRequest;
      }
      this.billingFeeCategoryLoading = true;
      this.billingFeeCategoryRequest = getDictionary({ code: 'fee_category' })
        .then(res => {
          this.billingFeeCategoryOptions = res.data.data || [];
          return this.billingFeeCategoryOptions;
        })
        .finally(() => {
          this.billingFeeCategoryLoading = false;
          this.billingFeeCategoryRequest = null;
        });
      return this.billingFeeCategoryRequest;
    },
    ensureBillingUnitOptions() {
      if (!this.config.enableBillingPlan) return Promise.resolve([]);
      if (this.billingUnitOptions.length) {
        return Promise.resolve(this.billingUnitOptions);
      }
      if (this.billingUnitRequest) {
        return this.billingUnitRequest;
      }
      this.billingUnitLoading = true;
      this.billingUnitRequest = getDictionary({ code: 'unit_fee' })
        .then(res => {
          this.billingUnitOptions = res.data.data || [];
          return this.billingUnitOptions;
        })
        .finally(() => {
          this.billingUnitLoading = false;
          this.billingUnitRequest = null;
        });
      return this.billingUnitRequest;
    },
    resolveBillingFeeType(value) {
      const feeType = String(value || '').trim();
      if (!feeType) return '';
      const option = this.billingFeeCategoryOptions.find(
        item => String(item.dictKey || '') === feeType || String(item.dictValue || '') === feeType
      );
      return String((option && option.dictKey) || feeType);
    },
    getBillingFeeItemOptions(feeType) {
      const key = this.resolveBillingFeeType(feeType);
      return key ? this.billingFeeItemOptionsMap[key] || [] : [];
    },
    loadBillingFeeItemOptions(feeType) {
      const key = this.resolveBillingFeeType(feeType);
      if (!key) return Promise.resolve([]);
      if (this.billingFeeItemOptionsMap[key]) {
        return Promise.resolve(this.billingFeeItemOptionsMap[key]);
      }
      if (this.billingFeeItemRequestMap[key]) {
        return this.billingFeeItemRequestMap[key];
      }
      this.billingFeeItemLoadingMap[key] = true;
      this.billingFeeItemRequestMap[key] = getFeeItemList(1, 9999, { feeCategory: key })
        .then(res => {
          const records = extractRecords(res);
          this.billingFeeItemOptionsMap[key] = records;
          return records;
        })
        .finally(() => {
          this.billingFeeItemLoadingMap[key] = false;
          delete this.billingFeeItemRequestMap[key];
        });
      return this.billingFeeItemRequestMap[key];
    },
    loadBillingRuleFeeItems(rules = []) {
      const feeTypes = Array.from(
        new Set((rules || []).map(row => this.resolveBillingFeeType(row.feeType)).filter(Boolean))
      );
      feeTypes.forEach(feeType => this.loadBillingFeeItemOptions(feeType));
    },
    handleBillingFeeTypeChange(row, value) {
      row.feeType = this.resolveBillingFeeType(value);
      row.feeItem = '';
      this.loadBillingFeeItemOptions(row.feeType);
    },
    openBillingPlan(row, index = -1) {
      this.billingPlanMode = index > -1 ? 'edit' : 'add';
      this.billingPlanIndex = index;
      const plan = row ? this.cloneData(row) : defaultBillingPlan();
      this.billingPlanForm = {
        ...defaultBillingPlan(),
        ...plan,
        rules: this.normalizeBillingRules(
          Array.isArray(plan.rules) && plan.rules.length ? plan.rules : [defaultBillingRule()]
        ),
      };
      this.billingPlanBox = true;
      this.ensureBillingUnitOptions();
      this.ensureBillingFeeCategoryOptions().then(() => {
        this.billingPlanForm.rules = this.normalizeBillingRules(this.billingPlanForm.rules);
        this.loadBillingRuleFeeItems(this.billingPlanForm.rules);
      });
      this.$nextTick(() => this.$refs.billingPlanFormRef?.clearValidate?.());
    },
    submitBillingPlan() {
      this.$refs.billingPlanFormRef?.validate(valid => {
        if (!valid) return;
        if (!this.validateBillingRules()) return;
        const plan = this.cloneData(this.billingPlanForm);
        plan.rules = this.normalizeBillingRules(plan.rules).map(rule => {
          const ranges = this.canEditBillingLimit(rule) ? rule.limitRanges : [];
          return {
            ...rule,
            limitRanges: ranges,
            lowerLimit: ranges[0]?.lowerLimit || '',
            upperLimit: ranges[0]?.upperLimit || '',
            minimumBillingWeight: this.canEditMinimumBillingWeight(rule)
              ? rule.minimumBillingWeight
              : '',
          };
        });
        if (!this.billingPlanRows.length && this.billingPlanIndex === -1) {
          plan.defaultPlan = true;
        }
        if (plan.defaultPlan) {
          this.billingPlanRows.forEach(item => {
            item.defaultPlan = false;
          });
        }
        if (this.billingPlanIndex > -1) {
          this.billingPlanRows.splice(this.billingPlanIndex, 1, plan);
        } else {
          this.billingPlanRows.push(plan);
        }
        this.syncBillingPlanJson();
        this.billingPlanBox = false;
      });
    },
    validateBillingRules() {
      const groups = {};
      const requiredFields = [
        ['feeType', '费用类型'],
        ['feeItem', '费用项'],
        ['billingElement', '计费要素'],
        ['billingType', '计费类型'],
        ['billingUnit', '计费单位'],
        ['unitPrice', '单价'],
      ];
      for (const [index, row] of (this.billingPlanForm.rules || []).entries()) {
        const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(row[prop]));
        if (emptyField) {
          this.$message.warning(`第${index + 1}行${emptyField[1]}不能为空`);
          return false;
        }
        if (row.billingElement && !this.getBillingTypeOptions(row).includes(row.billingType)) {
          this.$message.warning('请选择计费要素对应的计费类型');
          return false;
        }
        if (!this.canEditBillingLimit(row)) {
          continue;
        }
        const key = row.billingElement || '';
        groups[key] = groups[key] || [];
        const ranges = this.getBillingLimitRanges(row);
        for (const range of ranges) {
          const lower = Number(range.lowerLimit);
          const upper = Number(range.upperLimit);
          if (
            range.lowerLimit === '' ||
            range.upperLimit === '' ||
            Number.isNaN(lower) ||
            Number.isNaN(upper)
          ) {
            this.$message.warning('请完整填写计费要素上下限');
            return false;
          }
          if (lower > upper) {
            this.$message.warning('计费要素下限不能大于上限');
            return false;
          }
          groups[key].push({ lower, upper });
        }
      }
      return this.validateBillingLimitRangeGroups(groups);
    },
    validateBillingLimitRangeGroups(groups) {
      const precision = 0.000001;
      for (const [billingElement, ranges] of Object.entries(groups)) {
        if (ranges.length <= 1) continue;
        const sortedRanges = [...ranges].sort((prev, next) => {
          if (prev.lower !== next.lower) return prev.lower - next.lower;
          return prev.upper - next.upper;
        });
        for (let index = 1; index < sortedRanges.length; index += 1) {
          const prevRange = sortedRanges[index - 1];
          const currentRange = sortedRanges[index];
          if (currentRange.lower < prevRange.upper - precision) {
            this.$message.warning(`${billingElement}的计费要素区间不能重叠`);
            return false;
          }
          if (currentRange.lower > prevRange.upper + precision) {
            this.$message.warning(`${billingElement}的计费要素区间必须连续，不能存在间隙`);
            return false;
          }
        }
      }
      return true;
    },
    isBillingFieldEmpty(value) {
      return value === undefined || value === null || String(value).trim() === '';
    },
    removeBillingPlan(index) {
      this.$confirm('确定删除该计费方案?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      }).then(() => {
        this.billingPlanRows.splice(index, 1);
        this.syncBillingPlanJson();
      });
    },
    addBillingRule() {
      const row = defaultBillingRule();
      this.billingPlanForm.rules.push(row);
      this.ensureBillingFeeCategoryOptions();
    },
    copyBillingRule(row) {
      const nextRow = this.cloneData(row);
      this.billingPlanForm.rules.push(nextRow);
      this.loadBillingFeeItemOptions(nextRow.feeType);
    },
    removeBillingRule(index) {
      this.billingPlanForm.rules.splice(index, 1);
      if (!this.billingPlanForm.rules.length) {
        this.billingPlanForm.rules.push(defaultBillingRule());
      }
    },
    getBillingTypeOptions(row) {
      return this.billingElementTypeMap[row.billingElement] || [];
    },
    handleBillingElementChange(row) {
      if (!this.getBillingTypeOptions(row).includes(row.billingType)) {
        row.billingType = '';
      }
      if (!this.canEditMinimumBillingWeight(row)) {
        row.minimumBillingWeight = '';
      }
      this.handleBillingTypeChange(row);
    },
    handleBillingTypeChange(row) {
      if (!this.canEditBillingLimit(row)) {
        row.limitRanges = [{ lowerLimit: '', upperLimit: '' }];
        row.lowerLimit = '';
        row.upperLimit = '';
        return;
      }
      row.limitRanges = this.normalizeBillingLimitRanges(row);
      this.syncBillingLegacyLimit(row);
    },
    canEditBillingLimit(row) {
      return (
        !this.dialogReadonly &&
        Boolean(row.billingElement) &&
        Boolean(row.billingType) &&
        (row.billingType.includes('区间') || row.billingType.includes('阶梯'))
      );
    },
    canEditMinimumBillingWeight(row) {
      return !this.dialogReadonly && ['按重量', '按吨·公里'].includes(row.billingElement);
    },
    getBillingLimitRanges(row) {
      return Array.isArray(row.limitRanges) && row.limitRanges.length
        ? row.limitRanges
        : [{ lowerLimit: '', upperLimit: '' }];
    },
    normalizeBillingRules(rules = []) {
      return (rules || []).map(rule => {
        const nextRule = {
          ...defaultBillingRule(),
          ...rule,
        };
        nextRule.feeType = this.resolveBillingFeeType(nextRule.feeType);
        nextRule.limitRanges = this.normalizeBillingLimitRanges(nextRule);
        this.syncBillingLegacyLimit(nextRule);
        return nextRule;
      });
    },
    normalizeBillingLimitRanges(row) {
      const ranges = Array.isArray(row.limitRanges) ? row.limitRanges : [];
      const nextRanges = ranges
        .map(item => ({
          lowerLimit: item?.lowerLimit ?? '',
          upperLimit: item?.upperLimit ?? '',
        }))
        .filter(item => item.lowerLimit !== '' || item.upperLimit !== '');
      if (!nextRanges.length && (row.lowerLimit !== '' || row.upperLimit !== '')) {
        nextRanges.push({
          lowerLimit: row.lowerLimit ?? '',
          upperLimit: row.upperLimit ?? '',
        });
      }
      return nextRanges.length ? nextRanges : [{ lowerLimit: '', upperLimit: '' }];
    },
    syncBillingLegacyLimit(row) {
      const firstRange = row.limitRanges?.[0] || {};
      row.lowerLimit = firstRange.lowerLimit || '';
      row.upperLimit = firstRange.upperLimit || '';
    },
    addBillingLimitRange(row, index) {
      row.limitRanges = this.getBillingLimitRanges(row);
      row.limitRanges.splice(index + 1, 0, { lowerLimit: '', upperLimit: '' });
    },
    removeBillingLimitRange(row, index) {
      row.limitRanges = this.getBillingLimitRanges(row);
      row.limitRanges.splice(index, 1);
      if (!row.limitRanges.length) {
        row.limitRanges.push({ lowerLimit: '', upperLimit: '' });
      }
      this.syncBillingLegacyLimit(row);
    },
    handleBillingLimitInput(row, index, prop, value) {
      row.limitRanges = this.getBillingLimitRanges(row);
      const text = String(value || '').replace(/[^\d.]/g, '');
      const parts = text.split('.');
      row.limitRanges[index][prop] =
        parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
      this.syncBillingLegacyLimit(row);
    },
    openBillingMatch(row, index) {
      this.billingMatchRuleIndex = index;
      this.billingMatchForm = this.normalizeBillingMatchCondition(row.matchCondition);
      this.billingMatchBox = true;
      this.ensureBillingMatchRegionOptions();
      this.ensureBillingCargoTypeOptions().then(() => {
        const path = this.resolveBillingMatchCargoTypePath(this.billingMatchForm);
        if (path.length) {
          this.handleBillingMatchCargoTypeChange(path);
        }
      });
    },
    saveBillingMatch() {
      const row = this.billingPlanForm.rules[this.billingMatchRuleIndex];
      if (row) {
        this.handleBillingMatchRegionChange('origin', this.billingMatchForm.originPath);
        this.handleBillingMatchRegionChange('destination', this.billingMatchForm.destinationPath);
        this.handleBillingMatchCargoTypeChange(this.billingMatchForm.cargoTypePath);
        row.matchCondition = this.cloneData(this.billingMatchForm);
      }
      this.billingMatchBox = false;
    },
    handleBillingDecimalInput(row, prop, value) {
      const text = String(value || '').replace(/[^\d.]/g, '');
      const parts = text.split('.');
      row[prop] =
        parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
    },
    searchReset() {
      this.query = {};
      this.page.currentPage = 1;
      this.onLoad(this.page);
    },
    searchChange(params, done) {
      this.query = params;
      this.page.currentPage = 1;
      this.onLoad(this.page, params);
      done();
    },
    selectionChange(list) {
      this.selectionList = list;
    },
    selectionClear() {
      this.selectionList = [];
      if (this.$refs.crud) {
        this.$refs.crud.toggleSelection();
      }
    },
    currentChange(currentPage) {
      this.page.currentPage = currentPage;
    },
    sizeChange(pageSize) {
      this.page.pageSize = pageSize;
    },
    refreshChange() {
      this.onLoad(this.page, this.query);
    },
    handleScopeChange() {
      if (!this.canViewAllDept) {
        this.allDept = 0;
      }
      this.page.currentPage = 1;
      this.onLoad(this.page);
    },
    normalizeSearchRangeValue(value, suffix) {
      if (!value) return undefined;
      return suffix ? `${value} ${suffix}` : value;
    },
    normalizeSearchRangeParams(params = {}) {
      const query = { ...params };
      const rangeMap = this.config.searchRangeMap || {};
      Object.entries(rangeMap).forEach(([rangeProp, mapping]) => {
        const [startProp, endProp, startSuffix, endSuffix] = Array.isArray(mapping)
          ? mapping
          : [mapping.startProp, mapping.endProp, mapping.startSuffix, mapping.endSuffix];
        const range = query[rangeProp];
        if (Array.isArray(range) && range.length === 2) {
          query[startProp] = this.normalizeSearchRangeValue(range[0], startSuffix);
          query[endProp] = this.normalizeSearchRangeValue(range[1], endSuffix);
        }
        delete query[rangeProp];
      });
      return query;
    },
    normalizeQueryParams(params = {}) {
      let query = { ...params };
      if (Object.keys(this.config.searchRangeMap || {}).length) {
        query = this.normalizeSearchRangeParams(query);
      }
      if (this.config.enableOrganizationSelect && query.organizationName) {
        const organizationId = Array.isArray(query.organizationName)
          ? query.organizationName[query.organizationName.length - 1]
          : query.organizationName;
        if (!organizationId) {
          delete query.organizationId;
          delete query.organizationName;
          return query;
        }
        const organization = this.findOrganizationOption(organizationId);
        query.organizationId = organizationId;
        if (organization) {
          query.organizationName = organization.rawLabel || organization.label || '';
        } else {
          delete query.organizationName;
        }
        if (this.canViewAllDept) {
          query.allDept = 1;
        }
      }
      return query;
    },
    onLoad(page, params = {}) {
      this.loading = true;
      const query = this.normalizeQueryParams({ ...params, ...this.query, allDept: this.allDept });
      this.api
        .getList(page.currentPage, page.pageSize, query)
        .then(res => {
          const result = res.data.data;
          this.page.total = result.total;
          this.data = result.records;
          this.selectionClear();
        })
        .finally(() => {
          this.loading = false;
        });
    },
    buildExportParams() {
      const query = this.normalizeQueryParams({ ...this.query, allDept: this.allDept });
      return {
        ...query,
        ids: this.ids,
        [this.website.tokenHeader]: getToken(),
      };
    },
    handleExport() {
      this.$confirm(`是否导出${this.config.exportName || this.config.title}?`, '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      }).then(() => {
        NProgress.start();
        exportBlob(this.config.exportUrl, this.buildExportParams(), { feedback: true })
          .then(res => {
            downloadXls(
              res.data,
              `${this.config.exportName || this.config.title}${this.$dayjs().format(
                'YYYY-MM-DD HH:mm:ss'
              )}.xlsx`
            );
          })
          .finally(() => {
            NProgress.done();
          });
      });
    },
    handleTemplate() {
      NProgress.start();
      exportBlob(
        this.config.templateUrl,
        {
          [this.website.tokenHeader]: getToken(),
        },
        { feedback: true }
      )
        .then(res => {
          downloadXls(res.data, `${this.config.title}模板.xlsx`);
        })
        .finally(() => {
          NProgress.done();
        });
    },
    handleImport() {
      const column = this.findColumn(this.excelOptionConfig.column, 'excelFile');
      column.action = this.config.importUrl;
      openImportDialog(this, this.config.title, () => this.onLoad(this.page, this.query));
    },
    handleCopy(row) {
      this.$confirm(`确定复制该${this.config.title}?`, '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      })
        .then(() => this.api.copy(row.id))
        .then(() => {
          this.$message.success('复制成功');
          this.onLoad(this.page, this.query);
        });
    },
    handleAction(action, row) {
      const actionName = {
        enable: '启用',
        disable: '停用',
        cancel: '取消',
        complete: '完成',
        reassign: '重新派单',
      }[action];
      this.$confirm(`确定${actionName}该${this.config.title}?`, '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      })
        .then(() => this.api[action](row.id))
        .then(() => {
          this.$message.success(`${actionName}成功`);
          this.onLoad(this.page, this.query);
        });
    },
    handleCustomOperation(operation, row) {
      if (operation.action === 'flow') {
        this.handleFlow(row);
        return;
      }
      const run = value => {
        const args = operation.prompt ? [row.id, value] : [row.id];
        this.api[operation.action](...args).then(() => {
          this.$message.success(`${operation.label}成功`);
          this.onLoad(this.page, this.query);
        });
      };
      if (operation.prompt) {
        this.$prompt(operation.prompt, operation.label, {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          inputPattern: /\S+/,
          inputErrorMessage: operation.promptError || '请输入内容',
        }).then(({ value }) => run(value));
        return;
      }
      this.$confirm(operation.confirm || `确定${operation.label}该${this.config.title}?`, '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      }).then(() => run());
    },
    handleFlow(row, loadedDetail = false) {
      const processInstanceId = row.processInstanceId || row.processInstanceID || row.procInstId;
      if (
        !processInstanceId &&
        !loadedDetail &&
        row.id &&
        typeof this.api.getDetail === 'function'
      ) {
        this.api.getDetail(row.id).then(res => this.handleFlow(res.data.data || {}, true));
        return;
      }
      if (!processInstanceId) {
        this.$message.warning('流程实例为空，无法查看流程');
        return;
      }
      if (this.website.design.designMode) {
        this.processInstanceId = processInstanceId;
      } else {
        this.flowUrl = `/api/blade-flow/process/diagram-view?processInstanceId=${processInstanceId}`;
      }
      this.flowBox = true;
    },
    handleChangeRecordFlow(row) {
      const processInstanceId = row.processInstanceId || row.processInstanceID || row.procInstId;
      if (!processInstanceId) {
        this.$message.warning('该变更记录暂无流程实例');
        return;
      }
      this.handleFlow(row, true);
    },
    handleBatchComplete() {
      if (!this.selectionList.length) {
        this.$message.warning('请选择至少一条数据');
        return;
      }
      this.$confirm('确定完成选中的运单?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      })
        .then(() => this.api.batchComplete(this.ids))
        .then(res => {
          const result = res.data.data || {};
          this.$message.success(
            `成功完成${result.successCount || 0}条，跳过${result.skippedCount || 0}条`
          );
          this.onLoad(this.page, this.query);
        });
    },
    handleRoadLoading() {
      if (!this.selectionList.length) {
        this.$message.warning('请选择至少一条数据');
        return;
      }
      if (typeof this.api.roadLoading !== 'function') {
        this.$message.warning('公路配载功能暂未配置');
        return;
      }
      this.$confirm('确定对选中的运单进行公路配载?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      })
        .then(() => this.api.roadLoading(this.ids))
        .then(() => {
          this.$message.success('公路配载成功');
          this.onLoad(this.page, this.query);
        });
    },
  },
};
</script>

<style lang="scss" scoped>
.business-crud-page {
  &__field {
    width: 100%;
  }

  .dialog-section-title {
    display: flex;
    align-items: center;
    width: 100%;
    color: #303133;
    font-size: 15px;
    font-weight: 600;

    &::before {
      display: block;
      width: 4px;
      height: 16px;
      margin-right: 8px;
      background: #409eff;
      content: '';
    }

    &--action {
      justify-content: space-between;
    }
  }

  &__shipping-title,
  &__goods-title {
    min-width: 100%;
  }

  &__goods-title &__section-actions,
  &__shipping-title .el-button {
    margin-left: auto;
  }

  &__section-actions {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    gap: 8px;
  }

  :deep(.business-crud-page__goods-info-cell .cell) {
    overflow: visible;
    line-height: 20px;
    white-space: normal;
    word-break: break-word;
    text-overflow: clip;
  }

  &__route-address-row {
    display: grid;
    grid-template-columns: 260px minmax(280px, 1fr) 40px 58px minmax(160px, 220px) 72px minmax(
        160px,
        220px
      );
    gap: 8px;
    align-items: center;
    width: 100%;

    :deep(.el-input),
    :deep(.el-select),
    :deep(.el-cascader) {
      width: 100%;
    }

    .el-button {
      width: 32px;
      min-width: 32px;
      padding: 0;
    }
  }

  &__route-label {
    color: #606266;
    font-size: 14px;
    line-height: 32px;
    text-align: right;
    white-space: nowrap;
  }

  &__cargo-wrap {
    display: block;
    width: 100%;
    min-width: 0;
    overflow-x: auto;
  }

  &__cargo-table {
    width: 100%;
    min-width: 1870px;

    :deep(.el-table__header th) {
      background: #f5f7fa;
      color: #303133;
      font-weight: 600;
    }

    :deep(.el-table__cell) {
      border-color: #eff1f7;
    }

    :deep(.el-input),
    :deep(.el-select),
    :deep(.el-cascader) {
      width: 100%;
    }
  }

  :deep(.business-crud-page__full-form-item) {
    width: 100%;
    max-width: 100%;
  }

  :deep(.business-crud-page__full-form-item .el-form-item) {
    width: 100%;
  }

  :deep(.business-crud-page__full-form-item .el-form-item__label) {
    display: none;
    width: 0 !important;
    padding: 0 !important;
  }

  :deep(.business-crud-page__full-form-item .el-form-item__content) {
    flex: 1 1 100%;
    width: 100%;
    max-width: 100%;
    margin-left: 0 !important;
  }

  :deep(.business-crud-page__full-form-item .el-form-item__content > div) {
    width: 100%;
    min-width: 0;
  }

  &__cargo-total {
    display: flex;
    justify-content: flex-end;
    padding-top: 12px;
    color: #303133;
    font-weight: 600;
  }

  &__dialog-search {
    padding: 12px 12px 4px;
    margin-bottom: 12px;
    border: 1px solid #eff1f7;
    border-radius: 4px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);

    :deep(.el-form-item) {
      margin-bottom: 8px;
    }

    :deep(.el-input),
    :deep(.el-select),
    :deep(.el-cascader) {
      width: 100%;
    }
  }

  &__dialog-search-actions {
    display: flex;
    align-items: flex-start;
    justify-content: flex-end;
  }

  &__dialog-pagination {
    display: flex;
    justify-content: flex-end;
    margin-top: 12px;
  }

  &__map-toolbar {
    display: flex;
    gap: 8px;
    margin-bottom: 12px;

    .el-input {
      flex: 1;
      min-width: 0;
    }
  }

  &__map {
    width: 100%;
    height: 420px;
    border: 1px solid #eff1f7;
  }

  &__map-info {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    margin-top: 10px;
    color: #606266;
    line-height: 1.6;
  }

  &__date-range {
    display: flex;
    align-items: center;
    width: 100%;
    min-width: 0;
  }

  &__date {
    flex: 1;
    min-width: 0;
  }

  &__date-separator {
    flex: 0 0 auto;
    padding: 0 8px;
    color: #303133;
  }

  &__inline-number {
    display: flex;
    align-items: center;
    width: 100%;

    .el-input {
      flex: 1;
      min-width: 0;
    }

    span {
      flex: 0 0 auto;
      margin-left: 8px;
      color: #303133;
    }
  }

  &__billing {
    width: 100%;
  }

  &__billing-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 16px;
  }

  &__billing-switch {
    display: flex;
    align-items: center;
    gap: 12px;
    color: #303133;
  }

  &__billing-table {
    width: 100%;

    :deep(.el-table__header th) {
      background: #f5f7fa;
      color: #303133;
      font-weight: 600;
    }

    :deep(.el-table__cell) {
      border-color: #eff1f7;
    }
  }

  &__settlement-rule {
    width: 100%;
  }

  &__settlement-switch {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 18px;
    color: #303133;
  }

  &__module-form {
    width: 100%;

    :deep(.el-form-item) {
      margin-bottom: 16px;
    }

    :deep(.el-select),
    :deep(.el-date-editor.el-input) {
      width: 100%;
    }
  }

  &__module-form--three {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    column-gap: 72px;
  }

  &__module-form--two {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 96px;
  }

  &__billing-dialog {
    :deep(.el-dialog__body) {
      padding-top: 28px;
    }

    :deep(.el-dialog__body),
    :deep(.el-form) {
      width: 100%;
    }
  }

  &__billing-form-row {
    display: grid;
    grid-template-columns: minmax(360px, 520px) 1fr;
    column-gap: 48px;
    align-items: center;
  }

  &__billing-default {
    margin-bottom: 18px;
  }

  &__billing-rule-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin: 20px 0 12px;
  }

  &__required-column {
    &::before {
      margin-right: 4px;
      color: #f56c6c;
      content: '*';
    }
  }

  &__billing-tip {
    display: flex;
    max-width: 620px;
    color: #ff6f86;
    line-height: 1.7;

    span {
      position: relative;
      display: inline-flex;
      flex: 0 0 auto;
      align-items: center;
      justify-content: center;
      width: 28px;
      height: 28px;
      margin-right: 12px;
      border-radius: 50%;
      background: #ffd6ea;
      color: #ff4f92;
      font-weight: 600;

      &::after {
        position: absolute;
        right: 6px;
        bottom: -8px;
        width: 0;
        height: 0;
        border: 8px solid transparent;
        border-top-color: #ffd6ea;
        content: '';
      }
    }
  }

  &__billing-rule-wrap {
    width: 100%;
    overflow-x: auto;
  }

  &__billing-rule-table {
    width: 100%;
    min-width: 2230px;

    :deep(.el-table__header th) {
      background: #f5f7fa;
      color: #303133;
      font-weight: 600;
    }

    :deep(.el-table__cell) {
      border-color: #eff1f7;
      vertical-align: top;
    }
  }

  &__limit-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: 100%;
  }

  &__limit-row {
    display: flex;
    align-items: center;
    gap: 8px;

    .el-input {
      flex: 1;
      min-width: 0;
    }

    .el-button {
      flex: 0 0 auto;
      padding: 0;
    }
  }

  &__dialog-footer {
    display: flex;
    justify-content: center;
    gap: 16px;
  }

  &__match-form {
    padding: 12px 32px 4px;

    .el-select,
    .el-cascader {
      width: 100%;
    }
  }

  &__attachment {
    width: 100%;
  }

  &__attachment-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 16px;
  }

  &__attachment-table {
    width: 100%;
    min-width: 1000px;

    :deep(.el-table__header th) {
      background: #f5f7fa;
      color: #303133;
      font-weight: 600;
    }

    :deep(.el-table__cell) {
      border-color: #eff1f7;
    }
  }

  &__change-table {
    width: 100%;

    :deep(.el-table__header th) {
      background: #f5f7fa;
      color: #303133;
      font-weight: 600;
    }

    :deep(.el-table__cell) {
      border-color: #eff1f7;
    }
  }

  @media (max-width: 1280px) {
    &__route-address-row {
      grid-template-columns: 240px minmax(240px, 1fr) 40px 58px minmax(140px, 1fr);
    }

    &__route-address-row &__route-label:nth-of-type(2),
    &__route-address-row .el-input:last-child {
      grid-column: auto;
    }
  }

  @media (max-width: 960px) {
    &__route-address-row {
      grid-template-columns: 1fr;
    }

    &__route-label {
      text-align: left;
    }

    &__dialog-search-actions {
      justify-content: flex-start;
    }
  }
}
</style>
