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

@@ -152,82 +152,3 @@ alwaysApply: true
- 密码/敏感字段用 `VbenInputPassword` 组件,禁止用 `VbenInput` 配 `type="password"`
- 单选/多选选项来自接口时,必须在弹窗/页面 `onMounted` 或 `onOpenChange` 里拉取后通过 `updateSchema` 注入,禁止写死常量
# 专项模块规范
## OA 通知模块前端规则
### 组件与封装复用
- 所有 OA 配置入口(系统配置 tab、CRUD 页面、员工 OA 账号弹窗)必须复用 `Tabs.TabPane`、`useVbenVxeGrid`、`useVbenModal`、`useVbenForm`,禁止用原生 antd 表格/表单
### 平台动态化(禁止写死)
- **平台列表必须从接口 `oa-platform/list` 动态拉取**,禁止在前端写死 `['work_wechat', 'dingtalk', 'feishu']` 常量数组
- 系统配置 tab 中的平台开关行、机器人表单中的平台下拉、场景表单中的机器人分组,都要走接口动态渲染
- 平台编码统一为字符串 snake_case如 `work_wechat`、`dingtalk`、`feishu`),与后端 `xk_oa_platform.platform_code` 字段对齐
### 系统配置 tab 交互
- 系统配置 tab 中的「总开关关闭」必须禁用下方所有平台子开关(视觉上置灰),禁止总开关关闭时仍能切换子开关
- 平台子开关切换必须立即调 `oa-platform/update-enabled` 接口,**禁止依赖保存按钮批量提交**(避免总开关与子开关状态不一致)
- 系统配置页的总开关走现有 `getSystemConfigList` / `saveSystemConfig` 接口config_key = `oa_notify_enabled`**禁止新建专用接口**
### 表单字段约定
- @ 人配置必须按平台分组(用 Tabs 或 Collapse 分组),员工必须从 `xk_admin` 表选择(复用员工 picker 组件)
- OA 机器人表单中的 webhook、secret 字段必须用密码型输入框(`InputPassword`),禁止明文展示
- OA 场景表单中的机器人多选必须按平台分组展示(用 `Tabs` 或 `Collapse` 分组)
### 消息类型全量支持(数据库驱动 schema
- 场景弹窗每个平台 Tab 内的消息类型选项**必须**通过 `GET /oa-scene/message-types` 接口动态拉取,
按 `platform_code` 过滤当前平台支持的所有 `message_type`
- 每个消息类型对应的 payload 表单**必须**按后端返回的 `payload_schema`JSON 字符串)动态渲染,
禁止前端写死 schema数据库可启停类型前端硬编码会不同步
- payload 字段渲染规则(与后端 `payload_schema` 约定):
- `Textarea`:多行文本
- `Input` / `InputNumber`:单行文本/数字
- `RadioGroup` / `Select`:选项组
- `GalleryPickLink`:素材库选择器,透传 `props.acceptTypes: number[]` 过滤文件类型
- `TemplateCardEditor`:企微模板卡片专用编辑器(用 `views/system/oa-scene/components/template-card-form.vue`
- 提交结构:`message_config: { platform_code: { message_type, payload } }`
### image-gallery-picker 文件类型过滤
- `image-gallery-picker.vue` 和 `gallery-pick-link.vue` 都支持 `acceptTypes?: number[]` prop
- 取值对应 `xk_file_type.value`1=图片、2=视频、3=音频、6=文件 等
- 不传 `acceptTypes` 时显示全部类型(默认行为,保持向后兼容)
- 传数组时只展示匹配类型 tab且自动锁定到第一个匹配类型
- OA 场景中图片字段必须传 `[1]`、语音字段传 `[3]`、文件字段传 `[6]`,避免用户选错类型
### 通用拖拽可视化编辑器(`components/oa-template-card-editor/`
- 任何「左侧组件库 → 右侧画布拖拽组装 → 自动生成 JSON」的需求**必须复用此通用组件**,禁止重复造轮子
- schema 驱动:新增块类型只需扩展 `BlockSchema[]` 配置,禁止改组件内部
- SortableJS 用法参照 `views/system/role/components/quick-nav-transfer.vue`(动态 import `sortablejs/modular/sortable.complete.esm.js`
- 双向绑定走 `v-model``modelValue` + `update:modelValue`),禁止内部直接修改 props
- OA 场景中的企微模板卡片必须用 `views/system/oa-scene/components/template-card-form.vue`(在通用编辑器之上做 OA 封装)
### 测试发送(全类型循环)
- OA 机器人 modal 的「测试发送」必须循环跑该平台支持的所有消息类型(单平台最多 8 种)
- 后端返回 `results` 数组,前端必须遍历分类型展示成功/失败(不要只展示第一条)
- 测试按钮 loading 用独立状态(不要复用 modal 的 `confirmLoading`
- 测试发送结果以列表形式展示在表单下方,每种类型显示状态/耗时/错误信息
- 测试发送支持两套入口(共用 `testSendOaRobot` API
- **表单弹窗内测试**modal.vue用于「保存前验证密钥」直接传 webhook_url/secret不带 id
- **行级测试**(独立组件 `test-send-modal.vue`):用于列表中已有机器人,仅传 `id`,后端自动取已加密密钥
- 行级测试弹窗入参:`test_content` + `test_at_all` 开关 + `test_mobiles` 手机号多选
- 飞书 webhook 不支持手机号 @,前端必须在飞书平台下隐藏手机号字段并提示用户(仅 @all 有效)
### 群聊绑定(按平台过滤的 N:N 多选)
- 「OA群聊管理」是独立菜单pid=232 OA 通知父级下,菜单 id=233路由 `/system/oa-chat`
- 群聊管理页结构必须与 `oa-robot` 一致:`index.vue + api/index.ts + config/{table,search,form}.ts + components/modal.vue`
- 机器人表单中的「绑定群聊」字段必须用 `Checkbox.Group`
- 群聊列表通过 `getOaChatListByPlatform()` 一次性拉取全量(按平台分组的对象)
- 前端按当前 `platform_code` 过滤展示(平台切换时自动过滤掉不属于当前平台的勾选)
- 平台切换通过 `componentProps.onChange` 监听vben form 字段值变化无现成 watch必须通过 schema 注入 onChange 回调)
- 编辑场景下,机器人详情接口返回 `chat_ids: number[]`,前端 `selectedChatIds` 直接回显
- 提交时把 `chat_ids` 数组合并到 payload后端 `OaRobotController::notRequest` 已包含该字段)
- 列表展示用后端拼接好的 `chat_names` 字符串(按「、」拼接),详情接口才返回 `chat_ids` 数组

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>