| | |
| | | <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' |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 纳管外部进件商户。 |
| | | * |
| | | * 流程刻意分两步:先「核验通道商户」做一次只读查询,把通道侧的商户注册名称、商户简称与主体类型 |
| | | * 展示出来,运营与所选商家核对后**显式勾选确认**才能提交。不做「名称是否对得上」的程序判定—— |
| | | * 我们这边只有一个商家名(常是简称或服务点名),通道那边是工商全称,形态不对应,规则化匹配必然误判。 |
| | | */ |
| | | 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 自己写回地址栏也会走到这里,那一次已经查过了,别重复请求。 |
| | |
| | | <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"> |
| | |
| | | <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" /> |
| | |
| | | /> |
| | | </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> |
| | | |
| | |
| | | .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; } |