yeg
2 天以前 d8eaa16682cf180e9563d6be05fb43057daf6c07
talent-card-admin/src/views/MerchantSettlementView.vue
@@ -1,8 +1,17 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Refresh, Search } from '@element-plus/icons-vue'
import { getMerchantSettlement, getMerchantSettlementMerchantTypes, getMerchantSettlements } from '../api/admin'
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import {
  adoptManagedOnboarding,
  getAdminMerchants,
  getMerchantSettlement,
  getMerchantSettlementMerchantTypes,
  getMerchantSettlements,
  previewManagedOnboarding,
} from '../api/admin'
import { hasAdminPermission } from '../utils/permissions'
type Kind = 'onboardings' | 'bank-cards' | 'settlements'
@@ -202,6 +211,108 @@
  }
}
/**
 * 纳管外部进件商户。
 *
 * 流程刻意分两步:先「核验通道商户」做一次只读查询,把通道侧的商户注册名称、商户简称与主体类型
 * 展示出来,运营与所选商家核对后**显式勾选确认**才能提交。不做「名称是否对得上」的程序判定——
 * 我们这边只有一个商家名(常是简称或服务点名),通道那边是工商全称,形态不对应,规则化匹配必然误判。
 */
const adoptVisible = ref(false)
const adoptPreviewing = ref(false)
const adoptSubmitting = ref(false)
const adoptConfirmed = ref(false)
const adoptPreview = ref<Record<string, any>>()
const adoptForm = reactive({ merchantId: undefined as number | string | undefined, providerHuifuId: '', providerApplyNo: '' })
const merchantOptions = ref<Array<{ id: number | string; merchantName: string }>>([])
const merchantLoading = ref(false)
async function searchMerchants(keyword: string) {
  merchantLoading.value = true
  try {
    const result = await getAdminMerchants({ page: 1, size: 20, keyword: keyword?.trim() || undefined })
    merchantOptions.value = result.records || []
  } catch {
    merchantOptions.value = []
  } finally {
    merchantLoading.value = false
  }
}
function openAdopt() {
  adoptForm.merchantId = undefined
  adoptForm.providerHuifuId = ''
  adoptForm.providerApplyNo = ''
  adoptPreview.value = undefined
  adoptConfirmed.value = false
  adoptVisible.value = true
  if (merchantOptions.value.length === 0) searchMerchants('')
}
/** 换了商家或商户号,之前那次核验的对象就不是现在填的了,确认必须重新勾。 */
function invalidateAdoptPreview() {
  adoptPreview.value = undefined
  adoptConfirmed.value = false
}
async function runAdoptPreview() {
  if (!adoptForm.merchantId) {
    ElMessage.warning('请先选择我方商家')
    return
  }
  if (!adoptForm.providerHuifuId.trim()) {
    ElMessage.warning('请先填写汇付商户号')
    return
  }
  adoptPreviewing.value = true
  try {
    adoptPreview.value = await previewManagedOnboarding({
      merchantId: adoptForm.merchantId,
      providerHuifuId: adoptForm.providerHuifuId.trim(),
    })
    adoptConfirmed.value = false
  } catch (error: any) {
    adoptPreview.value = undefined
    adoptConfirmed.value = false
    ElMessage.error(error?.message || '核验通道商户失败,请确认商户号后重试')
  } finally {
    adoptPreviewing.value = false
  }
}
async function submitAdopt() {
  if (!adoptPreview.value) {
    ElMessage.warning('请先核验通道商户')
    return
  }
  if (!adoptForm.providerApplyNo.trim()) {
    ElMessage.warning('请填写汇付侧的入驻申请单号')
    return
  }
  if (!adoptConfirmed.value) {
    ElMessage.warning('请先核对通道商户名称并勾选确认')
    return
  }
  adoptSubmitting.value = true
  try {
    const result = await adoptManagedOnboarding({
      merchantId: adoptForm.merchantId as number | string,
      providerHuifuId: adoptForm.providerHuifuId.trim(),
      providerApplyNo: adoptForm.providerApplyNo.trim(),
      confirmed: true,
    })
    adoptVisible.value = false
    ElMessage.success('纳管成功,已触发一次进度刷新')
    await load()
    // 纳管产出的就是一条进件记录,直接进详情看分阶段进度与待办理项。
    if (result?.id) router.push({ name: 'merchant-onboarding-detail', params: { id: String(result.id) } })
  } catch (error: any) {
    ElMessage.error(error?.message || '纳管失败,请稍后重试')
  } finally {
    adoptSubmitting.value = false
  }
}
watch(() => route.query.kind, () => {
  const next = routeKind()
  // changeKind 自己写回地址栏也会走到这里,那一次已经查过了,别重复请求。
@@ -265,6 +376,18 @@
          <el-button :icon="Refresh" @click="reset">重置</el-button>
        </el-form-item>
      </el-form>
      <!-- 纳管入口只放在「商户进件」页签:纳管产出的就是一条进件记录,这个页签正是进件记录的列表,
           纳管结果当场可见。位置沿用「商家查询」页「新建」按钮的既有习惯。 -->
      <div v-if="kind === 'onboardings'" class="settlement-table-tools">
        <el-button
          v-if="hasAdminPermission('admin.merchant-settlement.edit')"
          type="primary"
          :icon="Plus"
          @click="openAdopt"
        >
          纳管外部进件商户
        </el-button>
      </div>
    </section>
    <section class="table-panel">
@@ -289,6 +412,12 @@
          <el-table-column prop="merchantType" label="商家类型" min-width="110" />
          <el-table-column prop="subjectName" label="进件主体" min-width="150" show-overflow-tooltip />
          <el-table-column label="主体类型" min-width="105"><template #default="{ row }">{{ subjectTypeLabel(row.subjectType) }}</template></el-table-column>
          <!-- 来源标记:区分两条进件路径,纳管进来的要一眼看出来。 -->
          <el-table-column label="来源" min-width="100" align="center">
            <template #default="{ row }">
              <el-tag :type="row.source === 'managed' ? 'success' : 'info'" effect="plain">{{ row.source === 'managed' ? '纳管' : '自助进件' }}</el-tag>
            </template>
          </el-table-column>
        </template>
        <template v-else>
          <el-table-column prop="id" label="申请id" min-width="100" />
@@ -336,6 +465,125 @@
        />
      </div>
    </section>
    <el-dialog
      v-model="adoptVisible"
      title="纳管外部进件商户"
      width="640px"
      :close-on-click-modal="false"
      destroy-on-close
    >
      <p class="adopt-hint">
        用于把<b>已在汇付侧完成进件</b>的商户接到我方某个商家上。纳管全程只查询、不下发:不会补开微信、
        不会绑定人才端、不会改动支付场景与延迟入账。
      </p>
      <el-form label-width="120px" class="adopt-form">
        <el-form-item label="我方商家" required>
          <el-select
            v-model="adoptForm.merchantId"
            filterable
            remote
            reserve-keyword
            clearable
            placeholder="输入商家名称搜索"
            :remote-method="searchMerchants"
            :loading="merchantLoading"
            @change="invalidateAdoptPreview"
          >
            <el-option
              v-for="item in merchantOptions"
              :key="item.id"
              :label="item.merchantName"
              :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="汇付商户号" required>
          <el-input v-model="adoptForm.providerHuifuId" clearable placeholder="斗拱侧该商户的 huifu_id" @input="invalidateAdoptPreview" />
        </el-form-item>
        <el-form-item label="入驻申请单号" required>
          <el-input v-model="adoptForm.providerApplyNo" clearable placeholder="汇付控台入驻申请列表里的申请单号" />
        </el-form-item>
        <el-form-item label=" ">
          <el-button type="primary" plain :loading="adoptPreviewing" @click="runAdoptPreview">核验通道商户</el-button>
          <span class="adopt-tip">卡号不用填,从通道查回来。</span>
        </el-form-item>
      </el-form>
      <template v-if="adoptPreview">
        <el-descriptions :column="1" border size="small" class="adopt-preview">
          <el-descriptions-item label="我方商家">
            {{ adoptPreview.merchantName }}<span class="muted">(门店:{{ adoptPreview.storeName }})</span>
          </el-descriptions-item>
          <el-descriptions-item label="通道商户注册名称">{{ adoptPreview.channelMerchantName }}</el-descriptions-item>
          <el-descriptions-item label="通道商户简称">{{ adoptPreview.channelShortName }}</el-descriptions-item>
          <el-descriptions-item label="通道主体类型">
            {{ adoptPreview.channelSubjectType === 'enterprise' ? '企业' : '个人' }}
            <span v-if="adoptPreview.channelCompanyType" class="muted">({{ adoptPreview.channelCompanyType }})</span>
          </el-descriptions-item>
          <el-descriptions-item label="结算银行卡">
            {{ adoptPreview.settlementCardMasked || '-' }}
            <span v-if="adoptPreview.settlementBankName" class="muted">({{ adoptPreview.settlementBankName }})</span>
          </el-descriptions-item>
        </el-descriptions>
        <!-- 只提示不拦截:连锁各门店共用一个收款主体是允许的,这里列出已绑的商家让运营自己确认。 -->
        <el-alert
          v-if="adoptPreview.alreadyManaged"
          class="adopt-alert"
          type="success"
          :closable="false"
          title="该商家已纳管过这个汇付商户号"
          description="重复纳管是幂等的,提交会直接返回原来那条进件记录,不会产生第二条。"
        />
        <el-alert
          v-if="adoptPreview.boundMerchants?.length"
          class="adopt-alert"
          type="info"
          :closable="false"
          :title="`该汇付商户号已绑给:${adoptPreview.boundMerchants.join('、')}`"
          description="可以继续纳管(连锁各门店共用一个收款主体)。"
        />
        <el-alert
          v-if="adoptPreview.nameWarning"
          class="adopt-alert"
          type="warning"
          :closable="false"
          title="通道名称与所选商家名完全没有重合字符"
          :description="adoptPreview.nameWarning"
        />
        <el-alert
          v-if="adoptPreview.loginNotice"
          class="adopt-alert"
          type="warning"
          :closable="false"
          title="商户端登录"
          :description="adoptPreview.loginNotice"
        />
        <el-alert
          v-for="blocker in adoptPreview.blockers || []"
          :key="blocker"
          class="adopt-alert"
          type="error"
          :closable="false"
          :title="blocker"
        />
        <el-checkbox v-model="adoptConfirmed" :disabled="(adoptPreview.blockers || []).length > 0">
          我已把通道侧的商户注册名称与简称与所选商家核对一致(纳管后该商家的收款将进入这个收款主体)
        </el-checkbox>
      </template>
      <template #footer>
        <el-button @click="adoptVisible = false">取消</el-button>
        <el-button
          type="primary"
          :loading="adoptSubmitting"
          :disabled="!adoptPreview || !adoptConfirmed || (adoptPreview.blockers || []).length > 0"
          @click="submitAdopt"
        >
          提交纳管
        </el-button>
      </template>
    </el-dialog>
  </div>
</template>
@@ -352,6 +600,12 @@
.query-form :deep(.el-input), .query-form :deep(.el-select), .query-form :deep(.el-date-editor) { width: 100%; }
.query-actions { justify-content: flex-end; }
.query-actions :deep(.el-form-item__content) { justify-content: flex-end; }
.settlement-table-tools { margin: 0 0 12px; }
.adopt-hint { margin: 0 0 16px; color: #606266; font-size: 13px; line-height: 20px; }
.adopt-form :deep(.el-select) { width: 100%; }
.adopt-tip { margin-left: 12px; color: #909399; font-size: 12px; }
.adopt-preview { margin-bottom: 14px; }
.adopt-alert { margin-bottom: 12px; }
.table-panel { margin-top: 16px; padding: 0 20px 12px; }
.settlement-table { width: 100%; }
.settlement-table :deep(th.el-table__cell) { color: #303133; font-weight: 600; background: #f7f8fa; }