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
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:
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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('结束接诊失败,请重试');
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
// 是否显示搜索表单控制按钮
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 }">
|
||||
|
||||
@@ -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栅格,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
240
apps/web-antd/src/views/business/product/non-drug/config/form.ts
Normal file
240
apps/web-antd/src/views/business/product/non-drug/config/form.ts
Normal 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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
196
apps/web-antd/src/views/business/product/non-drug/index.vue
Normal file
196
apps/web-antd/src/views/business/product/non-drug/index.vue
Normal 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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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: {
|
||||
|
||||
@@ -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' } },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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('接诊成功');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user