fix: 常用方、患者管理
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled

This commit is contained in:
2026-01-07 14:22:01 +08:00
parent e0acff7faa
commit 5eec8b370e
62 changed files with 4061 additions and 165 deletions

View File

@@ -742,10 +742,6 @@ export const usePrescriptionStore = defineStore('prescription', () => {
customSendMode: number = 0,
customStoreId: number | null = null,
) => {
// #region agent log
fetch('http://127.0.0.1:7242/ingest/3cda25fa-2f46-4a02-889c-2ffb76bc49fe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'prescription.ts:sendPrescription:entry',message:'sendPrescription接收的参数',data:{customSendMode:customSendMode,customStoreId:customStoreId,doctorSecondSignValue:doctorSecondSignValue},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'D'})}).catch(()=>{});
// #endregion
if (currentDrugs.value.length === 0) {
message.error('请选择药品');
return false;
@@ -810,9 +806,6 @@ export const usePrescriptionStore = defineStore('prescription', () => {
send_mode: customSendMode,
custom_store_id: customSendMode === 1 ? customStoreId : null,
}).then((res) => {
// #region agent log
fetch('http://127.0.0.1:7242/ingest/3cda25fa-2f46-4a02-889c-2ffb76bc49fe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'prescription.ts:sendPrescription:afterAPI',message:'API请求成功后',data:{send_mode:customSendMode,custom_store_id:customSendMode === 1 ? customStoreId : null,res:res},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'D'})}).catch(()=>{});
// #endregion
message.success('处方已发送');
sendMessage({
roomId: chatStore.currentFriend.room_id,

View File

@@ -13,6 +13,7 @@ const props = defineProps({
const emit = defineEmits([
// ... existing emits
'openPrescription',
'end-consultation-success',
]);
// 开方功能触发函数
@@ -20,6 +21,11 @@ const handleOpenPrescription = (registerId) => {
emit('openPrescription', registerId);
};
// 结束接诊成功处理函数
const handleEndConsultationSuccess = () => {
emit('end-consultation-success');
};
// ... rest of the component
</script>
@@ -32,6 +38,9 @@ const handleOpenPrescription = (registerId) => {
<MessageList class="flex-1" />
<!-- 输入区域 -->
<MessageInput @open-prescription="handleOpenPrescription" />
<MessageInput
@open-prescription="handleOpenPrescription"
@end-consultation-success="handleEndConsultationSuccess"
/>
</div>
</template>

View File

@@ -742,6 +742,32 @@ if (props.message.message_type === 10) {
</div>
</div>
<!-- 系统消息/价格更新卡片 (type=9) -->
<div
v-else-if="message.message_type === 9"
class="system-message-card"
>
<div
v-if="parsedContent?.type === 'price_update'"
class="price-update-card"
>
<div class="price-update-header">
<i class="fas fa-tag price-icon"></i>
<span class="price-title">订单价格更新</span>
</div>
<div class="price-update-content">
<p class="price-message">{{ parsedContent.message }}</p>
<div class="price-details">
<span class="order-no">订单号{{ parsedContent.order_no }}</span>
<span class="price-amount">¥{{ parsedContent.total_pay_price }}</span>
</div>
</div>
</div>
<div v-else class="system-message-text">
{{ typeof parsedContent === 'string' ? parsedContent : '系统消息' }}
</div>
</div>
<!-- 患者就诊经历卡片 (type=11) -->
<PatientExperienceCard
v-else-if="message.message_type === 11"
@@ -1099,4 +1125,49 @@ if (props.message.message_type === 10) {
.prescription-card:hover {
@apply -translate-y-0.5 transform;
}
/* 系统消息/价格更新卡片样式 */
.system-message-card {
@apply w-full max-w-md rounded-xl p-4;
}
.price-update-card {
@apply rounded-xl border border-orange-200 bg-gradient-to-r from-orange-50 to-amber-50 p-4 shadow-sm dark:border-orange-700 dark:from-orange-900/30 dark:to-amber-900/30;
}
.price-update-header {
@apply mb-3 flex items-center gap-2;
}
.price-icon {
@apply text-orange-500;
}
.price-title {
@apply font-semibold text-orange-700 dark:text-orange-300;
}
.price-update-content {
@apply space-y-2;
}
.price-message {
@apply text-sm text-gray-700 dark:text-gray-300;
}
.price-details {
@apply flex items-center justify-between border-t border-orange-200/50 pt-2 dark:border-orange-700/50;
}
.order-no {
@apply text-xs text-gray-500 dark:text-gray-400;
}
.price-amount {
@apply text-lg font-bold text-orange-600 dark:text-orange-400;
}
.system-message-text {
@apply rounded-lg bg-gray-100 p-3 text-center text-sm text-gray-600 dark:bg-gray-700 dark:text-gray-300;
}
</style>

View File

@@ -300,7 +300,8 @@ onUnmounted(() => {
// 定义 emits
const emit = defineEmits([
'sendMessage',
'openPrescription' // 添加开方事件
'openPrescription', // 添加开方事件
'end-consultation-success' // 结束接诊成功事件
]);
// 开方功能触发函数
@@ -453,7 +454,21 @@ const handleEndConsultation = () => {
reason: '医生主动结束'
});
// 发送结束问诊消息卡片
const endTime = new Date().toLocaleString('zh-CN');
await sendMessage({
roomId: chatStore.currentFriend?.room_id,
senderId: userStore.currentUser?.doctor_id,
receiverId: chatStore.currentFriend?.id,
type: 'end-consultation',
content: JSON.stringify({
end_reason: '医生主动结束',
end_time: endTime
})
});
message.success('结束接诊成功');
emit('end-consultation-success');
} catch (error) {
console.error('结束接诊失败:', error);
message.error('结束接诊失败,请重试');

View File

@@ -113,7 +113,7 @@ const [Modal, modalApi] = useVbenModal({
watch(
() => prescriptionStore.currentDrugs,
(newValue) => {
console.log('currentDrugs updated:', newValue);
// currentDrugs 更新
},
{ deep: true },
);
@@ -265,8 +265,8 @@ const openWesternModal = () => {
activePatient_id: prescriptionStore.currentRegisterId,
getCurrentDrugs: () => {
// 这里可以添加获取当前药品的逻辑
console.log('getCurrentDrugs called from WesternModal');
},
storagePrefix: 'onlineConsultation', // 在线复诊加前缀
});
WesternDrugModalApi.open();
};

View File

@@ -29,6 +29,9 @@ export const useChatStore = defineStore('chat', () => {
const callTimeout = ref(null);
const hasMore = ref(true);
// 新挂号通知 - 当收到 message_type=10 时更新,用于触发患者列表刷新
const newRegisterNotification = ref<{ timestamp: number; data: any } | null>(null);
const webrtc = useWebRTC();
const userStore = useUserStore();
@@ -390,6 +393,15 @@ export const useChatStore = defineStore('chat', () => {
isSent: isSent,
};
// 检测挂号消息(message_type=10)或患者就诊经历消息(message_type=11),触发患者列表刷新通知
if (messageData.message_type === 10 || messageData.message_type === 11) {
newRegisterNotification.value = {
timestamp: Date.now(),
data: messageData,
};
logWithTime('log', `收到新消息(type=${messageData.message_type}),触发患者列表刷新通知`);
}
try {
// 不再需要手动解析getParsedContent 会在组件中按需解析
// await chatDB.saveMessage(newMessage, currentUserId, senderId);
@@ -1620,6 +1632,7 @@ export const useChatStore = defineStore('chat', () => {
callStatus,
callError,
callConnectionStatus,
newRegisterNotification,
isConnected,
isInCall,
isCalling,

View File

@@ -1,6 +1,8 @@
import { message } from 'ant-design-vue';
import axios from 'axios';
import { useAccessStore } from '@vben/stores';
// 创建 Axios 实例
const service = axios.create({
baseURL: '/im-api/',
@@ -122,6 +124,10 @@ export const sendMessage = (data) => {
'video-call': 6,
'audio-call': 7,
file: 8,
register: 10,
'patient-experience': 11,
'product-card': 12,
'end-consultation': 13,
};
// 确保 receiver_user_id 有 user- 前缀(如果是患者端)
@@ -149,10 +155,15 @@ export const sendMessage = (data) => {
requestData.call_status = data.callStatus;
}
// 从 accessStore 获取 Token
const accessStore = useAccessStore();
const token = accessStore.accessToken;
return axios
.post(apiUrl, requestData, {
headers: {
'Content-Type': 'application/json',
'Authorization': token ? (token.startsWith('Bearer ') ? token : `Bearer ${token}`) : '',
},
timeout: 15_000,
withCredentials: false,
@@ -206,10 +217,15 @@ export const sendCallSignal = (signalData) => {
console.log('发送通话信令到API:', apiUrl, requestData);
// 从 accessStore 获取 Token
const accessStore = useAccessStore();
const token = accessStore.accessToken;
return axios
.post(apiUrl, requestData, {
headers: {
'Content-Type': 'application/json',
'Authorization': token ? (token.startsWith('Bearer ') ? token : `Bearer ${token}`) : '',
},
timeout: 15_000,
withCredentials: false,

View File

@@ -23,6 +23,12 @@ export const gridOptions: VxeGridProps<RowType> = {
{ field: 'user_patient.name', align: 'left', title: '就诊人名称' },
{ field: 'store.name', align: 'left', title: '开方诊所' },
{ field: 'status', title: '状态', slots: { default: 'status' } },
{
field: 'is_online',
title: '处方来源',
width: 100,
slots: { default: 'is-online' },
},
{ field: 'created_at', title: '创建时间' },
{
type: 'html',

View File

@@ -105,6 +105,11 @@ const openPrescriptionSourceModal = (id) => {
<!-- <p>{{ row.created_at }}</p>-->
</div>
</template>
<template #is-online="{ row }">
<Tag v-if="row.is_online === 1" color="blue">在线问诊</Tag>
<Tag v-else-if="row.is_online === 2" color="green">在线复诊</Tag>
<Tag v-else color="default">线下就诊</Tag>
</template>
<template #toolbar-tools></template>
<template #action="{ row }">
<TableAction

View File

@@ -85,3 +85,11 @@ export async function expressDetailByOrderId(data: Record<string, any>) {
export async function exportOrderApi() {
return requestClient.download(`${prefix}export`);
}
/**
* 更新包邮状态
* @param data
*/
export async function updateFreeShipping(data: { id: number; is_free_shipping: number }) {
return requestClient.post<any>(`${prefix}update-free-shipping`, data);
}

View File

@@ -49,6 +49,18 @@ export const gridOptions: VxeGridProps<RowType> = {
title: 'Erp状态',
slots: { default: 'is-sync-erp' },
},
{
field: 'is_free_shipping',
title: '是否包邮',
width: 100,
slots: { default: 'is-free-shipping' },
},
{
field: 'is_online',
title: '订单来源',
width: 100,
slots: { default: 'is-online' },
},
{ field: 'pay_time', title: '支付时间', slots: { default: 'pay-time' } },
{ field: 'created_at', title: '下单时间' },
{
@@ -82,7 +94,7 @@ export const gridOptions: VxeGridProps<RowType> = {
},
},
},
height: 'auto',
// height: 'auto',
border: false,
toolbarConfig: {
// 是否显示搜索表单控制按钮

View File

@@ -11,7 +11,7 @@ import {
} from '@vben/common-ui';
import { SvgCakeIcon } from '@vben/icons';
import { Button, Image, message, Tag } from 'ant-design-vue';
import { Button, Image, message, Switch, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
@@ -20,6 +20,7 @@ import {
exportOrderApi,
getOrderList,
saleAmountApi,
updateFreeShipping,
} from '#/views/business/order/product-order/api';
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
@@ -112,7 +113,6 @@ const saleAmount = () => {
saleAmountApi({
search_time: gridApi.formApi.latestSubmissionValues.search_time,
}).then((res) => {
console.log(res, 'sssssssss');
income.value = res.income;
total.value = res.total;
overviewItems.value = [
@@ -166,6 +166,18 @@ const openRefundModal = (id) => {
});
RefundModalApi.open();
};
const toggleFreeShipping = async (row: any, checked: boolean) => {
try {
const res = await updateFreeShipping({ id: row.id, is_free_shipping: checked ? 1 : 0 });
row.is_free_shipping = res.is_free_shipping;
row.trans_expenses = res.trans_expenses;
row.total_pay_price = res.total_pay_price;
message.success('更新成功');
} catch (error: any) {
message.error(error.message || '更新失败');
}
};
</script>
<template>
@@ -232,6 +244,19 @@ const openRefundModal = (id) => {
<Tag v-if="row.is_sync_erp === 1" color="green">已同步</Tag>
<Tag v-else color="red">未同步</Tag>
</template>
<template #is-free-shipping="{ row }">
<Switch
:checked="row.is_free_shipping === 1"
checked-children="包邮"
un-checked-children="不包邮"
@change="(checked) => toggleFreeShipping(row, checked)"
/>
</template>
<template #is-online="{ row }">
<Tag v-if="row.is_online === 1" color="blue">在线问诊</Tag>
<Tag v-else-if="row.is_online === 2" color="green">在线复诊</Tag>
<Tag v-else color="default">线下就诊</Tag>
</template>
<template #delivery-method="{ row }">
<!-- 判断是否为非处方订单-->
<div v-if="row.order_type === 2">

View File

@@ -75,8 +75,10 @@ const openPrescriptionDetail = (values) => {
</template>
<template #type="{ row }">
<div class="mt-3">
<Tag v-if="row.type === 0" color="purple">线下</Tag>
<Tag v-else-if="row.type === 1" color="green">线上</Tag>
<Tag v-if="row.type === 0" color="purple">线下就诊</Tag>
<Tag v-else-if="row.type === 1" color="blue">在线问诊</Tag>
<Tag v-else-if="row.type === 3" color="green">在线复诊</Tag>
<Tag v-else color="default">未知</Tag>
</div>
</template>
<template #status="{ row }">

View File

@@ -1,7 +1,5 @@
import type {VbenFormProps} from '#/adapter/form';
import {getSupplierOption} from '#/views/system/supplier/api';
export const modalFormProps: VbenFormProps = {
wrapperClass: 'grid-cols-12', // 24栅格,

View File

@@ -166,27 +166,7 @@ const openExcelUploadModal = () => {
},
},
]"
:drop-down-actions="[
// {
// label: '编辑',
// type: 'link',
// icon: 'ant-design:delete-outlined',
// size: 'small',
// // auth: ['china-medicine', 'sys:role:detail'],
// onClick: showModal.bind(null, row, true),
// },
// {
// label: '删除',
// type: 'link',
// icon: 'ant-design:delete-outlined',
// size: 'small',
// // auth: ['china-medicine', 'sys:role:detail'],
// popConfirm: {
// title: '确定删除吗',
// confirm: deleteApi.bind(null, row.id),
// },
// },
]"
:drop-down-actions="[]"
/>
</template>
</Grid>

View File

@@ -0,0 +1,59 @@
import { requestClient } from '#/api/request';
const prefix = 'drug-categories/';
/**
* 获取分类列表
* @param data
*/
export async function getDrugCategoriesList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
/**
* 获取分类树(用于下拉选择)
* @param data
*/
export async function getDrugCategoriesTree(data?: any) {
return requestClient.get<any>(`${prefix}tree`, { params: data });
}
/**
* 获取分类详情
* @param id
*/
export async function getDrugCategoriesDetail(id: number) {
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
}
/**
* 新增分类
* @param data
*/
export async function createDrugCategories(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}create`, data);
}
/**
* 编辑分类
* @param data
*/
export async function updateDrugCategories(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update`, data);
}
/**
* 删除分类
* @param data
*/
export async function deleteDrugCategories(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}delete`, data);
}
/**
* 获取专区下拉选项
*/
export async function getZoneOptions() {
return requestClient.get<any>(`${prefix}zone-options`);
}

View File

@@ -0,0 +1,203 @@
<script lang="ts" setup>
import { ref, watch, computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Form, FormItem, Input, InputNumber, message, Select, TreeSelect } from 'ant-design-vue';
import { createDrugCategories, getDrugCategoriesTree, getZoneOptions, updateDrugCategories } from '../api';
const isUpdate = ref(false);
const gridApi = ref();
// 表单数据
const formData = ref({
id: null as number | null,
zone_id: undefined as number | undefined,
category_name: '',
parent_id: 0 as number | undefined,
sort: 0,
});
// 是否是添加子分类(有 parent_id 传入)
const isAddChild = ref(false);
// 是否正在初始化(用于避免 watch 清空 parent_id
const isInitializing = ref(false);
// 选项数据
const zoneOptions = ref<any[]>([]);
const parentTreeData = ref<any[]>([]);
const parentLoading = ref(false);
// 分区是否禁用(添加子分类时禁用)
const zoneDisabled = computed(() => isAddChild.value);
// 加载分区选项
async function loadZoneOptions() {
const res = await getZoneOptions();
zoneOptions.value = res || [];
}
// 加载父级分类树(根据 zone_id
async function loadParentTree(zoneId: number | undefined) {
if (!zoneId) {
parentTreeData.value = [];
return;
}
parentLoading.value = true;
try {
const res = await getDrugCategoriesTree({ zone_id: zoneId });
parentTreeData.value = res || [];
} finally {
parentLoading.value = false;
}
}
// 监听分区变化,动态加载父级分类
watch(
() => formData.value.zone_id,
async (newZoneId, oldZoneId) => {
// 只有在用户手动切换分区时才清空父级分类(排除初始化和添加子分类)
if (!isInitializing.value && oldZoneId !== undefined && oldZoneId !== newZoneId && !isAddChild.value) {
formData.value.parent_id = 0;
}
// 加载父级分类数据
await loadParentTree(newZoneId);
},
{ immediate: true }
);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
async onConfirm() {
// 简单验证
if (!formData.value.zone_id) {
message.error('请选择所属专区');
return;
}
if (!formData.value.category_name?.trim()) {
message.error('请输入分类名称');
return;
}
modalApi.setState({ loading: true, confirmLoading: true });
const submitData = {
...formData.value,
parent_id: formData.value.parent_id || 0,
};
const submitApi = isUpdate.value ? updateDrugCategories : createDrugCategories;
try {
await submitApi(submitData);
message.success(isUpdate.value ? '编辑成功' : '新增成功');
gridApi.value?.reload();
modalApi.close();
} catch (err: any) {
message.error(err?.message || '操作失败');
} finally {
modalApi.setState({ loading: false, confirmLoading: false });
}
},
async onOpenChange(isOpen: boolean) {
if (isOpen) {
const { values, update, gridApi: gApi } = modalApi.getData<Record<string, any>>();
gridApi.value = gApi;
isUpdate.value = update;
// 加载分区选项
await loadZoneOptions();
// 标记正在初始化,避免 watch 清空 parent_id
isInitializing.value = true;
if (update && values) {
// 编辑模式
isAddChild.value = false;
formData.value = {
id: values.id,
zone_id: values.zone_id || undefined,
category_name: values.category_name || '',
parent_id: values.parent_id || 0,
sort: values.sort || 0,
};
} else if (values?.parent_id) {
// 添加子分类模式
isAddChild.value = true;
formData.value = {
id: null,
zone_id: values.zone_id || undefined,
category_name: '',
parent_id: values.parent_id,
sort: 0,
};
} else {
// 新增顶级分类模式
isAddChild.value = false;
formData.value = {
id: null,
zone_id: undefined,
category_name: '',
parent_id: 0,
sort: 0,
};
}
// 初始化完成后,重置标记
// 使用 nextTick 确保 watch 已经处理完初始化的值变化
setTimeout(() => {
isInitializing.value = false;
}, 0);
}
},
});
</script>
<template>
<Modal :title="`${isUpdate ? '编辑' : '新增'}商品分类`" class="w-[500px]">
<Form :model="formData" layout="horizontal" :label-col="{ span: 5 }" :wrapper-col="{ span: 19 }">
<FormItem label="所属专区" required>
<Select
v-model:value="formData.zone_id"
:options="zoneOptions"
:field-names="{ label: 'title', value: 'id' }"
placeholder="请选择所属专区"
:disabled="zoneDisabled"
/>
</FormItem>
<FormItem label="分类名称" required>
<Input v-model:value="formData.category_name" placeholder="请输入分类名称" />
</FormItem>
<FormItem label="父级分类">
<TreeSelect
v-model:value="formData.parent_id"
:tree-data="parentTreeData"
:field-names="{ label: 'category_name', value: 'id', children: 'children' }"
:placeholder="formData.zone_id ? '请选择父级分类(不选则为顶级)' : '请先选择所属专区'"
:loading="parentLoading"
:disabled="!formData.zone_id || isAddChild"
allow-clear
tree-default-expand-all
/>
</FormItem>
<FormItem label="排序">
<InputNumber
v-model:value="formData.sort"
:min="0"
:max="9999"
placeholder="请输入排序值,数值越小越靠前"
style="width: 100%"
/>
</FormItem>
</Form>
</Modal>
</template>

View File

@@ -0,0 +1,78 @@
import type { VbenFormProps } from '#/adapter/form';
import { getDrugCategoriesTree, getZoneOptions } from '../api';
export const modalFormProps: VbenFormProps = {
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
fieldName: 'id',
label: 'ID',
dependencies: {
show: false,
triggerFields: ['id'],
},
},
{
component: 'ApiSelect',
componentProps: {
api: getZoneOptions,
labelField: 'title',
valueField: 'id',
placeholder: '请选择所属专区',
},
fieldName: 'zone_id',
formItemClass: 'col-span-12',
label: '所属专区',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入分类名称',
},
fieldName: 'category_name',
formItemClass: 'col-span-12',
label: '分类名称',
rules: 'required',
},
{
component: 'ApiTreeSelect',
componentProps: {
childrenField: 'children',
labelField: 'category_name',
valueField: 'id',
api: getDrugCategoriesTree,
placeholder: '请选择父级分类(不选则为顶级)',
allowClear: true,
treeDefaultExpandAll: true,
},
defaultValue: 0,
fieldName: 'parent_id',
formItemClass: 'col-span-12',
label: '父级分类',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入排序值,数值越小越靠前',
min: 0,
max: 9999,
},
defaultValue: 0,
fieldName: 'sort',
formItemClass: 'col-span-12',
label: '排序',
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,54 @@
import type { VbenFormProps } from '#/adapter/form';
import { getZoneOptions } from '../api';
export const formOptions: VbenFormProps = {
collapsed: false,
commonConfig: {
componentProps: {
class: 'w-full',
},
},
schema: [
{
component: 'ApiSelect',
componentProps: {
api: getZoneOptions,
labelField: 'title',
valueField: 'id',
placeholder: '请选择专区',
allowClear: true,
},
fieldName: 'zone_id',
label: '所属专区',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入分类名称',
},
fieldName: 'category_name',
label: '分类名称',
},
{
component: 'VbenSelect',
componentProps: {
allowClear: true,
options: [
{ label: '一级分类', value: 1 },
{ label: '二级分类', value: 2 },
{ label: '三级分类', value: 3 },
],
placeholder: '请选择层级',
},
fieldName: 'level',
label: '分类层级',
},
],
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
};

View File

@@ -0,0 +1,88 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getDrugCategoriesList } from '../api';
interface RowType {
id: number;
category_name: string;
zone_id: number;
zone_name: string;
level: number;
parent_id: number;
sort: number;
created_at: string;
updated_at: string;
}
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: '',
},
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
columns: [
{ type: 'checkbox', width: 60 },
{ width: 60, treeNode: true },
{ field: 'id', align: 'left', title: 'ID', width: 80 },
{ field: 'zone_name', align: 'left', title: '所属专区', width: 120 },
{ field: 'category_name', align: 'left', title: '分类名称' },
{
field: 'level',
title: '层级',
width: 100,
formatter: ({ cellValue }) => {
const levelMap: Record<number, string> = {
1: '一级分类',
2: '二级分类',
3: '三级分类',
};
return levelMap[cellValue] || `${cellValue}`;
},
},
{ field: 'sort', title: '排序', width: 80 },
{ field: 'created_at', title: '创建时间', width: 180 },
{ type: 'html', title: '操作', width: 200, slots: { default: 'action' } },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getDrugCategoriesList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
treeConfig: {
parentField: 'parent_id',
rowField: 'id',
transform: true,
expandAll: false,
},
height: 'auto',
border: false,
toolbarConfig: {
// @ts-ignore
search: true,
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons',
},
custom: {
icon: 'vxe-icon-menu',
},
},
showOverflow: false,
};

View File

@@ -0,0 +1,159 @@
<script lang="ts" setup>
import type { VxeGridListeners } from '#/adapter/vxe-table';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import { deleteDrugCategories } from './api';
import FormModalDemo from './components/modal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
const hasTopTableDropDownActions = ref(false);
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
checkboxAll() {
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents,
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
const showModal = (data: any = null, isUpdate = false) => {
formModalApi.setData({
values: data,
update: isUpdate,
gridApi,
});
formModalApi.open();
};
const deleteApi = (row: any) => {
let ids = [];
if (row) {
ids.push(row);
} else {
ids = gridApi.grid.getCheckboxRecords().map((item: any) => item.id);
}
deleteDrugCategories({ ids })
.then(() => {
message.success('删除成功!');
gridApi.reload();
})
.catch((err: any) => {
message.error(err?.message || '删除失败');
});
};
const expandAll = () => {
gridApi.grid?.setAllTreeExpand(true);
};
const collapseAll = () => {
gridApi.grid?.setAllTreeExpand(false);
};
</script>
<template>
<Page auto-content-height title="商品分类管理">
<FormModal />
<Grid>
<template #toolbar-buttons>
<TableAction
:flex="false"
:actions="[
{
label: '新增',
type: 'primary',
icon: 'ant-design:plus-outlined',
onClick: () => showModal(null, false),
},
{
label: '展开全部',
type: 'default',
onClick: expandAll,
},
{
label: '收起全部',
type: 'default',
onClick: collapseAll,
},
]"
:drop-down-actions="[
{
label: '批量删除',
icon: 'ant-design:delete-outlined',
ifShow: hasTopTableDropDownActions,
popConfirm: {
title: '确定删除选中的分类吗',
confirm: () => deleteApi(null),
},
},
]"
>
<template #more>
<Button style="margin-left: 16px">
批量操作
<Icon icon="ant-design:down-outlined" />
</Button>
</template>
</TableAction>
</template>
<template #toolbar-tools></template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '新增子分类',
type: 'link',
icon: 'ant-design:plus-outlined',
size: 'small',
ifShow: row.level < 3,
onClick: () =>
showModal({ parent_id: row.id, level: row.level + 1, zone_id: row.zone_id }, false),
},
{
label: '编辑',
type: 'link',
icon: 'uil:edit',
size: 'small',
onClick: () => showModal(row, true),
},
{
label: '删除',
type: 'link',
icon: 'ant-design:delete-outlined',
size: 'small',
popConfirm: {
title: '确定删除该分类吗?',
confirm: () => deleteApi(row.id),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -77,3 +77,12 @@ export async function deleteHealthFood(data: Record<string, any>) {
export async function importHealthFoodApi(data: Record<string, any>) {
return requestClient.upload(`${prefix}import`, data);
}
/**
* 更新保健食品分区分类
* @param data - 包含id、zone_id和category_id的对象
* @returns 返回更新结果
*/
export async function updateZoneCategory(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update-zone-category`, data);
}

View File

@@ -0,0 +1,138 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Form, FormItem, Input, message, Select, TreeSelect } from 'ant-design-vue';
import { getDrugCategoriesTree, getZoneOptions } from '#/views/business/product/drug-categories/api';
import { updateZoneCategory } from '../api';
const gridApi = ref();
const currentRow = ref<Record<string, any>>({});
// 表单数据
const formData = ref({
id: null as number | null,
drug_name: '',
zone_id: undefined as number | undefined,
category_id: undefined as number | undefined,
});
// 选项数据
const zoneOptions = ref<any[]>([]);
const categoryTreeData = ref<any[]>([]);
const categoryLoading = ref(false);
// 加载分区选项
async function loadZoneOptions() {
const res = await getZoneOptions();
zoneOptions.value = res || [];
}
// 加载分类树(根据 zone_id
async function loadCategoryTree(zoneId: number | undefined) {
if (!zoneId) {
categoryTreeData.value = [];
return;
}
categoryLoading.value = true;
try {
const res = await getDrugCategoriesTree({ zone_id: zoneId });
categoryTreeData.value = res || [];
} finally {
categoryLoading.value = false;
}
}
// 监听分区变化,动态加载分类
watch(
() => formData.value.zone_id,
async (newZoneId, oldZoneId) => {
// 只有在用户手动切换分区时才清空分类(排除初始化)
if (oldZoneId !== undefined) {
formData.value.category_id = undefined;
}
await loadCategoryTree(newZoneId);
}
);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
async onConfirm() {
modalApi.setState({ loading: true, confirmLoading: true });
try {
await updateZoneCategory({
id: formData.value.id!,
zone_id: formData.value.zone_id || null,
category_id: formData.value.category_id || null,
});
message.success('设置成功');
gridApi.value?.reload();
modalApi.close();
} finally {
modalApi.setState({ loading: false, confirmLoading: false });
}
},
async onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = modalApi.getData<Record<string, any>>();
gridApi.value = data?.gridApi;
currentRow.value = data?.row || {};
// 初始化表单数据
formData.value = {
id: currentRow.value.id,
drug_name: currentRow.value.drug_name,
zone_id: currentRow.value.zone_id || undefined,
category_id: currentRow.value.category_id || undefined,
};
// 加载分区选项
await loadZoneOptions();
// 如果有初始 zone_id加载对应分类
if (formData.value.zone_id) {
await loadCategoryTree(formData.value.zone_id);
}
}
},
});
</script>
<template>
<Modal title="分区分类设置" class="w-[450px]">
<Form :model="formData" layout="horizontal" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<FormItem label="保健食品名称">
<Input v-model:value="formData.drug_name" disabled />
</FormItem>
<FormItem label="所属分区">
<Select
v-model:value="formData.zone_id"
:options="zoneOptions"
:field-names="{ label: 'title', value: 'id' }"
placeholder="请选择分区"
allow-clear
/>
</FormItem>
<FormItem label="所属分类">
<TreeSelect
v-model:value="formData.category_id"
:tree-data="categoryTreeData"
:field-names="{ label: 'category_name', value: 'id', children: 'children' }"
:placeholder="formData.zone_id ? '请选择分类' : '请先选择分区'"
:loading="categoryLoading"
:disabled="!formData.zone_id"
allow-clear
tree-default-expand-all
/>
</FormItem>
</Form>
</Modal>
</template>

View File

@@ -3,6 +3,8 @@ import type {VbenFormProps} from '#/adapter/form';
// 导入供应商选项API
import {getSupplierOption} from '#/views/system/supplier/api';
// 导入分类树API和分区选项API
import {getDrugCategoriesTree, getZoneOptions} from '#/views/business/product/drug-categories/api';
/**
* 保健食品管理表单配置
@@ -82,6 +84,55 @@ export const modalFormProps: VbenFormProps = {
label: '所属供应商',
rules: 'required', // 必填验证
},
{
// 所属分区字段:可选,下拉选择分区
component: 'ApiSelect',
componentProps: {
api: getZoneOptions,
labelField: 'title',
valueField: 'id',
placeholder: '请选择分区',
allowClear: true,
},
fieldName: 'zone_id',
formItemClass: 'col-span-6',
label: '所属分区',
},
{
// 所属分类字段:可选,树形下拉选择分类(联动分区)
component: 'ApiTreeSelect',
componentProps: {
api: getDrugCategoriesTree,
childrenField: 'children',
labelField: 'category_name',
valueField: 'id',
placeholder: '请先选择分区',
allowClear: true,
treeDefaultExpandAll: true,
immediate: false, // 首次不立即加载
alwaysLoad: true, // 每次展开都重新加载
},
fieldName: 'category_id',
formItemClass: 'col-span-6',
label: '所属分类',
dependencies: {
componentProps(values, formApi) {
return {
// 使用 beforeFetch 在请求发出前动态获取最新的 zone_id
beforeFetch: async () => {
const currentValues = await formApi.getValues();
return { zone_id: currentValues.zone_id };
},
placeholder: values.zone_id ? '请选择分类' : '请先选择分区',
};
},
trigger(values, formApi) {
// zone_id 变化时清空分类选择
formApi.setFieldValue('category_id', undefined);
},
triggerFields: ['zone_id'],
},
},
{
// 保健食品昵称字段:必填,保健食品的常用名称
component: 'VbenInput',

View File

@@ -13,6 +13,10 @@ interface RowType {
logo: string; // 保健食品Logo
introduce: string; // 保健食品介绍
created_at: string; // 创建时间
zone_id: number; // 分区ID
zone_name: string; // 分区名称
category_id: number; // 分类ID
category_name: string; // 分类名称
}
/**
@@ -39,6 +43,18 @@ export const gridOptions: VxeGridProps<RowType> = {
{ field: 'id', align: 'left', title: 'ID', width: 100 }, // ID列
{ field: 'drug_name', align: 'left', title: '保健食品名称' }, // 保健食品名称列
{ field: 'pinyin_simple', title: '拼音' }, // 拼音列
{
field: 'zone_name',
title: '分区',
width: 100,
slots: { default: 'zone' }, // 使用插槽显示可点击的分区链接
},
{
field: 'category_name',
title: '分类',
width: 120,
slots: { default: 'category' }, // 使用插槽显示可点击的分类链接
},
{ field: 'supplier.name', title: '供应商', slots: { default: 'supplier' } }, // 供应商列,使用插槽自定义显示
{
field: 'image',

View File

@@ -24,6 +24,8 @@ import { deleteHealthFood, exportHealthFoodApi } from './api';
import FormModalDemo from './components/modal.vue';
// 导入Excel上传组件
import ExcelUpload from './components/ExcelUpload.vue';
// 导入分类设置弹窗组件
import CategoryModal from './components/CategoryModal.vue';
// 导入搜索表单配置
import { formOptions } from './config/search';
// 导入表格配置
@@ -67,6 +69,23 @@ const [ExcelUploadModal, ExcelUploadModalApi] = useVbenModal({
connectedComponent: ExcelUpload,
});
// 初始化分类设置弹窗组件
const [CategoryModalComp, categoryModalApi] = useVbenModal({
connectedComponent: CategoryModal,
});
/**
* 打开分类设置弹窗
* @param row - 当前行数据
*/
const openCategoryModal = (row: any) => {
categoryModalApi.setData({
row,
gridApi,
});
categoryModalApi.open();
};
/**
* 显示表单弹窗
* @param data - 表单数据,默认为空对象
@@ -135,6 +154,7 @@ const openExcelUploadModal = () => {
<Page auto-content-height title="保健食品管理">
<ExcelUploadModal />
<FormModal />
<CategoryModalComp />
<Grid>
<template #toolbar-buttons>
<TableAction
@@ -185,6 +205,16 @@ const openExcelUploadModal = () => {
<template #supplier="{ row }">
{{ row.supplier?.name || row.source }}
</template>
<template #zone="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.zone_name || '未设置' }}
</a>
</template>
<template #category="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.category_name || '未设置' }}
</a>
</template>
<template #instruction="{ row }">
<div class="div" style="width: 120px; height: 120px; overflow: scroll">
<Image :src="row.instruction" height="30" width="30" />

View File

@@ -0,0 +1,78 @@
import { requestClient } from '#/api/request';
const prefix = 'medical-device/';
/**
* 分页查询医疗器械列表
* @param data
*/
export async function getMedicalDeviceList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
/**
* 获取医疗器械下拉选项
* @param data
*/
export async function getMedicalDeviceOption(data: any) {
return requestClient.get<any>(`${prefix}option`, { params: data });
}
/**
* 获取医疗器械详情
* @param id
*/
export async function getMedicalDeviceInfo(id: number) {
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
}
/**
* 导出医疗器械
*/
export async function exportMedicalDeviceApi() {
return requestClient.download(`${prefix}export`);
}
/**
* 新增医疗器械
* @param data
*/
export async function createMedicalDevice(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}create`, data);
}
/**
* 编辑医疗器械
* @param data
*/
export async function updateMedicalDevice(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update`, data);
}
/**
* 删除医疗器械
* @param data
*/
export async function deleteMedicalDevice(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}delete`, data);
}
/**
* 导入医疗器械
* @param data
*/
export async function importMedicalDeviceApi(data: Record<string, any>) {
return requestClient.upload(`${prefix}import`, data);
}
/**
* 更新医疗器械分区分类
* @param data
*/
export async function updateZoneCategory(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update-zone-category`, data);
}

View File

@@ -0,0 +1,138 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Form, FormItem, Input, message, Select, TreeSelect } from 'ant-design-vue';
import { getDrugCategoriesTree, getZoneOptions } from '#/views/business/product/drug-categories/api';
import { updateZoneCategory } from '../api';
const gridApi = ref();
const currentRow = ref<Record<string, any>>({});
// 表单数据
const formData = ref({
id: null as number | null,
drug_name: '',
zone_id: undefined as number | undefined,
category_id: undefined as number | undefined,
});
// 选项数据
const zoneOptions = ref<any[]>([]);
const categoryTreeData = ref<any[]>([]);
const categoryLoading = ref(false);
// 加载分区选项
async function loadZoneOptions() {
const res = await getZoneOptions();
zoneOptions.value = res || [];
}
// 加载分类树(根据 zone_id
async function loadCategoryTree(zoneId: number | undefined) {
if (!zoneId) {
categoryTreeData.value = [];
return;
}
categoryLoading.value = true;
try {
const res = await getDrugCategoriesTree({ zone_id: zoneId });
categoryTreeData.value = res || [];
} finally {
categoryLoading.value = false;
}
}
// 监听分区变化,动态加载分类
watch(
() => formData.value.zone_id,
async (newZoneId, oldZoneId) => {
// 只有在用户手动切换分区时才清空分类(排除初始化)
if (oldZoneId !== undefined) {
formData.value.category_id = undefined;
}
await loadCategoryTree(newZoneId);
}
);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
async onConfirm() {
modalApi.setState({ loading: true, confirmLoading: true });
try {
await updateZoneCategory({
id: formData.value.id!,
zone_id: formData.value.zone_id || null,
category_id: formData.value.category_id || null,
});
message.success('设置成功');
gridApi.value?.reload();
modalApi.close();
} finally {
modalApi.setState({ loading: false, confirmLoading: false });
}
},
async onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = modalApi.getData<Record<string, any>>();
gridApi.value = data?.gridApi;
currentRow.value = data?.row || {};
// 初始化表单数据
formData.value = {
id: currentRow.value.id,
drug_name: currentRow.value.drug_name,
zone_id: currentRow.value.zone_id || undefined,
category_id: currentRow.value.category_id || undefined,
};
// 加载分区选项
await loadZoneOptions();
// 如果有初始 zone_id加载对应分类
if (formData.value.zone_id) {
await loadCategoryTree(formData.value.zone_id);
}
}
},
});
</script>
<template>
<Modal title="分区分类设置" class="w-[450px]">
<Form :model="formData" layout="horizontal" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<FormItem label="器械名称">
<Input v-model:value="formData.drug_name" disabled />
</FormItem>
<FormItem label="所属分区">
<Select
v-model:value="formData.zone_id"
:options="zoneOptions"
:field-names="{ label: 'title', value: 'id' }"
placeholder="请选择分区"
allow-clear
/>
</FormItem>
<FormItem label="所属分类">
<TreeSelect
v-model:value="formData.category_id"
:tree-data="categoryTreeData"
:field-names="{ label: 'category_name', value: 'id', children: 'children' }"
:placeholder="formData.zone_id ? '请选择分类' : '请先选择分区'"
:loading="categoryLoading"
:disabled="!formData.zone_id"
allow-clear
tree-default-expand-all
/>
</FormItem>
</Form>
</Modal>
</template>

View File

@@ -0,0 +1,167 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, UploadDragger, type UploadFile } from 'ant-design-vue';
import { importMedicalDeviceApi } from '../api';
const gridApi = ref();
const fileList = ref<UploadFile[]>([]);
const selectedFile = ref<File | null>(null);
const isFileSelected = ref(false);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
selectedFile.value = null;
isFileSelected.value = false;
fileList.value = [];
modalApi.close();
},
onConfirm: async () => {
if (!selectedFile.value) {
message.warning('请先选择要上传的文件');
return;
}
modalApi.setState({ loading: true, confirmLoading: true });
try {
await importMedicalDeviceApi({
file: selectedFile.value,
});
message.success('导入成功');
gridApi.value?.reload();
selectedFile.value = null;
isFileSelected.value = false;
fileList.value = [];
modalApi.close();
} catch {
message.error('导入失败');
} finally {
modalApi.setState({ loading: false, confirmLoading: false });
}
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
selectedFile.value = null;
isFileSelected.value = false;
fileList.value = [];
},
});
const handleChange = (info: { file: UploadFile }) => {
const { file } = info;
if (file.status === 'removed') {
selectedFile.value = null;
isFileSelected.value = false;
return;
}
if (file) {
selectedFile.value = file;
isFileSelected.value = true;
message.success('文件已选择,请点击确认按钮上传');
}
};
</script>
<template>
<Modal class="w-[30%]" title="上传Excel">
<div
v-if="isFileSelected"
class="mb-4 rounded border border-green-200 bg-green-50 p-3"
>
<p class="text-sm text-green-700">已选择文件{{ selectedFile?.name }}</p>
<p class="mt-1 text-xs text-green-600">
文件已准备就绪点击确认按钮开始上传
</p>
</div>
<UploadDragger
v-model:file-list="fileList"
:before-upload="() => false"
:max-count="1"
:on-change="handleChange"
accept=".xlsx,.xls"
name="file"
>
<p class="flex justify-center">
<svg
height="64"
viewBox="0 0 32 32"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient
id="vscodeIconsFileTypeExcel0"
gradientTransform="translate(0 2100)"
gradientUnits="userSpaceOnUse"
x1="4.494"
x2="13.832"
y1="-2092.086"
y2="-2075.914"
>
<stop offset="0" stop-color="#18884f" />
<stop offset=".5" stop-color="#117e43" />
<stop offset="1" stop-color="#0b6631" />
</linearGradient>
</defs>
<path
d="M19.581 15.35L8.512 13.4v14.409A1.19 1.19 0 0 0 9.705 29h19.1A1.19 1.19 0 0 0 30 27.809V22.5Z"
fill="#185c37"
/>
<path
d="M19.581 3H9.705a1.19 1.19 0 0 0-1.193 1.191V9.5L19.581 16l5.861 1.95L30 16V9.5Z"
fill="#21a366"
/>
<path d="M8.512 9.5h11.069V16H8.512Z" fill="#107c41" />
<path
d="M16.434 8.2H8.512v16.25h7.922a1.2 1.2 0 0 0 1.194-1.191V9.391A1.2 1.2 0 0 0 16.434 8.2"
opacity="0.1"
/>
<path
d="M15.783 8.85H8.512V25.1h7.271a1.2 1.2 0 0 0 1.194-1.191V10.041a1.2 1.2 0 0 0-1.194-1.191"
opacity="0.2"
/>
<path
d="M15.783 8.85H8.512V23.8h7.271a1.2 1.2 0 0 0 1.194-1.191V10.041a1.2 1.2 0 0 0-1.194-1.191"
opacity="0.2"
/>
<path
d="M15.132 8.85h-6.62V23.8h6.62a1.2 1.2 0 0 0 1.194-1.191V10.041a1.2 1.2 0 0 0-1.194-1.191"
opacity="0.2"
/>
<path
d="M3.194 8.85h11.938a1.193 1.193 0 0 1 1.194 1.191v11.918a1.193 1.193 0 0 1-1.194 1.191H3.194A1.19 1.19 0 0 1 2 21.959V10.041A1.19 1.19 0 0 1 3.194 8.85"
fill="url(#vscodeIconsFileTypeExcel0)"
/>
<path
d="m5.7 19.873l2.511-3.884l-2.3-3.862h1.847L9.013 14.6c.116.234.2.408.238.524h.017q.123-.281.26-.546l1.342-2.447h1.7l-2.359 3.84l2.419 3.905h-1.809l-1.45-2.711A2.4 2.4 0 0 1 9.2 16.8h-.024a1.7 1.7 0 0 1-.168.351l-1.493 2.722Z"
fill="#fff"
/>
<path
d="M28.806 3h-9.225v6.5H30V4.191A1.19 1.19 0 0 0 28.806 3"
fill="#33c481"
/>
<path d="M19.581 16H30v6.5H19.581Z" fill="#107c41" />
</svg>
</p>
<p class="ant-upload-text">点击或拖动文件到此区域进行选择</p>
<p class="ant-upload-hint">
支持单个上传xlsx格式文件选择文件后需要点击确认按钮进行上传
</p>
</UploadDragger>
</Modal>
</template>

View File

@@ -0,0 +1,107 @@
<script lang="ts" setup>
import {ref} from 'vue';
import {useVbenModal} from '@vben/common-ui';
import {message} from 'ant-design-vue';
import {useVbenForm} from '#/adapter/form';
import {getDrugUseList} from '#/views/doctor/doctor-reception/api';
import {createMedicalDevice, getMedicalDeviceInfo, updateMedicalDevice,} from '../api';
import {modalFormProps} from '../config/form';
const drugUnit = ref([]);
getDrugUseList().then((res) => {
drugUnit.value = res.drug_unit.map((item) => {
return {
label: item.name,
value: item.id,
};
});
return res;
});
const isUpdate = ref(false);
const gridApi = ref();
const [Form, formApi] = useVbenForm(modalFormProps);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
formApi.validate().then(async (e: any) => {
if (e.valid) {
const values = await formApi.getValues();
modalApi.setState({ loading: true, confirmLoading: true });
const submitApi = isUpdate.value
? updateMedicalDevice
: createMedicalDevice;
submitApi(values)
.then(() => {
message.success('保存成功');
gridApi.value?.reload();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
}
});
await formApi.validateAndSubmitForm();
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (isOpen) {
formApi.updateSchema([
{
componentProps: {
options: drugUnit.value,
},
fieldName: 'unit_id',
},
]);
const { values, update } = modalApi.getData<Record<string, any>>();
if (values) {
isUpdate.value = update;
if (update && values.id) {
getMedicalDeviceInfo(values.id)
.then((res: any) => {
if (res) {
formApi.setValues({
...values,
introduction_images: res.introduction_images,
});
} else {
formApi.setValues(values);
}
})
.catch((error) => {
console.error('获取商品详情失败:', error);
formApi.setValues(values);
});
} else {
formApi.setValues(values);
}
}
}
},
});
</script>
<template>
<Modal
:title="`${isUpdate === true ? '编辑' : '新增'}医疗器械`"
class="w-[30%]"
>
<Form />
</Modal>
</template>

View File

@@ -0,0 +1,240 @@
import type {VbenFormProps} from '#/adapter/form';
import {getSupplierOption} from '#/views/system/supplier/api';
import {getDrugCategoriesTree, getZoneOptions} from '#/views/business/product/drug-categories/api';
export const modalFormProps: VbenFormProps = {
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
fieldName: 'id',
label: 'ID',
formItemClass: 'col-span-6',
dependencies: {
show: false,
triggerFields: ['id'],
},
},
{
component: 'VbenInput',
fieldName: 'showSelect',
label: 'ID',
formItemClass: 'col-span-6',
dependencies: {
show: false,
triggerFields: ['id'],
},
},
{
component: 'Avatar',
fieldName: 'image',
label: '产品图片',
rules: 'required',
formItemClass: 'col-span-6',
},
{
component: 'ApiSelect',
componentProps: {
allowClear: true,
showSearch: true,
filterOption: (input: string, option: any) => {
return option?.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
},
afterFetch: (data: { id: number; name: string }[]) => {
return data.map((item: any) => ({
label: item.name,
value: item.id,
}));
},
api: getSupplierOption,
placeholder: '请选择',
},
fieldName: 'supplier_id',
formItemClass: 'col-span-6',
label: '所属供应商',
rules: 'required',
},
{
component: 'ApiSelect',
componentProps: {
api: getZoneOptions,
labelField: 'title',
valueField: 'id',
placeholder: '请选择分区',
allowClear: true,
},
fieldName: 'zone_id',
formItemClass: 'col-span-6',
label: '所属分区',
},
{
component: 'ApiTreeSelect',
componentProps: {
api: getDrugCategoriesTree,
childrenField: 'children',
labelField: 'category_name',
valueField: 'id',
placeholder: '请先选择分区',
allowClear: true,
treeDefaultExpandAll: true,
immediate: false, // 首次不立即加载
alwaysLoad: true, // 每次展开都重新加载
},
fieldName: 'category_id',
formItemClass: 'col-span-6',
label: '所属分类',
dependencies: {
componentProps(values, formApi) {
return {
// 使用 beforeFetch 在请求发出前动态获取最新的 zone_id
beforeFetch: async () => {
const currentValues = await formApi.getValues();
return { zone_id: currentValues.zone_id };
},
placeholder: values.zone_id ? '请选择分类' : '请先选择分区',
};
},
trigger(values, formApi) {
// zone_id 变化时清空分类选择
formApi.setFieldValue('category_id', undefined);
},
triggerFields: ['zone_id'],
},
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入器械名称',
},
fieldName: 'drug_name',
formItemClass: 'col-span-6',
label: '器械名称',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入器械别名',
},
fieldName: 'drug_alias',
formItemClass: 'col-span-6',
label: '器械别名',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入器械编号',
},
fieldName: 'drug_number',
formItemClass: 'col-span-6',
label: '器械编号',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入规格型号',
},
fieldName: 'specification',
formItemClass: 'col-span-6',
label: '规格型号',
rules: 'required',
},
{
component: 'VbenSelect',
componentProps: {
options: [],
placeholder: '请选择',
},
fieldName: 'unit_id',
formItemClass: 'col-span-6',
label: '单位',
rules: 'required',
},
{
component: 'RadioGroup',
componentProps: {
placeholder: '请选择',
options: [
{
label: '草稿',
value: 1,
},
{
label: '下架',
value: 2,
},
{
label: '上架',
value: 3,
},
],
},
defaultValue: 1,
formItemClass: 'col-span-12',
fieldName: 'status',
label: '商品状态',
rules: 'required',
},
{
component: 'Textarea',
componentProps: {
placeholder: '请输入器械功能描述',
},
fieldName: 'function',
label: '器械功能',
},
{
component: 'Avatar',
fieldName: 'instruction',
label: '产品说明书',
formItemClass: 'col-span-6',
},
{
component: 'UploadImageSortable',
fieldName: 'introduction_images',
label: '商品介绍图',
formItemClass: 'col-span-12',
componentProps: {
maxCount: 20,
multiple: true,
},
},
],
showDefaultActions: false,
};
export const uploadExcelProps: VbenFormProps = {
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [
{
component: 'Upload',
componentProps: {
placeholder: '请选择',
multiple: true,
},
fieldName: 'frequency_id',
formItemClass: 'col-span-12',
label: '导入excel',
rules: 'file',
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,26 @@
import type { VbenFormProps } from '#/adapter/form';
export const formOptions: VbenFormProps = {
collapsed: false,
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: '输入名称',
},
defaultValue: '',
fieldName: 'name',
label: '医疗器械名称',
},
],
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
submitOnChange: true,
submitOnEnter: false,
};

View File

@@ -0,0 +1,93 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getMedicalDeviceList } from '../api';
interface RowType {
id: string;
name: string;
logo: string;
introduce: string;
created_at: string;
zone_id: number;
zone_name: string;
category_id: number;
category_name: string;
}
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: '',
},
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'drug_name', align: 'left', title: '器械名称' },
{ field: 'pinyin_simple', title: '拼音' },
{
field: 'zone_name',
title: '分区',
width: 100,
slots: { default: 'zone' },
},
{
field: 'category_name',
title: '分类',
width: 120,
slots: { default: 'category' },
},
{ field: 'supplier.name', title: '供应商', slots: { default: 'supplier' } },
{
field: 'image',
align: 'left',
title: '产品图片',
slots: { default: 'image' },
width: 130,
},
{ field: 'function', title: '器械功能' },
{ field: 'specification', title: '规格型号' },
{ field: 'status', title: '状态', slots: { default: 'status' } },
{ field: 'created_at', title: '发布时间' },
{ type: 'html', title: '操作', width: 230, slots: { default: 'action' } },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getMedicalDeviceList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
height: 'auto',
border: false,
toolbarConfig: {
// @ts-ignore
search: true,
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons',
},
custom: {
icon: 'vxe-icon-menu',
},
},
showOverflow: false,
};

View File

@@ -0,0 +1,196 @@
<script lang="ts" setup>
import type { VxeGridListeners } from '#/adapter/vxe-table';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, Image, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import { downloadByData } from '#/util/tool';
import { deleteMedicalDevice, exportMedicalDeviceApi } from './api';
import FormModalDemo from './components/modal.vue';
import ExcelUpload from './components/ExcelUpload.vue';
import CategoryModal from './components/CategoryModal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
const hasTopTableDropDownActions = ref(false);
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
checkboxAll() {
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents,
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
const [ExcelUploadModal, ExcelUploadModalApi] = useVbenModal({
connectedComponent: ExcelUpload,
});
const [CategoryModalComp, categoryModalApi] = useVbenModal({
connectedComponent: CategoryModal,
});
const openCategoryModal = (row: any) => {
categoryModalApi.setData({
row,
gridApi,
});
categoryModalApi.open();
};
const showModal = (data = {}, isUpdate = false) => {
formModalApi.setData({
values: data,
update: isUpdate,
gridApi,
});
formModalApi.open();
};
const deleteApi = (row: any) => {
let ids = [];
if (row) {
ids.push(row);
} else {
ids = gridApi.grid.getCheckboxRecords().map((item) => item.id);
}
deleteMedicalDevice({ ids }).then(() => {
message.success('删除成功!');
gridApi.reload();
});
};
const passApplication = () => {
exportMedicalDeviceApi().then((res) => {
downloadByData(res.data, '萧康云医-医疗器械导出.xlsx');
message.success('导出成功!');
});
};
const openExcelUploadModal = () => {
ExcelUploadModalApi.setData({
gridApi,
});
ExcelUploadModalApi.open();
};
</script>
<template>
<Page auto-content-height title="医疗器械管理">
<ExcelUploadModal />
<FormModal />
<CategoryModalComp />
<Grid>
<template #toolbar-buttons>
<TableAction
:flex="false"
:actions="[
{
label: '新增',
type: 'primary',
icon: 'ant-design:plus-outlined',
onClick: showModal.bind(null),
},
{
label: '导入',
type: 'primary',
icon: 'ix:export-check',
onClick: openExcelUploadModal.bind(null),
},
{
label: '导出',
type: 'primary',
icon: 'mingcute:download-3-fill',
onClick: passApplication.bind(null),
},
]"
:drop-down-actions="[
{
label: '删除',
icon: 'ant-design:delete-outlined',
ifShow: hasTopTableDropDownActions,
popConfirm: {
title: '确定删除吗',
confirm: deleteApi.bind(null, false),
},
},
]"
>
<template #more>
<Button style="margin-left: 16px">
批量操作
<Icon icon="ant-design:down-outlined" />
</Button>
</template>
</TableAction>
</template>
<template #image="{ row }">
<Image :src="row.image" height="30" width="30" />
</template>
<template #supplier="{ row }">
{{ row.supplier?.name || row.source }}
</template>
<template #zone="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.zone_name || '未设置' }}
</a>
</template>
<template #category="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.category_name || '未设置' }}
</a>
</template>
<template #status="{ row }">
<Tag :color="row.status_color">{{ row.status_txt }}</Tag>
</template>
<template #toolbar-tools></template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '编辑',
type: 'link',
icon: 'uil:edit',
size: 'small',
onClick: showModal.bind(null, row, true),
},
{
label: '删除',
type: 'link',
icon: 'ant-design:delete-outlined',
size: 'small',
popConfirm: {
title: '确定删除吗?',
confirm: deleteApi.bind(null, row.id),
},
},
]"
:drop-down-actions="[]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,78 @@
import { requestClient } from '#/api/request';
const prefix = 'non-drug/';
/**
* 分页查询非药品列表
* @param data
*/
export async function getNonDrugList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
/**
* 获取非药品下拉选项
* @param data
*/
export async function getNonDrugOption(data: any) {
return requestClient.get<any>(`${prefix}option`, { params: data });
}
/**
* 获取非药品详情
* @param id
*/
export async function getNonDrugInfo(id: number) {
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
}
/**
* 导出非药品
*/
export async function exportNonDrugApi() {
return requestClient.download(`${prefix}export`);
}
/**
* 新增非药品
* @param data
*/
export async function createNonDrug(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}create`, data);
}
/**
* 编辑非药品
* @param data
*/
export async function updateNonDrug(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update`, data);
}
/**
* 删除非药品
* @param data
*/
export async function deleteNonDrug(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}delete`, data);
}
/**
* 导入非药品
* @param data
*/
export async function importNonDrugApi(data: Record<string, any>) {
return requestClient.upload(`${prefix}import`, data);
}
/**
* 更新非药品分区分类
* @param data
*/
export async function updateZoneCategory(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update-zone-category`, data);
}

View File

@@ -0,0 +1,138 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Form, FormItem, Input, message, Select, TreeSelect } from 'ant-design-vue';
import { getDrugCategoriesTree, getZoneOptions } from '#/views/business/product/drug-categories/api';
import { updateZoneCategory } from '../api';
const gridApi = ref();
const currentRow = ref<Record<string, any>>({});
// 表单数据
const formData = ref({
id: null as number | null,
drug_name: '',
zone_id: undefined as number | undefined,
category_id: undefined as number | undefined,
});
// 选项数据
const zoneOptions = ref<any[]>([]);
const categoryTreeData = ref<any[]>([]);
const categoryLoading = ref(false);
// 加载分区选项
async function loadZoneOptions() {
const res = await getZoneOptions();
zoneOptions.value = res || [];
}
// 加载分类树(根据 zone_id
async function loadCategoryTree(zoneId: number | undefined) {
if (!zoneId) {
categoryTreeData.value = [];
return;
}
categoryLoading.value = true;
try {
const res = await getDrugCategoriesTree({ zone_id: zoneId });
categoryTreeData.value = res || [];
} finally {
categoryLoading.value = false;
}
}
// 监听分区变化,动态加载分类
watch(
() => formData.value.zone_id,
async (newZoneId, oldZoneId) => {
// 只有在用户手动切换分区时才清空分类(排除初始化)
if (oldZoneId !== undefined) {
formData.value.category_id = undefined;
}
await loadCategoryTree(newZoneId);
}
);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
async onConfirm() {
modalApi.setState({ loading: true, confirmLoading: true });
try {
await updateZoneCategory({
id: formData.value.id!,
zone_id: formData.value.zone_id || null,
category_id: formData.value.category_id || null,
});
message.success('设置成功');
gridApi.value?.reload();
modalApi.close();
} finally {
modalApi.setState({ loading: false, confirmLoading: false });
}
},
async onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = modalApi.getData<Record<string, any>>();
gridApi.value = data?.gridApi;
currentRow.value = data?.row || {};
// 初始化表单数据
formData.value = {
id: currentRow.value.id,
drug_name: currentRow.value.drug_name,
zone_id: currentRow.value.zone_id || undefined,
category_id: currentRow.value.category_id || undefined,
};
// 加载分区选项
await loadZoneOptions();
// 如果有初始 zone_id加载对应分类
if (formData.value.zone_id) {
await loadCategoryTree(formData.value.zone_id);
}
}
},
});
</script>
<template>
<Modal title="分区分类设置" class="w-[450px]">
<Form :model="formData" layout="horizontal" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<FormItem label="产品名称">
<Input v-model:value="formData.drug_name" disabled />
</FormItem>
<FormItem label="所属分区">
<Select
v-model:value="formData.zone_id"
:options="zoneOptions"
:field-names="{ label: 'title', value: 'id' }"
placeholder="请选择分区"
allow-clear
/>
</FormItem>
<FormItem label="所属分类">
<TreeSelect
v-model:value="formData.category_id"
:tree-data="categoryTreeData"
:field-names="{ label: 'category_name', value: 'id', children: 'children' }"
:placeholder="formData.zone_id ? '请选择分类' : '请先选择分区'"
:loading="categoryLoading"
:disabled="!formData.zone_id"
allow-clear
tree-default-expand-all
/>
</FormItem>
</Form>
</Modal>
</template>

View File

@@ -0,0 +1,167 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, UploadDragger, type UploadFile } from 'ant-design-vue';
import { importNonDrugApi } from '../api';
const gridApi = ref();
const fileList = ref<UploadFile[]>([]);
const selectedFile = ref<File | null>(null);
const isFileSelected = ref(false);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
selectedFile.value = null;
isFileSelected.value = false;
fileList.value = [];
modalApi.close();
},
onConfirm: async () => {
if (!selectedFile.value) {
message.warning('请先选择要上传的文件');
return;
}
modalApi.setState({ loading: true, confirmLoading: true });
try {
await importNonDrugApi({
file: selectedFile.value,
});
message.success('导入成功');
gridApi.value?.reload();
selectedFile.value = null;
isFileSelected.value = false;
fileList.value = [];
modalApi.close();
} catch {
message.error('导入失败');
} finally {
modalApi.setState({ loading: false, confirmLoading: false });
}
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
selectedFile.value = null;
isFileSelected.value = false;
fileList.value = [];
},
});
const handleChange = (info: { file: UploadFile }) => {
const { file } = info;
if (file.status === 'removed') {
selectedFile.value = null;
isFileSelected.value = false;
return;
}
if (file) {
selectedFile.value = file;
isFileSelected.value = true;
message.success('文件已选择,请点击确认按钮上传');
}
};
</script>
<template>
<Modal class="w-[30%]" title="上传Excel">
<div
v-if="isFileSelected"
class="mb-4 rounded border border-green-200 bg-green-50 p-3"
>
<p class="text-sm text-green-700">已选择文件{{ selectedFile?.name }}</p>
<p class="mt-1 text-xs text-green-600">
文件已准备就绪点击确认按钮开始上传
</p>
</div>
<UploadDragger
v-model:file-list="fileList"
:before-upload="() => false"
:max-count="1"
:on-change="handleChange"
accept=".xlsx,.xls"
name="file"
>
<p class="flex justify-center">
<svg
height="64"
viewBox="0 0 32 32"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient
id="vscodeIconsFileTypeExcel0"
gradientTransform="translate(0 2100)"
gradientUnits="userSpaceOnUse"
x1="4.494"
x2="13.832"
y1="-2092.086"
y2="-2075.914"
>
<stop offset="0" stop-color="#18884f" />
<stop offset=".5" stop-color="#117e43" />
<stop offset="1" stop-color="#0b6631" />
</linearGradient>
</defs>
<path
d="M19.581 15.35L8.512 13.4v14.409A1.19 1.19 0 0 0 9.705 29h19.1A1.19 1.19 0 0 0 30 27.809V22.5Z"
fill="#185c37"
/>
<path
d="M19.581 3H9.705a1.19 1.19 0 0 0-1.193 1.191V9.5L19.581 16l5.861 1.95L30 16V9.5Z"
fill="#21a366"
/>
<path d="M8.512 9.5h11.069V16H8.512Z" fill="#107c41" />
<path
d="M16.434 8.2H8.512v16.25h7.922a1.2 1.2 0 0 0 1.194-1.191V9.391A1.2 1.2 0 0 0 16.434 8.2"
opacity="0.1"
/>
<path
d="M15.783 8.85H8.512V25.1h7.271a1.2 1.2 0 0 0 1.194-1.191V10.041a1.2 1.2 0 0 0-1.194-1.191"
opacity="0.2"
/>
<path
d="M15.783 8.85H8.512V23.8h7.271a1.2 1.2 0 0 0 1.194-1.191V10.041a1.2 1.2 0 0 0-1.194-1.191"
opacity="0.2"
/>
<path
d="M15.132 8.85h-6.62V23.8h6.62a1.2 1.2 0 0 0 1.194-1.191V10.041a1.2 1.2 0 0 0-1.194-1.191"
opacity="0.2"
/>
<path
d="M3.194 8.85h11.938a1.193 1.193 0 0 1 1.194 1.191v11.918a1.193 1.193 0 0 1-1.194 1.191H3.194A1.19 1.19 0 0 1 2 21.959V10.041A1.19 1.19 0 0 1 3.194 8.85"
fill="url(#vscodeIconsFileTypeExcel0)"
/>
<path
d="m5.7 19.873l2.511-3.884l-2.3-3.862h1.847L9.013 14.6c.116.234.2.408.238.524h.017q.123-.281.26-.546l1.342-2.447h1.7l-2.359 3.84l2.419 3.905h-1.809l-1.45-2.711A2.4 2.4 0 0 1 9.2 16.8h-.024a1.7 1.7 0 0 1-.168.351l-1.493 2.722Z"
fill="#fff"
/>
<path
d="M28.806 3h-9.225v6.5H30V4.191A1.19 1.19 0 0 0 28.806 3"
fill="#33c481"
/>
<path d="M19.581 16H30v6.5H19.581Z" fill="#107c41" />
</svg>
</p>
<p class="ant-upload-text">点击或拖动文件到此区域进行选择</p>
<p class="ant-upload-hint">
支持单个上传xlsx格式文件选择文件后需要点击确认按钮进行上传
</p>
</UploadDragger>
</Modal>
</template>

View File

@@ -0,0 +1,107 @@
<script lang="ts" setup>
import {ref} from 'vue';
import {useVbenModal} from '@vben/common-ui';
import {message} from 'ant-design-vue';
import {useVbenForm} from '#/adapter/form';
import {getDrugUseList} from '#/views/doctor/doctor-reception/api';
import {createNonDrug, getNonDrugInfo, updateNonDrug,} from '../api';
import {modalFormProps} from '../config/form';
const drugUnit = ref([]);
getDrugUseList().then((res) => {
drugUnit.value = res.drug_unit.map((item) => {
return {
label: item.name,
value: item.id,
};
});
return res;
});
const isUpdate = ref(false);
const gridApi = ref();
const [Form, formApi] = useVbenForm(modalFormProps);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
formApi.validate().then(async (e: any) => {
if (e.valid) {
const values = await formApi.getValues();
modalApi.setState({ loading: true, confirmLoading: true });
const submitApi = isUpdate.value
? updateNonDrug
: createNonDrug;
submitApi(values)
.then(() => {
message.success('保存成功');
gridApi.value?.reload();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
}
});
await formApi.validateAndSubmitForm();
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (isOpen) {
formApi.updateSchema([
{
componentProps: {
options: drugUnit.value,
},
fieldName: 'unit_id',
},
]);
const { values, update } = modalApi.getData<Record<string, any>>();
if (values) {
isUpdate.value = update;
if (update && values.id) {
getNonDrugInfo(values.id)
.then((res: any) => {
if (res) {
formApi.setValues({
...values,
introduction_images: res.introduction_images,
});
} else {
formApi.setValues(values);
}
})
.catch((error) => {
console.error('获取商品详情失败:', error);
formApi.setValues(values);
});
} else {
formApi.setValues(values);
}
}
}
},
});
</script>
<template>
<Modal
:title="`${isUpdate === true ? '编辑' : '新增'}非药品`"
class="w-[30%]"
>
<Form />
</Modal>
</template>

View File

@@ -0,0 +1,240 @@
import type {VbenFormProps} from '#/adapter/form';
import {getSupplierOption} from '#/views/system/supplier/api';
import {getDrugCategoriesTree, getZoneOptions} from '#/views/business/product/drug-categories/api';
export const modalFormProps: VbenFormProps = {
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
fieldName: 'id',
label: 'ID',
formItemClass: 'col-span-6',
dependencies: {
show: false,
triggerFields: ['id'],
},
},
{
component: 'VbenInput',
fieldName: 'showSelect',
label: 'ID',
formItemClass: 'col-span-6',
dependencies: {
show: false,
triggerFields: ['id'],
},
},
{
component: 'Avatar',
fieldName: 'image',
label: '产品图片',
rules: 'required',
formItemClass: 'col-span-6',
},
{
component: 'ApiSelect',
componentProps: {
allowClear: true,
showSearch: true,
filterOption: (input: string, option: any) => {
return option?.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
},
afterFetch: (data: { id: number; name: string }[]) => {
return data.map((item: any) => ({
label: item.name,
value: item.id,
}));
},
api: getSupplierOption,
placeholder: '请选择',
},
fieldName: 'supplier_id',
formItemClass: 'col-span-6',
label: '所属供应商',
rules: 'required',
},
{
component: 'ApiSelect',
componentProps: {
api: getZoneOptions,
labelField: 'title',
valueField: 'id',
placeholder: '请选择分区',
allowClear: true,
},
fieldName: 'zone_id',
formItemClass: 'col-span-6',
label: '所属分区',
},
{
component: 'ApiTreeSelect',
componentProps: {
api: getDrugCategoriesTree,
childrenField: 'children',
labelField: 'category_name',
valueField: 'id',
placeholder: '请先选择分区',
allowClear: true,
treeDefaultExpandAll: true,
immediate: false, // 首次不立即加载
alwaysLoad: true, // 每次展开都重新加载
},
fieldName: 'category_id',
formItemClass: 'col-span-6',
label: '所属分类',
dependencies: {
componentProps(values, formApi) {
return {
// 使用 beforeFetch 在请求发出前动态获取最新的 zone_id
beforeFetch: async () => {
const currentValues = await formApi.getValues();
return { zone_id: currentValues.zone_id };
},
placeholder: values.zone_id ? '请选择分类' : '请先选择分区',
};
},
trigger(values, formApi) {
// zone_id 变化时清空分类选择
formApi.setFieldValue('category_id', undefined);
},
triggerFields: ['zone_id'],
},
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入产品名称',
},
fieldName: 'drug_name',
formItemClass: 'col-span-6',
label: '产品名称',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入产品别名',
},
fieldName: 'drug_alias',
formItemClass: 'col-span-6',
label: '产品别名',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入产品编号',
},
fieldName: 'drug_number',
formItemClass: 'col-span-6',
label: '产品编号',
rules: 'required',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入规格',
},
fieldName: 'specification',
formItemClass: 'col-span-6',
label: '规格',
rules: 'required',
},
{
component: 'VbenSelect',
componentProps: {
options: [],
placeholder: '请选择',
},
fieldName: 'unit_id',
formItemClass: 'col-span-6',
label: '单位',
rules: 'required',
},
{
component: 'RadioGroup',
componentProps: {
placeholder: '请选择',
options: [
{
label: '草稿',
value: 1,
},
{
label: '下架',
value: 2,
},
{
label: '上架',
value: 3,
},
],
},
defaultValue: 1,
formItemClass: 'col-span-12',
fieldName: 'status',
label: '商品状态',
rules: 'required',
},
{
component: 'Textarea',
componentProps: {
placeholder: '请输入产品功能描述',
},
fieldName: 'function',
label: '产品功能',
},
{
component: 'Avatar',
fieldName: 'instruction',
label: '产品说明书',
formItemClass: 'col-span-6',
},
{
component: 'UploadImageSortable',
fieldName: 'introduction_images',
label: '商品介绍图',
formItemClass: 'col-span-12',
componentProps: {
maxCount: 20,
multiple: true,
},
},
],
showDefaultActions: false,
};
export const uploadExcelProps: VbenFormProps = {
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [
{
component: 'Upload',
componentProps: {
placeholder: '请选择',
multiple: true,
},
fieldName: 'frequency_id',
formItemClass: 'col-span-12',
label: '导入excel',
rules: 'file',
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,26 @@
import type { VbenFormProps } from '#/adapter/form';
export const formOptions: VbenFormProps = {
collapsed: false,
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: '输入名称',
},
defaultValue: '',
fieldName: 'name',
label: '非药品名称',
},
],
showCollapseButton: true,
submitButtonOptions: {
content: '查询',
},
submitOnChange: true,
submitOnEnter: false,
};

View File

@@ -0,0 +1,93 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getNonDrugList } from '../api';
interface RowType {
id: string;
name: string;
logo: string;
introduce: string;
created_at: string;
zone_id: number;
zone_name: string;
category_id: number;
category_name: string;
}
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: '',
},
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'drug_name', align: 'left', title: '产品名称' },
{ field: 'pinyin_simple', title: '拼音' },
{
field: 'zone_name',
title: '分区',
width: 100,
slots: { default: 'zone' },
},
{
field: 'category_name',
title: '分类',
width: 120,
slots: { default: 'category' },
},
{ field: 'supplier.name', title: '供应商', slots: { default: 'supplier' } },
{
field: 'image',
align: 'left',
title: '产品图片',
slots: { default: 'image' },
width: 130,
},
{ field: 'function', title: '产品功能' },
{ field: 'specification', title: '规格' },
{ field: 'status', title: '状态', slots: { default: 'status' } },
{ field: 'created_at', title: '发布时间' },
{ type: 'html', title: '操作', width: 230, slots: { default: 'action' } },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getNonDrugList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
height: 'auto',
border: false,
toolbarConfig: {
// @ts-ignore
search: true,
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons',
},
custom: {
icon: 'vxe-icon-menu',
},
},
showOverflow: false,
};

View File

@@ -0,0 +1,196 @@
<script lang="ts" setup>
import type { VxeGridListeners } from '#/adapter/vxe-table';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, Image, message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import { downloadByData } from '#/util/tool';
import { deleteNonDrug, exportNonDrugApi } from './api';
import FormModalDemo from './components/modal.vue';
import ExcelUpload from './components/ExcelUpload.vue';
import CategoryModal from './components/CategoryModal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
const hasTopTableDropDownActions = ref(false);
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
checkboxAll() {
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents,
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
const [ExcelUploadModal, ExcelUploadModalApi] = useVbenModal({
connectedComponent: ExcelUpload,
});
const [CategoryModalComp, categoryModalApi] = useVbenModal({
connectedComponent: CategoryModal,
});
const openCategoryModal = (row: any) => {
categoryModalApi.setData({
row,
gridApi,
});
categoryModalApi.open();
};
const showModal = (data = {}, isUpdate = false) => {
formModalApi.setData({
values: data,
update: isUpdate,
gridApi,
});
formModalApi.open();
};
const deleteApi = (row: any) => {
let ids = [];
if (row) {
ids.push(row);
} else {
ids = gridApi.grid.getCheckboxRecords().map((item) => item.id);
}
deleteNonDrug({ ids }).then(() => {
message.success('删除成功!');
gridApi.reload();
});
};
const passApplication = () => {
exportNonDrugApi().then((res) => {
downloadByData(res.data, '萧康云医-非药品导出.xlsx');
message.success('导出成功!');
});
};
const openExcelUploadModal = () => {
ExcelUploadModalApi.setData({
gridApi,
});
ExcelUploadModalApi.open();
};
</script>
<template>
<Page auto-content-height title="非药品管理">
<ExcelUploadModal />
<FormModal />
<CategoryModalComp />
<Grid>
<template #toolbar-buttons>
<TableAction
:flex="false"
:actions="[
{
label: '新增',
type: 'primary',
icon: 'ant-design:plus-outlined',
onClick: showModal.bind(null),
},
{
label: '导入',
type: 'primary',
icon: 'ix:export-check',
onClick: openExcelUploadModal.bind(null),
},
{
label: '导出',
type: 'primary',
icon: 'mingcute:download-3-fill',
onClick: passApplication.bind(null),
},
]"
:drop-down-actions="[
{
label: '删除',
icon: 'ant-design:delete-outlined',
ifShow: hasTopTableDropDownActions,
popConfirm: {
title: '确定删除吗',
confirm: deleteApi.bind(null, false),
},
},
]"
>
<template #more>
<Button style="margin-left: 16px">
批量操作
<Icon icon="ant-design:down-outlined" />
</Button>
</template>
</TableAction>
</template>
<template #image="{ row }">
<Image :src="row.image" height="30" width="30" />
</template>
<template #supplier="{ row }">
{{ row.supplier?.name || row.source }}
</template>
<template #zone="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.zone_name || '未设置' }}
</a>
</template>
<template #category="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.category_name || '未设置' }}
</a>
</template>
<template #status="{ row }">
<Tag :color="row.status_color">{{ row.status_txt }}</Tag>
</template>
<template #toolbar-tools></template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '编辑',
type: 'link',
icon: 'uil:edit',
size: 'small',
onClick: showModal.bind(null, row, true),
},
{
label: '删除',
type: 'link',
icon: 'ant-design:delete-outlined',
size: 'small',
popConfirm: {
title: '确定删除吗?',
confirm: deleteApi.bind(null, row.id),
},
},
]"
:drop-down-actions="[]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -47,3 +47,11 @@ export async function updateServicePack(data: Record<string, any>) {
export async function deleteServicePack(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}delete`, data);
}
/**
* 更新服务包分区分类
* @param data
*/
export async function updateZoneCategory(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update-zone-category`, data);
}

View File

@@ -0,0 +1,138 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Form, FormItem, Input, message, Select, TreeSelect } from 'ant-design-vue';
import { getDrugCategoriesTree, getZoneOptions } from '#/views/business/product/drug-categories/api';
import { updateZoneCategory } from '../api';
const gridApi = ref();
const currentRow = ref<Record<string, any>>({});
// 表单数据
const formData = ref({
id: null as number | null,
drug_name: '',
zone_id: undefined as number | undefined,
category_id: undefined as number | undefined,
});
// 选项数据
const zoneOptions = ref<any[]>([]);
const categoryTreeData = ref<any[]>([]);
const categoryLoading = ref(false);
// 加载分区选项
async function loadZoneOptions() {
const res = await getZoneOptions();
zoneOptions.value = res || [];
}
// 加载分类树(根据 zone_id
async function loadCategoryTree(zoneId: number | undefined) {
if (!zoneId) {
categoryTreeData.value = [];
return;
}
categoryLoading.value = true;
try {
const res = await getDrugCategoriesTree({ zone_id: zoneId });
categoryTreeData.value = res || [];
} finally {
categoryLoading.value = false;
}
}
// 监听分区变化,动态加载分类
watch(
() => formData.value.zone_id,
async (newZoneId, oldZoneId) => {
// 只有在用户手动切换分区时才清空分类(排除初始化)
if (oldZoneId !== undefined) {
formData.value.category_id = undefined;
}
await loadCategoryTree(newZoneId);
}
);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
async onConfirm() {
modalApi.setState({ loading: true, confirmLoading: true });
try {
await updateZoneCategory({
id: formData.value.id!,
zone_id: formData.value.zone_id || null,
category_id: formData.value.category_id || null,
});
message.success('设置成功');
gridApi.value?.reload();
modalApi.close();
} finally {
modalApi.setState({ loading: false, confirmLoading: false });
}
},
async onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = modalApi.getData<Record<string, any>>();
gridApi.value = data?.gridApi;
currentRow.value = data?.row || {};
// 初始化表单数据
formData.value = {
id: currentRow.value.id,
drug_name: currentRow.value.drug_name,
zone_id: currentRow.value.zone_id || undefined,
category_id: currentRow.value.category_id || undefined,
};
// 加载分区选项
await loadZoneOptions();
// 如果有初始 zone_id加载对应分类
if (formData.value.zone_id) {
await loadCategoryTree(formData.value.zone_id);
}
}
},
});
</script>
<template>
<Modal title="分区分类设置" class="w-[450px]">
<Form :model="formData" layout="horizontal" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<FormItem label="服务包名称">
<Input v-model:value="formData.drug_name" disabled />
</FormItem>
<FormItem label="所属分区">
<Select
v-model:value="formData.zone_id"
:options="zoneOptions"
:field-names="{ label: 'title', value: 'id' }"
placeholder="请选择分区"
allow-clear
/>
</FormItem>
<FormItem label="所属分类">
<TreeSelect
v-model:value="formData.category_id"
:tree-data="categoryTreeData"
:field-names="{ label: 'category_name', value: 'id', children: 'children' }"
:placeholder="formData.zone_id ? '请选择分类' : '请先选择分区'"
:loading="categoryLoading"
:disabled="!formData.zone_id"
allow-clear
tree-default-expand-all
/>
</FormItem>
</Form>
</Modal>
</template>

View File

@@ -3,6 +3,7 @@ import type { VbenFormProps } from '#/adapter/form';
import { useUserStore } from '@vben/stores';
import { getSupplierOption } from '#/views/system/supplier/api';
import { getDrugCategoriesTree, getZoneOptions } from '#/views/business/product/drug-categories/api';
const userStore = useUserStore();
@@ -75,6 +76,53 @@ export const modalFormProps: VbenFormProps = {
label: '所属供应商',
rules: 'required',
},
{
component: 'ApiSelect',
componentProps: {
api: getZoneOptions,
labelField: 'title',
valueField: 'id',
placeholder: '请选择分区',
allowClear: true,
},
fieldName: 'zone_id',
formItemClass: 'col-span-6',
label: '所属分区',
},
{
component: 'ApiTreeSelect',
componentProps: {
api: getDrugCategoriesTree,
childrenField: 'children',
labelField: 'category_name',
valueField: 'id',
placeholder: '请先选择分区',
allowClear: true,
treeDefaultExpandAll: true,
immediate: false, // 首次不立即加载
alwaysLoad: true, // 每次展开都重新加载
},
fieldName: 'category_id',
formItemClass: 'col-span-6',
label: '所属分类',
dependencies: {
componentProps(values, formApi) {
return {
// 使用 beforeFetch 在请求发出前动态获取最新的 zone_id
beforeFetch: async () => {
const currentValues = await formApi.getValues();
return { zone_id: currentValues.zone_id };
},
placeholder: values.zone_id ? '请选择分类' : '请先选择分区',
};
},
trigger(values, formApi) {
// zone_id 变化时清空分类选择
formApi.setFieldValue('category_id', undefined);
},
triggerFields: ['zone_id'],
},
},
{
component: 'VbenInput',
componentProps: {

View File

@@ -8,6 +8,10 @@ interface RowType {
logo: string;
introduce: string;
created_at: string;
zone_id: number;
zone_name: string;
category_id: number;
category_name: string;
}
export const gridOptions: VxeGridProps<RowType> = {
@@ -26,6 +30,18 @@ export const gridOptions: VxeGridProps<RowType> = {
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'drug_name', align: 'left', title: '服务包名称' },
{ field: 'pinyin_simple', title: '拼音' },
{
field: 'zone_name',
title: '分区',
width: 100,
slots: { default: 'zone' },
},
{
field: 'category_name',
title: '分类',
width: 120,
slots: { default: 'category' },
},
{ field: 'supplier.name', title: '供应商' },
{
field: 'image',

View File

@@ -12,6 +12,7 @@ import { TableAction } from '#/components/table-action';
import { deleteServicePack } from './api';
import FormModalDemo from './components/modal.vue';
import ZoneCategoryModal from './components/ZoneCategoryModal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
@@ -40,6 +41,18 @@ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
const [ZoneCategoryModalComp, zoneCategoryModalApi] = useVbenModal({
connectedComponent: ZoneCategoryModal,
});
const openZoneCategoryModal = (row: any) => {
zoneCategoryModalApi.setData({
row,
gridApi,
});
zoneCategoryModalApi.open();
};
const showModal = (data = {}, isUpdate = false) => {
formModalApi.setData({
// 表单值
@@ -67,6 +80,7 @@ const deleteApi = (row: any) => {
<template>
<Page auto-content-height title="产品服务包管理">
<FormModal />
<ZoneCategoryModalComp />
<Grid>
<template #toolbar-buttons>
<TableAction
@@ -100,6 +114,16 @@ const deleteApi = (row: any) => {
</template>
</TableAction>
</template>
<template #zone="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openZoneCategoryModal(row)">
{{ row.zone_name || '未设置' }}
</a>
</template>
<template #category="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openZoneCategoryModal(row)">
{{ row.category_name || '未设置' }}
</a>
</template>
<template #image="{ row }">
<Image :src="row.image" height="30" width="30" />
</template>

View File

@@ -64,3 +64,11 @@ export async function deleteWesternMedicine(data: Record<string, any>) {
export async function importWesternMedicineApi(data: Record<string, any>) {
return requestClient.upload(`${prefix}import`, data);
}
/**
* 更新药品分区分类
* @param data
*/
export async function updateZoneCategory(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update-zone-category`, data);
}

View File

@@ -0,0 +1,138 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Form, FormItem, Input, message, Select, TreeSelect } from 'ant-design-vue';
import { getDrugCategoriesTree, getZoneOptions } from '#/views/business/product/drug-categories/api';
import { updateZoneCategory } from '../api';
const gridApi = ref();
const currentRow = ref<Record<string, any>>({});
// 表单数据
const formData = ref({
id: null as number | null,
drug_name: '',
zone_id: undefined as number | undefined,
category_id: undefined as number | undefined,
});
// 选项数据
const zoneOptions = ref<any[]>([]);
const categoryTreeData = ref<any[]>([]);
const categoryLoading = ref(false);
// 加载分区选项
async function loadZoneOptions() {
const res = await getZoneOptions();
zoneOptions.value = res || [];
}
// 加载分类树(根据 zone_id
async function loadCategoryTree(zoneId: number | undefined) {
if (!zoneId) {
categoryTreeData.value = [];
return;
}
categoryLoading.value = true;
try {
const res = await getDrugCategoriesTree({ zone_id: zoneId });
categoryTreeData.value = res || [];
} finally {
categoryLoading.value = false;
}
}
// 监听分区变化,动态加载分类
watch(
() => formData.value.zone_id,
async (newZoneId, oldZoneId) => {
// 只有在用户手动切换分区时才清空分类(排除初始化)
if (oldZoneId !== undefined) {
formData.value.category_id = undefined;
}
await loadCategoryTree(newZoneId);
}
);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
async onConfirm() {
modalApi.setState({ loading: true, confirmLoading: true });
try {
await updateZoneCategory({
id: formData.value.id!,
zone_id: formData.value.zone_id || null,
category_id: formData.value.category_id || null,
});
message.success('设置成功');
gridApi.value?.reload();
modalApi.close();
} finally {
modalApi.setState({ loading: false, confirmLoading: false });
}
},
async onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = modalApi.getData<Record<string, any>>();
gridApi.value = data?.gridApi;
currentRow.value = data?.row || {};
// 初始化表单数据
formData.value = {
id: currentRow.value.id,
drug_name: currentRow.value.drug_name,
zone_id: currentRow.value.zone_id || undefined,
category_id: currentRow.value.category_id || undefined,
};
// 加载分区选项
await loadZoneOptions();
// 如果有初始 zone_id加载对应分类
if (formData.value.zone_id) {
await loadCategoryTree(formData.value.zone_id);
}
}
},
});
</script>
<template>
<Modal title="分区分类设置" class="w-[450px]">
<Form :model="formData" layout="horizontal" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<FormItem label="药品名称">
<Input v-model:value="formData.drug_name" disabled />
</FormItem>
<FormItem label="所属分区">
<Select
v-model:value="formData.zone_id"
:options="zoneOptions"
:field-names="{ label: 'title', value: 'id' }"
placeholder="请选择分区"
allow-clear
/>
</FormItem>
<FormItem label="所属分类">
<TreeSelect
v-model:value="formData.category_id"
:tree-data="categoryTreeData"
:field-names="{ label: 'category_name', value: 'id', children: 'children' }"
:placeholder="formData.zone_id ? '请选择分类' : '请先选择分区'"
:loading="categoryLoading"
:disabled="!formData.zone_id"
allow-clear
tree-default-expand-all
/>
</FormItem>
</Form>
</Modal>
</template>

View File

@@ -1,6 +1,7 @@
import type {VbenFormProps} from '#/adapter/form';
import {getSupplierOption} from '#/views/system/supplier/api';
import {getDrugCategoriesTree, getZoneOptions} from '#/views/business/product/drug-categories/api';
export const modalFormProps: VbenFormProps = {
@@ -66,6 +67,53 @@ export const modalFormProps: VbenFormProps = {
label: '所属供应商',
rules: 'required',
},
{
component: 'ApiSelect',
componentProps: {
api: getZoneOptions,
labelField: 'title',
valueField: 'id',
placeholder: '请选择分区',
allowClear: true,
},
fieldName: 'zone_id',
formItemClass: 'col-span-6',
label: '所属分区',
},
{
component: 'ApiTreeSelect',
componentProps: {
api: getDrugCategoriesTree,
childrenField: 'children',
labelField: 'category_name',
valueField: 'id',
placeholder: '请先选择分区',
allowClear: true,
treeDefaultExpandAll: true,
immediate: false, // 首次不立即加载
alwaysLoad: true, // 每次展开都重新加载
},
fieldName: 'category_id',
formItemClass: 'col-span-6',
label: '所属分类',
dependencies: {
componentProps(values, formApi) {
return {
// 使用 beforeFetch 在请求发出前动态获取最新的 zone_id
beforeFetch: async () => {
const currentValues = await formApi.getValues();
return { zone_id: currentValues.zone_id };
},
placeholder: values.zone_id ? '请选择分类' : '请先选择分区',
};
},
trigger(values, formApi) {
// zone_id 变化时清空分类选择
formApi.setFieldValue('category_id', undefined);
},
triggerFields: ['zone_id'],
},
},
{
component: 'VbenInput',
componentProps: {

View File

@@ -8,6 +8,10 @@ interface RowType {
logo: string;
introduce: string;
created_at: string;
zone_id: number;
zone_name: string;
category_id: number;
category_name: string;
}
export const gridOptions: VxeGridProps<RowType> = {
@@ -26,6 +30,18 @@ export const gridOptions: VxeGridProps<RowType> = {
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'drug_name', align: 'left', title: '药品名称' },
{ field: 'pinyin_simple', title: '拼音' },
{
field: 'zone_name',
title: '分区',
width: 100,
slots: { default: 'zone' },
},
{
field: 'category_name',
title: '分类',
width: 120,
slots: { default: 'category' },
},
{ field: 'supplier.name', title: '供应商', slots: { default: 'supplier' } },
{
field: 'image',
@@ -43,6 +59,12 @@ export const gridOptions: VxeGridProps<RowType> = {
},
{ field: 'function', title: '主要功能' },
{ field: 'specification', title: '规格' },
{
field: 'is_otc',
title: '处方药',
width: 100,
slots: { default: 'is_otc' },
},
{ field: 'status', title: '状态', slots: { default: 'status' } },
{ field: 'created_at', title: '发布时间' },
{ type: 'html', title: '操作', width: 230, slots: { default: 'action' } },

View File

@@ -14,6 +14,7 @@ import { downloadByData } from '#/util/tool';
import { deleteWesternMedicine, exportWesternMedicineApi } from './api';
import FormModalDemo from './components/modal.vue';
import ExcelUpload from './components/ExcelUpload.vue';
import CategoryModal from './components/CategoryModal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
@@ -46,6 +47,18 @@ const [ExcelUploadModal, ExcelUploadModalApi] = useVbenModal({
connectedComponent: ExcelUpload,
});
const [CategoryModalComp, categoryModalApi] = useVbenModal({
connectedComponent: CategoryModal,
});
const openCategoryModal = (row: any) => {
categoryModalApi.setData({
row,
gridApi,
});
categoryModalApi.open();
};
const showModal = (data = {}, isUpdate = false) => {
formModalApi.setData({
// 表单值
@@ -92,6 +105,7 @@ const openExcelUploadModal = () => {
<Page auto-content-height title="西(中成)药管理">
<ExcelUploadModal />
<FormModal />
<CategoryModalComp />
<Grid>
<template #toolbar-buttons>
<TableAction
@@ -146,11 +160,26 @@ const openExcelUploadModal = () => {
<template #supplier="{ row }">
{{ row.supplier?.name || row.source }}
</template>
<template #zone="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.zone_name || '未设置' }}
</a>
</template>
<template #category="{ row }">
<a class="text-primary cursor-pointer hover:underline" @click="openCategoryModal(row)">
{{ row.category_name || '未设置' }}
</a>
</template>
<template #instruction="{ row }">
<div class="div" style="width: 120px; height: 120px; overflow: scroll">
<Image :src="row.instruction" height="30" width="30" />
</div>
</template>
<template #is_otc="{ row }">
<Tag :color="row.is_otc === 0 ? 'red' : 'green'">
{{ row.is_otc === 0 ? '处方药' : '非处方药' }}
</Tag>
</template>
<template #status="{ row }">
<Tag :color="row.status_color">{{ row.status_txt }}</Tag>
</template>

View File

@@ -0,0 +1,45 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
const title = ref('确认');
const content = ref('');
const onConfirmCallback = ref<(() => void) | null>(null);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm() {
if (onConfirmCallback.value) {
onConfirmCallback.value();
}
modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = modalApi.getData<{
title?: string;
content: string;
onConfirm: () => void;
}>();
title.value = data?.title || '确认';
content.value = data?.content || '';
onConfirmCallback.value = data?.onConfirm || null;
}
},
});
</script>
<template>
<Modal :title="title" class="w-[400px]">
<div class="py-4 text-center">
{{ content }}
</div>
</Modal>
</template>

View File

@@ -2,6 +2,7 @@
import { computed, ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import {
ContainerOutlined,
@@ -35,6 +36,9 @@ import { usePrescriptionStore } from '#/store/prescription'
const prescriptionStore = usePrescriptionStore();
const { currentDrugs, initializeForModal, updateCurrentDrugs } = prescriptionStore;
// 获取用户信息
const userStore = useUserStore();
// 搜索关键词
const searchKey = ref('');
// 药品类型1-中药2-西药
@@ -57,6 +61,8 @@ const drugUseWay = ref([]);
// 当前选中的药品ID
const selectProductId = ref(0);
const ChatTypeCheck = ref('');
// 缓存前缀(用于区分接诊和在线复诊)
const storagePrefix = ref('');
// 预览图片URL
const previewImage = ref('');
@@ -68,8 +74,11 @@ const setVisible = (value, instruction = '') => {
previewImage.value = instruction;
};
// 本地存储键名
const storageKey = computed(() => `prescriptionData${activePatientId.value}`);
// 本地存储键名(支持前缀区分不同场景)
const storageKey = computed(() => {
const prefix = storagePrefix.value ? `${storagePrefix.value}-` : '';
return `${prefix}prescriptionData_${type.value}_${activePatientId.value}`;
});
// 获取当前患者的药品数据
const getCurrentDrugs = () => {
@@ -97,7 +106,7 @@ const getDrugListByWesternModal = debounce(async (searchText = '') => {
try {
const res = await getProductListDoctorReception({
store_id: 2,
store_id: userStore.userInfo?.store_id || 0,
type: type.value,
name: searchText,
});
@@ -337,18 +346,19 @@ const [Modal, modalApi] = useVbenModal({
currentDrugsWestern.value = data?.getCurrentDrugs;
// 获取参数
const { values, activePatient_id, isChat } = data || {};
const { values, activePatient_id, isChat, storagePrefix: prefix } = data || {};
if (values) {
// 设置缓存前缀
storagePrefix.value = prefix || '';
// 设置药品类型
type.value = values;
// 设置患者ID
activePatientId.value = activePatient_id;
if (isChat === 'chat') {
ChatTypeCheck.value = isChat;
activePatientId.value = `-chat-${activePatient_id.value}`;
// 使用轻量级初始化,不获取患者信息
initializeForModal(activePatient_id.value);
initializeForModal(activePatient_id);
}
// 获取药品列表
getDrugListByWesternModal();

View File

@@ -56,6 +56,8 @@ import RefusalOfTreatmentModal
from "#/views/doctor/doctor-reception/components/RefusalOfTreatmentModal.vue";
// 常用方选择弹窗组件
import CommonPrescriptionModal from './components/CommonPrescriptionModal.vue';
// 确认模态框组件
import ConfirmModal from './components/ConfirmModal.vue';
// 保存常用方API
import {
saveChineseCommonPrescriptionApi,
@@ -601,22 +603,6 @@ watch(
},
);
// 监听tabType.value变化
watch(
() => activeCategory.value,
(newValue) => {
if (newValue === activeCategory.value) {
return;
}
// 清空当前数据
currentDrugs.value = [];
diagnosis.value = '';
medicalAdvice.value = '';
dosage.value = 1;
dayDosage.value = 1;
updateLocalStorage();
},
);
const [WesternDrugModal, WesternDrugModalApi] = useVbenModal({
connectedComponent: WesternModal,
@@ -677,6 +663,11 @@ const [CommonPrescriptionModals, CommonPrescriptionModalApi] = useVbenModal({
connectedComponent: CommonPrescriptionModal,
});
// ==================== 确认模态框 ====================
const [ConfirmModalComponent, confirmModalApi] = useVbenModal({
connectedComponent: ConfirmModal,
});
/**
* 打开常用方选择弹窗
* @description 打开常用方弹窗,选择后自动填充药品到处方中
@@ -704,7 +695,7 @@ async function handleSelectCommonPrescription(data: any, type: number) {
recipes.forEach((recipe: any) => {
const newProduct = {
index_id: recipe.id,
id: recipe.id,
id: recipe.drug_id, // 使用药品主表ID
drug_name: recipe.drug_name,
number: recipe.number || 1,
use_num: recipe.use_time,
@@ -721,28 +712,28 @@ async function handleSelectCommonPrescription(data: any, type: number) {
type: recipe.type,
select_number: 1,
};
// 检查是否已存在
// 检查是否已存在 - 使用药品主表ID比对
const existItem = currentDrugs.value.find(
(item) => item.id === recipe.id,
(item) => item.id === recipe.drug_id,
);
if (!existItem) {
currentDrugs.value.push(newProduct);
}
});
} else {
// 中药/颗粒药处方 - 使用 drug_id 作为唯一标识
// 中药/颗粒药处方 - 使用 drug_id 作为唯一标识药品主表ID
recipes.forEach((recipe: any) => {
const drugId = recipe.drug_id || recipe.id;
const drugId = recipe.drug_id || recipe.id; // 药品主表ID
const newProduct = {
index_id: drugId,
id: drugId,
index_id: recipe.id, // 处方明细ID
id: drugId, // 药品主表ID
drug_name: recipe.drug_name || recipe.name,
number: recipe.number || 1,
price: recipe.price || 0,
way_id: recipe.way_id || 0,
select_number: 1,
};
// 检查是否已存在 - 使用 drug_id 比对
// 检查是否已存在 - 使用药品主表ID比对
const existItem = currentDrugs.value.find(
(item) => item.id === drugId,
);
@@ -900,6 +891,7 @@ const openWesternModal = () => {
values: activeCategory.value,
activePatient_id: activePatient.value?.id,
getCurrentDrugs,
storagePrefix: '', // 接诊页面不加前缀
});
WesternDrugModalApi.open();
};
@@ -959,10 +951,6 @@ function reception() {
const updateReceptionStatus = () => {
receptionStatus.value = 0;
tabType.value = 0;
console.log('这', {
receptionStatus: receptionStatus.value,
tabType: tabType.value
})
}
/**
@@ -1029,7 +1017,7 @@ loadProcessRuleData();
*/
function selectProcessRule(id) {
processRuleId.value = id;
getProcessRuleListByDoctor(id);
loadProcessRuleData(id); // 加载煎法选项
}
/**
@@ -1038,7 +1026,7 @@ function selectProcessRule(id) {
*/
function selectProcessRuleNot(id) {
childProcessRuleId.value = id;
getProcessRuleListByDoctor(0, id);
loadProcessRuleData(0, id); // 加载备注选项
}
/**
@@ -1067,8 +1055,6 @@ function updateChineseNumber() {
updateLocalStorage();
}
// 新药品确认弹窗
const showNewDrugModal = ref(false);
// 二次签名确认弹窗
const doctorSecondSignModal = ref(false);
@@ -1076,14 +1062,19 @@ const doctorSecondSignModal = ref(false);
* 在新的药品数量框失去焦点后弹出新药品弹窗
*/
function newDrugBlur() {
if (newDrugInfo.value.id > 0 && newDrugInfo.value.number > 0) {
showNewDrugModal.value = true;
// 如果没输入克数,默认为 1g
if (!newDrugInfo.value.number) {
newDrugInfo.value.number = 1;
}
}
function newDrugModalOk() {
addDrugByChinese();
showNewDrugModal.value = false;
if (newDrugInfo.value.id && newDrugInfo.value.name && newDrugInfo.value.number > 0) {
confirmModalApi.setData({
title: '确认添加',
content: `要把【${newDrugInfo.value.name}】添加到清单吗?`,
onConfirm: addDrugByChinese,
});
confirmModalApi.open();
}
}
function doctorSecondSignModalOk() {
@@ -2302,9 +2293,8 @@ watch(
<PrescriptionDetailModal/>
<!-- 常用方选择弹窗 -->
<CommonPrescriptionModals/>
<Modal v-model:open="showNewDrugModal" @ok="newDrugModalOk">
要把【{{ newDrugInfo.name }}】添加到清单吗?
</Modal>
<!-- 确认添加药品弹窗 -->
<ConfirmModalComponent />
<AntModal title="二次签名" v-model:open="doctorSecondSignModal" @ok="doctorSecondSignModalOk">
<div class="doctor-second-sign-title">有毒:</div>
<p v-for="item in checkData.message.poisonous || []">【{{ item }}】</p>

View File

@@ -1,6 +1,6 @@
<script setup>
import { computed, ref } from 'vue';
import { Card, Button, message } from 'ant-design-vue';
import { Button, message } from 'ant-design-vue';
import { usePrescriptionStore } from '#/store/prescription';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
@@ -103,91 +103,271 @@ const handleAddToPrescription = async () => {
</script>
<template>
<Card class="patient-experience-card" :bordered="true" style="max-width: 400px">
<div class="patient-experience-content">
<div class="title">患者就诊经历</div>
<div v-if="experienceData.symptom_description" class="info-item">
<div class="label">症状描述</div>
<div class="value">{{ experienceData.symptom_description }}</div>
<div class="patient-experience-card">
<!-- 卡片头部 -->
<div class="card-header">
<div class="header-icon">
<i class="fas fa-notes-medical"></i>
</div>
<div class="info-item">
<div class="label">就诊情况</div>
<div class="value">{{ hasVisitedText }}</div>
</div>
<div class="info-item">
<div class="label">用药情况</div>
<div class="value">{{ hasUsedDrugText }}</div>
</div>
<div v-if="experienceData.drug_name" class="info-item">
<div class="label">使用药物</div>
<div class="value">{{ experienceData.drug_name }}</div>
</div>
<div v-if="experienceData.illness_info" class="info-item">
<div class="label">病情信息</div>
<div class="value">{{ experienceData.illness_info }}</div>
</div>
<!-- 添加到处方单按钮 -->
<div v-if="experienceData.drug_name" class="action-section">
<Button
type="primary"
size="small"
:loading="addingDrug"
@click="handleAddToPrescription"
>
添加药品到处方单
</Button>
<div class="header-title">患者就诊经历</div>
<div class="header-tags">
<span :class="['tag', hasVisitedText === '已就诊过' ? 'tag-success' : 'tag-warning']">
{{ hasVisitedText }}
</span>
<span :class="['tag', hasUsedDrugText === '已使用过药物' ? 'tag-info' : 'tag-default']">
{{ hasUsedDrugText }}
</span>
</div>
</div>
</Card>
<!-- 卡片内容 -->
<div class="card-body">
<!-- 症状描述 -->
<div v-if="experienceData.symptom_description" class="info-row">
<div class="info-icon">
<i class="fas fa-stethoscope"></i>
</div>
<div class="info-content">
<div class="info-label">症状描述</div>
<div class="info-value">{{ experienceData.symptom_description }}</div>
</div>
</div>
<!-- 病情信息 -->
<div v-if="experienceData.illness_info" class="info-row">
<div class="info-icon">
<i class="fas fa-file-medical"></i>
</div>
<div class="info-content">
<div class="info-label">病情信息</div>
<div class="info-value">{{ experienceData.illness_info }}</div>
</div>
</div>
<!-- 使用药物 -->
<div v-if="experienceData.drug_name" class="info-row drug-row">
<div class="info-icon">
<i class="fas fa-pills"></i>
</div>
<div class="info-content">
<div class="info-label">使用药物</div>
<div class="info-value drug-name">{{ experienceData.drug_name }}</div>
</div>
</div>
</div>
<!-- 卡片底部操作 -->
<div v-if="experienceData.drug_name" class="card-footer">
<Button
type="primary"
:loading="addingDrug"
@click="handleAddToPrescription"
class="add-btn"
>
<i class="fas fa-plus-circle mr-1"></i>
添加药品到处方单
</Button>
</div>
</div>
</template>
<style scoped>
.patient-experience-card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
width: 100%;
min-width: 420px;
max-width: 520px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 16px;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
overflow: hidden;
transition: all 0.3s ease;
}
.patient-experience-content {
padding: 8px;
.patient-experience-card:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.title {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
color: #1890ff;
}
.info-item {
margin-bottom: 8px;
/* 卡片头部 */
.card-header {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
color: white;
}
.label {
.header-icon {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.header-title {
flex: 1;
font-size: 17px;
font-weight: 600;
letter-spacing: 0.5px;
}
.header-tags {
display: flex;
gap: 8px;
}
.tag {
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
color: #666;
margin-bottom: 4px;
font-weight: 500;
}
.value {
.tag-success {
background: rgba(34, 197, 94, 0.2);
color: #86efac;
}
.tag-warning {
background: rgba(251, 191, 36, 0.2);
color: #fcd34d;
}
.tag-info {
background: rgba(96, 165, 250, 0.2);
color: #93c5fd;
}
.tag-default {
background: rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.8);
}
/* 卡片内容 */
.card-body {
padding: 20px;
}
.info-row {
display: flex;
align-items: flex-start;
gap: 14px;
padding: 14px 16px;
background: white;
border-radius: 12px;
margin-bottom: 12px;
border: 1px solid #e2e8f0;
transition: all 0.2s ease;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-row:hover {
border-color: #3b82f6;
background: #f8fafc;
}
.info-icon {
width: 36px;
height: 36px;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #3b82f6;
font-size: 14px;
color: #333;
flex-shrink: 0;
}
.drug-row .info-icon {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
color: #d97706;
}
.info-content {
flex: 1;
min-width: 0;
}
.info-label {
font-size: 12px;
color: #64748b;
margin-bottom: 4px;
font-weight: 500;
}
.info-value {
font-size: 14px;
color: #1e293b;
line-height: 1.6;
word-break: break-word;
}
.action-section {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e4e7ed;
.drug-name {
color: #d97706;
font-weight: 600;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
padding: 4px 12px;
border-radius: 6px;
display: inline-block;
}
/* 卡片底部 */
.card-footer {
padding: 16px 20px;
background: #f8fafc;
border-top: 1px solid #e2e8f0;
display: flex;
justify-content: flex-end;
}
.add-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 20px;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
transition: all 0.2s ease;
}
.add-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
/* 暗黑模式适配 */
:global(.dark) .patient-experience-card {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border-color: #334155;
}
:global(.dark) .info-row {
background: #1e293b;
border-color: #334155;
}
:global(.dark) .info-row:hover {
background: #334155;
}
:global(.dark) .info-value {
color: #e2e8f0;
}
:global(.dark) .card-footer {
background: #0f172a;
border-color: #334155;
}
</style>

View File

@@ -44,16 +44,11 @@ const fetchPatientDetail = async () => {
}
}
console.log('查询患者详情register_id:', registerId, 'room_id:', props.patient.room_id, 'patient:', props.patient);
// 支持只有 register_id 没有 room_id 的情况
const res = await getPatientDetail(props.patient.room_id || '', registerId);
console.log('患者详情API响应:', res);
// 确保正确解析返回数据
const data = res?.result || res?.data || res || null;
patientDetail.value = data;
console.log('患者详情数据:', patientDetail.value);
console.log('就诊记录数量:', patientDetail.value?.register_infos?.length || 0);
} catch (error) {
console.error('获取患者详情失败:', error);
message.error('获取患者详情失败,请重试');
@@ -78,7 +73,6 @@ const handleReception = async () => {
try {
// 传递挂号ID
const res = await reception(props.patient.register_id);
console.log('接诊API响应:', res);
// res 已经是 data.result 的内容,无需判断 code请求封装会自动判断失败会抛出异常
message.success('接诊成功');

View File

@@ -1,8 +1,11 @@
<script setup>
import { ref, watch, onMounted } from 'vue';
import { RadioButton, RadioGroup, Button } from 'ant-design-vue';
import { RadioButton, RadioGroup, Button, message } from 'ant-design-vue';
import { ReloadOutlined } from '@ant-design/icons-vue';
import { getPatientList } from '../api/index.ts';
import { useChatStore } from '#/views/business/chat/stores/chat';
const chatStore = useChatStore();
const props = defineProps({
doctorId: {
@@ -26,7 +29,6 @@ const fetchPatientList = async () => {
// 确保正确解析返回数据jok函数返回格式为 { code: 0, message: "...", result: [...] }
const data = res?.result || res?.data || res || [];
patients.value = Array.isArray(data) ? data : [];
console.log('患者列表数据:', patients.value);
} catch (error) {
console.error('获取患者列表失败:', error);
patients.value = [];
@@ -98,6 +100,24 @@ onMounted(() => {
// 监听列表类型变化
watch(listType, handleTypeChange);
// 监听新挂号通知,自动刷新患者列表
watch(
() => chatStore.newRegisterNotification,
(newNotification) => {
if (newNotification && listType.value === 1) {
// 只在"当前"列表时自动刷新
message.info('有新的挂号,正在刷新列表...');
fetchPatientList();
}
},
{ deep: true }
);
// 暴露方法供父组件调用
defineExpose({
fetchPatientList
});
</script>
<template>

View File

@@ -36,6 +36,9 @@ const leftShow = ref(true);
// 当前选中的患者
const selectedPatient = ref(null);
// PatientList 组件的 ref
const patientListRef = ref(null);
// 视图模式:'list' = 列表模式(显示患者列表和信息),'chat' = 聊天模式(显示聊天界面)
const viewMode = ref('list');
@@ -99,7 +102,6 @@ const openPrescriptionModule = (id) => {
// 处理处方发送完成事件
const handlePrescriptionSent = () => {
console.log('处方发送完成');
// 可以在这里添加其他逻辑,如刷新聊天记录等
};
@@ -107,8 +109,12 @@ const handlePrescriptionSent = () => {
const handleSelectPatient = (patient) => {
selectedPatient.value = patient;
// 切换到列表模式以显示患者信息
viewMode.value = 'list';
// 如果是已接诊的患者status=2直接进入聊天模式否则显示患者信息
if (patient.status === 2) {
viewMode.value = 'chat';
} else {
viewMode.value = 'list';
}
// 设置当前好友,以便聊天功能正常工作
if (patient && patient.room_id) {
@@ -240,13 +246,14 @@ const handleEndConsultation = () => {
// res 已经是 result 内容,无需判断 code
message.success('结束接诊成功');
// 更新患者状态
if (selectedPatient.value) {
selectedPatient.value.status = 3; // 已结束
}
// 清空选中的患者
selectedPatient.value = null;
// 可以切换到列表模式或刷新当前患者信息
// 切换到列表模式
viewMode.value = 'list';
// 刷新患者列表
patientListRef.value?.fetchPatientList();
} catch (error) {
console.error('结束接诊失败:', error);
message.error('结束接诊失败,请重试');
@@ -255,6 +262,16 @@ const handleEndConsultation = () => {
});
};
// 处理结束接诊成功事件(从 ChatArea 传递过来)
const handleEndConsultationSuccess = () => {
// 清空选中的患者
selectedPatient.value = null;
// 切换到列表模式
viewMode.value = 'list';
// 刷新患者列表
patientListRef.value?.fetchPatientList();
};
// 将患者购买的药品加入处方单
const handleAddPatientProductsToPrescription = async () => {
// 检查处方store是否已初始化
@@ -469,6 +486,7 @@ const handleAddPatientProductsToPrescription = async () => {
{{ leftShow ? '收起患者列表' : '展开患者列表' }}
</Button>
<PatientList
ref="patientListRef"
v-if="leftShow"
:doctor-id="userStore.currentUser?.doctor_id"
@select-patient="handleSelectPatient"
@@ -495,6 +513,7 @@ const handleAddPatientProductsToPrescription = async () => {
<ChatArea
v-if="chatStore.currentFriend"
@open-prescription="openPrescriptionModule"
@end-consultation-success="handleEndConsultationSuccess"
/>
<EmptyState v-else />
</div>

View File

@@ -62,6 +62,7 @@ const [Modal, modalApi] = useVbenModal({
sort_order: values.sort_order ?? 0,
status: values.status ?? 1,
store_id: values.store_id ?? null,
drug_type: values.drug_type ?? undefined,
};
formApi.setValues(formData);
} else {
@@ -77,6 +78,7 @@ const [Modal, modalApi] = useVbenModal({
sort_order: 0,
status: 1,
store_id: null,
drug_type: undefined,
});
}
} else {

View File

@@ -40,6 +40,8 @@ export const modalFormProps: VbenFormProps = {
{ label: '处方药专区', value: 'prescription' },
{ label: '保健食品专区', value: 'health_food' },
{ label: '产品服务包专区', value: 'service_package' },
{ label: '非药品专区', value: 'non_drug' },
{ label: '医疗器械专区', value: 'medical_device' },
],
},
fieldName: 'type',
@@ -47,6 +49,23 @@ export const modalFormProps: VbenFormProps = {
rules: 'required',
defaultValue: undefined,
},
{
component: 'VbenSelect',
componentProps: {
placeholder: '请选择映射的药品类型',
options: [
{ label: '西药', value: 2 },
{ label: '保健食品', value: 3 },
{ label: '产品服务包', value: 5 },
{ label: '非药品', value: 6 },
{ label: '医疗器械', value: 7 },
],
allowClear: true,
},
fieldName: 'drug_type',
label: '药品类型',
defaultValue: undefined,
},
{
component: 'Avatar',
fieldName: 'icon',

View File

@@ -12,6 +12,8 @@ interface RowType {
status: number;
status_txt: string;
store_id: number | null;
drug_type: number | null;
drug_type_txt: string;
created_at: string;
updated_at: string;
}
@@ -44,10 +46,18 @@ export const gridOptions: VxeGridProps<RowType> = {
prescription: '处方药专区',
health_food: '保健食品专区',
service_package: '产品服务包专区',
non_drug: '非药品专区',
medical_device: '医疗器械专区',
};
return map[cellValue] || cellValue || '-';
},
},
{
field: 'drug_type_txt',
align: 'left',
title: '药品类型',
width: 120,
},
{
field: 'icon',
align: 'left',