feat:优化了门店部门财务功能,新增开票功能
This commit is contained in:
42
apps/web-antd/src/views/finance/invoice-center/api/index.ts
Normal file
42
apps/web-antd/src/views/finance/invoice-center/api/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const prefix = 'invoice-center/';
|
||||
|
||||
/** 开票申请列表 */
|
||||
export async function getInvoiceCenterListApi(data: any) {
|
||||
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||
}
|
||||
|
||||
/** 开票详情 */
|
||||
export async function getInvoiceCenterDetailApi(data: any) {
|
||||
return requestClient.get<any>(`${prefix}detail`, { params: data });
|
||||
}
|
||||
|
||||
/** 受理 */
|
||||
export async function acceptInvoiceApi(data: { id: number }) {
|
||||
return requestClient.post<any>(`${prefix}accept`, data);
|
||||
}
|
||||
|
||||
/** 拒绝 */
|
||||
export async function rejectInvoiceApi(data: { id: number; reject_reason: string }) {
|
||||
return requestClient.post<any>(`${prefix}reject`, data);
|
||||
}
|
||||
|
||||
/** 完成开票(上传文件并按需发邮) */
|
||||
export async function completeInvoiceApi(data: {
|
||||
id: number;
|
||||
invoice_code: string;
|
||||
invoice_files: string[];
|
||||
}) {
|
||||
return requestClient.post<any>(`${prefix}complete`, data);
|
||||
}
|
||||
|
||||
/** 重发邮件 */
|
||||
export async function resendInvoiceEmailApi(data: { id: number }) {
|
||||
return requestClient.post<any>(`${prefix}resend-email`, data);
|
||||
}
|
||||
|
||||
/** 各状态数量(Tab 徽标) */
|
||||
export async function getInvoiceStatusStatsApi() {
|
||||
return requestClient.get<Record<string, number>>(`${prefix}status-stats`);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 完成开票:填写票据代码 + 拖拽/粘贴上传发票文件(图片/PDF)
|
||||
*/
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Input, message } from 'ant-design-vue';
|
||||
|
||||
import UploadDraggerPaste from '#/components/form/components/upload-dragger-paste.vue';
|
||||
|
||||
import { completeInvoiceApi } from '../api';
|
||||
|
||||
const invoiceCode = ref('');
|
||||
const fileUrls = ref<string[]>([]);
|
||||
const rowId = ref(0);
|
||||
const receiveType = ref(1);
|
||||
const email = ref('');
|
||||
const gridApi = ref<any>();
|
||||
|
||||
const emailHint = computed(() => {
|
||||
if (email.value) {
|
||||
return `填写了邮箱则会发送至:${email.value};同时可在小程序查收`;
|
||||
}
|
||||
return '未填写邮箱则仅小程序可查收;完成后将通知患者';
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
async onConfirm() {
|
||||
const code = invoiceCode.value.trim();
|
||||
if (!code) {
|
||||
message.warning('请填写票据代码');
|
||||
return;
|
||||
}
|
||||
if (!fileUrls.value.length) {
|
||||
message.warning('请上传发票文件');
|
||||
return;
|
||||
}
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
try {
|
||||
const res: any = await completeInvoiceApi({
|
||||
id: rowId.value,
|
||||
invoice_code: code,
|
||||
invoice_files: fileUrls.value,
|
||||
});
|
||||
if (res?.subscribe_warn) {
|
||||
message.warning(String(res.subscribe_warn));
|
||||
} else {
|
||||
message.success('开票完成');
|
||||
}
|
||||
gridApi.value?.reload?.();
|
||||
modalApi.close();
|
||||
} finally {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) return;
|
||||
const data = modalApi.getData<Record<string, any>>() || {};
|
||||
rowId.value = Number(data.id || 0);
|
||||
receiveType.value = Number(data.receive_type || 1);
|
||||
email.value = String(data.email || '');
|
||||
gridApi.value = data.gridApi;
|
||||
invoiceCode.value = '';
|
||||
fileUrls.value = [];
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="完成开票" class="w-[560px]">
|
||||
<div class="space-y-4 py-2">
|
||||
<div class="text-sm text-gray-500">{{ emailHint }}</div>
|
||||
<div>
|
||||
<div class="mb-1 font-medium">票据代码</div>
|
||||
<Input v-model:value="invoiceCode" placeholder="请输入发票代码/号码" allow-clear />
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1 font-medium">发票文件(图片或 PDF)</div>
|
||||
<UploadDraggerPaste
|
||||
v-model="fileUrls"
|
||||
accept=".jpg,.jpeg,.png,.webp,.pdf,image/*,application/pdf"
|
||||
tip="点击或拖拽发票文件到此处(建议上传图片,小程序内可直接预览;PDF 将通过微信打开)"
|
||||
:max-count="9"
|
||||
:max-size-mb="20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 拒绝开票申请弹窗
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Input, message } from 'ant-design-vue';
|
||||
|
||||
import { rejectInvoiceApi } from '../api';
|
||||
|
||||
const reason = ref('');
|
||||
const rowId = ref(0);
|
||||
const gridApi = ref<any>();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
async onConfirm() {
|
||||
const text = reason.value.trim();
|
||||
if (!text) {
|
||||
message.warning('请填写拒绝原因');
|
||||
return;
|
||||
}
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
try {
|
||||
await rejectInvoiceApi({ id: rowId.value, reject_reason: text });
|
||||
message.success('已拒绝');
|
||||
gridApi.value?.reload?.();
|
||||
modalApi.close();
|
||||
} finally {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) return;
|
||||
const data = modalApi.getData<Record<string, any>>() || {};
|
||||
rowId.value = Number(data.id || 0);
|
||||
gridApi.value = data.gridApi;
|
||||
reason.value = '';
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="拒绝开票申请" class="w-[480px]">
|
||||
<div class="py-2">
|
||||
<div class="mb-2 text-sm text-gray-500">拒绝后患者可重新申请开票</div>
|
||||
<Input.TextArea v-model:value="reason" :rows="4" placeholder="请填写拒绝原因" />
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
/**
|
||||
* 开票中心列表搜索表单
|
||||
*/
|
||||
export const formOptions: VbenFormProps = {
|
||||
collapsed: false,
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '订单号 / 抬头 / 邮箱',
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'keyword',
|
||||
label: '关键词',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '订单号',
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'order_no',
|
||||
label: '订单号',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '接收方式',
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '小程序下载', value: 1 },
|
||||
{ label: '邮箱接收', value: 2 },
|
||||
],
|
||||
},
|
||||
defaultValue: '',
|
||||
fieldName: 'receive_type',
|
||||
label: '接收方式',
|
||||
},
|
||||
],
|
||||
showCollapseButton: false,
|
||||
submitButtonOptions: { content: '查询' },
|
||||
submitOnChange: false,
|
||||
submitOnEnter: true,
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { getInvoiceCenterListApi } from '../api';
|
||||
|
||||
interface RowType {
|
||||
id: number;
|
||||
order_no: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开票中心表格配置;statusFilter 由页面注入当前 Tab 状态
|
||||
*/
|
||||
export function createGridOptions(getStatus: () => string | number): VxeGridProps<RowType> {
|
||||
return {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: '',
|
||||
},
|
||||
columns: [
|
||||
{ field: 'id', title: 'ID', width: 70 },
|
||||
{ field: 'order_no', title: '订单号', minWidth: 160 },
|
||||
{ field: 'store_name', title: '诊所', minWidth: 140 },
|
||||
{ field: 'title_type_text', title: '抬头类型', width: 90 },
|
||||
{ field: 'title_name', title: '抬头名称', minWidth: 120 },
|
||||
{ field: 'amount', title: '金额', width: 100 },
|
||||
{ field: 'receive_type_text', title: '接收方式', width: 100 },
|
||||
{ field: 'email', title: '邮箱', minWidth: 160 },
|
||||
{ field: 'status_text', title: '状态', width: 90, slots: { default: 'status' } },
|
||||
{ field: 'created_at', title: '申请时间', minWidth: 160 },
|
||||
{
|
||||
title: '操作',
|
||||
align: 'right',
|
||||
slots: { default: 'action' },
|
||||
width: 260,
|
||||
fixed: 'right',
|
||||
},
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
columnConfig: { useKey: true },
|
||||
rowConfig: { useKey: true },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
const status = getStatus();
|
||||
return await getInvoiceCenterListApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
status: status === '' || status === 'all' ? '' : status,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 'auto',
|
||||
border: false,
|
||||
toolbarConfig: {
|
||||
// @ts-ignore
|
||||
search: true,
|
||||
refresh: true,
|
||||
print: false,
|
||||
export: false,
|
||||
zoom: true,
|
||||
slots: { buttons: 'toolbar-buttons' },
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
}
|
||||
316
apps/web-antd/src/views/finance/invoice-center/index.vue
Normal file
316
apps/web-antd/src/views/finance/invoice-center/index.vue
Normal file
@@ -0,0 +1,316 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 财务开票中心:待受理 / 已受理 / 已完成 / 已拒绝 / 全部
|
||||
* Tab 记忆 localStorage;各状态徽标数量(0 不显示)
|
||||
* 发票文件:图片用 Image 预览,PDF 用 Modal + blob iframe
|
||||
*/
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Badge, Button, Image, Modal, Space, Tabs, Tag, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
import {
|
||||
acceptInvoiceApi,
|
||||
getInvoiceStatusStatsApi,
|
||||
resendInvoiceEmailApi,
|
||||
} from './api';
|
||||
import CompleteModal from './components/CompleteModal.vue';
|
||||
import RejectModal from './components/RejectModal.vue';
|
||||
import { formOptions } from './config/search';
|
||||
import { createGridOptions } from './config/table';
|
||||
|
||||
defineOptions({ name: 'FinanceInvoiceCenter' });
|
||||
|
||||
/** Tab 记忆键,刷新后恢复上次选中状态 */
|
||||
const TAB_STORAGE_KEY = 'finance-invoice-center-tab';
|
||||
const TAB_KEYS = ['0', '1', '2', '9', 'all'] as const;
|
||||
|
||||
/**
|
||||
* 从 localStorage 读取合法 Tab,非法则默认待受理
|
||||
*/
|
||||
function readStoredTab(): string {
|
||||
const stored = localStorage.getItem(TAB_STORAGE_KEY);
|
||||
if (stored && (TAB_KEYS as readonly string[]).includes(stored)) {
|
||||
return stored;
|
||||
}
|
||||
return '0';
|
||||
}
|
||||
|
||||
const activeTab = ref<string>(readStoredTab());
|
||||
const statusFilter = computed(() =>
|
||||
activeTab.value === 'all' ? '' : activeTab.value,
|
||||
);
|
||||
|
||||
/** 各状态数量,供徽标展示 */
|
||||
const statusStats = ref<Record<string, number>>({
|
||||
'0': 0,
|
||||
'1': 0,
|
||||
'2': 0,
|
||||
'9': 0,
|
||||
});
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions: createGridOptions(() => statusFilter.value),
|
||||
});
|
||||
|
||||
/**
|
||||
* 列表 + 徽标一并刷新(弹窗回调与操作后复用)
|
||||
*/
|
||||
function reloadAll() {
|
||||
gridApi.reload();
|
||||
loadStatusStats();
|
||||
}
|
||||
|
||||
/** 传给弹窗的伪 gridApi,reload 时同步刷新徽标 */
|
||||
const gridApiWithStats = {
|
||||
reload: reloadAll,
|
||||
};
|
||||
|
||||
const [RejectModalComp, rejectModalApi] = useVbenModal({
|
||||
connectedComponent: RejectModal,
|
||||
});
|
||||
const [CompleteModalComp, completeModalApi] = useVbenModal({
|
||||
connectedComponent: CompleteModal,
|
||||
});
|
||||
|
||||
/** 文件预览弹窗 */
|
||||
const previewVisible = ref(false);
|
||||
const previewTitle = ref('');
|
||||
const previewIsImage = ref(false);
|
||||
const previewImageUrl = ref('');
|
||||
const previewPdfUrl = ref('');
|
||||
const previewLoading = ref(false);
|
||||
|
||||
/**
|
||||
* 拉取各状态数量;失败静默,避免打断列表
|
||||
*/
|
||||
async function loadStatusStats() {
|
||||
try {
|
||||
const res: any = await getInvoiceStatusStatsApi();
|
||||
const data = res?.data ?? res ?? {};
|
||||
statusStats.value = {
|
||||
'0': Number(data['0'] ?? data[0] ?? 0),
|
||||
'1': Number(data['1'] ?? data[1] ?? 0),
|
||||
'2': Number(data['2'] ?? data[2] ?? 0),
|
||||
'9': Number(data['9'] ?? data[9] ?? 0),
|
||||
};
|
||||
} catch {
|
||||
// 徽标非关键路径,忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
function handleTabChange(key: string | number) {
|
||||
const next = String(key);
|
||||
activeTab.value = next;
|
||||
localStorage.setItem(TAB_STORAGE_KEY, next);
|
||||
reloadAll();
|
||||
}
|
||||
|
||||
/** 受理 */
|
||||
async function handleAccept(row: any) {
|
||||
Modal.confirm({
|
||||
title: '确认受理该开票申请?',
|
||||
content: '受理后将写入站内通知(订阅消息在开票完成时发送)',
|
||||
async onOk() {
|
||||
await acceptInvoiceApi({ id: row.id });
|
||||
message.success('受理成功');
|
||||
reloadAll();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function openReject(row: any) {
|
||||
rejectModalApi.setData({ id: row.id, gridApi: gridApiWithStats });
|
||||
rejectModalApi.open();
|
||||
}
|
||||
|
||||
function openComplete(row: any) {
|
||||
completeModalApi.setData({
|
||||
id: row.id,
|
||||
receive_type: row.receive_type,
|
||||
email: row.email,
|
||||
gridApi: gridApiWithStats,
|
||||
});
|
||||
completeModalApi.open();
|
||||
}
|
||||
|
||||
/** 重发邮件(有邮箱即可,不限 receive_type) */
|
||||
async function handleResend(row: any) {
|
||||
Modal.confirm({
|
||||
title: '确认重发邮件?',
|
||||
content: `将再次发送至 ${row.email || ''}`,
|
||||
async onOk() {
|
||||
await resendInvoiceEmailApi({ id: row.id });
|
||||
message.success('邮件已重发');
|
||||
reloadAll();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function statusColor(status: number) {
|
||||
if (status === 0) return 'processing';
|
||||
if (status === 1) return 'warning';
|
||||
if (status === 2) return 'success';
|
||||
if (status === 9) return 'error';
|
||||
return 'default';
|
||||
}
|
||||
|
||||
function isImageUrl(url: string) {
|
||||
return /\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(url || '');
|
||||
}
|
||||
|
||||
function revokePdfBlob() {
|
||||
if (previewPdfUrl.value && previewPdfUrl.value.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(previewPdfUrl.value);
|
||||
}
|
||||
previewPdfUrl.value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览发票文件:图片走 Image;PDF fetch 成 blob 后 iframe
|
||||
*/
|
||||
async function previewFiles(files: string[]) {
|
||||
if (!files?.length) {
|
||||
message.info('暂无文件');
|
||||
return;
|
||||
}
|
||||
const first = files[0];
|
||||
revokePdfBlob();
|
||||
previewTitle.value = first.split('/').pop() || '发票文件';
|
||||
if (isImageUrl(first)) {
|
||||
previewIsImage.value = true;
|
||||
previewImageUrl.value = first;
|
||||
previewVisible.value = true;
|
||||
return;
|
||||
}
|
||||
previewIsImage.value = false;
|
||||
previewLoading.value = true;
|
||||
previewVisible.value = true;
|
||||
try {
|
||||
const resp = await fetch(first);
|
||||
if (!resp.ok) throw new Error('文件加载失败');
|
||||
const blob = await resp.blob();
|
||||
const pdfBlob =
|
||||
blob.type && blob.type !== 'application/octet-stream'
|
||||
? blob
|
||||
: new Blob([blob], { type: 'application/pdf' });
|
||||
previewPdfUrl.value = URL.createObjectURL(pdfBlob);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '预览失败,可尝试重新上传为图片');
|
||||
previewVisible.value = false;
|
||||
} finally {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onPreviewClose() {
|
||||
previewVisible.value = false;
|
||||
revokePdfBlob();
|
||||
previewImageUrl.value = '';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStatusStats();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
revokePdfBlob();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height title="开票中心">
|
||||
<RejectModalComp />
|
||||
<CompleteModalComp />
|
||||
<Tabs :active-key="activeTab" class="mb-2" @change="handleTabChange">
|
||||
<Tabs.TabPane key="0">
|
||||
<template #tab>
|
||||
<Badge :count="statusStats['0']" :offset="[10, 0]" :number-style="{ fontSize: '12px' }">
|
||||
<span>待受理</span>
|
||||
</Badge>
|
||||
</template>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="1">
|
||||
<template #tab>
|
||||
<Badge :count="statusStats['1']" :offset="[10, 0]" :number-style="{ fontSize: '12px' }">
|
||||
<span>已受理</span>
|
||||
</Badge>
|
||||
</template>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="2">
|
||||
<template #tab>
|
||||
<Badge :count="statusStats['2']" :offset="[10, 0]" :number-style="{ fontSize: '12px' }">
|
||||
<span>已完成</span>
|
||||
</Badge>
|
||||
</template>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="9">
|
||||
<template #tab>
|
||||
<Badge :count="statusStats['9']" :offset="[10, 0]" :number-style="{ fontSize: '12px' }">
|
||||
<span>已拒绝</span>
|
||||
</Badge>
|
||||
</template>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="all" tab="全部" />
|
||||
</Tabs>
|
||||
<Grid>
|
||||
<template #toolbar-buttons />
|
||||
<template #status="{ row }">
|
||||
<Tag :color="statusColor(row.status)">{{ row.status_text }}</Tag>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Space>
|
||||
<Button v-if="row.status === 0" type="link" size="small" @click="handleAccept(row)">
|
||||
受理
|
||||
</Button>
|
||||
<Button v-if="row.status === 0" type="link" size="small" danger @click="openReject(row)">
|
||||
拒绝
|
||||
</Button>
|
||||
<Button v-if="row.status === 1" type="link" size="small" @click="openComplete(row)">
|
||||
完成开票
|
||||
</Button>
|
||||
<Button
|
||||
v-if="row.status === 2 && row.invoice_files?.length"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="previewFiles(row.invoice_files)"
|
||||
>
|
||||
查看文件
|
||||
</Button>
|
||||
<Button
|
||||
v-if="row.status === 2 && row.email"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleResend(row)"
|
||||
>
|
||||
重发邮件
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<Modal
|
||||
:open="previewVisible"
|
||||
:title="previewTitle"
|
||||
:footer="null"
|
||||
width="80%"
|
||||
destroy-on-close
|
||||
@cancel="onPreviewClose"
|
||||
>
|
||||
<div v-if="previewLoading" class="py-16 text-center text-gray-500">加载中…</div>
|
||||
<div v-else-if="previewIsImage" class="flex justify-center">
|
||||
<Image :src="previewImageUrl" :preview="true" style="max-height: 70vh" />
|
||||
</div>
|
||||
<iframe
|
||||
v-else-if="previewPdfUrl"
|
||||
:src="previewPdfUrl"
|
||||
class="h-[70vh] w-full border-0"
|
||||
title="发票预览"
|
||||
/>
|
||||
</Modal>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -3,7 +3,7 @@ import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, Input, InputNumber, message, Radio, Space, Switch, Tabs, Tag } from 'ant-design-vue';
|
||||
import { Button, Card, Checkbox, Input, InputNumber, message, Radio, Space, Switch, Tabs, Tag } from 'ant-design-vue';
|
||||
|
||||
import type { QuickDiscountOption } from '#/utils/pricePercentAdjust';
|
||||
|
||||
@@ -37,6 +37,8 @@ const logisticsShowWhNameUser = ref(false);
|
||||
const logisticsShowWhNameClinic = ref(true);
|
||||
/** 业务员是否允许编辑/改价/配置所属诊所(默认否) */
|
||||
const salespersonStoreEditEnabled = ref(false);
|
||||
/** 开票通知渠道:subscribe / sms,默认仅订阅消息 */
|
||||
const invoiceNoticeChannels = ref<string[]>(['subscribe']);
|
||||
|
||||
function readStoredTab() {
|
||||
const stored = localStorage.getItem(TAB_STORAGE_KEY);
|
||||
@@ -45,7 +47,8 @@ function readStoredTab() {
|
||||
stored === 'input_audit' ||
|
||||
stored === 'miniprogram' ||
|
||||
stored === 'logistics' ||
|
||||
stored === 'salesperson'
|
||||
stored === 'salesperson' ||
|
||||
stored === 'invoice_notice'
|
||||
) {
|
||||
activeKey.value = stored;
|
||||
}
|
||||
@@ -110,6 +113,22 @@ async function load() {
|
||||
if (row.config_key === 'salesperson_store_edit_enabled') {
|
||||
salespersonStoreEditEnabled.value = parseBoolConfig(row.config_value, false);
|
||||
}
|
||||
if (row.config_key === 'invoice_notice_channels') {
|
||||
let channels: unknown = row.config_value;
|
||||
if (typeof channels === 'string') {
|
||||
try {
|
||||
channels = JSON.parse(channels);
|
||||
} catch {
|
||||
channels = ['subscribe'];
|
||||
}
|
||||
}
|
||||
if (Array.isArray(channels) && channels.length) {
|
||||
invoiceNoticeChannels.value = channels.map(String).filter((c) => c === 'subscribe' || c === 'sms');
|
||||
}
|
||||
if (!invoiceNoticeChannels.value.length) {
|
||||
invoiceNoticeChannels.value = ['subscribe'];
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -167,6 +186,16 @@ async function handleSave() {
|
||||
description: '允许业务员编辑/改价/配置所属诊所',
|
||||
sort: 300,
|
||||
},
|
||||
{
|
||||
config_key: 'invoice_notice_channels',
|
||||
config_value: JSON.stringify(
|
||||
invoiceNoticeChannels.value.length ? invoiceNoticeChannels.value : ['subscribe'],
|
||||
),
|
||||
value_type: 'json',
|
||||
config_group: 'invoice',
|
||||
description: '开票通知渠道:subscribe订阅消息 / sms短信',
|
||||
sort: 200,
|
||||
},
|
||||
]);
|
||||
message.success('保存成功');
|
||||
} finally {
|
||||
@@ -271,6 +300,19 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="invoice_notice" tab="开票通知">
|
||||
<div class="py-4">
|
||||
<div class="mb-2 font-medium">患者开票通知渠道</div>
|
||||
<div class="mb-2 text-sm text-gray-500">
|
||||
受理与开票完成时按所选渠道发送;可多选(全选)。默认仅小程序订阅消息。
|
||||
</div>
|
||||
<Checkbox.Group v-model:value="invoiceNoticeChannels">
|
||||
<Checkbox value="subscribe">小程序订阅消息</Checkbox>
|
||||
<Checkbox value="sms">短信</Checkbox>
|
||||
</Checkbox.Group>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
|
||||
<div class="mt-4">
|
||||
|
||||
Reference in New Issue
Block a user