423 lines
13 KiB
Vue
423 lines
13 KiB
Vue
<script lang="ts" setup>
|
||
/**
|
||
* 门店详情 - 资金账户面板(余额 + 资金流水 + 账户变动)
|
||
* 仅超管/系统管理员在 StoreCardModal 中可见,数据按门店 ID 查询
|
||
* 口径说明:资金流水按履约店 store_id;账户变动/余额按分账利润归属店 user_id
|
||
*/
|
||
import type { Dayjs } from 'dayjs';
|
||
|
||
import { ref, watch } from 'vue';
|
||
|
||
import { DatePicker, Empty, Segmented, Spin, Table, Tag, message } from 'ant-design-vue';
|
||
|
||
import { monthToTodayRangeDayjs } from '#/utils/search-time-range';
|
||
import {
|
||
getStoreAccountChangeLogListApi,
|
||
getStoreBalanceApi,
|
||
getStoreFundWaterListApi,
|
||
} from '#/views/system/store/api';
|
||
|
||
const props = defineProps<{
|
||
storeId: number;
|
||
}>();
|
||
|
||
const RangePicker = DatePicker.RangePicker;
|
||
|
||
const balanceLoading = ref(false);
|
||
const balance = ref<Record<string, any> | null>(null);
|
||
|
||
const fundWaterLoading = ref(false);
|
||
const fundWaterList = ref<any[]>([]);
|
||
const fundWaterPage = ref(1);
|
||
const fundWaterPageSize = ref(10);
|
||
const fundWaterTotal = ref(0);
|
||
|
||
const changeLogLoading = ref(false);
|
||
const changeLogList = ref<any[]>([]);
|
||
const changeLogPage = ref(1);
|
||
const changeLogPageSize = ref(10);
|
||
const changeLogTotal = ref(0);
|
||
|
||
const fundWaterTime = ref<[Dayjs, Dayjs]>(monthToTodayRangeDayjs());
|
||
const changeLogTime = ref<[Dayjs, Dayjs]>(monthToTodayRangeDayjs());
|
||
|
||
/** 流水列表切换:资金流水 / 账户变动(用 Segmented 避免窄容器下 Tab 折叠成 ...) */
|
||
const activeListTab = ref<'fundWater' | 'changeLog'>('fundWater');
|
||
const changeLogLoaded = ref(false);
|
||
const listTabOptions = [
|
||
{ label: '资金流水', value: 'fundWater' },
|
||
{ label: '账户变动', value: 'changeLog' },
|
||
];
|
||
|
||
/** 余额概览卡片配置 */
|
||
const balanceCards = [
|
||
{ key: 'balance', label: '可提现余额' },
|
||
{ key: 'total', label: '累计收益' },
|
||
{ key: 'pending_earnings', label: '待结算' },
|
||
{ key: 'withdrawn', label: '已提现' },
|
||
{ key: 'withdrawn_frozen', label: '审核中' },
|
||
{ key: 'public_account_amount', label: '公账累计' },
|
||
{ key: 'tcm_balance', label: '中药可提' },
|
||
{ key: 'western_balance', label: '西药可提' },
|
||
{ key: 'other_balance', label: '其他可提' },
|
||
];
|
||
|
||
const fundWaterColumns = [
|
||
{ title: '金额', dataIndex: 'price', key: 'price', width: 110 },
|
||
{ title: '流水类型', dataIndex: 'type', key: 'type', width: 110 },
|
||
{ title: '订单类型', dataIndex: 'order_type', key: 'order_type', width: 110 },
|
||
// store_id 便于核对是否本店履约(委托复诊时流水店与利润店可能不同)
|
||
{ title: '门店ID', dataIndex: 'store_id', key: 'store_id', width: 90 },
|
||
{ title: '诊所', dataIndex: 'store_name', key: 'store_name', width: 120, ellipsis: true },
|
||
{ title: '订单号', dataIndex: 'order_no', key: 'order_no', ellipsis: true },
|
||
{ title: '医生', dataIndex: 'doctor_name', key: 'doctor_name', width: 100 },
|
||
{ title: '时间', dataIndex: 'created_at', key: 'created_at', width: 170 },
|
||
];
|
||
|
||
const changeLogColumns = [
|
||
{ title: '时间', dataIndex: 'created_at', key: 'created_at', width: 170 },
|
||
{ title: '来源', dataIndex: 'source_type_txt', key: 'source_type_txt', width: 100 },
|
||
{ title: '字段', dataIndex: 'field_name_txt', key: 'field_name_txt', width: 140 },
|
||
{ title: '变动前', dataIndex: 'before_amount', key: 'before_amount', width: 100 },
|
||
{ title: '变动后', dataIndex: 'after_amount', key: 'after_amount', width: 100 },
|
||
{ title: '变动值', dataIndex: 'change_amount', key: 'change_amount', width: 100 },
|
||
{ title: '主体ID', dataIndex: 'user_id', key: 'user_id', width: 90 },
|
||
{ title: '订单号', dataIndex: 'order_no', key: 'order_no', ellipsis: true },
|
||
{ title: '来源表', dataIndex: 'source_table_txt', key: 'source_table_txt', width: 110 },
|
||
{ title: '备注', dataIndex: 'remark', key: 'remark', ellipsis: true },
|
||
];
|
||
|
||
/** 时间范围转接口参数 */
|
||
function timeParam(range: [Dayjs, Dayjs]): [string, string] {
|
||
return [
|
||
range[0].format('YYYY-MM-DD 00:00:00'),
|
||
range[1].format('YYYY-MM-DD 23:59:59'),
|
||
];
|
||
}
|
||
|
||
function formatMoney(val: unknown) {
|
||
return `¥${Number(val || 0).toFixed(2)}`;
|
||
}
|
||
|
||
/** 拉取门店余额概览 */
|
||
async function loadBalance() {
|
||
if (!props.storeId) return;
|
||
balanceLoading.value = true;
|
||
try {
|
||
balance.value = await getStoreBalanceApi({ id: props.storeId });
|
||
} catch (e: any) {
|
||
balance.value = null;
|
||
message.error(e?.message || '加载余额失败');
|
||
} finally {
|
||
balanceLoading.value = false;
|
||
}
|
||
}
|
||
|
||
/** 拉取资金流水 */
|
||
async function loadFundWater(page = fundWaterPage.value) {
|
||
if (!props.storeId) return;
|
||
fundWaterLoading.value = true;
|
||
fundWaterPage.value = page;
|
||
try {
|
||
const res = await getStoreFundWaterListApi({
|
||
id: props.storeId,
|
||
page,
|
||
pageSize: fundWaterPageSize.value,
|
||
search_time: timeParam(fundWaterTime.value),
|
||
});
|
||
fundWaterList.value = (res?.items || []).map((item: any) => ({
|
||
...item,
|
||
doctor_name: item?.doctor_info?.name || '-',
|
||
store_name: item?.store?.name || '-',
|
||
}));
|
||
fundWaterTotal.value = Number(res?.total || 0);
|
||
} catch (e: any) {
|
||
fundWaterList.value = [];
|
||
fundWaterTotal.value = 0;
|
||
message.error(e?.message || '加载资金流水失败');
|
||
} finally {
|
||
fundWaterLoading.value = false;
|
||
}
|
||
}
|
||
|
||
/** 拉取账户变动 */
|
||
async function loadChangeLog(page = changeLogPage.value) {
|
||
if (!props.storeId) return;
|
||
changeLogLoading.value = true;
|
||
changeLogPage.value = page;
|
||
try {
|
||
const res = await getStoreAccountChangeLogListApi({
|
||
id: props.storeId,
|
||
page,
|
||
pageSize: changeLogPageSize.value,
|
||
search_time: timeParam(changeLogTime.value),
|
||
});
|
||
changeLogList.value = res?.items || [];
|
||
changeLogTotal.value = Number(res?.total || 0);
|
||
} catch (e: any) {
|
||
changeLogList.value = [];
|
||
changeLogTotal.value = 0;
|
||
message.error(e?.message || '加载账户变动失败');
|
||
} finally {
|
||
changeLogLoading.value = false;
|
||
}
|
||
}
|
||
|
||
/** 重置分页并加载余额与当前列表 Tab 数据 */
|
||
function loadAll() {
|
||
fundWaterPage.value = 1;
|
||
changeLogPage.value = 1;
|
||
changeLogLoaded.value = false;
|
||
activeListTab.value = 'fundWater';
|
||
loadBalance();
|
||
loadFundWater(1);
|
||
}
|
||
|
||
/** 列表切换:懒加载账户变动 */
|
||
function onListTabChange(value: string | number) {
|
||
activeListTab.value = value === 'changeLog' ? 'changeLog' : 'fundWater';
|
||
if (activeListTab.value === 'changeLog' && !changeLogLoaded.value) {
|
||
loadChangeLog(1);
|
||
changeLogLoaded.value = true;
|
||
}
|
||
}
|
||
|
||
/** 重置面板状态(弹窗关闭时由父组件调用) */
|
||
function reset() {
|
||
balance.value = null;
|
||
fundWaterList.value = [];
|
||
changeLogList.value = [];
|
||
fundWaterTotal.value = 0;
|
||
changeLogTotal.value = 0;
|
||
fundWaterPage.value = 1;
|
||
changeLogPage.value = 1;
|
||
fundWaterTime.value = monthToTodayRangeDayjs();
|
||
changeLogTime.value = monthToTodayRangeDayjs();
|
||
activeListTab.value = 'fundWater';
|
||
changeLogLoaded.value = false;
|
||
}
|
||
|
||
defineExpose({ loadAll, reset });
|
||
|
||
watch(
|
||
() => props.storeId,
|
||
(id) => {
|
||
if (id) loadAll();
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
function fundWaterTypeTag(type: string) {
|
||
if (type === 'refund') return { color: 'purple', text: '出账' };
|
||
if (type === 'enter') return { color: 'green', text: '入账' };
|
||
if (type === 'withdraw') return { color: 'orange', text: '提现' };
|
||
if (type === 'charge') return { color: 'red', text: '代付手续费' };
|
||
return { color: 'default', text: type || '-' };
|
||
}
|
||
|
||
function orderTypeTag(orderType: number) {
|
||
if (orderType === 1) return { color: 'purple', text: '产品订单' };
|
||
if (orderType === 2) return { color: 'green', text: '挂号订单' };
|
||
if (orderType === 3) return { color: 'blue', text: '问诊订单' };
|
||
return { color: 'default', text: '-' };
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="store-finance-panel pl-4">
|
||
<section class="finance-section">
|
||
<div class="section-title">余额概览</div>
|
||
<Spin :spinning="balanceLoading">
|
||
<div v-if="balance" class="balance-grid">
|
||
<div
|
||
v-for="card in balanceCards"
|
||
:key="card.key"
|
||
class="balance-card"
|
||
>
|
||
<span class="balance-card__label">{{ card.label }}</span>
|
||
<span class="balance-card__value">{{ formatMoney(balance[card.key]) }}</span>
|
||
</div>
|
||
</div>
|
||
<Empty v-else-if="!balanceLoading" description="暂无余额数据" />
|
||
</Spin>
|
||
</section>
|
||
|
||
<section class="finance-section finance-section--list">
|
||
<p class="finance-hint">
|
||
资金流水按订单履约店;账户变动/余额按分账利润归属店。委托复诊等场景两边主体可能不同,属业务口径而非串店。
|
||
</p>
|
||
<Segmented
|
||
v-model:value="activeListTab"
|
||
block
|
||
class="finance-list-segment"
|
||
:options="listTabOptions"
|
||
@change="onListTabChange"
|
||
/>
|
||
|
||
<div v-show="activeListTab === 'fundWater'">
|
||
<div class="list-toolbar">
|
||
<RangePicker
|
||
v-model:value="fundWaterTime"
|
||
format="YYYY-MM-DD"
|
||
@change="loadFundWater(1)"
|
||
/>
|
||
</div>
|
||
<Table
|
||
:columns="fundWaterColumns"
|
||
:data-source="fundWaterList"
|
||
:loading="fundWaterLoading"
|
||
:pagination="{
|
||
current: fundWaterPage,
|
||
pageSize: fundWaterPageSize,
|
||
total: fundWaterTotal,
|
||
showSizeChanger: true,
|
||
showTotal: (total: number) => `共 ${total} 条`,
|
||
}"
|
||
row-key="id"
|
||
size="small"
|
||
:scroll="{ x: 1000, y: 320 }"
|
||
@change="(pag: any) => {
|
||
fundWaterPageSize = pag.pageSize || fundWaterPageSize;
|
||
loadFundWater(pag.current || 1);
|
||
}"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'price'">
|
||
{{ formatMoney(record.price) }}
|
||
</template>
|
||
<template v-else-if="column.key === 'type'">
|
||
<Tag :color="fundWaterTypeTag(record.type).color">
|
||
{{ fundWaterTypeTag(record.type).text }}
|
||
</Tag>
|
||
</template>
|
||
<template v-else-if="column.key === 'order_type'">
|
||
<Tag :color="orderTypeTag(Number(record.order_type)).color">
|
||
{{ orderTypeTag(Number(record.order_type)).text }}
|
||
</Tag>
|
||
</template>
|
||
</template>
|
||
</Table>
|
||
</div>
|
||
|
||
<div v-show="activeListTab === 'changeLog'">
|
||
<div class="list-toolbar">
|
||
<RangePicker
|
||
v-model:value="changeLogTime"
|
||
format="YYYY-MM-DD"
|
||
@change="loadChangeLog(1)"
|
||
/>
|
||
</div>
|
||
<Table
|
||
:columns="changeLogColumns"
|
||
:data-source="changeLogList"
|
||
:loading="changeLogLoading"
|
||
:pagination="{
|
||
current: changeLogPage,
|
||
pageSize: changeLogPageSize,
|
||
total: changeLogTotal,
|
||
showSizeChanger: true,
|
||
showTotal: (total: number) => `共 ${total} 条`,
|
||
}"
|
||
row-key="id"
|
||
size="small"
|
||
:scroll="{ x: 1080, y: 320 }"
|
||
@change="(pag: any) => {
|
||
changeLogPageSize = pag.pageSize || changeLogPageSize;
|
||
loadChangeLog(pag.current || 1);
|
||
}"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template
|
||
v-if="
|
||
column.key === 'before_amount' ||
|
||
column.key === 'after_amount' ||
|
||
column.key === 'change_amount'
|
||
"
|
||
>
|
||
{{ formatMoney(record[column.key]) }}
|
||
</template>
|
||
</template>
|
||
</Table>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.store-finance-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
}
|
||
|
||
.finance-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: hsl(var(--foreground));
|
||
}
|
||
|
||
.finance-section--list {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
.finance-hint {
|
||
margin: 0 0 8px;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
color: hsl(var(--muted-foreground));
|
||
}
|
||
|
||
.finance-list-segment {
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.list-toolbar {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.balance-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.balance-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
padding: 14px 16px;
|
||
border: 1px solid hsl(var(--primary) / 30%);
|
||
border-radius: 10px;
|
||
background: hsl(var(--card, var(--background)));
|
||
box-shadow: 0 0 6px hsl(var(--primary) / 18%);
|
||
}
|
||
|
||
.balance-card__label {
|
||
font-size: 12px;
|
||
color: hsl(var(--muted-foreground));
|
||
}
|
||
|
||
.balance-card__value {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: hsl(var(--foreground));
|
||
}
|
||
</style>
|