医生药房批量迁移

This commit is contained in:
zoujiandong
2026-09-17 08:57:53 +08:00
parent 89d191a22d
commit c4ffba3cd8
3 changed files with 530 additions and 21 deletions
+25
View File
@@ -133,3 +133,28 @@ export function exportPharmacyDoctor(data) {
data,
});
}
/**
* 获取药房下线受影响医生列表(预检接口)
* @param {String|Number} pharmacy_id - 药房 ID
* @returns {Promise}
*/
export function getAffectedDoctors(pharmacy_id) {
return request({
url: `/admin/pharmacy/affected-doctors/${pharmacy_id}`,
method: 'get',
});
}
/**
* 批量迁移默认药房并执行下线操作
* @param {Object} data - { source_pharmacy_id, action: 'disable'|'delete', unbind_source: 1|0, transfers: Array<{ doctor_id, target_pharmacy_id }> }
* @returns {Promise}
*/
export function batchTransferAndAction(data) {
return request({
url: '/admin/pharmacy/batch-transfer-and-action',
method: 'post',
data,
});
}
@@ -0,0 +1,407 @@
<template>
<a-drawer
v-model:visible="visible"
:title="drawerTitle"
:width="920"
:footer="false"
:mask-closable="false"
@cancel="handleClose"
>
<!-- 顶部警告提示 -->
<a-alert type="warning" style="margin-bottom: 16px">
<template #icon><icon-exclamation-circle-fill /></template>
当前【<strong>{{ sourcePharmacy?.pharmacy_name }}</strong>】共有
<span style="color: #f53f3f; font-weight: bold">{{ affectedDoctorCount }}</span>
位医生的首选默认药房受到影响。
为保障处方业务正常运转,请为他们指定新的默认药房;确认后系统将自动完成迁移并执行<strong>{{ actionText }}</strong>。
</a-alert>
<!-- 批量快捷配置工具栏 -->
<div class="batch-toolbar">
<a-space>
<span class="batch-label">批量操作:</span>
<a-select
v-model="batchTargetPharmacyId"
placeholder="选择统一替代药房"
allow-search
style="width: 220px"
size="small"
>
<a-option
v-for="item in availablePharmacies"
:key="item.pharmacy_id"
:value="String(item.pharmacy_id)"
:label="item.pharmacy_name"
>
{{ item.pharmacy_name }}
</a-option>
</a-select>
<a-button
type="primary"
size="small"
:disabled="selectedRowKeys.length === 0 || !batchTargetPharmacyId"
@click="handleApplyBatch"
>
应用到已选医生 ({{ selectedRowKeys.length }})
</a-button>
<a-button
type="outline"
size="small"
:disabled="!batchTargetPharmacyId"
@click="handleApplyAll"
>
应用到全部医生 ({{ doctorList.length }})
</a-button>
</a-space>
</div>
<!-- 医生分流配置表格 -->
<a-table
:columns="columns"
:data="doctorList"
:loading="loading"
:pagination="false"
row-key="doctor_id"
:row-selection="rowSelection"
v-model:selected-keys="selectedRowKeys"
:scroll="{ x: 800, y: 460 }"
style="margin-top: 12px"
>
<!-- 医生信息 -->
<template #doctor="{ record }">
<div class="doctor-cell">
<div class="doctor-name">{{ record.user_name || '-' }}</div>
<a-tag size="mini" color="blue">{{ record.doctor_title_name || '医生' }}</a-tag>
<div class="doctor-phone">{{ record.mobile || '-' }}</div>
</div>
</template>
<!-- 医院与科室 -->
<template #hospital="{ record }">
<div>{{ record.hospital_name || '-' }}</div>
<div style="font-size: 12px; color: #86909c">{{ record.department_custom_name || '-' }}</div>
</template>
<!-- 已绑定的其他药房 (快捷点选) -->
<template #other_pharmacies="{ record }">
<div v-if="record.other_bound_pharmacies && record.other_bound_pharmacies.length > 0" class="tag-group">
<a-tooltip
v-for="item in record.other_bound_pharmacies"
:key="item.pharmacy_id"
content="点击直接选为新默认药房"
>
<a-tag
color="arcoblue"
size="small"
class="clickable-tag"
:class="{ 'tag-active': String(record.target_pharmacy_id) === String(item.pharmacy_id) }"
@click="handleQuickSelect(record, item.pharmacy_id)"
>
<template #icon><icon-swap /></template>
{{ item.pharmacy_name }}
</a-tag>
</a-tooltip>
</div>
<span v-else style="color: #c9cdd4; font-size: 13px">无其他绑定药房</span>
</template>
<!-- 新默认药房 (必选) -->
<template #target_pharmacy="{ record }">
<a-select
v-model="record.target_pharmacy_id"
placeholder="请指定新默认药房"
allow-search
size="small"
:error="!record.target_pharmacy_id"
style="width: 100%"
>
<a-option
v-for="item in availablePharmacies"
:key="item.pharmacy_id"
:value="String(item.pharmacy_id)"
:label="item.pharmacy_name"
>
{{ item.pharmacy_name }}
</a-option>
</a-select>
</template>
</a-table>
<!-- 抽屉底部动作栏 -->
<div class="drawer-footer">
<div class="footer-left">
<a-checkbox
v-model="unbindSource"
:disabled="actionType === 'delete'"
>
同时解除医生与当前原药房的绑定关系
</a-checkbox>
<span v-if="actionType === 'delete'" class="delete-tip">(删除药房将强制彻底解绑)</span>
</div>
<div class="footer-right">
<a-space>
<a-button @click="handleClose">取消</a-button>
<a-button
type="primary"
:status="actionType === 'delete' ? 'danger' : 'warning'"
:loading="submitLoading"
:disabled="!isAllConfigured"
@click="handleSubmit"
>
确认迁移并{{ actionText }}
<template v-if="!isAllConfigured">
({{ unconfiguredCount }}位未指定)
</template>
</a-button>
</a-space>
</div>
</div>
</a-drawer>
</template>
<script setup>
import { ref, reactive, computed } from 'vue';
import { Message } from '@arco-design/web-vue';
import { getPharmacyList, getAffectedDoctors, batchTransferAndAction } from '@/api/basic/pharmacy';
const emit = defineEmits(['success']);
const visible = ref(false);
const loading = ref(false);
const submitLoading = ref(false);
const sourcePharmacy = ref(null);
const actionType = ref('disable'); // 'disable' | 'delete'
const unbindSource = ref(true);
const doctorList = ref([]);
const selectedRowKeys = ref([]);
const batchTargetPharmacyId = ref('');
const pharmacyOptions = ref([]);
// 过滤掉当前药房自身的目标药房候选列表
const availablePharmacies = computed(() => {
const currentId = String(sourcePharmacy.value?.pharmacy_id || '');
return pharmacyOptions.value.filter((p) => String(p.pharmacy_id) !== currentId);
});
const actionText = computed(() => (actionType.value === 'delete' ? '删除药房' : '禁用药房'));
const drawerTitle = computed(() => `处理受影响医生默认药房(${actionText.value}前置迁移)`);
const affectedDoctorCount = computed(() => doctorList.value.length);
// 校验未指定新药房的医生数量
const unconfiguredCount = computed(() => {
return doctorList.value.filter((d) => !d.target_pharmacy_id).length;
});
const isAllConfigured = computed(() => {
return doctorList.value.length > 0 && unconfiguredCount.value === 0;
});
const rowSelection = reactive({
type: 'checkbox',
showCheckedAll: true,
});
const columns = [
{ title: '医生信息', slotName: 'doctor', width: 170 },
{ title: '执业机构/科室', slotName: 'hospital', width: 180 },
{ title: '医生已绑定其他药房(可快捷点选)', slotName: 'other_pharmacies', minWidth: 230 },
{ title: '新默认药房(必选)', slotName: 'target_pharmacy', width: 220 },
];
// 获取正常启用状态的药房列表
const fetchPharmacyOptions = async () => {
try {
const res = await getPharmacyList({ status: 1 });
if (res.code === 200 && res.data) {
pharmacyOptions.value = Array.isArray(res.data) ? res.data : (res.data.list || []);
}
} catch (err) {
console.error('获取替代药房列表失败:', err);
}
};
// 快捷点选已有药房
const handleQuickSelect = (record, pharmacyId) => {
record.target_pharmacy_id = String(pharmacyId);
};
// 批量应用到已选医生
const handleApplyBatch = () => {
if (!batchTargetPharmacyId.value) return;
doctorList.value.forEach((d) => {
if (selectedRowKeys.value.includes(d.doctor_id)) {
d.target_pharmacy_id = batchTargetPharmacyId.value;
}
});
Message.success(`已为选中的 ${selectedRowKeys.value.length} 位医生设置替代药房`);
};
// 批量应用到全部受影响医生
const handleApplyAll = () => {
if (!batchTargetPharmacyId.value) return;
doctorList.value.forEach((d) => {
d.target_pharmacy_id = batchTargetPharmacyId.value;
});
Message.success(`已为全部 ${doctorList.value.length} 位医生设置替代药房`);
};
// 初始化受影响医生列表(若只绑定了另外一家药房则智能预选)
const initDoctorList = (list) => {
doctorList.value = list.map((item) => ({
...item,
target_pharmacy_id: item.other_bound_pharmacies?.length === 1
? String(item.other_bound_pharmacies[0].pharmacy_id)
: '',
}));
};
// 打开抽屉主入口
const open = async ({ pharmacy, action = 'disable', affectedData = null }) => {
sourcePharmacy.value = pharmacy;
actionType.value = action;
unbindSource.value = true;
selectedRowKeys.value = [];
batchTargetPharmacyId.value = '';
visible.value = true;
fetchPharmacyOptions();
if (affectedData?.doctor_list) {
initDoctorList(affectedData.doctor_list);
} else {
loading.value = true;
try {
const res = await getAffectedDoctors(pharmacy.pharmacy_id);
if (res.code === 200 && res.data) {
initDoctorList(res.data.doctor_list || []);
} else {
Message.error(res.message || '获取受影响医生列表失败');
}
} catch (e) {
Message.error('获取受影响医生列表异常');
} finally {
loading.value = false;
}
}
};
const handleClose = () => {
visible.value = false;
};
// 提交迁移并执行下线
const handleSubmit = async () => {
if (!isAllConfigured.value) {
Message.warning(`尚有 ${unconfiguredCount.value} 位医生未指定新的默认药房`);
return;
}
const payload = {
source_pharmacy_id: String(sourcePharmacy.value.pharmacy_id),
action: actionType.value,
unbind_source: actionType.value === 'delete' ? 1 : (unbindSource.value ? 1 : 0),
transfers: doctorList.value.map((d) => ({
doctor_id: String(d.doctor_id),
target_pharmacy_id: String(d.target_pharmacy_id),
})),
};
submitLoading.value = true;
try {
const res = await batchTransferAndAction(payload);
if (res.code === 200) {
Message.success(`医生分流完成,药房已成功${actionText.value}`);
visible.value = false;
emit('success', { action: actionType.value, pharmacy_id: payload.source_pharmacy_id });
} else {
Message.error(res.message || '迁移下线操作失败');
}
} catch (err) {
Message.error('迁移下线请求异常');
} finally {
submitLoading.value = false;
}
};
defineExpose({
open,
});
</script>
<style lang="scss" scoped>
.batch-toolbar {
background-color: #f7f8fa;
padding: 10px 14px;
border-radius: 6px;
display: flex;
align-items: center;
.batch-label {
font-size: 13px;
font-weight: 500;
color: #4e5969;
}
}
.doctor-cell {
.doctor-name {
font-weight: 500;
color: #1d2129;
margin-bottom: 2px;
}
.doctor-phone {
font-size: 12px;
color: #86909c;
margin-top: 2px;
}
}
.tag-group {
display: flex;
flex-wrap: wrap;
gap: 6px;
.clickable-tag {
cursor: pointer;
transition: all 0.2s;
user-select: none;
&:hover {
opacity: 0.85;
transform: translateY(-1px);
}
}
.tag-active {
background-color: #165dff !important;
color: #fff !important;
font-weight: 500;
}
}
.drawer-footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background-color: #fff;
border-top: 1px solid #e5e6eb;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 10;
.footer-left {
display: flex;
align-items: center;
.delete-tip {
font-size: 12px;
color: #86909c;
margin-left: 4px;
}
}
}
</style>
+98 -21
View File
@@ -228,17 +228,16 @@
<template #icon><icon-user-group /></template>
关联医生
</a-button>
<!-- <a-popconfirm
<!-- <a-button
v-if="hasPermission('admin:sysPharmacy:remove')"
content="确定要删除此药房吗?删除后不可恢复!"
type="warning"
@ok="handleDelete(record)"
type="text"
status="danger"
size="small"
@click="handleDeleteClick(record)"
>
<a-button type="text" status="danger" size="small">
<template #icon><icon-delete /></template>
删除
</a-button>
</a-popconfirm> -->
<template #icon><icon-delete /></template>
删除
</a-button> -->
</a-space>
</template>
</a-table>
@@ -248,17 +247,26 @@
<!-- 关联医生列表抽屉 -->
<PharmacyDoctorDrawer ref="doctorDrawerRef" />
<!-- 医生分流迁移抽屉 -->
<PharmacyTransferDrawer ref="transferDrawerRef" @success="handleQuery" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { Message } from '@arco-design/web-vue';
import { Message, Modal } from '@arco-design/web-vue';
import { usePermissionStore } from '@/store/permission';
import { getPharmacyPage, updatePharmacyStatus, deletePharmacy } from '@/api/basic/pharmacy';
import {
getPharmacyPage,
updatePharmacyStatus,
deletePharmacy,
getAffectedDoctors,
} from '@/api/basic/pharmacy';
import { getAdminAreaList } from '@/api/basic/list';
import PharmacyModal from './components/pharmacyModal.vue';
import PharmacyDoctorDrawer from './components/pharmacyDoctorDrawer.vue';
import PharmacyTransferDrawer from './components/pharmacyTransferDrawer.vue';
const permissionStore = usePermissionStore();
@@ -271,6 +279,7 @@ const hasPermission = (permission) => {
const queryFormRef = ref(null);
const modalRef = ref(null);
const doctorDrawerRef = ref(null);
const transferDrawerRef = ref(null);
const tableLoading = ref(false);
const tableData = ref([]);
@@ -320,7 +329,7 @@ const columns = [
{ title: '地址', slotName: 'full_address', minWidth: 240 },
{ title: '状态', slotName: 'status', width: 100, align: 'center' },
{ title: '创建时间', dataIndex: 'created_at', width: 180 },
{ title: '操作', slotName: 'action', width: 380, fixed: 'right', align: 'center' },
{ title: '操作', slotName: 'action', width: 350, fixed: 'right', align: 'center' },
];
// 获取区域信息
@@ -464,32 +473,100 @@ const handleOpenDoctorDrawer = (record) => {
doctorDrawerRef.value?.open(record);
};
// 状态切换
// 状态切换(启用 / 禁用)
const handleStatusToggle = async (record, val) => {
const newStatus = val ? 1 : 0;
const originalStatus = record.status;
// 场景 A:若是"启用"操作,无需迁移,直接调用原有接口
if (newStatus === 1) {
record._statusLoading = true;
try {
const res = await updatePharmacyStatus(record.pharmacy_id, 1);
if (res.code === 200) {
record.status = 1;
Message.success('已启用该药房');
} else {
record.status = originalStatus;
Message.error(res.message || '启用药房失败');
}
} catch (e) {
record.status = originalStatus;
Message.error('切换状态异常');
} finally {
record._statusLoading = false;
}
return;
}
// 场景 B:若是"禁用"操作,进行前置受影响医生预检
record._statusLoading = true;
try {
const res = await updatePharmacyStatus(record.pharmacy_id, newStatus);
const preCheckRes = await getAffectedDoctors(record.pharmacy_id);
if (preCheckRes.code === 200 && preCheckRes.data?.default_doctor_count > 0) {
// 存在受影响默认医生:阻止直接禁用,回滚开关状态并唤起分流抽屉
record.status = originalStatus;
record._statusLoading = false;
transferDrawerRef.value?.open({
pharmacy: record,
action: 'disable',
affectedData: preCheckRes.data,
});
return;
}
// 无受影响医生:执行常规禁用
const res = await updatePharmacyStatus(record.pharmacy_id, 0);
if (res.code === 200) {
record.status = newStatus;
Message.success(newStatus === 1 ? '已启用该药房' : '已禁用该药房');
record.status = 0;
Message.success('已禁用该药房');
} else {
record.status = originalStatus;
Message.error(res.message || '修改状态失败');
Message.error(res.message || '禁用药房失败');
}
} catch (err) {
record.status = originalStatus;
Message.error('切换状态异常');
Message.error('禁用药房预检或处理异常');
} finally {
record._statusLoading = false;
}
};
// 删除
const handleDelete = async (record) => {
// 点击删除按钮逻辑(带预检)
const handleDeleteClick = async (record) => {
try {
const res = await deletePharmacy(record.pharmacy_id);
// 1. 调用预检接口判断受影响医生
const preCheckRes = await getAffectedDoctors(record.pharmacy_id);
if (preCheckRes.code === 200 && preCheckRes.data?.default_doctor_count > 0) {
// 存在受影响医生:打开分流抽屉,动作指定为 'delete'
transferDrawerRef.value?.open({
pharmacy: record,
action: 'delete',
affectedData: preCheckRes.data,
});
return;
}
// 2. 无受影响医生:走常规删除二次弹窗确认
Modal.warning({
title: '确认删除药房',
content: `确定要删除药房【${record.pharmacy_name || '-'}】吗?删除后不可恢复!`,
hideCancel: false,
okButtonProps: { status: 'danger' },
onOk: async () => {
await executeDeletePharmacy(record.pharmacy_id);
},
});
} catch (error) {
console.error('预检受影响医生异常:', error);
Message.error('预检受影响医生异常');
}
};
// 执行常规删除请求
const executeDeletePharmacy = async (pharmacyId) => {
try {
const res = await deletePharmacy(pharmacyId);
if (res.code === 200) {
Message.success('药房删除成功');
// 如果当前页只有一条且非第一页,删除后向前退一页