1. 订单追溯、模拟支付(仅限本地环境)
Some checks failed
Close stale issues / stale (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled

This commit is contained in:
李琦
2026-06-02 08:20:11 +08:00
parent 6e5dec9f92
commit ee25ed00fb
14 changed files with 1191 additions and 143 deletions

View File

@@ -0,0 +1,31 @@
import { requestClient } from '#/api/request';
const prefix = 'order/';
export async function simulatePayApi(data: {
order_id: number;
order_type: 'product' | 'register';
}) {
return requestClient.post<any>(`${prefix}simulate-pay`, data);
}
export async function getOrderTraceApi(params: {
order_id: number;
scene: 'product' | 'register';
}) {
return requestClient.get<any>(`${prefix}trace`, { params });
}
export async function getOrderLedgerDetailApi(params: {
order_id: number;
order_type: number;
scope?: 'all' | 'platform' | 'store';
}) {
return requestClient.get<any>(`${prefix}ledger-detail`, { params });
}
export async function getOrderReconciliationDetailApi(params: {
product_order_id: number;
}) {
return requestClient.get<any>(`${prefix}reconciliation-detail`, { params });
}

View File

@@ -0,0 +1,467 @@
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import {
Button,
Descriptions,
Image,
Space,
Spin,
Table,
Tabs,
Tag,
Timeline,
} from 'ant-design-vue';
import {
getOrderLedgerDetailApi,
getOrderReconciliationDetailApi,
getOrderTraceApi,
} from '#/views/business/order/api/order-ops';
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
import DetailModal from '../product-order/components/detail.vue';
import ReconciliationDetailTable from './reconciliation-detail-table.vue';
defineOptions({ name: 'OrderTraceDrawer' });
type LedgerScope = 'all' | 'platform' | 'store';
const router = useRouter();
const userStore = useUserStore();
const loading = ref(false);
const ledgerLoading = ref(false);
const reconciliationLoading = ref(false);
const activeTab = ref('timeline');
const ledgerScope = ref<LedgerScope>('all');
const ledgerSubTab = ref<'product' | 'register'>('product');
const traceData = ref<Record<string, any> | null>(null);
const ledgerProduct = ref<Record<string, any> | null>(null);
const ledgerRegister = ref<Record<string, any> | null>(null);
const reconciliationData = ref<Record<string, any> | null>(null);
const loadedTabs = ref(new Set<string>());
const isPlatformAdmin = computed(
() => userStore?.userInfo?.roles?.user_type === 2,
);
const ledgerColumns = [
{ title: '费用类型', dataIndex: 'fee_type_txt', key: 'fee_type_txt' },
{ title: '药品图片', dataIndex: 'drug_image', key: 'drug_image', width: 80 },
{ title: '药品名称', dataIndex: 'drug_name', key: 'drug_name' },
{ title: '药品编号', dataIndex: 'drug_number', key: 'drug_number' },
{ title: '药品规格', dataIndex: 'specification', key: 'specification' },
{ title: '分账对象', dataIndex: 'user_type_txt', key: 'user_type_txt' },
{ title: '门店', dataIndex: ['store', 'name'], key: 'store_name' },
{ title: '分账金额', dataIndex: 'money', key: 'money' },
{ title: '结算状态', dataIndex: 'status_txt', key: 'status_txt' },
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
];
const currentLedgerData = computed(() =>
ledgerSubTab.value === 'product' ? ledgerProduct.value : ledgerRegister.value,
);
function formatDrugCell(value?: string) {
return value?.trim() ? value : '-';
}
function resetDrawerState() {
traceData.value = null;
ledgerProduct.value = null;
ledgerRegister.value = null;
reconciliationData.value = null;
loadedTabs.value = new Set<string>();
activeTab.value = 'timeline';
ledgerScope.value = isPlatformAdmin.value ? 'all' : 'store';
ledgerSubTab.value = 'product';
}
const [Drawer, drawerApi] = useVbenDrawer({
class: 'w-[80%]',
placement: 'right',
showConfirmButton: false,
showCancelButton: false,
destroyOnClose: true,
onOpenChange(isOpen) {
if (isOpen) {
ledgerScope.value = isPlatformAdmin.value ? 'all' : 'store';
void loadTrace();
} else {
resetDrawerState();
}
},
});
const [PrescriptionDetailModal, PrescriptionDetailModalApi] = useVbenModal({
connectedComponent: PrescriptionDetail,
});
const [OrderDetailModal, OrderDetailModalApi] = useVbenModal({
connectedComponent: DetailModal,
});
async function loadTrace() {
const data = drawerApi.getData<{
order_id: number;
order_no?: string;
scene: 'product' | 'register';
}>();
if (!data?.order_id) {
return;
}
loading.value = true;
drawerApi.setState({ loading: true });
try {
traceData.value = await getOrderTraceApi({
scene: data.scene,
order_id: data.order_id,
});
if (traceData.value?.meta?.is_platform_admin === false) {
ledgerScope.value = 'store';
}
} finally {
loading.value = false;
drawerApi.setState({ loading: false });
}
}
async function loadLedgerDetail(force = false) {
if (!force && loadedTabs.value.has(`ledger:${ledgerScope.value}`)) {
return;
}
const productOrderId = traceData.value?.links?.product_order_id;
const registerId = traceData.value?.links?.register_id;
ledgerLoading.value = true;
try {
const requests: Promise<any>[] = [];
if (productOrderId) {
requests.push(
getOrderLedgerDetailApi({
order_id: productOrderId,
order_type: 1,
scope: ledgerScope.value,
}).then((res) => {
ledgerProduct.value = res;
}),
);
} else {
ledgerProduct.value = null;
}
if (registerId) {
requests.push(
getOrderLedgerDetailApi({
order_id: registerId,
order_type: 2,
scope: ledgerScope.value,
}).then((res) => {
ledgerRegister.value = res;
}),
);
} else {
ledgerRegister.value = null;
}
await Promise.all(requests);
loadedTabs.value.add(`ledger:${ledgerScope.value}`);
} finally {
ledgerLoading.value = false;
}
}
async function loadReconciliationDetail() {
if (loadedTabs.value.has('reconciliation')) {
return;
}
const productOrderId = traceData.value?.links?.product_order_id;
if (!productOrderId) {
reconciliationData.value = {
items: [],
sum_sales_price: '0',
sum_supply_price: '0',
reconciliation_hint:
traceData.value?.summary?.reconciliation_hint || '暂无关联商品订单',
};
loadedTabs.value.add('reconciliation');
return;
}
reconciliationLoading.value = true;
try {
reconciliationData.value = await getOrderReconciliationDetailApi({
product_order_id: productOrderId,
});
loadedTabs.value.add('reconciliation');
} finally {
reconciliationLoading.value = false;
}
}
async function handleMainTabChange(key: string) {
activeTab.value = key;
if (key === 'ledger') {
await loadLedgerDetail();
} else if (key === 'reconciliation') {
await loadReconciliationDetail();
}
}
async function handleLedgerScopeChange(scope: LedgerScope) {
ledgerScope.value = scope;
loadedTabs.value.delete(`ledger:${scope}`);
await loadLedgerDetail(true);
}
function openPrescriptionDetail() {
const id = traceData.value?.links?.prescription_id;
if (!id) {
return;
}
PrescriptionDetailModalApi.setData({ values: id });
PrescriptionDetailModalApi.open();
}
function openOrderDetail() {
const id = traceData.value?.links?.product_order_id;
if (!id) {
return;
}
OrderDetailModalApi.setData({ id });
OrderDetailModalApi.open();
}
function goAuditPrescription() {
const id = traceData.value?.links?.prescription_id;
if (!id) {
return;
}
void router.push({
path: '/pharmacist/review-prescription',
query: { prescription_id: String(id) },
});
}
function prescriptionStatusTag() {
const summary = traceData.value?.summary;
if (!summary?.prescription_id) {
return null;
}
const auditNode = traceData.value?.timeline?.find(
(n: Record<string, any>) =>
n.key === 'prescription_audited' || n.key === 'prescription_rejected',
);
if (auditNode) {
return auditNode.status;
}
return '待审核';
}
const reconciliationHint = computed(
() =>
reconciliationData.value?.reconciliation_hint ||
traceData.value?.summary?.reconciliation_hint ||
'',
);
watch(isPlatformAdmin, (val) => {
if (!val) {
ledgerScope.value = 'store';
}
});
</script>
<template>
<Drawer title="订单溯源">
<Spin :spinning="loading">
<div v-if="traceData" class="flex flex-col gap-4">
<Descriptions bordered :column="2" size="small">
<Descriptions.Item label="商品订单号">
{{ traceData.summary?.product_order_no || '—' }}
</Descriptions.Item>
<Descriptions.Item label="挂号订单号">
{{ traceData.summary?.register_order_no || '—' }}
</Descriptions.Item>
<Descriptions.Item label="处方号">
{{ traceData.summary?.prescription_no || '—' }}
</Descriptions.Item>
<Descriptions.Item label="结算状态">
{{ traceData.summary?.is_settled === 1 ? '已结算' : '未结算' }}
</Descriptions.Item>
</Descriptions>
<Space wrap>
<Button
v-if="traceData.links?.prescription_id"
size="small"
type="primary"
@click="openPrescriptionDetail"
>
处方详情
</Button>
<Button
v-if="traceData.links?.product_order_id"
size="small"
@click="openOrderDetail"
>
订单详情
</Button>
<Button
v-if="traceData.links?.prescription_id && prescriptionStatusTag() === '待审核'"
size="small"
danger
@click="goAuditPrescription"
>
去审方
</Button>
<Tag v-else-if="prescriptionStatusTag()" color="green">
审方{{ prescriptionStatusTag() }}
</Tag>
</Space>
<Tabs v-model:active-key="activeTab" @change="(key) => handleMainTabChange(String(key))">
<Tabs.TabPane key="timeline" tab="溯源时间线">
<Timeline v-if="traceData.timeline?.length">
<Timeline.Item
v-for="node in traceData.timeline"
:key="node.key"
>
<div class="font-medium">{{ node.title }}</div>
<div class="text-sm text-gray-500">{{ node.time }}</div>
<div v-if="node.status" class="text-sm">
<Tag>{{ node.status }}</Tag>
</div>
<div v-if="node.extra" class="mt-1 text-sm text-gray-600">
{{ node.extra }}
</div>
</Timeline.Item>
</Timeline>
<div v-else class="py-6 text-center text-gray-500">暂无溯源记录</div>
</Tabs.TabPane>
<Tabs.TabPane key="ledger" tab="分账明细">
<Spin :spinning="ledgerLoading">
<Tabs
v-if="isPlatformAdmin"
v-model:active-key="ledgerScope"
type="card"
class="mb-3"
@change="(key) => handleLedgerScopeChange(key as LedgerScope)"
>
<Tabs.TabPane key="all" tab="全部" />
<Tabs.TabPane key="store" tab="门店" />
<Tabs.TabPane key="platform" tab="平台" />
</Tabs>
<Tabs v-model:active-key="ledgerSubTab" type="card">
<Tabs.TabPane key="product" tab="商品订单分账">
<div class="mb-2 text-sm">
合计分账{{ ledgerProduct?.sum_money ?? '0' }}
</div>
<Table
:columns="ledgerColumns"
:data-source="ledgerProduct?.items ?? []"
:pagination="false"
row-key="id"
size="small"
>
<template #bodyCell="{ column, record, text }">
<template v-if="column.key === 'drug_image'">
<Image
v-if="record.drug_image"
:src="record.drug_image"
:width="48"
:height="48"
class="object-cover"
/>
<span v-else>-</span>
</template>
<template
v-else-if="
column.key === 'drug_name' ||
column.key === 'drug_number' ||
column.key === 'specification'
"
>
{{ formatDrugCell(text) }}
</template>
</template>
</Table>
<div
v-if="!ledgerProduct?.items?.length && !ledgerLoading"
class="py-6 text-center text-gray-500"
>
暂无分账记录
</div>
</Tabs.TabPane>
<Tabs.TabPane key="register" tab="挂号订单分账">
<div class="mb-2 text-sm">
合计分账:{{ ledgerRegister?.sum_money ?? '0' }}
</div>
<Table
:columns="ledgerColumns"
:data-source="ledgerRegister?.items ?? []"
:pagination="false"
row-key="id"
size="small"
>
<template #bodyCell="{ column, record, text }">
<template v-if="column.key === 'drug_image'">
<Image
v-if="record.drug_image"
:src="record.drug_image"
:width="48"
:height="48"
class="object-cover"
/>
<span v-else>-</span>
</template>
<template
v-else-if="
column.key === 'drug_name' ||
column.key === 'drug_number' ||
column.key === 'specification'
"
>
{{ formatDrugCell(text) }}
</template>
</template>
</Table>
<div
v-if="!ledgerRegister?.items?.length && !ledgerLoading"
class="py-6 text-center text-gray-500"
>
暂无分账记录
</div>
</Tabs.TabPane>
</Tabs>
</Spin>
</Tabs.TabPane>
<Tabs.TabPane key="reconciliation" tab="对账明细">
<Spin :spinning="reconciliationLoading">
<ReconciliationDetailTable
:items="reconciliationData?.items ?? []"
:sum-sales-price="reconciliationData?.sum_sales_price"
:sum-supply-price="reconciliationData?.sum_supply_price"
:summary="reconciliationData?.summary"
:hint="reconciliationHint"
/>
</Spin>
</Tabs.TabPane>
</Tabs>
</div>
</Spin>
<PrescriptionDetailModal />
<OrderDetailModal />
</Drawer>
</template>

View File

@@ -0,0 +1,154 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Table, Tag } from 'ant-design-vue';
defineOptions({ name: 'ReconciliationDetailTable' });
const props = defineProps<{
hint?: string;
items: Record<string, any>[];
sumSalesPrice?: string;
sumSupplyPrice?: string;
summary?: Record<string, any> | null;
}>();
const columns = [
{ title: '药品名称', dataIndex: 'drug_name', key: 'drug_name' },
{ title: '药品编号', dataIndex: 'drug_number', key: 'drug_number' },
{ title: '数量', dataIndex: 'number', key: 'number', width: 80 },
{ title: '销售总价', dataIndex: 'total_price', key: 'total_price' },
{ title: '供货总价', dataIndex: 'total_buy_price', key: 'total_buy_price' },
{ title: '状态', dataIndex: 'status_txt', key: 'status_txt', width: 90 },
{ title: '对账时间', dataIndex: 'created_at', key: 'created_at', width: 170 },
];
function hasValue(v: unknown): boolean {
if (v === null || v === undefined || v === '') return false;
const n = Number(v);
return !Number.isNaN(n) && n !== 0;
}
const displaySummary = computed(() => props.summary ?? null);
const totalSales = computed(
() => displaySummary.value?.total_sales_price ?? props.sumSalesPrice,
);
const totalSupply = computed(
() => displaySummary.value?.total_supply_price ?? props.sumSupplyPrice,
);
const showHerbal = computed(() => {
const s = displaySummary.value;
if (!s) return false;
return hasValue(s.herbal_sales) || hasValue(s.herbal_sales_price) || hasValue(s.herbal_supply_price);
});
const showMedicine = computed(() => {
const s = displaySummary.value;
if (!s) return false;
return hasValue(s.medicine_sales) || hasValue(s.medicine_sales_price) || hasValue(s.medicine_supply_price);
});
const showServicePackage = computed(() => {
const s = displaySummary.value;
if (!s) return false;
return (
hasValue(s.service_package_sales)
|| hasValue(s.service_package_sales_price)
|| hasValue(s.service_package_supply_price)
);
});
const showOtherFees = computed(() => {
const s = displaySummary.value;
if (!s) return false;
return hasValue(s.express_price) || hasValue(s.process_price) || hasValue(s.treatment_price);
});
const showRegistration = computed(() => hasValue(displaySummary.value?.registration_price));
</script>
<template>
<div>
<div v-if="displaySummary" class="mb-4">
<div class="mb-3 flex flex-wrap gap-4 text-sm font-medium">
<span>总销售额{{ totalSales }}</span>
<span>总供货额{{ totalSupply }}</span>
<span v-if="showRegistration">
挂号费{{ displaySummary?.registration_price }}
<template v-if="displaySummary?.register_order_no">
{{ displaySummary.register_order_no }}
</template>
</span>
</div>
<div class="mb-3 grid grid-cols-1 gap-3 md:grid-cols-2">
<div v-if="showHerbal" class="rounded border border-gray-200 p-3 dark:border-gray-700">
<div class="mb-2 text-sm font-medium">草药</div>
<div class="flex flex-col gap-1">
<Tag color="green">销售数量{{ displaySummary?.herbal_sales }}</Tag>
<Tag color="orange">销售总价{{ displaySummary?.herbal_sales_price }}</Tag>
<Tag color="red">供货总价{{ displaySummary?.herbal_supply_price }}</Tag>
</div>
</div>
<div v-if="showMedicine" class="rounded border border-gray-200 p-3 dark:border-gray-700">
<div class="mb-2 text-sm font-medium">西药</div>
<div class="flex flex-col gap-1">
<Tag color="green">销售数量{{ displaySummary?.medicine_sales }}</Tag>
<Tag color="orange">销售总价{{ displaySummary?.medicine_sales_price }}</Tag>
<Tag color="red">供货总价{{ displaySummary?.medicine_supply_price }}</Tag>
</div>
</div>
<div v-if="showServicePackage" class="rounded border border-gray-200 p-3 dark:border-gray-700">
<div class="mb-2 text-sm font-medium">服务包</div>
<div class="flex flex-col gap-1">
<Tag color="green">销售数量{{ displaySummary?.service_package_sales }}</Tag>
<Tag color="orange">销售总价{{ displaySummary?.service_package_sales_price }}</Tag>
<Tag color="red">供货总价{{ displaySummary?.service_package_supply_price }}</Tag>
</div>
</div>
<div v-if="showOtherFees" class="rounded border border-gray-200 p-3 dark:border-gray-700">
<div class="mb-2 text-sm font-medium">其他费用</div>
<div class="flex flex-col gap-1">
<Tag v-if="hasValue(displaySummary?.express_price)" color="green">
快递费{{ displaySummary?.express_price }}
</Tag>
<Tag v-if="hasValue(displaySummary?.process_price)" color="green">
加工费{{ displaySummary?.process_price }}
</Tag>
<Tag v-if="hasValue(displaySummary?.treatment_price)" color="green">
诊疗费{{ displaySummary?.treatment_price }}
</Tag>
</div>
</div>
</div>
</div>
<div v-else-if="sumSalesPrice != null" class="mb-3 flex gap-4 text-sm">
<span>销售总价合计{{ sumSalesPrice }}</span>
<span>供货总价合计{{ sumSupplyPrice }}</span>
</div>
<div v-if="items.length" class="mb-2 text-sm font-medium">药品明细</div>
<Table
:columns="columns"
:data-source="items"
:pagination="false"
row-key="id"
size="small"
/>
<div
v-if="!items.length && !displaySummary"
class="py-8 text-center text-gray-500 dark:text-gray-400"
>
{{ hint || '订单尚未结算或无对账记录' }}
</div>
<div
v-else-if="!items.length && displaySummary && !showHerbal && !showMedicine && !showServicePackage && !showOtherFees && !showRegistration"
class="py-4 text-center text-gray-500 dark:text-gray-400"
>
{{ hint || '暂无药品对账明细' }}
</div>
</div>
</template>

View File

@@ -1,11 +1,13 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { Card, Descriptions, Image, Tag, Timeline } from 'ant-design-vue';
import { Button, Card, Descriptions, Image, Space, Tag, Timeline } from 'ant-design-vue';
import {expressDetailByOrderId, getOrderInfo} from '../api';
import OrderTraceDrawer from '#/views/business/order/components/order-trace-drawer.vue';
import { expressDetailByOrderId, getOrderInfo } from '../api';
defineOptions({
name: 'DetailModal',
@@ -19,6 +21,22 @@ const expressDetail = ref({});
// 订单发货方式
const deliveryMethod = ref(-1);
const [TraceDrawer, traceDrawerApi] = useVbenDrawer({
connectedComponent: OrderTraceDrawer,
});
function openOrderTrace() {
if (!data.value?.id) {
return;
}
traceDrawerApi.setData({
scene: 'product',
order_id: data.value.id,
order_no: data.value.order_no,
});
traceDrawerApi.open();
}
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
@@ -87,6 +105,11 @@ const orderTypeMap = {
<template>
<Modal class="w-[80%]" title="订单详情">
<div v-if="data" class="flex flex-col gap-4">
<Space>
<Button type="primary" size="small" @click="openOrderTrace">
查看溯源
</Button>
</Space>
<h3 class="mt-4">订单信息</h3>
<Descriptions
:column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }"
@@ -319,6 +342,7 @@ const orderTypeMap = {
</div>
</div>
</Modal>
<TraceDrawer />
</template>
<style scoped>

View File

@@ -8,6 +8,7 @@ import {
AnalysisOverview,
type AnalysisOverviewItem,
Page,
useVbenDrawer,
useVbenModal,
} from '@vben/common-ui';
import { SvgCakeIcon } from '@vben/icons';
@@ -24,6 +25,8 @@ import {
saleAmountApi,
updateFreeShipping,
} from '#/views/business/order/product-order/api';
import { simulatePayApi } from '#/views/business/order/api/order-ops';
import OrderTraceDrawer from '#/views/business/order/components/order-trace-drawer.vue';
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
import DetailModal from './components/detail.vue';
@@ -179,6 +182,34 @@ const saleAmount = () => {
const [PrescriptionDetailModal, PrescriptionDetailModalApi] = useVbenModal({
connectedComponent: PrescriptionDetail,
});
const [TraceDrawer, traceDrawerApi] = useVbenDrawer({
connectedComponent: OrderTraceDrawer,
});
function openOrderTrace(row: Record<string, any>) {
traceDrawerApi.setData({
scene: 'product',
order_id: row.id,
order_no: row.order_no,
});
traceDrawerApi.open();
}
function handleSimulatePay(row: Record<string, any>) {
AntdModal.confirm({
title: '模拟支付',
content: `确认为订单 ${row.order_no} 模拟易票联支付回调?`,
okText: '确认',
cancelText: '取消',
onOk: async () => {
await simulatePayApi({ order_type: 'product', order_id: row.id });
message.success('模拟支付成功');
await gridApi.query();
},
});
}
const openPrescriptionDetail = (values) => {
// 打开西药处方模态框逻辑
PrescriptionDetailModalApi.setData({
@@ -319,6 +350,7 @@ const openOrderAmountVerify = () => {
</AntdModal>
<RefundModal />
<PrescriptionDetailModal />
<TraceDrawer />
<AnalysisOverview :items="overviewItems" :my-card="false" />
<Grid>
<template #toolbar-buttons>
@@ -475,6 +507,13 @@ const openOrderAmountVerify = () => {
// auth: ['order', 'sys:role:detail'],
onClick: infoModal.bind(null, row),
},
{
label: '溯源',
type: 'link',
icon: 'mdi:timeline-text',
size: 'small',
onClick: openOrderTrace.bind(null, row),
},
{
label: '处方',
type: 'link',
@@ -483,19 +522,27 @@ const openOrderAmountVerify = () => {
// auth: ['超级订单', 'sys:user:save'],
onClick: openPrescriptionDetail.bind(null, row.p_id),
},
]"
:drop-down-actions="[
{
label: '模拟支付',
type: 'link',
icon: 'mdi:cash-check',
auth: ['Super Admin', 'sys:user:save'],
ifShow: row.is_pay === 0,
onClick: handleSimulatePay.bind(null, row),
},
{
label: '发货',
type: 'link',
icon: 'ri:send-plane-fill',
// auth: ['超级订单', 'sys:user:save'],
auth: ['Super Admin','Admin', 'sys:user:save'],
onClick: wareSend.bind(null, row),
// popConfirm: {
// title: '确定发货吗?',
// confirm: wareSend.bind(null, row),
// },
},
]"
:drop-down-actions="[
{
label: '退款',
type: 'link',

View File

@@ -22,13 +22,13 @@ export const gridOptions: VxeGridProps<RegisterOrderItem> = {
{ field: 'is_pay', title: '是否支付', width: 100, slots: { default: 'is_pay' } },
{ field: 'status', title: '状态', slots: { default: 'status' } },
{ field: 'created_at', title: '创建时间' },
// {
// type: 'html',
// title: '操作',
// align: 'right',
// slots: { default: 'action' },
// width: 200,
// },
{
type: 'html',
title: '操作',
align: 'right',
slots: { default: 'action' },
width: 200,
},
],
keepSource: true,
pagerConfig: {},

View File

@@ -4,15 +4,14 @@ import type { VxeGridListeners } from '#/adapter/vxe-table';
import { nextTick, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import {
Page,
useVbenModal,
} from '@vben/common-ui';
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { Button, Tag } from 'ant-design-vue';
import { Button, message, Modal as AntdModal, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import { simulatePayApi } from '#/views/business/order/api/order-ops';
import OrderTraceDrawer from '#/views/business/order/components/order-trace-drawer.vue';
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
import { formOptions } from './config/search';
@@ -56,13 +55,40 @@ onMounted(() => {
const [PrescriptionDetailModal, PrescriptionDetailModalApi] = useVbenModal({
connectedComponent: PrescriptionDetail,
});
const openPrescriptionDetail = (values) => {
// 打开西药处方模态框逻辑
const [TraceDrawer, traceDrawerApi] = useVbenDrawer({
connectedComponent: OrderTraceDrawer,
});
function openPrescriptionDetail(id: number) {
PrescriptionDetailModalApi.setData({
values,
values: { id },
});
PrescriptionDetailModalApi.open();
};
}
function openOrderTrace(row: Record<string, any>) {
traceDrawerApi.setData({
scene: 'register',
order_id: row.id,
order_no: row.order_no,
});
traceDrawerApi.open();
}
function handleSimulatePay(row: Record<string, any>) {
AntdModal.confirm({
title: '模拟支付',
content: `确认为挂号订单 ${row.order_no} 模拟易票联支付回调?`,
okText: '确认',
cancelText: '取消',
onOk: async () => {
await simulatePayApi({ order_type: 'register', order_id: row.id });
message.success('模拟支付成功');
await gridApi.query();
},
});
}
function formatRegisterPrice(price: unknown) {
const n = Number(price);
@@ -76,12 +102,10 @@ function formatRegisterPrice(price: unknown) {
<template>
<Page auto-content-height title="订单管理">
<PrescriptionDetailModal />
<TraceDrawer />
<Grid>
<template #toolbar-buttons>
<TableAction
:actions="[]"
:drop-down-actions="[]"
>
<TableAction :actions="[]" :drop-down-actions="[]">
<template #more>
<Button style="margin-left: 16px">
批量操作
@@ -91,10 +115,11 @@ function formatRegisterPrice(price: unknown) {
</TableAction>
</template>
<template #prescription="{ row }">
<div v-for="item in row.prescription">
<Button type="link" @click="openPrescriptionDetail(item.id)">{{ item.prescription_no }}</Button>
<div v-for="item in row.prescription" :key="item.id">
<Button type="link" @click="openPrescriptionDetail(item.id)">
{{ item.prescription_no }}
</Button>
</div>
</template>
<template #price="{ row }">
<span>{{ formatRegisterPrice(row.price) }}</span>
@@ -114,7 +139,6 @@ function formatRegisterPrice(price: unknown) {
</template>
<template #status="{ row }">
<div class="mt-3">
<!-- 状态:0待支付1已支付待接诊2接诊中3已结束4已取消5待评价6已评价7已拒诊-->
<Tag v-if="row.status === 0" color="red">代支付</Tag>
<Tag v-else-if="row.status === 1" color="green">待接诊</Tag>
<Tag v-else-if="row.status === 2" color="red">接诊中</Tag>
@@ -123,7 +147,6 @@ function formatRegisterPrice(price: unknown) {
<Tag v-else-if="row.status === 5" color="purple">待评价</Tag>
<Tag v-else-if="row.status === 6" color="purple">已评价</Tag>
<Tag v-else-if="row.status === 7" color="red">已拒诊</Tag>
<!-- <p>{{ row.created_at }}</p>-->
</div>
</template>
<template #toolbar-tools></template>
@@ -131,20 +154,28 @@ function formatRegisterPrice(price: unknown) {
<TableAction
:actions="[
{
label: '查看处方',
label: '溯源',
type: 'link',
icon: 'marketeq:eye',
// auth: ['超级订单', 'sys:user:save'],
onClick: openPrescriptionDetail.bind(null, row.id),
icon: 'mdi:timeline-text',
onClick: openOrderTrace.bind(null, row),
},
]"
:drop-down-actions="[
{
label: '模拟支付',
type: 'link',
icon: 'mdi:cash-check',
auth: ['Super Admin', 'sys:user:save'],
ifShow: row.is_pay === 0,
onClick: handleSimulatePay.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>
</template>
<style scoped lang="scss">
.custom-list {
list-style-type: none;

View File

@@ -0,0 +1,7 @@
import { requestClient } from '#/api/request';
const prefix = 'account-able-change-log/';
export async function getAccountAbleChangeLogList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}

View File

@@ -0,0 +1,119 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { Descriptions, Spin, Table } from 'ant-design-vue';
import { getSettlementLedgerDetail } from '#/views/finance/withdrawal/api/settlement';
const loading = ref(false);
const detail = ref<{
items: Record<string, any>[];
ledger_log: Record<string, any> | null;
order_id: number;
order_no: string;
order_type: number;
sum_money: string;
}>({
items: [],
ledger_log: null,
order_id: 0,
order_no: '',
order_type: 0,
sum_money: '0',
});
const columns = [
{ title: '费用类型', dataIndex: 'fee_type_txt', key: 'fee_type_txt' },
{ title: '分账金额', dataIndex: 'money', key: 'money' },
{ title: '结算状态', dataIndex: 'status_txt', key: 'status_txt' },
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
];
const [Drawer, drawerApi] = useVbenDrawer({
onOpenChange(isOpen) {
if (isOpen) {
loadDetail();
} else {
detail.value = {
items: [],
ledger_log: null,
order_id: 0,
order_no: '',
order_type: 0,
sum_money: '0',
};
}
},
});
async function loadDetail() {
const data = drawerApi.getData<{
ledger_log_id?: number;
merged?: boolean;
order_id?: number;
order_type?: number;
}>();
if (!data) {
return;
}
loading.value = true;
try {
const params =
data.merged && data.order_id && data.order_type
? { order_id: data.order_id, order_type: data.order_type }
: { ledger_log_id: data.ledger_log_id };
const res = await getSettlementLedgerDetail(params);
detail.value = res || detail.value;
} finally {
loading.value = false;
}
}
defineExpose({
open(row: Record<string, any>) {
drawerApi.setData({
ledger_log_id: row.id,
merged: row.merged,
order_id: row.order_id,
order_type: row.order_type,
});
drawerApi.open();
},
});
</script>
<template>
<Drawer class="w-[720px]" title="结算明细">
<Spin :spinning="loading">
<Descriptions bordered :column="1" size="small" class="mb-4">
<Descriptions.Item label="订单号">
{{ detail.order_no || '-' }}
</Descriptions.Item>
<Descriptions.Item label="合计分账">
{{ detail.sum_money }}
</Descriptions.Item>
<template v-if="detail.ledger_log">
<Descriptions.Item label="费用类型">
{{ detail.ledger_log.fee_type_txt }}
</Descriptions.Item>
<Descriptions.Item label="结算类型">
{{ detail.ledger_log.type_txt }}
</Descriptions.Item>
<Descriptions.Item label="说明">
{{ detail.ledger_log.content || '-' }}
</Descriptions.Item>
</template>
</Descriptions>
<Table
:columns="columns"
:data-source="detail.items"
:pagination="false"
row-key="id"
size="small"
/>
</Spin>
</Drawer>
</template>

View File

@@ -0,0 +1,37 @@
import type { VbenFormProps } from '#/adapter/form';
export const accountChangeSearchOptions: VbenFormProps = {
collapsed: false,
schema: [
{
component: 'VbenSelect',
componentProps: {
placeholder: '来源类型',
allowClear: true,
options: [
{ label: '分账', value: 'settle' },
{ label: '提现', value: 'withdraw' },
{ label: '退款', value: 'refund' },
],
},
defaultValue: '',
fieldName: 'source_type',
label: '来源类型',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '输入订单号',
},
defaultValue: '',
fieldName: 'order_no',
label: '订单号',
},
],
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
submitOnChange: true,
submitOnEnter: false,
};

View File

@@ -0,0 +1,50 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getAccountAbleChangeLogList } from '#/views/finance/withdrawal/api/account-change';
export const accountChangeGridOptions: VxeGridProps = {
columnConfig: {
useKey: true,
},
rowConfig: {
keyField: 'id',
useKey: true,
},
columns: [
{ field: 'created_at', title: '变动时间', width: 170 },
{ field: 'source_type_txt', title: '来源类型', width: 100 },
{ field: 'field_name_txt', title: '变更字段', width: 120 },
{ field: 'before_amount', title: '变动前', width: 110 },
{ field: 'after_amount', title: '变动后', width: 110 },
{ field: 'change_amount', title: '变动值', width: 110 },
{ field: 'order_no', title: '订单号', minWidth: 160 },
{ field: 'source_table', title: '来源表', width: 140 },
{ field: 'remark', title: '备注', minWidth: 180 },
{ type: 'html', title: '操作', slots: { default: 'action' }, width: 100 },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getAccountAbleChangeLogList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
border: false,
toolbarConfig: {
search: true,
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons',
},
},
showOverflow: false,
};

View File

@@ -0,0 +1,48 @@
import type { VbenFormProps } from '#/adapter/form';
export const formOptions3: VbenFormProps = {
collapsed: false,
schema: [
{
component: 'Switch',
componentProps: {
class: 'w-auto',
},
defaultValue: true,
fieldName: 'merge_by_order',
label: '同订单合并',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '输入订单号',
},
defaultValue: '',
fieldName: 'order_no',
label: '订单号',
},
{
component: 'VbenSelect',
componentProps: {
placeholder: '选择费用类型',
options: [
{ label: '药品费用', value: 1 },
{ label: '挂号费用', value: 2 },
{ label: '快递费用', value: 3 },
{ label: '代煎费用', value: 4 },
{ label: '加工费用', value: 5 },
{ label: '诊疗费用', value: 6 },
],
},
defaultValue: '',
fieldName: 'fee_type',
label: '费用类型',
},
],
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
submitOnChange: true,
submitOnEnter: false,
};

View File

@@ -77,6 +77,13 @@ export const gridOptions2: VxeGridProps<RowType> = {
showOverflow: false,
};
function normalizeMergeByOrder(value: unknown): 0 | 1 {
if (value === false || value === 0 || value === '0') {
return 0;
}
return 1;
}
export const gridOptions3: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
@@ -86,29 +93,31 @@ export const gridOptions3: VxeGridProps<RowType> = {
useKey: true,
},
rowConfig: {
keyField: 'id',
useKey: true,
},
columns: [
// { type: 'checkbox', width: 60 },
// { field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'order_no', title: '订单号' },
{ field: 'order_type_txt', title: '订单类型' },
{ field: 'fee_type_txt', title: '费用类型' },
{ field: 'amount', title: '金额' },
{ field: 'type_txt', title: '类型' },
{ field: 'content', title: '说明' },
{ field: 'created_at', title: '创建时间' },
{ field: 'created_at', title: '结算时间' },
{ type: 'html', title: '操作', slots: { default: 'action' } },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
// 请求后端接口方法
query: async ({ page }, formValues) => {
const { merge_by_order, ...rest } = formValues || {};
return await getSettlementLedgerLogList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
merge_by_order: normalizeMergeByOrder(merge_by_order),
exclude_zero_amount: 1,
...rest,
});
},
},

View File

@@ -1,19 +1,27 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { AnalysisChartsTabs, Page } from '@vben/common-ui';
import { AnalysisChartsTabs, Page, useVbenModal } from '@vben/common-ui';
import { FloatButton, FloatButtonGroup, Popover, Tag } from 'ant-design-vue';
import { FloatButton, FloatButtonGroup, message, Popover, Tag } 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 SettlementDetailDrawer from './components/settlement-detail-drawer.vue';
import Statistics from './components/statistics.vue';
import { accountChangeSearchOptions } from './config/account-change-search';
import { accountChangeGridOptions } from './config/account-change-table';
import { formOptions } from './config/search';
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';
const router = useRouter();
const [Grid, GridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
@@ -24,24 +32,51 @@ const [Grid2] = useVbenVxeGrid({
gridOptions: gridOptions2,
});
const settlementDetailDrawerRef =
ref<InstanceType<typeof SettlementDetailDrawer>>();
const [Grid3] = useVbenVxeGrid({
formOptions: formOptions2,
formOptions: formOptions3,
gridOptions: gridOptions3,
});
const [Grid4] = useVbenVxeGrid({
formOptions: accountChangeSearchOptions,
gridOptions: accountChangeGridOptions,
});
const [OrderDetailModal, OrderDetailModalApi] = useVbenModal({
connectedComponent: DetailModal,
});
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();
return;
}
if (orderType === 2 && orderNo) {
router.push({ path: '/business/register/list', query: { order_no: orderNo } });
return;
}
message.info('暂不支持查看该类型订单详情');
}
const chartTabs = [
{
label: '提现记录',
value: 'trends',
},
{
label: '结算记录',
value: 'visits',
},
{
label: '结算明细',
value: 'visitsItems',
},
{ label: '提现记录', value: 'trends' },
{ label: '结算记录', value: 'visits' },
{ label: '结算明细', value: 'visitsItems' },
{ label: '资金变动记录', value: 'accountChange' },
];
const isShow = ref(true);
@@ -57,63 +92,73 @@ function updateShowStatus() {
description="提现前请确认好您的打款账户"
title="提现管理"
>
<OrderDetailModal />
<Statistics v-show="isShow" :grid-api="GridApi" />
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
<template #trends>
<!-- 提现记录 --->
<div style="min-height: 500px;">
<Grid>
<template #toolbar-buttons></template>
<template #toolbar-tools></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>
<span v-else-if="row.user_type === 3">{{ row.supplier.name }}</span>
</template>
<template #check_status="{ row }">
<Tag v-if="row.check_status === 1" color="blue">待审核</Tag>
<Tag v-else-if="row.check_status === 2" color="green">审核成功</Tag>
<Tag v-else-if="row.check_status === 3" color="red">拒绝</Tag>
</template>
<template #check_result="{ row }">
<Tag v-if="row.check_status === 2" color="green">
{{ row.check_result }}
</Tag>
<Tag v-else-if="row.check_status === 3" color="red">
{{ row.check_result }}
</Tag>
</template>
<template #dakuan_status="{ row }">
<span v-if="row.dakuan_status === -1">打款失败</span>
<span v-else-if="row.dakuan_status === 0">处理中</span>
<span v-else-if="row.dakuan_status === 1">打款成功</span>
</template>
<template #check_id="{ row }">
<span>{{
row.check_admin?.username || row.check_admin?.nick_name || '暂无'
}}</span>
</template>
<template #action="{ row }">
<TableAction
:actions="[
// {
// label: '编辑',
// type: 'link',
// icon: 'uil:edit',
// size: 'small',
// onClick: showModal.bind(null, row, true),
// },
]"
:drop-down-actions="[]"
/>
</template>
</Grid>
<template #toolbar-buttons></template>
<template #toolbar-tools></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>
<span v-else-if="row.user_type === 3">{{ row.supplier.name }}</span>
</template>
<template #check_status="{ row }">
<Tag v-if="row.check_status === 1" color="blue">待审核</Tag>
<Tag v-else-if="row.check_status === 2" color="green">审核成功</Tag>
<Tag v-else-if="row.check_status === 3" color="red">拒绝</Tag>
</template>
<template #check_result="{ row }">
<Tag v-if="row.check_status === 2" color="green">{{ row.check_result }}</Tag>
<Tag v-else-if="row.check_status === 3" color="red">{{ row.check_result }}</Tag>
</template>
<template #dakuan_status="{ row }">
<span v-if="row.dakuan_status === -1">打款失败</span>
<span v-else-if="row.dakuan_status === 0">处理中</span>
<span v-else-if="row.dakuan_status === 1">打款成功</span>
</template>
<template #check_id="{ row }">
<span>{{ row.check_admin?.username || row.check_admin?.nick_name || '暂无' }}</span>
</template>
<template #action="{ row }">
<TableAction :actions="[]" :drop-down-actions="[]" />
</template>
</Grid>
</div>
</template>
<template #visits>
<!-- 结算记录 --->
<div style="min-height: 500px;">
<Grid3>
<template #toolbar-buttons></template>
<template #toolbar-tools></template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '查看明细',
type: 'link',
size: 'small',
onClick: showSettlementDetail.bind(null, row),
},
{
label: '订单详情',
type: 'link',
size: 'small',
onClick: openOrderDetail.bind(null, row),
},
]"
:drop-down-actions="[]"
/>
</template>
</Grid3>
<SettlementDetailDrawer ref="settlementDetailDrawerRef" />
</div>
</template>
<template #visitsItems>
<div style="min-height: 500px;">
<Grid2>
<template #toolbar-buttons></template>
<template #toolbar-tools></template>
<template #user_id="{ row }">
@@ -121,57 +166,36 @@ function updateShowStatus() {
<span v-else-if="row.user_type === 2">萧康平台</span>
<span v-else-if="row.user_type === 3">{{ row.supplier.name }}</span>
</template>
<template #action="{ row }">
<TableAction :actions="[]" :drop-down-actions="[]" />
</template>
</Grid2>
</div>
</template>
<template #accountChange>
<div style="min-height: 500px;">
<Grid4>
<template #toolbar-buttons></template>
<template #toolbar-tools></template>
<template #action="{ row }">
<TableAction
:actions="[
// {
// label: '编辑',
// type: 'link',
// icon: 'uil:edit',
// size: 'small',
// // auth: ['admin', 'sys:role:detail'],
// onClick: showModal.bind(null, row, true),
// },
]"
{
label: '订单详情',
type: 'link',
size: 'small',
ifShow: !!row.order_id,
onClick: openOrderDetail.bind(null, row),
},
]"
:drop-down-actions="[]"
/>
</template>
</Grid3>
</div>
</template>
<template #visitsItems>
<!-- 结算明细 --->
<div style="min-height: 500px;">
<Grid2>
<template #toolbar-buttons></template>
<template #toolbar-tools></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>
<span v-else-if="row.user_type === 3">{{ row.supplier.name }}</span>
</template>
<template #action="{ row }">
<TableAction
:actions="[
// {
// label: '编辑',
// type: 'link',
// icon: 'uil:edit',
// size: 'small',
// // auth: ['admin', 'sys:role:detail'],
// onClick: showModal.bind(null, row, true),
// },
]"
:drop-down-actions="[]"
/>
</template>
</Grid2>
</Grid4>
</div>
</template>
</AnalysisChartsTabs>
<!-- 浮动按钮组 -->
<FloatButtonGroup :style="{ right: '24px', bottom: '84px' }" shape="circle">
<!-- 打开显示余额卡片的按钮 -->
<Popover placement="left" title="显示余额卡片">
<template #content>
<p>点击展开卡片信息再次点击收起</p>