Files
b2894lxlx ced61dfc65 fix: 提现展示并校验可提现余额
排除处理中提现金额,防止重复提现超过总额。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-11 00:47:43 +08:00

599 lines
22 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page-body">
<view class="overview-card">
<view class="overview-header">
<view class="summary-main">
<view class="summary-label">总余额</view>
<view class="summary-amount">¥{{ formatMoney(totalBalance) }}</view>
<view class="summary-time" v-if="balanceInfo.updateTime">最近更新时间:{{ balanceInfo.updateTime }}</view>
</view>
<el-button type="text" class="toggle-button" @click="toggleMerchantList">
{{ merchantExpanded ? "收起明细" : "展开明细" }}
</el-button>
</view>
<view class="merchant-list" v-show="merchantExpanded">
<view class="merchant-row" v-for="item in balanceInfoList" :key="item.accNo">
<view class="merchant-info">
<view class="merchant-name">{{ item.merchantName }}</view>
<view class="merchant-meta">{{ item.accNo }}</view>
<view class="merchant-meta" v-if="item.updateTime">更新时间:{{ item.updateTime }}</view>
</view>
<view class="merchant-side">
<view class="merchant-balance">¥{{ formatMoney(item.balance) }}</view>
<view class="merchant-meta" v-if="Number(item.pendingAmount || 0) > 0">
处理中提现:¥{{ formatMoney(item.pendingAmount) }},可提现:¥{{ formatMoney(item.availableBalance) }}
</view>
<view class="summary-actions">
<el-button size="small" :loading="balanceQuery[item.accNo]" @click="queryBalance(item)">查询余额</el-button>
<el-button type="warning" size="small" icon="el-icon-wallet"
:disabled="Number(item.availableBalance || 0) <= 0"
@click="openWithdrawDialog(item)">提现</el-button>
</view>
</view>
</view>
</view>
</view>
<vk-data-table-query v-model="queryForm1.formData" :columns="queryForm1.columns"
@search="search"></vk-data-table-query>
<vk-data-table ref="table1" :action="table1.action" :columns="table1.columns" :query-form-param="queryForm1"
:pagination="true"
@current-change="currentChange"
@selection-change="selectionChange">
<template v-slot:action="{ row }">
<el-button v-if="row.srcReqId" type="primary" size="mini" @click="queryActionLog(row)">查询状态</el-button>
</template>
</vk-data-table>
<el-dialog title="提现" :visible.sync="withdrawDialog.show" width="520px" :close-on-click-modal="false">
<el-form label-width="90px">
<el-form-item label="提现企业">
<view class="withdraw-company">{{ withdrawDialog.form.merchantName || "-" }}</view>
</el-form-item>
<el-form-item label="提现账号">
<view class="withdraw-company">{{ withdrawDialog.form.merchantId || "-" }}</view>
</el-form-item>
<el-form-item label="当前余额">
<view class="withdraw-balance">¥{{ formatMoney(withdrawDialog.form.balance) }}</view>
</el-form-item>
<el-form-item label="处理中提现" v-if="Number(withdrawDialog.form.pendingAmount || 0) > 0">
<view class="withdraw-company">¥{{ formatMoney(withdrawDialog.form.pendingAmount) }}</view>
</el-form-item>
<el-form-item label="可提现余额">
<view class="withdraw-balance">¥{{ formatMoney(withdrawDialog.form.availableBalance) }}</view>
</el-form-item>
<el-form-item label="提现金额" required>
<el-input-number v-model="withdrawDialog.form.amount" :min="0" :precision="2" :step="100"
:max="Number(withdrawDialog.form.availableBalance || 0)"
controls-position="right" style="width: 100%;"></el-input-number>
</el-form-item>
<el-form-item label="说明">
<el-input v-model="withdrawDialog.form.description" type="textarea" :rows="4" maxlength="100"
show-word-limit placeholder="请输入提现说明"></el-input>
</el-form-item>
</el-form>
<template v-slot:footer>
<el-button @click="withdrawDialog.show = false">取消</el-button>
<el-button type="primary" :loading="withdrawDialog.loading" @click="submitWithdraw">确认提现</el-button>
</template>
</el-dialog>
</view>
</template>
<script>
var that;
var vk = uni.vk;
const typeData = [
{ value: "income", label: "收入" },
{ value: "expense", label: "支出" }
];
const merchantOptions = [
{ value: "zg000002466577", accNo: "zg000002466577", label: "广西金久信电子商务有限公司" },
{ value: "zg000002460927", accNo: "zg000002460927", label: "南宁亿利通钢管有限公司" },
{ value: "zg000002494918", accNo: "zg000002494918", label: "广西冀旺贸易有限公司" },
{ value: "zg000002459804", accNo: "zg000002459804", label: "广西金久信电子商务有限公司" }
];
export default {
computed: {
totalBalance() {
return this.balanceInfoList.reduce((total, item) => {
return total + Number(item.balance || 0);
}, 0);
}
},
data() {
return {
loading: false,
data: {},
merchantOptions,
balanceInfo: {
balance: 0,
updateTime: ""
},
merchantExpanded: true,
balanceInfoList: merchantOptions.map(item => ({
accNo: item.accNo,
merchantName: item.label,
balance: 0,
pendingAmount: 0,
availableBalance: 0,
updateTime: ""
})),
table1: {
action: "admin/finance/balance/sys/getList",
columns: [
{
key: "description",
title: "说明",
type: "text",
align: "left",
minWidth: 240
},
{
key: "type",
title: "类型",
type: "select",
width: 120,
data: typeData
},
{
key: "amount",
title: "金额",
type: "text",
width: 140,
formatter: (val, row) => {
const sign = row.type === "income" ? "+" : "-";
return `${sign}¥${that.formatMoney(val)}`;
}
},
{
key: "logTime",
title: "时间",
type: "time",
width: 180,
sortable: "custom"
},
{
key: "_add_time",
title: "创建时间",
type: "time",
width: 180,
sortable: "custom"
},
{
key: "status",
title: "状态",
type: "text",
width: 120,
formatter: (val) => {
return that.formatStatus(val);
}
},
{
key: "action",
title: "操作",
type: "html",
width: 140
}
],
multipleSelection: [],
selectItem: ""
},
queryForm1: {
formData: {},
columns: [
{
key: "type",
title: "类型",
type: "select",
mode: "=",
data: typeData
},
{
key: "logTime",
title: "时间",
type: "datetimerange",
width: 380,
mode: "[]"
}
]
},
withdrawDialog: {
show: false,
loading: false,
form: {
merchantId: "",
merchantName: "",
balance: 0,
pendingAmount: 0,
availableBalance: 0,
amount: 0,
description: ""
}
},
balanceQuery: merchantOptions.reduce((map, item) => {
map[item.accNo] = false;
return map;
}, {})
};
},
onLoad(options = {}) {
that = this;
vk = that.vk;
that.options = options;
that.init(options);
},
methods: {
init() {
that.getBalanceSummary();
},
search() {
that.$refs.table1.search();
that.getBalanceSummary();
},
refresh() {
that.$refs.table1.refresh();
that.getBalanceSummary();
},
toggleMerchantList() {
that.merchantExpanded = !that.merchantExpanded;
},
currentChange(val) {
that.table1.selectItem = val;
},
selectionChange(list) {
that.table1.multipleSelection = list;
},
async getBalanceSummary() {
const { data = {} } = await vk.callFunction({
url: "admin/finance/balance/sys/getSummary",
data: {}
});
that.balanceInfo = {
balance: 0,
updateTime: data.updateTime || ""
};
const serverList = Array.isArray(data.balanceInfoList) ? data.balanceInfoList : [];
const serverMap = serverList.reduce((map, item) => {
map[item.accNo] = item;
return map;
}, {});
that.balanceInfoList = merchantOptions.map(item => {
const current = serverMap[item.accNo] || {};
const balance = Number(current.balance || 0);
const pendingAmount = Number(current.pendingAmount || 0);
const availableBalance = current.availableBalance !== undefined && current.availableBalance !== null
? Number(current.availableBalance || 0)
: Math.max(0, balance - pendingAmount);
return {
accNo: item.accNo,
merchantName: current.merchantName || item.label,
balance,
pendingAmount,
availableBalance,
updateTime: current.updateTime || ""
};
});
},
async queryBalance(item) {
const accNo = item && item.accNo;
if (!accNo) {
vk.toast("未配置提现账号");
return;
}
that.balanceQuery[accNo] = true;
try {
await vk.callFunction({
url: "admin/finance/balance/sys/getInfo",
data: {
accNo
}
});
await that.getBalanceSummary();
vk.toast(`${item.merchantName}余额已更新`);
} finally {
that.balanceQuery[accNo] = false;
}
},
openWithdrawDialog(item) {
const availableBalance = Number(item.availableBalance || 0);
if (availableBalance <= 0) {
const pendingAmount = Number(item.pendingAmount || 0);
vk.toast(
pendingAmount > 0
? `可提现余额不足(处理中提现¥${that.formatMoney(pendingAmount)}已占用)`
: "当前企业余额小于等于0,无法提现"
);
return;
}
that.withdrawDialog.form = {
merchantId: item.accNo,
merchantName: item.merchantName,
balance: Number(item.balance || 0),
pendingAmount: Number(item.pendingAmount || 0),
availableBalance,
amount: 0,
description: ""
};
that.withdrawDialog.show = true;
},
async queryActionLog(row) {
const accNo = String(row && (row.merchantId || row.accNo) || "").trim();
const reqJournalNo = String(row && row.srcReqId || "").trim();
const reqDate = that.formatReqDate(row && row._add_time);
if (!accNo) {
vk.toast("账号不能为空");
return;
}
if (!reqJournalNo) {
vk.toast("srcReqId不能为空");
return;
}
if (!reqDate) {
vk.toast("日期格式不正确");
return;
}
try {
const res = await vk.callFunction({
url: "admin/finance/balance/sys/getActionLog",
data: {
accNo,
reqDate,
reqJournalNo,
logId: row && row._id ? row._id : ""
}
});
const status = that.getActionStatusValue(res) || (
res && res.data && res.data.status !== undefined && res.data.status !== null
? Number(res.data.status)
: ""
);
const statusText = that.formatStatus(status) || (res && res.data && res.data.statusText) || "";
if (status !== "" && row && row._id) {
row.status = status;
}
await that.getBalanceSummary();
vk.toast(statusText || "未识别到状态");
console.log("getActionLog", res);
} catch (err) {
vk.toast((err && err.message) || "查询状态失败");
}
},
async submitWithdraw() {
const { amount, description, merchantId, availableBalance, pendingAmount } = that.withdrawDialog.form;
if (!merchantId) {
vk.toast("提现账号不能为空");
return;
}
if (!amount || Number(amount) <= 0) {
vk.toast("提现金额必须大于0");
return;
}
if (Number(availableBalance || 0) <= 0) {
vk.toast(
Number(pendingAmount || 0) > 0
? `可提现余额不足(处理中提现¥${that.formatMoney(pendingAmount)}已占用)`
: "当前企业余额小于等于0,无法提现"
);
return;
}
if (Number(amount) > Number(availableBalance || 0)) {
vk.toast(
Number(pendingAmount || 0) > 0
? `提现金额不能大于可提现余额¥${that.formatMoney(availableBalance)}(已排除处理中提现)`
: "提现金额不能大于当前企业余额"
);
return;
}
that.withdrawDialog.loading = true;
try {
await vk.callFunction({
url: "admin/finance/balance/sys/withdraw",
data: {
merchantId,
amount: Number(amount),
description
}
});
that.withdrawDialog.show = false;
that.refresh();
vk.toast("提现成功");
} finally {
that.withdrawDialog.loading = false;
}
},
formatMoney(value) {
const amount = Number(value || 0);
return amount.toFixed(2);
},
formatReqDate(value) {
const raw = value || "";
const date = typeof raw === "number"
? new Date(raw)
: new Date(String(raw).replace(/-/g, "/"));
if (Number.isNaN(date.getTime())) {
return "";
}
return vk.pubfn.timeFormat(date, "yyyyMMdd");
},
getActionStatusValue(res) {
const orderSet = this.pickOrderSet(res);
if (!orderSet) {
return "";
}
let orderList = [];
try {
orderList = JSON.parse(orderSet);
} catch (err) {
console.log("orderSet parse error", err, orderSet);
return "";
}
if (!Array.isArray(orderList) || !orderList.length) {
return "";
}
const firstOrder = orderList[0] || {};
const status = Number(firstOrder.status);
return Number.isFinite(status) ? status : "";
},
formatStatus(status) {
const statusMap = {
1: "处理中",
2: "成功",
3: "处理失败",
5: "退票",
6: "退票重提"
};
return statusMap[Number(status)] || "";
},
pickOrderSet(res) {
const candidates = [
res && res.orderSet,
res && res.data && res.data.orderSet,
res && res.info && res.info.orderSet,
res && res.content && res.content.orderSet,
res && res.data && Array.isArray(res.data) && res.data[0] && res.data[0].orderSet,
res && res.list && Array.isArray(res.list) && res.list[0] && res.list[0].orderSet,
res && res.rows && Array.isArray(res.rows) && res.rows[0] && res.rows[0].orderSet,
res && res.data && res.data.list && Array.isArray(res.data.list) && res.data.list[0] && res.data.list[0].orderSet,
res && res.data && res.data.rows && Array.isArray(res.data.rows) && res.data.rows[0] && res.data.rows[0].orderSet
];
for (let i = 0; i < candidates.length; i++) {
if (typeof candidates[i] === "string" && candidates[i]) {
return candidates[i];
}
}
return "";
}
}
};
</script>
<style lang="scss" scoped>
.page-body {
.overview-card {
display: flex;
flex-direction: column;
padding: 24rpx 32rpx;
margin-bottom: 24rpx;
border-radius: 16rpx;
background: linear-gradient(135deg, #fff7e6 0%, #fff1d6 100%);
border: 1px solid #f7d8a8;
}
.overview-header {
width: 100%;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24rpx;
padding-bottom: 24rpx;
margin-bottom: 24rpx;
border-bottom: 1px solid rgba(194, 106, 0, 0.12);
}
.toggle-button {
flex-shrink: 0;
padding: 0;
}
.merchant-list {
display: flex;
flex-direction: column;
gap: 20rpx;
width: 100%;
}
.merchant-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24rpx;
padding: 20rpx 0;
border-bottom: 1px solid rgba(194, 106, 0, 0.12);
}
.merchant-row:last-child {
padding-bottom: 0;
border-bottom: none;
}
.merchant-info {
min-width: 0;
flex: 1;
}
.merchant-side {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12rpx;
flex-shrink: 0;
}
.summary-main {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.summary-actions {
display: flex;
align-items: center;
gap: 16rpx;
flex-shrink: 0;
}
.summary-label {
font-size: 26rpx;
color: #8c6b2f;
}
.merchant-name {
font-size: 28rpx;
font-weight: 600;
color: #8c6b2f;
}
.merchant-meta {
font-size: 24rpx;
color: #9f8252;
word-break: break-all;
}
.summary-amount {
font-size: 52rpx;
line-height: 1.1;
font-weight: 700;
color: #c26a00;
}
.merchant-balance {
font-size: 34rpx;
line-height: 1.1;
font-weight: 700;
color: #c26a00;
}
.summary-time {
font-size: 24rpx;
color: #9f8252;
}
.withdraw-balance {
font-size: 32rpx;
font-weight: 600;
color: #c26a00;
}
.withdraw-company {
color: #8c6b2f;
word-break: break-all;
}
}
</style>