新增个人贷款申请

main
halo 2026-08-10 17:32:03 +08:00
commit 361f30d549
3 changed files with 484 additions and 124 deletions

View File

@ -1,62 +1,113 @@
<!-- src/views/wallet/account/AccountDetail.vue -->
<!-- 2.6.4/2.6.5 账户主页:余额卡片 + 交易明细(时间范围查询+前端本地分页)+ 提现/保证金/回单/对账单入口
WalletAccountDetailVo 不含 accountName,页面标题与提交字段用的账户名来自路由 query( AccountList 传递) -->
WalletAccountDetailVo 不含 accountName,页面标题与提交字段用的账户名来自路由 query( AccountList 传递)
2026-08-10 起本页按最新参考原型美化,并去掉了"所属渠道"筛选条件:渠道编号完全依赖 AccountList
"查看"按钮通过路由 query 透传的 record.channelNo(见下方 channelNo 定义处注释),不再提供页面内
手动选择渠道的入口所以本页所有依赖 channelNo 的操作(账户主页加载/查询/提现/回单/对账单)
若发现 channelNo 缺失只能提示用户返回列表重新进入,无法在本页补选 -->
<template>
<div class="wallet-account-detail-page">
<a-page-header :title="`账户主页 - ${accountName}`" @back="goBack" />
<a-page-header class="account-page-header" :title="accountName" @back="goBack" />
<a-card style="margin-bottom: 16px">
<a-form layout="inline">
<a-form-item label="所属渠道" required>
<ChannelSelect v-model:model-value="channelNo" />
</a-form-item>
</a-form>
</a-card>
<a-alert
v-if="!channelNo"
type="warning"
show-icon
message="当前账户缺少渠道信息,账户主页与交易明细暂无法加载,请返回账户列表重新进入本页"
style="margin-bottom: 16px"
/>
<a-row :gutter="16" style="margin-bottom: 16px">
<a-col :span="6" v-if="detail.mainAccountNo">
<a-card title="主账户(A1)">
<p>账号:{{ detail.mainAccountNo }}</p>
<p>余额:{{ detail.mainBalance }}</p>
<a-button type="primary" size="small" :disabled="!channelNo" @click="openWithdraw"></a-button>
</a-card>
</a-col>
<a-col :span="6" v-if="detail.a2AccountNo">
<a-card title="电商账户(A2)">
<p>账号:{{ detail.a2AccountNo }}</p>
<p>余额:{{ detail.a2Balance }}</p>
</a-card>
</a-col>
<a-col :span="6" v-if="detail.a6AccountNo">
<a-card title="保证金账户(A6)">
<p>账号:{{ detail.a6AccountNo }}</p>
<p>余额:{{ detail.a6Balance }}</p>
<a-space>
<a-button size="small" @click="openMargin('pay')"></a-button>
<a-button size="small" @click="openMargin('release')"></a-button>
</a-space>
</a-card>
</a-col>
<a-col :span="6" v-if="detail.a7AccountNo">
<a-card title="分户账户(A7)">
<p>账号:{{ detail.a7AccountNo }}</p>
<p>余额:{{ detail.a7Balance }}</p>
</a-card>
</a-col>
</a-row>
<div class="account-cards">
<div v-if="detail.mainAccountNo" class="account-card">
<div class="account-card__head">
<a-tag color="blue" class="account-card__tag">主账户A1</a-tag>
<span class="account-card__no">{{ detail.mainAccountNo }}</span>
</div>
<div class="account-card__body">
<div class="account-card__balance-wrap">
<span class="account-card__balance-label">余额:</span>
<span class="account-card__balance">¥{{ detail.mainBalance }}</span>
</div>
<div class="account-card__actions">
<a-button type="link" size="small" :disabled="!channelNo" @click="openWithdraw"></a-button>
</div>
</div>
</div>
<a-card title="交易明细">
<template #extra>
<a-space>
<a-button size="small" @click="setQuickRange('today')"></a-button>
<a-button size="small" @click="setQuickRange('lastMonth')"></a-button>
<a-button size="small" @click="setQuickRange('last3Months')"></a-button>
<a-range-picker v-model:value="rangeValue" value-format="YYYY-MM-DD" @change="handleRangeChange" />
<div v-if="detail.a2AccountNo" class="account-card">
<div class="account-card__head">
<a-tag color="cyan" class="account-card__tag">电商账户A2</a-tag>
<span class="account-card__no">{{ detail.a2AccountNo }}</span>
</div>
<div class="account-card__body">
<div class="account-card__balance-wrap">
<span class="account-card__balance-label">余额:</span>
<span class="account-card__balance">¥{{ detail.a2Balance }}</span>
</div>
</div>
</div>
<div v-if="detail.a6AccountNo" class="account-card">
<div class="account-card__head">
<a-tag color="gold" class="account-card__tag">保证金账户A6</a-tag>
<span class="account-card__no">{{ detail.a6AccountNo }}</span>
</div>
<div class="account-card__body">
<div class="account-card__balance-wrap">
<span class="account-card__balance-label">余额:</span>
<span class="account-card__balance">¥{{ detail.a6Balance }}</span>
</div>
<div class="account-card__actions">
<a-button type="link" size="small" @click="openMargin('pay')"></a-button>
<a-button type="link" size="small" @click="openMargin('release')"></a-button>
</div>
</div>
</div>
<div v-if="detail.a7AccountNo" class="account-card">
<div class="account-card__head">
<a-tag color="purple" class="account-card__tag">分户账户A7</a-tag>
<span class="account-card__no">{{ detail.a7AccountNo }}</span>
</div>
<div class="account-card__body">
<div class="account-card__balance-wrap">
<span class="account-card__balance-label">余额:</span>
<span class="account-card__balance">¥{{ detail.a7Balance }}</span>
</div>
</div>
</div>
</div>
<div class="transaction-panel">
<div class="filter-bar">
<span class="filter-bar__label">交易时间:</span>
<a-range-picker v-model:value="rangeValue" value-format="YYYY-MM-DD" @change="onRangePickerChange" />
<div class="quick-range-group">
<a-button
v-for="opt in quickRangeOptions"
:key="opt.value"
size="small"
class="quick-range-btn"
:class="{ active: activeQuickRange === opt.value }"
@click="setQuickRange(opt.value)"
>
{{ opt.label }}
</a-button>
</div>
<a-button type="primary" :disabled="!channelNo" :loading="txLoading" @click="loadTransactionDetail"></a-button>
<a-button :disabled="!channelNo" @click="handleDownloadReceipt"></a-button>
<a-button :disabled="!channelNo" @click="statementModalOpen = true">对账单下载</a-button>
</a-space>
</template>
<a-button @click="handleReset"></a-button>
</div>
<div class="action-links">
<span class="link-item" @click="openStatsModal"><BarChartOutlined /> 统计视图</span>
<span class="link-item" :class="{ disabled: !channelNo }" @click="handleDownloadReceipt">
<DownloadOutlined /> 回单({{ selectedTxKeys.length }})
</span>
<span class="link-item" :class="{ disabled: !channelNo }" @click="openStatementModal">
<DownloadOutlined /> 对账单
</span>
<span class="link-item" @click="handleExportDetail"><FileExcelOutlined /> 明细表</span>
</div>
<a-table
:columns="txColumns"
@ -76,7 +127,7 @@
</template>
</template>
</a-table>
</a-card>
</div>
<WithdrawModal ref="withdrawModalRef" :channel-no="channelNo" @success="handleFundActionSuccess" />
<MarginModal ref="marginModalRef" @success="handleFundActionSuccess" />
@ -88,15 +139,25 @@
</a-form-item>
</a-form>
</a-modal>
<a-modal v-model:open="statsModalOpen" title="统计视图" :footer="null" width="480px">
<p class="stats-hint">基于当前已查询到的交易明细( {{ txStats.total }} )统计</p>
<a-descriptions :column="1" bordered size="small">
<a-descriptions-item label="收入笔数/金额">{{ txStats.incomeCount }} / ¥{{ txStats.incomeAmount }}</a-descriptions-item>
<a-descriptions-item label="支出笔数/金额">{{ txStats.expenseCount }} / ¥{{ txStats.expenseAmount }}</a-descriptions-item>
<a-descriptions-item label="净额(收入-支出)">¥{{ txStats.net }}</a-descriptions-item>
</a-descriptions>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { ref, reactive, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import dayjs from 'dayjs'
import ChannelSelect from '@/components/ChannelSelect.vue'
import * as XLSX from 'xlsx'
import { BarChartOutlined, DownloadOutlined, FileExcelOutlined } from '@ant-design/icons-vue'
import WithdrawModal from '@/components/WithdrawModal.vue'
import MarginModal from '@/components/MarginModal.vue'
import { fetchWalletAccountDetailApi, fetchTransactionDetailApi, downloadReceiptApi, downloadStatementApi } from '@/api/wallet'
@ -107,7 +168,10 @@ const route = useRoute()
const router = useRouter()
const accountName = route.query.accountName || ''
const channelNo = ref(undefined)
// 2026-08-10 ""(record.channelNo),
// ""/, query (
// ),channelNo undefined,,,
const channelNo = ref(route.query.channelNo || undefined)
const withdrawModalRef = ref(null)
const marginModalRef = ref(null)
@ -122,6 +186,15 @@ const detail = reactive({
a7Balance: ''
})
const quickRangeOptions = [
{ value: 'today', label: '今天' },
{ value: 'yesterday', label: '昨天' },
{ value: 'last7Days', label: '近七天' },
{ value: 'currentMonth', label: '当月' },
{ value: 'lastMonth', label: '上月' }
]
const activeQuickRange = ref('today')
const rangeValue = ref([dayjs().format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD')])
const startDate = ref(`${dayjs().format('YYYY-MM-DD')} 00:00:00`)
const endDate = ref(`${dayjs().format('YYYY-MM-DD')} 23:59:59`)
@ -134,6 +207,8 @@ const statementModalOpen = ref(false)
const statementDownloading = ref(false)
const statementRangeValue = ref([dayjs().startOf('month').format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD')])
const statsModalOpen = ref(false)
const txColumns = [
{ title: '序号', dataIndex: 'index', width: 60 },
{ title: '系统流水号', dataIndex: 'id' },
@ -152,8 +227,37 @@ const pagedTxList = computed(() => {
return txList.value.slice(start, start + txPagination.pageSize)
})
// : txList,()
const txStats = computed(() => {
let incomeCount = 0
let incomeAmount = 0
let expenseCount = 0
let expenseAmount = 0
txList.value.forEach((item) => {
const amount = parseFloat(String(item.transAmount).replace(/,/g, '')) || 0
if (amount < 0) {
expenseCount += 1
expenseAmount += Math.abs(amount)
} else {
incomeCount += 1
incomeAmount += amount
}
})
return {
total: txList.value.length,
incomeCount,
incomeAmount: incomeAmount.toFixed(2),
expenseCount,
expenseAmount: expenseAmount.toFixed(2),
net: (incomeAmount - expenseAmount).toFixed(2)
}
})
// 2026-08-10 /api/wallet-account/detail id (channelNo), channelNo
// ( query), onMounted , channelNo
async function loadDetail() {
const res = await fetchWalletAccountDetailApi({ id: route.query.id })
if (!channelNo.value) return
const res = await fetchWalletAccountDetailApi({ id: route.query.id, channelNo: channelNo.value })
if (res.data.code === 200) {
Object.assign(detail, res.data.data)
}
@ -168,7 +272,7 @@ function sortTxList(list) {
async function loadTransactionDetail() {
if (!channelNo.value || !detail.mainAccountNo) {
showErrorModal('请先选择所属渠道')
showErrorModal('当前账户缺少渠道信息,无法查询交易明细')
return
}
txLoading.value = true
@ -199,26 +303,49 @@ function handleRangeChange(values) {
}
}
// ,("////")
function onRangePickerChange(values) {
activeQuickRange.value = null
handleRangeChange(values)
}
function setQuickRange(type) {
const today = dayjs()
if (type === 'today') {
rangeValue.value = [today.format('YYYY-MM-DD'), today.format('YYYY-MM-DD')]
} else if (type === 'yesterday') {
const yesterday = today.subtract(1, 'day')
rangeValue.value = [yesterday.format('YYYY-MM-DD'), yesterday.format('YYYY-MM-DD')]
} else if (type === 'last7Days') {
rangeValue.value = [today.subtract(6, 'day').format('YYYY-MM-DD'), today.format('YYYY-MM-DD')]
} else if (type === 'currentMonth') {
rangeValue.value = [today.startOf('month').format('YYYY-MM-DD'), today.format('YYYY-MM-DD')]
} else if (type === 'lastMonth') {
const lastMonth = today.subtract(1, 'month')
rangeValue.value = [lastMonth.startOf('month').format('YYYY-MM-DD'), lastMonth.endOf('month').format('YYYY-MM-DD')]
} else if (type === 'last3Months') {
rangeValue.value = [today.subtract(3, 'month').format('YYYY-MM-DD'), today.format('YYYY-MM-DD')]
}
activeQuickRange.value = type
handleRangeChange(rangeValue.value)
loadTransactionDetail()
}
// :(""),,""
function handleReset() {
activeQuickRange.value = 'today'
const today = dayjs().format('YYYY-MM-DD')
rangeValue.value = [today, today]
startDate.value = `${today} 00:00:00`
endDate.value = `${today} 23:59:59`
selectedTxKeys.value = []
}
function handleTxTableChange(pag) {
txPagination.current = pag.current
txPagination.pageSize = pag.pageSize
}
async function handleDownloadReceipt() {
if (!channelNo.value) return
const targets = selectedTxKeys.value.length ? txList.value.filter((item) => selectedTxKeys.value.includes(item.id)) : txList.value
if (!targets.length) {
showErrorModal('当前查询范围内无可下载的流水')
@ -242,6 +369,11 @@ async function handleDownloadReceipt() {
}
}
function openStatementModal() {
if (!channelNo.value) return
statementModalOpen.value = true
}
async function handleDownloadStatement() {
if (!statementRangeValue.value || statementRangeValue.value.length !== 2) {
showErrorModal('请选择时间范围')
@ -267,6 +399,38 @@ async function handleDownloadStatement() {
}
}
function openStatsModal() {
if (!txList.value.length) {
showErrorModal('请先查询交易明细')
return
}
statsModalOpen.value = true
}
// : txList xlsx,()
function handleExportDetail() {
if (!txList.value.length) {
showErrorModal('请先查询交易明细')
return
}
const rows = txList.value.map((item, index) => ({
序号: index + 1,
系统流水号: item.id,
记账日期: item.accountDate,
交易类型: item.transType,
对方户名: item.oppAccountName,
对方账号: item.oppAccountNo,
交易金额: item.transAmount,
账户余额: item.balance,
交易时间: item.tradeTime,
摘要: item.remark
}))
const worksheet = XLSX.utils.json_to_sheet(rows)
const workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, worksheet, '交易明细')
XLSX.writeFile(workbook, `交易明细表_${accountName}_${startDate.value.slice(0, 10)}_${endDate.value.slice(0, 10)}.xlsx`)
}
function openWithdraw() {
withdrawModalRef.value.show({ accountNo: detail.mainAccountNo, accountName, maxAmount: detail.mainBalance })
}
@ -285,10 +449,141 @@ function goBack() {
router.push('/wallet/account')
}
onMounted(loadDetail)
// channelNo query,( change ),
// immediate:true detail
watch(
channelNo,
(val) => {
if (val) loadDetail()
},
{ immediate: true }
)
</script>
<style scoped>
.account-page-header {
padding: 0 0 16px;
}
.account-page-header :deep(.ant-page-header-heading-title) {
font-size: 22px;
font-weight: 600;
}
.account-cards {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 16px;
}
.account-card {
flex: 1 1 260px;
min-width: 260px;
padding: 16px 20px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
}
.account-card__head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.account-card__tag {
margin: 0;
}
.account-card__no {
font-weight: 600;
color: #1f2329;
}
.account-card__body {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
}
.account-card__balance-label {
color: #8c8c8c;
margin-right: 8px;
}
.account-card__balance {
font-size: 20px;
font-weight: 700;
color: #1f2329;
}
.account-card__actions {
display: flex;
gap: 4px;
}
.transaction-panel {
padding: 20px;
background: #fff;
border-radius: 8px;
}
.filter-bar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
}
.filter-bar__label {
color: #606266;
white-space: nowrap;
}
.quick-range-group {
display: flex;
gap: 8px;
}
.quick-range-btn.active {
color: #1677ff;
border-color: #1677ff;
background: #e6f4ff;
}
.action-links {
display: flex;
justify-content: flex-end;
gap: 24px;
margin-bottom: 12px;
}
.action-links .link-item {
display: inline-flex;
align-items: center;
gap: 4px;
color: #1677ff;
cursor: pointer;
}
.action-links .link-item:hover {
color: #4096ff;
}
.action-links .link-item.disabled {
color: #c0c4cc;
cursor: not-allowed;
}
.stats-hint {
color: #8c8c8c;
margin-bottom: 12px;
}
.amount-negative {
color: #ff4d4f;
}

View File

@ -1,8 +1,12 @@
<!-- src/views/wallet/account/AccountList.vue -->
<!-- "所属机构"筛选按机构数据隔离设计要求补充当前 WalletAccountVo/分页接口均不含
organizationId 字段(账户仅关联 customerId),后端需先建立"钱包账户 -> 客户 -> 所属机构"
的关联并在 /api/wallet-account/page 中支持该过滤参数,该筛选才能真正生效,已登记于
缺失的后端接口.md -->
<!-- 2026-08-10 按最新原型截图(website_detail 商业平台账户列表页)+ 后端接口变更改造筛选区:
后端 /api/wallet-account/page 查询条件已改为 channelNo(渠道)/organizationId(所属机构),
并删除了 accountType(账户类型)查询参数(账户类型仍作为列表结果字段展示,只是不再支持按它
筛选)"所属机构"沿用现有 OrgTreeSelect(内部已通过 /api/organization/list-all 换取机构ID
再送给后端,不是本次新增)"客户类型"是本次后端新增的独立查询参数 customerType,
"账户类型"(A1/A2/A3/A6/A7)是两个不同维度,取值为 PERSONAL(个人)/ENTERPRISE(企业)
(2026-08-10 后端已确认,与项目里"个人/企业"场景常见的 accountProperty('1'/'2')编码
不是同一套,不要混用),详见缺失的后端接口.md -->
<template>
<div class="wallet-account-list-page">
<ProTable
@ -11,20 +15,25 @@
:fetch-data="loadList"
row-selection="radio"
v-model:selected-row-keys="selectedRowKeys"
:initial-search="{ accountName: '', accountNo: '', accountType: undefined, organizationId: getCurrentOrganizationId() }"
:initial-search="{
accountName: '',
customerType: undefined,
channelNo: undefined,
organizationId: getCurrentOrganizationId()
}"
>
<template #search="{ form }">
<a-form-item label="账户名称">
<a-input v-model:value="form.accountName" placeholder="请输入账户名称" allow-clear style="width: 180px" />
<a-form-item label="户名称">
<a-input v-model:value="form.accountName" placeholder="请输入户名称" allow-clear style="width: 180px" />
</a-form-item>
<a-form-item label="账号">
<a-input v-model:value="form.accountNo" placeholder="请输入账号" allow-clear style="width: 180px" />
</a-form-item>
<a-form-item label="账户类型">
<a-select v-model:value="form.accountType" placeholder="请选择账户类型" allow-clear style="width: 160px">
<a-select-option v-for="opt in accountTypeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</a-select-option>
<a-form-item label="客户类型">
<a-select v-model:value="form.customerType" placeholder="请选择客户类型" allow-clear style="width: 160px">
<a-select-option v-for="opt in customerTypeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="渠道">
<ChannelSelect v-model:model-value="form.channelNo" placeholder="请选择渠道" style="width: 200px" />
</a-form-item>
<a-form-item label="所属机构">
<OrgTreeSelect v-model="form.organizationId" placeholder="请选择所属机构" allow-clear style="width: 200px" />
</a-form-item>
@ -56,21 +65,24 @@
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">{{ index + 1 }}</template>
<template v-else-if="column.dataIndex === 'accountType'">
<StatusTag :value="record.accountType" :map="accountTypeMap" />
<template v-else-if="column.dataIndex === 'customerType'">
<StatusTag :value="record.customerType" :map="customerTypeMap" />
</template>
<template v-else-if="column.dataIndex === 'accountRelation'">
<StatusTag :value="record.accountRelation" :map="accountRelationMap" />
<template v-else-if="column.dataIndex === 'mobilePhone'">
{{ maskMobile(record.mobilePhone) || '-' }}
</template>
<template v-else-if="column.dataIndex === 'certificateNumber'">
{{ maskIdCard(record.certificateNumber) || '-' }}
</template>
<template v-else-if="column.dataIndex === 'channelName'">
{{ record.channelName || '-' }}
</template>
<template v-else-if="column.dataIndex === 'organizationName'">
{{ record.organizationName || '-' }}
</template>
<template v-else-if="column.dataIndex === 'accountStatus'">
<StatusTag :value="record.accountStatus" :map="accountStatusMap" />
</template>
<template v-else-if="column.dataIndex === 'mainBalance'">
{{ record.accountRelation === '0' ? balanceMap[record.accountNo]?.mainBalance ?? '--' : '' }}
</template>
<template v-else-if="column.dataIndex === 'a6Balance'">
{{ record.accountRelation === '0' ? balanceMap[record.accountNo]?.a6Balance ?? '--' : '' }}
</template>
<template v-else-if="column.dataIndex === 'action'">
<a @click="goDetail(record)"></a>
</template>
@ -105,9 +117,9 @@ import ProTable from '@/components/ProTable.vue'
import StatusTag from '@/components/StatusTag.vue'
import BindCardModal from '@/components/BindCardModal.vue'
import OrgTreeSelect from '@/components/OrgTreeSelect.vue'
import ChannelSelect from '@/components/ChannelSelect.vue'
import {
fetchWalletAccountPageApi,
fetchWalletAccountDetailApi,
closeWalletAccountApi,
openWalletSubAccountApi,
updateWalletAccountApi,
@ -115,12 +127,12 @@ import {
syncWalletAccountBalanceApi
} from '@/api/wallet'
import { getCurrentOrganizationId } from '@/utils/orgScope'
import { maskMobile, maskIdCard } from '@/utils/mask'
const router = useRouter()
const tableRef = ref(null)
const bindCardModalRef = ref(null)
const selectedRowKeys = ref([])
const balanceMap = reactive({})
const syncing = ref(false)
const updateModalOpen = ref(false)
const mobileModalOpen = ref(false)
@ -129,21 +141,17 @@ const changingMobile = ref(false)
const updateForm = reactive({ mobile: '', bankCardNumber: '', bankNo: '', bankName: '' })
const mobileForm = reactive({ newMobile: '' })
const accountTypeOptions = [
{ value: 'A1', label: '主账户' },
{ value: 'A2', label: '电商A2账户' },
{ value: 'A3', label: '电商A3账户' },
{ value: 'A6', label: '保证金账户' },
{ value: 'A7', label: '分户账户' }
// /: customerType , PERSONAL()/ENTERPRISE()
// (2026-08-10 , accountProperty('1'/'2'),),
// .md
const customerTypeOptions = [
{ value: 'PERSONAL', label: '个人' },
{ value: 'ENTERPRISE', label: '企业' }
]
const accountTypeMap = {
A1: { text: '主账户', color: 'blue' },
A2: { text: '电商A2账户', color: 'cyan' },
A3: { text: '电商A3账户', color: 'purple' },
A6: { text: '保证金账户', color: 'orange' },
A7: { text: '分户账户', color: 'green' }
const customerTypeMap = {
PERSONAL: { text: '个人', color: 'blue' },
ENTERPRISE: { text: '企业', color: 'purple' }
}
const accountRelationMap = { 0: { text: '主', color: 'blue' }, 1: { text: '从', color: 'default' } }
const accountStatusMap = {
NORMAL: { text: '正常', color: 'green' },
FROZEN: { text: '冻结', color: 'orange' },
@ -151,15 +159,20 @@ const accountStatusMap = {
LOCKED: { text: '锁定', color: 'red' }
}
// 2026-08-10 :////,
// /(A1~A7)//(,/
// ,("" A1 )使 record.accountType/
// record.accountRelation,)/( utils/mask.js)
const columns = [
{ title: '序号', dataIndex: 'index', width: 60 },
{ title: '客户类型', dataIndex: 'customerType', width: 90 },
{ title: '账户名称', dataIndex: 'accountName' },
{ title: '账号', dataIndex: 'accountNo' },
{ title: '账户类型', dataIndex: 'accountType' },
{ title: '主/从标识', dataIndex: 'accountRelation', width: 90 },
{ title: '手机号', dataIndex: 'mobilePhone', width: 130 },
{ title: '证件号', dataIndex: 'certificateNumber', width: 160 },
{ title: '状态', dataIndex: 'accountStatus' },
{ title: '活期余额', dataIndex: 'mainBalance' },
{ title: '保证金余额', dataIndex: 'a6Balance' },
{ title: '开户日期', dataIndex: 'openDate', width: 160 },
{ title: '渠道', dataIndex: 'channelName' },
{ title: '机构', dataIndex: 'organizationName' },
{ title: '操作', dataIndex: 'action', width: 80 }
]
@ -169,26 +182,10 @@ const selectedRecord = computed(() =>
const canOperate = computed(() => !!selectedRecord.value && selectedRecord.value.accountStatus !== 'CLOSED')
const canOpenSubAccount = computed(() => canOperate.value && selectedRecord.value.accountType === 'A1')
async function loadBalanceForRecord(record) {
try {
const res = await fetchWalletAccountDetailApi({ id: record.id })
if (res.data.code === 200) {
balanceMap[record.accountNo] = {
mainBalance: res.data.data.mainBalance,
a6Balance: res.data.data.a6Balance
}
}
} catch (e) {
balanceMap[record.accountNo] = { mainBalance: '--', a6Balance: '--' }
}
}
async function loadList(params) {
const res = await fetchWalletAccountPageApi(params)
if (res.data.code === 200) {
const records = res.data.data.records || []
records.filter((r) => r.accountRelation === '0').forEach((r) => loadBalanceForRecord(r))
return { list: records, total: res.data.data.total || 0 }
return { list: res.data.data.records || [], total: res.data.data.total || 0 }
}
return { list: [], total: 0 }
}
@ -223,7 +220,6 @@ async function handleSyncBalance() {
const res = await syncWalletAccountBalanceApi({ id: selectedRecord.value.id })
if (res.data.code === 200) {
message.success('余额同步成功')
await loadBalanceForRecord(selectedRecord.value)
}
} finally {
syncing.value = false
@ -277,6 +273,9 @@ function openBindCard() {
}
function goDetail(record) {
router.push({ path: '/wallet/account/detail', query: { id: record.id, accountNo: record.accountNo, accountName: record.accountName } })
router.push({
path: '/wallet/account/detail',
query: { id: record.id, accountNo: record.accountNo, accountName: record.accountName, channelNo: record.channelNo }
})
}
</script>

View File

@ -1569,6 +1569,73 @@ INSERT INTO "SFT"."AUTH__ROLE_PERMISSION" ("ID", "ROLE_ID", "PERMISSION_ID") VAL
参数可以覆盖这个行为,必须让后端加上真正的 `fileName`/`fileExt` 请求字段,或者让后端的
存储逻辑去解析 `fileBase64` 前缀/内容魔数,前端单方面无法解决。
## Part 20:个人开户 `create-draft` 新增字段 + 账户列表筛选区改造 —— 2026-08-09/08-10 补充
124. **`personal-opening/create-draft` 请求体新增 `channelName`/`organizationId`/
`organizationName` 三个字段(2026-08-09 依据后端 `CreateDraftRequest` 源码截图确认,非前端
猜测)**:`channelName` 是所选渠道的名称(`channelNo` 对应的名称,`ChannelSelect.vue` 内部
已拿到,只是此前对外只暴露编号,已改造为额外通过 `update:channelName` 事件回传);
`organizationId`/`organizationName` 是所选客户的所属机构ID/机构名称(客户详情接口
`customer-info/personal/detail` 已带 `organizationId`,机构名称本地没有,前端复用
`/api/organization/list-all` 建 id→name 映射换取,与 `PersonalList.vue` 等既有做法一致,
未新增接口)。这三个字段均未在截图中标注 `@NotNull`,前端按选填处理(留空时不传)。
`PersonalOpenForm.vue``personal-opening/update`(编辑态)本身是前端假设契约(见 Part 11
第91条),本次一并把这三个字段带上,但**尚未拿到 `update` 接口是否真的接受/需要这三个字段
的确认**。
125. **`/api/wallet-account/page`(账户列表)查询条件按最新原型截图 + 口头确认改造:新增
`channelNo`(渠道)/`organizationId`(所属机构),删除 `accountType`(账户类型)查询参数**
(2026-08-10 补充,当前仓库最新 swagger `swagger_project_scfs_2026-08-05_15-15-22.json` 仍是
旧契约 `{page,pageSize,accountName,accountNo,accountType}`,本次改动依据的是比这份 swagger
更新的口头确认,尚无对应新版 swagger 文件佐证,后端出正式文档后需重新核对字段名):
- `accountType` 作为查询参数已删除,但列表结果 `WalletAccountVo.accountType` 字段本身不受
影响,列表"账户类型"列展示不变。
- 新增 `organizationId`:前端沿用现有 `OrgTreeSelect.vue`(内部通过
`/api/organization/list-all` 取机构列表,选中后 value 即为机构 id),不是本次新增组件。
- 新增 `channelNo`:前端沿用现有 `ChannelSelect.vue`
- 同时按最新原型截图把筛选区的"账户名称"改标签为"客户名称"(仍是同一个 `accountName`
字段,swagger 描述本就写的是"客户名称",此前前端标签文案有误)、去掉了"账号"
(`accountNo`)筛选框,并新增一个**后端新增的独立查询参数 `customerType`**(客户类型,与
`accountType` 是两个不同维度)。**取值已由后端确认为 `PERSONAL`(个人)/`ENTERPRISE`
(企业)**(2026-08-10),与项目里"个人/企业"场景常见的 `accountProperty`('1'/'2',见
`BindCardModal.vue`/`PersonalOpenForm.vue`)不是同一套编码,不要混用;与授信管理模块
贷款申请的 `customerType`(见第29条、Part 16)取值风格一致。
- 本次未新增"钱包编号"/"手机号"筛选项(产品确认暂不需要)。
126. **`/api/wallet-account/page` 列表结果 `WalletAccountVo` 按最新原型截图同步新增展示字段
(2026-08-10 补充)**:列表页表格列已改为"客户类型/账户名称/手机号/证件号/状态/开户日期/
渠道/机构",不再展示"账号/账户类型(A1~A7)/主从标识/余额"(产品确认余额不需要在列表页
展示;账户类型/主从标识只是不展示,`record.accountType`/`record.accountRelation` 内部逻辑
仍在用,如"开分户"仍要求 `accountType==='A1'`)。展示所需字段及后端确认情况:
- `customerType`(客户类型,`PERSONAL`/`ENTERPRISE`)、`certificateNumber`(证件号)、
`channelNo`/`channelName`(渠道编号/名称)、`mobilePhone`(手机号)——**已由后端确认字段名**。
- `organizationName`(机构名称)——**前端假设字段名**,沿用项目里 organizationId/
organizationName 配对出现的一贯命名习惯(如 `OrgTreeSelect.vue`/个人开户机构名称,详见
Part 20 第124条),未获后端逐字确认,若实际字段名不同需同步修改 `AccountList.vue`
`bodyCell` 对应的 `record.organizationName` 取值。
- `openDate`(开户日期)——`WalletAccountVo` 原有字段,非本次新增。
- 手机号/证件号按项目统一脱敏规则展示(`src/utils/mask.js` 的 `maskMobile`/`maskIdCard`),
不改变提交/展示以外场景的原始值。
- 当前仓库最新 swagger(`swagger_project_scfs_2026-08-05_15-15-22.json`)的
`WalletAccountVo` 尚不含上述任何新增字段,均依据本次口头确认改造,后端出正式文档后需
整体重新核对。
127. **`POST /api/wallet-account/detail` 请求体新增必传 `channelNo`(渠道编号)字段
(2026-08-10 口头确认,仅明确针对"账户主页"页面场景,未确认是否对该接口的其它调用方
同样生效)**:`AccountDetail.vue`(账户主页,路由 `/wallet/account/detail`)页面本身已有
"所属渠道" `ChannelSelect`,已改为 `fetchWalletAccountDetailApi({ id, channelNo })`,并把
原来 `onMounted` 时无条件调用改成监听 `channelNo` 选中/切换后才调用(未选渠道前不发请求,
与页面其它按钮"必须先选渠道才可用"的既有约束一致)。**`src/views/payment/order-payment/
PaymentFormModal.vue`(订单支付弹窗选择付款钱包账户后)调用的同一个接口本次未同步修改**,
该弹窗当前没有渠道选择UI,继续只传 `{id}`——若后端实际是对整个 `/wallet-account/detail`
接口全局加了必传校验,这处调用会报错,需要后端确认该接口是否所有场景都要求
`channelNo`,如果是,前端需要在 `PaymentFormModal.vue` 补一个渠道选择入口(而不是凭空
编造一个值)。**2026-08-10 补充修复**:上一版改动只是"等用户手动选渠道后才调用",但账户
列表"查看"按钮跳转时**没有把该账户已知的渠道信息带过来**,导致进入详情页后 `channelNo`
为空、`detail` 接口一直不会被调用,表现上像是"点查看没反应"。已修复为:`AccountList.vue`
`goDetail``record.channelNo`(列表页本身已展示的"渠道"列)一并放进跳转路由的
query,`AccountDetail.vue` 用它预填渠道选择器(`ref(route.query.channelNo || undefined)`),
并把原来的 `watch(channelNo, cb)` 加上 `{ immediate: true }`,让"路由带入初始值"这种非
用户手动触发变化的场景也能在页面打开时自动发起一次 `detail` 请求;若列表记录本身没有
渠道(历史数据缺失),仍保持 `undefined`,退化为原来的"需要用户手动选择"。
---
## Part 20:企业开户移动端H5确认页(`fryl_h5` 新增页面)+ 管理后台"邀请扫码办理"入口 —— 2026-08-07 补充
@ -1825,4 +1892,3 @@ INSERT INTO "SFT"."AUTH__ROLE_PERMISSION" ("ID", "ROLE_ID", "PERMISSION_ID") VAL