feat:诊所问题优化,气泡卡片优化,修复了导入药品的时候无法打开开方组件并且切换tab的问题
This commit is contained in:
@@ -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` 数组
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user