feat:诊所问题优化,气泡卡片优化,修复了导入药品的时候无法打开开方组件并且切换tab的问题

This commit is contained in:
李琦
2026-08-01 08:04:31 +08:00
parent 371594bfda
commit 3bc22d02e6
12 changed files with 241 additions and 142 deletions

View File

@@ -54,12 +54,15 @@ export namespace AuthApi {
* 登录
*/
export async function loginApi(data: AuthApi.LoginParams) {
// PC 管理端固定 web供单机登录按端区分会话
const clientType = 'web';
if (data.account) {
return requestClient.post<AuthApi.LoginResult>('login', {
account: data.account,
password: data.password,
code: data.code,
admin_id: data.admin_id,
client_type: clientType,
});
}
@@ -67,6 +70,7 @@ export async function loginApi(data: AuthApi.LoginParams) {
login_method: data.login_method || 'phone',
password: data.password,
code: data.code,
client_type: clientType,
};
if (data.login_method === 'phone') {

View File

@@ -12,7 +12,7 @@ import {
} from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue';
import { message, Modal } from 'ant-design-vue';
// eslint-disable-next-line n/no-extraneous-import
import { Base64 } from 'js-base64';
@@ -37,6 +37,8 @@ function createRequestClient(baseURL: string) {
/**
* 重新认证逻辑
*/
/** 防重复:并发 401 只弹一次重新登录提示 */
let authModalShowing = false;
async function doReAuthenticate() {
const accessStore = useAccessStore();
const authStore = useAuthStore();
@@ -47,6 +49,21 @@ function createRequestClient(baseURL: string) {
) {
accessStore.setLoginExpired(true);
} else {
// 被踢/登录失效时用模态框提示,确认后再退出
if (!authModalShowing) {
authModalShowing = true;
await new Promise<void>((resolve) => {
Modal.warning({
title: '登录失效',
content: '账号需重新登录',
okText: '重新登录',
centered: true,
onOk: () => resolve(),
onCancel: () => resolve(),
});
});
authModalShowing = false;
}
await authStore.logout(true);
}
}

View File

@@ -8,6 +8,7 @@ import type { Dayjs } from 'dayjs';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import dayjs from 'dayjs';
import {
@@ -35,9 +36,15 @@ import {
} from '#/views/system/store/api';
import { getSalespersonList } from '#/views/system/store/api/salesperson';
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
import AddDoctorFromStoreModal from '#/views/system/store/components/AddDoctorFromStoreModal.vue';
import { canAddStoreDoctor } from '#/views/system/admin/_shared/platform-admin-role';
const RangePicker = DatePicker.RangePicker;
const userStore = useUserStore();
// 是否允许「添加医生」:超管/管理员/省经理/市经理/业务员
const canAddDoctor = computed(() => canAddStoreDoctor(userStore.userInfo));
const storeId = ref(0);
const loading = ref(false);
const activeTab = ref('basic');
@@ -121,6 +128,25 @@ const [DoctorModal, DoctorModalApi] = useVbenModal({
connectedComponent: DoctorCardModal,
});
/** 添加医生弹窗:诊所详情医生团队 Tab 内入口 */
const [AddDoctorModal, AddDoctorModalApi] = useVbenModal({
connectedComponent: AddDoctorFromStoreModal,
});
/**
* 打开「添加医生」弹窗:默认绑定当前门店,提交成功后刷新医生团队列表
*/
const openAddDoctorModal = () => {
AddDoctorModalApi.setData({
storeId: storeId.value,
storeName: (store.value as any)?.name || '',
onSuccess: () => {
loadCard();
},
});
AddDoctorModalApi.open();
};
function searchTimeParam(): [string, string] {
return [
searchTime.value[0].format('YYYY-MM-DD 00:00:00'),
@@ -256,6 +282,7 @@ function clinicTypeText(v: number) {
<template>
<Modal :title="modalTitle">
<AddDoctorModal />
<Spin :spinning="loading">
<div v-if="cardData" class="mb-4 flex flex-wrap items-center justify-between gap-3 px-1">
<div class="text-sm text-muted-foreground">统计时间范围</div>
@@ -399,6 +426,15 @@ function clinicTypeText(v: number) {
<Tabs.TabPane v-if="isClinic" key="doctors" tab="诊所医生团队">
<div class="pl-4">
<div v-if="canAddDoctor" class="mb-3">
<Button
type="primary"
size="small"
@click="openAddDoctorModal"
>
添加医生
</Button>
</div>
<Empty v-if="doctorList.length === 0" description="暂无医生" />
<div v-else class="grid grid-cols-1 gap-3 md:grid-cols-2">
<div

View File

@@ -83,8 +83,6 @@ const prescriptionStore = usePrescriptionStore();
/** 处方类型来自后端下发store.categories */
const categories = computed(() => prescriptionStore.categories);
const rxSwipeStartX = ref(0);
const rxSwipeStartY = ref(0);
const allowInsuranceCategory = computed(
() => Number(prescriptionStore.registerStoreInfo?.allow_insurance_category ?? 0) === 1,
@@ -445,26 +443,6 @@ const tabChange = async (id: number) => {
}
};
/** 记录左右滑起点 */
const onRxPanelPointerDown = (e: PointerEvent) => {
rxSwipeStartX.value = e.clientX;
rxSwipeStartY.value = e.clientY;
};
/** 左右滑切换相邻处方类型 */
const onRxPanelPointerUp = (e: PointerEvent) => {
const dx = e.clientX - rxSwipeStartX.value;
const dy = e.clientY - rxSwipeStartY.value;
if (Math.abs(dx) < 80 || Math.abs(dx) <= Math.abs(dy)) return;
const list = categories.value || [];
if (list.length < 2) return;
const idx = list.findIndex((c) => Number(c.value) === Number(prescriptionStore.activeCategory));
if (idx < 0) return;
const nextIdx = dx < 0 ? idx + 1 : idx - 1;
if (nextIdx < 0 || nextIdx >= list.length) return;
tabChange(list[nextIdx].value);
};
/**
* 检测并显示转诊提示
* @description 当切换到中药处方时,如果当前诊所为西医诊所,显示转诊提示
@@ -1058,11 +1036,7 @@ const cancelSaveCommonPrescription = () => {
</button>
</div>
<div
class="rx-swipe-panel"
@pointerdown="onRxPanelPointerDown"
@pointerup="onRxPanelPointerUp"
>
<div class="rx-swipe-panel">
<!-- 费用类型选择和添加商品按钮 -->
<div class="mb-4 flex items-center justify-between">

View File

@@ -576,16 +576,15 @@ const handleSwitchClinicType = () => {
</p>
<p class="text-sm text-gray-500">
专业:
<span
:class="
doctor.identity === 1
doctor.doctor?.identity === 1
? 'bg-green-100 text-green-700'
: 'bg-gray-100 text-gray-700'
"
class="rounded px-2 py-1"
>
{{ doctor.identity === 1 ? '中医' : '西医' }}
{{ doctor.doctor?.identity === 1 ? '中医' : '西医' }}
</span>
</p>
</div>

View File

@@ -322,7 +322,21 @@ function parseRecipeContent(content: string | Record<string, unknown>) {
alt="签名"
style="width: 120px; height: 50px; display: inline-block"
/>
<span v-else>{{ item.content?.doctor?.name }}</span>
<!-- 二次签名:相冲继续开方时展示第二张医生签名 -->
<img
v-if="
item.doctor_second_sign == 1 &&
item.doctor_info?.identity_info?.sign_image
"
:src="
getImageSource(item.doctor_info.identity_info.sign_image)
"
alt="二次签名"
style="width: 120px; height: 50px; display: inline-block"
/>
<span v-else-if="!item.doctor_info?.identity_info?.sign_image">{{
item.content?.doctor?.name
}}</span>
</div>
<div class="signature">
<label>审核药师:</label>

View File

@@ -419,9 +419,6 @@ const categories = ref([
]);
/** 后端默认处方类型 */
const prescriptionTypeDefault = ref(2);
/** 内容区左右滑切换 Tab */
const rxSwipeStartX = ref(0);
const rxSwipeStartY = ref(0);
// 当前状态
const activePatient = ref<null | Patient>(null);
@@ -1488,25 +1485,6 @@ function tabChange(id) {
focusActiveCategoryTab();
}
/** 记录左右滑起点 */
function onRxPanelPointerDown(e: PointerEvent) {
rxSwipeStartX.value = e.clientX;
rxSwipeStartY.value = e.clientY;
}
/** 左右滑切换相邻处方类型 */
function onRxPanelPointerUp(e: PointerEvent) {
const dx = e.clientX - rxSwipeStartX.value;
const dy = e.clientY - rxSwipeStartY.value;
if (Math.abs(dx) < 80 || Math.abs(dx) <= Math.abs(dy)) return;
const list = categories.value || [];
if (list.length < 2) return;
const idx = list.findIndex((c) => Number(c.value) === Number(activeCategory.value));
if (idx < 0) return;
const nextIdx = dx < 0 ? idx + 1 : idx - 1;
if (nextIdx < 0 || nextIdx >= list.length) return;
tabChange(list[nextIdx].value);
}
/**
* 检测并显示转诊提示
@@ -2860,11 +2838,7 @@ watch(
</button>
</div>
<div
class="rx-swipe-panel"
@pointerdown="onRxPanelPointerDown"
@pointerup="onRxPanelPointerUp"
>
<div class="rx-swipe-panel">
<SpecialPrescriptionImportCard
v-if="hasSpecialPrescriptionRecord"

View File

@@ -18,3 +18,13 @@ export function canEditDoctorServiceUser(userInfo: PlatformAdminUserInfo): boole
export function canManageStoreBankCard(userInfo: PlatformAdminUserInfo): boolean {
return isPlatformSuperAdmin(userInfo);
}
/**
* 是否可在诊所页/诊所详情发起「添加医生」操作
* 角色1 超级管理员 / 2 系统管理员 / 3 省经理 / 4 市经理 / 6 业务员
* 数据范围由后端 StoreService::assertCanViewStore 兜底校验
*/
export function canAddStoreDoctor(userInfo: PlatformAdminUserInfo): boolean {
const roleId = Number(userInfo?.role_id ?? userInfo?.roles?.id);
return [1, 2, 3, 4, 6].includes(roleId);
}

View File

@@ -0,0 +1,85 @@
<script lang="ts" setup>
/**
* 诊所「添加医生」弹窗
* 复用 doctor/doctor 的表单 schema 子集infoModalFormProps
* 诊所字段由外部传入并锁定显示,提交时强制写入选中 store_id
* 成功后通过传入的 onSuccess 回调刷新父级医生列表。
*/
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createDoctor } from '#/views/doctor/doctor/api';
import { infoModalFormProps } from '#/views/doctor/doctor/config/form';
// 当前门店信息(外部 setData 传入)
const storeId = ref(0);
const storeName = ref('');
// 提交成功后回调(由父组件注入,用于刷新医生团队列表)
const onSuccess = ref<(() => void) | null>(null);
const [Form, formApi] = useVbenForm(infoModalFormProps);
const [Modal, modalApi] = useVbenModal({
closeOnClickModal: false,
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
// 校验表单通过后再提交
formApi.validate().then(async (e: any) => {
if (!e.valid) return;
const values = await formApi.getValues();
// 强制写入当前诊所 id数组形式匹配后端 normalizeStoreIds
values.store_id = [storeId.value];
modalApi.setState({ loading: true, confirmLoading: true });
createDoctor(values)
.then(() => {
message.success('添加成功');
// 通知父组件刷新医生团队列表
onSuccess.value?.();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
});
await formApi.validateAndSubmitForm();
},
onOpenChange(isOpen: boolean) {
if (!isOpen) return;
const data = modalApi.getData<{
storeId: number;
storeName?: string;
onSuccess?: () => void;
}>() || { storeId: 0 };
storeId.value = Number(data.storeId || 0);
storeName.value = data.storeName || '';
onSuccess.value = typeof data.onSuccess === 'function' ? data.onSuccess : null;
if (storeId.value) {
// 1) 锁定诊所字段:禁用 + 默认值
formApi.setValues({ store_id: [storeId.value] });
formApi.updateSchema([
{
fieldName: 'store_id',
componentProps: {
disabled: true,
placeholder: `当前诊所:${storeName.value || storeId.value}`,
},
},
]);
}
},
});
</script>
<template>
<Modal class="w-[60%]" title="添加医生">
<Form />
</Modal>
</template>

View File

@@ -39,6 +39,7 @@ import BindConsultationModal from './components/BindConsultationModal.vue';
import BindSpecialPrescriptionDoctorModal from './components/BindSpecialPrescriptionDoctorModal.vue';
import StoreExternalFieldModal from './components/StoreExternalFieldModal.vue';
import SalespersonCommissionDrawer from './components/SalespersonCommissionDrawer.vue';
import AddDoctorFromStoreModal from './components/AddDoctorFromStoreModal.vue';
import StoreCardModal from '#/components/store-card/StoreCardModal.vue';
import BankCardReportModal from '#/views/system/store-bank-card/components/BankCardReportModal.vue';
import BankCardStatusModal from '#/views/system/store-bank-card/components/BankCardStatusModal.vue';
@@ -46,7 +47,10 @@ import BankCardStoreEditModal from '#/views/system/store-bank-card/components/Ba
import { getStoreBankCardReportDetail } from '#/views/system/store-bank-card/api';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
import { canManageStoreBankCard } from '#/views/system/admin/_shared/platform-admin-role';
import {
canAddStoreDoctor,
canManageStoreBankCard,
} from '#/views/system/admin/_shared/platform-admin-role';
const userStore = useUserStore();
const router = useRouter();
@@ -60,6 +64,9 @@ const canManageBankCard = computed(() =>
canManageStoreBankCard(userStore.userInfo),
);
// 是否允许「添加医生」:超管/管理员/省经理/市经理/业务员
const canAddDoctor = computed(() => canAddStoreDoctor(userStore.userInfo));
// 跳转到审核页面
const goToAudit = () => {
router.push('/system/store-input/audit');
@@ -94,6 +101,11 @@ const [StoreCardModalComp, StoreCardModalApi] = useVbenModal({
connectedComponent: StoreCardModal,
});
/** 添加医生弹窗:诊所列表行操作和详情医生团队共用 */
const [AddDoctorModalComp, addDoctorModalApi] = useVbenModal({
connectedComponent: AddDoctorFromStoreModal,
});
/** 打开门店详情卡片 */
function openStoreCard(row: Record<string, any>) {
if (!row?.id) return;
@@ -101,6 +113,22 @@ function openStoreCard(row: Record<string, any>) {
StoreCardModalApi.open();
}
/**
* 打开「添加医生」弹窗
* @param row 诊所行数据,需要 id 和 name
*/
const openAddDoctorModal = (row: any) => {
addDoctorModalApi.setData({
storeId: Number(row?.id || 0),
storeName: row?.name || '',
onSuccess: () => {
// 刷新诊所列表/详情卡(医生列表数据来自 store card
gridApi.query();
},
});
addDoctorModalApi.open();
};
const openQrCodeModal = (url, title, address) => {
QrCodePreviewApi.setData({
// 表单值
@@ -416,6 +444,7 @@ const handleSwitchClinicType = (row: any) => {
<BankCardStatusModalComponent />
<BankCardStoreEditModalComponent />
<StoreCardModalComp />
<AddDoctorModalComp />
<QrCodePreviewModal />
<DrugPriceModalComponent />
<BindConsultationModalComponent />
@@ -669,6 +698,14 @@ const handleSwitchClinicType = (row: any) => {
ifShow: isPlatformAdmin,
onClick: showDrugPriceModal.bind(null, row),
},
{
label: '添加医生',
type: 'link',
icon: 'ant-design:user-add-outlined',
size: 'small',
ifShow: canAddDoctor,
onClick: openAddDoctorModal.bind(null, row),
},
{
label: '绑定特色方医生',
type: 'link',

View File

@@ -38,6 +38,8 @@ const logisticsShowWhNameUser = ref(false);
const logisticsShowWhNameClinic = ref(true);
/** 业务员是否允许编辑/改价/配置所属诊所(默认否) */
const salespersonStoreEditEnabled = ref(false);
/** 业务端单机登录:医生/药师/诊所管理员等同一账号仅 1 手机 + 1 网页(默认否) */
const doctorSingleDeviceLogin = ref(false);
/** 开票通知渠道subscribe / sms默认仅订阅消息 */
const invoiceNoticeChannels = ref<string[]>(['subscribe']);
@@ -49,12 +51,13 @@ function readStoredTab() {
stored === 'miniprogram' ||
stored === 'logistics' ||
stored === 'salesperson' ||
stored === 'invoice_notice'
stored === 'invoice_notice' ||
stored === 'doctor_login' ||
stored === 'oa_notify'
) {
if (stored === 'price_adjust' || stored === 'input_audit' || stored === 'miniprogram' || stored === 'oa_notify') {
activeKey.value = stored;
}
}}
}
function parseBoolConfig(val: unknown, defaultVal: boolean): boolean {
if (val === undefined || val === null || val === '') return defaultVal;
@@ -115,6 +118,9 @@ async function load() {
if (row.config_key === 'salesperson_store_edit_enabled') {
salespersonStoreEditEnabled.value = parseBoolConfig(row.config_value, false);
}
if (row.config_key === 'doctor_single_device_login') {
doctorSingleDeviceLogin.value = parseBoolConfig(row.config_value, false);
}
if (row.config_key === 'invoice_notice_channels') {
let channels: unknown = row.config_value;
if (typeof channels === 'string') {
@@ -188,6 +194,14 @@ async function handleSave() {
description: '允许业务员编辑/改价/配置所属诊所',
sort: 300,
},
{
config_key: 'doctor_single_device_login',
config_value: doctorSingleDeviceLogin.value ? '1' : '0',
value_type: 'bool',
config_group: 'auth',
description: '业务端单机登录(医生/药师/诊所管理员等1手机+1网页',
sort: 310,
},
{
config_key: 'invoice_notice_channels',
config_value: JSON.stringify(
@@ -303,6 +317,20 @@ onMounted(() => {
</div>
</Tabs.TabPane>
<Tabs.TabPane key="doctor_login" tab="登录安全">
<div class="py-4">
<div class="mb-2 font-medium">单机登录</div>
<div class="mb-2 text-sm text-gray-500">
适用于医生/药师/诊所管理员/平台管理员/业务员及 PC 管理端关闭时不限制登录设备开启后同一账号仅允许 1 个手机端会话 + 1 PC 网页会话同端后登录会使先登录失效同手机号下不同身份互不影响
</div>
<Switch
v-model:checked="doctorSingleDeviceLogin"
checked-children="开启"
un-checked-children="关闭"
/>
</div>
</Tabs.TabPane>
<Tabs.TabPane key="invoice_notice" tab="开票通知">
<div class="py-4">
<div class="mb-2 font-medium">患者开票通知渠道</div>