<script setup lang="ts">
|
import { onMounted, ref } from 'vue'
|
import { useRoute } from 'vue-router'
|
import { getMerchantOnboardingDetail, refreshMerchantOnboarding, refreshMerchantPaymentCapability, submitMerchantRealname } from '../api/admin'
|
import { hasAdminPermission } from '../utils/permissions'
|
import { ElMessage } from 'element-plus'
|
|
const route = useRoute()
|
const loading = ref(false)
|
const detail = ref<Record<string, any>>()
|
const syncing = ref(false)
|
const stageLabel = (value: string) => ({ confirmed: '已确认', pending: '处理中', action_required: '待办理', missing: '待补齐', failed: '失败', unknown: '未确认' }[value] || value)
|
async function refreshProgress(retry = false) {
|
if (syncing.value) return
|
syncing.value = true
|
try {
|
const progress = await refreshMerchantOnboarding(String(route.params.id), retry)
|
if (detail.value) detail.value.progress = progress
|
} finally { syncing.value = false }
|
}
|
|
function realnameStage() {
|
const stages = detail.value?.progress?.stages
|
return Array.isArray(stages) ? stages.find((stage: Record<string, any>) => stage.key === 'realname') : undefined
|
}
|
|
// 查询通道状态:重新读一次斗拱侧的收款能力快照(含结算卡绑定状态)。进件通过后**新增**的结算卡
|
// 只有绑定回调能推进,回调一丢就永远停在处理中——这条查询路径就是它的对等物。
|
async function refreshChannelStatus() {
|
if (syncing.value) return
|
syncing.value = true
|
try {
|
await refreshMerchantPaymentCapability(String(route.params.id))
|
ElMessage.success('已按通道最新状态刷新')
|
await load()
|
} catch (error: any) {
|
ElMessage.error(error?.message || '查询通道状态失败,请稍后重试')
|
} finally { syncing.value = false }
|
}
|
// 只有「还没发起」或「已被通道驳回」才给发起入口:处理中、待本人办理、已确认都不能重复发起,
|
// 未决申请重复提交会给同一商户报出两份实名申请。
|
function realnameActionable() {
|
const status = realnameStage()?.status
|
return status === 'missing' || status === 'failed'
|
}
|
async function startRealname() {
|
if (syncing.value) return
|
syncing.value = true
|
try {
|
const progress = await submitMerchantRealname(String(route.params.id), realnameStage()?.status === 'failed')
|
if (detail.value) detail.value.progress = progress
|
ElMessage.success('实名认证已提交,请按提示完成办理')
|
} catch (error: any) {
|
ElMessage.error(error?.message || '发起实名失败,请稍后重试')
|
} finally { syncing.value = false }
|
}
|
|
function text(value: unknown) {
|
return value === undefined || value === null || value === '' ? '-' : String(value)
|
}
|
|
function dateText(value: unknown) {
|
const result = text(value)
|
return /^\d{8}$/.test(result) ? `${result.slice(0, 4)}-${result.slice(4, 6)}-${result.slice(6, 8)}` : result
|
}
|
|
/** 后端给的是 ISO 串(2026-09-18T14:35:49),去掉 T 和毫秒,和列表页的写法保持一致。 */
|
function dateTimeText(value: unknown) {
|
if (value === undefined || value === null || value === '') return '-'
|
return String(value).replace('T', ' ').replace(/\.\d+$/, '')
|
}
|
|
function statusLabel(value: unknown) {
|
return ({ pending: '待审核', approved: '已通过', rejected: '已驳回' } as Record<string, string>)[String(value || '')] || text(value)
|
}
|
|
function subjectTypeLabel(value: unknown) {
|
const normalized = String(value || '').toLowerCase()
|
if (normalized === 'enterprise' || normalized === 'company' || normalized === '企业') return '企业'
|
return normalized === 'individual' || normalized === 'person' || normalized === '个人' ? '个人' : text(value)
|
}
|
|
function isIndividual(value: unknown) {
|
const normalized = String(value || '').toLowerCase()
|
return normalized === 'individual' || normalized === 'person' || normalized === '个人'
|
}
|
|
function validityTypeLabel(value: unknown) {
|
const normalized = String(value || '').toLowerCase()
|
if (normalized === 'long_term' || normalized === 'long-term' || normalized === '长期' || normalized === '1') return '长期'
|
if (normalized === 'fixed' || normalized === '非长期' || normalized === '0') return '非长期'
|
return text(value)
|
}
|
|
async function load() {
|
loading.value = true
|
try {
|
detail.value = await getMerchantOnboardingDetail(String(route.params.id))
|
} finally {
|
loading.value = false
|
}
|
}
|
|
onMounted(load)
|
</script>
|
|
<template>
|
<div v-loading="loading" class="onboarding-detail-page">
|
<section v-if="detail" class="detail-panel">
|
<h3>审核状态</h3>
|
<!-- 审核状态原来在 progress 存在时会被「开通尚未完成」顶掉,运营看不到这条进件到底审没审。
|
两者是不同的事:审核是这条申请的结论,开通是通道能力推进到哪一步。 -->
|
<div class="status-value">{{ statusLabel(detail.status) }}</div>
|
<el-alert
|
v-if="detail.rejectReason"
|
class="reject-reason"
|
type="error"
|
:closable="false"
|
:title="`驳回原因:${detail.rejectReason}`"
|
/>
|
|
<h3 class="onboarding-heading">进件信息</h3>
|
<div class="detail-grid">
|
<div class="detail-field"><span>进件编号</span><strong>{{ text(detail.applicationNo) }}</strong></div>
|
<div class="detail-field"><span>申请时间</span><strong>{{ dateTimeText(detail.createdAt) }}</strong></div>
|
<div class="detail-field"><span>商户编号</span><strong>{{ text(detail.merchantId) }}</strong></div>
|
<div class="detail-field"><span>所属门店</span><strong>{{ text(detail.storeName) }}</strong></div>
|
<div class="detail-field"><span>通道商户号</span><strong>{{ text(detail.providerHuifuId) }}</strong></div>
|
<div class="detail-field"><span>通道申请单号</span><strong>{{ text(detail.providerApplyNo || detail.providerRequestNo) }}</strong></div>
|
<div class="detail-field"><span>审核时间</span><strong>{{ dateTimeText(detail.reviewedAt) }}</strong></div>
|
<div class="detail-field"><span>最后更新时间</span><strong>{{ dateTimeText(detail.updatedAt) }}</strong></div>
|
</div>
|
|
<template v-if="detail.progress">
|
<h3 class="onboarding-heading">开通状态</h3>
|
<div class="status-value">{{ detail.progress.status === 'approved' ? '开通已完成' : '开通尚未完成' }}</div>
|
</template>
|
<section v-if="detail.progress" class="progress-panel">
|
<p>{{ detail.progress.reason }}</p>
|
<p>最后成功同步:{{ dateTimeText(detail.progress.lastSyncedAt) }}</p>
|
<el-alert v-if="detail.progress.syncError" :title="detail.progress.syncError" type="warning" :closable="false" />
|
<div v-for="stage in detail.progress.stages" :key="stage.key" class="progress-stage">
|
<strong>{{ stage.label }} · {{ stageLabel(stage.status) }}</strong>
|
<p>{{ stage.reason }}</p>
|
<img v-if="stage.qrCode" :src="stage.qrCode" alt="微信实名授权二维码" width="180" height="180" />
|
</div>
|
<p>刷新会查询通道进度,并继续办理已满足条件的缺失能力。待本人实名或授权时,请办理后再次刷新。</p>
|
<el-button v-if="hasAdminPermission('admin.merchant-settlement.edit')" type="primary" :loading="syncing" @click="refreshProgress()">刷新进度</el-button>
|
<el-button v-if="hasAdminPermission('admin.merchant-settlement.edit') && detail.progress.retryable" :disabled="syncing" @click="refreshProgress(true)">重试失败步骤</el-button>
|
<el-button v-if="hasAdminPermission('admin.merchant-settlement.edit')" :disabled="syncing" @click="refreshChannelStatus()">查询通道状态</el-button>
|
<el-button v-if="hasAdminPermission('admin.merchant-settlement.edit') && realnameActionable()" :disabled="syncing" @click="startRealname()">{{ realnameStage()?.status === 'failed' ? '重新发起微信实名' : '发起微信实名' }}</el-button>
|
</section>
|
|
<h3 class="onboarding-heading">商户进件</h3>
|
<div class="detail-grid common-grid">
|
<div class="detail-field"><span>商户类型</span><strong>{{ subjectTypeLabel(detail.subjectType) }}</strong></div>
|
</div>
|
<div v-if="!isIndividual(detail.subjectType)" class="detail-grid">
|
<div class="detail-field"><span>商户名称</span><strong>{{ text(detail.onboardingMerchantName) }}</strong></div>
|
<div class="detail-field"><span>商户简称</span><strong>{{ text(detail.merchantShortName) }}</strong></div>
|
<div class="detail-field"><span>公司类型</span><strong>{{ text(detail.companyType) }}</strong></div>
|
<div class="detail-field"><span>统一社会信用代码</span><strong>{{ text(detail.creditCode) }}</strong></div>
|
<div class="detail-field"><span>营业执照</span><strong><a v-if="detail.businessLicenseUrl" :href="detail.businessLicenseUrl" target="_blank">查看图片</a><template v-else>-</template></strong></div>
|
<div class="detail-field"><span>营业执照有效期类型</span><strong>{{ validityTypeLabel(detail.licenseValidityType) }}</strong></div>
|
<div class="detail-field"><span>证照有效期起始日期</span><strong>{{ dateText(detail.licenseBeginDate) }}</strong></div>
|
<div class="detail-field"><span>证照有效期结束日期</span><strong>{{ dateText(detail.licenseEndDate) }}</strong></div>
|
<div class="detail-field"><span>成立时间</span><strong>{{ dateText(detail.establishedDate) }}</strong></div>
|
<div class="detail-field"><span>注册地址(省、市、区)</span><strong>{{ text(detail.registeredRegion) }}</strong></div>
|
<div class="detail-field detail-field--auto-height"><span>详细注册地址</span><strong>{{ text(detail.registeredAddress) }}</strong></div>
|
<div class="detail-field"><span>注册资本</span><strong>{{ text(detail.registeredCapital) }}</strong></div>
|
<div class="detail-field"><span>法人姓名</span><strong>{{ text(detail.subjectName) }}</strong></div>
|
<div class="detail-field"><span>法人证件类型</span><strong>身份证</strong></div>
|
<div class="detail-field"><span>法人身份证人像面</span><strong><a v-if="detail.legalIdFrontUrl" :href="detail.legalIdFrontUrl" target="_blank">查看图片</a><template v-else>-</template></strong></div>
|
<div class="detail-field"><span>法人身份证国徽面</span><strong><a v-if="detail.legalIdBackUrl" :href="detail.legalIdBackUrl" target="_blank">查看图片</a><template v-else>-</template></strong></div>
|
<div class="detail-field"><span>法人证件号码</span><strong>{{ text(detail.subjectIdNo || detail.subjectIdNoMasked) }}</strong></div>
|
<div class="detail-field"><span>法人证件有效期类型</span><strong>{{ validityTypeLabel(detail.subjectCertValidityType) }}</strong></div>
|
<div class="detail-field"><span>法人证件有效期开始日期</span><strong>{{ dateText(detail.subjectCertBeginDate) }}</strong></div>
|
<div class="detail-field"><span>法人证件有效期截止日期</span><strong>{{ dateText(detail.subjectCertEndDate) }}</strong></div>
|
<div class="detail-field"><span>管理员姓名</span><strong>{{ text(detail.contactName) }}</strong></div>
|
<div class="detail-field"><span>管理员手机号</span><strong>{{ text(detail.contactMobile) }}</strong></div>
|
<div class="detail-field detail-field--auto-height"><span>法人证件地址</span><strong>{{ text(detail.legalCertificateAddress) }}</strong></div>
|
<div class="detail-field"><span>管理员邮箱</span><strong>{{ text(detail.administratorEmail) }}</strong></div>
|
</div>
|
<div v-else class="detail-grid">
|
<div class="detail-field"><span>个人姓名</span><strong>{{ text(detail.subjectName) }}</strong></div>
|
<div class="detail-field"><span>个人证件类型</span><strong>身份证</strong></div>
|
<div class="detail-field"><span>个人证件号码</span><strong>{{ text(detail.subjectIdNo || detail.subjectIdNoMasked) }}</strong></div>
|
<div class="detail-field"><span>个人证件有效期类型</span><strong>{{ validityTypeLabel(detail.subjectCertValidityType) }}</strong></div>
|
<div class="detail-field"><span>个人证件有效期开始日期</span><strong>{{ dateText(detail.subjectCertBeginDate) }}</strong></div>
|
<div class="detail-field"><span>个人证件有效期截止日期</span><strong>{{ dateText(detail.subjectCertEndDate) }}</strong></div>
|
<div class="detail-field"><span>手机号</span><strong>{{ text(detail.contactMobile) }}</strong></div>
|
<!-- 个人主体的 company_type 存的是进件时选的经营行业(企业主体那一列才是公司类型),
|
它同时决定上报汇付的 MCC,所以运营要能在这里核对。 -->
|
<div class="detail-field"><span>经营行业</span><strong>{{ text(detail.companyType) }}</strong></div>
|
<div class="detail-field"><span>经营地区(省、市、区)</span><strong>{{ text(detail.registeredRegion) }}</strong></div>
|
<div class="detail-field detail-field--auto-height"><span>个人证件地址</span><strong>{{ text(detail.legalCertificateAddress) }}</strong></div>
|
<div class="detail-field"><span>邮箱</span><strong>{{ text(detail.administratorEmail) }}</strong></div>
|
<div class="detail-field"><span>个人身份证人像面</span><strong><a v-if="detail.legalIdFrontUrl" :href="detail.legalIdFrontUrl" target="_blank">查看图片</a><template v-else>-</template></strong></div>
|
<div class="detail-field"><span>个人身份证国徽面</span><strong><a v-if="detail.legalIdBackUrl" :href="detail.legalIdBackUrl" target="_blank">查看图片</a><template v-else>-</template></strong></div>
|
</div>
|
|
<!-- 结算卡和门店照片对企业主体同样必要,原来只画在个人主体那一支:企业进件即使传了
|
也看不到,而微信业务开通与微信实名认证都要这张店铺照片。 -->
|
<h3 class="onboarding-heading">结算银行卡</h3>
|
<div v-if="detail.initialBankCardId" class="detail-grid">
|
<div class="detail-field"><span>卡号</span><strong>{{ text(detail.settlementCardMasked) }}</strong></div>
|
<div class="detail-field"><span>开户银行</span><strong>{{ text(detail.settlementBankName) }}</strong></div>
|
<div class="detail-field"><span>开户支行</span><strong>{{ text(detail.settlementBranchName) }}</strong></div>
|
<div class="detail-field"><span>开户城市</span><strong>{{ text(detail.settlementCityName) }}</strong></div>
|
<div class="detail-field"><span>绑定状态</span><strong>{{ statusLabel(detail.settlementCardStatus) }}</strong></div>
|
<!-- 结算凭证图一列两义:对私(含个体工商户)是银行卡卡号面,对公是开户许可证。
|
名字按主体切换,判定来自后端,前端不另抄一份规则。 -->
|
<div class="detail-field"><span>{{ detail.privateSettlement ? '结算银行卡卡号面' : '开户许可证' }}</span><strong><a v-if="detail.settlementCardFrontUrl" :href="detail.settlementCardFrontUrl" target="_blank">查看图片</a><template v-else>-</template></strong></div>
|
<div v-if="detail.settlementCardRejectReason" class="detail-field detail-field--auto-height"><span>绑定失败原因</span><strong>{{ detail.settlementCardRejectReason }}</strong></div>
|
</div>
|
<div v-else class="detail-grid">
|
<div class="detail-field"><span>结算银行卡</span><strong>未绑定</strong></div>
|
</div>
|
|
<h3 class="onboarding-heading">门店照片</h3>
|
<div class="detail-grid">
|
<div class="detail-field"><span>店铺照片</span><strong><a v-if="detail.storeHeaderUrl" :href="detail.storeHeaderUrl" target="_blank">查看图片</a><template v-else>-</template></strong></div>
|
<div class="detail-field"><span>店内环境照</span><strong><a v-if="detail.storeIndoorUrl" :href="detail.storeIndoorUrl" target="_blank">查看图片</a><template v-else>-</template></strong></div>
|
</div>
|
</section>
|
</div>
|
</template>
|
|
<style scoped>
|
.onboarding-detail-page { min-width: 1040px; }
|
.progress-panel { margin: 18px 0; padding: 16px; background: #f5f7fa; font-size: 14px; }
|
.progress-stage { padding: 12px 0; border-bottom: 1px solid #e2e6ec; }
|
.progress-stage p { margin: 6px 0; color: #606266; }
|
.detail-panel { min-height: 560px; padding: 18px 20px 54px; background: #fff; border: 1px solid #edf0f5; }
|
.detail-panel h3 { margin: 0; color: #1d2129; font-size: 15px; }
|
.status-value { margin-top: 16px; color: #1d2129; font-size: 13px; font-weight: 600; }
|
.reject-reason { margin-top: 12px; }
|
.onboarding-heading { margin-top: 24px !important; }
|
.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px 46px; margin-top: 22px; max-width: 1032px; }
|
.common-grid { margin-bottom: -2px; }
|
.detail-field { display: grid; grid-template-columns: 144px minmax(0, 1fr); align-items: center; gap: 12px; min-height: 32px; }
|
.detail-field > span { color: #303133; font-size: 13px; font-weight: 500; white-space: nowrap; }
|
.detail-field > strong { min-width: 0; min-height: 32px; box-sizing: border-box; padding: 7px 10px; color: #606266; font-size: 13px; font-weight: 400; line-height: 18px; word-break: break-all; background: #f5f7fa; }
|
.detail-field--auto-height { align-items: stretch; }
|
.detail-field--auto-height > strong { height: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
|
.placeholder-field { visibility: hidden; }
|
</style>
|