ced61dfc65
排除处理中提现金额,防止重复提现超过总额。 Co-authored-by: Cursor <cursoragent@cursor.com>
599 lines
22 KiB
Vue
599 lines
22 KiB
Vue
<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>
|