fix: 常用方、患者管理

This commit is contained in:
2025-12-31 11:13:16 +08:00
parent ae364d4319
commit e0acff7faa
6 changed files with 297 additions and 63 deletions

View File

@@ -50,6 +50,13 @@ export async function saveNavUrlApi(data) {
export async function deleteNavApi(data) {
return requestClient.post<any>(`${prefix}delete-nav`, data);
}
/**
* 更新轮播图排序
* @param ids 按顺序排列的轮播图ID数组
*/
export async function updateNavSortApi(ids: number[]) {
return requestClient.post<any>(`${prefix}update-nav-sort`, { ids });
}
/*
----------------------------推广员---------------------------------

View File

@@ -1,17 +1,19 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { EllipsisText, Page, useVbenModal } from '@vben/common-ui';
import { DeleteOutlined } from '@ant-design/icons-vue';
import { DeleteOutlined, EyeOutlined, HolderOutlined } from '@ant-design/icons-vue';
import {
Avatar,
Button,
Card,
Image,
ImagePreviewGroup,
InputNumber,
message,
notification,
Popconfirm,
RadioButton,
RadioGroup,
Tag,
@@ -24,6 +26,7 @@ import {
deleteNavApi,
getStoreInfoApi,
saveZSalePercentApi,
updateNavSortApi,
} from '#/views/business/store/settings/api';
import UploadModal from './components/uploadModal.vue';
@@ -37,6 +40,7 @@ interface TabBarItem {
interface StoreInfo {
name: string;
type: number; // 0=诊所, 1=药店
star?: any;
title: { name: string };
contact: string;
@@ -82,24 +86,54 @@ const openDoctorQrCode = (doctorId: number, isNotQr = false) => {
DoctorQrCodePreviewModalApi.open();
};
// 映射数据
const activeTabBar = ref(1);
const tabBar: TabBarItem[] = [
{ value: 1, label: '诊所资料' },
{ value: 2, label: '商品价格' },
{ value: 3, label: '诊所二维码' },
{ value: 4, label: '我的医生团队' },
{ value: 5, label: '轮播图' },
{ value: 6, label: '推广员' },
];
// 诊所/药店数据
const data = ref<null | StoreInfo>(null);
// Tab状态持久化
const TAB_STORAGE_KEY = 'xk_store_settings_tab';
const savedTab = localStorage.getItem(TAB_STORAGE_KEY);
const activeTabBar = ref(savedTab ? Number(savedTab) : 1);
// 判断是否是药店 (type=1为药店, type=0为诊所)
const isPharmacy = computed(() => data.value?.type === 1);
// 根据类型动态生成tab选项
const tabBar = computed<TabBarItem[]>(() => {
if (isPharmacy.value) {
// 药店只显示:药店资料、药店二维码、轮播图、推广员
return [
{ value: 1, label: '药店资料' },
{ value: 3, label: '药店二维码' },
{ value: 5, label: '轮播图' },
{ value: 6, label: '推广员' },
];
}
// 诊所显示全部:诊所资料、商品价格、诊所二维码、我的医生团队、轮播图、推广员
return [
{ value: 1, label: '诊所资料' },
{ value: 2, label: '商品价格' },
{ value: 3, label: '诊所二维码' },
{ value: 4, label: '我的医生团队' },
{ value: 5, label: '轮播图' },
{ value: 6, label: '推广员' },
];
});
// 监听tabBar变化验证当前tab是否有效药店和诊所tab选项不同
watch(tabBar, (newTabBar) => {
const validValues = newTabBar.map(item => item.value);
if (!validValues.includes(activeTabBar.value)) {
// 如果当前tab不在可用列表中切换到第一个tab
activeTabBar.value = validValues[0] || 1;
localStorage.setItem(TAB_STORAGE_KEY, String(activeTabBar.value));
}
}, { immediate: true });
// 格式化日期
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString();
};
const data = ref<null | StoreInfo>(null);
/**
* 获取当前登录诊所信息
*/
@@ -108,12 +142,14 @@ const getMyInfo = () => {
data.value = res as StoreInfo;
});
};
getMyInfo();
/**
* 医嘱列表
* Tab切换处理
*/
const tabBarChange = () => {
// 保存当前tab到localStorage
localStorage.setItem(TAB_STORAGE_KEY, String(activeTabBar.value));
switch (activeTabBar.value) {
case 1: {
getMyInfo();
@@ -124,6 +160,10 @@ const tabBarChange = () => {
}
}
};
// 初始化时加载基础数据所有tab都需要data
onMounted(() => {
getMyInfo();
});
const qrCodeUrl = ref<HTMLElement | null>(null);
const htmlToImage = ref<HTMLElement | null>(null);
@@ -178,6 +218,83 @@ const deleteNavItem = (id: number) => {
});
}
};
// 拖拽排序相关
const draggedIndex = ref(-1);
const dragOverIndex = ref(-1);
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 (!data.value || draggedIndex.value === -1 || draggedIndex.value === targetIndex) {
draggedIndex.value = -1;
dragOverIndex.value = -1;
return;
}
const list = [...data.value.nav];
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);
data.value.nav = list;
draggedIndex.value = -1;
dragOverIndex.value = -1;
// 保存排序到后端
saveNavSort();
};
const onDragEnd = () => {
draggedIndex.value = -1;
dragOverIndex.value = -1;
};
// 保存轮播图排序
const saveNavSort = () => {
if (!data.value) return;
const ids = data.value.nav.map((item) => item.id);
updateNavSortApi(ids).then(() => {
notification.success({
message: '排序保存成功',
duration: 2,
});
});
};
// 图片预览相关
const previewVisible = ref(false);
const previewCurrent = ref(0);
const openPreview = (index: number) => {
previewCurrent.value = index;
previewVisible.value = true;
};
</script>
<template>
@@ -199,7 +316,7 @@ const deleteNavItem = (id: number) => {
</RadioButton>
</RadioGroup>
</div>
<Card v-if="activeTabBar === 1" title="诊所资料">
<Card v-if="activeTabBar === 1" :title="isPharmacy ? '药店资料' : '诊所资料'">
<!-- 头部信息 -->
<div class="mb-8 flex flex-col gap-6 md:flex-row md:gap-8">
<div class="flex-1">
@@ -307,7 +424,7 @@ const deleteNavItem = (id: number) => {
</InputNumber>
<Button type="primary" @click="saveZSalePercent">保存</Button>
</Card>
<Card v-else-if="activeTabBar === 3" title="诊所二维码">
<Card v-else-if="activeTabBar === 3" :title="isPharmacy ? '药店二维码' : '诊所二维码'">
<template #extra>
<Button
style="margin: 20px auto"
@@ -335,7 +452,7 @@ const deleteNavItem = (id: number) => {
width="30"
/>
<div class="qrAddress">
<span class="addressTitle">诊所地址</span>{{ data.province.name
<span class="addressTitle">{{ isPharmacy ? '药店地址:' : '诊所地址:' }}</span>{{ data.province.name
}}{{ data.city.name }}{{ data.position }}
</div>
</div>
@@ -444,39 +561,111 @@ const deleteNavItem = (id: number) => {
</Card>
<Card v-else-if="activeTabBar === 5" title="轮播图">
<div class="mb-4 flex items-center justify-between">
<span>最多可添加5张轮播图</span>
<div>
<span>最多可添加5张轮播图</span>
<span class="ml-4 text-gray-400 text-sm">提示拖拽卡片可调整排序</span>
</div>
<Button v-if="data.nav.length < 5" type="primary" @click="openUpload">
上传新图片
</Button>
</div>
<div class="mt-4 flex flex-wrap gap-4">
<Card
v-for="(item, index) in data.nav"
:key="item.id"
class="relative"
style="width: 240px"
>
<div class="group relative">
<Image :src="item.pic" class="h-40 w-full rounded object-cover" />
<!-- 图片预览组 -->
<ImagePreviewGroup
:preview="{
visible: previewVisible,
current: previewCurrent,
onVisibleChange: (vis: boolean) => { previewVisible = vis; },
}"
>
<div class="mt-4 flex flex-wrap gap-4">
<template v-for="(item, index) in data.nav" :key="item.id">
<!-- 拖拽占位符 -->
<div
class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 opacity-0 transition-opacity group-hover:opacity-100"
v-if="dragOverIndex === index && draggedIndex !== -1 && draggedIndex !== index"
class="relative rounded-lg border-2 border-dashed border-blue-500 bg-blue-50/50 dark:bg-blue-900/30"
style="width: 240px; height: 200px"
@dragenter="onDragEnter(index)"
@dragover="onDragOver"
@drop="onDrop($event, index)"
>
<Button
class="shadow-lg"
danger
shape="circle"
size="large"
type="primary"
@click.stop="deleteNavItem(item.id)"
>
<DeleteOutlined />
</Button>
<div class="flex h-full items-center justify-center text-blue-500">
释放以插入此处
</div>
</div>
</div>
<div class="mt-2 text-center text-sm text-gray-500">点击图片删除</div>
</Card>
</div>
<!-- 轮播图卡片 -->
<Card
:class="[
'relative cursor-move transition-all duration-300',
{
'opacity-30': draggedIndex === index,
'translate-y-1': dragOverIndex === index && draggedIndex !== -1 && draggedIndex !== index,
}
]"
style="width: 240px"
draggable="true"
@dragstart="onDragStart($event, index)"
@dragenter="onDragEnter(index)"
@dragover="onDragOver"
@drop="onDrop($event, index)"
@dragend="onDragEnd"
>
<!-- 序号标识 -->
<div class="absolute left-2 top-2 z-10 flex h-6 w-6 items-center justify-center rounded-full bg-blue-500 text-xs text-white">
{{ index + 1 }}
</div>
<!-- 拖拽手柄 -->
<div class="absolute right-2 top-2 z-10 flex h-6 w-6 items-center justify-center rounded bg-gray-800/50 text-white">
<HolderOutlined />
</div>
<div class="group relative">
<Image
:src="item.pic"
:preview="false"
class="h-40 w-full rounded object-cover pointer-events-none"
/>
<div
class="absolute inset-0 flex items-center justify-center gap-3 bg-black bg-opacity-50 opacity-0 transition-opacity group-hover:opacity-100"
>
<!-- 预览按钮 -->
<Button
class="shadow-lg"
shape="circle"
size="large"
type="primary"
@click.stop="openPreview(index)"
>
<EyeOutlined />
</Button>
<!-- 删除按钮带确认框 -->
<Popconfirm
title="确定要删除这张轮播图吗?"
ok-text="确定"
cancel-text="取消"
@confirm="deleteNavItem(item.id)"
>
<Button
class="shadow-lg"
danger
shape="circle"
size="large"
type="primary"
@click.stop
>
<DeleteOutlined />
</Button>
</Popconfirm>
</div>
<!-- 隐藏的Image用于预览组 -->
<Image :src="item.pic" class="hidden" />
</div>
<div class="mt-2 text-center text-sm text-gray-500">拖拽排序 | 悬停操作</div>
</Card>
</template>
</div>
</ImagePreviewGroup>
</Card>
<Card v-else-if="activeTabBar === 6" title="推广员">

View File

@@ -6,7 +6,7 @@
* @author 系统
* @date 2024
*/
import { ref } from 'vue';
import { onMounted, ref } from 'vue';
import { EllipsisText, Page, useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
@@ -50,7 +50,11 @@ const userInfoStore = useUserStore();
// 映射数据(示例)
const identityMap = { 1: '中医', 2: '西医' };
const activeTabBar = ref(1);
// Tab状态持久化
const TAB_STORAGE_KEY = 'xk_doctor_settings_tab';
const savedTab = localStorage.getItem(TAB_STORAGE_KEY);
const activeTabBar = ref(savedTab ? Number(savedTab) : 1);
/**
* Tab栏配置
@@ -91,7 +95,6 @@ const getMyInfo = () => {
data.value = res;
});
};
getMyInfo();
/**
* 保存挂号服务
*/
@@ -244,6 +247,9 @@ const doctorOrder = ref([]);
* @description 根据当前选中的Tab加载对应的数据
*/
const tabBarChange = () => {
// 保存当前tab到localStorage
localStorage.setItem(TAB_STORAGE_KEY, String(activeTabBar.value));
switch (activeTabBar.value) {
case 1: {
// 个人资料
@@ -267,6 +273,14 @@ const tabBarChange = () => {
}
}
};
// 初始化时加载基础数据和当前tab对应的数据
onMounted(() => {
getMyInfo(); // 基础数据(个人资料)始终需要
// 如果当前tab不是1还需要加载对应tab的数据
if (activeTabBar.value !== 1) {
tabBarChange();
}
});
const doctorOrderContent = ref('');
/**

View File

@@ -1,5 +1,7 @@
import type { VbenFormProps } from '#/adapter/form';
import { getStoreOption } from '#/views/system/store/api';
export const modalFormProps: VbenFormProps = {
wrapperClass: 'grid-cols-12', // 24栅格,
commonConfig: {
@@ -23,6 +25,33 @@ export const modalFormProps: VbenFormProps = {
triggerFields: ['id'],
},
},
{
component: 'ApiSelect',
componentProps: {
placeholder: '请选择绑定店铺(留空为平台通用)',
allowClear: true,
showSearch: true,
filterOption: (input: string, option: any) => {
return option.label?.toLowerCase().includes(input.toLowerCase());
},
api: async () => {
const res = await getStoreOption({});
// 添加"平台通用"选项
const options = [
{ label: '平台通用', value: 0 },
...(res || []).map((item: any) => ({
label: item.name || item.label,
value: item.id || item.value,
})),
];
return options;
},
},
fieldName: 'store_id',
label: '绑定店铺',
formItemClass: 'col-span-12',
defaultValue: 0,
},
{
component: 'Avatar',
fieldName: 'drug_license_image',
@@ -92,21 +121,6 @@ export const modalFormProps: VbenFormProps = {
formItemClass: 'col-span-6',
defaultValue: 1,
},
{
component: 'VbenInputNumber',
componentProps: {
placeholder: '门店ID留空表示平台通用',
min: 0,
},
dependencies: {
show: false,
triggerFields: ['id'],
},
fieldName: 'store_id',
label: '门店ID',
formItemClass: 'col-span-6',
defaultValue: 0,
},
],
showDefaultActions: false,
};

View File

@@ -74,7 +74,13 @@ export const gridOptions: VxeGridProps<RowType> = {
slots: { default: 'internet_drug_license_image' },
width: 180,
},
{ field: 'store_id', align: 'left', title: '门店ID', width: 120 },
{
field: 'store_name',
align: 'left',
title: '绑定店铺',
width: 150,
slots: { default: 'store_name' },
},
{ field: 'sort_order', align: 'left', title: '排序', width: 100 },
{
field: 'status_txt',

View File

@@ -207,6 +207,10 @@ const deleteQualificationsApi = (row: any) => {
/>
<span v-else>暂无</span>
</template>
<template #store_name="{ row }">
<span v-if="row.store_id === 0 || row.store_id === null">平台通用</span>
<span v-else>{{ row.store_name || `店铺ID: ${row.store_id}` }}</span>
</template>
<template #toolbar-tools></template>
<template #action="{ row }">
<TableAction