feat: 提现支持按单申请并查看订单详情

账户岛可下钻可提订单,申请支持勾选/时间范围/自定义金额及差额确认;提现详情可打开商品单和挂号单弹窗。
This commit is contained in:
李琦
2026-08-18 15:43:50 +08:00
parent b685257a93
commit 66e666754a
17 changed files with 1215 additions and 87 deletions

View File

@@ -10,15 +10,22 @@ import { IconifyIcon as VbenIcon } from '@vben/icons';
import type { StatIslandItem } from './typing';
withDefaults(
const props = withDefaults(
defineProps<{
items: StatIslandItem[];
/** 每行列数(桌面端),默认 4窄屏自动降为 2 列 */
columns?: number;
/** 点击统计岛时调用(只处理 clickable 或全部可点项) */
onItemClick?: (item: StatIslandItem) => void;
}>(),
{ columns: 4 },
);
function handleItemClick(item: StatIslandItem) {
if (!item.clickable || !props.onItemClick) return;
props.onItemClick(item);
}
/** 金额展示为两位小数;非数字原样透出(如「—」占位) */
function formatValue(val: number | string) {
const n = Number(val);
@@ -32,8 +39,12 @@ function formatValue(val: number | string) {
v-for="item in items"
:key="item.key"
class="stat-island"
:class="{ 'stat-island--emphasize': item.emphasize }"
:class="{
'stat-island--emphasize': item.emphasize,
'stat-island--clickable': item.clickable,
}"
:aria-label="item.label"
@click="handleItemClick(item)"
>
<div class="stat-island-glow" />
<VbenIcon class="stat-icon" :icon="item.icon" />
@@ -79,6 +90,10 @@ function formatValue(val: number | string) {
border-color 0.2s ease;
}
.stat-island--clickable {
cursor: pointer;
}
.stat-island:hover {
border-color: hsl(var(--primary) / 40%);
box-shadow:

View File

@@ -16,4 +16,6 @@ export interface StatIslandItem {
emphasize?: boolean;
/** 值前缀,默认 ¥;传空串则不显示前缀 */
prefix?: string;
/** 是否可点击下钻;为 true 时用 pointer 并触发 onItemClick */
clickable?: boolean;
}

View File

@@ -13,6 +13,7 @@ import { TableAction } from '#/components/table-action';
import { getChargeCashPayRecordListApi } from '#/views/finance/charge-cash-pay-record/api';
import StatisticsReconciliation from '#/views/finance/reconciliation/components/statistics.vue';
import WithdrawalRelationDetail from '#/views/finance/withdrawal/components/withdrawal-relation-detail.vue';
import { useWithdrawOrderDetailModals } from '#/views/finance/withdrawal/utils/use-withdraw-order-detail';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
@@ -24,6 +25,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
const statistics = ref();
const { OrderDetailModal, RegisterDetailModal, openOrderDetail } =
useWithdrawOrderDetailModals();
const [RelationDetailModal, relationDetailApi] = useVbenModal({
connectedComponent: WithdrawalRelationDetail,
});
@@ -59,13 +63,15 @@ initTableAjax();
/** 打开提现+代付互通详情 */
const openDetail = (id: number) => {
relationDetailApi.setData({ id, source: 'charge' });
relationDetailApi.setData({ id, source: 'charge', openOrderDetail });
relationDetailApi.open();
};
</script>
<template>
<Page auto-content-height title="代支付手续费记录">
<OrderDetailModal />
<RegisterDetailModal />
<RelationDetailModal />
<StatisticsReconciliation
v-if="statistics"

View File

@@ -7,7 +7,7 @@ import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, Spin, Tag } from 'ant-design-vue';
import { Button, message, Spin, Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
@@ -23,6 +23,8 @@ const gridApi = ref();
const type = ref(0);
const loading = ref(false);
const preview = ref<Record<string, any> | null>(null);
/** 页面级打开关联订单详情,审核弹窗不嵌套订单弹窗 */
const openOrderDetail = ref<(row: Record<string, any>) => void>();
const [WithdrawalAuditForm, WithdrawalAuditFormApi] =
useVbenForm(withdrawalAudit);
@@ -54,10 +56,12 @@ const [Modal, modalApi] = useVbenModal({
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (!isOpen) {
preview.value = null;
openOrderDetail.value = undefined;
return;
}
const { id, modalType } = modalApi.getData<Record<string, any>>();
type.value = modalType;
openOrderDetail.value = modalApi.getData()?.openOrderDetail;
WithdrawalAuditFormApi.setValues({ id, reason: '' });
loading.value = true;
try {
@@ -73,6 +77,10 @@ function formatMoney(val: any) {
const n = Number(val || 0);
return Number.isFinite(n) ? n.toFixed(2) : '0.00';
}
function viewOrder(od: Record<string, any>) {
openOrderDetail.value?.(od);
}
</script>
<template>
@@ -116,6 +124,24 @@ function formatMoney(val: any) {
</Tag>
</span>
</div>
<div v-if="preview.orders && preview.orders.length" class="preview-orders">
<div class="preview-orders-title">关联订单 · 每单金额</div>
<div
v-for="od in preview.orders"
:key="od.id || od.order_id"
class="preview-row"
>
<span class="label">
<Button type="link" size="small" class="order-no-btn" @click="viewOrder(od)">
{{ od.order_no || '-' }}
</Button>
</span>
<span class="value">
{{ od.category_txt || od.order_type_txt || '' }}
¥{{ formatMoney(od.amount) }}
</span>
</div>
</div>
</div>
<WithdrawalAuditForm />
</Spin>
@@ -163,6 +189,30 @@ function formatMoney(val: any) {
color: hsl(var(--primary));
}
.label.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.order-no-btn {
padding-inline: 0;
height: auto;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.preview-orders {
max-height: 180px;
margin-top: 8px;
padding-top: 8px;
overflow: auto;
border-top: 1px solid hsl(var(--border));
}
.preview-orders-title {
margin-bottom: 8px;
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.ml-2 {
margin-left: 8px;
}

View File

@@ -12,6 +12,7 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import StatisticsReconciliation from '#/views/finance/reconciliation/components/statistics.vue';
import WithdrawalRelationDetail from '#/views/finance/withdrawal/components/withdrawal-relation-detail.vue';
import { useWithdrawOrderDetailModals } from '#/views/finance/withdrawal/utils/use-withdraw-order-detail';
import WithdrawalAudit from './components/WithdrawalAudit.vue';
import { formOptions } from './config/search';
@@ -24,6 +25,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
const statistics = ref();
const { OrderDetailModal, RegisterDetailModal, openOrderDetail } =
useWithdrawOrderDetailModals();
const [WithdrawalAuditModal, WithdrawalAuditModalApi] = useVbenModal({
connectedComponent: WithdrawalAudit,
});
@@ -42,13 +46,14 @@ const infoModal = (id: number, modalType = 0) => {
id,
modalType,
gridApi,
openOrderDetail,
});
WithdrawalAuditModalApi.open();
};
/** 打开提现+代付互通详情 */
const openDetail = (id: number) => {
relationDetailApi.setData({ id, source: 'apply' });
relationDetailApi.setData({ id, source: 'apply', openOrderDetail });
relationDetailApi.open();
};
</script>
@@ -56,6 +61,8 @@ const openDetail = (id: number) => {
<template>
<Page auto-content-height title="提现审核">
<WithdrawalAuditModal />
<OrderDetailModal />
<RegisterDetailModal />
<RelationDetailModal />
<StatisticsReconciliation v-if="statistics" :statistics="statistics" />
<Grid>

View File

@@ -59,3 +59,8 @@ export async function resetPassword(id: number) {
id,
});
}
/** 可提订单明细分页 xk-api GET /withdrawal-application/withdrawable-orders */
export async function getWithdrawableOrders(data: Record<string, any>) {
return requestClient.get<any>(`${prefix}withdrawable-orders`, { params: data });
}

View File

@@ -31,14 +31,29 @@ const props = defineProps<{
totalEarnings: number;
withdrawnAmount: number;
withdrawnFrozenAmount: number;
tcmBalance?: number;
westernBalance?: number;
otherBalance?: number;
customGap?: number;
/** 造成差额的历史自定义提现单 */
customGapApplies?: Record<string, any>[];
/** 点击统计岛 */
onStatClick?: (key: string) => void;
}>();
const emit = defineEmits<{
apply: [];
editCard: [];
refresh: [];
statClick: [key: string];
viewApply: [id: number];
}>();
function handleStatClick(key: string) {
props.onStatClick?.(key);
emit('statClick', key);
}
/** 鼠标在银行卡上的相对位置(用于光泽跟随),-1 表示未进入 */
const glowX = ref(-1);
const glowY = ref(-1);
@@ -108,6 +123,24 @@ const statItems = computed(() => {
value: Number(props.balance || 0),
icon: 'lucide:scale',
},
{
key: 'tcm',
label: '中药可提',
value: Number(props.tcmBalance || 0),
icon: 'lucide:leaf',
},
{
key: 'western',
label: '西药可提',
value: Number(props.westernBalance || 0),
icon: 'lucide:pill',
},
{
key: 'other',
label: '其他可提',
value: Number(props.otherBalance || 0),
icon: 'lucide:layers',
},
];
if (!props.isStoreUser) {
items.push({
@@ -247,9 +280,10 @@ function formatMoney(val: number) {
<article
v-for="item in statItems"
:key="item.key"
class="stat-island"
class="stat-island stat-island--clickable"
:class="{ 'stat-island--fee': item.emphasize }"
:aria-label="item.label"
@click="handleStatClick(item.key)"
>
<div class="stat-island-glow"></div>
<!-- 右上角图标规范SVG 图标固定尺寸 -->
@@ -261,6 +295,20 @@ function formatMoney(val: number) {
</article>
</div>
</div>
<div v-if="Number(customGap || 0) > 0" class="gap-tip">
按单可提合计比账户余额多 ¥{{ formatMoney(Number(customGap || 0)) }}多半是历史自定义提现未按单核销点击打款单号可看申请审核打款进度
<div v-if="customGapApplies && customGapApplies.length" class="gap-applies">
<button
v-for="row in customGapApplies"
:key="row.id"
type="button"
class="gap-order"
@click.stop="emit('viewApply', Number(row.id))"
>
{{ row.order_no }}
</button>
</div>
</div>
</div>
</template>
@@ -610,11 +658,41 @@ function formatMoney(val: number) {
}
/* ===== 统计岛:玻璃质感 + 右上角图标 + 淡荧光 ===== */
.gap-tip {
margin-top: 10px;
padding: 8px 10px;
font-size: 12px;
line-height: 1.5;
color: hsl(var(--warning));
background: hsl(var(--warning) / 0.12);
border: 1px solid hsl(var(--warning) / 30%);
border-radius: 8px;
}
.gap-applies {
display: flex;
flex-wrap: wrap;
gap: 6px 10px;
margin-top: 6px;
}
.gap-order {
padding: 0;
border: 0;
background: none;
color: hsl(var(--primary));
cursor: pointer;
}
.gap-order:hover {
text-decoration: underline;
}
.stat-island {
position: relative;
padding: 14px;
overflow: hidden;
cursor: default;
cursor: pointer;
background: linear-gradient(
135deg,
hsl(var(--primary) / 4%),

View File

@@ -0,0 +1,65 @@
<script lang="ts" setup>
/**
* 提现单 / 资金变动关联订单列表,再下钻业务订单
*/
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Empty, Table } from 'ant-design-vue';
const emit = defineEmits<{
viewOrder: [row: Record<string, any>];
}>();
const orders = ref<Record<string, any>[]>([]);
const title = ref('关联订单');
const columns = [
{ title: '订单号', dataIndex: 'order_no', key: 'order_no' },
{ title: '订单类型', dataIndex: 'order_type_txt', key: 'order_type_txt', width: 110 },
{ title: '分类', dataIndex: 'category_txt', key: 'category_txt', width: 80 },
{ title: '占用金额', dataIndex: 'amount', key: 'amount', width: 110 },
{ title: '操作', key: 'action', width: 90 },
];
const [Modal, modalApi] = useVbenModal({
footer: false,
onOpenChange(isOpen) {
if (isOpen) {
const data = modalApi.getData<Record<string, any>>() || {};
orders.value = data.orders || [];
title.value = data.title || '关联订单';
}
},
});
defineExpose({
open(payload: { orders?: Record<string, any>[]; title?: string }) {
modalApi.setData(payload);
modalApi.open();
},
});
</script>
<template>
<Modal class="w-[720px]" :title="title">
<Table
v-if="orders.length > 0"
:columns="columns"
:data-source="orders"
:pagination="false"
row-key="id"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<Button type="link" size="small" @click="emit('viewOrder', record)">
订单详情
</Button>
</template>
</template>
</Table>
<Empty v-else description="该记录没有关联业务订单(自定义金额提现不会核销具体订单)" />
</Modal>
</template>

View File

@@ -0,0 +1,133 @@
<script lang="ts" setup>
/**
* 按单合计超过余额的确认弹窗(一级申请的二级弹窗)
* 与申请弹窗同级挂载zIndex 1500 叠在申请之上点打款单号再开页面级进度弹窗2000
*/
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
const requested = ref(0);
const balance = ref(0);
const gap = ref(0);
const applies = ref<Record<string, any>[]>([]);
const openApplyProgress = ref<(id: number) => void>();
const onAccept = ref<() => void>();
const [Modal, modalApi] = useVbenModal({
title: '按单金额超过可提现余额',
class: 'w-[560px]',
draggable: true,
confirmText: '按余额申请',
cancelText: '暂时不提现',
centered: true,
// 必须高于申请提现(1000),否则会像截图那样被挡在后面;进度详情用 3000
zIndex: 2000,
onConfirm() {
onAccept.value?.();
modalApi.close();
},
onOpenChange(isOpen) {
if (!isOpen) return;
const data = modalApi.getData<Record<string, any>>() || {};
requested.value = Number(data.requested || 0);
balance.value = Number(data.balance || 0);
gap.value = Number(data.gap || requested.value - balance.value);
applies.value = data.applies || [];
openApplyProgress.value = data.openApplyProgress;
onAccept.value = data.onAccept;
},
});
function formatMoney(val: number | string) {
return Number(val || 0).toFixed(2);
}
function openApply(row: Record<string, any>) {
const id = Number(row.id || 0);
if (id > 0) {
openApplyProgress.value?.(id);
}
}
</script>
<template>
<Modal>
<div class="gap-confirm">
<p>
按单可提合计 <b>¥{{ formatMoney(requested) }}</b>
超过可提现余额 <b>¥{{ formatMoney(balance) }}</b>差额
<b>¥{{ formatMoney(gap) }}</b>
</p>
<p>
确认后将按可提现余额 <b>¥{{ formatMoney(balance) }}</b> 直接申请取消则暂时不提现
</p>
<p v-if="applies.length > 0" class="hint">
可能原因是这些提现申请使用了自定义金额点击打款单号可看进度
</p>
<p v-else class="hint">可能原因是历史自定义提现未按单核销</p>
<ul v-if="applies.length > 0" class="apply-list">
<li v-for="row in applies" :key="row.id">
<button type="button" class="order-btn" @click="openApply(row)">
{{ row.order_no }}
</button>
<span class="meta">
¥{{ formatMoney(row.apply_cash) }} · {{ row.apply_time || '-' }}
· {{ row.check_status_txt || '-' }}
· {{ row.dakuan_status_txt || '-' }}
</span>
</li>
</ul>
</div>
</Modal>
</template>
<style scoped>
.gap-confirm {
font-size: 13px;
line-height: 1.6;
color: hsl(var(--foreground));
}
.gap-confirm p {
margin: 0 0 8px;
}
.hint {
color: hsl(var(--muted-foreground));
}
.apply-list {
max-height: 220px;
margin: 0;
padding: 0;
overflow: auto;
list-style: none;
}
.apply-list li {
display: flex;
flex-wrap: wrap;
gap: 6px 10px;
align-items: baseline;
padding: 6px 0;
border-bottom: 1px solid hsl(var(--border));
}
.order-btn {
padding: 0;
border: 0;
background: none;
color: hsl(var(--primary));
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
cursor: pointer;
}
.order-btn:hover {
text-decoration: underline;
}
.meta {
color: hsl(var(--muted-foreground));
}
</style>

View File

@@ -13,6 +13,7 @@ import { getAdminAccountBalance, getMyCard } from '#/views/system/admin/api';
import AccountInfoCard from './account-info-card.vue';
import SaveMyCard from './card-modal.vue';
import GapConfirmModal from './gap-confirm-modal.vue';
import FormModalDemo from './withdrawal-modal.vue';
defineOptions({
@@ -34,6 +35,11 @@ const isStoreUser = computed(() => userType.value === 1);
/** 平台账号需展示累计代付手续费 */
const isPlatformUser = computed(() => userType.value === 2);
const emit = defineEmits<{
statClick: [key: string];
viewApply: [id: number];
}>();
const balance = ref(0);
const pendingEarnings = ref(0);
const settledEarnings = ref(0);
@@ -42,6 +48,11 @@ const withdrawnAmount = ref(0);
const withdrawnFrozenAmount = ref(0);
const frozenAmount = ref(0);
const fee = ref(0);
const tcmBalance = ref(0);
const westernBalance = ref(0);
const otherBalance = ref(0);
const customGap = ref(0);
const customGapApplies = ref<Record<string, any>[]>([]);
const myCard = ref<Record<string, any> | null>(null);
/**
@@ -59,6 +70,11 @@ function showCard() {
withdrawnFrozenAmount.value = res.withdrawn_frozen;
frozenAmount.value = res.frozen;
fee.value = res.charge;
tcmBalance.value = Number(res.tcm_balance || 0);
westernBalance.value = Number(res.western_balance || 0);
otherBalance.value = Number(res.other_balance || 0);
customGap.value = Number(res.custom_gap || 0);
customGapApplies.value = res.custom_gap_applies || [];
});
}
@@ -74,10 +90,28 @@ showCard();
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
zIndex: 1000,
});
const [saveMyCardModal, saveMyCardApi] = useVbenModal({
connectedComponent: SaveMyCard,
});
/** 差额确认与申请弹窗同级挂载zIndex 内外都写,避免 connectedComponent 吃掉内部层级 */
const [GapConfirmModalComp, gapConfirmModalApi] = useVbenModal({
connectedComponent: GapConfirmModal,
zIndex: 2000,
});
function openApplyProgress(id: number) {
emit('viewApply', id);
}
function openGapConfirm(data: Record<string, any>) {
gapConfirmModalApi.setData({
...data,
openApplyProgress,
});
gapConfirmModalApi.open();
}
function refresh() {
message.success('刷新成功');
@@ -87,9 +121,13 @@ function refresh() {
const showModal = (data = {}) => {
formModalApi.setData({
values: data,
balance,
balance: balance.value,
customGap: customGap.value,
customGapApplies: customGapApplies.value,
gridApi: props.gridApi,
showCard,
openGapConfirm,
openApplyProgress,
});
formModalApi.open();
};
@@ -108,6 +146,7 @@ const showCardModal = () => {
<template>
<div>
<FormModal />
<GapConfirmModalComp />
<saveMyCardModal />
<AccountInfoCard
:my-card="myCard"
@@ -120,9 +159,16 @@ const showCardModal = () => {
:withdrawn-frozen-amount="withdrawnFrozenAmount"
:frozen-amount="frozenAmount"
:fee="fee"
:tcm-balance="tcmBalance"
:western-balance="westernBalance"
:other-balance="otherBalance"
:custom-gap="customGap"
:custom-gap-applies="customGapApplies"
@apply="showModal"
@edit-card="showCardModal"
@refresh="refresh"
@stat-click="emit('statClick', $event)"
@view-apply="emit('viewApply', $event)"
/>
</div>
</template>

View File

@@ -0,0 +1,103 @@
<script lang="ts" setup>
/**
* 可提订单明细抽屉:按中药/西药/其他下钻,并可查看业务订单
*/
import { ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { Button, Empty, Spin, Table } from 'ant-design-vue';
import { getWithdrawableOrders } from '#/views/finance/withdrawal/api';
const emit = defineEmits<{
viewOrder: [row: Record<string, any>];
}>();
const loading = ref(false);
const items = ref<Record<string, any>[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(15);
const category = ref(0);
const title = ref('可提订单');
const columns = [
{ title: '订单号', dataIndex: 'order_no', key: 'order_no', ellipsis: true },
{ title: '订单类型', dataIndex: 'order_type_txt', key: 'order_type_txt', width: 100 },
{ title: '分类', dataIndex: 'category_txt', key: 'category_txt', width: 80 },
{ title: '可提金额', dataIndex: 'remain', key: 'remain', width: 110 },
{ title: '结算时间', dataIndex: 'settled_at_txt', key: 'settled_at_txt', width: 170 },
{ title: '操作', key: 'action', width: 90 },
];
const [Drawer, drawerApi] = useVbenDrawer({
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<Record<string, any>>() || {};
category.value = Number(data.category || 0);
title.value = data.title || '可提订单';
page.value = 1;
loadList();
}
},
});
async function loadList() {
loading.value = true;
try {
const res = await getWithdrawableOrders({
page: page.value,
pageSize: pageSize.value,
category: category.value,
});
items.value = res?.items || [];
total.value = Number(res?.total || 0);
} finally {
loading.value = false;
}
}
function onPageChange(current: number, size: number) {
page.value = current;
pageSize.value = size;
loadList();
}
defineExpose({
open(payload: { category?: number; title?: string }) {
drawerApi.setData(payload);
drawerApi.open();
},
});
</script>
<template>
<Drawer class="w-[860px]" :title="title">
<Spin :spinning="loading">
<Table
v-if="items.length > 0 || loading"
:columns="columns"
:data-source="items"
:pagination="{
current: page,
pageSize,
total,
showSizeChanger: true,
onChange: onPageChange,
}"
row-key="order_id"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<Button type="link" size="small" @click="emit('viewOrder', record)">
订单详情
</Button>
</template>
</template>
</Table>
<Empty v-else description="暂无可提订单" />
</Spin>
</Drawer>
</template>

View File

@@ -1,79 +1,323 @@
<script lang="ts" setup>
import { ref } from 'vue';
/**
* 申请提现:按订单 / 按时间范围 / 自定义金额
* 差额确认、打款进度都走页面级二级弹窗connectedComponent + zIndex不嵌套、不用 confirm()
*/
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
import { Alert, Button, DatePicker, Input, message, Radio, RadioGroup, Select, Table } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createWithdrawalApplicationWithdrawal } from '#/views/finance/withdrawal/api';
import { modalFormProps } from '../config/form';
import { createWithdrawalApplicationWithdrawal, getWithdrawableOrders } from '#/views/finance/withdrawal/api';
const gridApi = ref();
const showCard = ref();
const openGapConfirm = ref<(data: Record<string, any>) => void>();
const openApplyProgress = ref<(id: number) => void>();
const maxAmount = ref(0);
const customGap = ref(0);
const customGapApplies = ref<Record<string, any>[]>([]);
const mode = ref(2);
const amount = ref('');
const category = ref(0);
const dateRange = ref<any[]>([]);
const orderLoading = ref(false);
const orderItems = ref<Record<string, any>[]>([]);
const selectedRowKeys = ref<string[]>([]);
const selectedRows = ref<Record<string, any>[]>([]);
const [Form, formApi] = useVbenForm(modalFormProps);
const modeOptions = [
{ label: '按订单', value: 2 },
{ label: '按时间范围', value: 3 },
{ label: '自定义金额', value: 1 },
];
const categoryOptions = [
{ label: '全部类型', value: 0 },
{ label: '中药', value: 1 },
{ label: '西药', value: 2 },
{ label: '其他', value: 3 },
];
const orderColumns = [
{ title: '订单号', dataIndex: 'order_no', key: 'order_no', ellipsis: true },
{ title: '分类', dataIndex: 'category_txt', key: 'category_txt', width: 80 },
{ title: '可提金额', dataIndex: 'remain', key: 'remain', width: 110 },
{ title: '结算时间', dataIndex: 'settled_at_txt', key: 'settled_at_txt', width: 170 },
];
const selectedSum = computed(() => {
return selectedRows.value.reduce((sum, row) => sum + Number(row.remain || 0), 0);
});
const rangeSum = computed(() => {
return orderItems.value.reduce((sum, row) => sum + Number(row.remain || 0), 0);
});
/** 当前模式即将申请的按单合计,用来判断要不要弹出差额确认 */
const requestedSum = computed(() => {
if (mode.value === 2) return selectedSum.value;
if (mode.value === 3) return rangeSum.value;
return 0;
});
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
zIndex: 1000,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
formApi.validate().then(async (e: any) => {
if (e.valid) {
const values = await formApi.getValues();
modalApi.setState({ loading: true, confirmLoading: true });
const submitApi = createWithdrawalApplicationWithdrawal;
submitApi(values)
.then(() => {
message.success('保存成功');
gridApi.value?.reload();
showCard.value();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
}
});
await formApi.validateAndSubmitForm();
const payload = buildPayload();
if (!payload) return;
if (mode.value !== 1 && requestedSum.value > Number(maxAmount.value) + 0.0001) {
openGapConfirm.value?.({
requested: requestedSum.value,
balance: Number(maxAmount.value || 0),
gap: requestedSum.value - Number(maxAmount.value || 0),
applies: customGapApplies.value,
onAccept: () => submitApply({ ...payload, accept_gap: 1 }),
});
return;
}
await submitApply(payload);
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
showCard.value = isOpen ? modalApi.getData()?.showCard : null;
const data = isOpen ? modalApi.getData<Record<string, any>>() || {} : {};
gridApi.value = isOpen ? data.gridApi : null;
showCard.value = isOpen ? data.showCard : null;
openGapConfirm.value = isOpen ? data.openGapConfirm : undefined;
openApplyProgress.value = isOpen ? data.openApplyProgress : undefined;
if (isOpen) {
const { values, balance } = modalApi.getData<Record<string, any>>();
if (values) {
maxAmount.value = balance;
formApi.setValues(values);
}
maxAmount.value = Number(data.balance || 0);
customGap.value = Number(data.customGap || 0);
customGapApplies.value = data.customGapApplies || [];
mode.value = 2;
amount.value = '';
category.value = 0;
dateRange.value = [];
selectedRowKeys.value = [];
selectedRows.value = [];
loadOrders({});
}
},
});
const setAmount = (amount: number) => {
formApi.setValues({ amount });
};
function rowKeyOf(row: Record<string, any>) {
return `${row.order_id}_${row.order_type}`;
}
async function loadOrders(extra: Record<string, any>) {
orderLoading.value = true;
try {
const res = await getWithdrawableOrders({
page: 1,
pageSize: 100,
category: category.value,
...extra,
});
orderItems.value = res?.items || [];
} finally {
orderLoading.value = false;
}
}
function onSelectChange(keys: (string | number)[], rows: Record<string, any>[]) {
selectedRowKeys.value = keys.map(String);
selectedRows.value = rows;
}
function previewRange() {
if (!dateRange.value || dateRange.value.length !== 2) {
message.warning('请选择时间范围');
return;
}
const start = dateRange.value[0];
const end = dateRange.value[1];
loadOrders({
start_at: start?.startOf?.('day')?.unix?.() || start?.unix?.() || 0,
end_at: end?.endOf?.('day')?.unix?.() || end?.unix?.() || 0,
});
}
function setAmount(val: number) {
amount.value = String(val);
}
function viewApplyProgress(id: number) {
openApplyProgress.value?.(id);
}
async function submitApply(payload: Record<string, any>) {
modalApi.setState({ loading: true, confirmLoading: true });
try {
await createWithdrawalApplicationWithdrawal(payload);
message.success('提交成功');
gridApi.value?.reload();
showCard.value?.();
modalApi.close();
} finally {
modalApi.setState({ loading: false, confirmLoading: false });
}
}
function buildPayload(): Record<string, any> | null {
if (mode.value === 1) {
const amt = Number(amount.value);
if (!amt || amt < 1) {
message.warning('请输入不小于 1 元的提现金额');
return null;
}
if (amt > maxAmount.value) {
message.warning('不能超过可提现余额');
return null;
}
return { mode: 1, amount: amt, is_custom: 1 };
}
if (mode.value === 2) {
if (selectedRows.value.length === 0) {
message.warning('请勾选要提现的订单');
return null;
}
return {
mode: 2,
order_ids: selectedRows.value.map((row) => ({
order_id: row.order_id,
order_type: row.order_type,
})),
};
}
if (!dateRange.value || dateRange.value.length !== 2) {
message.warning('请选择时间范围');
return null;
}
const start = dateRange.value[0];
const end = dateRange.value[1];
return {
mode: 3,
category: category.value,
start_at: start?.startOf?.('day')?.unix?.() || start?.unix?.() || 0,
end_at: end?.endOf?.('day')?.unix?.() || end?.unix?.() || 0,
};
}
</script>
<template>
<Modal class="w-[80%] h-[80%]" title="申请提现">
<Form />
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
<Button type="primary" @click="setAmount(1000)"> 1000 </Button>
<Button type="primary" @click="setAmount(3000)"> 3000 </Button>
<Button type="primary" @click="setAmount(5000)"> 5000 </Button>
<Button type="primary" @click="setAmount(10000)"> 10000 </Button>
<Button type="primary" @click="setAmount(30000)"> 30000 </Button>
<Button type="primary" @click="setAmount(50000)"> 50000 </Button>
<Button type="primary" @click="setAmount(100000)"> 100000 </Button>
<Button type="primary" @click="setAmount(maxAmount.value)">
全部提现
</Button>
<Modal class="w-[860px]" title="申请提现">
<Alert
v-if="Number(customGap) > 0"
type="warning"
show-icon
class="mb-3"
>
<template #message>
按单可提比账户余额多 ¥{{ Number(customGap).toFixed(2) }}点击打款单号可看申请审核打款进度
</template>
<template #description>
<div class="gap-applies">
<button
v-for="row in customGapApplies"
:key="row.id"
type="button"
class="gap-order"
@click="viewApplyProgress(Number(row.id))"
>
{{ row.order_no }}{{ row.apply_cash }}
</button>
<span v-if="customGapApplies.length === 0">历史自定义提现未按单核销</span>
</div>
</template>
</Alert>
<div class="mb-3 text-sm text-muted-foreground">
可提现余额 ¥{{ Number(maxAmount || 0).toFixed(2) }}
</div>
<RadioGroup v-model:value="mode" class="mb-4">
<Radio v-for="opt in modeOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</Radio>
</RadioGroup>
<template v-if="mode === 1">
<div class="mb-2 text-sm">提现金额</div>
<Input
v-model:value="amount"
type="number"
min="1"
placeholder="请输入提现金额"
/>
<div class="mt-4 grid grid-cols-2 gap-3 md:grid-cols-4">
<Button type="primary" @click="setAmount(1000)">1000</Button>
<Button type="primary" @click="setAmount(3000)">3000</Button>
<Button type="primary" @click="setAmount(5000)">5000</Button>
<Button type="primary" @click="setAmount(10000)">10000</Button>
<Button type="primary" @click="setAmount(30000)">30000</Button>
<Button type="primary" @click="setAmount(50000)">50000</Button>
<Button type="primary" @click="setAmount(100000)">100000</Button>
<Button type="primary" @click="setAmount(maxAmount)">全部提现</Button>
</div>
</template>
<template v-else-if="mode === 2">
<div class="mb-2 flex items-center justify-between">
<Select
v-model:value="category"
class="w-40"
:options="categoryOptions"
@change="loadOrders({})"
/>
<span class="text-sm">已选 {{ selectedRows.length }} 合计 ¥{{ selectedSum.toFixed(2) }}</span>
</div>
<Table
:columns="orderColumns"
:data-source="orderItems"
:loading="orderLoading"
:pagination="false"
:row-key="rowKeyOf"
:row-selection="{ selectedRowKeys, onChange: onSelectChange }"
size="small"
/>
</template>
<template v-else>
<div class="mb-3 flex flex-wrap items-center gap-2">
<DatePicker.RangePicker v-model:value="dateRange" />
<Select v-model:value="category" class="w-32" :options="categoryOptions" />
<Button type="primary" @click="previewRange">预览订单</Button>
<span class="text-sm"> {{ orderItems.length }} 合计 ¥{{ rangeSum.toFixed(2) }}</span>
</div>
<Table
:columns="orderColumns"
:data-source="orderItems"
:loading="orderLoading"
:pagination="false"
:row-key="rowKeyOf"
size="small"
/>
</template>
</Modal>
</template>
<style scoped>
.text-muted-foreground {
color: hsl(var(--muted-foreground));
}
.gap-applies {
display: flex;
flex-wrap: wrap;
gap: 6px 10px;
margin-top: 4px;
}
.gap-order {
padding: 0;
border: 0;
background: none;
color: hsl(var(--primary));
cursor: pointer;
}
.gap-order:hover {
text-decoration: underline;
}
</style>

View File

@@ -7,7 +7,7 @@ import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Spin, Tag } from 'ant-design-vue';
import { Button, Spin, Steps, Table, Tag } from 'ant-design-vue';
import { getChargeCashPayRecordDetailApi } from '#/views/finance/charge-cash-pay-record/api';
import { getWithdrawalApplicationInfo } from '#/views/finance/withdrawal/api';
@@ -19,15 +19,22 @@ defineOptions({
const loading = ref(false);
/** 归一化后的展示数据 */
const view = ref<Record<string, any> | null>(null);
/** 页面级打开订单详情(不嵌套在本弹窗内,避免层级被挡住) */
const openOrderDetail = ref<(row: Record<string, any>) => void>();
const [Modal, modalApi] = useVbenModal({
footer: false,
draggable: true,
// 点打款单号打开:盖住申请(1000)和差额确认(2000)
zIndex: 3000,
onOpenChange(isOpen) {
if (isOpen) {
const data = modalApi.getData<Record<string, any>>() || {};
openOrderDetail.value = data.openOrderDetail;
loadDetail();
} else {
view.value = null;
openOrderDetail.value = undefined;
}
},
});
@@ -45,6 +52,94 @@ const checkStatusColor = computed(() => {
return 'warning';
});
/**
* 申请 → 审核 → 打款 → 到账
* 按 check_status / dakuan_status 标 finish/process/error/wait点打款单号就能看见走到哪一步
*/
const progressItems = computed(() => {
const v = view.value;
if (!v) return [];
return buildWithdrawProgress(v);
});
const relatedOrders = computed(() => {
const list = view.value?.orders;
return Array.isArray(list) ? list : [];
});
const relatedOrderColumns = [
{ title: '订单号', dataIndex: 'order_no', key: 'order_no', ellipsis: true },
{ title: '类型', dataIndex: 'order_type_txt', key: 'order_type_txt', width: 90 },
{ title: '分类', dataIndex: 'category_txt', key: 'category_txt', width: 70 },
{ title: '金额', dataIndex: 'amount', key: 'amount', width: 110, align: 'right' as const },
{ title: '操作', key: 'action', width: 80 },
];
function buildWithdrawProgress(row: Record<string, any>) {
const check = Number(row.check_status ?? 0);
const dakuan = Number(row.dakuan_status ?? 0);
const apply = {
title: '申请',
description: row.apply_time || '已提交',
status: 'finish' as const,
};
let audit: Record<string, string>;
if (check === 1) {
audit = { title: '审核', description: '待审核', status: 'process' };
} else if (check === 2) {
audit = {
title: '审核',
description: row.check_time || row.check_status_txt || '审核通过',
status: 'finish',
};
} else if (check === 3) {
audit = {
title: '审核',
description: row.check_result || '已拒绝',
status: 'error',
};
} else if (check === 4) {
audit = {
title: '审核',
description: row.check_result || '提现失败',
status: 'error',
};
} else {
audit = { title: '审核', description: '待审核', status: 'wait' };
}
let pay: Record<string, string>;
let arrived: Record<string, string>;
if (check === 1) {
pay = { title: '打款', description: '待审核通过后打款', status: 'wait' };
arrived = { title: '到账', description: '尚未打款', status: 'wait' };
} else if (check === 3) {
pay = { title: '打款', description: '已拒绝,未打款', status: 'wait' };
arrived = { title: '到账', description: '未到账', status: 'wait' };
} else if (check === 4 || dakuan === -1) {
pay = {
title: '打款',
description: row.dakuan_time || row.dakuan_status_txt || '打款失败',
status: 'error',
};
arrived = { title: '到账', description: '未到账', status: 'error' };
} else if (dakuan === 1) {
pay = {
title: '打款',
description: row.dakuan_time || '打款成功',
status: 'finish',
};
arrived = {
title: '到账',
description: row.dakuan_time || '已到账',
status: 'finish',
};
} else {
pay = { title: '打款', description: '打款处理中', status: 'process' };
arrived = { title: '到账', description: '等待到账', status: 'wait' };
}
return [apply, audit, pay, arrived];
}
/**
* 按入口源加载apply 走提现详情charge 走代付详情再归一化
*/
@@ -97,6 +192,10 @@ function normalizeFromApply(res: Record<string, any>) {
charge_status: res?.charge_status ?? charge?.status,
charge_status_txt: res?.charge_status_txt,
charge_created_at: charge?.created_at,
orders: res?.orders || [],
tcm_amount: res?.tcm_amount,
western_amount: res?.western_amount,
other_amount: res?.other_amount,
};
}
@@ -123,6 +222,10 @@ function normalizeFromCharge(res: Record<string, any>) {
charge_status: res?.status,
charge_status_txt: res?.status_txt,
charge_created_at: res?.created_at,
orders: res?.orders || apply?.orders || [],
tcm_amount: apply?.tcm_amount,
western_amount: apply?.western_amount,
other_amount: apply?.other_amount,
};
}
@@ -131,6 +234,14 @@ function formatMoney(val: any) {
return Number.isFinite(n) ? n.toFixed(2) : '0.00';
}
/** 点订单号/详情:交给页面级弹窗,本弹窗不嵌套订单详情 */
function onViewOrder(record: Record<string, any>) {
if (typeof openOrderDetail.value === 'function') {
openOrderDetail.value(record);
return;
}
}
function chargeTagColor(status: number) {
if (status === 2) return 'success';
if (status === 3) return 'default';
@@ -139,7 +250,7 @@ function chargeTagColor(status: number) {
</script>
<template>
<Modal class="w-[640px]" title="提现与代付详情">
<Modal class="w-[720px]" title="提现与代付详情">
<Spin :spinning="loading">
<div v-if="view" class="relation-detail">
<div class="hero">
@@ -155,6 +266,11 @@ function chargeTagColor(status: number) {
</div>
</div>
<div class="section">
<div class="section-title">提现进度</div>
<Steps size="small" :items="progressItems" />
</div>
<div class="section">
<div class="section-title">提现申请</div>
<div class="rows">
@@ -176,6 +292,12 @@ function chargeTagColor(status: number) {
<span class="label">打款金额</span>
<span class="value">¥{{ formatMoney(view.true_cash) }}</span>
</div>
<div v-if="Number(view.tcm_amount || 0) || Number(view.western_amount || 0) || Number(view.other_amount || 0)" class="row">
<span class="label">/西/其他</span>
<span class="value">
¥{{ formatMoney(view.tcm_amount) }} / ¥{{ formatMoney(view.western_amount) }} / ¥{{ formatMoney(view.other_amount) }}
</span>
</div>
<div class="row">
<span class="label">申请时间</span>
<span class="value">{{ view.apply_time || '-' }}</span>
@@ -200,6 +322,48 @@ function chargeTagColor(status: number) {
</div>
</div>
<div class="section">
<div class="section-title">
关联订单
<span v-if="relatedOrders.length > 0" class="section-extra">
{{ relatedOrders.length }}
</span>
</div>
<Table
v-if="relatedOrders.length > 0"
:columns="relatedOrderColumns"
:data-source="relatedOrders"
:pagination="false"
:scroll="{ y: 240 }"
row-key="id"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'order_no'">
<Button
type="link"
size="small"
class="order-no-btn"
@click="onViewOrder(record)"
>
{{ record.order_no || '-' }}
</Button>
</template>
<template v-else-if="column.key === 'amount'">
<span class="value fee">¥{{ formatMoney(record.amount) }}</span>
</template>
<template v-else-if="column.key === 'action'">
<Button type="link" size="small" @click="onViewOrder(record)">
详情
</Button>
</template>
</template>
</Table>
<div v-else class="empty-orders">
该记录没有关联业务订单自定义金额提现不会核销具体订单
</div>
</div>
<div class="section section--fee">
<div class="section-title">代付手续费</div>
<div class="rows">
@@ -280,12 +444,20 @@ function chargeTagColor(status: number) {
}
.section-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
font-size: 13px;
font-weight: 600;
color: hsl(var(--foreground));
}
.section-extra {
font-weight: 400;
color: hsl(var(--muted-foreground));
}
.rows {
display: flex;
flex-direction: column;
@@ -329,4 +501,16 @@ function chargeTagColor(status: number) {
text-align: center;
color: hsl(var(--muted-foreground));
}
.empty-orders {
padding: 12px 0;
font-size: 13px;
text-align: center;
color: hsl(var(--muted-foreground));
}
.order-no-btn {
padding-inline: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
</style>

View File

@@ -30,8 +30,18 @@ export const gridOptions: VxeGridProps<RowType> = {
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{
field: 'order_no',
title: '打款订单',
width: 220,
slots: { default: 'order_no' },
},
{ field: 'user_id', title: '名称', slots: { default: 'user_id' } },
{ field: 'apply_cash', title: '申请金额' },
{ field: 'apply_cash', title: '申请金额', width: 110 },
{ field: 'mode_txt', title: '提现方式', width: 100 },
{ field: 'tcm_amount', title: '中药', width: 90 },
{ field: 'western_amount', title: '西药', width: 90 },
{ field: 'other_amount', title: '其他', width: 90 },
{ field: 'true_cash', title: '实际到账金额' },
{ field: 'display_charge_cash', title: '手续费' },
{ field: 'apply_time', title: '申请时间' },

View File

@@ -5,19 +5,20 @@
* 状态列统一用主题化状态胶囊StatusPill 样式),金额涨跌用 --success/--destructive 变量,全量适配暗色
*/
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { AnalysisChartsTabs, Page, useVbenModal } from '@vben/common-ui';
import { IconifyIcon as VbenIcon } from '@vben/icons';
import { FloatButton, FloatButtonGroup, message, Popover } from 'ant-design-vue';
import { Button, FloatButton, FloatButtonGroup, Popover } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import DetailModal from '#/views/business/order/product-order/components/detail.vue';
import ApplyOrdersModal from './components/apply-orders-modal.vue';
import SettlementDetailDrawer from './components/settlement-detail-drawer.vue';
import Statistics from './components/statistics.vue';
import WithdrawableOrderDrawer from './components/withdrawable-order-drawer.vue';
import WithdrawalRelationDetail from './components/withdrawal-relation-detail.vue';
import { getAccountChangeSearchOptions } from './config/account-change-search';
import { accountChangeGridOptions } from './config/account-change-table';
import { formOptions } from './config/search';
@@ -25,8 +26,10 @@ import { formOptions3 } from './config/settlement-log-search';
import { formOptions2 } from './config/settlement-search';
import { gridOptions2, gridOptions3 } from './config/settlement-table';
import { gridOptions } from './config/table';
import { useWithdrawOrderDetailModals } from './utils/use-withdraw-order-detail';
const router = useRouter();
const { OrderDetailModal, RegisterDetailModal, openOrderDetail } =
useWithdrawOrderDetailModals();
const [Grid, GridApi] = useVbenVxeGrid({
formOptions,
@@ -40,6 +43,10 @@ const [Grid2] = useVbenVxeGrid({
const settlementDetailDrawerRef =
ref<InstanceType<typeof SettlementDetailDrawer>>();
const withdrawableOrderDrawerRef =
ref<InstanceType<typeof WithdrawableOrderDrawer>>();
const applyOrdersModalRef = ref<InstanceType<typeof ApplyOrdersModal>>();
const activeTab = ref('trends');
const [Grid3] = useVbenVxeGrid({
formOptions: formOptions3,
@@ -52,34 +59,68 @@ const [Grid4] = useVbenVxeGrid({
gridOptions: accountChangeGridOptions,
});
const [OrderDetailModal, OrderDetailModalApi] = useVbenModal({
connectedComponent: DetailModal,
const [RelationDetailModal, relationDetailApi] = useVbenModal({
connectedComponent: WithdrawalRelationDetail,
});
/** 点打款单号看申请→审核→打款→到账 */
function openApplyProgress(id: number) {
if (!id) return;
relationDetailApi.setData({ id, source: 'apply', openOrderDetail });
relationDetailApi.open();
}
function showSettlementDetail(row: Record<string, any>) {
settlementDetailDrawerRef.value?.open(row);
}
function openOrderDetail(row: Record<string, any>) {
const orderType = Number(row.order_type || 0);
const orderId = Number(row.order_id || 0);
const orderNo = String(row.order_no || '');
if (orderType === 1 && orderId > 0) {
OrderDetailModalApi.setData({ id: orderId });
OrderDetailModalApi.open();
function showApplyOrders(row: Record<string, any>) {
const orders = row.orders || [];
if (orders.length === 1) {
openOrderDetail(orders[0]);
return;
}
applyOrdersModalRef.value?.open({
title: row.order_no ? `关联订单 · ${row.order_no}` : '关联订单',
orders,
});
}
if (orderType === 2 && orderNo) {
router.push({
path: '/business/register/list',
query: { order_no: orderNo },
});
function onAccountChangeOrder(row: Record<string, any>) {
const orders = row.orders || [];
if (orders.length > 1 || (row.source_type === 'withdraw' && orders.length > 0)) {
showApplyOrders(row);
return;
}
openOrderDetail(row);
}
message.info('暂不支持查看该类型订单详情');
function onStatClick(key: string) {
const categoryMap: Record<string, { category: number; title: string }> = {
balance: { category: 0, title: '全部可提订单' },
tcm: { category: 1, title: '中药可提订单' },
western: { category: 2, title: '西药可提订单' },
other: { category: 3, title: '其他可提订单' },
};
if (categoryMap[key]) {
withdrawableOrderDrawerRef.value?.open(categoryMap[key]);
return;
}
if (key === 'reviewing') {
activeTab.value = 'trends';
GridApi.formApi?.setValues({ check_status: 1 });
GridApi.query?.();
return;
}
if (key === 'withdrawn') {
activeTab.value = 'trends';
GridApi.formApi?.setValues({ check_status: 2 });
GridApi.query?.();
return;
}
if (key === 'pending') {
activeTab.value = 'visits';
}
}
const chartTabs = [
@@ -120,15 +161,39 @@ function dakuanStatusPill(status: number) {
title="提现管理"
>
<OrderDetailModal />
<Statistics v-show="isShow" :grid-api="GridApi" />
<RegisterDetailModal />
<RelationDetailModal />
<WithdrawableOrderDrawer
ref="withdrawableOrderDrawerRef"
@view-order="openOrderDetail"
/>
<ApplyOrdersModal ref="applyOrdersModalRef" @view-order="openOrderDetail" />
<Statistics
v-show="isShow"
:grid-api="GridApi"
@stat-click="onStatClick"
@view-apply="openApplyProgress"
/>
<div class="withdrawal-lists" style="min-height: 500px">
<AnalysisChartsTabs :tabs="chartTabs" class="mt-3">
<AnalysisChartsTabs v-model:value="activeTab" :tabs="chartTabs" class="mt-3">
<template #trends>
<div style="min-height: 500px">
<Grid>
<template #toolbar-actions></template>
<template #toolbar-tools></template>
<template #order_no="{ row }">
<Button
v-if="row.order_no"
type="link"
size="small"
class="order-no-btn"
@click="openApplyProgress(Number(row.id))"
>
{{ row.order_no }}
</Button>
<span v-else class="cell-muted">-</span>
</template>
<template #user_id="{ row }">
<span v-if="row.user_type === 1">{{ row.store?.name }}</span>
<span v-else-if="row.user_type === 2">萧康平台</span>
@@ -177,7 +242,23 @@ function dakuanStatusPill(status: number) {
}}</span>
</template>
<template #action="{ row }">
<TableAction :actions="[]" :drop-down-actions="[]" />
<TableAction
:actions="[
{
label: '提现进度',
type: 'link',
size: 'small',
onClick: openApplyProgress.bind(null, Number(row.id)),
},
{
label: '关联订单',
type: 'link',
size: 'small',
onClick: showApplyOrders.bind(null, row),
},
]"
:drop-down-actions="[]"
/>
</template>
</Grid>
</div>
@@ -261,8 +342,8 @@ function dakuanStatusPill(status: number) {
label: '订单详情',
type: 'link',
size: 'small',
ifShow: !!row.order_id,
onClick: openOrderDetail.bind(null, row),
ifShow: !!row.order_id || (row.orders && row.orders.length > 0),
onClick: onAccountChangeOrder.bind(null, row),
},
]"
:drop-down-actions="[]"
@@ -391,4 +472,9 @@ function dakuanStatusPill(status: number) {
.cell-muted {
color: hsl(var(--muted-foreground));
}
.order-no-btn {
padding-inline: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
</style>

View File

@@ -0,0 +1,89 @@
/**
* 提现相关页共用:页面级挂载商品订单 / 挂号订单详情弹窗
* 二级弹窗不嵌套在提现详情里zIndex 4000 盖住提现进度(3000)
*/
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { getRegisterListApi } from '#/views/business/order/register/api';
import RegisterOrderDetail from '#/views/business/order/register/components/RegisterDetailModal.vue';
import ProductOrderDetail from '#/views/business/order/product-order/components/detail.vue';
/** 叠在提现详情(3000)之上,避免详情被挡住 */
const ORDER_DETAIL_Z_INDEX = 4000;
/**
* 按关联订单行打开业务订单详情
* 商品单(order_type=1)走订单详情接口;挂号单(order_type=2)没有独立详情接口,按订单号拉列表行再开挂号弹窗
*/
export async function openWithdrawRelatedOrderDetail(
row: Record<string, any>,
apis: {
productModalApi: { setData: (data: Record<string, any>) => void; open: () => void };
registerModalApi: { setData: (data: Record<string, any>) => void; open: () => void };
},
) {
const orderType = Number(row.order_type || 0);
const orderId = Number(row.order_id || 0);
const orderNo = String(row.order_no || '');
if (orderType === 1 && orderId > 0) {
apis.productModalApi.setData({ id: orderId });
apis.productModalApi.open();
return;
}
if (orderType === 2 && (orderId > 0 || orderNo)) {
const hide = message.loading('加载挂号详情...', 0);
try {
const res = await getRegisterListApi({
...(orderId > 0 ? { id: orderId } : {}),
...(orderNo ? { order_no: orderNo } : {}),
page: 1,
pageSize: 20,
});
const items = res?.items || [];
const item =
items.find((it) => Number(it.id) === orderId) ||
items.find((it) => String(it.order_no) === orderNo) ||
items[0];
if (!item) {
message.info('未找到挂号订单');
return;
}
apis.registerModalApi.setData({ row: item });
apis.registerModalApi.open();
} finally {
hide();
}
return;
}
message.info('暂不支持查看该类型订单详情');
}
/** 提现 / 审核 / 代付页挂载订单详情弹窗,并把打开方法传给提现详情 */
export function useWithdrawOrderDetailModals() {
const [OrderDetailModal, productModalApi] = useVbenModal({
connectedComponent: ProductOrderDetail,
zIndex: ORDER_DETAIL_Z_INDEX,
});
const [RegisterDetailModal, registerModalApi] = useVbenModal({
connectedComponent: RegisterOrderDetail,
zIndex: ORDER_DETAIL_Z_INDEX,
});
async function openOrderDetail(row: Record<string, any>) {
await openWithdrawRelatedOrderDetail(row, {
productModalApi,
registerModalApi,
});
}
return {
OrderDetailModal,
RegisterDetailModal,
openOrderDetail,
};
}

View File

@@ -17,14 +17,19 @@ const props = withDefaults(defineProps<Props>(), {
tabs: () => [],
});
const defaultValue = computed(() => {
return props.tabs?.[0]?.value;
/** 受控当前 Tab未传时回落到第一项 */
const activeTab = defineModel<string>('value');
const currentTab = computed({
get: () => activeTab.value || props.tabs?.[0]?.value || '',
set: (val: string) => {
activeTab.value = val;
},
});
</script>
<template>
<div class="card-box w-full px-4 pt-3 pb-5">
<Tabs :default-value="defaultValue">
<Tabs :model-value="currentTab" @update:model-value="currentTab = String($event)">
<TabsList>
<template v-for="tab in tabs" :key="tab.label">
<TabsTrigger :value="tab.value"> {{ tab.label }} </TabsTrigger>