前端页面修改
parent
2d168b2a77
commit
7786c231d8
|
|
@ -1,62 +1,113 @@
|
||||||
<!-- src/views/wallet/account/AccountDetail.vue -->
|
<!-- src/views/wallet/account/AccountDetail.vue -->
|
||||||
<!-- 2.6.4/2.6.5 账户主页:余额卡片 + 交易明细(时间范围查询+前端本地分页)+ 提现/保证金/回单/对账单入口。
|
<!-- 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>
|
<template>
|
||||||
<div class="wallet-account-detail-page">
|
<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-alert
|
||||||
<a-form layout="inline">
|
v-if="!channelNo"
|
||||||
<a-form-item label="所属渠道" required>
|
type="warning"
|
||||||
<ChannelSelect v-model:model-value="channelNo" />
|
show-icon
|
||||||
</a-form-item>
|
message="当前账户缺少渠道信息,账户主页与交易明细暂无法加载,请返回账户列表重新进入本页"
|
||||||
</a-form>
|
style="margin-bottom: 16px"
|
||||||
</a-card>
|
/>
|
||||||
|
|
||||||
<a-row :gutter="16" style="margin-bottom: 16px">
|
<div class="account-cards">
|
||||||
<a-col :span="6" v-if="detail.mainAccountNo">
|
<div v-if="detail.mainAccountNo" class="account-card">
|
||||||
<a-card title="主账户(A1)">
|
<div class="account-card__head">
|
||||||
<p>账号:{{ detail.mainAccountNo }}</p>
|
<a-tag color="blue" class="account-card__tag">主账户A1</a-tag>
|
||||||
<p>余额:{{ detail.mainBalance }}</p>
|
<span class="account-card__no">{{ detail.mainAccountNo }}</span>
|
||||||
<a-button type="primary" size="small" :disabled="!channelNo" @click="openWithdraw">提现</a-button>
|
</div>
|
||||||
</a-card>
|
<div class="account-card__body">
|
||||||
</a-col>
|
<div class="account-card__balance-wrap">
|
||||||
<a-col :span="6" v-if="detail.a2AccountNo">
|
<span class="account-card__balance-label">余额:</span>
|
||||||
<a-card title="电商账户(A2)">
|
<span class="account-card__balance">¥{{ detail.mainBalance }}</span>
|
||||||
<p>账号:{{ detail.a2AccountNo }}</p>
|
</div>
|
||||||
<p>余额:{{ detail.a2Balance }}</p>
|
<div class="account-card__actions">
|
||||||
</a-card>
|
<a-button type="link" size="small" :disabled="!channelNo" @click="openWithdraw">提现</a-button>
|
||||||
</a-col>
|
</div>
|
||||||
<a-col :span="6" v-if="detail.a6AccountNo">
|
</div>
|
||||||
<a-card title="保证金账户(A6)">
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
<a-card title="交易明细">
|
<div v-if="detail.a2AccountNo" class="account-card">
|
||||||
<template #extra>
|
<div class="account-card__head">
|
||||||
<a-space>
|
<a-tag color="cyan" class="account-card__tag">电商账户A2</a-tag>
|
||||||
<a-button size="small" @click="setQuickRange('today')">今天</a-button>
|
<span class="account-card__no">{{ detail.a2AccountNo }}</span>
|
||||||
<a-button size="small" @click="setQuickRange('lastMonth')">上月</a-button>
|
</div>
|
||||||
<a-button size="small" @click="setQuickRange('last3Months')">近三月</a-button>
|
<div class="account-card__body">
|
||||||
<a-range-picker v-model:value="rangeValue" value-format="YYYY-MM-DD" @change="handleRangeChange" />
|
<div class="account-card__balance-wrap">
|
||||||
<a-button type="primary" :disabled="!channelNo" :loading="txLoading" @click="loadTransactionDetail">查询</a-button>
|
<span class="account-card__balance-label">余额:</span>
|
||||||
<a-button :disabled="!channelNo" @click="handleDownloadReceipt">回单下载</a-button>
|
<span class="account-card__balance">¥{{ detail.a2Balance }}</span>
|
||||||
<a-button :disabled="!channelNo" @click="statementModalOpen = true">对账单下载</a-button>
|
</div>
|
||||||
</a-space>
|
</div>
|
||||||
</template>
|
</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 @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
|
<a-table
|
||||||
:columns="txColumns"
|
:columns="txColumns"
|
||||||
|
|
@ -76,7 +127,7 @@
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</a-card>
|
</div>
|
||||||
|
|
||||||
<WithdrawModal ref="withdrawModalRef" :channel-no="channelNo" @success="handleFundActionSuccess" />
|
<WithdrawModal ref="withdrawModalRef" :channel-no="channelNo" @success="handleFundActionSuccess" />
|
||||||
<MarginModal ref="marginModalRef" @success="handleFundActionSuccess" />
|
<MarginModal ref="marginModalRef" @success="handleFundActionSuccess" />
|
||||||
|
|
@ -88,15 +139,25 @@
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import dayjs from 'dayjs'
|
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 WithdrawModal from '@/components/WithdrawModal.vue'
|
||||||
import MarginModal from '@/components/MarginModal.vue'
|
import MarginModal from '@/components/MarginModal.vue'
|
||||||
import { fetchWalletAccountDetailApi, fetchTransactionDetailApi, downloadReceiptApi, downloadStatementApi } from '@/api/wallet'
|
import { fetchWalletAccountDetailApi, fetchTransactionDetailApi, downloadReceiptApi, downloadStatementApi } from '@/api/wallet'
|
||||||
|
|
@ -107,7 +168,10 @@ const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const accountName = route.query.accountName || ''
|
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 withdrawModalRef = ref(null)
|
||||||
const marginModalRef = ref(null)
|
const marginModalRef = ref(null)
|
||||||
|
|
||||||
|
|
@ -122,6 +186,15 @@ const detail = reactive({
|
||||||
a7Balance: ''
|
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 rangeValue = ref([dayjs().format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD')])
|
||||||
const startDate = ref(`${dayjs().format('YYYY-MM-DD')} 00:00:00`)
|
const startDate = ref(`${dayjs().format('YYYY-MM-DD')} 00:00:00`)
|
||||||
const endDate = ref(`${dayjs().format('YYYY-MM-DD')} 23:59:59`)
|
const endDate = ref(`${dayjs().format('YYYY-MM-DD')} 23:59:59`)
|
||||||
|
|
@ -134,6 +207,8 @@ const statementModalOpen = ref(false)
|
||||||
const statementDownloading = ref(false)
|
const statementDownloading = ref(false)
|
||||||
const statementRangeValue = ref([dayjs().startOf('month').format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD')])
|
const statementRangeValue = ref([dayjs().startOf('month').format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD')])
|
||||||
|
|
||||||
|
const statsModalOpen = ref(false)
|
||||||
|
|
||||||
const txColumns = [
|
const txColumns = [
|
||||||
{ title: '序号', dataIndex: 'index', width: 60 },
|
{ title: '序号', dataIndex: 'index', width: 60 },
|
||||||
{ title: '系统流水号', dataIndex: 'id' },
|
{ title: '系统流水号', dataIndex: 'id' },
|
||||||
|
|
@ -152,8 +227,37 @@ const pagedTxList = computed(() => {
|
||||||
return txList.value.slice(start, start + txPagination.pageSize)
|
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() {
|
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) {
|
if (res.data.code === 200) {
|
||||||
Object.assign(detail, res.data.data)
|
Object.assign(detail, res.data.data)
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +272,7 @@ function sortTxList(list) {
|
||||||
|
|
||||||
async function loadTransactionDetail() {
|
async function loadTransactionDetail() {
|
||||||
if (!channelNo.value || !detail.mainAccountNo) {
|
if (!channelNo.value || !detail.mainAccountNo) {
|
||||||
showErrorModal('请先选择所属渠道')
|
showErrorModal('当前账户缺少渠道信息,无法查询交易明细')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
txLoading.value = true
|
txLoading.value = true
|
||||||
|
|
@ -199,26 +303,49 @@ function handleRangeChange(values) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用户手动改动日期区间选择器时,清除快捷按钮的高亮态(与点击"今天/昨天/近七天/当月/上月"区分开)
|
||||||
|
function onRangePickerChange(values) {
|
||||||
|
activeQuickRange.value = null
|
||||||
|
handleRangeChange(values)
|
||||||
|
}
|
||||||
|
|
||||||
function setQuickRange(type) {
|
function setQuickRange(type) {
|
||||||
const today = dayjs()
|
const today = dayjs()
|
||||||
if (type === 'today') {
|
if (type === 'today') {
|
||||||
rangeValue.value = [today.format('YYYY-MM-DD'), today.format('YYYY-MM-DD')]
|
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') {
|
} else if (type === 'lastMonth') {
|
||||||
const lastMonth = today.subtract(1, 'month')
|
const lastMonth = today.subtract(1, 'month')
|
||||||
rangeValue.value = [lastMonth.startOf('month').format('YYYY-MM-DD'), lastMonth.endOf('month').format('YYYY-MM-DD')]
|
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)
|
handleRangeChange(rangeValue.value)
|
||||||
loadTransactionDetail()
|
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) {
|
function handleTxTableChange(pag) {
|
||||||
txPagination.current = pag.current
|
txPagination.current = pag.current
|
||||||
txPagination.pageSize = pag.pageSize
|
txPagination.pageSize = pag.pageSize
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDownloadReceipt() {
|
async function handleDownloadReceipt() {
|
||||||
|
if (!channelNo.value) return
|
||||||
const targets = selectedTxKeys.value.length ? txList.value.filter((item) => selectedTxKeys.value.includes(item.id)) : txList.value
|
const targets = selectedTxKeys.value.length ? txList.value.filter((item) => selectedTxKeys.value.includes(item.id)) : txList.value
|
||||||
if (!targets.length) {
|
if (!targets.length) {
|
||||||
showErrorModal('当前查询范围内无可下载的流水')
|
showErrorModal('当前查询范围内无可下载的流水')
|
||||||
|
|
@ -242,6 +369,11 @@ async function handleDownloadReceipt() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openStatementModal() {
|
||||||
|
if (!channelNo.value) return
|
||||||
|
statementModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
async function handleDownloadStatement() {
|
async function handleDownloadStatement() {
|
||||||
if (!statementRangeValue.value || statementRangeValue.value.length !== 2) {
|
if (!statementRangeValue.value || statementRangeValue.value.length !== 2) {
|
||||||
showErrorModal('请选择时间范围')
|
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() {
|
function openWithdraw() {
|
||||||
withdrawModalRef.value.show({ accountNo: detail.mainAccountNo, accountName, maxAmount: detail.mainBalance })
|
withdrawModalRef.value.show({ accountNo: detail.mainAccountNo, accountName, maxAmount: detail.mainBalance })
|
||||||
}
|
}
|
||||||
|
|
@ -285,10 +449,141 @@ function goBack() {
|
||||||
router.push('/wallet/account')
|
router.push('/wallet/account')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadDetail)
|
// channelNo 完全来自路由 query,一进入页面就是确定值(不会再有用户手动触发 change 的场景),
|
||||||
|
// 这里 immediate:true 是为了让页面打开时就自动发起一次 detail 请求
|
||||||
|
watch(
|
||||||
|
channelNo,
|
||||||
|
(val) => {
|
||||||
|
if (val) loadDetail()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.amount-negative {
|
||||||
color: #ff4d4f;
|
color: #ff4d4f;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
<!-- src/views/wallet/account/AccountList.vue -->
|
<!-- src/views/wallet/account/AccountList.vue -->
|
||||||
<!-- "所属机构"筛选按机构数据隔离设计要求补充。当前 WalletAccountVo/分页接口均不含
|
<!-- 2026-08-10 按最新原型截图(website_detail 商业平台账户列表页)+ 后端接口变更改造筛选区:
|
||||||
organizationId 字段(账户仅关联 customerId),后端需先建立"钱包账户 -> 客户 -> 所属机构"
|
后端 /api/wallet-account/page 查询条件已改为 channelNo(渠道)/organizationId(所属机构),
|
||||||
的关联并在 /api/wallet-account/page 中支持该过滤参数,该筛选才能真正生效,已登记于
|
并删除了 accountType(账户类型)查询参数(账户类型仍作为列表结果字段展示,只是不再支持按它
|
||||||
《缺失的后端接口.md》。 -->
|
筛选)。"所属机构"沿用现有 OrgTreeSelect(内部已通过 /api/organization/list-all 换取机构ID
|
||||||
|
再送给后端,不是本次新增)。"客户类型"是本次后端新增的独立查询参数 customerType,
|
||||||
|
与"账户类型"(A1/A2/A3/A6/A7)是两个不同维度,取值为 PERSONAL(个人)/ENTERPRISE(企业)
|
||||||
|
(2026-08-10 后端已确认,与项目里"个人/企业"场景常见的 accountProperty('1'/'2')编码
|
||||||
|
不是同一套,不要混用),详见《缺失的后端接口.md》。 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="wallet-account-list-page">
|
<div class="wallet-account-list-page">
|
||||||
<ProTable
|
<ProTable
|
||||||
|
|
@ -11,20 +15,25 @@
|
||||||
:fetch-data="loadList"
|
:fetch-data="loadList"
|
||||||
row-selection="radio"
|
row-selection="radio"
|
||||||
v-model:selected-row-keys="selectedRowKeys"
|
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 }">
|
<template #search="{ form }">
|
||||||
<a-form-item label="账户名称">
|
<a-form-item label="客户名称">
|
||||||
<a-input v-model:value="form.accountName" placeholder="请输入账户名称" allow-clear style="width: 180px" />
|
<a-input v-model:value="form.accountName" placeholder="请输入客户名称" allow-clear style="width: 180px" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="账号">
|
<a-form-item label="客户类型">
|
||||||
<a-input v-model:value="form.accountNo" placeholder="请输入账号" allow-clear style="width: 180px" />
|
<a-select v-model:value="form.customerType" placeholder="请选择客户类型" allow-clear style="width: 160px">
|
||||||
</a-form-item>
|
<a-select-option v-for="opt in customerTypeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</a-select-option>
|
||||||
<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-select>
|
</a-select>
|
||||||
</a-form-item>
|
</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="所属机构">
|
<a-form-item label="所属机构">
|
||||||
<OrgTreeSelect v-model="form.organizationId" placeholder="请选择所属机构" allow-clear style="width: 200px" />
|
<OrgTreeSelect v-model="form.organizationId" placeholder="请选择所属机构" allow-clear style="width: 200px" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
@ -56,21 +65,24 @@
|
||||||
|
|
||||||
<template #bodyCell="{ column, record, index }">
|
<template #bodyCell="{ column, record, index }">
|
||||||
<template v-if="column.dataIndex === 'index'">{{ index + 1 }}</template>
|
<template v-if="column.dataIndex === 'index'">{{ index + 1 }}</template>
|
||||||
<template v-else-if="column.dataIndex === 'accountType'">
|
<template v-else-if="column.dataIndex === 'customerType'">
|
||||||
<StatusTag :value="record.accountType" :map="accountTypeMap" />
|
<StatusTag :value="record.customerType" :map="customerTypeMap" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.dataIndex === 'accountRelation'">
|
<template v-else-if="column.dataIndex === 'mobilePhone'">
|
||||||
<StatusTag :value="record.accountRelation" :map="accountRelationMap" />
|
{{ 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>
|
||||||
<template v-else-if="column.dataIndex === 'accountStatus'">
|
<template v-else-if="column.dataIndex === 'accountStatus'">
|
||||||
<StatusTag :value="record.accountStatus" :map="accountStatusMap" />
|
<StatusTag :value="record.accountStatus" :map="accountStatusMap" />
|
||||||
</template>
|
</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'">
|
<template v-else-if="column.dataIndex === 'action'">
|
||||||
<a @click="goDetail(record)">查看</a>
|
<a @click="goDetail(record)">查看</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -105,9 +117,9 @@ import ProTable from '@/components/ProTable.vue'
|
||||||
import StatusTag from '@/components/StatusTag.vue'
|
import StatusTag from '@/components/StatusTag.vue'
|
||||||
import BindCardModal from '@/components/BindCardModal.vue'
|
import BindCardModal from '@/components/BindCardModal.vue'
|
||||||
import OrgTreeSelect from '@/components/OrgTreeSelect.vue'
|
import OrgTreeSelect from '@/components/OrgTreeSelect.vue'
|
||||||
|
import ChannelSelect from '@/components/ChannelSelect.vue'
|
||||||
import {
|
import {
|
||||||
fetchWalletAccountPageApi,
|
fetchWalletAccountPageApi,
|
||||||
fetchWalletAccountDetailApi,
|
|
||||||
closeWalletAccountApi,
|
closeWalletAccountApi,
|
||||||
openWalletSubAccountApi,
|
openWalletSubAccountApi,
|
||||||
updateWalletAccountApi,
|
updateWalletAccountApi,
|
||||||
|
|
@ -115,12 +127,12 @@ import {
|
||||||
syncWalletAccountBalanceApi
|
syncWalletAccountBalanceApi
|
||||||
} from '@/api/wallet'
|
} from '@/api/wallet'
|
||||||
import { getCurrentOrganizationId } from '@/utils/orgScope'
|
import { getCurrentOrganizationId } from '@/utils/orgScope'
|
||||||
|
import { maskMobile, maskIdCard } from '@/utils/mask'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const tableRef = ref(null)
|
const tableRef = ref(null)
|
||||||
const bindCardModalRef = ref(null)
|
const bindCardModalRef = ref(null)
|
||||||
const selectedRowKeys = ref([])
|
const selectedRowKeys = ref([])
|
||||||
const balanceMap = reactive({})
|
|
||||||
const syncing = ref(false)
|
const syncing = ref(false)
|
||||||
const updateModalOpen = ref(false)
|
const updateModalOpen = ref(false)
|
||||||
const mobileModalOpen = ref(false)
|
const mobileModalOpen = ref(false)
|
||||||
|
|
@ -129,21 +141,17 @@ const changingMobile = ref(false)
|
||||||
const updateForm = reactive({ mobile: '', bankCardNumber: '', bankNo: '', bankName: '' })
|
const updateForm = reactive({ mobile: '', bankCardNumber: '', bankNo: '', bankName: '' })
|
||||||
const mobileForm = reactive({ newMobile: '' })
|
const mobileForm = reactive({ newMobile: '' })
|
||||||
|
|
||||||
const accountTypeOptions = [
|
// 客户类型筛选项/展示映射:后端新增的 customerType 查询参数,取值为 PERSONAL(个人)/ENTERPRISE(企业)
|
||||||
{ value: 'A1', label: '主账户' },
|
// (2026-08-10 后端已确认,与 accountProperty('1'/'2')是两套不同的编码,不要混用),详见
|
||||||
{ value: 'A2', label: '电商A2账户' },
|
// 《缺失的后端接口.md》
|
||||||
{ value: 'A3', label: '电商A3账户' },
|
const customerTypeOptions = [
|
||||||
{ value: 'A6', label: '保证金账户' },
|
{ value: 'PERSONAL', label: '个人' },
|
||||||
{ value: 'A7', label: '分户账户' }
|
{ value: 'ENTERPRISE', label: '企业' }
|
||||||
]
|
]
|
||||||
const accountTypeMap = {
|
const customerTypeMap = {
|
||||||
A1: { text: '主账户', color: 'blue' },
|
PERSONAL: { text: '个人', color: 'blue' },
|
||||||
A2: { text: '电商A2账户', color: 'cyan' },
|
ENTERPRISE: { text: '企业', color: 'purple' }
|
||||||
A3: { text: '电商A3账户', color: 'purple' },
|
|
||||||
A6: { text: '保证金账户', color: 'orange' },
|
|
||||||
A7: { text: '分户账户', color: 'green' }
|
|
||||||
}
|
}
|
||||||
const accountRelationMap = { 0: { text: '主', color: 'blue' }, 1: { text: '从', color: 'default' } }
|
|
||||||
const accountStatusMap = {
|
const accountStatusMap = {
|
||||||
NORMAL: { text: '正常', color: 'green' },
|
NORMAL: { text: '正常', color: 'green' },
|
||||||
FROZEN: { text: '冻结', color: 'orange' },
|
FROZEN: { text: '冻结', color: 'orange' },
|
||||||
|
|
@ -151,15 +159,20 @@ const accountStatusMap = {
|
||||||
LOCKED: { text: '锁定', color: 'red' }
|
LOCKED: { text: '锁定', color: 'red' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2026-08-10 按最新原型截图改造列表展示字段:客户类型/手机号/证件号/渠道/机构,不再展示
|
||||||
|
// 账号/账户类型(A1~A7)/主从标识/余额(产品确认余额不需要在列表页展示,账户类型/主从标识
|
||||||
|
// 只是不在列表展示,内部逻辑(如"开分户"仅允许 A1 主账户操作)仍使用 record.accountType/
|
||||||
|
// record.accountRelation,不受影响)。手机号/证件号按项目统一的脱敏规则展示(见 utils/mask.js)。
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '序号', dataIndex: 'index', width: 60 },
|
{ title: '序号', dataIndex: 'index', width: 60 },
|
||||||
|
{ title: '客户类型', dataIndex: 'customerType', width: 90 },
|
||||||
{ title: '账户名称', dataIndex: 'accountName' },
|
{ title: '账户名称', dataIndex: 'accountName' },
|
||||||
{ title: '账号', dataIndex: 'accountNo' },
|
{ title: '手机号', dataIndex: 'mobilePhone', width: 130 },
|
||||||
{ title: '账户类型', dataIndex: 'accountType' },
|
{ title: '证件号', dataIndex: 'certificateNumber', width: 160 },
|
||||||
{ title: '主/从标识', dataIndex: 'accountRelation', width: 90 },
|
|
||||||
{ title: '状态', dataIndex: 'accountStatus' },
|
{ title: '状态', dataIndex: 'accountStatus' },
|
||||||
{ title: '活期余额', dataIndex: 'mainBalance' },
|
{ title: '开户日期', dataIndex: 'openDate', width: 160 },
|
||||||
{ title: '保证金余额', dataIndex: 'a6Balance' },
|
{ title: '渠道', dataIndex: 'channelName' },
|
||||||
|
{ title: '机构', dataIndex: 'organizationName' },
|
||||||
{ title: '操作', dataIndex: 'action', width: 80 }
|
{ title: '操作', dataIndex: 'action', width: 80 }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -169,26 +182,10 @@ const selectedRecord = computed(() =>
|
||||||
const canOperate = computed(() => !!selectedRecord.value && selectedRecord.value.accountStatus !== 'CLOSED')
|
const canOperate = computed(() => !!selectedRecord.value && selectedRecord.value.accountStatus !== 'CLOSED')
|
||||||
const canOpenSubAccount = computed(() => canOperate.value && selectedRecord.value.accountType === 'A1')
|
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) {
|
async function loadList(params) {
|
||||||
const res = await fetchWalletAccountPageApi(params)
|
const res = await fetchWalletAccountPageApi(params)
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
const records = res.data.data.records || []
|
return { list: res.data.data.records || [], total: res.data.data.total || 0 }
|
||||||
records.filter((r) => r.accountRelation === '0').forEach((r) => loadBalanceForRecord(r))
|
|
||||||
return { list: records, total: res.data.data.total || 0 }
|
|
||||||
}
|
}
|
||||||
return { list: [], total: 0 }
|
return { list: [], total: 0 }
|
||||||
}
|
}
|
||||||
|
|
@ -223,7 +220,6 @@ async function handleSyncBalance() {
|
||||||
const res = await syncWalletAccountBalanceApi({ id: selectedRecord.value.id })
|
const res = await syncWalletAccountBalanceApi({ id: selectedRecord.value.id })
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
message.success('余额同步成功')
|
message.success('余额同步成功')
|
||||||
await loadBalanceForRecord(selectedRecord.value)
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
syncing.value = false
|
syncing.value = false
|
||||||
|
|
@ -277,6 +273,9 @@ function openBindCard() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function goDetail(record) {
|
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>
|
</script>
|
||||||
|
|
|
||||||
67
缺失的后端接口.md
67
缺失的后端接口.md
|
|
@ -1566,5 +1566,72 @@ INSERT INTO "SFT"."AUTH__ROLE_PERMISSION" ("ID", "ROLE_ID", "PERMISSION_ID") VAL
|
||||||
参数可以覆盖这个行为,必须让后端加上真正的 `fileName`/`fileExt` 请求字段,或者让后端的
|
参数可以覆盖这个行为,必须让后端加上真正的 `fileName`/`fileExt` 请求字段,或者让后端的
|
||||||
存储逻辑去解析 `fileBase64` 前缀/内容魔数,前端单方面无法解决。
|
存储逻辑去解析 `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`,退化为原来的"需要用户手动选择"。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue