1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
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 kind = ref<Kind>('settlements')
const router = useRouter()
const route = useRoute()
const loading = ref(false)
const rows = ref<Record<string, any>[]>([])
const total = ref(0)
/** 结算记录逐商户查询,个别商户查不到时在这里提示,不影响整页。 */
const failures = ref<string[]>([])
const page = ref(1)
const pageSize = ref(20)
 
const filters = reactive({
  merchantName: '',
  contactMobile: '',
  merchantType: '',
  status: '',
  dateRange: [] as string[],
})
 
const tabOptions: Array<{ value: Kind; label: string }> = [
  { value: 'settlements', label: '结算记录' },
  { value: 'onboardings', label: '商户进件' },
  { value: 'bank-cards', label: '银行卡申请' },
]
 
// 商家类型选项来自后端目录。原先这里手写了一份 8 项清单,标签是目录里根本不用的别名
// (住宿服务/购房服务/体检服务),后来又新增的类型也没跟上,才会出现下拉里有的筛不出数据、
// 数据里有的选不到。现在只从接口取,取不到就留空。
const merchantTypes = ref<string[]>([])
// 结算记录没有本地审核状态:它的「状态」是通道流水的结算状态(S/F/P),与表格那一列同一套取值。
const statusOptions = computed(() => kind.value === 'settlements'
  ? [
      { value: 'S', label: '已结算' },
      { value: 'F', label: '结算失败' },
      { value: 'P', label: '处理中' },
    ]
  : [
      { value: 'pending', label: '待审核' },
      { value: 'approved', label: '已通过' },
      { value: 'rejected', label: '已驳回' },
    ])
 
function statusLabel(value: string) {
  return ({ pending: '待审核', approved: '已通过', rejected: '已驳回', processing: '处理中', paid: '已到账', failed: '提现失败' } as Record<string, string>)[value] || value || '-'
}
 
function statusType(value: string) {
  return ({ pending: 'warning', approved: 'success', paid: 'success', rejected: 'danger', failed: 'danger', processing: 'primary' } as Record<string, 'warning' | 'success' | 'danger' | 'primary'>)[value] || 'primary'
}
 
/**
 * 商家名称那一格的悬停内容:几家共用同一个汇付商户号时,把共用的商家都列出来。
 * 格子里只能写「共用收款主体(N 家)」——一条流水是这几家一起的,名字全塞进去放不下(7 家就有 50 多个字符)。
 */
function settlementMerchantTip(row: Record<string, any>) {
  const names: string[] = Array.isArray(row.merchantNames) ? row.merchantNames : []
  return names.length > 1 ? names.join('、') : String(row.merchantName || '')
}
 
/** 斗拱结算流水的交易状态:S 成功、F 失败、P 处理中。 */function settleStatusLabel(value: unknown) {
  return ({ S: '已结算', F: '结算失败', P: '处理中' } as Record<string, string>)[String(value || '')] || String(value || '-')
}
 
function settleStatusType(value: unknown) {
  const normalized = String(value || '')
  return normalized === 'S' ? 'success' : normalized === 'F' ? 'danger' : 'warning'
}
 
/** 结算日期是 yyyyMMdd 字符串,展示成 2026-09-14。 */
function formatSettleDate(value: unknown) {
  const text = String(value || '')
  return /^\d{8}$/.test(text) ? `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}` : text || '-'
}
 
function subjectTypeLabel(value: unknown) {
  const normalized = String(value || '').toLowerCase()
  if (normalized === 'enterprise' || normalized === 'company' || normalized === '企业') return '企业'
  return normalized === 'individual' || normalized === 'person' || normalized === '个人' ? '个人' : String(value || '-')
}
 
function formatAmount(value: unknown) {
  if (value === null || value === undefined || value === '') return '-'
  return `¥${(Number(value) / 100).toFixed(2)}`
}
 
function formatDate(value: unknown) {
  if (!value) return '-'
  return String(value).replace('T', ' ').replace(/\.\d+$/, '')
}
 
/**
 * 每次查询的序号。切 tab、改条件、翻页都会自增,只有最新那一次的结果允许写回表格。
 *
 * 结算记录要逐商户去问斗拱,是最慢的一次请求;先切到别的 tab 的话,它的响应会最后落地。
 * 少了这道闸,它就会把已经切过去的那个 tab 的表格覆盖成结算流水——表头是进件/银行卡的列,
 * 内容却是结算记录(连带分页总数也变成结算的条数)。
 */
let latestRequest = 0
 
async function load() {
  const request = ++latestRequest
  loading.value = true
  try {
    if (kind.value === 'settlements') {
      // 结算记录来自斗拱,逐商户查询后合并;条数上限 50 是通道的硬限制,传更大的值通道会判
      // 基础参数校验失败、整页查不出流水。商家、联系电话、商家类型送后端去筛商户(顺带少查几家),
      // 状态是通道流水的结算状态,只能由后端在拿到流水后筛。
      const result = await getMerchantSettlements({
        beginDate: normalizeDate(filters.dateRange?.[0]),
        endDate: normalizeDate(filters.dateRange?.[1]),
        size: 50,
        merchantName: filters.merchantName.trim() || undefined,
        contactMobile: filters.contactMobile.trim() || undefined,
        merchantType: filters.merchantType || undefined,
        status: filters.status || undefined,
      })
      if (request !== latestRequest) return
      rows.value = result.records || []
      total.value = Number(result.total || 0)
      failures.value = result.failures || []
      return
    }
    const result = await getMerchantSettlement(kind.value, {
      page: page.value,
      size: pageSize.value,
      merchantName: filters.merchantName.trim() || undefined,
      contactMobile: filters.contactMobile.trim() || undefined,
      merchantType: filters.merchantType || undefined,
      status: filters.status || undefined,
      // 日期选择器点「清空」时给的是 null(不是 []),这里必须用可选链,否则查询会因为
      // 读 null[0] 抛异常而静默失败,页面停在旧数据上,看起来像查询没生效。
      startDate: filters.dateRange?.[0],
      endDate: filters.dateRange?.[1],
    })
    if (request !== latestRequest) return
    rows.value = result.records || []
    total.value = Number(result.total || 0)
    failures.value = []
  } finally {
    // 转圈只归最新那次请求管:过期的响应早早关掉它,会让仍在加载的那一页看起来已经加载完了。
    if (request === latestRequest) loading.value = false
  }
}
 
/** 日期选择器给的是 yyyy-MM-dd,斗拱要 yyyyMMdd。 */
function normalizeDate(value: string | undefined) {
  return value ? value.replace(/-/g, '') : undefined
}
 
/** 商家类型筛选项取自后端目录;取不到就留空,不用一份可能过期的本地清单兜底。 */
async function loadMerchantTypes() {
  try {
    merchantTypes.value = (await getMerchantSettlementMerchantTypes()) || []
  } catch {
    merchantTypes.value = []
  }
}
 
function search() {
  page.value = 1
  load()
}
 
function reset() {
  filters.merchantName = ''
  filters.contactMobile = ''
  filters.merchantType = ''
  filters.status = ''
  filters.dateRange = []
  page.value = 1
  load()
}
 
function changeKind(value: string | number | boolean | undefined) {
  kind.value = value as Kind
  page.value = 1
  filters.status = ''
  // 页签要写回地址栏。不写的话,刷新、浏览器返回、把链接发给别人,落地的都是地址栏里那个
  // 旧页签——从进件详情按返回会掉回银行卡申请就是这么来的。用 replace 而不是 push,
  // 切页签不该在历史里留一串记录。
  router.replace({ query: { ...route.query, kind: kind.value } })
  load()
}
 
/** 地址栏里的页签;识别不了时返回 null,保留当前选择。 */
function routeKind(): Kind | null {
  const value = String(route.query.kind || '')
  if (value === 'onboardings' || value === 'bank-cards' || value === 'settlements') return value
  // 旧链接(提现管理)现在指向结算记录。
  if (value === 'withdrawals') return 'settlements'
  return null
}
 
function applyRouteKind() {
  const next = routeKind()
  if (next) kind.value = next
}
 
function openReview(row: Record<string, any>) {
  if (kind.value === 'onboardings') {
    router.push({ name: 'merchant-onboarding-detail', params: { id: row.id } })
  }
}
 
/**
 * 纳管外部进件商户。
 *
 * 流程刻意分两步:先「核验通道商户」做一次只读查询,把通道侧的商户注册名称、商户简称与主体类型
 * 展示出来,运营与所选商家核对后**显式勾选确认**才能提交。不做「名称是否对得上」的程序判定——
 * 我们这边只有一个商家名(常是简称或服务点名),通道那边是工商全称,形态不对应,规则化匹配必然误判。
 */
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(),
      providerApplyNo: adoptForm.providerApplyNo.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 自己写回地址栏也会走到这里,那一次已经查过了,别重复请求。
  if (!next || next === kind.value) return
  kind.value = next
  page.value = 1
  filters.status = ''
  load()
})
 
onMounted(() => {
  applyRouteKind()
  load()
  loadMerchantTypes()
})
</script>
 
<template>
  <div class="withdrawal-page">
    <div class="settlement-tabs">
      <el-radio-group v-model="kind" @change="changeKind">
        <el-radio-button v-for="tab in tabOptions" :key="tab.value" :label="tab.value">
          {{ tab.label }}
        </el-radio-button>
      </el-radio-group>
    </div>
 
    <section class="query-panel">
      <div class="panel-title">查询表格</div>
      <el-form class="query-form" label-width="70px" @submit.prevent="search">
        <el-form-item label="商家">
          <el-input v-model="filters.merchantName" clearable placeholder="请输入商家名称" />
        </el-form-item>
        <el-form-item label="联系电话">
          <el-input v-model="filters.contactMobile" clearable placeholder="请输入商家联系电话" />
        </el-form-item>
        <!-- 同一个日期区间在两个 tab 里筛的是不同东西:进件/银行卡按申请时间,结算记录按结算日期。 -->
        <el-form-item :label="kind === 'settlements' ? '结算日期' : '申请时间'">
          <el-date-picker
            v-model="filters.dateRange"
            type="daterange"
            value-format="YYYY-MM-DD"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            unlink-panels
          />
        </el-form-item>
        <el-form-item label="商家类型">
          <el-select v-model="filters.merchantType" clearable placeholder="全部">
            <el-option v-for="item in merchantTypes" :key="item" :label="item" :value="item" />
          </el-select>
        </el-form-item>
        <el-form-item label="状态">
          <el-select v-model="filters.status" clearable placeholder="全部">
            <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
          </el-select>
        </el-form-item>
        <el-form-item class="query-actions">
          <el-button type="primary" :icon="Search" @click="search">查询</el-button>
          <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 v-loading="loading" :data="rows" border class="settlement-table" empty-text="暂无数据">
        <template v-if="kind === 'settlements'">
          <el-table-column prop="providerTransactionNo" label="结算流水号" min-width="190" show-overflow-tooltip />
          <!-- 几家商家共用一个收款主体时,格子写成「共用收款主体(N 家)」——一条流水是几家一起的,
               把名字全塞进这一格放不下(7 家就 50 多个字符),所以名单放到悬停提示里。 -->
          <el-table-column label="商家名称" min-width="180">
            <template #default="{ row }">
              <el-tooltip placement="top" :content="settlementMerchantTip(row)">
                <span class="cell-ellipsis">{{ row.merchantName }}</span>
              </el-tooltip>
            </template>
          </el-table-column>
          <el-table-column label="结算日期" min-width="118"><template #default="{ row }">{{ formatSettleDate(row.settleDate) }}</template></el-table-column>
          <el-table-column label="结算金额" min-width="110" align="right"><template #default="{ row }">{{ formatAmount(row.settleAmountCents) }}</template></el-table-column>
          <el-table-column label="手续费" min-width="96" align="right"><template #default="{ row }">{{ formatAmount(row.feeAmountCents) }}</template></el-table-column>
          <el-table-column prop="settleCycle" label="结算周期" min-width="96" />
          <el-table-column label="状态" min-width="96">
            <template #default="{ row }"><el-tag :type="settleStatusType(row.status)">{{ settleStatusLabel(row.status) }}</el-tag></template>
          </el-table-column>
          <el-table-column prop="cardMasked" label="到账卡号" min-width="160" />
          <el-table-column prop="cardHolder" label="持卡人" min-width="180" show-overflow-tooltip />
        </template>
        <template v-else-if="kind === 'onboardings'">
          <el-table-column prop="applicationNo" label="进件id" min-width="156" />
          <el-table-column prop="merchantName" label="商家名称" min-width="150" show-overflow-tooltip />
          <el-table-column prop="contactMobile" label="联系方式" min-width="118" />
          <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" />
          <el-table-column prop="merchantName" label="商家名称" min-width="150" show-overflow-tooltip />
          <el-table-column prop="contactMobile" label="联系方式" min-width="118" />
          <el-table-column prop="merchantType" label="商家类型" min-width="110" />
          <el-table-column prop="bankName" label="开户银行" min-width="145" show-overflow-tooltip />
          <el-table-column prop="branchName" label="开户支行" min-width="180" show-overflow-tooltip />
          <el-table-column prop="cityName" label="开户城市" min-width="120" show-overflow-tooltip />
          <!-- 卡号只展示后端生成的脱敏值:明文 PAN 不再从接口下发。 -->
          <el-table-column prop="cardMasked" label="银行卡号" min-width="180" />
        </template>
        <template v-if="kind !== 'settlements'">
          <el-table-column label="申请时间" min-width="165">
            <template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
          </el-table-column>
          <el-table-column label="状态" min-width="100" align="center">
            <template #default="{ row }">
              <el-tag :type="statusType(row.status)" effect="light">{{ statusLabel(row.status) }}</el-tag>
            </template>
          </el-table-column>
        </template>
        <el-table-column v-if="kind === 'onboardings'" label="操作" width="110" fixed="right" align="center">
          <template #default="{ row }">
            <el-button link type="primary" @click="openReview(row)">查看</el-button>
          </template>
        </el-table-column>
      </el-table>
      <el-alert
        v-if="kind === 'settlements' && failures.length > 0"
        class="settlement-failures"
        type="warning"
        :closable="false"
        :title="`部分商户结算记录查询失败:${failures.join(';')}`"
      />
      <div v-if="kind !== 'settlements'" class="pagination-bar">
        <el-pagination
          v-model:current-page="page"
          v-model:page-size="pageSize"
          background
          layout="total, sizes, prev, pager, next"
          :page-sizes="[10, 20, 50]"
          :total="total"
          @change="load"
        />
      </div>
    </section>
 
    <el-dialog
      v-model="adoptVisible"
      title="纳管外部进件商户"
      width="640px"
      :close-on-click-modal="false"
      destroy-on-close
    >
      <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="汇付控台入驻申请列表里的申请单号" @input="invalidateAdoptPreview" />
        </el-form-item>
        <el-form-item label=" ">
          <el-button type="primary" plain :loading="adoptPreviewing" @click="runAdoptPreview">核验通道商户</el-button>
        </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 v-if="adoptPreview.storeName && adoptPreview.storeName !== adoptPreview.merchantName" 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="该商家已纳管过这个汇付商户号,提交会返回原进件记录,不会新建"
        />
        <el-alert
          v-if="adoptPreview.boundMerchants?.length"
          class="adopt-alert"
          type="info"
          :closable="false"
          :title="`该汇付商户号已绑给:${adoptPreview.boundMerchants.join('、')},可继续纳管`"
        />
        <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="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>
 
<style scoped>
.withdrawal-page { min-width: 1040px; }
.settlement-failures { margin-top: 12px; }.settlement-tabs { margin: 0 0 16px; }
.settlement-tabs :deep(.el-radio-button__inner) { min-width: 96px; }
.query-panel, .table-panel { background: #fff; border: 1px solid #edf0f5; }
.query-panel { padding: 18px 20px 6px; }
.panel-title { margin-bottom: 16px; color: #1d2129; font-size: 15px; font-weight: 600; }
.query-form { display: grid; grid-template-columns: repeat(3, minmax(250px, 1fr)); column-gap: 24px; }
.query-form :deep(.el-form-item) { margin-bottom: 16px; }
.query-form :deep(.el-form-item__content) { min-width: 0; }
.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-form :deep(.el-select) { width: 100%; }
.adopt-preview { margin-bottom: 14px; }
.adopt-alert { margin-bottom: 12px; }
.table-panel { margin-top: 16px; padding: 0 20px 12px; }
.settlement-table { width: 100%; }
/* 自绘 tooltip 的那一格自己截断:不再用列的 show-overflow-tooltip(否则会和自绘的提示打架)。 */
.cell-ellipsis { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.settlement-table :deep(th.el-table__cell) { color: #303133; font-weight: 600; background: #f7f8fa; }
.settlement-table :deep(.el-table__cell) { padding: 10px 0; }
.pagination-bar { display: flex; align-items: center; justify-content: flex-end; gap: 18px; padding-top: 14px; color: #606266; font-size: 13px; }
.muted { color: #909399; }
.review-summary { display: grid; gap: 14px; }
.review-summary div { display: flex; justify-content: space-between; gap: 16px; }
.review-summary span { color: #909399; }
.review-summary strong { color: #303133; font-weight: 500; text-align: right; }
@media (max-width: 1280px) { .query-form { grid-template-columns: repeat(2, minmax(280px, 1fr)); } }
</style>