新增个人贷款申请
commit
361f30d549
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
68
缺失的后端接口.md
68
缺失的后端接口.md
|
|
@ -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
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue