1. 优化医生、诊所的设置
Some checks failed
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
CI / CI OK (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
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
Some checks failed
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
CI / CI OK (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
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
This commit is contained in:
@@ -24,6 +24,7 @@ import {downloadByData} from "#/util/tool";
|
|||||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||||
|
|
||||||
const arr = new Set([
|
const arr = new Set([
|
||||||
|
'account',
|
||||||
'accept_tel',
|
'accept_tel',
|
||||||
'doctor',
|
'doctor',
|
||||||
'express_mobile',
|
'express_mobile',
|
||||||
@@ -36,7 +37,18 @@ const arr = new Set([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// 敏感数据
|
// 敏感数据
|
||||||
const sensitiveData = new Set(['id_card', 'idcard']);
|
const sensitiveData = new Set([
|
||||||
|
'account',
|
||||||
|
'accept_tel',
|
||||||
|
'doctor',
|
||||||
|
'express_mobile',
|
||||||
|
'id_card',
|
||||||
|
'idcard',
|
||||||
|
'mobile',
|
||||||
|
'password',
|
||||||
|
'patient_mobile',
|
||||||
|
'phone',
|
||||||
|
]);
|
||||||
|
|
||||||
function createRequestClient(baseURL: string) {
|
function createRequestClient(baseURL: string) {
|
||||||
const client = new RequestClient({
|
const client = new RequestClient({
|
||||||
@@ -199,36 +211,37 @@ function getRes(obj: any, isDecode = true) {
|
|||||||
}
|
}
|
||||||
if (isDecode && sensitiveData.has(key)) {
|
if (isDecode && sensitiveData.has(key)) {
|
||||||
// 数据脱敏,自动匹配姓名、手机号、身份证
|
// 数据脱敏,自动匹配姓名、手机号、身份证
|
||||||
switch (key) {
|
// switch (key) {
|
||||||
// case 'accept_name':
|
// case 'accept_name':
|
||||||
// case 'express_name':
|
// case 'express_name':
|
||||||
// case 'express_region':
|
// case 'express_region':
|
||||||
// case 'patient': {
|
// case 'patient': {
|
||||||
// // 保留前两个字符,其余用星号代替
|
// // 保留前两个字符,其余用星号代替
|
||||||
// aseFile = aseFile.slice(0, 1).padEnd(aseFile.length, '*');
|
// aseFile = aseFile.slice(0, 1).padEnd(aseFile.length, '*');
|
||||||
// break;
|
// break;
|
||||||
// }
|
// }
|
||||||
// case 'express_mobile':
|
// case 'express_mobile':
|
||||||
// case 'mobile': {
|
// case 'mobile':
|
||||||
// // 保留前三位和后四位,中间用星号代替
|
// case 'phone': {
|
||||||
// aseFile =
|
// // 保留前三位和后四位,中间用星号代替
|
||||||
// aseFile.slice(0, 3).padEnd(aseFile.length - 4, '*') +
|
// aseFile =
|
||||||
// aseFile.slice(-4);
|
// aseFile.slice(0, 3).padEnd(aseFile.length - 4, '*') +
|
||||||
// break;
|
// aseFile.slice(-4);
|
||||||
// }
|
// break;
|
||||||
case 'id_card':
|
// }
|
||||||
case 'idcard': {
|
// case 'id_card':
|
||||||
// 保留前六位和后四位,中间用星号代替
|
// case 'idcard': {
|
||||||
aseFile =
|
// // 保留前六位和后四位,中间用星号代替
|
||||||
aseFile.slice(0, 6).padEnd(aseFile.length - 4, '*') +
|
// aseFile =
|
||||||
aseFile.slice(-4);
|
// aseFile.slice(0, 6).padEnd(aseFile.length - 4, '*') +
|
||||||
break;
|
// aseFile.slice(-4);
|
||||||
}
|
// break;
|
||||||
default: {
|
// }
|
||||||
// 默认情况下,全部用星号代替
|
// default: {
|
||||||
break;
|
// // 默认情况下,全部用星号代替
|
||||||
}
|
// break;
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
obj[key] = aseFile;
|
obj[key] = aseFile;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
|||||||
import { Modal, Upload } from 'ant-design-vue';
|
import { Modal, Upload } from 'ant-design-vue';
|
||||||
import type { UploadFile } from 'ant-design-vue';
|
import type { UploadFile } from 'ant-design-vue';
|
||||||
|
|
||||||
// 导入上传相关API和工具函数
|
import GalleryPickLink from '#/components/form/components/gallery-pick-link.vue';
|
||||||
import { uploadFile } from '#/api/core/upload';
|
import { uploadFile } from '#/api/core/upload';
|
||||||
import { uploadToOss } from '#/utils/oss-upload';
|
import { uploadToOss } from '#/utils/oss-upload';
|
||||||
import {
|
import {
|
||||||
@@ -279,6 +279,29 @@ const handlePreview = async (file: UploadFile) => {
|
|||||||
previewTitle.value = file.name || file.url?.split('/').pop() || '';
|
previewTitle.value = file.name || file.url?.split('/').pop() || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function onGallerySelect(urls: string[]) {
|
||||||
|
const remain = props.maxCount - fileList.value.length;
|
||||||
|
const picked = props.multiple ? urls.slice(0, remain) : urls.slice(0, 1);
|
||||||
|
for (const url of picked) {
|
||||||
|
if (fileList.value.some((f) => f.url === url)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fileList.value = [
|
||||||
|
...fileList.value,
|
||||||
|
{
|
||||||
|
uid: `gallery-${url}`,
|
||||||
|
name: url.split('/').pop() || 'file',
|
||||||
|
status: 'done' as const,
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
updateModelValue();
|
||||||
|
nextTick(() => {
|
||||||
|
initSortable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 计算是否显示上传按钮(与upload-image.vue保持一致)
|
// 计算是否显示上传按钮(与upload-image.vue保持一致)
|
||||||
const showUploadButton = computed(() =>
|
const showUploadButton = computed(() =>
|
||||||
props.multiple
|
props.multiple
|
||||||
@@ -286,6 +309,8 @@ const showUploadButton = computed(() =>
|
|||||||
: fileList.value.length === 0,
|
: fileList.value.length === 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const galleryRemainCount = computed(() => Math.max(props.maxCount - fileList.value.length, 0));
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
initSortable();
|
initSortable();
|
||||||
@@ -294,7 +319,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="uploadListRef">
|
<div ref="uploadListRef" class="upload-image-wrap">
|
||||||
<Upload
|
<Upload
|
||||||
:file-list="fileList"
|
:file-list="fileList"
|
||||||
:before-upload="beforeImageUpload"
|
:before-upload="beforeImageUpload"
|
||||||
@@ -316,13 +341,20 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</Upload>
|
</Upload>
|
||||||
|
|
||||||
|
<GalleryPickLink
|
||||||
|
v-if="showUploadButton"
|
||||||
|
:multiple="multiple"
|
||||||
|
:max-count="galleryRemainCount"
|
||||||
|
@select="onGallerySelect"
|
||||||
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
v-model:visible="previewVisible"
|
v-model:visible="previewVisible"
|
||||||
:title="previewTitle"
|
:title="previewTitle"
|
||||||
footer=""
|
footer=""
|
||||||
width="60%"
|
width="60%"
|
||||||
>
|
>
|
||||||
|
<img alt="预览图片" style="width: 100%" :src="previewImage" />
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export interface RegisterOrderItem {
|
|||||||
is_pay: 0 | 1;
|
is_pay: 0 | 1;
|
||||||
type: number;
|
type: number;
|
||||||
status: number;
|
status: number;
|
||||||
|
refund_status?: number;
|
||||||
|
refund_time?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
doctor_info?: { name: string; depart?: { name: string } };
|
doctor_info?: { name: string; depart?: { name: string } };
|
||||||
user_patient?: { name: string };
|
user_patient?: { name: string };
|
||||||
@@ -25,3 +27,7 @@ export async function getRegisterListApi(data: Record<string, unknown>) {
|
|||||||
{ params: data },
|
{ params: data },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function refundRegisterApi(data: Record<string, unknown>) {
|
||||||
|
return requestClient.post<any>(`${prefix}refund`, data);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<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 { refundRegisterApi } from '../api';
|
||||||
|
import { modalRefundFormProps } from '../config/form';
|
||||||
|
|
||||||
|
const isUpdate = ref(false);
|
||||||
|
const gridApi = ref();
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm(modalRefundFormProps);
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
formApi.validate().then(async (e: any) => {
|
||||||
|
if (e.valid) {
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
modalApi.setState({ loading: true, confirmLoading: true });
|
||||||
|
refundRegisterApi(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, update } = modalApi.getData<Record<string, any>>();
|
||||||
|
if (values) {
|
||||||
|
isUpdate.value = update;
|
||||||
|
formApi.setValues(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
title="退款"
|
||||||
|
class="w-[30%]"
|
||||||
|
>
|
||||||
|
<Form />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -1,51 +1,32 @@
|
|||||||
import type { VbenFormProps } from '#/adapter/form';
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
export const modalFormProps: VbenFormProps = {
|
export const modalRefundFormProps: VbenFormProps = {
|
||||||
wrapperClass: 'grid-cols-12', // 24栅格,
|
wrapperClass: 'grid-cols-12',
|
||||||
commonConfig: {
|
commonConfig: {
|
||||||
formItemClass: 'col-span-12',
|
formItemClass: 'col-span-12',
|
||||||
// 所有表单项
|
|
||||||
componentProps: {
|
componentProps: {
|
||||||
class: 'w-full',
|
class: 'w-full',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// handleSubmit: onSubmit,
|
|
||||||
layout: 'horizontal',
|
layout: 'horizontal',
|
||||||
schema: [
|
schema: [
|
||||||
{
|
{
|
||||||
component: 'VbenInput',
|
component: 'VbenInput',
|
||||||
fieldName: 'id',
|
fieldName: 'register_id',
|
||||||
label: 'ID',
|
label: 'ID',
|
||||||
|
formItemClass: 'col-span-6',
|
||||||
dependencies: {
|
dependencies: {
|
||||||
show: false,
|
show: false,
|
||||||
triggerFields: ['id'],
|
triggerFields: ['register_id'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'VbenInput',
|
component: 'Textarea',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
placeholder: '请输入角色昵称',
|
placeholder: '请输入退款备注',
|
||||||
},
|
},
|
||||||
fieldName: 'name',
|
fieldName: 'refund_reason',
|
||||||
label: '昵称',
|
label: '退款备注',
|
||||||
rules: 'required',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'VbenInput',
|
|
||||||
componentProps: {
|
|
||||||
placeholder: '请输入角色代码',
|
|
||||||
},
|
|
||||||
fieldName: 'value',
|
|
||||||
label: '角色代码',
|
|
||||||
rules: 'required',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'VbenInput',
|
|
||||||
componentProps: {
|
|
||||||
placeholder: '请输入角色说明',
|
|
||||||
},
|
|
||||||
fieldName: 'desc',
|
|
||||||
label: '角色说明',
|
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -21,15 +21,16 @@ export const gridOptions: VxeGridProps<RegisterOrderItem> = {
|
|||||||
{ field: 'prescription', title: '处方', width: 240, slots: { default: 'prescription'} },
|
{ field: 'prescription', title: '处方', width: 240, slots: { default: 'prescription'} },
|
||||||
{ field: 'price', title: '挂号价格', width: 110, slots: { default: 'price' } },
|
{ field: 'price', title: '挂号价格', width: 110, slots: { default: 'price' } },
|
||||||
{ field: 'is_pay', title: '是否支付', width: 100, slots: { default: 'is_pay' } },
|
{ field: 'is_pay', title: '是否支付', width: 100, slots: { default: 'is_pay' } },
|
||||||
|
{ field: 'refund_status', title: '退款状态', width: 100, slots: { default: 'refund_status' } },
|
||||||
{ field: 'status', title: '状态', slots: { default: 'status' } },
|
{ field: 'status', title: '状态', slots: { default: 'status' } },
|
||||||
{ field: 'created_at', title: '创建时间' },
|
{ field: 'created_at', title: '创建时间' },
|
||||||
// {
|
{
|
||||||
// type: 'html',
|
type: 'html',
|
||||||
// title: '操作',
|
title: '操作',
|
||||||
// align: 'right',
|
align: 'right',
|
||||||
// slots: { default: 'action' },
|
slots: { default: 'action' },
|
||||||
// width: 200,
|
width: 200,
|
||||||
// },
|
},
|
||||||
],
|
],
|
||||||
keepSource: true,
|
keepSource: true,
|
||||||
pagerConfig: {},
|
pagerConfig: {},
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
import type { VxeGridListeners } from "#/adapter/vxe-table";
|
||||||
|
|
||||||
import { nextTick, onMounted, ref } from 'vue';
|
import { nextTick, onMounted, ref } from "vue";
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
import { Page, useVbenModal } from '@vben/common-ui';
|
import { Page, useVbenModal } from "@vben/common-ui";
|
||||||
|
|
||||||
import { Button, Image, message, Modal as AntdModal, Tag } from 'ant-design-vue';
|
import { Button, Image, message, Modal as AntdModal, Tag } from "ant-design-vue";
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from "#/adapter/vxe-table";
|
||||||
import { TableAction } from '#/components/table-action';
|
import { TableAction } from "#/components/table-action";
|
||||||
import { simulatePayApi } from '#/views/business/order/api/order-ops';
|
import { simulatePayApi } from "#/views/business/order/api/order-ops";
|
||||||
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
|
import PrescriptionDetail from "#/views/doctor/doctor-reception/components/PrescriptionDetail.vue";
|
||||||
|
|
||||||
import { formOptions } from './config/search';
|
import { formOptions } from "./config/search";
|
||||||
import { gridOptions } from './config/table';
|
import { gridOptions } from "./config/table";
|
||||||
|
import Refund from "./components/refund.vue";
|
||||||
|
|
||||||
const hasTopTableDropDownActions = ref(false);
|
const hasTopTableDropDownActions = ref(false);
|
||||||
|
|
||||||
@@ -28,13 +29,13 @@ const gridEvents: VxeGridListeners<any> = {
|
|||||||
// eslint-disable-next-line no-use-before-define
|
// eslint-disable-next-line no-use-before-define
|
||||||
const records = gridApi.grid.getCheckboxRecords();
|
const records = gridApi.grid.getCheckboxRecords();
|
||||||
hasTopTableDropDownActions.value = records.length > 0;
|
hasTopTableDropDownActions.value = records.length > 0;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [Grid, gridApi] = useVbenVxeGrid({
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
formOptions,
|
formOptions,
|
||||||
gridOptions,
|
gridOptions,
|
||||||
gridEvents,
|
gridEvents
|
||||||
});
|
});
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -52,34 +53,52 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [PrescriptionDetailModal, PrescriptionDetailModalApi] = useVbenModal({
|
const [PrescriptionDetailModal, PrescriptionDetailModalApi] = useVbenModal({
|
||||||
connectedComponent: PrescriptionDetail,
|
connectedComponent: PrescriptionDetail
|
||||||
|
});
|
||||||
|
|
||||||
|
const [RefundModal, RefundModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Refund
|
||||||
});
|
});
|
||||||
|
|
||||||
function openPrescriptionDetail(id: number) {
|
function openPrescriptionDetail(id: number) {
|
||||||
PrescriptionDetailModalApi.setData({
|
PrescriptionDetailModalApi.setData({
|
||||||
values: id,
|
values: id
|
||||||
});
|
});
|
||||||
PrescriptionDetailModalApi.open();
|
PrescriptionDetailModalApi.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSimulatePay(row: Record<string, any>) {
|
function handleSimulatePay(row: Record<string, any>) {
|
||||||
AntdModal.confirm({
|
AntdModal.confirm({
|
||||||
title: '模拟支付',
|
title: "模拟支付",
|
||||||
content: `确认为挂号订单 ${row.order_no} 模拟易票联支付回调?`,
|
content: `确认为挂号订单 ${row.order_no} 模拟易票联支付回调?`,
|
||||||
okText: '确认',
|
okText: "确认",
|
||||||
cancelText: '取消',
|
cancelText: "取消",
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
await simulatePayApi({ order_type: 'register', order_id: row.id });
|
await simulatePayApi({ order_type: "register", order_id: row.id });
|
||||||
message.success('模拟支付成功');
|
message.success("模拟支付成功");
|
||||||
await gridApi.query();
|
await gridApi.query();
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openRefundModal(id: number) {
|
||||||
|
RefundModalApi.setData({
|
||||||
|
values: {
|
||||||
|
register_id: id
|
||||||
|
},
|
||||||
|
gridApi
|
||||||
|
});
|
||||||
|
RefundModalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function canRefund(row: Record<string, any>) {
|
||||||
|
return row.is_pay === 1 && row.refund_status !== 1 && row.refund_status !== 3;
|
||||||
|
}
|
||||||
|
|
||||||
function formatRegisterPrice(price: unknown) {
|
function formatRegisterPrice(price: unknown) {
|
||||||
const n = Number(price);
|
const n = Number(price);
|
||||||
if (!Number.isFinite(n)) {
|
if (!Number.isFinite(n)) {
|
||||||
return '—';
|
return "—";
|
||||||
}
|
}
|
||||||
return `¥${n.toFixed(2)}`;
|
return `¥${n.toFixed(2)}`;
|
||||||
}
|
}
|
||||||
@@ -88,6 +107,7 @@ function formatRegisterPrice(price: unknown) {
|
|||||||
<template>
|
<template>
|
||||||
<Page auto-content-height title="订单管理">
|
<Page auto-content-height title="订单管理">
|
||||||
<PrescriptionDetailModal />
|
<PrescriptionDetailModal />
|
||||||
|
<RefundModal />
|
||||||
<Grid>
|
<Grid>
|
||||||
<template #toolbar-buttons>
|
<template #toolbar-buttons>
|
||||||
<TableAction :actions="[]" :drop-down-actions="[]">
|
<TableAction :actions="[]" :drop-down-actions="[]">
|
||||||
@@ -114,7 +134,7 @@ function formatRegisterPrice(price: unknown) {
|
|||||||
:height="28"
|
:height="28"
|
||||||
class="rounded-full object-cover"
|
class="rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
<span>{{ row.salesperson.nick_name || '—' }}</span>
|
<span>{{ row.salesperson.nick_name || "—" }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-else class="text-sm text-gray-500">无推广员</span>
|
<span v-else class="text-sm text-gray-500">无推广员</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -123,9 +143,14 @@ function formatRegisterPrice(price: unknown) {
|
|||||||
</template>
|
</template>
|
||||||
<template #is_pay="{ row }">
|
<template #is_pay="{ row }">
|
||||||
<Tag :color="row.is_pay === 1 ? 'green' : 'red'">
|
<Tag :color="row.is_pay === 1 ? 'green' : 'red'">
|
||||||
{{ row.is_pay === 1 ? '已支付' : '未支付' }}
|
{{ row.is_pay === 1 ? "已支付" : "未支付" }}
|
||||||
</Tag>
|
</Tag>
|
||||||
</template>
|
</template>
|
||||||
|
<template #refund_status="{ row }">
|
||||||
|
<Tag v-if="row.refund_status === 1" color="pink">退款中</Tag>
|
||||||
|
<Tag v-else-if="row.refund_status === 3" color="green">已退款</Tag>
|
||||||
|
<Tag v-else color="default">未退款</Tag>
|
||||||
|
</template>
|
||||||
<template #type="{ row }">
|
<template #type="{ row }">
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<Tag v-if="row.type === 0" color="purple">线下就诊</Tag>
|
<Tag v-if="row.type === 0" color="purple">线下就诊</Tag>
|
||||||
@@ -149,17 +174,16 @@ function formatRegisterPrice(price: unknown) {
|
|||||||
<template #toolbar-tools></template>
|
<template #toolbar-tools></template>
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<TableAction
|
<TableAction
|
||||||
:actions="[]"
|
:actions="[
|
||||||
:drop-down-actions="[
|
|
||||||
{
|
{
|
||||||
label: '模拟支付',
|
label: '退款',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
icon: 'mdi:cash-check',
|
icon: 'mingcute:refund-dollar-fill',
|
||||||
auth: ['Super Admin', 'sys:user:save'],
|
ifShow: canRefund(row),
|
||||||
ifShow: row.is_pay === 0,
|
onClick: openRefundModal.bind(null, row.id),
|
||||||
onClick: handleSimulatePay.bind(null, row),
|
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
|
:drop-down-actions="[]"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'special-prescription-category/';
|
||||||
|
|
||||||
|
export async function getSpecialPrescriptionCategoryList(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSpecialPrescriptionCategoryOption(data?: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}option`, { params: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSpecialPrescriptionCategoryInfo(id: number) {
|
||||||
|
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSpecialPrescriptionCategory(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}create`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSpecialPrescriptionCategory(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}update`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteSpecialPrescriptionCategory(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}delete`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSpecialPrescriptionCategorySortOrder(data: { ids: number[] }) {
|
||||||
|
return requestClient.post<any>(`${prefix}update-sort-order`, data);
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<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 {
|
||||||
|
createSpecialPrescriptionCategory,
|
||||||
|
updateSpecialPrescriptionCategory,
|
||||||
|
} from '../api';
|
||||||
|
import { modalFormProps } from '../config/form';
|
||||||
|
|
||||||
|
const isUpdate = ref(false);
|
||||||
|
const gridApi = ref();
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
formApi.validate().then(async (e: any) => {
|
||||||
|
if (e.valid) {
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
modalApi.setState({ loading: true, confirmLoading: true });
|
||||||
|
const submitApi = isUpdate.value
|
||||||
|
? updateSpecialPrescriptionCategory
|
||||||
|
: createSpecialPrescriptionCategory;
|
||||||
|
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, update } = modalApi.getData<Record<string, any>>();
|
||||||
|
if (values && update) {
|
||||||
|
isUpdate.value = true;
|
||||||
|
formApi.setValues({
|
||||||
|
id: values.id || '',
|
||||||
|
name: values.name || '',
|
||||||
|
status: values.status ?? 1,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
isUpdate.value = false;
|
||||||
|
formApi.resetForm();
|
||||||
|
formApi.setValues({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
status: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
formApi.resetForm();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal :title="`${isUpdate === true ? '编辑' : '新增'}特色方分类`" class="w-[50%]">
|
||||||
|
<Form />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { Icon } from '#/components/icon';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getSpecialPrescriptionCategoryList, updateSpecialPrescriptionCategorySortOrder } from '../api';
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
const ids = sortableList.value.map((item) => item.id);
|
||||||
|
if (!ids.length) {
|
||||||
|
message.warning('没有可排序的数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.setState({ loading: true, confirmLoading: true });
|
||||||
|
updateSpecialPrescriptionCategorySortOrder({ ids })
|
||||||
|
.then(() => {
|
||||||
|
message.success('排序更新成功');
|
||||||
|
const gridApi = modalApi.getData()?.gridApi;
|
||||||
|
if (gridApi) {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
modalApi.close();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message.error('排序更新失败');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
modalApi.setState({ loading: false, confirmLoading: false });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
loadList();
|
||||||
|
} else {
|
||||||
|
sortableList.value = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortableList = ref<any[]>([]);
|
||||||
|
const draggedIndex = ref(-1);
|
||||||
|
const dragOverIndex = ref(-1);
|
||||||
|
|
||||||
|
const loadList = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getSpecialPrescriptionCategoryList({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1000,
|
||||||
|
});
|
||||||
|
const data = Array.isArray(res?.items) ? res.items : [];
|
||||||
|
if (!data.length) {
|
||||||
|
message.warning('暂无数据可排序');
|
||||||
|
}
|
||||||
|
sortableList.value = [...data].sort((a, b) => (a.sort || 0) - (b.sort || 0));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载分类失败:', error);
|
||||||
|
message.error('加载数据失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragStart = (event: DragEvent, index: number) => {
|
||||||
|
draggedIndex.value = index;
|
||||||
|
dragOverIndex.value = -1;
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnter = (index: number) => {
|
||||||
|
if (draggedIndex.value !== -1 && draggedIndex.value !== index) {
|
||||||
|
dragOverIndex.value = index;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragOver = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.dropEffect = 'move';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDrop = (event: DragEvent, targetIndex: number) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (draggedIndex.value === -1 || draggedIndex.value === targetIndex) {
|
||||||
|
draggedIndex.value = -1;
|
||||||
|
dragOverIndex.value = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = [...sortableList.value];
|
||||||
|
const itemToMove = list[draggedIndex.value];
|
||||||
|
list.splice(draggedIndex.value, 1);
|
||||||
|
let finalIndex = targetIndex;
|
||||||
|
if (draggedIndex.value < targetIndex) {
|
||||||
|
finalIndex = targetIndex - 1;
|
||||||
|
}
|
||||||
|
list.splice(finalIndex, 0, itemToMove);
|
||||||
|
sortableList.value = list;
|
||||||
|
draggedIndex.value = -1;
|
||||||
|
dragOverIndex.value = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = () => {
|
||||||
|
draggedIndex.value = -1;
|
||||||
|
dragOverIndex.value = -1;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="分类拖拽排序" class="w-[600px]">
|
||||||
|
<div class="py-5">
|
||||||
|
<div class="mb-5 rounded-md border-l-4 border-blue-500 bg-blue-50 px-4 py-3">
|
||||||
|
<p class="m-0 text-sm text-gray-600">提示:拖拽列表项可调整分类排序,排在前面的分类在小程序中优先展示</p>
|
||||||
|
</div>
|
||||||
|
<div class="max-h-[500px] overflow-y-auto">
|
||||||
|
<template v-for="(item, index) in sortableList" :key="item.id">
|
||||||
|
<div
|
||||||
|
v-if="dragOverIndex === index && draggedIndex !== -1 && draggedIndex !== index"
|
||||||
|
class="mb-3 rounded-lg border-2 border-dashed border-blue-500 bg-blue-50/50"
|
||||||
|
@dragenter="onDragEnter(index)"
|
||||||
|
@dragover="onDragOver"
|
||||||
|
@drop="onDrop($event, index)"
|
||||||
|
>
|
||||||
|
<div class="pointer-events-none flex items-center gap-4 p-4 opacity-60">
|
||||||
|
<Icon icon="ant-design:menu-outlined" class="text-xl text-blue-500" />
|
||||||
|
<div class="flex-1">{{ sortableList[draggedIndex]?.name }}</div>
|
||||||
|
<div class="text-sm text-blue-600">释放以插入此处</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mb-3 flex cursor-move items-center gap-4 rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-all hover:border-blue-400"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="onDragStart($event, index)"
|
||||||
|
@dragenter="onDragEnter(index)"
|
||||||
|
@dragover="onDragOver"
|
||||||
|
@drop="onDrop($event, index)"
|
||||||
|
@dragend="onDragEnd"
|
||||||
|
>
|
||||||
|
<Icon icon="ant-design:menu-outlined" class="text-xl text-gray-400" />
|
||||||
|
<div class="flex-1 font-medium">{{ item.name }}</div>
|
||||||
|
<div class="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-600">{{ index + 1 }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
export const modalFormProps: VbenFormProps = {
|
||||||
|
wrapperClass: 'grid-cols-12',
|
||||||
|
commonConfig: {
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
fieldName: 'id',
|
||||||
|
label: 'ID',
|
||||||
|
formItemClass: 'col-span-6',
|
||||||
|
defaultValue: '',
|
||||||
|
dependencies: {
|
||||||
|
show: false,
|
||||||
|
triggerFields: ['id'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入分类名称',
|
||||||
|
},
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '分类名称',
|
||||||
|
rules: 'required',
|
||||||
|
defaultValue: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenSelect',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
options: [
|
||||||
|
{ label: '启用', value: 1 },
|
||||||
|
{ label: '禁用', value: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
formItemClass: 'col-span-6',
|
||||||
|
defaultValue: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showDefaultActions: false,
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { VbenFormProps } from '@vben/common-ui';
|
||||||
|
|
||||||
|
export const formOptions: VbenFormProps = {
|
||||||
|
layout: 'inline',
|
||||||
|
showResetButton: true,
|
||||||
|
showSubmitButton: true,
|
||||||
|
schemas: [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
component: 'Input',
|
||||||
|
label: '分类名称',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入分类名称',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
component: 'Select',
|
||||||
|
label: '状态',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
allowClear: true,
|
||||||
|
options: [
|
||||||
|
{ label: '启用', value: 1 },
|
||||||
|
{ label: '禁用', value: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { getSpecialPrescriptionCategoryList } from '../api';
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
sort: number;
|
||||||
|
status: number;
|
||||||
|
status_txt: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gridOptions: VxeGridProps<RowType> = {
|
||||||
|
checkboxConfig: {
|
||||||
|
highlight: true,
|
||||||
|
labelField: '',
|
||||||
|
},
|
||||||
|
columnConfig: {
|
||||||
|
useKey: true,
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
useKey: true,
|
||||||
|
isHover: true,
|
||||||
|
isCurrent: true,
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ field: 'id', align: 'left', title: 'ID', width: 80 },
|
||||||
|
{ field: 'name', align: 'left', title: '分类名称', minWidth: 160 },
|
||||||
|
{ field: 'sort', align: 'left', title: '排序', width: 100 },
|
||||||
|
{ field: 'status_txt', align: 'left', title: '状态', width: 100 },
|
||||||
|
{ field: 'created_at', title: '创建时间', width: 180 },
|
||||||
|
{ type: 'html', title: '操作', slots: { default: 'action' }, width: 160 },
|
||||||
|
],
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getSpecialPrescriptionCategoryList({
|
||||||
|
page: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
border: false,
|
||||||
|
toolbarConfig: {
|
||||||
|
search: true,
|
||||||
|
refresh: true,
|
||||||
|
print: false,
|
||||||
|
export: false,
|
||||||
|
zoom: true,
|
||||||
|
slots: {
|
||||||
|
buttons: 'toolbar-buttons',
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
icon: 'vxe-icon-menu',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showOverflow: false,
|
||||||
|
};
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { TableAction } from '#/components/table-action';
|
||||||
|
|
||||||
|
import { deleteSpecialPrescriptionCategory } from './api';
|
||||||
|
import CategoryModal from './components/modal.vue';
|
||||||
|
import CategorySortModal from './components/sort-modal.vue';
|
||||||
|
import { formOptions as searchFormOptions } from './config/search';
|
||||||
|
import { gridOptions } from './config/table';
|
||||||
|
|
||||||
|
const hasTopTableDropDownActions = ref(false);
|
||||||
|
|
||||||
|
const gridEvents: VxeGridListeners<any> = {
|
||||||
|
checkboxChange() {
|
||||||
|
const records = gridApi.grid.getCheckboxRecords();
|
||||||
|
hasTopTableDropDownActions.value = records.length > 0;
|
||||||
|
},
|
||||||
|
checkboxAll() {
|
||||||
|
const records = gridApi.grid.getCheckboxRecords();
|
||||||
|
hasTopTableDropDownActions.value = records.length > 0;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: searchFormOptions,
|
||||||
|
gridOptions,
|
||||||
|
gridEvents,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [CategoryFormModal, categoryFormModalApi] = useVbenModal({
|
||||||
|
connectedComponent: CategoryModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [SortModal, sortModalApi] = useVbenModal({
|
||||||
|
connectedComponent: CategorySortModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const showSortModal = () => {
|
||||||
|
sortModalApi.setData({ gridApi });
|
||||||
|
sortModalApi.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const showCategoryModal = (data = {}, isUpdate = false) => {
|
||||||
|
categoryFormModalApi.setData({
|
||||||
|
values: data,
|
||||||
|
update: isUpdate,
|
||||||
|
gridApi,
|
||||||
|
});
|
||||||
|
categoryFormModalApi.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteCategoryApi = (row: any) => {
|
||||||
|
let ids: (string | number)[] = [];
|
||||||
|
if (row) {
|
||||||
|
ids.push(row);
|
||||||
|
} else {
|
||||||
|
ids = gridApi.grid.getCheckboxRecords().map((item: any) => item.id);
|
||||||
|
}
|
||||||
|
deleteSpecialPrescriptionCategory({ ids }).then(() => {
|
||||||
|
message.success('删除成功!');
|
||||||
|
gridApi.query();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="特色方分类管理">
|
||||||
|
<CategoryFormModal />
|
||||||
|
<SortModal />
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '新增分类',
|
||||||
|
type: 'primary',
|
||||||
|
icon: 'ant-design:plus-outlined',
|
||||||
|
onClick: () => showCategoryModal({}, false),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '排序',
|
||||||
|
icon: 'ant-design:ordered-list-outlined',
|
||||||
|
onClick: showSortModal,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<template #toolbar-buttons>
|
||||||
|
<Button
|
||||||
|
v-if="hasTopTableDropDownActions"
|
||||||
|
danger
|
||||||
|
type="primary"
|
||||||
|
@click="deleteCategoryApi()"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #action="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '编辑',
|
||||||
|
onClick: () => showCategoryModal(row, true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
color: 'error',
|
||||||
|
popConfirm: {
|
||||||
|
title: '确定要删除该分类吗?',
|
||||||
|
onConfirm: () => deleteCategoryApi(row.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, Card, Input, InputNumber, message, Radio, Switch, Tabs } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import FormAvatar from '#/components/form/components/avatar.vue';
|
||||||
|
|
||||||
|
import { getSystemConfigList, saveSystemConfig } from '#/views/system/system-config/api';
|
||||||
|
|
||||||
|
defineOptions({ name: 'SpecialPrescriptionDisplayConfig' });
|
||||||
|
|
||||||
|
const TAB_STORAGE_KEY = 'special_prescription_config_active_tab';
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
const activeKey = ref('global');
|
||||||
|
|
||||||
|
const specialPrescriptionEnabled = ref(true);
|
||||||
|
const specialPrescriptionTitleImage = ref('');
|
||||||
|
const specialPrescriptionMascotImage = ref('');
|
||||||
|
const specialPrescriptionSubtitle = ref('精选时令养生茶饮');
|
||||||
|
const specialPrescriptionHomeLimit = ref(4);
|
||||||
|
|
||||||
|
const specialPrescriptionListLayout = ref<'sidebar' | 'tabs'>('sidebar');
|
||||||
|
const specialPrescriptionListHeaderStyle = ref<'simple' | 'card'>('simple');
|
||||||
|
const specialPrescriptionListSort = ref<'id_desc' | 'sales_desc'>('id_desc');
|
||||||
|
const specialPrescriptionListTitleImage = ref('');
|
||||||
|
const specialPrescriptionListMascotImage = ref('');
|
||||||
|
const specialPrescriptionListSubtitle = ref('精选时令养生茶饮');
|
||||||
|
|
||||||
|
function readStoredTab() {
|
||||||
|
const stored = localStorage.getItem(TAB_STORAGE_KEY);
|
||||||
|
if (stored === 'global' || stored === 'home' || stored === 'list') {
|
||||||
|
activeKey.value = stored;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTabChange(key: string | number) {
|
||||||
|
activeKey.value = String(key);
|
||||||
|
localStorage.setItem(TAB_STORAGE_KEY, String(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const list = await getSystemConfigList();
|
||||||
|
const rows = Array.isArray(list) ? list : list?.data || [];
|
||||||
|
for (const row of rows) {
|
||||||
|
if (row.config_key === 'special_prescription_enabled') {
|
||||||
|
specialPrescriptionEnabled.value =
|
||||||
|
row.config_value === '1' || row.config_value === true || row.config_value === 'true';
|
||||||
|
}
|
||||||
|
if (row.config_key === 'special_prescription_title_image') {
|
||||||
|
specialPrescriptionTitleImage.value = String(row.config_value || '');
|
||||||
|
}
|
||||||
|
if (row.config_key === 'special_prescription_mascot_image') {
|
||||||
|
specialPrescriptionMascotImage.value = String(row.config_value || '');
|
||||||
|
}
|
||||||
|
if (row.config_key === 'special_prescription_subtitle') {
|
||||||
|
specialPrescriptionSubtitle.value = String(row.config_value || '精选时令养生茶饮');
|
||||||
|
}
|
||||||
|
if (row.config_key === 'special_prescription_home_limit') {
|
||||||
|
specialPrescriptionHomeLimit.value = Math.max(1, Number(row.config_value) || 4);
|
||||||
|
}
|
||||||
|
if (row.config_key === 'special_prescription_list_layout') {
|
||||||
|
specialPrescriptionListLayout.value = row.config_value === 'tabs' ? 'tabs' : 'sidebar';
|
||||||
|
}
|
||||||
|
if (row.config_key === 'special_prescription_list_header_style') {
|
||||||
|
specialPrescriptionListHeaderStyle.value = row.config_value === 'card' ? 'card' : 'simple';
|
||||||
|
}
|
||||||
|
if (row.config_key === 'special_prescription_list_sort') {
|
||||||
|
specialPrescriptionListSort.value = row.config_value === 'sales_desc' ? 'sales_desc' : 'id_desc';
|
||||||
|
}
|
||||||
|
if (row.config_key === 'special_prescription_list_title_image') {
|
||||||
|
specialPrescriptionListTitleImage.value = String(row.config_value || '');
|
||||||
|
}
|
||||||
|
if (row.config_key === 'special_prescription_list_mascot_image') {
|
||||||
|
specialPrescriptionListMascotImage.value = String(row.config_value || '');
|
||||||
|
}
|
||||||
|
if (row.config_key === 'special_prescription_list_subtitle') {
|
||||||
|
specialPrescriptionListSubtitle.value = String(row.config_value || '精选时令养生茶饮');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
await saveSystemConfig([
|
||||||
|
{
|
||||||
|
config_key: 'special_prescription_enabled',
|
||||||
|
config_value: specialPrescriptionEnabled.value ? '1' : '0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config_key: 'special_prescription_title_image',
|
||||||
|
config_value: specialPrescriptionTitleImage.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config_key: 'special_prescription_mascot_image',
|
||||||
|
config_value: specialPrescriptionMascotImage.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config_key: 'special_prescription_subtitle',
|
||||||
|
config_value: specialPrescriptionSubtitle.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config_key: 'special_prescription_home_limit',
|
||||||
|
config_value: String(specialPrescriptionHomeLimit.value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config_key: 'special_prescription_list_layout',
|
||||||
|
config_value: specialPrescriptionListLayout.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config_key: 'special_prescription_list_header_style',
|
||||||
|
config_value: specialPrescriptionListHeaderStyle.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config_key: 'special_prescription_list_sort',
|
||||||
|
config_value: specialPrescriptionListSort.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config_key: 'special_prescription_list_title_image',
|
||||||
|
config_value: specialPrescriptionListTitleImage.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config_key: 'special_prescription_list_mascot_image',
|
||||||
|
config_value: specialPrescriptionListMascotImage.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config_key: 'special_prescription_list_subtitle',
|
||||||
|
config_value: specialPrescriptionListSubtitle.value,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
message.success('保存成功');
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
readStoredTab();
|
||||||
|
load();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="特色方展示配置">
|
||||||
|
<Card :loading="loading">
|
||||||
|
<Tabs :active-key="activeKey" @change="handleTabChange">
|
||||||
|
<Tabs.TabPane key="global" tab="全局">
|
||||||
|
<div class="py-4">
|
||||||
|
<div class="mb-2 font-medium">特色方功能开关</div>
|
||||||
|
<Switch
|
||||||
|
v-model:checked="specialPrescriptionEnabled"
|
||||||
|
checked-children="开启"
|
||||||
|
un-checked-children="关闭"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<Tabs.TabPane key="home" tab="首页">
|
||||||
|
<div class="py-4">
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="mb-2 font-medium">首页标题图</div>
|
||||||
|
<FormAvatar v-model:value="specialPrescriptionTitleImage" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="mb-2 font-medium">吉祥物图</div>
|
||||||
|
<FormAvatar v-model:value="specialPrescriptionMascotImage" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="mb-2 font-medium">副标题</div>
|
||||||
|
<Input
|
||||||
|
v-model:value="specialPrescriptionSubtitle"
|
||||||
|
placeholder="如:精选时令养生茶饮"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="mb-2 font-medium">首页展示数量</div>
|
||||||
|
<InputNumber v-model:value="specialPrescriptionHomeLimit" :min="1" :max="20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<Tabs.TabPane key="list" tab="列表页">
|
||||||
|
<div class="py-4">
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="mb-2 font-medium">列表筛选布局</div>
|
||||||
|
<Radio.Group v-model:value="specialPrescriptionListLayout">
|
||||||
|
<Radio value="sidebar">左侧分类(默认)</Radio>
|
||||||
|
<Radio value="tabs">顶部标签</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="mb-2 font-medium">头部样式</div>
|
||||||
|
<Radio.Group v-model:value="specialPrescriptionListHeaderStyle">
|
||||||
|
<Radio value="simple">简洁横幅(默认)</Radio>
|
||||||
|
<Radio value="card">异形卡片</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="mb-2 font-medium">列表排序</div>
|
||||||
|
<Radio.Group v-model:value="specialPrescriptionListSort">
|
||||||
|
<Radio value="id_desc">按 ID 倒序(默认)</Radio>
|
||||||
|
<Radio value="sales_desc">按销量倒序</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="mb-2 font-medium">列表页标题图</div>
|
||||||
|
<FormAvatar v-model:value="specialPrescriptionListTitleImage" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="mb-2 font-medium">列表页 IP 图</div>
|
||||||
|
<FormAvatar v-model:value="specialPrescriptionListMascotImage" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="mb-2 font-medium">列表页副标题</div>
|
||||||
|
<Input
|
||||||
|
v-model:value="specialPrescriptionListSubtitle"
|
||||||
|
placeholder="如:精选时令养生茶饮"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<Button type="primary" :loading="saving" @click="handleSave">保存配置</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'special-prescription/';
|
||||||
|
|
||||||
|
export async function getSpecialPrescriptionList(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSpecialPrescriptionInfo(id: number) {
|
||||||
|
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSpecialPrescription(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}create`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateSpecialPrescription(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}update`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteSpecialPrescription(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}delete`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applySpecialPrescriptionApi(data: { register_id: number }) {
|
||||||
|
return requestClient.post<any>(`${prefix}apply`, data);
|
||||||
|
}
|
||||||
@@ -0,0 +1,451 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { nextTick, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { DeleteTwoTone } from '@ant-design/icons-vue';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
FormItem,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import { debounce } from 'lodash-es';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getDrugUseList,
|
||||||
|
getProductListDoctorReception,
|
||||||
|
} from '#/views/doctor/doctor-reception/api';
|
||||||
|
|
||||||
|
export interface ChineseDrugItem {
|
||||||
|
_key?: number;
|
||||||
|
id: number;
|
||||||
|
drug_id: number;
|
||||||
|
drug_name: string;
|
||||||
|
number: number;
|
||||||
|
price: number;
|
||||||
|
way_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue?: ChineseDrugItem[];
|
||||||
|
dosage?: number;
|
||||||
|
dayDosage?: number;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
modelValue: () => [],
|
||||||
|
dosage: 7,
|
||||||
|
dayDosage: 2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: ChineseDrugItem[]];
|
||||||
|
'update:dosage': [value: number];
|
||||||
|
'update:dayDosage': [value: number];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const drugList = ref<ChineseDrugItem[]>([]);
|
||||||
|
const dosageLocal = ref(props.dosage);
|
||||||
|
const dayDosageLocal = ref(props.dayDosage);
|
||||||
|
const drugUseWay = ref<any[]>([]);
|
||||||
|
const chineseSearchResults = ref<any[]>([]);
|
||||||
|
const newDrugInfo = ref<{
|
||||||
|
id?: number;
|
||||||
|
name?: string;
|
||||||
|
number?: number;
|
||||||
|
price?: number;
|
||||||
|
way_id?: number;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
drugList.value = [...(props.modelValue || [])];
|
||||||
|
try {
|
||||||
|
const res = await getDrugUseList();
|
||||||
|
drugUseWay.value = res.drug_use_way || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载药品用法失败:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function syncToParent() {
|
||||||
|
emit('update:modelValue', [...drugList.value]);
|
||||||
|
emit('update:dosage', dosageLocal.value);
|
||||||
|
emit('update:dayDosage', dayDosageLocal.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchChineseDrugs = debounce(async (keyword: string) => {
|
||||||
|
if (!keyword || keyword.length < 1) {
|
||||||
|
chineseSearchResults.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await getProductListDoctorReception({
|
||||||
|
name: keyword,
|
||||||
|
type: 1,
|
||||||
|
store_id: userStore.userInfo?.store_id || 2,
|
||||||
|
});
|
||||||
|
chineseSearchResults.value = Array.isArray(res) ? res : [];
|
||||||
|
} catch {
|
||||||
|
chineseSearchResults.value = [];
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
function selectNewDrug(drugId: number) {
|
||||||
|
const exists = drugList.value.some(
|
||||||
|
(item) => item.id === drugId || item.drug_id === drugId,
|
||||||
|
);
|
||||||
|
if (exists) {
|
||||||
|
message.warning('该药品已在列表中');
|
||||||
|
newDrugInfo.value.id = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selectedItem = chineseSearchResults.value.find(
|
||||||
|
(item) => item.drug?.id === drugId || item.drug_id === drugId,
|
||||||
|
);
|
||||||
|
if (selectedItem) {
|
||||||
|
newDrugInfo.value.name = selectedItem.drug?.drug_name || '';
|
||||||
|
newDrugInfo.value.price = selectedItem.price || 0;
|
||||||
|
newDrugInfo.value.way_id = 0;
|
||||||
|
nextTick(() => {
|
||||||
|
const numberInput = document.querySelector('.new-chinese-number input') as HTMLInputElement;
|
||||||
|
numberInput?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addChineseDrug() {
|
||||||
|
if (!newDrugInfo.value.id) {
|
||||||
|
message.warning('请先选择药品');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!newDrugInfo.value.number || newDrugInfo.value.number <= 0) {
|
||||||
|
message.warning('请输入正确的克数');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drugList.value.push({
|
||||||
|
_key: Date.now(),
|
||||||
|
id: newDrugInfo.value.id,
|
||||||
|
drug_id: newDrugInfo.value.id,
|
||||||
|
drug_name: newDrugInfo.value.name || '',
|
||||||
|
number: newDrugInfo.value.number,
|
||||||
|
price: newDrugInfo.value.price || 0,
|
||||||
|
way_id: newDrugInfo.value.way_id || 0,
|
||||||
|
});
|
||||||
|
newDrugInfo.value = {};
|
||||||
|
chineseSearchResults.value = [];
|
||||||
|
syncToParent();
|
||||||
|
message.success('已添加药品');
|
||||||
|
nextTick(() => {
|
||||||
|
const nameInput = document.querySelector('.new-chinese-name input') as HTMLInputElement;
|
||||||
|
nameInput?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChineseKeydown(event: KeyboardEvent, isNew: boolean) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
if (isNew) {
|
||||||
|
addChineseDrug();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeChineseDrug(index: number, drugId: number) {
|
||||||
|
const exists = drugList.value.some(
|
||||||
|
(item, i) => i !== index && (item.id === drugId || item.drug_id === drugId),
|
||||||
|
);
|
||||||
|
if (exists) {
|
||||||
|
message.warning('该药品已在列表中');
|
||||||
|
const drug = drugList.value[index];
|
||||||
|
if (drug) drug.id = drug.drug_id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selectedItem = chineseSearchResults.value.find(
|
||||||
|
(item) => item.drug?.id === drugId || item.drug_id === drugId,
|
||||||
|
);
|
||||||
|
if (selectedItem) {
|
||||||
|
const drug = drugList.value[index];
|
||||||
|
if (drug) {
|
||||||
|
drug.id = drugId;
|
||||||
|
drug.drug_id = drugId;
|
||||||
|
drug.drug_name = selectedItem.drug?.drug_name || '';
|
||||||
|
drug.price = selectedItem.price || 0;
|
||||||
|
syncToParent();
|
||||||
|
nextTick(() => {
|
||||||
|
const numberInput = document.querySelector(`.chinese-number-${index} input`) as HTMLInputElement;
|
||||||
|
numberInput?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDrug(index: number) {
|
||||||
|
drugList.value.splice(index, 1);
|
||||||
|
syncToParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrugNumberChange() {
|
||||||
|
syncToParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDosageChange() {
|
||||||
|
syncToParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDrugs(recipes: any[], dosage?: number, dayDosage?: number) {
|
||||||
|
drugList.value = (recipes || []).map((recipe) => ({
|
||||||
|
_key: Date.now() + Math.random(),
|
||||||
|
id: recipe.drug_id || recipe.id,
|
||||||
|
drug_id: recipe.drug_id || recipe.id,
|
||||||
|
drug_name: recipe.drug_name || recipe.name || '',
|
||||||
|
number: recipe.number || 1,
|
||||||
|
price: recipe.price || 0,
|
||||||
|
way_id: recipe.way_id || 0,
|
||||||
|
}));
|
||||||
|
if (dosage !== undefined) dosageLocal.value = dosage;
|
||||||
|
if (dayDosage !== undefined) dayDosageLocal.value = dayDosage;
|
||||||
|
syncToParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDrugsPayload() {
|
||||||
|
return drugList.value.map((drug) => ({
|
||||||
|
drug_id: drug.drug_id || drug.id,
|
||||||
|
drug_name: drug.drug_name,
|
||||||
|
number: drug.number || 1,
|
||||||
|
price: drug.price || 0,
|
||||||
|
way_id: drug.way_id || 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getDrugsPayload,
|
||||||
|
loadDrugs,
|
||||||
|
drugList,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="chinese-drug-editor">
|
||||||
|
<Row :gutter="16" class="mb-4">
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="剂量(天/剂)">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="dosageLocal"
|
||||||
|
:min="1"
|
||||||
|
:max="100"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="onDosageChange"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
<Col :span="12">
|
||||||
|
<FormItem label="频次(次/天)">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="dayDosageLocal"
|
||||||
|
:min="1"
|
||||||
|
:max="10"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="onDosageChange"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<FormItem label="药品列表" required>
|
||||||
|
<Row :gutter="[12, 12]">
|
||||||
|
<Col
|
||||||
|
v-for="(drug, index) in drugList"
|
||||||
|
:key="drug._key"
|
||||||
|
:xs="24"
|
||||||
|
:sm="12"
|
||||||
|
:md="8"
|
||||||
|
:lg="8"
|
||||||
|
>
|
||||||
|
<Card size="small" class="chinese-drug-card">
|
||||||
|
<div class="chinese-drug-content">
|
||||||
|
<span class="chinese-drug-index">{{ index + 1 }}、</span>
|
||||||
|
<div class="chinese-drug-form">
|
||||||
|
<Select
|
||||||
|
v-model:value="drug.id"
|
||||||
|
show-search
|
||||||
|
:filter-option="false"
|
||||||
|
placeholder="药名"
|
||||||
|
class="chinese-drug-name"
|
||||||
|
@search="searchChineseDrugs"
|
||||||
|
@change="(val) => changeChineseDrug(index, val as number)"
|
||||||
|
>
|
||||||
|
<SelectOption v-if="chineseSearchResults.length === 0" :value="drug.id">
|
||||||
|
{{ drug.drug_name }}
|
||||||
|
</SelectOption>
|
||||||
|
<SelectOption
|
||||||
|
v-for="item in chineseSearchResults"
|
||||||
|
v-else
|
||||||
|
:key="item.drug?.id || item.id"
|
||||||
|
:value="item.drug?.id || item.id"
|
||||||
|
>
|
||||||
|
{{ item.drug?.drug_name || item.drug_name }}
|
||||||
|
</SelectOption>
|
||||||
|
</Select>
|
||||||
|
<span class="chinese-drug-comma">,</span>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="drug.number"
|
||||||
|
:class="`chinese-number-${index}`"
|
||||||
|
:controls="false"
|
||||||
|
:min="1"
|
||||||
|
style="width: 60px"
|
||||||
|
@change="onDrugNumberChange"
|
||||||
|
/>
|
||||||
|
<span class="chinese-drug-unit">g</span>
|
||||||
|
<span class="chinese-drug-comma">,</span>
|
||||||
|
<Select
|
||||||
|
v-model:value="drug.way_id"
|
||||||
|
placeholder="用法"
|
||||||
|
style="width: 70px"
|
||||||
|
@change="onDrugNumberChange"
|
||||||
|
>
|
||||||
|
<SelectOption :value="0">煎服</SelectOption>
|
||||||
|
<SelectOption
|
||||||
|
v-for="item in drugUseWay"
|
||||||
|
:key="item.id"
|
||||||
|
:value="item.id"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</SelectOption>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="chinese-drug-price">
|
||||||
|
¥{{ ((drug.price || 0) * (drug.number || 1)).toFixed(2) }}
|
||||||
|
</div>
|
||||||
|
<Button type="link" class="chinese-drug-delete" @click="removeDrug(index)">
|
||||||
|
<DeleteTwoTone two-tone-color="#ff4d4f" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col :xs="24" :sm="12" :md="8" :lg="8">
|
||||||
|
<Card size="small" class="chinese-drug-card chinese-drug-card-new">
|
||||||
|
<div class="chinese-drug-content">
|
||||||
|
<span class="chinese-drug-index">{{ drugList.length + 1 }}、</span>
|
||||||
|
<div class="chinese-drug-form">
|
||||||
|
<Select
|
||||||
|
v-model:value="newDrugInfo.id"
|
||||||
|
show-search
|
||||||
|
:filter-option="false"
|
||||||
|
placeholder="药名"
|
||||||
|
class="chinese-drug-name new-chinese-name"
|
||||||
|
@search="searchChineseDrugs"
|
||||||
|
@change="selectNewDrug"
|
||||||
|
>
|
||||||
|
<SelectOption
|
||||||
|
v-for="item in chineseSearchResults"
|
||||||
|
:key="item.drug?.id || item.id"
|
||||||
|
:value="item.drug?.id || item.id"
|
||||||
|
>
|
||||||
|
{{ item.drug?.drug_name || item.drug_name }}
|
||||||
|
</SelectOption>
|
||||||
|
</Select>
|
||||||
|
<span class="chinese-drug-comma">,</span>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="newDrugInfo.number"
|
||||||
|
class="new-chinese-number"
|
||||||
|
:controls="false"
|
||||||
|
:min="1"
|
||||||
|
style="width: 60px"
|
||||||
|
@keydown="(e) => handleChineseKeydown(e, true)"
|
||||||
|
/>
|
||||||
|
<span class="chinese-drug-unit">g</span>
|
||||||
|
<span class="chinese-drug-comma">,</span>
|
||||||
|
<Select
|
||||||
|
v-model:value="newDrugInfo.way_id"
|
||||||
|
placeholder="用法"
|
||||||
|
style="width: 70px"
|
||||||
|
@keydown="(e) => handleChineseKeydown(e, true)"
|
||||||
|
>
|
||||||
|
<SelectOption :value="0">煎服</SelectOption>
|
||||||
|
<SelectOption v-for="item in drugUseWay" :key="item.id" :value="item.id">
|
||||||
|
{{ item.name }}
|
||||||
|
</SelectOption>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="chinese-drug-price">
|
||||||
|
¥{{ ((newDrugInfo.price || 0) * (newDrugInfo.number || 1)).toFixed(2) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.chinese-drug-card {
|
||||||
|
position: relative;
|
||||||
|
min-height: 80px;
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-drug-card-new {
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-drug-content {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-drug-index {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
@apply text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-drug-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding-left: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-drug-name {
|
||||||
|
min-width: 80px;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-drug-comma {
|
||||||
|
@apply text-muted-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-drug-unit {
|
||||||
|
@apply text-muted-foreground text-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-drug-price {
|
||||||
|
position: absolute;
|
||||||
|
right: 24px;
|
||||||
|
bottom: 0;
|
||||||
|
@apply text-destructive font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chinese-drug-delete {
|
||||||
|
position: absolute;
|
||||||
|
right: -8px;
|
||||||
|
top: -8px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Alert, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createSpecialPrescription,
|
||||||
|
getSpecialPrescriptionInfo,
|
||||||
|
updateSpecialPrescription,
|
||||||
|
} from '../api';
|
||||||
|
import ChineseDrugEditor from './ChineseDrugEditor.vue';
|
||||||
|
import { modalFormProps } from '../config/form';
|
||||||
|
|
||||||
|
const isUpdate = ref(false);
|
||||||
|
const gridApi = ref();
|
||||||
|
const prescriptionType = ref<'chinese' | 'west' | 'granular'>('chinese');
|
||||||
|
const chineseDrugEditorRef = ref<InstanceType<typeof ChineseDrugEditor>>();
|
||||||
|
const dosage = ref(7);
|
||||||
|
const dayDosage = ref(2);
|
||||||
|
const drugsChanged = ref(false);
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: true,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
const validation = await formApi.validate();
|
||||||
|
if (!validation.valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
prescriptionType.value = values.prescription_type || 'chinese';
|
||||||
|
|
||||||
|
let drugs: any[] = [];
|
||||||
|
if (prescriptionType.value === 'chinese') {
|
||||||
|
drugs = chineseDrugEditorRef.value?.getDrugsPayload() || [];
|
||||||
|
if (!isUpdate.value && drugs.length === 0) {
|
||||||
|
message.warning('请至少添加一种中药');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (drugs.length > 0) {
|
||||||
|
drugsChanged.value = true;
|
||||||
|
}
|
||||||
|
} else if (!isUpdate.value) {
|
||||||
|
message.warning('当前仅支持在后台配置中药类型处方,请选择中药类型');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: Record<string, any> = {
|
||||||
|
...values,
|
||||||
|
tags: Array.isArray(values.tags)
|
||||||
|
? values.tags
|
||||||
|
: String(values.tags || '')
|
||||||
|
.split(',')
|
||||||
|
.map((s: string) => s.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (prescriptionType.value === 'chinese' && drugs.length > 0) {
|
||||||
|
payload.drugs = drugs;
|
||||||
|
payload.dosage = dosage.value;
|
||||||
|
payload.day_dosage = dayDosage.value;
|
||||||
|
payload.rule_type = 1;
|
||||||
|
payload.package_method_id = 2;
|
||||||
|
} else if (isUpdate.value && !drugsChanged.value) {
|
||||||
|
delete payload.drugs;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalApi.setState({ loading: true, confirmLoading: true });
|
||||||
|
const submitApi = isUpdate.value ? updateSpecialPrescription : createSpecialPrescription;
|
||||||
|
submitApi(payload)
|
||||||
|
.then(() => {
|
||||||
|
message.success('保存成功');
|
||||||
|
gridApi.value?.reload();
|
||||||
|
modalApi.close();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
modalApi.setState({ loading: false, confirmLoading: false });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||||
|
if (isOpen) {
|
||||||
|
bindPrescriptionTypeChange();
|
||||||
|
const { values, update } = modalApi.getData<Record<string, any>>();
|
||||||
|
drugsChanged.value = false;
|
||||||
|
if (values && update) {
|
||||||
|
isUpdate.value = true;
|
||||||
|
getSpecialPrescriptionInfo(values.id).then((res: any) => {
|
||||||
|
if (!res) return;
|
||||||
|
prescriptionType.value = res.prescription_type || 'chinese';
|
||||||
|
formApi.setValues({
|
||||||
|
id: res.id,
|
||||||
|
name: res.name || '',
|
||||||
|
category_id: res.category_id,
|
||||||
|
cover_image: res.cover_image || '',
|
||||||
|
tags: Array.isArray(res.tags) ? res.tags : [],
|
||||||
|
price_per_dose: res.price_per_dose ?? 0,
|
||||||
|
sales_count: res.sales_count ?? 0,
|
||||||
|
status: res.status ?? 1,
|
||||||
|
prescription_type: res.prescription_type || 'chinese',
|
||||||
|
intro_text: res.intro_text || '',
|
||||||
|
introduction_images: res.introduction_images || [],
|
||||||
|
});
|
||||||
|
if (res.prescription_type === 'chinese' && res.prescription_detail?.length) {
|
||||||
|
const first = res.prescription_detail[0];
|
||||||
|
chineseDrugEditorRef.value?.loadDrugs(
|
||||||
|
res.prescription_detail,
|
||||||
|
first?.dosage ?? 7,
|
||||||
|
first?.consumption ?? 2,
|
||||||
|
);
|
||||||
|
dosage.value = first?.dosage ?? 7;
|
||||||
|
dayDosage.value = first?.consumption ?? 2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
isUpdate.value = false;
|
||||||
|
prescriptionType.value = 'chinese';
|
||||||
|
dosage.value = 7;
|
||||||
|
dayDosage.value = 2;
|
||||||
|
formApi.resetForm();
|
||||||
|
formApi.setValues({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
category_id: undefined,
|
||||||
|
cover_image: '',
|
||||||
|
tags: [],
|
||||||
|
price_per_dose: 0,
|
||||||
|
sales_count: 0,
|
||||||
|
status: 1,
|
||||||
|
prescription_type: 'chinese',
|
||||||
|
intro_text: '',
|
||||||
|
introduction_images: [],
|
||||||
|
});
|
||||||
|
chineseDrugEditorRef.value?.loadDrugs([], 7, 2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
formApi.resetForm();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function bindPrescriptionTypeChange() {
|
||||||
|
formApi.updateSchema([
|
||||||
|
{
|
||||||
|
fieldName: 'prescription_type',
|
||||||
|
componentProps: {
|
||||||
|
onChange: (val: string) => {
|
||||||
|
prescriptionType.value = val || 'chinese';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="`${isUpdate ? '编辑' : '新增'}特色方`" class="w-[80%]">
|
||||||
|
<Form />
|
||||||
|
<div v-if="prescriptionType === 'chinese'" class="mt-4 border-t pt-4">
|
||||||
|
<ChineseDrugEditor
|
||||||
|
ref="chineseDrugEditorRef"
|
||||||
|
v-model:dosage="dosage"
|
||||||
|
v-model:day-dosage="dayDosage"
|
||||||
|
@update:model-value="drugsChanged = true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Alert
|
||||||
|
v-else
|
||||||
|
class="mt-4"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
message="西药/颗粒药类型请切换为中药后在后台配置,或通过创建接口传入 drugs 数组保存。"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { getSpecialPrescriptionCategoryOption } from '#/views/business/special-prescription-category/api';
|
||||||
|
|
||||||
|
export const modalFormProps: VbenFormProps = {
|
||||||
|
wrapperClass: 'grid-cols-12',
|
||||||
|
commonConfig: {
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
fieldName: 'id',
|
||||||
|
label: 'ID',
|
||||||
|
dependencies: {
|
||||||
|
show: false,
|
||||||
|
triggerFields: ['id'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入特色方名称',
|
||||||
|
},
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '名称',
|
||||||
|
rules: 'required',
|
||||||
|
defaultValue: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择分类',
|
||||||
|
api: getSpecialPrescriptionCategoryOption,
|
||||||
|
allowClear: true,
|
||||||
|
showSearch: true,
|
||||||
|
filterOption: (input: string, option: any) =>
|
||||||
|
option?.label?.toLowerCase().includes(input.toLowerCase()),
|
||||||
|
afterFetch: (data: { id: number; name: string }[]) =>
|
||||||
|
data.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
fieldName: 'category_id',
|
||||||
|
label: '分类',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Avatar',
|
||||||
|
fieldName: 'cover_image',
|
||||||
|
label: '封面',
|
||||||
|
defaultValue: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
mode: 'tags',
|
||||||
|
placeholder: '输入标签后回车',
|
||||||
|
},
|
||||||
|
fieldName: 'tags',
|
||||||
|
label: '标签',
|
||||||
|
rules: 'required',
|
||||||
|
defaultValue: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '每剂价格',
|
||||||
|
min: 0,
|
||||||
|
precision: 2,
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
fieldName: 'price_per_dose',
|
||||||
|
label: '每剂价格',
|
||||||
|
formItemClass: 'col-span-6',
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '销量',
|
||||||
|
min: 0,
|
||||||
|
style: { width: '100%' },
|
||||||
|
},
|
||||||
|
fieldName: 'sales_count',
|
||||||
|
label: '销量',
|
||||||
|
formItemClass: 'col-span-6',
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenSelect',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
options: [
|
||||||
|
{ label: '上架', value: 1 },
|
||||||
|
{ label: '下架', value: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '状态',
|
||||||
|
formItemClass: 'col-span-6',
|
||||||
|
defaultValue: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenSelect',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择处方类型',
|
||||||
|
options: [
|
||||||
|
{ label: '中药', value: 'chinese' },
|
||||||
|
{ label: '西药', value: 'west' },
|
||||||
|
{ label: '颗粒药', value: 'granular' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fieldName: 'prescription_type',
|
||||||
|
label: '处方类型',
|
||||||
|
rules: 'required',
|
||||||
|
defaultValue: 'chinese',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Editor',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入介绍文案',
|
||||||
|
},
|
||||||
|
fieldName: 'intro_text',
|
||||||
|
label: '介绍文案',
|
||||||
|
defaultValue: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'UploadImageSortable',
|
||||||
|
fieldName: 'introduction_images',
|
||||||
|
label: '介绍图',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
maxCount: 20,
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showDefaultActions: false,
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import type { VbenFormProps } from '@vben/common-ui';
|
||||||
|
|
||||||
|
export const formOptions: VbenFormProps = {
|
||||||
|
layout: 'inline',
|
||||||
|
showResetButton: true,
|
||||||
|
showSubmitButton: true,
|
||||||
|
schemas: [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
component: 'Input',
|
||||||
|
label: '名称',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入特色方名称',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'category_id',
|
||||||
|
component: 'InputNumber',
|
||||||
|
label: '分类ID',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '分类ID',
|
||||||
|
allowClear: true,
|
||||||
|
min: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
component: 'Select',
|
||||||
|
label: '状态',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
allowClear: true,
|
||||||
|
options: [
|
||||||
|
{ label: '上架', value: 1 },
|
||||||
|
{ label: '下架', value: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { getSpecialPrescriptionList } from '../api';
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
category_id: number;
|
||||||
|
category_name: string;
|
||||||
|
cover_image: string;
|
||||||
|
tags: string[];
|
||||||
|
price_per_dose: number;
|
||||||
|
sales_count: number;
|
||||||
|
sort: number;
|
||||||
|
status: number;
|
||||||
|
status_txt: string;
|
||||||
|
prescription_sub_type: number;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prescriptionTypeMap: Record<number, string> = {
|
||||||
|
1: '中药',
|
||||||
|
2: '西药',
|
||||||
|
3: '颗粒药',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const gridOptions: VxeGridProps<RowType> = {
|
||||||
|
checkboxConfig: {
|
||||||
|
highlight: true,
|
||||||
|
labelField: '',
|
||||||
|
},
|
||||||
|
columnConfig: {
|
||||||
|
useKey: true,
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
useKey: true,
|
||||||
|
isHover: true,
|
||||||
|
isCurrent: true,
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ field: 'id', align: 'left', title: 'ID', width: 80 },
|
||||||
|
{ field: 'name', align: 'left', title: '名称', minWidth: 160 },
|
||||||
|
{ field: 'category_name', align: 'left', title: '分类', width: 120 },
|
||||||
|
{
|
||||||
|
field: 'cover_image',
|
||||||
|
align: 'left',
|
||||||
|
title: '封面',
|
||||||
|
width: 100,
|
||||||
|
slots: { default: 'cover' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'tags',
|
||||||
|
align: 'left',
|
||||||
|
title: '标签',
|
||||||
|
minWidth: 160,
|
||||||
|
slots: { default: 'tags' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'prescription_sub_type',
|
||||||
|
align: 'left',
|
||||||
|
title: '处方类型',
|
||||||
|
width: 100,
|
||||||
|
formatter: ({ cellValue }) => prescriptionTypeMap[cellValue] || '-',
|
||||||
|
},
|
||||||
|
{ field: 'price_per_dose', align: 'left', title: '每剂价格', width: 100 },
|
||||||
|
{ field: 'sales_count', align: 'left', title: '销量', width: 80 },
|
||||||
|
{ field: 'sort', align: 'left', title: '排序', width: 80 },
|
||||||
|
{ field: 'status_txt', align: 'left', title: '状态', width: 80 },
|
||||||
|
{ field: 'created_at', title: '创建时间', width: 180 },
|
||||||
|
{ type: 'html', title: '操作', slots: { default: 'action' }, width: 160 },
|
||||||
|
],
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getSpecialPrescriptionList({
|
||||||
|
page: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
border: false,
|
||||||
|
toolbarConfig: {
|
||||||
|
search: true,
|
||||||
|
refresh: true,
|
||||||
|
print: false,
|
||||||
|
export: false,
|
||||||
|
zoom: true,
|
||||||
|
slots: {
|
||||||
|
buttons: 'toolbar-buttons',
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
icon: 'vxe-icon-menu',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showOverflow: false,
|
||||||
|
};
|
||||||
129
apps/web-antd/src/views/business/special-prescription/index.vue
Normal file
129
apps/web-antd/src/views/business/special-prescription/index.vue
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, Image, message, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { TableAction } from '#/components/table-action';
|
||||||
|
|
||||||
|
import { deleteSpecialPrescription } from './api';
|
||||||
|
import SpecialPrescriptionModal from './components/modal.vue';
|
||||||
|
import { formOptions as searchFormOptions } from './config/search';
|
||||||
|
import { gridOptions } from './config/table';
|
||||||
|
|
||||||
|
const hasTopTableDropDownActions = ref(false);
|
||||||
|
|
||||||
|
const gridEvents: VxeGridListeners<any> = {
|
||||||
|
checkboxChange() {
|
||||||
|
const records = gridApi.grid.getCheckboxRecords();
|
||||||
|
hasTopTableDropDownActions.value = records.length > 0;
|
||||||
|
},
|
||||||
|
checkboxAll() {
|
||||||
|
const records = gridApi.grid.getCheckboxRecords();
|
||||||
|
hasTopTableDropDownActions.value = records.length > 0;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: searchFormOptions,
|
||||||
|
gridOptions,
|
||||||
|
gridEvents,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: SpecialPrescriptionModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const showModal = (data = {}, isUpdate = false) => {
|
||||||
|
formModalApi.setData({
|
||||||
|
values: data,
|
||||||
|
update: isUpdate,
|
||||||
|
gridApi,
|
||||||
|
});
|
||||||
|
formModalApi.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteApi = (row: any) => {
|
||||||
|
let ids: (string | number)[] = [];
|
||||||
|
if (row) {
|
||||||
|
ids.push(row);
|
||||||
|
} else {
|
||||||
|
ids = gridApi.grid.getCheckboxRecords().map((item: any) => item.id);
|
||||||
|
}
|
||||||
|
deleteSpecialPrescription({ ids }).then(() => {
|
||||||
|
message.success('删除成功!');
|
||||||
|
gridApi.query();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="特色方管理">
|
||||||
|
<FormModal />
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '新增特色方',
|
||||||
|
type: 'primary',
|
||||||
|
icon: 'ant-design:plus-outlined',
|
||||||
|
onClick: () => showModal({}, false),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<template #cover="{ row }">
|
||||||
|
<Image
|
||||||
|
v-if="row.cover_image"
|
||||||
|
:width="60"
|
||||||
|
:height="60"
|
||||||
|
:src="row.cover_image"
|
||||||
|
:fallback="'/static/mine/avatar_1.png'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #tags="{ row }">
|
||||||
|
<Tag v-for="tag in row.tags || []" :key="tag" class="mb-1">
|
||||||
|
{{ tag }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #toolbar-buttons>
|
||||||
|
<Button
|
||||||
|
v-if="hasTopTableDropDownActions"
|
||||||
|
danger
|
||||||
|
type="primary"
|
||||||
|
@click="deleteApi()"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #action="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '编辑',
|
||||||
|
onClick: () => showModal(row, true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
color: 'error',
|
||||||
|
popConfirm: {
|
||||||
|
title: '确定要删除该特色方吗?',
|
||||||
|
onConfirm: () => deleteApi(row.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import html2canvas from 'html2canvas';
|
import html2canvas from 'html2canvas';
|
||||||
|
|
||||||
import DoctorQrCodePreview from '#/components/modal/DoctorQrCodePreview.vue';
|
import DoctorQrCodePreview from '#/components/modal/DoctorQrCodePreview.vue';
|
||||||
|
import DoctorCardModal from '#/views/doctor/doctor/components/DoctorCardModal.vue';
|
||||||
import {
|
import {
|
||||||
deleteNavApi,
|
deleteNavApi,
|
||||||
getStoreInfoApi,
|
getStoreInfoApi,
|
||||||
@@ -67,6 +68,10 @@ const [DoctorQrCodePreviewModal, DoctorQrCodePreviewModalApi] = useVbenModal({
|
|||||||
connectedComponent: DoctorQrCodePreview,
|
connectedComponent: DoctorQrCodePreview,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [DoctorCardModals, DoctorCardModalApi] = useVbenModal({
|
||||||
|
connectedComponent: DoctorCardModal,
|
||||||
|
});
|
||||||
|
|
||||||
const [UploadModals, UploadModalApi] = useVbenModal({
|
const [UploadModals, UploadModalApi] = useVbenModal({
|
||||||
connectedComponent: UploadModal,
|
connectedComponent: UploadModal,
|
||||||
});
|
});
|
||||||
@@ -89,6 +94,16 @@ const openDoctorQrCode = (doctorId: number, isNotQr = false) => {
|
|||||||
DoctorQrCodePreviewModalApi.open();
|
DoctorQrCodePreviewModalApi.open();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 打开医生档案卡片(诊所团队场景隐藏关联诊所 Tab) */
|
||||||
|
const openDoctorCard = (doctor: any) => {
|
||||||
|
DoctorCardModalApi.setData({
|
||||||
|
id: doctor.doctor?.id,
|
||||||
|
su_id: doctor.doctor?.su_id,
|
||||||
|
hideStoresTab: true,
|
||||||
|
});
|
||||||
|
DoctorCardModalApi.open();
|
||||||
|
};
|
||||||
|
|
||||||
// 诊所/药店数据
|
// 诊所/药店数据
|
||||||
const data = ref<null | StoreInfo>(null);
|
const data = ref<null | StoreInfo>(null);
|
||||||
|
|
||||||
@@ -341,6 +356,7 @@ const handleSwitchClinicType = () => {
|
|||||||
<template>
|
<template>
|
||||||
<Page v-if="data" :title="`${data.name}的配置中心`" auto-content-height>
|
<Page v-if="data" :title="`${data.name}的配置中心`" auto-content-height>
|
||||||
<DoctorQrCodePreviewModal />
|
<DoctorQrCodePreviewModal />
|
||||||
|
<DoctorCardModals />
|
||||||
<UploadModals />
|
<UploadModals />
|
||||||
<div class="pb-2">
|
<div class="pb-2">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
@@ -537,7 +553,9 @@ const handleSwitchClinicType = () => {
|
|||||||
/>
|
/>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<h3 class="text-lg font-semibold">
|
<h3 class="text-lg font-semibold">
|
||||||
<span>{{ doctor.doctor?.name || '未知医生' }}</span>
|
<Button type="link" class="!p-0 !h-auto text-lg font-semibold" @click="openDoctorCard(doctor)">
|
||||||
|
{{ doctor.doctor?.name || '未知医生' }}
|
||||||
|
</Button>
|
||||||
<span
|
<span
|
||||||
:class="doctor.is_online ? 'text-green-500' : 'text-gray-500'"
|
:class="doctor.is_online ? 'text-green-500' : 'text-gray-500'"
|
||||||
class="ml-2 text-sm"
|
class="ml-2 text-sm"
|
||||||
|
|||||||
@@ -577,68 +577,70 @@ async function runBatchApply() {
|
|||||||
v-if="!parsedReady"
|
v-if="!parsedReady"
|
||||||
class="flex h-[calc(100vh-220px)] w-full flex-col items-center justify-center px-4 sm:px-12 md:px-24"
|
class="flex h-[calc(100vh-220px)] w-full flex-col items-center justify-center px-4 sm:px-12 md:px-24"
|
||||||
>
|
>
|
||||||
<!-- 将宽度限制放开,外层容器最高占屏幕85%宽度 -->
|
<div class="w-full max-w-[85vw] xl:max-w-screen-xl relative">
|
||||||
<div class="w-full max-w-[85vw] xl:max-w-screen-xl">
|
<!-- 核心隔离:State A 也采用遮罩加载,彻底抛弃包裹式用法 -->
|
||||||
<Spin :spinning="loadingAll || parseLoading" class="w-full" wrapperClassName="w-full">
|
<div
|
||||||
<div class="flex w-full flex-col items-center gap-8">
|
v-if="loadingAll || parseLoading"
|
||||||
|
class="absolute inset-0 z-50 flex items-center justify-center bg-white/70 backdrop-blur-[2px] dark:bg-gray-900/70 rounded-3xl"
|
||||||
|
>
|
||||||
|
<Spin size="large" tip="解析数据中..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="flex w-full flex-col items-center gap-8">
|
||||||
<h2 class="mb-3 text-3xl font-medium text-gray-800 dark:text-gray-100">导入药品价格表格</h2>
|
|
||||||
<p class="text-lg text-gray-500">上传 Excel 自动对比并提示价格涨跌差异</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 拖拽上传容器,配合 CSS :deep 强行 100% 宽度 -->
|
<div class="text-center">
|
||||||
<div class="batch-price-upload batch-price-upload--hero w-full transition-transform duration-300 hover:scale-[1.01]">
|
<h2 class="mb-3 text-3xl font-medium text-gray-800 dark:text-gray-100">导入药品价格表格</h2>
|
||||||
<UploadDragger
|
<p class="text-lg text-gray-500">上传 Excel 自动对比并提示价格涨跌差异</p>
|
||||||
v-model:file-list="fileList"
|
|
||||||
:before-upload="() => false"
|
|
||||||
:max-count="1"
|
|
||||||
:show-upload-list="false"
|
|
||||||
accept=".xlsx,.xls"
|
|
||||||
name="file"
|
|
||||||
@change="handleFileChange"
|
|
||||||
>
|
|
||||||
<div class="flex min-h-[420px] w-full flex-col items-center justify-center py-16">
|
|
||||||
<div class="mb-8 rounded-full bg-blue-50 p-6 text-blue-500 ring-8 ring-blue-50/50 dark:bg-blue-900/30 dark:ring-blue-900/20">
|
|
||||||
<svg class="h-14 w-14" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p class="text-2xl font-medium text-gray-700 dark:text-gray-200">点击或将 Excel 文件拖拽至此</p>
|
|
||||||
<p class="mt-4 text-lg text-gray-400">
|
|
||||||
纯前端本地解析,保护数据隐私,不会上传至服务器
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</UploadDragger>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex min-h-[28px] items-center gap-2">
|
|
||||||
<p v-if="selectedFile" class="text-lg font-medium text-blue-600 dark:text-blue-400">
|
|
||||||
已选文件:{{ selectedFile.name }}
|
|
||||||
<span v-if="parseLoading" class="ml-2 animate-pulse text-gray-400">解析中...</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
:disabled="loadingAll || warehouseItems.length === 0"
|
|
||||||
:loading="loadingAll"
|
|
||||||
size="large"
|
|
||||||
type="dashed"
|
|
||||||
class="!rounded-full px-10 py-2 shadow-sm"
|
|
||||||
@click="downloadTemplate"
|
|
||||||
>
|
|
||||||
下载标准模板
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</Spin>
|
|
||||||
|
<div class="batch-price-upload batch-price-upload--hero w-full transition-transform duration-300 hover:scale-[1.01]">
|
||||||
|
<UploadDragger
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
:before-upload="() => false"
|
||||||
|
:max-count="1"
|
||||||
|
:show-upload-list="false"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
name="file"
|
||||||
|
@change="handleFileChange"
|
||||||
|
>
|
||||||
|
<div class="flex min-h-[420px] w-full flex-col items-center justify-center py-16">
|
||||||
|
<div class="mb-8 rounded-full bg-blue-50 p-6 text-blue-500 ring-8 ring-blue-50/50 dark:bg-blue-900/30 dark:ring-blue-900/20">
|
||||||
|
<svg class="h-14 w-14" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="text-2xl font-medium text-gray-700 dark:text-gray-200">点击或将 Excel 文件拖拽至此</p>
|
||||||
|
<p class="mt-4 text-lg text-gray-400">
|
||||||
|
纯前端本地解析,保护数据隐私,不会上传至服务器
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</UploadDragger>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex min-h-[28px] items-center gap-2">
|
||||||
|
<p v-if="selectedFile" class="text-lg font-medium text-blue-600 dark:text-blue-400">
|
||||||
|
已选文件:{{ selectedFile.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
:disabled="loadingAll || warehouseItems.length === 0"
|
||||||
|
size="large"
|
||||||
|
type="dashed"
|
||||||
|
class="!rounded-full px-10 py-2 shadow-sm"
|
||||||
|
@click="downloadTemplate"
|
||||||
|
>
|
||||||
|
下载标准模板
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- State B: Result Grid View -->
|
<!-- State B: Result Grid View -->
|
||||||
<div v-else class="flex h-[calc(100vh-220px)] flex-col gap-4">
|
<div v-else class="flex flex-col gap-4 w-full" style="height: calc(100vh - 200px);">
|
||||||
|
|
||||||
<!-- Header Info Bar -->
|
<!-- Header Info Bar -->
|
||||||
<div class="flex flex-col gap-4 rounded-xl border border-gray-100 bg-gray-50/50 p-4 shadow-sm backdrop-blur-sm dark:border-gray-800 dark:bg-gray-900/30 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-4 rounded-xl border border-gray-100 bg-gray-50/50 p-4 shadow-sm backdrop-blur-sm dark:border-gray-800 dark:bg-gray-900/30 sm:flex-row sm:items-center sm:justify-between shrink-0">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="batch-price-upload batch-price-upload--mini">
|
<div class="batch-price-upload batch-price-upload--mini">
|
||||||
<UploadDragger
|
<UploadDragger
|
||||||
@@ -669,7 +671,7 @@ async function runBatchApply() {
|
|||||||
<div class="h-4 w-px bg-gray-300 dark:bg-gray-700"></div>
|
<div class="h-4 w-px bg-gray-300 dark:bg-gray-700"></div>
|
||||||
<Button
|
<Button
|
||||||
v-if="selectedFile"
|
v-if="selectedFile"
|
||||||
:loading="parseLoading"
|
:disabled="parseLoading"
|
||||||
size="small"
|
size="small"
|
||||||
type="link"
|
type="link"
|
||||||
@click="runParse"
|
@click="runParse"
|
||||||
@@ -679,19 +681,28 @@ async function runBatchApply() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Compare Grid -->
|
<!-- Main Compare Grid Container (原生 Flex,无第三方组件污染) -->
|
||||||
<div class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-gray-100 dark:bg-gray-900 dark:ring-gray-800">
|
<div class="relative flex flex-1 flex-col overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-gray-100 dark:bg-gray-900 dark:ring-gray-800 min-h-0">
|
||||||
<Spin :spinning="loadingAll || parseLoading" wrapperClassName="h-full flex flex-col min-h-0">
|
|
||||||
|
<!-- 核心隔离:绝对定位的 Loading 遮罩层,完美规避 Spin 破坏 Flex 布局高度的深坑 -->
|
||||||
|
<div
|
||||||
|
v-if="loadingAll || parseLoading"
|
||||||
|
class="absolute inset-0 z-50 flex items-center justify-center bg-white/70 backdrop-blur-[2px] dark:bg-gray-900/70"
|
||||||
|
>
|
||||||
|
<Spin size="large" tip="数据处理中..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 核心隔离:原生的独立滚动容器舱。不管内部自定义组件根节点什么脾气,在这里统统乖乖滚动 -->
|
||||||
|
<div class="flex-1 w-full overflow-y-auto p-4 custom-scrollbar relative">
|
||||||
<PriceCompareCardGrid
|
<PriceCompareCardGrid
|
||||||
v-model:filter-tab="filterTab"
|
v-model:filter-tab="filterTab"
|
||||||
class="flex min-h-0 flex-1 flex-col overflow-hidden p-4"
|
|
||||||
:rows="compareRows"
|
:rows="compareRows"
|
||||||
:saving-row-id="savingRowId"
|
:saving-row-id="savingRowId"
|
||||||
:unmatched-rows="unmatchedRows"
|
:unmatched-rows="unmatchedRows"
|
||||||
@cell-change="syncRowFromGrid"
|
@cell-change="syncRowFromGrid"
|
||||||
@row-save="handleRowSave"
|
@row-save="handleRowSave"
|
||||||
/>
|
/>
|
||||||
</Spin>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
@@ -728,6 +739,23 @@ async function runBatchApply() {
|
|||||||
transform: translateY(-20px);
|
transform: translateY(-20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 美化区:全局/局部自定义优雅滚动条 */
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(156, 163, 175, 0.5);
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba(107, 114, 128, 0.8);
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Upload Dragger overrides for modern aesthetic & FORCE FULL WIDTH */
|
/* Upload Dragger overrides for modern aesthetic & FORCE FULL WIDTH */
|
||||||
.batch-price-upload {
|
.batch-price-upload {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
const prefix = 'doctor-reception/';
|
const prefix = 'doctor-reception/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 医生应用特色方到开方区
|
||||||
|
*/
|
||||||
|
export async function applySpecialPrescriptionApi(data: { register_id: number }) {
|
||||||
|
return requestClient.post<any>('special-prescription/apply', data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询用户列表
|
* 分页查询用户列表
|
||||||
* @param data
|
* @param data
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
getProcessRuleList,
|
getProcessRuleList,
|
||||||
getProductListDoctorReception,
|
getProductListDoctorReception,
|
||||||
receptionApi,
|
receptionApi,
|
||||||
|
applySpecialPrescriptionApi,
|
||||||
switchStoreApi,
|
switchStoreApi,
|
||||||
withdrawPrescriptionApi,
|
withdrawPrescriptionApi,
|
||||||
} from '#/views/doctor/doctor-reception/api';
|
} from '#/views/doctor/doctor-reception/api';
|
||||||
@@ -94,6 +95,24 @@ interface Patient {
|
|||||||
name: string;
|
name: string;
|
||||||
mobile: string;
|
mobile: string;
|
||||||
status: number;
|
status: number;
|
||||||
|
user_patient?: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
age?: number;
|
||||||
|
sex?: number;
|
||||||
|
mobile?: string;
|
||||||
|
};
|
||||||
|
special_prescription_patient_record?: {
|
||||||
|
id: number;
|
||||||
|
status: number;
|
||||||
|
special_prescription_id: number;
|
||||||
|
dose_count: number;
|
||||||
|
special_prescription?: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
cover_image?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserPatientHealthInquiry {
|
interface UserPatientHealthInquiry {
|
||||||
@@ -319,6 +338,14 @@ const activeCategory = ref(2);
|
|||||||
const canUseCommonPrescription = computed(() =>
|
const canUseCommonPrescription = computed(() =>
|
||||||
[1, 2].includes(activeCategory.value),
|
[1, 2].includes(activeCategory.value),
|
||||||
);
|
);
|
||||||
|
/** 当前挂号是否有关联的特色方选方记录(可重复导入) */
|
||||||
|
const hasSpecialPrescriptionRecord = computed(
|
||||||
|
() => !!patientInfo.value?.special_prescription_patient_record,
|
||||||
|
);
|
||||||
|
const specialPrescriptionRecord = computed(
|
||||||
|
() => patientInfo.value?.special_prescription_patient_record ?? null,
|
||||||
|
);
|
||||||
|
const applyingSpecialPrescription = ref(false);
|
||||||
const diagnosis = ref('');
|
const diagnosis = ref('');
|
||||||
const medicalAdvice = ref('');
|
const medicalAdvice = ref('');
|
||||||
const treatmentPrice = ref(0);
|
const treatmentPrice = ref(0);
|
||||||
@@ -997,6 +1024,63 @@ async function handleSelectCommonPrescription(data: any, type: number) {
|
|||||||
updateLocalStorage();
|
updateLocalStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用患者选择的特色方到处方购物车
|
||||||
|
* @param switchToPrescription 是否切换到开方 Tab(患者信息页导入时为 true)
|
||||||
|
*/
|
||||||
|
async function handleApplySpecialPrescription(switchToPrescription = false) {
|
||||||
|
const registerId = doctorReceptionRegisterId.value;
|
||||||
|
if (!registerId) {
|
||||||
|
message.warning('请先选择患者');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyingSpecialPrescription.value = true;
|
||||||
|
try {
|
||||||
|
const res = await applySpecialPrescriptionApi({ register_id: registerId });
|
||||||
|
const typeMap: Record<string, number> = {
|
||||||
|
west: 1,
|
||||||
|
chinese: 2,
|
||||||
|
granular: 3,
|
||||||
|
};
|
||||||
|
const categoryMap: Record<string, number> = {
|
||||||
|
west: 2,
|
||||||
|
chinese: 1,
|
||||||
|
granular: 1,
|
||||||
|
};
|
||||||
|
const prescriptionType = res.prescription_type || 'chinese';
|
||||||
|
activeCategory.value = categoryMap[prescriptionType] || 1;
|
||||||
|
if (switchToPrescription) {
|
||||||
|
tabType.value = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleSelectCommonPrescription(
|
||||||
|
{
|
||||||
|
prescription: {
|
||||||
|
dosage: res.dosage ?? res.dose_count ?? 7,
|
||||||
|
day_dosage: res.day_dosage ?? 2,
|
||||||
|
rule_type: res.rule_type ?? 1,
|
||||||
|
package_method_id: res.package_method_id ?? 2,
|
||||||
|
clinical_diagnose: res.clinical_diagnose ?? '',
|
||||||
|
doctor_order: res.doctor_order ?? '',
|
||||||
|
},
|
||||||
|
recipes: res.recipes || [],
|
||||||
|
},
|
||||||
|
typeMap[prescriptionType] || 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectPatientId.value) {
|
||||||
|
const value = await getPatientItem(selectPatientId.value);
|
||||||
|
patientInfo.value = value;
|
||||||
|
}
|
||||||
|
message.success(`已应用特色方:${res.special_prescription_name || ''}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('应用特色方失败:', error);
|
||||||
|
message.error('应用特色方失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
applyingSpecialPrescription.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 保存常用方相关方法 ====================
|
// ==================== 保存常用方相关方法 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2109,7 +2193,19 @@ watch(
|
|||||||
@click="selectPatient(patient)"
|
@click="selectPatient(patient)"
|
||||||
>
|
>
|
||||||
<div class="patient-info">
|
<div class="patient-info">
|
||||||
<h3>{{ patient.user_patient.name }}</h3>
|
<h3 class="flex flex-wrap items-center gap-2">
|
||||||
|
{{ patient.user_patient.name }}
|
||||||
|
<Tag
|
||||||
|
v-if="patient.special_prescription_patient_record"
|
||||||
|
:color="patient.special_prescription_patient_record.status === 0 ? 'orange' : 'default'"
|
||||||
|
>
|
||||||
|
特色方{{
|
||||||
|
patient.special_prescription_patient_record.status === 0
|
||||||
|
? '·待导入'
|
||||||
|
: ''
|
||||||
|
}}
|
||||||
|
</Tag>
|
||||||
|
</h3>
|
||||||
<p class="phone">{{ patient.user_patient.mobile }}</p>
|
<p class="phone">{{ patient.user_patient.mobile }}</p>
|
||||||
<p class="text-sm text-gray-500">
|
<p class="text-sm text-gray-500">
|
||||||
推广员:{{ patient.salesperson?.nick_name || '无' }}
|
推广员:{{ patient.salesperson?.nick_name || '无' }}
|
||||||
@@ -2183,6 +2279,52 @@ watch(
|
|||||||
}}
|
}}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
|
<Card
|
||||||
|
v-if="specialPrescriptionRecord"
|
||||||
|
class="mt-5"
|
||||||
|
title="特色方"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
|
<Image
|
||||||
|
v-if="specialPrescriptionRecord.special_prescription?.cover_image"
|
||||||
|
:src="specialPrescriptionRecord.special_prescription.cover_image"
|
||||||
|
:width="80"
|
||||||
|
:height="80"
|
||||||
|
class="rounded object-cover"
|
||||||
|
/>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="text-base font-medium">
|
||||||
|
{{ specialPrescriptionRecord.special_prescription?.name || '特色方' }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-gray-500">
|
||||||
|
剂量:{{ specialPrescriptionRecord.dose_count }} 剂
|
||||||
|
</div>
|
||||||
|
<Tag
|
||||||
|
v-if="specialPrescriptionRecord.status === 0"
|
||||||
|
class="mt-2"
|
||||||
|
color="orange"
|
||||||
|
>
|
||||||
|
待导入
|
||||||
|
</Tag>
|
||||||
|
<Tag
|
||||||
|
v-else-if="specialPrescriptionRecord.status === 1"
|
||||||
|
class="mt-2"
|
||||||
|
color="green"
|
||||||
|
>
|
||||||
|
已应用
|
||||||
|
</Tag>
|
||||||
|
<Tag v-else class="mt-2" color="default">已完成</Tag>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
v-if="hasSpecialPrescriptionRecord"
|
||||||
|
type="primary"
|
||||||
|
:loading="applyingSpecialPrescription"
|
||||||
|
@click="handleApplySpecialPrescription(true)"
|
||||||
|
>
|
||||||
|
导入
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
<Descriptions
|
<Descriptions
|
||||||
v-if="userPatientHealthInquiry"
|
v-if="userPatientHealthInquiry"
|
||||||
:column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 2, xs: 2 }"
|
:column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 2, xs: 2 }"
|
||||||
@@ -2389,6 +2531,15 @@ watch(
|
|||||||
>
|
>
|
||||||
选择常用方
|
选择常用方
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="hasSpecialPrescriptionRecord"
|
||||||
|
type="primary"
|
||||||
|
class="ml-3"
|
||||||
|
:loading="applyingSpecialPrescription"
|
||||||
|
@click="() => handleApplySpecialPrescription()"
|
||||||
|
>
|
||||||
|
应用特色方
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="canUseCommonPrescription"
|
v-if="canUseCommonPrescription"
|
||||||
type="default"
|
type="default"
|
||||||
|
|||||||
73
apps/web-antd/src/views/doctor/doctor/api/card.ts
Normal file
73
apps/web-antd/src/views/doctor/doctor/api/card.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'doctor/';
|
||||||
|
|
||||||
|
/** 构建医生卡片查询参数:id 与 su_id 至少传一个 */
|
||||||
|
function buildDoctorCardParams(
|
||||||
|
id?: number,
|
||||||
|
suId?: number,
|
||||||
|
extra: Record<string, any> = {},
|
||||||
|
) {
|
||||||
|
const params: Record<string, any> = { ...extra };
|
||||||
|
if (id) {
|
||||||
|
params.id = id;
|
||||||
|
}
|
||||||
|
if (suId) {
|
||||||
|
params.su_id = suId;
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDoctorCardApi(id?: number, suId?: number) {
|
||||||
|
return requestClient.get<any>(`${prefix}card`, {
|
||||||
|
params: buildDoctorCardParams(id, suId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDoctorCardStatsApi(
|
||||||
|
id?: number,
|
||||||
|
suId?: number,
|
||||||
|
storeId?: number,
|
||||||
|
) {
|
||||||
|
return requestClient.get<any>(`${prefix}card-stats`, {
|
||||||
|
params: buildDoctorCardParams(id, suId, { store_id: storeId }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDoctorCardPrescriptionsApi(
|
||||||
|
id?: number,
|
||||||
|
suId?: number,
|
||||||
|
params: Record<string, any> = {},
|
||||||
|
) {
|
||||||
|
return requestClient.get<any>(`${prefix}card-prescriptions`, {
|
||||||
|
params: buildDoctorCardParams(id, suId, params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDoctorCardPatientsApi(
|
||||||
|
id?: number,
|
||||||
|
suId?: number,
|
||||||
|
params: Record<string, any> = {},
|
||||||
|
) {
|
||||||
|
return requestClient.get<any>(`${prefix}card-patients`, {
|
||||||
|
params: buildDoctorCardParams(id, suId, params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDoctorCardOrdersApi(
|
||||||
|
id?: number,
|
||||||
|
suId?: number,
|
||||||
|
params: Record<string, any> = {},
|
||||||
|
) {
|
||||||
|
return requestClient.get<any>(`${prefix}card-orders`, {
|
||||||
|
params: buildDoctorCardParams(id, suId, params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateDoctorCredentialsApi(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}update-credentials`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateDoctorSignatureByAdminApi(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}update-signature-by-admin`, data);
|
||||||
|
}
|
||||||
@@ -0,0 +1,682 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Descriptions,
|
||||||
|
Empty,
|
||||||
|
Form,
|
||||||
|
Image,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
Spin,
|
||||||
|
Table,
|
||||||
|
Tabs,
|
||||||
|
Tag,
|
||||||
|
Divider
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import AvatarUpload from '#/components/form/components/avatar.vue';
|
||||||
|
import DoctorQrCodePreview from '#/components/modal/DoctorQrCodePreview.vue';
|
||||||
|
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
|
||||||
|
import OrderDetail from '#/views/business/order/product-order/components/detail.vue';
|
||||||
|
import { openPcWindowsApi, updateDoctor } from '../api';
|
||||||
|
import {
|
||||||
|
getDoctorCardApi,
|
||||||
|
getDoctorCardOrdersApi,
|
||||||
|
getDoctorCardPatientsApi,
|
||||||
|
getDoctorCardPrescriptionsApi,
|
||||||
|
updateDoctorCredentialsApi,
|
||||||
|
updateDoctorSignatureByAdminApi,
|
||||||
|
} from '../api/card';
|
||||||
|
|
||||||
|
const gridApi = ref<any>();
|
||||||
|
const doctorId = ref(0);
|
||||||
|
const suId = ref(0);
|
||||||
|
const hideStoresTab = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const activeTab = ref('overview');
|
||||||
|
const cardData = ref<any>(null);
|
||||||
|
|
||||||
|
const overviewForm = ref({
|
||||||
|
avatar: '',
|
||||||
|
name: '',
|
||||||
|
mobile: '',
|
||||||
|
good_at: '',
|
||||||
|
intro: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const credentialsForm = ref({
|
||||||
|
card_up: '',
|
||||||
|
card_down: '',
|
||||||
|
sign_type: 1,
|
||||||
|
sign_image: '',
|
||||||
|
qualification: '',
|
||||||
|
practicing: '',
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const savingOverview = ref(false);
|
||||||
|
const savingCredentials = ref(false);
|
||||||
|
|
||||||
|
const prescriptionLoading = ref(false);
|
||||||
|
const prescriptionList = ref<any[]>([]);
|
||||||
|
const prescriptionPage = ref(1);
|
||||||
|
const prescriptionTotal = ref(0);
|
||||||
|
|
||||||
|
const patientLoading = ref(false);
|
||||||
|
const patientList = ref<any[]>([]);
|
||||||
|
const patientPage = ref(1);
|
||||||
|
const patientTotal = ref(0);
|
||||||
|
|
||||||
|
const orderLoading = ref(false);
|
||||||
|
const orderList = ref<any[]>([]);
|
||||||
|
const orderPage = ref(1);
|
||||||
|
const orderTotal = ref(0);
|
||||||
|
|
||||||
|
const stats = computed(() => cardData.value?.stats_summary ?? {});
|
||||||
|
|
||||||
|
const [QrCodeModal, QrCodeModalApi] = useVbenModal({
|
||||||
|
connectedComponent: DoctorQrCodePreview,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [PrescriptionDetailModal, PrescriptionDetailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: PrescriptionDetail,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [OrderDetailDrawer, OrderDetailModalApi] = useVbenModal({
|
||||||
|
connectedComponent: OrderDetail,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 获取当前医生标识,列表类接口复用 */
|
||||||
|
function getCardIdentity() {
|
||||||
|
return {
|
||||||
|
id: doctorId.value || undefined,
|
||||||
|
suId: suId.value || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCard() {
|
||||||
|
if (!doctorId.value && !suId.value) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const { id, suId: querySuId } = getCardIdentity();
|
||||||
|
const res = await getDoctorCardApi(id, querySuId);
|
||||||
|
cardData.value = res;
|
||||||
|
doctorId.value = res?.doctor_info?.id ?? doctorId.value;
|
||||||
|
suId.value = res?.doctor_info?.su_id ?? suId.value;
|
||||||
|
overviewForm.value = {
|
||||||
|
avatar: res?.doctor_info?.avatar ?? '',
|
||||||
|
name: res?.doctor_info?.name ?? '',
|
||||||
|
mobile: res?.doctor_info?.mobile ?? '',
|
||||||
|
good_at: res?.doctor_info?.good_at ?? '',
|
||||||
|
intro: res?.doctor_info?.intro ?? '',
|
||||||
|
};
|
||||||
|
credentialsForm.value = {
|
||||||
|
card_up: res?.identity?.card_up ?? '',
|
||||||
|
card_down: res?.identity?.card_down ?? '',
|
||||||
|
sign_type: res?.identity?.sign_type ?? 1,
|
||||||
|
sign_image: res?.identity?.sign_image ?? '',
|
||||||
|
qualification: res?.practicing?.qualification ?? '',
|
||||||
|
practicing: res?.practicing?.practicing ?? '',
|
||||||
|
title: res?.practicing?.title ?? '',
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveOverview() {
|
||||||
|
savingOverview.value = true;
|
||||||
|
try {
|
||||||
|
const res = await updateDoctor({
|
||||||
|
id: doctorId.value,
|
||||||
|
su_id: suId.value,
|
||||||
|
...overviewForm.value,
|
||||||
|
});
|
||||||
|
if (res?.sync && !res.sync.admin_found) {
|
||||||
|
message.warning('资料已保存,但该医生未开通 PC 后台,管理员头像未同步');
|
||||||
|
} else {
|
||||||
|
message.success('保存成功');
|
||||||
|
}
|
||||||
|
await loadCard();
|
||||||
|
gridApi.value?.reload?.();
|
||||||
|
} finally {
|
||||||
|
savingOverview.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCredentials() {
|
||||||
|
savingCredentials.value = true;
|
||||||
|
try {
|
||||||
|
await updateDoctorCredentialsApi({
|
||||||
|
id: doctorId.value,
|
||||||
|
...credentialsForm.value,
|
||||||
|
});
|
||||||
|
if (credentialsForm.value.sign_image) {
|
||||||
|
await updateDoctorSignatureByAdminApi({
|
||||||
|
id: doctorId.value,
|
||||||
|
sign_image: credentialsForm.value.sign_image,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
message.success('证书资料保存成功');
|
||||||
|
await loadCard();
|
||||||
|
} finally {
|
||||||
|
savingCredentials.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPrescriptions(page = 1) {
|
||||||
|
prescriptionLoading.value = true;
|
||||||
|
try {
|
||||||
|
const { id, suId: querySuId } = getCardIdentity();
|
||||||
|
const res = await getDoctorCardPrescriptionsApi(id, querySuId, {
|
||||||
|
page,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
prescriptionList.value = res.items ?? [];
|
||||||
|
prescriptionPage.value = res.page ?? 1;
|
||||||
|
prescriptionTotal.value = res.total ?? 0;
|
||||||
|
} finally {
|
||||||
|
prescriptionLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPatients(page = 1) {
|
||||||
|
patientLoading.value = true;
|
||||||
|
try {
|
||||||
|
const { id, suId: querySuId } = getCardIdentity();
|
||||||
|
const res = await getDoctorCardPatientsApi(id, querySuId, {
|
||||||
|
page,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
patientList.value = res.items ?? [];
|
||||||
|
patientPage.value = res.page ?? 1;
|
||||||
|
patientTotal.value = res.total ?? 0;
|
||||||
|
} finally {
|
||||||
|
patientLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadOrders(page = 1) {
|
||||||
|
orderLoading.value = true;
|
||||||
|
try {
|
||||||
|
const { id, suId: querySuId } = getCardIdentity();
|
||||||
|
const res = await getDoctorCardOrdersApi(id, querySuId, {
|
||||||
|
page,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
orderList.value = res.items ?? [];
|
||||||
|
orderPage.value = res.page ?? 1;
|
||||||
|
orderTotal.value = res.total ?? 0;
|
||||||
|
} finally {
|
||||||
|
orderLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTabChange(key: string) {
|
||||||
|
activeTab.value = key;
|
||||||
|
if (key === 'prescriptions' && prescriptionList.value.length === 0) {
|
||||||
|
loadPrescriptions();
|
||||||
|
}
|
||||||
|
if (key === 'patients' && patientList.value.length === 0) {
|
||||||
|
loadPatients();
|
||||||
|
}
|
||||||
|
if (key === 'orders' && orderList.value.length === 0) {
|
||||||
|
loadOrders();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPrescriptionDetail(prescriptionId: number) {
|
||||||
|
PrescriptionDetailModalApi.setData({ values: prescriptionId });
|
||||||
|
PrescriptionDetailModalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openOrderDetail(orderId: number) {
|
||||||
|
OrderDetailModalApi.setData({ id: orderId });
|
||||||
|
OrderDetailModalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openStoreQr(storeId: number) {
|
||||||
|
QrCodeModalApi.setData({
|
||||||
|
values: { doctor_id: suId.value, store_id: storeId },
|
||||||
|
});
|
||||||
|
QrCodeModalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPcWindows() {
|
||||||
|
openPcWindowsApi(doctorId.value).then(() => {
|
||||||
|
message.success('开通成功');
|
||||||
|
loadCard();
|
||||||
|
gridApi.value?.reload?.();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const prescriptionColumns = [
|
||||||
|
{ title: '处方号', dataIndex: 'prescription_no', key: 'prescription_no' },
|
||||||
|
{ title: '患者', dataIndex: ['user_patient', 'name'], key: 'patient' },
|
||||||
|
{ title: '诊所', dataIndex: ['store', 'name'], key: 'store' },
|
||||||
|
{ title: '金额(元)', dataIndex: 'total_pay_price', key: 'amount' },
|
||||||
|
{ title: '开具时间', dataIndex: 'created_at', key: 'created_at' },
|
||||||
|
{ title: '操作', key: 'action', width: 80 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const patientColumns = [
|
||||||
|
{ title: '姓名', dataIndex: 'name', key: 'name' },
|
||||||
|
{ title: '性别', dataIndex: 'sex', key: 'sex' },
|
||||||
|
{ title: '手机号', dataIndex: 'mobile', key: 'mobile' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const orderColumns = [
|
||||||
|
{ title: '订单ID', dataIndex: 'id', key: 'id' },
|
||||||
|
{ title: '处方号', dataIndex: ['prescription', 'prescription_no'], key: 'pno' },
|
||||||
|
{ title: '患者', dataIndex: ['user_patient', 'name'], key: 'patient' },
|
||||||
|
{ title: '金额(元)', dataIndex: 'total_price', key: 'total_price' },
|
||||||
|
{ title: '支付状态', dataIndex: 'is_pay', key: 'is_pay' },
|
||||||
|
{ title: '操作', key: 'action', width: 80 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
closeOnClickModal: false,
|
||||||
|
fullscreenButton: true,
|
||||||
|
draggable: true,
|
||||||
|
class: 'w-[900px] xl:w-[1100px]',
|
||||||
|
footer: false,
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
const data = modalApi.getData<Record<string, any>>();
|
||||||
|
gridApi.value = data?.gridApi;
|
||||||
|
doctorId.value = data?.id ?? data?.values?.id ?? 0;
|
||||||
|
suId.value = data?.su_id ?? data?.values?.su_id ?? 0;
|
||||||
|
hideStoresTab.value = !!data?.hideStoresTab;
|
||||||
|
activeTab.value = 'overview';
|
||||||
|
prescriptionList.value = [];
|
||||||
|
patientList.value = [];
|
||||||
|
orderList.value = [];
|
||||||
|
loadCard();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="cardData?.doctor_info?.name ? `医生档案 · ${cardData.doctor_info.name}` : '医生档案'">
|
||||||
|
<Spin :spinning="loading">
|
||||||
|
|
||||||
|
<!-- 顶部核心数据区:图文结合 + 暗色自适应 -->
|
||||||
|
<div v-if="cardData" class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6 px-1">
|
||||||
|
<!-- 接诊数 -->
|
||||||
|
<div class="relative overflow-hidden flex flex-col p-5 bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800/50 rounded-xl shadow-sm group hover:shadow-md transition-all">
|
||||||
|
<div class="flex justify-between items-start relative z-10">
|
||||||
|
<div>
|
||||||
|
<span class="text-blue-600/80 dark:text-blue-300/80 text-sm font-semibold">累计接诊</span>
|
||||||
|
<div class="text-2xl font-bold text-blue-900 dark:text-blue-100 mt-2">
|
||||||
|
{{ stats.reception_count ?? 0 }} <span class="text-xs font-normal text-blue-700/60 dark:text-blue-300/60">次</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-2 bg-blue-100 dark:bg-blue-800/50 rounded-lg text-blue-500 dark:text-blue-300">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 拒诊数 -->
|
||||||
|
<div class="relative overflow-hidden flex flex-col p-5 bg-rose-50 dark:bg-rose-900/20 border border-rose-100 dark:border-rose-800/50 rounded-xl shadow-sm group hover:shadow-md transition-all">
|
||||||
|
<div class="flex justify-between items-start relative z-10">
|
||||||
|
<div>
|
||||||
|
<span class="text-rose-600/80 dark:text-rose-300/80 text-sm font-semibold">拒诊数量</span>
|
||||||
|
<div class="text-2xl font-bold text-rose-900 dark:text-rose-100 mt-2">
|
||||||
|
{{ stats.refused_count ?? 0 }} <span class="text-xs font-normal text-rose-700/60 dark:text-rose-300/60">次</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-2 bg-rose-100 dark:bg-rose-800/50 rounded-lg text-rose-500 dark:text-rose-300">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" /></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 处方数 -->
|
||||||
|
<div class="relative overflow-hidden flex flex-col p-5 bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-100 dark:border-emerald-800/50 rounded-xl shadow-sm group hover:shadow-md transition-all">
|
||||||
|
<div class="flex justify-between items-start relative z-10">
|
||||||
|
<div>
|
||||||
|
<span class="text-emerald-600/80 dark:text-emerald-300/80 text-sm font-semibold">处方总数</span>
|
||||||
|
<div class="text-2xl font-bold text-emerald-900 dark:text-emerald-100 mt-2">
|
||||||
|
{{ stats.prescription_count ?? 0 }} <span class="text-xs font-normal text-emerald-700/60 dark:text-emerald-300/60">张</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-2 bg-emerald-100 dark:bg-emerald-800/50 rounded-lg text-emerald-500 dark:text-emerald-300">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 处方金额 -->
|
||||||
|
<div class="relative overflow-hidden flex flex-col p-5 bg-purple-50 dark:bg-purple-900/20 border border-purple-100 dark:border-purple-800/50 rounded-xl shadow-sm group hover:shadow-md transition-all">
|
||||||
|
<div class="flex justify-between items-start relative z-10">
|
||||||
|
<div>
|
||||||
|
<span class="text-purple-600/80 dark:text-purple-300/80 text-sm font-semibold">处方总额</span>
|
||||||
|
<div class="text-2xl font-bold text-purple-900 dark:text-purple-100 mt-2">
|
||||||
|
<span class="text-lg">¥</span> {{ Number(stats.prescription_amount ?? 0).toFixed(2) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-2 bg-purple-100 dark:bg-purple-800/50 rounded-lg text-purple-500 dark:text-purple-300">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider class="!my-4 dark:border-slate-700/60" />
|
||||||
|
|
||||||
|
<!-- 主体内容区 -->
|
||||||
|
<Tabs
|
||||||
|
:active-key="activeTab"
|
||||||
|
@change="onTabChange"
|
||||||
|
tab-position="left"
|
||||||
|
class="min-h-[480px] custom-vertical-tabs"
|
||||||
|
>
|
||||||
|
<Tabs.TabPane key="overview" tab="概览信息">
|
||||||
|
<div class="pl-6">
|
||||||
|
<div class="flex justify-between items-end mb-4">
|
||||||
|
<h3 class="text-base font-semibold text-gray-800 dark:text-slate-200">基本资料</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 dark:bg-[#1f242e] p-5 rounded-lg border border-gray-100 dark:border-slate-700 mb-6 transition-colors">
|
||||||
|
<Row :gutter="32">
|
||||||
|
<Col :span="6" class="flex flex-col items-center border-r border-gray-200 dark:border-slate-700">
|
||||||
|
<div class="text-gray-500 dark:text-slate-400 mb-3 text-sm">医生头像</div>
|
||||||
|
<AvatarUpload v-model:value="overviewForm.avatar" class="shadow-sm rounded-full overflow-hidden" />
|
||||||
|
</Col>
|
||||||
|
<Col :span="18">
|
||||||
|
<Form layout="vertical" class="grid grid-cols-2 gap-x-6">
|
||||||
|
<Form.Item label="医生姓名" class="mb-4">
|
||||||
|
<Input v-model:value="overviewForm.name" placeholder="请输入姓名" size="large" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="手机号码" class="mb-4">
|
||||||
|
<Input v-model:value="overviewForm.mobile" placeholder="请输入手机号码" size="large" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="专业擅长" class="col-span-2 mb-4">
|
||||||
|
<Input.TextArea v-model:value="overviewForm.good_at" :rows="2" placeholder="填写医生擅长的领域..." />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="个人简介" class="col-span-2 mb-4">
|
||||||
|
<Input.TextArea v-model:value="overviewForm.intro" :rows="3" placeholder="填写医生简介..." />
|
||||||
|
</Form.Item>
|
||||||
|
<div class="col-span-2 text-right mt-2">
|
||||||
|
<Button type="primary" size="large" :loading="savingOverview" @click="saveOverview">
|
||||||
|
保存基本信息
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-base font-semibold text-gray-800 dark:text-slate-200 mb-4 mt-8">系统档案 (只读)</h3>
|
||||||
|
<Descriptions bordered :column="3" size="middle" class="bg-white dark:bg-[#18181c] shadow-sm rounded-lg overflow-hidden border-gray-200 dark:border-slate-700">
|
||||||
|
<Descriptions.Item label="医生ID">{{ cardData?.doctor_info?.id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="微信平台ID">{{ cardData?.doctor_info?.su_id }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="审核状态">
|
||||||
|
<Tag v-if="cardData?.service_user?.status === 2" color="success" class="!mr-0">已认证通过</Tag>
|
||||||
|
<Tag v-else color="default" class="!mr-0">{{ cardData?.service_user?.status || '未审核' }}</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="所属科室">{{ cardData?.doctor_info?.depart?.name ?? '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="执业职称">{{ cardData?.doctor_info?.title?.name ?? '-' }}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="诊疗科目">{{ cardData?.doctor_info?.subjects?.name ?? '-' }}</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<Tabs.TabPane key="credentials" tab="证书与签名">
|
||||||
|
<div class="pl-6">
|
||||||
|
<div class="flex justify-between items-center mb-5">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-base font-semibold text-gray-800 dark:text-slate-200">资质证书管理</h3>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-slate-400 mt-1">请上传清晰、无遮挡的证件扫描件或照片</p>
|
||||||
|
</div>
|
||||||
|
<Button type="primary" :loading="savingCredentials" @click="saveCredentials">
|
||||||
|
保存全部资质
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form layout="vertical">
|
||||||
|
<!-- 通过比例控制让上传区域更像“证件” -->
|
||||||
|
<div class="grid grid-cols-2 lg:grid-cols-3 gap-6 bg-gray-50 dark:bg-[#1f242e] p-5 rounded-lg border border-gray-100 dark:border-slate-700">
|
||||||
|
<Form.Item label="身份证 (正面)" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 text-center">
|
||||||
|
<div class="aspect-[1.6/1] w-full flex items-center justify-center">
|
||||||
|
<AvatarUpload v-model:value="credentialsForm.card_up" />
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="身份证 (反面)" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 text-center">
|
||||||
|
<div class="aspect-[1.6/1] w-full flex items-center justify-center">
|
||||||
|
<AvatarUpload v-model:value="credentialsForm.card_down" />
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="执业资格证书" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 text-center">
|
||||||
|
<div class="aspect-[1.6/1] w-full flex items-center justify-center">
|
||||||
|
<AvatarUpload v-model:value="credentialsForm.qualification" />
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="执业证书" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 text-center">
|
||||||
|
<div class="aspect-[1.6/1] w-full flex items-center justify-center">
|
||||||
|
<AvatarUpload v-model:value="credentialsForm.practicing" />
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="职称证书" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 text-center">
|
||||||
|
<div class="aspect-[1.6/1] w-full flex items-center justify-center">
|
||||||
|
<AvatarUpload v-model:value="credentialsForm.title" />
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<Form.Item label="医生签名图 (透明底)" class="mb-0 bg-white dark:bg-[#18181c] p-4 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60 flex-1 text-center">
|
||||||
|
<AvatarUpload v-model:value="credentialsForm.sign_image" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="签名验证类型" class="mb-0 bg-white dark:bg-[#18181c] p-3 rounded-md shadow-sm border border-gray-100 dark:border-slate-700/60">
|
||||||
|
<Select v-model:value="credentialsForm.sign_type" size="large" :options="[
|
||||||
|
{ label: '电子认证签名', value: 1 },
|
||||||
|
{ label: '手写扫描签名', value: 2 },
|
||||||
|
]" />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<Tabs.TabPane v-if="!hideStoresTab" key="stores" tab="关联诊所">
|
||||||
|
<div class="pl-6 h-full">
|
||||||
|
<Empty v-if="!cardData?.stores?.length" description="暂无绑定的诊所记录" class="mt-20" />
|
||||||
|
<div v-else class="grid grid-cols-2 gap-5">
|
||||||
|
<Card
|
||||||
|
v-for="item in cardData.stores"
|
||||||
|
:key="item.store_id"
|
||||||
|
:title="item.store?.name ?? `诊所编号 #${item.store_id}`"
|
||||||
|
class="shadow-sm border-gray-200 dark:border-slate-700 hover:shadow-md transition-shadow dark:bg-[#18181c]"
|
||||||
|
:head-style="{ borderBottom: '1px solid var(--ant-color-split)' }"
|
||||||
|
>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-slate-400 space-y-3 mb-4">
|
||||||
|
<p class="flex items-start">
|
||||||
|
<svg class="w-4 h-4 mr-2 mt-0.5 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||||
|
<span>{{ item.store?.province }}{{ item.store?.city }}{{ item.store?.position }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-2 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" /></svg>
|
||||||
|
<span>{{ item.store?.contact }} ({{ item.store?.mobile }})</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between border-t border-gray-100 dark:border-slate-700/60 pt-4 mt-4">
|
||||||
|
<div v-if="item.qr_code" class="border dark:border-slate-600 p-1 rounded bg-white">
|
||||||
|
<Image :src="item.qr_code" :width="56" :height="56" class="rounded-sm" />
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-gray-400 text-xs">暂无专属海报</span>
|
||||||
|
<Button type="primary" ghost size="small" @click="openStoreQr(item.store_id)">查看分享海报</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<Tabs.TabPane key="admin" tab="后台账号">
|
||||||
|
<div class="pl-6 max-w-2xl">
|
||||||
|
<template v-if="cardData?.admin">
|
||||||
|
<div class="bg-white dark:bg-[#1f242e] border border-gray-200 dark:border-slate-700 rounded-lg p-6 flex items-center gap-6 shadow-sm">
|
||||||
|
<Avatar :src="cardData.admin.avatar" :size="84" class="border-2 border-blue-50 dark:border-blue-900 shadow-sm" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-xl font-bold text-gray-800 dark:text-slate-100 mb-1">{{ cardData.admin.nick_name }}</h3>
|
||||||
|
<p class="text-gray-500 dark:text-slate-400 mb-4 flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
|
||||||
|
{{ cardData.admin.phone }}
|
||||||
|
</p>
|
||||||
|
<Tag color="processing">PC 端权限已开通</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="bg-orange-50 dark:bg-orange-950/30 border border-orange-100 dark:border-orange-900/50 rounded-lg p-8 text-center mt-10">
|
||||||
|
<div class="text-orange-400 text-5xl mb-4">🖥️</div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-800 dark:text-slate-200 mb-2">未开通 PC 端管理权限</h3>
|
||||||
|
<p class="text-gray-500 dark:text-slate-400 mb-6 text-sm">开通后,医生可使用电脑端登录系统进行高级管理操作,如查看完整报表等。</p>
|
||||||
|
<Button type="primary" size="large" @click="openPcWindows">立即开通后台账号</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<Tabs.TabPane key="service" tab="服务配置">
|
||||||
|
<div class="pl-6">
|
||||||
|
<h3 class="text-base font-semibold text-gray-800 dark:text-slate-200 mb-4">出诊与挂号服务</h3>
|
||||||
|
<Descriptions v-if="cardData?.service" bordered :column="2" class="bg-white dark:bg-[#18181c] shadow-sm rounded-lg overflow-hidden border-gray-200 dark:border-slate-700">
|
||||||
|
<Descriptions.Item label="当前挂号状态">
|
||||||
|
<Tag :color="cardData.service.register_status === 1 ? 'success' : 'default'">
|
||||||
|
{{ cardData.service.status_text }}
|
||||||
|
</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="挂号服务费(元)">
|
||||||
|
<span class="text-lg font-bold text-orange-500 dark:text-orange-400">¥ {{ cardData.service.register_price }}</span>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="每日号源上限">
|
||||||
|
<span class="font-medium text-gray-800 dark:text-slate-300">{{ cardData.service.register_num }} 个</span>
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
<Empty v-else description="暂未配置任何服务项" class="mt-20" />
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<Tabs.TabPane key="prescriptions" tab="处方记录">
|
||||||
|
<div class="pl-6 h-full">
|
||||||
|
<Table
|
||||||
|
:columns="prescriptionColumns"
|
||||||
|
:data-source="prescriptionList"
|
||||||
|
:loading="prescriptionLoading"
|
||||||
|
:pagination="{
|
||||||
|
current: prescriptionPage,
|
||||||
|
total: prescriptionTotal,
|
||||||
|
pageSize: 10,
|
||||||
|
onChange: loadPrescriptions,
|
||||||
|
showTotal: (total) => `共 ${total} 条数据`
|
||||||
|
}"
|
||||||
|
row-key="id"
|
||||||
|
size="middle"
|
||||||
|
class="border border-gray-100 dark:border-slate-700 rounded-lg shadow-sm"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'prescription_no'">
|
||||||
|
<Button type="link" size="small" @click="openPrescriptionDetail(record.id)">
|
||||||
|
{{ record.prescription_no }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<Button type="link" size="small" @click="openPrescriptionDetail(record.id)">查看</Button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<Tabs.TabPane key="patients" tab="患者列表">
|
||||||
|
<div class="pl-6 h-full">
|
||||||
|
<Table
|
||||||
|
:columns="patientColumns"
|
||||||
|
:data-source="patientList"
|
||||||
|
:loading="patientLoading"
|
||||||
|
:pagination="{
|
||||||
|
current: patientPage,
|
||||||
|
total: patientTotal,
|
||||||
|
pageSize: 10,
|
||||||
|
onChange: loadPatients,
|
||||||
|
showTotal: (total) => `共 ${total} 名患者`
|
||||||
|
}"
|
||||||
|
row-key="id"
|
||||||
|
size="middle"
|
||||||
|
class="border border-gray-100 dark:border-slate-700 rounded-lg shadow-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<Tabs.TabPane key="orders" tab="相关订单">
|
||||||
|
<div class="pl-6 h-full">
|
||||||
|
<Table
|
||||||
|
:columns="orderColumns"
|
||||||
|
:data-source="orderList"
|
||||||
|
:loading="orderLoading"
|
||||||
|
:pagination="{
|
||||||
|
current: orderPage,
|
||||||
|
total: orderTotal,
|
||||||
|
pageSize: 10,
|
||||||
|
onChange: loadOrders,
|
||||||
|
showTotal: (total) => `共 ${total} 笔订单`
|
||||||
|
}"
|
||||||
|
row-key="id"
|
||||||
|
size="middle"
|
||||||
|
class="border border-gray-100 dark:border-slate-700 rounded-lg shadow-sm"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'id'">
|
||||||
|
<Button type="link" size="small" @click="openOrderDetail(record.id)">
|
||||||
|
{{ record.id }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'is_pay'">
|
||||||
|
<Tag :color="record.is_pay ? 'success' : 'default'">
|
||||||
|
{{ record.is_pay ? '已支付' : '待支付' }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'total_price'">
|
||||||
|
<span class="font-medium text-gray-700 dark:text-slate-300">¥ {{ record.total_price }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<Button type="link" size="small" @click="openOrderDetail(record.id)">查看</Button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</Spin>
|
||||||
|
</Modal>
|
||||||
|
<QrCodeModal />
|
||||||
|
<PrescriptionDetailModal />
|
||||||
|
<OrderDetailDrawer />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 使用 CSS 变量自适应暗黑模式的 Tabs 高亮,替代硬编码颜色 */
|
||||||
|
:deep(.custom-vertical-tabs .ant-tabs-nav) {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
:deep(.custom-vertical-tabs .ant-tabs-tab) {
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
:deep(.custom-vertical-tabs .ant-tabs-tab-active) {
|
||||||
|
background-color: var(--ant-primary-color-active-deprecated-f-12, rgba(22, 119, 255, 0.08));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -208,12 +208,6 @@ export const infoModalFormProps: VbenFormProps = {
|
|||||||
},
|
},
|
||||||
fieldName: 'mobile',
|
fieldName: 'mobile',
|
||||||
formItemClass: 'col-span-6',
|
formItemClass: 'col-span-6',
|
||||||
dependencies: {
|
|
||||||
show: (values) => {
|
|
||||||
return values.id == null;
|
|
||||||
},
|
|
||||||
triggerFields: ['id'],
|
|
||||||
},
|
|
||||||
label: '手机号码',
|
label: '手机号码',
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const gridOptions: VxeGridProps<RowType> = {
|
|||||||
columns: [
|
columns: [
|
||||||
{ type: 'checkbox', width: 60 },
|
{ type: 'checkbox', width: 60 },
|
||||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||||
{ field: 'name', align: 'left', title: '名称' },
|
{ field: 'name', align: 'left', title: '名称', slots: { default: 'name' } },
|
||||||
{ field: 'mobile', align: 'left', title: '手机号' },
|
{ field: 'mobile', align: 'left', title: '手机号' },
|
||||||
{
|
{
|
||||||
field: 'avatar',
|
field: 'avatar',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { auditApi, openPcWindowsApi, refuseApi } from './api';
|
|||||||
import BindStore from './components/BindStoreModal.vue';
|
import BindStore from './components/BindStoreModal.vue';
|
||||||
import BindSubjects from './components/BindSubjectsModal.vue';
|
import BindSubjects from './components/BindSubjectsModal.vue';
|
||||||
import FormModalDemo from './components/modal.vue';
|
import FormModalDemo from './components/modal.vue';
|
||||||
|
import DoctorCardModal from './components/DoctorCardModal.vue';
|
||||||
import { formOptions } from './config/search';
|
import { formOptions } from './config/search';
|
||||||
import { gridOptions } from './config/table';
|
import { gridOptions } from './config/table';
|
||||||
|
|
||||||
@@ -50,6 +51,10 @@ const [BindSubjectsModal, BindSubjectsModalApi] = useVbenModal({
|
|||||||
connectedComponent: BindSubjects,
|
connectedComponent: BindSubjects,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [DoctorCardModals, DoctorCardModalApi] = useVbenModal({
|
||||||
|
connectedComponent: DoctorCardModal,
|
||||||
|
});
|
||||||
|
|
||||||
const showModal = (data = {}, isUpdate = false) => {
|
const showModal = (data = {}, isUpdate = false) => {
|
||||||
formModalApi.setData({
|
formModalApi.setData({
|
||||||
// 表单值
|
// 表单值
|
||||||
@@ -77,6 +82,15 @@ const showBindSubjectsModal = (row: Record<string, any>) => {
|
|||||||
BindSubjectsModalApi.open();
|
BindSubjectsModalApi.open();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showDoctorCard = (row: Record<string, any>) => {
|
||||||
|
DoctorCardModalApi.setData({
|
||||||
|
id: row.id,
|
||||||
|
su_id: row.su_id,
|
||||||
|
gridApi,
|
||||||
|
});
|
||||||
|
DoctorCardModalApi.open();
|
||||||
|
};
|
||||||
|
|
||||||
const openPcWindows = (id: number) => {
|
const openPcWindows = (id: number) => {
|
||||||
openPcWindowsApi(id).then(() => {
|
openPcWindowsApi(id).then(() => {
|
||||||
message.success('开通成功!');
|
message.success('开通成功!');
|
||||||
@@ -104,6 +118,7 @@ const refuse = (id: number) => {
|
|||||||
<FormModal />
|
<FormModal />
|
||||||
<BindStoreModal />
|
<BindStoreModal />
|
||||||
<BindSubjectsModal />
|
<BindSubjectsModal />
|
||||||
|
<DoctorCardModals />
|
||||||
<Grid>
|
<Grid>
|
||||||
<template #toolbar-buttons>
|
<template #toolbar-buttons>
|
||||||
<TableAction
|
<TableAction
|
||||||
@@ -140,6 +155,11 @@ const refuse = (id: number) => {
|
|||||||
<template #avatar="{ row }">
|
<template #avatar="{ row }">
|
||||||
<Image :src="row.avatar" height="30" width="30" />
|
<Image :src="row.avatar" height="30" width="30" />
|
||||||
</template>
|
</template>
|
||||||
|
<template #name="{ row }">
|
||||||
|
<Button type="link" size="small" @click="showDoctorCard(row)">
|
||||||
|
{{ row.name }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
<template #stores="{ row }">
|
<template #stores="{ row }">
|
||||||
<p v-for="item in row.stores" :key="item.store_id">
|
<p v-for="item in row.stores" :key="item.store_id">
|
||||||
{{ item?.store?.name }}
|
{{ item?.store?.name }}
|
||||||
@@ -167,6 +187,13 @@ const refuse = (id: number) => {
|
|||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<TableAction
|
<TableAction
|
||||||
:actions="[
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '医生卡片',
|
||||||
|
type: 'link',
|
||||||
|
icon: 'ant-design:idcard-outlined',
|
||||||
|
size: 'small',
|
||||||
|
onClick: showDoctorCard.bind(null, row),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '编辑',
|
label: '编辑',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { formatAddressDisplay } from '#/util/address-index';
|
|||||||
|
|
||||||
import { createAdminGridOptions } from './table-config';
|
import { createAdminGridOptions } from './table-config';
|
||||||
import { getRoleMeta } from './role-meta';
|
import { getRoleMeta } from './role-meta';
|
||||||
|
import DoctorCardModal from '#/views/doctor/doctor/components/DoctorCardModal.vue';
|
||||||
|
|
||||||
function formatAdminRegion(row: {
|
function formatAdminRegion(row: {
|
||||||
province_id?: number;
|
province_id?: number;
|
||||||
@@ -107,6 +108,16 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [DoctorCardModals, DoctorCardModalApi] = useVbenModal({
|
||||||
|
connectedComponent: DoctorCardModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const openDoctorCard = (row: { doctor_id?: number }) => {
|
||||||
|
if (!row.doctor_id) return;
|
||||||
|
DoctorCardModalApi.setData({ su_id: row.doctor_id });
|
||||||
|
DoctorCardModalApi.open();
|
||||||
|
};
|
||||||
|
|
||||||
const gridEvents: VxeGridListeners<any> = {
|
const gridEvents: VxeGridListeners<any> = {
|
||||||
checkboxChange() {
|
checkboxChange() {
|
||||||
const records = gridApi.grid.getCheckboxRecords();
|
const records = gridApi.grid.getCheckboxRecords();
|
||||||
@@ -195,6 +206,7 @@ const copyToClipboard = async (text: string) => {
|
|||||||
>
|
>
|
||||||
<Form />
|
<Form />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<DoctorCardModals />
|
||||||
<Grid>
|
<Grid>
|
||||||
<template #toolbar-buttons>
|
<template #toolbar-buttons>
|
||||||
<TableAction
|
<TableAction
|
||||||
@@ -229,6 +241,17 @@ const copyToClipboard = async (text: string) => {
|
|||||||
<template #avatar="{ row }">
|
<template #avatar="{ row }">
|
||||||
<Image :src="row.avatar" height="30" width="30" />
|
<Image :src="row.avatar" height="30" width="30" />
|
||||||
</template>
|
</template>
|
||||||
|
<template #nick_name="{ row }">
|
||||||
|
<Button
|
||||||
|
v-if="meta.formType === 'doctor' && row.doctor_id"
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="openDoctorCard(row)"
|
||||||
|
>
|
||||||
|
{{ row.nick_name }}
|
||||||
|
</Button>
|
||||||
|
<span v-else>{{ row.nick_name }}</span>
|
||||||
|
</template>
|
||||||
<template #region="{ row }">
|
<template #region="{ row }">
|
||||||
{{ formatAdminRegion(row) }}
|
{{ formatAdminRegion(row) }}
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function buildColumns(formType: AdminFormType) {
|
|||||||
const cols: VxeGridProps<RowType>['columns'] = [
|
const cols: VxeGridProps<RowType>['columns'] = [
|
||||||
{ type: 'checkbox', width: 60 },
|
{ type: 'checkbox', width: 60 },
|
||||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||||
{ field: 'nick_name', align: 'left', title: '名称' },
|
{ field: 'nick_name', align: 'left', title: '名称', ...(formType === 'doctor' ? { slots: { default: 'nick_name' } } : {}) },
|
||||||
{
|
{
|
||||||
field: 'avatar',
|
field: 'avatar',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
end_time: dayjs(values.end_time || '20:00', 'HH:mm'),
|
end_time: dayjs(values.end_time || '20:00', 'HH:mm'),
|
||||||
// 确保订阅状态有值,如果后端没有返回则默认为0(订阅)
|
// 确保订阅状态有值,如果后端没有返回则默认为0(订阅)
|
||||||
subscribe_price_change: values.subscribe_price_change ?? 0,
|
subscribe_price_change: values.subscribe_price_change ?? 0,
|
||||||
|
sync_admin_phone: false,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
formApi.setValues({
|
formApi.setValues({
|
||||||
|
|||||||
@@ -154,6 +154,23 @@ export const modalFormProps: VbenFormProps = {
|
|||||||
label: '联系人手机号',
|
label: '联系人手机号',
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// 编辑时可选:同步修改药店管理员后台登录手机号
|
||||||
|
component: 'Switch',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
fieldName: 'sync_admin_phone',
|
||||||
|
label: '同步管理员登录号',
|
||||||
|
defaultValue: false,
|
||||||
|
componentProps: {
|
||||||
|
checkedChildren: '是',
|
||||||
|
unCheckedChildren: '否',
|
||||||
|
},
|
||||||
|
help: '勾选后,保存时将同步修改药店管理员后台登录手机号(需已开通后台)',
|
||||||
|
dependencies: {
|
||||||
|
show: (values) => values.id != null,
|
||||||
|
triggerFields: ['id'],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: 'PromoterPicker',
|
component: 'PromoterPicker',
|
||||||
formItemClass: 'col-span-6',
|
formItemClass: 'col-span-6',
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ export async function bindOnlineConsultationApi(data: {
|
|||||||
}) {
|
}) {
|
||||||
return requestClient.post<any>(`${prefix}bind-online-consultation`, data);
|
return requestClient.post<any>(`${prefix}bind-online-consultation`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定特色方默认挂号医生
|
||||||
|
*/
|
||||||
|
export async function updateSpecialPrescriptionDoctorApi(data: {
|
||||||
|
id: number;
|
||||||
|
special_prescription_doctor_id?: number | null;
|
||||||
|
}) {
|
||||||
|
return requestClient.post<any>(`${prefix}update-special-prescription-doctor`, data);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 分页查询用户列表
|
* 分页查询用户列表
|
||||||
* @param data
|
* @param data
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Form, FormItem, message, Select } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getDoctorOptionApi, updateSpecialPrescriptionDoctorApi } from '../api';
|
||||||
|
|
||||||
|
const formState = ref({
|
||||||
|
id: undefined as number | undefined,
|
||||||
|
special_prescription_doctor_id: undefined as number | undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const doctorOptions = ref<Array<{ id: number; name: string }>>([]);
|
||||||
|
const gridApiRef = ref<any>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
async function loadDoctorOptions(storeId?: number) {
|
||||||
|
if (!storeId) {
|
||||||
|
doctorOptions.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const doctors = await getDoctorOptionApi(storeId);
|
||||||
|
doctorOptions.value = doctors || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取医生列表失败:', error);
|
||||||
|
doctorOptions.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
loading.value = true;
|
||||||
|
modalApi.setState({ confirmLoading: true });
|
||||||
|
try {
|
||||||
|
await updateSpecialPrescriptionDoctorApi({
|
||||||
|
id: formState.value.id!,
|
||||||
|
special_prescription_doctor_id: formState.value.special_prescription_doctor_id || null,
|
||||||
|
});
|
||||||
|
message.success('绑定成功');
|
||||||
|
gridApiRef.value?.reload();
|
||||||
|
modalApi.close();
|
||||||
|
} catch {
|
||||||
|
message.error('绑定失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
modalApi.setState({ confirmLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
const { values, gridApi } = modalApi.getData<Record<string, any>>();
|
||||||
|
gridApiRef.value = gridApi;
|
||||||
|
formState.value = {
|
||||||
|
id: values?.id,
|
||||||
|
special_prescription_doctor_id: values?.special_prescription_doctor_id || undefined,
|
||||||
|
};
|
||||||
|
await loadDoctorOptions(formState.value.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="绑定特色方默认医生" class="w-[500px]">
|
||||||
|
<Form :model="formState" layout="vertical">
|
||||||
|
<FormItem label="特色方默认挂号医生">
|
||||||
|
<Select
|
||||||
|
v-model:value="formState.special_prescription_doctor_id"
|
||||||
|
:options="doctorOptions"
|
||||||
|
:field-names="{ label: 'name', value: 'id' }"
|
||||||
|
placeholder="请选择特色方默认挂号医生"
|
||||||
|
allow-clear
|
||||||
|
show-search
|
||||||
|
:filter-option="(input: string, option: any) => option.name.toLowerCase().includes(input.toLowerCase())"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -56,6 +56,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
end_time: dayjs(values.end_time || '20:00', 'HH:mm'),
|
end_time: dayjs(values.end_time || '20:00', 'HH:mm'),
|
||||||
// 确保订阅状态有值,如果后端没有返回则默认为0(订阅)
|
// 确保订阅状态有值,如果后端没有返回则默认为0(订阅)
|
||||||
subscribe_price_change: values.subscribe_price_change ?? 0,
|
subscribe_price_change: values.subscribe_price_change ?? 0,
|
||||||
|
sync_admin_phone: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,6 +179,23 @@ export const modalFormProps: VbenFormProps = {
|
|||||||
label: '联系人手机号',
|
label: '联系人手机号',
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// 编辑时可选:同步修改诊所管理员后台登录手机号
|
||||||
|
component: 'Switch',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
fieldName: 'sync_admin_phone',
|
||||||
|
label: '同步管理员登录号',
|
||||||
|
defaultValue: false,
|
||||||
|
componentProps: {
|
||||||
|
checkedChildren: '是',
|
||||||
|
unCheckedChildren: '否',
|
||||||
|
},
|
||||||
|
help: '勾选后,保存时将同步修改诊所管理员后台登录手机号(需已开通后台)',
|
||||||
|
dependencies: {
|
||||||
|
show: (values) => values.id != null,
|
||||||
|
triggerFields: ['id'],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: 'PromoterPicker',
|
component: 'PromoterPicker',
|
||||||
formItemClass: 'col-span-6',
|
formItemClass: 'col-span-6',
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ export const gridOptions: VxeGridProps<RowType> = {
|
|||||||
width: 160,
|
width: 160,
|
||||||
slots: { default: 'online_consultation_config' },
|
slots: { default: 'online_consultation_config' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'special_prescription_config',
|
||||||
|
align: 'left',
|
||||||
|
title: '特色方配置',
|
||||||
|
width: 140,
|
||||||
|
slots: { default: 'special_prescription_config' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'store_config_1',
|
field: 'store_config_1',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import StoreBasicInfoCell from './components/cells/StoreBasicInfoCell.vue';
|
|||||||
import StoreConfigTogglesCell from './components/cells/StoreConfigTogglesCell.vue';
|
import StoreConfigTogglesCell from './components/cells/StoreConfigTogglesCell.vue';
|
||||||
import DrugPriceModal from './components/DrugPriceModal.vue';
|
import DrugPriceModal from './components/DrugPriceModal.vue';
|
||||||
import BindConsultationModal from './components/BindConsultationModal.vue';
|
import BindConsultationModal from './components/BindConsultationModal.vue';
|
||||||
|
import BindSpecialPrescriptionDoctorModal from './components/BindSpecialPrescriptionDoctorModal.vue';
|
||||||
import StoreExternalFieldModal from './components/StoreExternalFieldModal.vue';
|
import StoreExternalFieldModal from './components/StoreExternalFieldModal.vue';
|
||||||
import SalespersonCommissionDrawer from './components/SalespersonCommissionDrawer.vue';
|
import SalespersonCommissionDrawer from './components/SalespersonCommissionDrawer.vue';
|
||||||
import BankCardReportModal from '#/views/system/store-bank-card/components/BankCardReportModal.vue';
|
import BankCardReportModal from '#/views/system/store-bank-card/components/BankCardReportModal.vue';
|
||||||
@@ -102,6 +103,11 @@ const [BindConsultationModalComponent, bindConsultationModalApi] = useVbenModal(
|
|||||||
connectedComponent: BindConsultationModal,
|
connectedComponent: BindConsultationModal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [BindSpecialPrescriptionDoctorModalComponent, bindSpecialPrescriptionDoctorModalApi] =
|
||||||
|
useVbenModal({
|
||||||
|
connectedComponent: BindSpecialPrescriptionDoctorModal,
|
||||||
|
});
|
||||||
|
|
||||||
const [StoreExternalFieldModalComponent, storeExternalFieldModalApi] = useVbenModal({
|
const [StoreExternalFieldModalComponent, storeExternalFieldModalApi] = useVbenModal({
|
||||||
connectedComponent: StoreExternalFieldModal,
|
connectedComponent: StoreExternalFieldModal,
|
||||||
});
|
});
|
||||||
@@ -216,6 +222,14 @@ const showBindConsultationModal = (row: any) => {
|
|||||||
bindConsultationModalApi.open();
|
bindConsultationModalApi.open();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showBindSpecialPrescriptionDoctorModal = (row: any) => {
|
||||||
|
bindSpecialPrescriptionDoctorModalApi.setData({
|
||||||
|
values: row,
|
||||||
|
gridApi,
|
||||||
|
});
|
||||||
|
bindSpecialPrescriptionDoctorModalApi.open();
|
||||||
|
};
|
||||||
|
|
||||||
const deleteApi = (row: any) => {
|
const deleteApi = (row: any) => {
|
||||||
let ids = [];
|
let ids = [];
|
||||||
if (row) {
|
if (row) {
|
||||||
@@ -369,6 +383,7 @@ const handleSwitchClinicType = (row: any) => {
|
|||||||
<QrCodePreviewModal />
|
<QrCodePreviewModal />
|
||||||
<DrugPriceModalComponent />
|
<DrugPriceModalComponent />
|
||||||
<BindConsultationModalComponent />
|
<BindConsultationModalComponent />
|
||||||
|
<BindSpecialPrescriptionDoctorModalComponent />
|
||||||
<StoreExternalFieldModalComponent />
|
<StoreExternalFieldModalComponent />
|
||||||
<SalespersonCommissionDrawer ref="salespersonDrawerRef" />
|
<SalespersonCommissionDrawer ref="salespersonDrawerRef" />
|
||||||
<Grid>
|
<Grid>
|
||||||
@@ -512,6 +527,23 @@ const handleSwitchClinicType = (row: any) => {
|
|||||||
绑定
|
绑定
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
|
<template #special_prescription_config="{ row }">
|
||||||
|
<div
|
||||||
|
v-if="row.special_prescription_doctor_id"
|
||||||
|
class="cursor-pointer hover:text-blue-500"
|
||||||
|
@click="showBindSpecialPrescriptionDoctorModal(row)"
|
||||||
|
>
|
||||||
|
<div>医生ID:{{ row.special_prescription_doctor_id }}</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
v-else
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="showBindSpecialPrescriptionDoctorModal(row)"
|
||||||
|
>
|
||||||
|
绑定
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
<template #start-time="{ row }">
|
<template #start-time="{ row }">
|
||||||
早:<Tag color="success">{{ row.start_time }}</Tag>
|
早:<Tag color="success">{{ row.start_time }}</Tag>
|
||||||
<br />
|
<br />
|
||||||
@@ -595,6 +627,14 @@ const handleSwitchClinicType = (row: any) => {
|
|||||||
ifShow: isPlatformAdmin,
|
ifShow: isPlatformAdmin,
|
||||||
onClick: showDrugPriceModal.bind(null, row),
|
onClick: showDrugPriceModal.bind(null, row),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '绑定特色方医生',
|
||||||
|
type: 'link',
|
||||||
|
icon: 'ant-design:user-add-outlined',
|
||||||
|
size: 'small',
|
||||||
|
auth: ['Super Admin', 'Admin'],
|
||||||
|
onClick: () => showBindSpecialPrescriptionDoctorModal(row),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '查看卡状态',
|
label: '查看卡状态',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
|
|||||||
@@ -3,16 +3,21 @@ import { onMounted, ref } from 'vue';
|
|||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { Button, Card, Input, InputNumber, message, Radio, Space, Switch, Tag } from 'ant-design-vue';
|
import { Button, Card, Input, InputNumber, message, Radio, Space, Switch, Tabs, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
import type { QuickDiscountOption } from '#/utils/pricePercentAdjust';
|
import type { QuickDiscountOption } from '#/utils/pricePercentAdjust';
|
||||||
|
|
||||||
|
import FormAvatar from '#/components/form/components/avatar.vue';
|
||||||
|
|
||||||
import { getSystemConfigList, saveSystemConfig } from './api';
|
import { getSystemConfigList, saveSystemConfig } from './api';
|
||||||
|
|
||||||
defineOptions({ name: 'SystemConfig' });
|
defineOptions({ name: 'SystemConfig' });
|
||||||
|
|
||||||
|
const TAB_STORAGE_KEY = 'system_config_active_tab';
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
|
const activeKey = ref('price_adjust');
|
||||||
const scope = ref<'both' | 'sale_only'>('sale_only');
|
const scope = ref<'both' | 'sale_only'>('sale_only');
|
||||||
const quickOptions = ref<QuickDiscountOption[]>([
|
const quickOptions = ref<QuickDiscountOption[]>([
|
||||||
{ name: '九五折', value: 95 },
|
{ name: '九五折', value: 95 },
|
||||||
@@ -25,6 +30,19 @@ const quickOptions = ref<QuickDiscountOption[]>([
|
|||||||
const newQuickName = ref('');
|
const newQuickName = ref('');
|
||||||
const newQuickValue = ref<number | null>(null);
|
const newQuickValue = ref<number | null>(null);
|
||||||
const inputAutoAuditPass = ref(true);
|
const inputAutoAuditPass = ref(true);
|
||||||
|
const miniprogramEmptyImage = ref('');
|
||||||
|
|
||||||
|
function readStoredTab() {
|
||||||
|
const stored = localStorage.getItem(TAB_STORAGE_KEY);
|
||||||
|
if (stored === 'price_adjust' || stored === 'input_audit' || stored === 'miniprogram') {
|
||||||
|
activeKey.value = stored;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTabChange(key: string | number) {
|
||||||
|
activeKey.value = String(key);
|
||||||
|
localStorage.setItem(TAB_STORAGE_KEY, String(key));
|
||||||
|
}
|
||||||
|
|
||||||
function parseQuickOptions(raw: unknown): QuickDiscountOption[] {
|
function parseQuickOptions(raw: unknown): QuickDiscountOption[] {
|
||||||
if (typeof raw === 'string') {
|
if (typeof raw === 'string') {
|
||||||
@@ -63,6 +81,9 @@ async function load() {
|
|||||||
if (row.config_key === 'input_auto_audit_pass') {
|
if (row.config_key === 'input_auto_audit_pass') {
|
||||||
inputAutoAuditPass.value = row.config_value === '1' || row.config_value === true || row.config_value === 'true';
|
inputAutoAuditPass.value = row.config_value === '1' || row.config_value === true || row.config_value === 'true';
|
||||||
}
|
}
|
||||||
|
if (row.config_key === 'miniprogram_empty_image') {
|
||||||
|
miniprogramEmptyImage.value = String(row.config_value || '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@@ -100,6 +121,10 @@ async function handleSave() {
|
|||||||
config_key: 'input_auto_audit_pass',
|
config_key: 'input_auto_audit_pass',
|
||||||
config_value: inputAutoAuditPass.value ? '1' : '0',
|
config_value: inputAutoAuditPass.value ? '1' : '0',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
config_key: 'miniprogram_empty_image',
|
||||||
|
config_value: miniprogramEmptyImage.value,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
message.success('保存成功');
|
message.success('保存成功');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -107,46 +132,64 @@ async function handleSave() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(load);
|
onMounted(() => {
|
||||||
|
readStoredTab();
|
||||||
|
load();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page auto-content-height title="系统配置">
|
<Page auto-content-height title="系统配置">
|
||||||
<Card :loading="loading" title="订单调价配置">
|
<Card :loading="loading">
|
||||||
<div class="mb-6">
|
<Tabs :active-key="activeKey" @change="handleTabChange">
|
||||||
<div class="mb-2 font-medium">订单打折时价格浮动范围</div>
|
<Tabs.TabPane key="price_adjust" tab="订单调价">
|
||||||
<Radio.Group v-model:value="scope">
|
<div class="py-4">
|
||||||
<Radio value="sale_only">仅售价浮动</Radio>
|
<div class="mb-6">
|
||||||
<Radio value="both">供货价与售价一起浮动</Radio>
|
<div class="mb-2 font-medium">订单打折时价格浮动范围</div>
|
||||||
</Radio.Group>
|
<Radio.Group v-model:value="scope">
|
||||||
</div>
|
<Radio value="sale_only">仅售价浮动</Radio>
|
||||||
|
<Radio value="both">供货价与售价一起浮动</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="mb-2 font-medium">调价快捷选项(name 展示名,value 比例:100=原价,90=九折,120=涨20%)</div>
|
<div class="mb-2 font-medium">调价快捷选项(name 展示名,value 比例:100=原价,90=九折,120=涨20%)</div>
|
||||||
<Space wrap class="mb-3">
|
<Space wrap class="mb-3">
|
||||||
<Tag
|
<Tag
|
||||||
v-for="item in quickOptions"
|
v-for="item in quickOptions"
|
||||||
:key="item.name + item.value"
|
:key="item.name + item.value"
|
||||||
closable
|
closable
|
||||||
:color="item.value < 100 ? 'processing' : 'warning'"
|
:color="item.value < 100 ? 'processing' : 'warning'"
|
||||||
@close="removeQuick(item)"
|
@close="removeQuick(item)"
|
||||||
>
|
>
|
||||||
{{ item.name }}({{ item.value }}%)
|
{{ item.name }}({{ item.value }}%)
|
||||||
</Tag>
|
</Tag>
|
||||||
</Space>
|
</Space>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Input v-model:value="newQuickName" placeholder="名称,如九折" style="width: 140px" />
|
<Input v-model:value="newQuickName" placeholder="名称,如九折" style="width: 140px" />
|
||||||
<InputNumber v-model:value="newQuickValue" :min="1" :max="500" placeholder="比例值" />
|
<InputNumber v-model:value="newQuickValue" :min="1" :max="500" placeholder="比例值" />
|
||||||
<Button @click="addQuick">添加快捷项</Button>
|
<Button @click="addQuick">添加快捷项</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
<Button type="primary" :loading="saving" @click="handleSave">保存配置</Button>
|
<Tabs.TabPane key="input_audit" tab="录入审核">
|
||||||
</Card>
|
<div class="py-4">
|
||||||
|
<div class="mb-2 font-medium">诊所/医生信息预填录入后是否自动通过审核</div>
|
||||||
|
<Switch v-model:checked="inputAutoAuditPass" checked-children="是" un-checked-children="否" />
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<Tabs.TabPane key="miniprogram" tab="小程序">
|
||||||
|
<div class="py-4">
|
||||||
|
<div class="mb-2 font-medium">空状态图片</div>
|
||||||
|
<div class="mb-2 text-sm text-gray-500">用于小程序列表等无数据时的占位图</div>
|
||||||
|
<FormAvatar v-model:value="miniprogramEmptyImage" />
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
<Card :loading="loading" class="mt-4" title="录入审核配置">
|
|
||||||
<div class="mb-2 font-medium">诊所/医生信息预填录入后是否自动通过审核</div>
|
|
||||||
<Switch v-model:checked="inputAutoAuditPass" checked-children="是" un-checked-children="否" />
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<Button type="primary" :loading="saving" @click="handleSave">保存配置</Button>
|
<Button type="primary" :loading="saving" @click="handleSave">保存配置</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user