fix: 月付账单相关页面

This commit is contained in:
2025-04-22 16:15:10 +08:00
parent 47092ab827
commit e5e95beea7
19 changed files with 1526 additions and 25 deletions

View File

@@ -8,3 +8,10 @@ const prefix = 'prescription/';
export async function getPrescriptionListApi(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
/**
* 处方溯源
* @param data
*/
export async function getPrescriptionSourceApi(data: any) {
return requestClient.get<any>(`${prefix}source`, { params: data });
}

View File

@@ -0,0 +1,310 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Timeline, TimelineItem } from 'ant-design-vue';
import {
FileTextOutlined,
MedicineBoxOutlined,
ShopOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
import { getPrescriptionSourceApi } from '../api';
defineOptions({ name: 'PrescriptionSource' });
// 处方溯源信息
const data = ref();
// 格式化时间戳
const formatTime = (timestamp) => {
if (!timestamp) return '--';
return new Date(timestamp * 1000).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
};
// 处方类型
const prescriptionTypeMap = {
1: '西药处方',
2: '中成药处方',
3: '中药处方',
};
// 处方状态
const statusMap = {
0: '待审核',
1: '已审核',
2: '已驳回',
3: '已过期',
};
// 计算属性:处方类型文本
const prescriptionTypeText = computed(() => {
return data.value
? prescriptionTypeMap[data.value.prescription_type] || '未知'
: '--';
});
// 计算属性:处方状态文本
const statusText = computed(() => {
return data.value ? statusMap[data.value.status] || '未知' : '--';
});
// 计算属性:状态颜色
const statusColor = computed(() => {
if (!data.value) return 'gray';
const statusColors = {
0: 'orange',
1: 'green',
2: 'red',
3: 'gray',
};
return statusColors[data.value.status] || 'gray';
});
// 计算属性:过期时间
const expireTime = computed(() => {
return data.value ? formatTime(data.value.auto_expire_time) : '--';
});
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
modalApi.close();
},
onOpenChange(isOpen: boolean) {
const { id } = modalApi.getData<Record<string, any>>();
if (isOpen && id) {
getPrescriptionSourceApi({ id }).then((res) => {
data.value = res;
});
} else {
data.value = null; // Reset data when modal is closed or id is missing
}
},
});
</script>
<template>
<Modal class="w-[80%]" title="处方溯源">
<div v-if="data" class="prescription-source-container">
<!-- 药店信息 -->
<div
class="mb-6 transform rounded-lg p-6 shadow-md transition-all duration-300 hover:shadow-lg"
>
<div class="mb-4 flex items-center">
<ShopOutlined class="mr-2 text-xl text-purple-500" />
<h2 class="text-xl font-bold">药店信息</h2>
</div>
<div v-if="data.store" class="grid grid-cols-1 gap-4">
<div class="info-item">
<span class="label">药店名称:</span>
<span class="value">{{ data.store.name }}</span>
</div>
</div>
<div v-else class="italic text-gray-500">暂无药店信息</div>
</div>
<!-- 处方基本信息 -->
<div
class="mb-6 transform rounded-lg p-6 shadow-md transition-all duration-300 hover:shadow-lg"
>
<div class="mb-4 flex items-center">
<FileTextOutlined class="mr-2 text-xl text-blue-500" />
<h2 class="text-xl font-bold">处方基本信息</h2>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<div class="info-item">
<span class="label">处方编号:</span>
<span class="value">{{ data.prescription_no }}</span>
</div>
<div class="info-item">
<span class="label">处方类型:</span>
<span class="value">{{ prescriptionTypeText }}</span>
</div>
<div class="info-item">
<span class="label">处方状态:</span>
<span :class="`text-${statusColor}-500`" class="value">{{
statusText
}}</span>
</div>
<div class="info-item">
<span class="label">总金额:</span>
<span class="value font-bold text-red-500">¥{{ data.total_pay_price }}</span>
</div>
<div class="info-item">
<span class="label">过期时间:</span>
<span class="value">{{ expireTime }}</span>
</div>
</div>
</div>
<!-- 医生信息 -->
<div
class="mb-6 transform rounded-lg p-6 shadow-md transition-all duration-300 hover:shadow-lg"
>
<div class="mb-4 flex items-center">
<MedicineBoxOutlined class="mr-2 text-xl text-green-500" />
<h2 class="text-xl font-bold">医生信息</h2>
</div>
<div
v-if="data.doctor_info"
class="grid grid-cols-1 gap-4 md:grid-cols-2"
>
<div class="info-item">
<span class="label">医生姓名:</span>
<span class="value">{{ data.doctor_info.name }}</span>
</div>
<div class="info-item">
<span class="label">所属科室:</span>
<span class="value">{{
data.doctor_info.depart?.name || '--'
}}</span>
</div>
</div>
<div v-else class="italic text-gray-500">暂无医生信息</div>
</div>
<!-- 患者信息 -->
<div
class="transform rounded-lg p-6 shadow-md transition-all duration-300 hover:shadow-lg"
>
<div class="mb-4 flex items-center">
<UserOutlined class="mr-2 text-xl text-amber-500" />
<h2 class="text-xl font-bold">患者信息</h2>
</div>
<div
v-if="data.user_patient"
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
>
<div class="info-item">
<span class="label">患者姓名:</span>
<span class="value">{{ data.user_patient.name }}</span>
</div>
<div class="info-item">
<span class="label">年龄:</span>
<span class="value">{{ data.user_patient.age }}岁</span>
</div>
<div class="info-item">
<span class="label">性别:</span>
<span class="value">{{
data.user_patient.sex === 1 ? '男' : '女'
}}</span>
</div>
</div>
<div v-else class="italic text-gray-500">暂无患者信息</div>
</div>
<!-- 时间线 -->
<div
class="mb-6 transform rounded-lg p-6 shadow-md transition-all duration-300 hover:shadow-lg"
>
<div class="mb-4 flex items-center">
<FileTextOutlined class="mr-2 text-xl text-blue-500" />
<h2 class="text-xl font-bold">处方基本信息</h2>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 mt-5">
<Timeline>
<TimelineItem v-if="data.pharmacist_view_time">
{{ data.pharmacist_view_time }}
<template v-if="data.pharmacist_info">
【{{ data.pharmacist_info.name }}】 审核
</template>
<template v-else> -- </template>
</TimelineItem>
<TimelineItem v-else>
待审核
</TimelineItem>
<TimelineItem>
{{ data.created_at }}
<template v-if="data.doctor_info">
【{{ data.doctor_info.name }}】 开方,诊断:{{
data.clinical_diagnose
}}
</template>
<template v-else> -- </template>
</TimelineItem>
<TimelineItem>
{{ data.register.created_at }}
<template v-if="data.doctor_info">
【{{ data.user_patient.name }}】 挂号
</template>
<template v-else> -- </template>
</TimelineItem>
</Timeline>
</div>
</div>
</div>
<!-- 加载状态 -->
<div v-else class="flex h-64 items-center justify-center">
<div
class="h-12 w-12 animate-spin rounded-full border-b-2 border-t-2 border-blue-500"
></div>
</div>
</Modal>
</template>
<style lang="scss" scoped>
.prescription-source-container {
@apply max-h-[70vh] overflow-auto p-4;
}
.info-item {
@apply flex flex-col rounded-md p-3 transition-all duration-300;
}
.label {
@apply mb-1 text-sm text-gray-500;
}
.value {
@apply font-medium;
}
/* 添加动感效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.prescription-source-container > div {
animation: fadeIn 0.5s ease-out forwards;
}
.prescription-source-container > div:nth-child(1) {
animation-delay: 0.1s;
}
.prescription-source-container > div:nth-child(2) {
animation-delay: 0.2s;
}
.prescription-source-container > div:nth-child(3) {
animation-delay: 0.3s;
}
.prescription-source-container > div:nth-child(4) {
animation-delay: 0.4s;
}
</style>

View File

@@ -0,0 +1,312 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Timeline, TimelineItem } from 'ant-design-vue';
import {
FileTextOutlined,
MedicineBoxOutlined,
ShopOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
import { getPrescriptionSourceApi } from '../api';
defineOptions({ name: 'PrescriptionSource' });
// 处方溯源信息
const data = ref();
// 格式化时间戳
const formatTime = (timestamp) => {
if (!timestamp) return '--';
return new Date(timestamp * 1000).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
};
// 处方类型
const prescriptionTypeMap = {
1: '西药处方',
2: '中成药处方',
3: '中药处方',
};
// 处方状态
const statusMap = {
0: '待审核',
1: '已审核',
2: '已驳回',
3: '已过期',
};
// 计算属性:处方类型文本
const prescriptionTypeText = computed(() => {
return data.value
? prescriptionTypeMap[data.value.prescription_type] || '未知'
: '--';
});
// 计算属性:处方状态文本
const statusText = computed(() => {
return data.value ? statusMap[data.value.status] || '未知' : '--';
});
// 计算属性:状态颜色
const statusColor = computed(() => {
if (!data.value) return 'gray';
const statusColors = {
0: 'orange',
1: 'green',
2: 'red',
3: 'gray',
};
return statusColors[data.value.status] || 'gray';
});
// 计算属性:过期时间
const expireTime = computed(() => {
return data.value ? formatTime(data.value.auto_expire_time) : '--';
});
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
modalApi.close();
},
onOpenChange(isOpen: boolean) {
const { id } = modalApi.getData<Record<string, any>>();
if (isOpen && id) {
getPrescriptionSourceApi({ id }).then((res) => {
data.value = res;
});
} else {
data.value = null; // Reset data when modal is closed or id is missing
}
},
});
</script>
<template>
<Modal class="w-[80%]" title="处方溯源">
<div v-if="data" class="prescription-source-container">
<!-- 药店信息 -->
<div
class="mb-6 transform rounded-lg p-6 shadow-md transition-all duration-300 hover:shadow-lg"
>
<div class="mb-4 flex items-center">
<ShopOutlined class="mr-2 text-xl text-purple-500" />
<h2 class="text-xl font-bold">药店信息</h2>
</div>
<div v-if="data.store" class="grid grid-cols-1 gap-4">
<div class="info-item">
<span class="label">药店名称:</span>
<span class="value">{{ data.store.name }}</span>
</div>
</div>
<div v-else class="italic text-gray-500">暂无药店信息</div>
</div>
<!-- 时间线 -->
<div
class="mb-6 transform rounded-lg p-6 shadow-md transition-all duration-300 hover:shadow-lg"
>
<div class="mb-4 flex items-center">
<FileTextOutlined class="mr-2 text-xl text-blue-500" />
<h2 class="text-xl font-bold">时间线</h2>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 mt-5">
<Timeline>
<TimelineItem v-if="data.pharmacist_view_time">
{{ data.pharmacist_view_time }}
<template v-if="data.pharmacist_info">
{{ data.pharmacist_info.name }} 审核
</template>
<template v-else> -- </template>
</TimelineItem>
<TimelineItem v-else>
{{ statusText }}
{{ data.cancel_remark }}
</TimelineItem>
<TimelineItem>
{{ data.created_at }}
<template v-if="data.doctor_info">
{{ data.doctor_info.name }} 开方诊断{{
data.clinical_diagnose
}}
</template>
<template v-else> -- </template>
</TimelineItem>
<TimelineItem>
{{ data.register.created_at }}
<template v-if="data.doctor_info">
{{ data.user_patient.name }} 挂号
</template>
<template v-else> -- </template>
</TimelineItem>
</Timeline>
</div>
</div>
<!-- 处方基本信息 -->
<div
class="mb-6 transform rounded-lg p-6 shadow-md transition-all duration-300 hover:shadow-lg"
>
<div class="mb-4 flex items-center">
<FileTextOutlined class="mr-2 text-xl text-blue-500" />
<h2 class="text-xl font-bold">处方基本信息</h2>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<div class="info-item">
<span class="label">处方编号:</span>
<span class="value">{{ data.prescription_no }}</span>
</div>
<div class="info-item">
<span class="label">处方类型:</span>
<span class="value">{{ prescriptionTypeText }}</span>
</div>
<div class="info-item">
<span class="label">处方状态:</span>
<span :class="`text-${statusColor}-500`" class="value">{{
statusText
}}</span>
</div>
<div class="info-item">
<span class="label">总金额:</span>
<span class="value font-bold text-red-500">¥{{ data.total_pay_price }}</span>
</div>
<div class="info-item">
<span class="label">过期时间:</span>
<span class="value">{{ expireTime }}</span>
</div>
</div>
</div>
<!-- 医生信息 -->
<div
class="mb-6 transform rounded-lg p-6 shadow-md transition-all duration-300 hover:shadow-lg"
>
<div class="mb-4 flex items-center">
<MedicineBoxOutlined class="mr-2 text-xl text-green-500" />
<h2 class="text-xl font-bold">医生信息</h2>
</div>
<div
v-if="data.doctor_info"
class="grid grid-cols-1 gap-4 md:grid-cols-2"
>
<div class="info-item">
<span class="label">医生姓名:</span>
<span class="value">{{ data.doctor_info.name }}</span>
</div>
<div class="info-item">
<span class="label">所属科室:</span>
<span class="value">{{
data.doctor_info.depart?.name || '--'
}}</span>
</div>
</div>
<div v-else class="italic text-gray-500">暂无医生信息</div>
</div>
<!-- 患者信息 -->
<div
class="transform rounded-lg p-6 shadow-md transition-all duration-300 hover:shadow-lg"
>
<div class="mb-4 flex items-center">
<UserOutlined class="mr-2 text-xl text-amber-500" />
<h2 class="text-xl font-bold">患者信息</h2>
</div>
<div
v-if="data.user_patient"
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
>
<div class="info-item">
<span class="label">患者姓名:</span>
<span class="value">{{ data.user_patient.name }}</span>
</div>
<div class="info-item">
<span class="label">年龄:</span>
<span class="value">{{ data.user_patient.age }}</span>
</div>
<div class="info-item">
<span class="label">性别:</span>
<span class="value">{{
data.user_patient.sex === 1 ? '男' : '女'
}}</span>
</div>
</div>
<div v-else class="italic text-gray-500">暂无患者信息</div>
</div>
</div>
<!-- 加载状态 -->
<div v-else class="flex h-64 items-center justify-center">
<div
class="h-12 w-12 animate-spin rounded-full border-b-2 border-t-2 border-blue-500"
></div>
</div>
</Modal>
</template>
<style lang="scss" scoped>
.prescription-source-container {
@apply max-h-[70vh] overflow-auto p-4;
}
.info-item {
@apply flex flex-col rounded-md p-3 transition-all duration-300;
}
.label {
@apply mb-1 text-sm text-gray-500;
}
.value {
@apply font-medium;
}
/* 添加动感效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.prescription-source-container > div {
animation: fadeIn 0.5s ease-out forwards;
}
.prescription-source-container > div:nth-child(1) {
animation-delay: 0.1s;
}
.prescription-source-container > div:nth-child(2) {
animation-delay: 0.2s;
}
.prescription-source-container > div:nth-child(3) {
animation-delay: 0.3s;
}
.prescription-source-container > div:nth-child(4) {
animation-delay: 0.4s;
}
</style>

View File

@@ -29,7 +29,7 @@ export const gridOptions: VxeGridProps<RowType> = {
title: '操作',
align: 'right',
slots: { default: 'action' },
width: 200,
width: 250,
},
],
keepSource: true,

View File

@@ -13,6 +13,7 @@ import { Button, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import PrescrtionDetail from '#/views/doctor/doctor-reception/components/PrescrtionDetail.vue';
import PrescriptionSource from './components/source.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
@@ -41,6 +42,10 @@ const [Grid, gridApi] = useVbenVxeGrid({
const [PrescrtionDetailModal, PrescrtionDetailModalApi] = useVbenModal({
connectedComponent: PrescrtionDetail,
});
const [PrescriptionSourceModal, PrescriptionSourceModalApi] = useVbenModal({
connectedComponent: PrescriptionSource,
});
const openPrescriptionDetail = (values) => {
// 打开西药处方模态框逻辑
PrescrtionDetailModalApi.setData({
@@ -48,11 +53,19 @@ const openPrescriptionDetail = (values) => {
});
PrescrtionDetailModalApi.open();
};
const openPrescriptionSourceModal = (id) => {
// 打开西药处方模态框逻辑
PrescriptionSourceModalApi.setData({
id,
});
PrescriptionSourceModalApi.open();
};
</script>
<template>
<Page auto-content-height title="订单管理">
<PrescrtionDetailModal />
<PrescriptionSourceModal />
<Grid>
<template #toolbar-buttons>
<TableAction
@@ -102,6 +115,13 @@ const openPrescriptionDetail = (values) => {
// auth: ['超级订单', 'sys:user:save'],
onClick: openPrescriptionDetail.bind(null, row.id),
},
{
label: '处方溯源',
type: 'link',
icon: 'ri:send-plane-fill',
// auth: ['超级订单', 'sys:user:save'],
onClick: openPrescriptionSourceModal.bind(null, row.id),
},
]"
:drop-down-actions="[
]"

View File

@@ -55,7 +55,7 @@ initTableAjax();
</script>
<template>
<Page auto-content-height title="订单管理">
<Page auto-content-height title="代支付手续费记录">
<StatisticsReconciliation
v-if="statistics"
:statistics="statistics"

View File

@@ -1,18 +1,17 @@
<script lang="ts" setup>
import { ref } from 'vue';
import {ref} from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import {Page, useVbenModal} from '@vben/common-ui';
import { Button, Tag } from 'ant-design-vue';
import {Button, Tag} from 'ant-design-vue';
import {useVbenVxeGrid} from '#/adapter/vxe-table';
import {TableAction} from '#/components/table-action';
import {formOptions} from './config/search';
import {gridOptions} from './config/table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import DetailModal from '#/views/business/order/product-order/components/detail.vue';
import StatisticsReconciliation from "#/views/finance/reconciliation/components/statistics.vue";
import StatisticsReconciliation from '#/views/finance/reconciliation/components/statistics.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
@@ -23,7 +22,7 @@ const statistics = ref();
const [OrderDetailModal, OrderDetailModalApi] = useVbenModal({
connectedComponent: DetailModal,
})
});
const infoModal = (id) => {
OrderDetailModalApi.setData({
@@ -35,22 +34,16 @@ const infoModal = (id) => {
</script>
<template>
<Page auto-content-height title="订单管理">
<OrderDetailModal/>
<StatisticsReconciliation
v-if="statistics"
:statistics="statistics"
/>
<Page auto-content-height title="资金流水记录">
<OrderDetailModal />
<StatisticsReconciliation v-if="statistics" :statistics="statistics" />
<Grid>
<template #toolbar-buttons>
<TableAction
:actions="[]"
:drop-down-actions="[]"
>
<TableAction :actions="[]" :drop-down-actions="[]">
<template #more>
<Button style="margin-left: 16px">
批量操作
<Icon icon="ant-design:down-outlined"/>
<Icon icon="ant-design:down-outlined" />
</Button>
</template>
</TableAction>
@@ -81,7 +74,7 @@ const infoModal = (id) => {
// auth: ['order', 'sys:role:detail'],
onClick: infoModal.bind(null, row.order_id),
},
]"
]"
/>
</template>
</Grid>

View File

@@ -0,0 +1,10 @@
import { requestClient } from '#/api/request';
const prefix = 'monthly-payment/';
/**
* 月付账单记录
* @param data
*/
export async function getMonthlyPaymentListApi(data: any) {
return requestClient.get<any>(`${prefix}settlement-list`, { params: data });
}

View File

@@ -0,0 +1,67 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createWithdrawalApplicationWithdrawal } from '#/views/finance/withdrawal/api';
import { invalidatedForm, settlementForm } from '../config/form';
const gridApi = ref();
const type = ref(1);
const id = ref(0);
const [InvalidatedForm, InvalidatedFormApi] = useVbenForm(invalidatedForm);
const [SettlementForm, SettlementFormApi] = useVbenForm(settlementForm);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
let formApi = type.value === 1 ? InvalidatedFormApi : SettlementFormApi;
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();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
}
});
await formApi.validateAndSubmitForm();
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (isOpen) {
const { values, modalType } = modalApi.getData<Record<string, any>>();
if (values) {
id.value = values;
}
if (type) {
type.value = modalType;
}
}
},
});
</script>
<template>
<Modal class="w-[30%]" title="申请提现">
<InvalidatedForm />
<SettlementForm />
</Modal>
</template>

View File

@@ -0,0 +1,65 @@
import type { VbenFormProps } from '#/adapter/form';
/**
* 作废表单
*/
export const invalidatedForm: VbenFormProps = {
wrapperClass: 'grid-cols-12', // 24栅格,
commonConfig: {
formItemClass: 'col-span-12',
// 所有表单项
componentProps: {
class: 'w-full',
},
},
// handleSubmit: onSubmit,
layout: 'horizontal',
schema: [
{
component: 'Textarea',
componentProps: {
placeholder: '请输入作废原因',
},
fieldName: 'amount',
label: '作废原因',
rules: 'required',
},
],
showDefaultActions: false,
};
/**
* 结算表单
*/
export const settlementForm: VbenFormProps = {
wrapperClass: 'grid-cols-12', // 24栅格,
commonConfig: {
formItemClass: 'col-span-12',
// 所有表单项
componentProps: {
class: 'w-full',
},
},
// handleSubmit: onSubmit,
layout: 'horizontal',
schema: [
{
component: 'Textarea',
componentProps: {
placeholder: '请输入作废原因',
},
fieldName: 'amount',
label: '作废原因',
rules: 'required',
},
{
component: 'Avatar',
componentProps: {
placeholder: '请上传转账截图',
},
fieldName: 'amount',
label: '转账截图',
rules: 'required',
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,47 @@
import type { VbenFormProps } from '#/adapter/form';
import { getStoreOption } from '#/views/system/store/api';
// import dayjs from 'dayjs';
export const formOptions: VbenFormProps = {
// 默认展开
collapsed: false,
schema: [
{
component: 'ApiSelect',
componentProps: {
placeholder: '请选择',
// 菜单接口转options格式
afterFetch: (data: { id: number; name: string }[]) => {
return data.map((item: any) => ({
label: item.name,
value: item.id,
}));
},
api: getStoreOption,
},
// defaultValue: 0,
fieldName: 'store_id',
label: '诊所',
},
{
component: 'RangePicker',
componentProps: {
format: 'YYYY-MM-DD', // 确保格式与你需要的一致
},
// defaultValue: [dayjs().startOf('month'), dayjs()],
fieldName: 'search_time',
label: '时间范围',
},
],
// 控制表单是否显示折叠按钮
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
// 是否在字段值改变时提交表单
// submitOnChange: true,
// submitOnChange: true,
// 按下回车时是否提交表单
submitOnEnter: true,
};

View File

@@ -0,0 +1,80 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getMonthlyPaymentListApi } from '../api';
interface RowType {
id: string;
name: string;
logo: string;
introduce: string;
created_at: string;
}
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: '',
},
columns: [
{ field: 'id', align: 'left', title: 'ID' },
{ field: 'store.name', title: '诊所' },
{ field: 'amount', title: '金额' },
{ field: 'url', title: '付款证据', slots: { default: 'url' } },
{ field: 'status', title: '流水类型', slots: { default: 'status' } },
{ field: 'created_at', title: '记录时间' },
// {
// type: 'html',
// title: '操作',
// align: 'right',
// slots: { default: 'action' },
// width: 200,
// },
],
keepSource: true,
pagerConfig: {},
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
// scrollY: {
// enabled: true,
// gt: 0,
// },
proxyConfig: {
ajax: {
// 请求后端接口方法
query: async ({ page }, formValues) => {
return await getMonthlyPaymentListApi({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
height: 'auto',
border: false,
toolbarConfig: {
// 是否显示搜索表单控制按钮
// @ts-ignore 正式环境时有完整的类型声明
search: true,
refresh: true, // 刷新
print: false, // 打印
export: false, // 导出
// custom: true, // 自定义列
zoom: true, // 最大化最小化
slots: {
buttons: 'toolbar-buttons',
},
custom: {
// 自定义列-图标
icon: 'vxe-icon-menu',
},
},
expandConfig: {
// expandAll: true,
},
showOverflow: false,
};

View File

@@ -0,0 +1,109 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, Tag, Image } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import MonthPayment from './components/MonthlyPayment.vue'
import StatisticsReconciliation from '#/views/finance/reconciliation/components/statistics.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
import {getMonthlyPaymentListApi} from "#/views/finance/monthly-payment/api";
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
});
const statistics = ref();
const [MonthlyPaymentModal, MonthlyPaymentModalApi] = useVbenModal({
connectedComponent: MonthPayment,
});
const infoModal = (id) => {
MonthlyPaymentModalApi.setData({
// 表单值
id,
});
MonthlyPaymentModalApi.open();
};
</script>
<template>
<Page auto-content-height title="资金流水记录">
<MonthlyPaymentModal />
<StatisticsReconciliation v-if="statistics" :statistics="statistics" />
<Grid>
<template #toolbar-buttons>
<TableAction :actions="[]" :drop-down-actions="[]">
<template #more>
<Button style="margin-left: 16px">
批量操作
<Icon icon="ant-design:down-outlined" />
</Button>
</template>
</TableAction>
</template>
<template #url="{ row }">
<Image :src="row.url" />
</template>
<template #status="{ row }">
<div class="mt-3">
<Tag v-if="row.status === 0" color="purple">待结算</Tag>
<Tag v-else-if="row.status === 1" color="green">已结算</Tag>
<Tag v-else-if="row.status === 2" color="orange">作废已取消</Tag>
</div>
</template>
<template #toolbar-tools></template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '作废',
type: 'link',
icon: 'uil:edit',
size: 'small',
// auth: ['order', 'sys:role:detail'],
onClick: infoModal.bind(null, row.order_id),
},
]"
/>
</template>
</Grid>
</Page>
</template>
<style scoped lang="scss">
.custom-list {
list-style-type: none;
padding-left: 0;
}
.custom-list-item {
background-color: rgba(64, 158, 255, 0.04);
border-radius: 4px;
margin-bottom: 8px;
padding: 8px 12px;
font-size: 14px;
transition: background-color 0.3s;
&:hover {
background-color: rgba(64, 158, 255, 0.1);
}
&::before {
content: '';
display: inline-block;
width: 6px;
height: 6px;
background-color: #409eff;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
}
}
</style>

View File

@@ -0,0 +1,27 @@
import { requestClient } from '#/api/request';
const prefix = 'monthly-payment/';
/**
* 月付账单记录
* @param data
*/
export async function getMonthlyPaymentListApi(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
/**
* 结算
* @param data
*/
export async function settlementApi(data: any) {
return requestClient.post<any>(`${prefix}settlement`, data);
}
/**
* 作废订单
* @param data
*/
export async function invalidatedApi(data: any) {
return requestClient.post<any>(`${prefix}invalidated`, data);
}

View File

@@ -0,0 +1,71 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
invalidatedApi,
settlementApi,
} from '#/views/finance/monthly-payment/api';
import { invalidatedForm, settlementForm } from '../config/form';
const gridApi = ref();
const type = ref(1);
const id = ref(0);
const [InvalidatedForm, InvalidatedFormApi] = useVbenForm(invalidatedForm);
const [SettlementForm, SettlementFormApi] = useVbenForm(settlementForm);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
const formApi = type.value === 1 ? InvalidatedFormApi : SettlementFormApi;
formApi.validate().then(async (e: any) => {
if (e.valid) {
const values = await formApi.getValues();
modalApi.setState({ loading: true, confirmLoading: true });
const submitApi = type.value === 1 ? invalidatedApi : settlementApi;
submitApi(values)
.then(() => {
message.success('保存成功');
gridApi.value?.reload();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
}
});
await formApi.validateAndSubmitForm();
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (isOpen) {
const { modalType, id } = modalApi.getData<Record<string, any>>();
if (modalType) {
type.value = modalType;
}
if (id && type.value === 1) {
InvalidatedFormApi.setValues({
id,
});
}
}
},
});
</script>
<template>
<Modal :title="type === 1 ? '作废订单' : '结算订单'" class="w-[30%]">
<InvalidatedForm v-if="type === 1" />
<SettlementForm v-else />
</Modal>
</template>

View File

@@ -0,0 +1,85 @@
import type { VbenFormProps } from '#/adapter/form';
import {getStoreOption} from "#/views/system/store/api";
/**
* 作废表单
*/
export const invalidatedForm: VbenFormProps = {
wrapperClass: 'grid-cols-12', // 24栅格,
commonConfig: {
formItemClass: 'col-span-12',
// 所有表单项
componentProps: {
class: 'w-full',
},
},
// handleSubmit: onSubmit,
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
fieldName: 'id',
label: 'ID',
dependencies: {
show: false,
triggerFields: ['id'],
},
rules: 'required',
},
{
component: 'Textarea',
componentProps: {
placeholder: '请输入作废原因',
},
fieldName: 'invalidated_reason',
label: '作废原因',
rules: 'required',
},
],
showDefaultActions: false,
};
/**
* 结算表单
*/
export const settlementForm: VbenFormProps = {
wrapperClass: 'grid-cols-12', // 24栅格,
commonConfig: {
formItemClass: 'col-span-12',
// 所有表单项
componentProps: {
class: 'w-full',
},
},
// handleSubmit: onSubmit,
layout: 'horizontal',
schema: [
{
component: 'ApiSelect',
componentProps: {
placeholder: '请选择',
// 菜单接口转options格式
afterFetch: (data: { id: number; name: string }[]) => {
return data.map((item: any) => ({
label: item.name,
value: item.id,
}));
},
api: getStoreOption,
},
// defaultValue: 0,
fieldName: 'store_id',
label: '诊所',
rules: 'required',
},
{
component: 'Avatar',
componentProps: {
placeholder: '请上传转账截图',
},
fieldName: 'url',
label: '转账截图',
rules: 'required',
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,70 @@
import type { VbenFormProps } from '#/adapter/form';
import {getStoreOption} from "#/views/system/store/api";
// import dayjs from 'dayjs';
export const formOptions: VbenFormProps = {
// 默认展开
collapsed: false,
schema: [
{
component: 'ApiSelect',
componentProps: {
placeholder: '请选择',
// 菜单接口转options格式
afterFetch: (data: { id: number; name: string }[]) => {
return data.map((item: any) => ({
label: item.name,
value: item.id,
}));
},
api: getStoreOption,
},
// defaultValue: 0,
fieldName: 'store_id',
label: '诊所',
},
{
component: 'VbenSelect',
componentProps: {
placeholder: '请选择',
options: [
{
label: '待结算',
value: 0,
},
{
label: '已结算',
value: 1,
},
{
label: '作废',
value: 2,
},
],
},
// defaultValue: 0,
fieldName: 'status',
label: '结算状态',
},
{
component: 'RangePicker',
componentProps: {
format: 'YYYY-MM-DD', // 确保格式与你需要的一致
},
// defaultValue: [dayjs().startOf('month'), dayjs()],
fieldName: 'search_time',
label: '时间范围',
},
],
// 控制表单是否显示折叠按钮
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
// 是否在字段值改变时提交表单
// submitOnChange: true,
// submitOnChange: true,
// 按下回车时是否提交表单
submitOnEnter: true,
};

View File

@@ -0,0 +1,79 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getMonthlyPaymentListApi } from '../api';
interface RowType {
id: string;
name: string;
logo: string;
introduce: string;
created_at: string;
}
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: '',
},
columns: [
{ field: 'id', align: 'left', title: 'ID' },
{ field: 'store.name', title: '诊所' },
{ field: 'amount', title: '金额' },
{ field: 'status', title: '流水类型', slots: { default: 'status' } },
{ field: 'created_at', title: '记录时间' },
{
type: 'html',
title: '操作',
align: 'right',
slots: { default: 'action' },
width: 200,
},
],
keepSource: true,
pagerConfig: {},
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
// scrollY: {
// enabled: true,
// gt: 0,
// },
proxyConfig: {
ajax: {
// 请求后端接口方法
query: async ({ page }, formValues) => {
return await getMonthlyPaymentListApi({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
height: 'auto',
border: false,
toolbarConfig: {
// 是否显示搜索表单控制按钮
// @ts-ignore 正式环境时有完整的类型声明
search: true,
refresh: true, // 刷新
print: false, // 打印
export: false, // 导出
// custom: true, // 自定义列
zoom: true, // 最大化最小化
slots: {
buttons: 'toolbar-buttons',
},
custom: {
// 自定义列-图标
icon: 'vxe-icon-menu',
},
},
expandConfig: {
// expandAll: true,
},
showOverflow: false,
};

View File

@@ -0,0 +1,149 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import MonthPayment from './components/MonthlyPayment.vue'
import StatisticsReconciliation from '#/views/finance/reconciliation/components/statistics.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
import {getMonthlyPaymentListApi} from "#/views/finance/monthly-payment/api";
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
});
const statistics = ref();
const [MonthlyPaymentModal, MonthlyPaymentModalApi] = useVbenModal({
connectedComponent: MonthPayment,
});
const infoModal = (id, modalType) => {
MonthlyPaymentModalApi.setData({
// 表单值
id,
modalType,
gridApi,
});
MonthlyPaymentModalApi.open();
};
const initTableAjax = () => {
gridApi.setGridOptions({
proxyConfig: {
ajax: {
// 请求后端接口方法
query: async ({ page }, formValues) => {
return await getMonthlyPaymentListApi({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
}).then((res) => {
statistics.value = null;
statistics.value = [
{
title: '总待付',
value: res.count,
icon: 'mdi:cash-multiple',
color: 'text-blue-600',
unit: '¥',
},
];
return res.list;
});
},
},
},
});
};
initTableAjax();
</script>
<template>
<Page auto-content-height title="资金流水记录">
<MonthlyPaymentModal />
<StatisticsReconciliation v-if="statistics" :statistics="statistics" />
<Grid>
<template #toolbar-buttons>
<TableAction :actions="[
{
label: '结算订单',
type: 'primary',
icon: 'ant-design:plus-outlined',
// auth: ['超级诊所', 'sys:user:save'],
onClick: infoModal.bind(null, null, 2),
},
]" :drop-down-actions="[]">
<template #more>
<Button style="margin-left: 16px">
批量操作
<Icon icon="ant-design:down-outlined" />
</Button>
</template>
</TableAction>
</template>
<template #status="{ row }">
<div class="mt-3">
<Tag v-if="row.status === 0" color="purple">待结算</Tag>
<Tag v-else-if="row.status === 1" color="green">已结算</Tag>
<div v-else-if="row.status === 2">
<Tag color="orange">作废已取消</Tag>
<p class="mt-3" style="color: red;">{{ row.invalidated_reason }}</p>
</div>
</div>
</template>
<template #toolbar-tools></template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '作废',
type: 'link',
icon: 'uil:edit',
size: 'small',
// auth: ['order', 'sys:role:detail'],
onClick: infoModal.bind(null, row.id, 1),
},
]"
/>
</template>
</Grid>
</Page>
</template>
<style scoped lang="scss">
.custom-list {
list-style-type: none;
padding-left: 0;
}
.custom-list-item {
background-color: rgba(64, 158, 255, 0.04);
border-radius: 4px;
margin-bottom: 8px;
padding: 8px 12px;
font-size: 14px;
transition: background-color 0.3s;
&:hover {
background-color: rgba(64, 158, 255, 0.1);
}
&::before {
content: '';
display: inline-block;
width: 6px;
height: 6px;
background-color: #409eff;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
}
}
</style>