优化功能
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled

This commit is contained in:
李琦
2026-02-02 15:42:35 +08:00
parent 5ba5ec65c8
commit 4c03953127
28 changed files with 2690 additions and 1893 deletions

View File

@@ -27,9 +27,9 @@ const formSchema = computed((): VbenFormSchema[] => {
const schema: VbenFormSchema[] = [
// 1. Radio选择组最上面
{
component: 'RadioGroup',
component: 'VbenSelect',
componentProps: {
optionType: 'button',
// optionType: 'button',
options: [
{ label: '手机号登录', value: 'phone' },
{ label: '登录账号登录', value: 'login_account' },

View File

@@ -12,7 +12,6 @@ import { useThemeStore } from '../stores/theme.ts';
import { useUserStore } from '../stores/user.ts';
import { sendMessage } from '../utils/request.ts';
import { usePrescriptionStore } from '#/store/prescription';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
// 根据路由判断是在线复诊-药店还是在线复诊-诊所
const route = useRoute();
@@ -21,8 +20,8 @@ const isClinicModule = computed(() => {
});
// 动态导入对应的API和组件
import { endConsultation as endConsultationPharmacy } from '#/views/doctor/online-consultation/api/index';
import { endConsultation as endConsultationClinic } from '#/views/doctor/online-consultation-clinic/api/index';
import { endConsultation as endConsultationPharmacy, getDrugsByRegisterId as getDrugsByRegisterIdPharmacy } from '#/views/doctor/online-consultation/api/index';
import { endConsultation as endConsultationClinic, getDrugsByRegisterId as getDrugsByRegisterIdClinic } from '#/views/doctor/online-consultation-clinic/api/index';
import QuickReplyBubblesPharmacy from '#/views/doctor/online-consultation/components/QuickReplyBubbles.vue';
import QuickReplyBubblesClinic from '#/views/doctor/online-consultation-clinic/components/QuickReplyBubbles.vue';
@@ -31,6 +30,10 @@ const endConsultation = computed(() => {
return isClinicModule.value ? endConsultationClinic : endConsultationPharmacy;
});
const getDrugsByRegisterId = computed(() => {
return isClinicModule.value ? getDrugsByRegisterIdClinic : getDrugsByRegisterIdPharmacy;
});
const QuickReplyBubbles = computed(() => {
return isClinicModule.value ? QuickReplyBubblesClinic : QuickReplyBubblesPharmacy;
});
@@ -349,6 +352,7 @@ const handleQuickReplySelect = (content) => {
// 判断是否显示在线复诊相关按钮
const showOnlineConsultationButtons = computed(() => {
console.log('检查是否显示在线复诊相关按钮:', chatStore.currentFriend);
// 检查是否有 register_id如果有则说明是在线复诊场景
return !!chatStore.currentFriend?.register_id;
});
@@ -368,16 +372,6 @@ const handleAddPatientProductsToPrescription = async () => {
}
}
// 从聊天记录中筛选出所有 message_type=11 的消息(患者就诊经历)
const experienceMessages = chatStore.messages.filter(
(msg) => msg.message_type === 11
);
if (experienceMessages.length === 0) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 获取诊所ID
let storeId = prescriptionStore.myStoreId;
if (!storeId || storeId === 0) {
@@ -389,64 +383,34 @@ const handleAddPatientProductsToPrescription = async () => {
}
}
// 按时间排序,取最新的就诊经历
const sortedMessages = [...experienceMessages].sort((a, b) => {
const timeA = a.created_at ? new Date(a.created_at).getTime() : (a.timestamp || 0);
const timeB = b.created_at ? new Date(b.created_at).getTime() : (b.timestamp || 0);
return timeB - timeA; // 降序排列,最新的在前
});
const latestMessage = sortedMessages[0];
try {
// 解析最新消息的内容
let experienceData;
try {
experienceData = typeof latestMessage.message_content === 'string'
? JSON.parse(latestMessage.message_content)
: latestMessage.message_content;
} catch (e) {
console.error('解析就诊经历消息失败:', e);
message.warning('当前聊天没有可添加的药品');
return;
}
// 获取药品名称
const drugName = experienceData.drug_name;
if (!drugName) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 通过药品名称搜索药品
const res = await getProductListDoctorReception({
store_id: storeId,
type: 2, // 中成(西)药
name: drugName,
});
if (!res || res.length === 0) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 使用第一个匹配的药品
const drugData = res[0];
// 检查药品是否已在处方中
const existItem = prescriptionStore.currentDrugs.find(
(item) => item.index_id === drugData.id,
// 根据 register_id 获取药品信息
const res = await getDrugsByRegisterId.value(
prescriptionStore.currentRegisterId || chatStore.currentFriend?.register_id,
storeId
);
if (existItem) {
message.warning('该药品已在处方中');
const drugList = res?.result || res?.data || res || [];
if (!drugList || drugList.length === 0) {
message.warning('未找到药品信息');
return;
}
// 添加药品到处方单
const success = prescriptionStore.addProducts(drugData);
if (success) {
message.success(`已将${drugData.drug.drug_name}添加到处方单`);
// 遍历药品列表,添加到处方单
let addedCount = 0;
for (const drugData of drugList) {
const success = prescriptionStore.addProducts(drugData);
if (success) {
addedCount++;
}
}
if (addedCount > 0) {
message.success(`已成功添加 ${addedCount} 个药品到处方单`);
// 触发打开处方模态框事件
emit('openPrescription', prescriptionStore.currentRegisterId || chatStore.currentFriend?.register_id);
} else {
message.warning('所有药品已在处方中');
}
} catch (error) {
console.error('添加药品失败:', error);
@@ -522,13 +486,13 @@ const handleEndConsultation = () => {
>
<i class="fas fa-image"></i>
</button>
<button
class="function-btn"
@click="triggerFileInput('video')"
title="发送视频"
>
<i class="fas fa-video"></i>
</button>
<!-- <button-->
<!-- class="function-btn"-->
<!-- @click="triggerFileInput('video')"-->
<!-- title="发送视频"-->
<!-- >-->
<!-- <i class="fas fa-video"></i>-->
<!-- </button>-->
<button
:class="{ active: showEmojiPicker }"
class="function-btn"
@@ -537,18 +501,18 @@ const handleEndConsultation = () => {
>
<i class="fas fa-smile"></i>
</button>
<button
:class="{ recording: isRecording }"
class="function-btn record-btn"
title="按住录音"
@mousedown="startRecording"
@mouseleave="stopRecording"
@mouseup="stopRecording"
@touchend="stopRecording"
@touchstart="startRecording"
>
<i class="fas fa-microphone"></i>
</button>
<!-- <button-->
<!-- :class="{ recording: isRecording }"-->
<!-- class="function-btn record-btn"-->
<!-- title="按住录音"-->
<!-- @mousedown="startRecording"-->
<!-- @mouseleave="stopRecording"-->
<!-- @mouseup="stopRecording"-->
<!-- @touchend="stopRecording"-->
<!-- @touchstart="startRecording"-->
<!-- >-->
<!-- <i class="fas fa-microphone"></i>-->
<!-- </button>-->
<!-- 添加药品按钮仅在线复诊场景显示 -->
<button
v-if="showOnlineConsultationButtons"

View File

@@ -133,9 +133,9 @@ onMounted(() => {
<!-- 空状态 -->
<div
v-if="chatStore.messages.length === 0 && chatStore.currentFriend"
class="py-10 text-center text-gray-500 dark:text-gray-400"
class="flex h-full min-h-[300px] w-full items-center justify-center py-10 text-center text-gray-500 dark:text-gray-400"
>
<MessageOutlined class="mb-2 text-4xl" />
<MessageOutlined class="mb-2 text-4xl mr-2" />
<p>开始与 {{ chatStore.currentFriend.nick_name }} 对话吧</p>
</div>

View File

@@ -112,8 +112,8 @@ const openPrescriptionSourceModal = (id) => {
</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-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>

View File

@@ -254,7 +254,8 @@ const toggleFreeShipping = async (row: any, checked: boolean) => {
</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-if="row.is_online === 2" color="green">在线复诊诊所</Tag>
<Tag v-else-if="row.is_online === 3" color="green">在线复诊药店</Tag>
<Tag v-else color="default">线下就诊</Tag>
</template>
<template #delivery-method="{ row }">

View File

@@ -72,3 +72,11 @@ export async function importWesternMedicineApi(data: Record<string, any>) {
export async function updateZoneCategory(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update-zone-category`, data);
}
/**
* 更新最低调整金额
* @param data
*/
export async function updateMinAdjustPrice(data: { id: number; min_adjust_price: number | null }) {
return requestClient.post<any>(`${prefix}update-min-adjust-price`, data);
}

View File

@@ -0,0 +1,125 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Form, InputNumber, message } from 'ant-design-vue';
import { useVbenModal } from '@vben/common-ui';
import { updateMinAdjustPrice } from '../api';
defineOptions({
name: 'MinPriceModal',
});
const formRef = ref();
const loading = ref(false);
const formData = ref<{
id?: number;
drugName?: string;
currentMinPrice?: number | null;
}>({});
const minPrice = ref<number | null>(null);
const [ModalComponent, modalApi] = useVbenModal({
class: 'w-[500px]',
onCancel() {
modalApi.close();
},
onConfirm: async () => {
if (!formRef.value) {
return;
}
try {
await formRef.value.validate();
} catch (error) {
return;
}
if (!formData.value.id || minPrice.value === null) {
message.error('参数不完整');
return;
}
loading.value = true;
try {
await updateMinAdjustPrice({
id: formData.value.id,
min_adjust_price: minPrice.value,
});
message.success('设置成功');
modalApi.close();
const gridApi = modalApi.getData()?.gridApi;
if (gridApi) {
gridApi.reload();
}
} catch (error: any) {
console.error('设置失败:', error);
message.error(error?.message || '设置失败,请重试');
} finally {
loading.value = false;
}
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = modalApi.getData<typeof formData.value & { gridApi?: any }>();
if (data) {
formData.value = {
id: data.id,
drugName: data.drugName || data.drug_name,
currentMinPrice: data.currentMinPrice || data.min_adjust_price || null,
};
minPrice.value = formData.value.currentMinPrice;
}
} else {
formData.value = {};
minPrice.value = null;
}
},
});
</script>
<template>
<ModalComponent
title="设置最低调整金额"
:loading="loading"
>
<div v-if="formData.drugName" class="min-price-modal">
<div class="mb-4">
<p><strong>药品名称</strong>{{ formData.drugName }}</p>
<p v-if="formData.currentMinPrice" class="mt-2">
<strong>当前最低调整金额</strong>{{ Number(formData.currentMinPrice).toFixed(2) }}
</p>
<p v-else class="mt-2" style="color: #999">
<strong>当前最低调整金额</strong>未设置
</p>
</div>
<Form ref="formRef" :model="{ minPrice }" layout="vertical">
<Form.Item
label="最低调整金额"
name="minPrice"
:rules="[
{ required: true, message: '请输入最低调整金额' },
{ type: 'number', min: 0, message: '金额不能小于0' },
]"
>
<InputNumber
v-model:value="minPrice"
:min="0"
:precision="2"
:step="0.01"
style="width: 100%"
placeholder="请输入最低调整金额设置为0或留空表示不允许调整"
/>
</Form.Item>
<p class="text-gray-500 text-sm mt-2">
提示设置为0或留空表示该商品不允许调整价格
</p>
</Form>
</div>
</ModalComponent>
</template>
<style scoped>
.min-price-modal {
padding: 8px 0;
}
</style>

View File

@@ -67,7 +67,13 @@ export const gridOptions: VxeGridProps<RowType> = {
},
{ field: 'status', title: '状态', slots: { default: 'status' } },
{ field: 'created_at', title: '发布时间' },
{ type: 'html', title: '操作', width: 230, slots: { default: 'action' } },
{
field: 'min_adjust_price',
title: '最低调整金额',
width: 130,
slots: { default: 'min_adjust_price' },
},
{ type: 'html', title: '操作', width: 280, slots: { default: 'action' } },
],
keepSource: true,
pagerConfig: {},

View File

@@ -15,6 +15,7 @@ import { deleteWesternMedicine, exportWesternMedicineApi } from './api';
import FormModalDemo from './components/modal.vue';
import ExcelUpload from './components/ExcelUpload.vue';
import CategoryModal from './components/CategoryModal.vue';
import MinPriceModal from './components/MinPriceModal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
@@ -51,6 +52,10 @@ const [CategoryModalComp, categoryModalApi] = useVbenModal({
connectedComponent: CategoryModal,
});
const [MinPriceModalComp, minPriceModalApi] = useVbenModal({
connectedComponent: MinPriceModal,
});
const openCategoryModal = (row: any) => {
categoryModalApi.setData({
row,
@@ -59,6 +64,16 @@ const openCategoryModal = (row: any) => {
categoryModalApi.open();
};
const openMinPriceModal = (row: any) => {
minPriceModalApi.setData({
id: row.id,
drugName: row.drug_name,
min_adjust_price: row.min_adjust_price,
gridApi,
});
minPriceModalApi.open();
};
const showModal = (data = {}, isUpdate = false) => {
formModalApi.setData({
// 表单值
@@ -106,6 +121,7 @@ const openExcelUploadModal = () => {
<ExcelUploadModal />
<FormModal />
<CategoryModalComp />
<MinPriceModalComp />
<Grid>
<template #toolbar-buttons>
<TableAction
@@ -184,6 +200,17 @@ const openExcelUploadModal = () => {
<Tag :color="row.status_color">{{ row.status_txt }}</Tag>
</template>
<template #toolbar-tools></template>
<template #min_adjust_price="{ row }">
<a
class="text-primary cursor-pointer hover:underline"
@click="openMinPriceModal(row)"
>
<span v-if="row.min_adjust_price && row.min_adjust_price > 0">
{{ Number(row.min_adjust_price).toFixed(2) }}
</span>
<span v-else style="color: #999">未设置</span>
</a>
</template>
<template #action="{ row }">
<TableAction
:actions="[
@@ -195,6 +222,13 @@ const openExcelUploadModal = () => {
// auth: ['western-medicine', 'sys:role:detail'],
onClick: showModal.bind(null, row, true),
},
{
label: '设置底价',
type: 'link',
icon: 'ant-design:dollar-outlined',
size: 'small',
onClick: openMinPriceModal.bind(null, row),
},
{
label: '删除',
type: 'link',

View File

@@ -1,11 +1,10 @@
<script lang="ts" setup>
import {ref} from 'vue';
import {Page, useVbenModal} from '@vben/common-ui';
import { computed, ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import {
PlusOutlined,
DeleteOutlined,
CheckCircleFilled,
} from '@ant-design/icons-vue';
import {
Button,
@@ -14,11 +13,10 @@ import {
message,
Popconfirm,
Row,
Tooltip,
Pagination,
} from 'ant-design-vue';
import {
getDiseaseList,
} from '#/views/doctor/doctor-reception/api';
import { getDiseaseList } from '#/views/doctor/doctor-reception/api';
import { debounce } from 'lodash-es';
import {
getDoctorMyDiseaseListApi,
@@ -27,9 +25,22 @@ import {
} from "#/views/doctor/settings/api";
const searchKey = ref('');
const diagnosisList = ref([]);
// 存储接口返回的所有诊断数据
const allDiagnosisList = ref([]);
const selectDiagnosisList = ref('');
const setUpdateDiagnosis = ref();
// 分页相关状态
const currentPage = ref(1);
const pageSize = ref(30); // 每页显示30条tag比较小可以多显示点
// 计算属性:当前页展示的诊断数据
const paginatedDiagnosisList = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return allDiagnosisList.value.slice(start, end);
});
/**
* 常用诊断列表
*/
@@ -67,7 +78,7 @@ const [Modal, modalApi] = useVbenModal({
onOpenChange(isOpen: boolean) {
if (isOpen) {
setUpdateDiagnosis.value = isOpen ? modalApi.getData()?.updateDiagnosis : null;
const {values} =
const { values } =
modalApi.getData<Record<string, any>>();
if (values) {
selectDiagnosisList.value = values;
@@ -84,7 +95,8 @@ const [Modal, modalApi] = useVbenModal({
*/
function getDiagnosisList(searchKey = '') {
getDiseaseList(searchKey).then((res) => {
diagnosisList.value = res.map((item) => {
// 处理所有数据
const processedList = res.map((item) => {
item.isSelect = 0;
// 检查是否已在常用诊断中
item.isInMyList = doctorMyDiseaseList.value.some(
@@ -102,6 +114,10 @@ function getDiagnosisList(searchKey = '') {
}
return item;
});
// 更新总数据源并重置页码
allDiagnosisList.value = processedList;
currentPage.value = 1;
});
}
@@ -160,8 +176,9 @@ async function removeFromMyDiagnosis(item) {
message.success(`已将「${item.disease.name}」从常用诊断中移除`);
// 刷新常用诊断列表
await getDoctorMyDiseaseList();
// 更新搜索结果中的状态
const searchItem = diagnosisList.value.find(
// 更新总数据源中的状态 (不仅仅是当前页)
const searchItem = allDiagnosisList.value.find(
(d) => d.id === item.disease.id
);
if (searchItem) {
@@ -171,86 +188,106 @@ async function removeFromMyDiagnosis(item) {
message.error('删除失败,请重试');
}
}
/**
* 页码改变
*/
function handlePageChange(page: number, size: number) {
currentPage.value = page;
pageSize.value = size;
}
</script>
<template>
<Modal class="w-[60%]" title="常用诊断">
<Page>
<Row :gutter="12">
<Col :span="24">
<InputSearch
:model="searchKey"
enter-button
placeholder="请输想要搜索的内容..."
@input="(e) => getDiagnosisListChage(e.target.value)"
@search="getDiagnosisList"
/>
<Col :span="24" class="mb-4">
<InputSearch :model="searchKey" allow-clear enter-button placeholder="请输入想要搜索的诊断名称..." size="large"
@input="(e) => getDiagnosisListChage(e.target.value)" @search="getDiagnosisList" />
</Col>
<Col :span="24">
<!-- 常用诊断列表 -->
<h1 class="diagnosis-section-title" v-if="doctorMyDiseaseList.length > 0">常用</h1>
<div class="diagnosis-list">
<div class="section-header" v-if="doctorMyDiseaseList.length > 0">
<span class="section-title">常用诊断</span>
<span class="section-count">{{ doctorMyDiseaseList.length }}</span>
</div>
<div class="tag-container" v-if="doctorMyDiseaseList.length > 0">
<div
v-for="item in doctorMyDiseaseList"
:key="item.id"
class="diagnosis-item"
class="smart-tag"
:class="{ 'is-selected': item.disease.isSelect === 1 }"
@click="selectDiagnosis(item.disease)"
>
<Button
type="primary"
:ghost="item.disease.isSelect !== 1"
@click="selectDiagnosis(item.disease)"
>
{{ item.disease.name }}
</Button>
<div class="diagnosis-actions" @click.stop>
<span class="tag-text">{{ item.disease.name }}</span>
<!-- Action Area -->
<div class="tag-action-wrapper" @click.stop>
<Popconfirm
title="确定从常用诊断中移除吗?"
placement="topRight"
@confirm="removeFromMyDiagnosis(item)"
>
<Button
type="text"
size="small"
danger
class="action-btn"
>
<template #icon><DeleteOutlined /></template>
</Button>
<Tooltip title="移除常用">
<span class="action-icon delete">
<DeleteOutlined />
</span>
</Tooltip>
</Popconfirm>
</div>
</div>
</div>
<!-- 搜索结果列表 -->
<h1 class="diagnosis-section-title" v-if="diagnosisList.length > 0">搜索结果</h1>
<div class="diagnosis-list">
<div class="section-header mt-6" v-if="allDiagnosisList.length > 0">
<span class="section-title">搜索结果</span>
<span class="section-count"> {{ allDiagnosisList.length }} </span>
</div>
<div class="tag-container">
<div
v-for="item in diagnosisList"
v-for="item in paginatedDiagnosisList"
:key="item.id"
class="diagnosis-item"
class="smart-tag"
:class="{ 'is-selected': item.isSelect === 1 }"
@click="selectDiagnosis(item)"
>
<Button
type="primary"
:ghost="item.isSelect !== 1"
@click="selectDiagnosis(item)"
>
{{ item.name }}
</Button>
<div class="diagnosis-actions" @click.stop>
<Button
v-if="!item.isInMyList"
type="text"
size="small"
class="action-btn add-btn"
@click="addToMyDiagnosis(item)"
>
<template #icon><PlusOutlined /></template>
<span class="action-text">加入常用</span>
</Button>
<span v-else class="already-added">已添加</span>
<span class="tag-text">{{ item.name }}</span>
<div class="tag-action-wrapper" @click.stop>
<Tooltip v-if="!item.isInMyList" title="加入常用">
<span class="action-icon add" @click="addToMyDiagnosis(item)">
<PlusOutlined />
</span>
</Tooltip>
<Tooltip v-else title="已在常用列表">
<span class="action-icon added">
<CheckCircleFilled />
</span>
</Tooltip>
</div>
</div>
<div v-if="allDiagnosisList.length === 0 && searchKey" class="empty-state">
暂无相关诊断
</div>
<!-- 分页组件 -->
<div v-if="allDiagnosisList.length > 0" class="pagination-wrapper">
<Pagination
v-model:current="currentPage"
v-model:pageSize="pageSize"
:total="allDiagnosisList.length"
:show-total="total => `${total}`"
show-size-changer
show-quick-jumper
size="small"
@change="handlePageChange"
/>
</div>
</div>
</Col>
</Row>
@@ -259,80 +296,208 @@ async function removeFromMyDiagnosis(item) {
</template>
<style lang="scss" scoped>
.diagnosis-section-title {
margin-top: 12px;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: #333;
/* Section Headers */
.section-header {
display: flex;
align-items: center;
margin-bottom: 12px;
.section-title {
font-size: 15px;
font-weight: 600;
color: #1f2937;
position: relative;
padding-left: 10px;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 14px;
background-color: #3b82f6;
border-radius: 2px;
}
}
.section-count {
margin-left: 8px;
font-size: 12px;
color: #9ca3af;
background: #f3f4f6;
padding: 1px 6px;
border-radius: 10px;
}
}
.dark .diagnosis-section-title {
color: #e5e7eb;
.dark .section-header {
.section-title {
color: #e5e7eb;
}
.section-count {
background: #374151;
color: #9ca3af;
}
}
.diagnosis-list {
/* Tag Container */
.tag-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 10px;
min-height: 40px;
align-content: flex-start;
}
.diagnosis-item {
/* Smart Tag Styles */
.smart-tag {
position: relative;
display: inline-flex;
align-items: center;
margin: 4px 0;
&:hover .diagnosis-actions {
opacity: 1;
visibility: visible;
}
}
.diagnosis-actions {
position: absolute;
right: -8px;
top: -8px;
display: flex;
align-items: center;
gap: 2px;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 2px;
}
.dark .diagnosis-actions {
background: #374151;
}
.action-btn {
display: flex;
align-items: center;
font-size: 12px;
padding: 2px 6px;
height: auto;
}
.add-btn {
color: #1890ff;
padding: 6px 14px;
background-color: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 9999px; /* Pill shape */
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
max-width: 100%;
&:hover {
color: #40a9ff;
background: rgba(24, 144, 255, 0.1);
background-color: #f3f4f6;
border-color: #d1d5db;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
.tag-action-wrapper {
max-width: 24px;
margin-left: 8px;
opacity: 1;
}
}
/* Selected State */
&.is-selected {
background-color: #eff6ff;
border-color: #3b82f6;
color: #2563eb;
font-weight: 500;
&:hover {
background-color: #dbeafe;
}
.tag-text {
color: #2563eb;
}
}
/* Text Styling */
.tag-text {
font-size: 14px;
color: #4b5563;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.2s;
}
}
.action-text {
margin-left: 2px;
.dark .smart-tag {
background-color: #1f2937;
border-color: #374151;
.tag-text {
color: #d1d5db;
}
&:hover {
background-color: #374151;
border-color: #4b5563;
}
&.is-selected {
background-color: rgba(59, 130, 246, 0.15);
border-color: #3b82f6;
.tag-text {
color: #60a5fa;
}
}
}
.already-added {
font-size: 11px;
color: #999;
padding: 2px 6px;
/* Action Area (The Delete/Add buttons) */
.tag-action-wrapper {
display: inline-flex;
align-items: center;
justify-content: center;
max-width: 0;
overflow: hidden;
opacity: 0;
transition: all 0.3s ease;
margin-left: 0;
/* Show existing "Added" checkmark always or control via props?
Here we keep it hidden until hover for cleaner look,
OR we can make "Added" status distinct.
Let's keep the slide-in effect for actions. */
}
/* Specifically for "Already Added" checkmark - maybe always show it?
To keep UI clean, let's show it on hover or if it's "isInMyList"
but the requirement is to optimize hover. */
.action-icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
font-size: 12px;
transition: all 0.2s;
&.delete {
color: #9ca3af;
&:hover {
background-color: #fee2e2;
color: #ef4444;
}
}
&.add {
color: #9ca3af;
&:hover {
background-color: #dbeafe;
color: #3b82f6;
}
}
&.added {
color: #10b981;
cursor: default;
}
}
.empty-state {
width: 100%;
text-align: center;
padding: 20px;
color: #9ca3af;
font-size: 14px;
}
.pagination-wrapper {
width: 100%;
display: flex;
justify-content: flex-end;
margin-top: 16px;
padding-top: 12px;
border-top: 1px dashed #e5e7eb;
}
.dark .pagination-wrapper {
border-color: #374151;
}
</style>

View File

@@ -1,11 +1,10 @@
<script lang="ts" setup>
import {ref} from 'vue';
import {Page, useVbenModal} from '@vben/common-ui';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import {
PlusOutlined,
DeleteOutlined,
EnterOutlined,
} from '@ant-design/icons-vue';
import {
Button,
@@ -14,11 +13,10 @@ import {
message,
Popconfirm,
Row,
Tooltip,
} from 'ant-design-vue';
import {
getDoctorOrderList,
} from '#/views/doctor/doctor-reception/api';
import { getDoctorOrderList } from '#/views/doctor/doctor-reception/api';
import {
createDoctorOrderApi,
deleteDoctorOrderApi,
@@ -48,7 +46,7 @@ const [Modal, modalApi] = useVbenModal({
onOpenChange(isOpen: boolean) {
if (isOpen) {
setUpdateDoctorOrder.value = isOpen ? modalApi.getData()?.updateDoctorOrder : null;
const {values} =
const { values } =
modalApi.getData<Record<string, any>>();
if (values) {
selectDiagnosisList.value = values;
@@ -183,60 +181,56 @@ function cancelAdd() {
<Page>
<Row :gutter="12">
<!-- 我的医嘱 -->
<Col :span="24">
<h1 class="order-section-title">我的医嘱</h1>
<div class="order-list">
<Col :span="24" class="mb-4">
<div class="section-header">
<span class="section-title">我的医嘱</span>
</div>
<div class="tag-container">
<div
v-for="item in doctorOrderMyList"
:key="item.id"
class="order-item"
class="smart-tag"
:class="{ 'is-selected': item.isSelect === 1 }"
@click="selectDoctorOrder(item, 1)"
>
<Button
type="primary"
:ghost="item.isSelect !== 1"
@click="selectDoctorOrder(item, 1)"
>
{{ item.content }}
</Button>
<div class="order-actions" @click.stop>
<span class="tag-text">{{ item.content }}</span>
<!-- Action Area -->
<div class="tag-action-wrapper" @click.stop>
<Popconfirm
title="确定删除这条医嘱吗?"
placement="topRight"
@confirm="deleteMyDoctorOrder(item)"
>
<Button
type="text"
size="small"
danger
class="action-btn"
>
<template #icon><DeleteOutlined /></template>
</Button>
<Tooltip title="删除医嘱">
<span class="action-icon delete">
<DeleteOutlined />
</span>
</Tooltip>
</Popconfirm>
</div>
</div>
<!-- 添加医嘱按钮/输入框 -->
<div
v-if="!showAddInput"
class="add-order-btn"
@click="showAddInput = true"
>
<PlusOutlined />
<span>添加医嘱</span>
</div>
<!-- Add New Button/Input Area -->
<div class="add-tag-wrapper" :class="{ 'is-input': showAddInput }">
<div v-if="!showAddInput" class="add-btn-tag" @click="showAddInput = true">
<PlusOutlined class="mr-1"/> 添加医嘱
</div>
<div v-else class="add-order-input">
<Input
v-model:value="newOrderContent"
placeholder="输入医嘱内容"
@pressEnter="addMyDoctorOrder"
/>
<div class="add-actions">
<Button size="small" @click="cancelAdd">取消</Button>
<Button type="primary" size="small" @click="addMyDoctorOrder">
添加
</Button>
<div v-else class="add-input-container">
<Input
v-model:value="newOrderContent"
placeholder="输入内容后回车"
size="small"
class="custom-input"
ref="inputRef"
@pressEnter="addMyDoctorOrder"
@blur="!newOrderContent && cancelAdd()"
/>
<span class="enter-icon" @click="addMyDoctorOrder">
<EnterOutlined />
</span>
</div>
</div>
</div>
@@ -244,21 +238,19 @@ function cancelAdd() {
<!-- 公共医嘱 -->
<Col :span="24">
<h1 class="order-section-title" v-if="doctorOrderCommonList.length > 0">公共医嘱</h1>
<div class="order-list">
<div class="section-header" v-if="doctorOrderCommonList.length > 0">
<span class="section-title">公共医嘱</span>
</div>
<div class="tag-container">
<div
v-for="item in doctorOrderCommonList"
:key="item.id"
class="order-item"
class="smart-tag"
:class="{ 'is-selected': item.isSelect === 1 }"
@click="selectDoctorOrder(item, 2)"
>
<Button
type="primary"
:ghost="item.isSelect !== 1"
@click="selectDoctorOrder(item, 2)"
>
{{ item.name }}
</Button>
<span class="tag-text">{{ item.name }}</span>
</div>
</div>
</Col>
@@ -268,122 +260,203 @@ function cancelAdd() {
</template>
<style lang="scss" scoped>
.order-section-title {
margin-top: 12px;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: #333;
/* Common Headers */
.section-header {
display: flex;
align-items: center;
margin-bottom: 12px;
.section-title {
font-size: 15px;
font-weight: 600;
color: #1f2937;
position: relative;
padding-left: 10px;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 14px;
background-color: #3b82f6;
border-radius: 2px;
}
}
}
.dark .order-section-title {
.dark .section-header .section-title {
color: #e5e7eb;
}
.order-list {
/* Tag Layout */
.tag-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 10px;
align-items: center;
}
.order-item {
/* Modern Tag Style */
.smart-tag {
position: relative;
display: inline-flex;
align-items: center;
margin: 4px 0;
&:hover .order-actions {
opacity: 1;
visibility: visible;
}
}
.order-actions {
position: absolute;
right: -8px;
top: -8px;
display: flex;
align-items: center;
gap: 2px;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 2px;
}
.dark .order-actions {
background: #374151;
}
.action-btn {
display: flex;
align-items: center;
font-size: 12px;
padding: 2px 6px;
height: auto;
}
.add-order-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
padding: 6px 14px;
background-color: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 9999px;
cursor: pointer;
font-size: 13px;
color: #666;
transition: all 0.3s;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
height: 34px;
&:hover {
border-color: #1890ff;
color: #1890ff;
background-color: #f3f4f6;
border-color: #d1d5db;
transform: translateY(-1px);
.tag-action-wrapper {
max-width: 24px;
margin-left: 6px;
opacity: 1;
}
}
&.is-selected {
background-color: #eff6ff;
border-color: #3b82f6;
color: #2563eb;
.tag-text {
color: #2563eb;
font-weight: 500;
}
}
.tag-text {
font-size: 14px;
color: #4b5563;
}
}
.dark .add-order-btn {
.dark .smart-tag {
background-color: #1f2937;
border-color: #374151;
.tag-text { color: #d1d5db; }
&:hover {
background-color: #374151;
border-color: #4b5563;
}
&.is-selected {
background-color: rgba(59, 130, 246, 0.15);
border-color: #3b82f6;
.tag-text { color: #60a5fa; }
}
}
/* Action Icon slide-in logic */
.tag-action-wrapper {
display: inline-flex;
align-items: center;
max-width: 0;
overflow: hidden;
opacity: 0;
transition: all 0.25s ease;
margin-left: 0;
}
.action-icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
font-size: 12px;
&.delete {
color: #9ca3af;
&:hover {
background-color: #fee2e2;
color: #ef4444;
}
}
}
/* Add New Tag Button Area */
.add-tag-wrapper {
height: 34px;
display: inline-flex;
align-items: center;
transition: all 0.3s;
&.is-input {
width: 200px; /* Expand width when typing */
}
}
.add-btn-tag {
display: inline-flex;
align-items: center;
padding: 0 14px;
height: 100%;
border: 1px dashed #d1d5db;
border-radius: 9999px;
color: #6b7280;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #3b82f6;
color: #3b82f6;
background-color: rgba(59, 130, 246, 0.05);
}
}
.dark .add-btn-tag {
border-color: #4b5563;
color: #9ca3af;
&:hover {
border-color: #3b82f6;
color: #3b82f6;
}
}
.add-order-input {
display: flex;
align-items: center;
gap: 8px;
padding: 4px;
border: 1px solid #1890ff;
border-radius: 6px;
background: #fff;
.add-input-container {
position: relative;
width: 100%;
height: 100%;
:deep(.ant-input) {
flex: 1;
border: none;
outline: none;
box-shadow: none;
.custom-input {
height: 100%;
border-radius: 9999px;
padding-right: 30px; /* Space for enter icon */
font-size: 13px;
&:focus {
box-shadow: none;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
}
.enter-icon {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
cursor: pointer;
font-size: 12px;
padding: 2px;
&:hover {
color: #3b82f6;
}
}
}
.dark .add-order-input {
background: #374151;
border-color: #3b82f6;
}
.add-actions {
display: flex;
gap: 4px;
}
</style>

View File

@@ -6,8 +6,10 @@ import { useUserStore } from '@vben/stores';
import {
ContainerOutlined,
DeleteOutlined,
MinusOutlined,
PlusOutlined,
ShoppingCartOutlined,
} from '@ant-design/icons-vue';
import {
Badge,
@@ -23,6 +25,7 @@ import {
Row,
Select,
SelectOption,
Tag,
} from 'ant-design-vue';
import { debounce } from 'lodash-es';
@@ -118,7 +121,13 @@ const getDrugListByWesternModal = debounce(async (searchText = '') => {
select_number: matched?.select_number || 0,
drug: {
...item.drug,
// 如果已选使用已选的数量否则默认为1
number: matched?.number || 1,
// 如果已选,同步已选的用法
frequency_id: matched?.frequency_id || item.drug.frequency_id,
type_id: matched?.type_id || item.drug.type_id,
time_id: matched?.time_id || item.drug.time_id,
unit_id: matched?.unit_id || item.drug.unit_id,
},
};
});
@@ -294,6 +303,34 @@ function propProducts(data) {
}
}
/**
* 直接设置商品数量 (用于输入框)
* @param {object} data - 商品对象
* @param {number} value - 新数量
*/
function setProductQuantity(data, value) {
if (value < 0) return;
if (value === 0) {
propProducts(data); // 如果设为0走删除逻辑
return;
}
const existItem = selectList.value.find((item) => item.index_id === data.id);
if (existItem) {
selectList.value = selectList.value.map((item) =>
item.index_id === data.id ? { ...item, select_number: value } : item,
);
syncDrugList(data.id, value);
updateSelectStorage();
} else {
// 如果还没添加,先添加一次,然后再设置数量
addProducts(data);
// 这里因为addProducts是异步或者状态更新可能滞后简单处理为再次调用以修正数量
// 或者更严谨地构造新对象。鉴于addProducts逻辑较重这里假设用户是先点加号再改数量。
// 如果用户直接在输入框输入数字目前UI没开放未选中的输入框需要额外处理。
}
}
/**
* 更新本地存储中的选择列表
*/
@@ -493,7 +530,7 @@ function selectUnitChange(id) {
}
/**
* 更新药品数量
* 更新药品数量 (单次剂量)
* @param {number} id - 药品ID
* @param {number} number - 新的数量
*/
@@ -534,47 +571,177 @@ function updateProductNumber(id, number) {
<Page>
<Row :gutter="12">
<!-- 搜索框 -->
<Col :span="24">
<Col :span="24" class="mb-4">
<InputSearch
v-model:value="searchKey"
enter-button
placeholder="请输入商品名称或拼音首拼"
size="large"
@input="(e) => getDrugListByWesternModal(e.target.value)"
@search="getDrugListByWesternModal"
/>
</Col>
<!-- 药品列表 -->
<Col v-for="item in drugList" :key="item.id" :span="6">
<Badge :count="item.select_number" class="mt-5">
<!-- 中药卡片 -->
<Card v-if="type === 1" hoverable>
<template #actions>
<Button
v-if="item.select_number > 0"
key="prop"
type="link"
@click="propProducts(item)"
>
从清单移除
</Button>
<Button v-else key="add" type="link" @click="addProducts(item)">
添加到清单
</Button>
</template>
<CardMeta
:description="item.drug?.pinyin_simple"
:title="item.drug?.drug_name"
>
<template #avatar>
<a-avatar src="https://joeschmoe.io/api/v1/random" />
</template>
</CardMeta>
<div class="card-box mt-5" style="padding: 10px 5px">
<p>单价{{ item.price }}/g</p>
<!-- 选择中药煎法 -->
<!-- 西药列表视图 (参考EditModal) -->
<Col :span="24" v-if="type === 2">
<div v-if="drugList.length === 0" class="text-center py-8 text-gray-400 bg-gray-50 rounded-lg border border-dashed border-gray-200">
暂无药品请搜索添加
</div>
<div v-else class="flex flex-col gap-2">
<Card
v-for="item in drugList"
:key="item.id"
size="small"
class="drug-row-card"
:class="{ 'is-selected': item.select_number > 0 }"
hoverable
>
<div class="flex items-center gap-4">
<!-- 1. 图片区 -->
<div class="w-12 h-12 flex-shrink-0 cursor-pointer relative group" @click="setVisible(true, item.drug.instruction)">
<Image
v-if="item.drug?.image"
:src="item.drug.image"
:width="48"
:height="48"
class="rounded object-cover"
:preview="false"
/>
<div
v-else
class="w-12 h-12 bg-gray-100 rounded flex items-center justify-center text-gray-400 text-xs"
>
无图
</div>
<!-- 说明书图标遮罩 -->
<div class="absolute inset-0 bg-black/40 hidden group-hover:flex items-center justify-center rounded transition-all">
<ContainerOutlined class="text-white" />
</div>
</div>
<!-- 2. 信息区 -->
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span class="font-bold text-gray-800 truncate" :title="item.drug?.drug_name">{{ item.drug?.drug_name }}</span>
<Tag v-if="item.select_number > 0" color="blue" class="m-0 text-xs scale-90">已选</Tag>
</div>
<div class="text-xs text-gray-500 truncate mb-1" :title="item.drug?.function">
{{ item.drug?.pinyin_simple }}
</div>
<div class="text-red-500 font-medium text-sm">
¥{{ item.price }} <span class="text-gray-400 text-xs font-normal">/ {{ item.drug?.unit?.name || '单位' }}</span>
</div>
</div>
<!-- 3. 用法选择区 (仅当选中或一直显示但未选中时置灰) -->
<div class="flex gap-2">
<!-- 用法 -->
<Select
v-model:value="item.drug.type_id"
placeholder="用法"
style="width: 90px"
size="small"
@change="selectTypeChange"
@dropdown-visible-change="selectProductChange(item.id)"
>
<SelectOption v-for="val in drugUseType" :key="val.id" :value="val.id">{{ val.name }}</SelectOption>
</Select>
<!-- 频率 -->
<Select
v-model:value="item.drug.frequency_id"
placeholder="频率"
style="width: 100px"
size="small"
@change="selectFrequencyChange"
@dropdown-visible-change="selectProductChange(item.id)"
>
<SelectOption v-for="val in drugUseFrequency" :key="val.id" :value="val.id">{{ val.name }}</SelectOption>
</Select>
<!-- 时间 (可选) -->
<Select
v-model:value="item.drug.time_id"
placeholder="时间"
style="width: 90px"
size="small"
@change="selectTimeChange"
@dropdown-visible-change="selectProductChange(item.id)"
>
<SelectOption v-for="val in drugTime" :key="val.id" :value="val.id">{{ val.name }}</SelectOption>
</Select>
</div>
<!-- 4. 单次剂量 -->
<div class="flex items-center gap-1 bg-gray-50 p-1 rounded border border-gray-100">
<span class="text-xs text-gray-400 px-1">每次</span>
<InputNumber
v-model:value="item.drug.number"
:min="1"
:max="100"
size="small"
style="width: 50px"
:controls="false"
class="text-center-input"
@change="updateProductNumber(item.id, item.drug.number)"
/>
<Select
v-model:value="item.drug.unit_id"
class="no-border-select"
style="width: 50px"
size="small"
@change="selectUnitChange"
@dropdown-visible-change="selectProductChange(item.id)"
>
<SelectOption v-for="val in drugUnit" :key="val.id" :value="val.id">{{ val.name }}</SelectOption>
</Select>
</div>
<!-- 5. 数量操作区 -->
<div class="flex items-center gap-2 pl-2 border-l border-gray-100">
<template v-if="item.select_number > 0">
<Button size="small" shape="circle" @click="propProducts(item)">
<template #icon><MinusOutlined class="text-xs" /></template>
</Button>
<span class="font-bold w-6 text-center text-primary">{{ item.select_number }}</span>
<Button size="small" shape="circle" @click="addProducts(item)">
<template #icon><PlusOutlined class="text-xs" /></template>
</Button>
<Button type="text" danger size="small" @click="propProducts({ ...item, select_number: 1 })">
<DeleteOutlined />
</Button>
</template>
<Button v-else type="primary" size="small" ghost @click="addProducts(item)">
<template #icon><ShoppingCartOutlined /></template>
添加
</Button>
</div>
</div>
</Card>
</div>
</Col>
<!-- 中药列表视图 (保持 Grid 但优化样式) -->
<Col v-else-if="type === 1" v-for="item in drugList" :key="item.id" :span="6">
<Badge :count="item.select_number" class="w-full mt-4">
<Card hoverable size="small" class="chinese-drug-grid-card" :class="{ 'is-selected': item.select_number > 0 }">
<div class="flex flex-col gap-2">
<div class="flex justify-between items-start">
<div>
<div class="font-bold text-base text-gray-800">{{ item.drug?.drug_name }}</div>
<div class="text-xs text-gray-400 mt-1">{{ item.drug?.pinyin_simple }}</div>
</div>
<div class="text-right">
<div class="text-red-500">¥{{ item.price }}</div>
<div class="text-xs text-gray-400">/g</div>
</div>
</div>
<!-- 煎法选择 -->
<Select
:value="item.drug?.way_id"
class="mt-3 w-full"
class="w-full"
size="small"
placeholder="煎服"
@change="selectDrugUseWayChange"
@dropdown-visible-change="selectProductChange(item.id)"
@@ -588,174 +755,30 @@ function updateProductNumber(id, number) {
{{ value.name }}
</SelectOption>
</Select>
<div class="mt-3">
<!-- 数量输入框 -->
<InputNumber
v-model:value="item.drug.number"
class="w-3/4"
@change="updateProductNumber(item.id, item.drug.number)"
>
<template #addonBefore>
<MinusOutlined
key="prop"
@click="
updateProductNumber(
item.id,
item.drug.number > 1 ? item.drug.number - 1 : 0,
)
"
/>
</template>
<template #addonAfter>
<PlusOutlined
key="add"
@click="
updateProductNumber(
item.id,
item.drug.number < 1000
? item.drug.number + 1
: 1000,
)
"
/>
</template>
</InputNumber>
<!-- 单位选择 -->
<Select
:value="item.drug?.unit_id"
class="w-1/4"
disabled
@change="selectUnitChange"
@dropdown-visible-change="selectProductChange(item.id)"
>
<SelectOption
v-for="(value, index) in drugUnit"
:key="index"
:value="value.id"
>
{{ value.name }}
</SelectOption>
</Select>
</div>
</div>
</Card>
<!-- 西药卡片 -->
<Card v-else-if="type === 2" hoverable>
<template #cover>
<Image :height="200" :src="item.drug?.image" />
</template>
<template #actions>
<MinusOutlined key="prop" @click="propProducts(item)" />
<ContainerOutlined
@click="setVisible(true, item.drug.instruction)"
/>
<PlusOutlined key="add" @click="addProducts(item)" />
</template>
<CardMeta
:description="item.drug?.function"
:title="`${item.drug?.drug_name}(${item.drug?.pinyin_simple})`"
>
<template #avatar>
<a-avatar src="https://joeschmoe.io/api/v1/random" />
</template>
</CardMeta>
<div class="card-box mt-5" style="padding: 10px 5px">
<Popover>
<!-- <p class="drug-function text-xs">-->
<!-- 功效{{ item.drug?.function }}-->
<!-- </p>-->
<template #content>
<p>功效{{ item.drug?.function }}</p>
<p>用法{{ item.drug?.usage }}</p>
</template>
</Popover>
<p class="text-source mt-5">{{ item.drug?.source }}</p>
<!-- 选择频次 -->
<Select
v-model:value="item.drug.frequency_id"
class="mt-3 w-full"
@change="selectFrequencyChange"
@dropdown-visible-change="selectProductChange(item.id)"
>
<SelectOption
v-for="(value, index) in drugUseFrequency"
:key="index"
:value="value.id"
>
{{ value.name }}
</SelectOption>
</Select>
<!-- 选择时间 -->
<Select
v-model:value="item.drug.time_id"
class="mt-3 w-1/2"
@change="selectTimeChange"
@dropdown-visible-change="selectProductChange(item.id)"
>
<SelectOption
v-for="(value, index) in drugTime"
:key="index"
:value="value.id"
>
{{ value.name }}
</SelectOption>
</Select>
<!-- 选择使用方法 -->
<Select
v-model:value="item.drug.type_id"
class="mt-3 w-1/2"
@change="selectTypeChange"
@dropdown-visible-change="selectProductChange(item.id)"
>
<SelectOption
v-for="(value, index) in drugUseType"
:key="index"
:value="value.id"
>
{{ value.name }}
</SelectOption>
</Select>
<div class="mt-3">
<!-- 数量输入框 -->
<InputNumber v-model:value="item.drug.number" class="w-3/5">
<template #addonBefore>
<MinusOutlined
key="prop"
@click="
updateProductNumber(
item.id,
item.drug.number > 1 ? item.drug.number - 1 : 0,
)
"
/>
</template>
<template #addonAfter>
<PlusOutlined
key="add"
@click="
updateProductNumber(
item.id,
item.drug.number < 100 ? item.drug.number + 1 : 100,
)
"
/>
</template>
</InputNumber>
<!-- 单位选择 -->
<Select
v-model:value="item.drug.unit_id"
class="w-2/5"
@change="selectUnitChange"
@dropdown-visible-change="selectProductChange(item.id)"
>
<SelectOption
v-for="(value, index) in drugUnit"
:key="index"
:value="value.id"
>
{{ value.name }}
</SelectOption>
</Select>
<!-- 数量控制 -->
<div class="flex items-center justify-between mt-1">
<!-- 数量输入 (dose) -->
<div class="flex items-center border rounded px-1 flex-1 mr-2 bg-gray-50">
<InputNumber
v-model:value="item.drug.number"
:controls="false"
size="small"
class="flex-1 !bg-transparent !border-0 shadow-none text-center"
@change="updateProductNumber(item.id, item.drug.number)"
/>
<span class="text-gray-400 text-xs px-1">g</span>
</div>
<!-- 加减按钮 (Action) -->
<div class="flex items-center gap-1">
<Button v-if="item.select_number > 0" size="small" type="text" danger class="!px-1" @click="propProducts(item)">
移除
</Button>
<Button v-else size="small" type="primary" ghost class="!px-2" @click="addProducts(item)">
添加
</Button>
</div>
</div>
</div>
</Card>
@@ -766,7 +789,49 @@ function updateProductNumber(id, number) {
</Modal>
</template>
<style lang="scss">
<style lang="scss" scoped>
.drug-row-card {
transition: all 0.2s;
border: 1px solid #f0f0f0;
&.is-selected {
border-color: #3b82f6;
background-color: #f0f9ff;
}
&:hover {
border-color: #3b82f6;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
:deep(.ant-card-body) {
padding: 10px 12px;
}
}
.chinese-drug-grid-card {
transition: all 0.2s;
&.is-selected {
border-color: #10b981;
background-color: #ecfdf5;
}
}
.text-center-input {
text-align: center;
:deep(input) {
text-align: center;
}
}
.no-border-select {
:deep(.ant-select-selector) {
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
}
}
.drug-function {
display: -webkit-box;
-webkit-line-clamp: 2;

View File

@@ -100,3 +100,14 @@ export async function getRegisterStoreInfo(registerId: number) {
params: { register_id: registerId }
});
}
/**
* 根据挂号ID获取药品信息
* @param registerId 挂号ID
* @param storeId 门店ID
*/
export async function getDrugsByRegisterId(registerId: number, storeId: number) {
return requestClient.get<any>(`${prefix}get-drugs-by-register-id`, {
params: { register_id: registerId, store_id: storeId }
});
}

View File

@@ -2,7 +2,7 @@
import { computed, ref } from 'vue';
import { Button, message } from 'ant-design-vue';
import { usePrescriptionStore } from '#/store/prescription';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
import { getDrugsByRegisterId } from '../api/index.ts';
const props = defineProps({
content: {
@@ -40,58 +40,47 @@ const hasUsedDrugText = computed(() => {
// 添加药品到处方单
const handleAddToPrescription = async () => {
if (!experienceData.value.drug_name) {
message.warning('该就诊经历中没有药品信息');
return;
}
// 检查处方store是否已初始化
if (!prescriptionStore.currentRegisterId) {
message.warning('请先接诊患者');
return;
}
// 获取门店ID
let storeId = prescriptionStore.myStoreId;
if (!storeId || storeId === 0) {
if (prescriptionStore.myStoreList && prescriptionStore.myStoreList.length > 0) {
storeId = prescriptionStore.myStoreList[0].id;
} else {
message.warning('请先选择诊所');
return;
}
}
addingDrug.value = true;
try {
// 通过药品名称搜索药品
// 如果myStoreId为0尝试使用myStoreList的第一个
let storeId = prescriptionStore.myStoreId;
if (!storeId || storeId === 0) {
if (prescriptionStore.myStoreList && prescriptionStore.myStoreList.length > 0) {
storeId = prescriptionStore.myStoreList[0].id;
} else {
message.warning('请先选择诊所');
return;
// 根据 register_id 获取药品信息
const res = await getDrugsByRegisterId(prescriptionStore.currentRegisterId, storeId);
const drugList = res?.result || res?.data || res || [];
if (!drugList || drugList.length === 0) {
message.warning('未找到药品信息');
return;
}
// 遍历药品列表,添加到处方单
let addedCount = 0;
for (const drugData of drugList) {
const success = prescriptionStore.addProducts(drugData);
if (success) {
addedCount++;
}
}
const res = await getProductListDoctorReception({
store_id: storeId,
type: 2, // 中成(西)药
name: experienceData.value.drug_name,
});
if (!res || res.length === 0) {
message.warning(`未找到药品:${experienceData.value.drug_name}`);
return;
}
// 使用第一个匹配的药品
const drugData = res[0];
// 检查药品是否已在处方中
const existItem = prescriptionStore.currentDrugs.find(
(item) => item.index_id === drugData.id,
);
if (existItem) {
message.warning('该药品已在处方中');
return;
}
// 添加药品到处方单
const success = prescriptionStore.addProducts(drugData);
if (success) {
message.success(`已将${drugData.drug.drug_name}添加到处方单`);
if (addedCount > 0) {
message.success(`已成功添加 ${addedCount} 个药品到处方单`);
} else {
message.warning('所有药品已在处方中');
}
} catch (error) {
console.error('添加药品失败:', error);

View File

@@ -19,7 +19,7 @@ const props = defineProps({
}
});
const emit = defineEmits(['reception-success', 'enter-chat']);
const emit = defineEmits(['reception-success', 'enter-chat', 'refresh-list']);
const patientDetail = ref(null);
const loading = ref(false);
@@ -82,6 +82,7 @@ const handleReception = async () => {
status: 2, // 接诊中
};
emit('reception-success', receptionResult);
emit('refresh-list'); // 通知父组件刷新列表
await fetchPatientDetail();
} catch (error) {
console.error('接诊失败:', error);

View File

@@ -215,6 +215,12 @@ const handleReceptionSuccess = (patient) => {
}
};
// 处理刷新列表
const handleRefreshList = () => {
// 刷新患者列表
patientListRef.value?.fetchPatientList();
};
// 结束接诊
const handleEndConsultation = () => {
if (!selectedPatient.value) {
@@ -506,6 +512,7 @@ const handleAddPatientProductsToPrescription = async () => {
:doctor-id="userStore.currentUser?.doctor_id"
@reception-success="handleReceptionSuccess"
@enter-chat="handleEnterChat"
@refresh-list="handleRefreshList"
class="patient-info-panel"
/>
</template>

View File

@@ -101,3 +101,37 @@ export async function getRegisterStoreInfo(registerId: number) {
});
}
/**
* 根据挂号ID获取药品信息
* @param registerId 挂号ID
* @param storeId 门店ID
*/
export async function getDrugsByRegisterId(registerId: number, storeId: number) {
return requestClient.get<any>(`${prefix}get-drugs-by-register-id`, {
params: { register_id: registerId, store_id: storeId }
});
}
/**
* 获取患者订单列表
* @param registerId 挂号ID
*/
export async function getPatientOrders(registerId: number) {
return requestClient.get<any>(`${prefix}get-patient-orders`, {
params: { register_id: registerId }
});
}
/**
* 调整订单商品价格
* @param data 调整参数
*/
export async function adjustOrderItemPrice(data: {
product_order_id: number;
product_order_item_id: number;
new_price: number;
register_id: number;
remark?: string;
}) {
return requestClient.post<any>(`${prefix}adjust-order-item-price`, data);
}

View File

@@ -0,0 +1,64 @@
<script setup lang="ts">
import { Button } from 'ant-design-vue';
import { ShoppingCartOutlined } from '@ant-design/icons-vue';
import PatientOrderListModal from './PatientOrderListModal.vue';
import { useVbenDrawer } from '@vben/common-ui';
const props = defineProps<{
registerId?: number | null;
}>();
const [OrderListDrawer, orderListDrawerApi] = useVbenDrawer({
connectedComponent: PatientOrderListModal,
});
const handleClick = () => {
if (!props.registerId) {
return;
}
orderListDrawerApi.setData({
registerId: props.registerId,
});
orderListDrawerApi.open();
};
</script>
<template>
<div v-if="registerId" class="order-list-float-button">
<Button
type="primary"
shape="circle"
size="large"
@click="handleClick"
class="float-btn"
style="width: 56px; height: 56px; padding: 0; display: flex; align-items: center; justify-content: center;"
>
<template #icon>
<ShoppingCartOutlined :style="{ fontSize: '20px' }" />
</template>
</Button>
<OrderListDrawer />
</div>
</template>
<style scoped>
.order-list-float-button {
position: fixed;
right: 24px;
top: 50%;
transform: translateY(-50%);
z-index: 1000;
}
.float-btn {
width: 56px;
height: 56px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s;
}
.float-btn:hover {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
transform: translateY(-2px);
}
</style>

View File

@@ -2,7 +2,7 @@
import { computed, ref } from 'vue';
import { Button, message } from 'ant-design-vue';
import { usePrescriptionStore } from '#/store/prescription';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
import { getDrugsByRegisterId } from '../api/index.ts';
const props = defineProps({
content: {
@@ -40,58 +40,47 @@ const hasUsedDrugText = computed(() => {
// 添加药品到处方单
const handleAddToPrescription = async () => {
if (!experienceData.value.drug_name) {
message.warning('该就诊经历中没有药品信息');
return;
}
// 检查处方store是否已初始化
if (!prescriptionStore.currentRegisterId) {
message.warning('请先接诊患者');
return;
}
// 获取门店ID
let storeId = prescriptionStore.myStoreId;
if (!storeId || storeId === 0) {
if (prescriptionStore.myStoreList && prescriptionStore.myStoreList.length > 0) {
storeId = prescriptionStore.myStoreList[0].id;
} else {
message.warning('请先选择诊所');
return;
}
}
addingDrug.value = true;
try {
// 通过药品名称搜索药品
// 如果myStoreId为0尝试使用myStoreList的第一个
let storeId = prescriptionStore.myStoreId;
if (!storeId || storeId === 0) {
if (prescriptionStore.myStoreList && prescriptionStore.myStoreList.length > 0) {
storeId = prescriptionStore.myStoreList[0].id;
} else {
message.warning('请先选择诊所');
return;
// 根据 register_id 获取药品信息
const res = await getDrugsByRegisterId(prescriptionStore.currentRegisterId, storeId);
const drugList = res?.result || res?.data || res || [];
if (!drugList || drugList.length === 0) {
message.warning('未找到药品信息');
return;
}
// 遍历药品列表,添加到处方单
let addedCount = 0;
for (const drugData of drugList) {
const success = prescriptionStore.addProducts(drugData);
if (success) {
addedCount++;
}
}
const res = await getProductListDoctorReception({
store_id: storeId,
type: 2, // 中成(西)药
name: experienceData.value.drug_name,
});
if (!res || res.length === 0) {
message.warning(`未找到药品:${experienceData.value.drug_name}`);
return;
}
// 使用第一个匹配的药品
const drugData = res[0];
// 检查药品是否已在处方中
const existItem = prescriptionStore.currentDrugs.find(
(item) => item.index_id === drugData.id,
);
if (existItem) {
message.warning('该药品已在处方中');
return;
}
// 添加药品到处方单
const success = prescriptionStore.addProducts(drugData);
if (success) {
message.success(`已将${drugData.drug.drug_name}添加到处方单`);
if (addedCount > 0) {
message.success(`已成功添加 ${addedCount} 个药品到处方单`);
} else {
message.warning('所有药品已在处方中');
}
} catch (error) {
console.error('添加药品失败:', error);
@@ -315,7 +304,7 @@ const handleAddToPrescription = async () => {
.drug-name {
color: #d97706;
font-weight: 600;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
//background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
padding: 4px 12px;
border-radius: 6px;
display: inline-block;

View File

@@ -43,7 +43,7 @@ const fetchPatientDetail = async () => {
registerId = id;
}
}
// 支持只有 register_id 没有 room_id 的情况
const res = await getPatientDetail(props.patient.room_id || '', registerId);
// 确保正确解析返回数据
@@ -73,7 +73,7 @@ const handleReception = async () => {
try {
// 传递挂号ID
const res = await reception(props.patient.register_id);
// res 已经是 data.result 的内容,无需判断 code请求封装会自动判断失败会抛出异常
message.success('接诊成功');
const receptionResult = {
@@ -131,7 +131,8 @@ watch(
<text>加载中...</text>
</div>
<div v-else-if="!patientDetail && !patient" class="empty-state">
<div v-else-if="!patientDetail && !patient"
class="empty-state flex items-center justify-center h-full w-full min-h-[300px]">
<Empty description="请选择患者" />
</div>
@@ -148,7 +149,7 @@ watch(
进入聊天
</Button>
</div>
<!-- 患者基本信息 -->
<Descriptions
:column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 2, xs: 2 }"
@@ -227,7 +228,6 @@ watch(
display: flex;
align-items: center;
justify-content: center;
height: 200px;
}
.patient-content {

View File

@@ -65,6 +65,15 @@ const getStatusText = (status) => {
return statusMap[status] || '未知';
};
// 获取挂号类型文本
const getRegisterTypeText = (registerType) => {
const typeMap = {
2: '诊所复诊',
3: '药店复诊',
};
return typeMap[registerType] || '';
};
// 获取状态样式类
const getStatusClass = (status) => {
const classMap = {
@@ -164,9 +173,14 @@ defineExpose({
<p class="patient-phone">{{ patient.user_patient?.mobile || patient.user?.mobile || '' }}</p>
<p v-if="patient.order_no" class="order-no">订单号{{ patient.order_no }}</p>
<div class="patient-meta">
<span :class="['status', getStatusClass(patient.status)]">
{{ getStatusText(patient.status) }}
</span>
<div class="meta-left">
<span :class="['status', getStatusClass(patient.status)]">
{{ getStatusText(patient.status) }}
</span>
<span v-if="patient.register_type" class="register-type-tag">
{{ getRegisterTypeText(patient.register_type) }}
</span>
</div>
<span class="time text-gray-500 dark:text-gray-400">{{ formatTime(patient.last_message_time) }}</span>
</div>
</div>
@@ -257,6 +271,29 @@ defineExpose({
margin-top: 4px;
}
.meta-left {
display: flex;
align-items: center;
gap: 8px;
}
.register-type-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
background: #e6f7ff;
color: #1890ff;
border: 1px solid #91d5ff;
}
.dark .register-type-tag {
background: rgba(24, 144, 255, 0.2);
color: #69c0ff;
border-color: rgba(24, 144, 255, 0.3);
}
.status {
display: inline-block;
padding: 2px 8px;

View File

@@ -0,0 +1,400 @@
<script setup lang="ts">
import { ref } from 'vue';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { Tag, Button, message, Image } from 'ant-design-vue';
import { useVbenDrawer } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getPatientOrders } from '../api/index.ts';
import PriceAdjustModal from './PriceAdjustModal.vue';
defineOptions({
name: 'PatientOrderListModal',
});
interface Product {
id: number;
drug_id: number;
drug_name: string;
price: number;
number: number;
type: number;
is_chinese_medicine: boolean;
min_adjust_price: number | null;
can_adjust: boolean;
drug_image?: string;
specification?: string;
source_name?: string;
}
interface Order {
id: number;
order_no: string;
prescription_type: number;
items_price: number;
total_pay_price: number;
trans_expenses?: number;
status: number;
is_pay: number;
created_at: string;
products: Product[];
}
const [DrawerComponent, drawerApi] = useVbenDrawer({
class: 'w-[1000px]',
onCancel() {
drawerApi.close();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = drawerApi.getData<{ registerId: number }>();
if (data?.registerId) {
registerId.value = data.registerId;
fetchOrders();
}
} else {
gridApi.setGridOptions({ data: [] });
registerId.value = null;
}
},
});
const [PriceAdjustDrawerComponent, priceAdjustDrawerApi] = useVbenDrawer({
connectedComponent: PriceAdjustModal,
});
const registerId = ref<number | null>(null);
const gridOptions: VxeGridProps<Order> = {
checkboxConfig: {
highlight: true,
labelField: '',
},
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
columns: [
{ type: 'expand', width: 80, slots: { content: 'expand-content' } },
{ type: 'seq', width: 60, title: '序号' },
{ field: 'order_no', title: '订单号', width: 180 },
{
field: 'prescription_type',
title: '处方类型',
width: 100,
slots: { default: 'prescription_type' },
},
{
field: 'items_price',
title: '商品总价',
width: 120,
slots: { default: 'items_price' },
},
{
field: 'total_pay_price',
title: '支付总价',
width: 120,
slots: { default: 'total_pay_price' },
},
{
field: 'trans_expenses',
title: '邮费',
width: 100,
slots: { default: 'trans_expenses' },
},
{
field: 'is_pay',
title: '支付状态',
width: 100,
slots: { default: 'is_pay' },
},
{ field: 'created_at', title: '订单生成时间', width: 180 },
{
type: 'html',
title: '操作',
width: 120,
slots: { default: 'action' },
},
],
data: [],
expandConfig: {},
pagerConfig: {
enabled: false,
},
height: 'auto',
border: false,
showOverflow: false,
};
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions,
showSearchForm: false,
});
const getPrescriptionTypeText = (type: number): string => {
const typeMap: Record<number, string> = {
1: '中药',
2: '西药',
3: '颗粒药',
};
return typeMap[type] || '未知';
};
const getPrescriptionTypeColor = (type: number): string => {
const colorMap: Record<number, string> = {
1: 'orange',
2: 'blue',
3: 'purple',
};
return colorMap[type] || 'default';
};
const handleAdjustPrice = (product: Product, orderId: number) => {
priceAdjustDrawerApi.setData({
productOrderId: orderId,
productOrderItemId: product.id,
drugId: product.drug_id,
drugName: product.drug_name,
currentPrice: product.price,
minAdjustPrice: product.min_adjust_price || 0,
registerId: registerId.value,
onSuccess: () => {
fetchOrders();
},
});
priceAdjustDrawerApi.open();
};
const fetchOrders = async () => {
if (!registerId.value) {
return;
}
gridApi.setLoading(true);
try {
const res = await getPatientOrders(registerId.value);
const orderList: Order[] = res?.result || res?.data || res || [];
gridApi.setGridOptions({ data: orderList });
} catch (error) {
console.error('获取订单列表失败:', error);
message.error('获取订单列表失败');
} finally {
gridApi.setLoading(false);
}
};
</script>
<template>
<DrawerComponent title="患者订单列表" class="order-list-drawer w-[60%]">
<Grid>
<!-- 处方类型 -->
<template #prescription_type="{ row }">
<Tag :color="getPrescriptionTypeColor(row.prescription_type)">
{{ getPrescriptionTypeText(row.prescription_type) }}
</Tag>
</template>
<!-- 商品总价 -->
<template #items_price="{ row }">
{{ Number(row.items_price || 0).toFixed(2) }}
</template>
<!-- 支付总价 -->
<template #total_pay_price="{ row }">
{{ Number(row.total_pay_price || 0).toFixed(2) }}
</template>
<!-- 邮费 -->
<template #trans_expenses="{ row }">
{{ Number(row.trans_expenses || 0).toFixed(2) }}
</template>
<!-- 支付状态 -->
<template #is_pay="{ row }">
<Tag :color="row.is_pay === 1 ? 'green' : 'red'">
{{ row.is_pay === 1 ? '已支付' : '未支付' }}
</Tag>
</template>
<!-- 操作 -->
<template #action="{ row }">
<span style="color: #999"> {{ row.products?.length || 0 }} 个商品</span>
</template>
<!-- 展开内容商品列表 -->
<template #expand-content="{ row }">
<div
v-if="row.prescription_type === 2 || row.prescription_type === 5"
class="product-list mt-5"
>
<div
v-for="(product, index) in row.products"
:key="product.id || index"
class="box-card mb-6 rounded-lg p-4"
>
<div class="flex items-start">
<div>
<div
class="mr-6 h-32 w-32 overflow-hidden rounded-lg bg-gray-200 dark:bg-gray-800"
>
<Image
:src="product.drug_image || '/img/empty.png'"
alt=""
height="100%"
width="100%"
/>
</div>
<div class="ml-3 mt-5 text-sm">
<span class="text-gray-500 dark:text-gray-400">商品编号</span>
X00{{ index + 1 }}
</div>
</div>
<div class="flex-grow space-y-2">
<div class="text-sm">
<span class="text-gray-500 dark:text-gray-400">商品名称:</span>
{{ product.drug_name }}
</div>
<div class="text-sm">
<span class="text-gray-500 dark:text-gray-400">商品规格:</span>
{{ product.specification || '暂无' }}
</div>
<div class="text-sm">
<span class="text-gray-500 dark:text-gray-400">商品数量:</span>
{{ product.number }}
</div>
<div class="text-sm">
<span class="text-gray-500 dark:text-gray-400">商品厂家:</span>
{{ product.source_name || '暂无' }}
</div>
<div class="text-sm">
<span class="text-gray-500 dark:text-gray-400">商品价格:</span>
<a
v-if="!product.is_chinese_medicine && product.can_adjust"
class="text-primary cursor-pointer hover:underline ml-1"
@click="handleAdjustPrice(product, row.id)"
>
{{ Number(product.price || 0).toFixed(4) }}
</a>
<span v-else>{{ Number(product.price || 0).toFixed(4) }}</span>
</div>
<div v-if="!product.is_chinese_medicine && product.can_adjust" class="text-sm mt-2">
<Button
type="link"
size="small"
@click="handleAdjustPrice(product, row.id)"
>
调整价格
</Button>
<span class="text-gray-500 dark:text-gray-400 ml-2">
(最低调整金额: {{ Number(product.min_adjust_price || 0).toFixed(2) }})
</span>
</div>
<div v-if="product.is_chinese_medicine" class="text-sm">
<span class="text-gray-500 dark:text-gray-400">备注:</span>
<span style="color: #999">中药不可调整</span>
</div>
</div>
</div>
</div>
</div>
<div
v-if="row.prescription_type === 1"
class="medication-details mt-5 p-10"
>
<span class="mb-2 font-medium">药品明细</span>
<ul class="custom-list pl-6">
<li
v-for="(product, index) in row.products"
:key="product.id || index"
class="custom-list-item"
>
{{ product.drug_id }} {{ product.drug_name }} * {{ product.number }}
</li>
</ul>
</div>
</template>
</Grid>
<PriceAdjustDrawerComponent />
</DrawerComponent>
</template>
<style scoped>
.order-list-drawer {
padding: 16px;
}
.product-list {
padding: 8px 0;
}
.product-item {
background-color: #fafafa;
border: 1px solid #e8e8e8;
}
.product-info {
display: flex;
flex-direction: column;
gap: 8px;
}
.product-row {
display: flex;
align-items: center;
font-size: 14px;
}
.product-label {
min-width: 120px;
color: #666;
font-weight: 500;
}
.product-value {
flex: 1;
color: #333;
}
.dark .product-item {
background-color: #1f1f1f;
border-color: #434343;
}
.dark .product-label {
color: #999;
}
.dark .product-value {
color: #e5e5e5;
}
.custom-list {
list-style-type: none;
padding-left: 0;
}
.custom-list-item {
background-color: rgba(64, 158, 255, 0.04);
border-radius: 4px;
margin-bottom: 8px;
padding: 8px 12px;
font-size: 14px;
transition: background-color 0.3s;
}
.custom-list-item:hover {
background-color: rgba(64, 158, 255, 0.1);
}
.custom-list-item::before {
content: '';
display: inline-block;
width: 6px;
height: 6px;
background-color: #409eff;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
}
</style>

View File

@@ -0,0 +1,167 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Form, InputNumber, message, Descriptions } from 'ant-design-vue';
import { useVbenDrawer } from '@vben/common-ui';
import { adjustOrderItemPrice } from '../api/index.ts';
defineOptions({
name: 'PriceAdjustModal',
});
const formRef = ref();
const loading = ref(false);
const formData = ref<{
productOrderId?: number;
productOrderItemId?: number;
drugId?: number;
drugName?: string;
currentPrice?: number;
minAdjustPrice?: number;
registerId?: number | null;
onSuccess?: () => void;
}>({});
const newPrice = ref<number | null>(null);
// 价格验证规则
const getPriceRules = () => {
return [
{ required: true, message: '请输入新价格' },
{
validator: (_rule: any, value: any) => {
return new Promise<void>((resolve, reject) => {
if (!value && value !== 0) {
reject(new Error('请输入新价格'));
return;
}
const minPrice = formData.value.minAdjustPrice ? Number(formData.value.minAdjustPrice) : 0;
if (minPrice > 0 && Number(value) < minPrice) {
reject(new Error(`新价格不能低于最低调整金额:¥${minPrice.toFixed(2)}`));
return;
}
resolve();
});
},
},
];
};
const [DrawerComponent, drawerApi] = useVbenDrawer({
class: 'w-[500px]',
onCancel() {
drawerApi.close();
},
onConfirm: async () => {
// 验证表单
if (!formRef.value) {
return;
}
try {
await formRef.value.validate();
} catch (error) {
return;
}
if (!formData.value.productOrderId || !formData.value.productOrderItemId || !newPrice.value) {
message.error('参数不完整');
return;
}
const minPrice = formData.value.minAdjustPrice ? Number(formData.value.minAdjustPrice) : 0;
if (minPrice > 0 && newPrice.value < minPrice) {
message.error(`新价格不能低于最低调整金额:¥${minPrice.toFixed(2)}`);
return;
}
loading.value = true;
try {
await adjustOrderItemPrice({
product_order_id: formData.value.productOrderId,
product_order_item_id: formData.value.productOrderItemId,
new_price: newPrice.value,
register_id: formData.value.registerId || 0,
});
message.success('价格调整成功');
// 先执行回调刷新列表,再关闭抽屉
const onSuccessCallback = formData.value.onSuccess;
if (onSuccessCallback) {
onSuccessCallback();
}
// 延迟关闭,确保回调执行完成
setTimeout(() => {
drawerApi.close();
}, 100);
} catch (error: any) {
console.error('调整价格失败:', error);
message.error(error?.message || '调整价格失败,请重试');
} finally {
loading.value = false;
}
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = drawerApi.getData<typeof formData.value>();
if (data) {
formData.value = {
...data,
currentPrice: data.currentPrice ? Number(data.currentPrice) : undefined,
minAdjustPrice: data.minAdjustPrice ? Number(data.minAdjustPrice) : undefined,
};
newPrice.value = formData.value.currentPrice || null;
}
} else {
formData.value = {};
newPrice.value = null;
}
},
});
</script>
<template>
<DrawerComponent
title="调整商品价格"
:loading="loading"
>
<div v-if="formData.drugName" class="price-adjust-modal">
<Descriptions :column="1" bordered size="small" class="mb-4">
<Descriptions.Item label="药品名称">
{{ formData.drugName }}
</Descriptions.Item>
<Descriptions.Item label="当前价格">
{{ formData.currentPrice ? Number(formData.currentPrice).toFixed(4) : '0.0000' }}
</Descriptions.Item>
<Descriptions.Item label="最低调整金额">
<span v-if="formData.minAdjustPrice && Number(formData.minAdjustPrice) > 0">
{{ Number(formData.minAdjustPrice).toFixed(2) }}
</span>
<span v-else style="color: #999">未设置</span>
</Descriptions.Item>
</Descriptions>
<Form ref="formRef" :model="{ newPrice }" layout="vertical">
<Form.Item
label="新价格"
name="newPrice"
:rules="getPriceRules()"
>
<InputNumber
v-model:value="newPrice"
:min="formData.minAdjustPrice ? Number(formData.minAdjustPrice) : 0"
:precision="4"
:step="0.01"
style="width: 100%"
placeholder="请输入新价格"
/>
</Form.Item>
</Form>
</div>
</DrawerComponent>
</template>
<style scoped>
.price-adjust-modal {
padding: 8px 0;
}
</style>

View File

@@ -12,8 +12,8 @@ import { useUserStore } from '#/views/business/chat/stores/user';
import { usePrescriptionStore } from '#/store/prescription';
import PatientList from './components/PatientList.vue';
import PatientInfoPanel from './components/PatientInfoPanel.vue';
import { endConsultation } from './api/index.ts';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
import OrderListFloatButton from './components/OrderListFloatButton.vue';
import { endConsultation, getDrugsByRegisterId } from './api/index.ts';
const route = useRoute();
const router = useRouter();
@@ -280,16 +280,6 @@ const handleAddPatientProductsToPrescription = async () => {
return;
}
// 从聊天记录中筛选出所有 message_type=11 的消息(患者就诊经历)
const productMessages = chatStore.messages.filter(
(msg) => msg.message_type === 11
);
if (productMessages.length === 0) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 获取诊所ID
let storeId = prescriptionStore.myStoreId;
if (!storeId || storeId === 0) {
@@ -301,69 +291,25 @@ const handleAddPatientProductsToPrescription = async () => {
}
}
let addedCount = 0;
let skippedCount = 0;
const errors = [];
try {
// 遍历每条商品消息,添加到处方单
for (const msg of productMessages) {
try {
// 解析消息内容
let productData;
try {
productData = typeof msg.message_content === 'string'
? JSON.parse(msg.message_content)
: msg.message_content;
} catch (e) {
console.error('解析就诊经历消息失败:', e);
continue;
}
// 根据 register_id 获取药品信息
const res = await getDrugsByRegisterId(prescriptionStore.currentRegisterId, storeId);
const drugList = res?.result || res?.data || res || [];
// 获取药品名称(从就诊经历中提取 drug_name
const drugName = productData.drug_name;
if (!drugName) {
skippedCount++;
continue;
}
if (!drugList || drugList.length === 0) {
message.warning('未找到药品信息');
return;
}
// 通过药品名称搜索药品
const res = await getProductListDoctorReception({
store_id: storeId,
type: 2, // 中成(西)药
name: drugName,
});
if (!res || res.length === 0) {
errors.push(`未找到药品:${drugName}`);
skippedCount++;
continue;
}
// 使用第一个匹配的药品
const drugData = res[0];
// 检查药品是否已在处方中
const existItem = prescriptionStore.currentDrugs.find(
(item) => item.index_id === drugData.id,
);
if (existItem) {
skippedCount++;
continue;
}
// 添加药品到处方单
const success = prescriptionStore.addProducts(drugData);
if (success) {
addedCount++;
}
} catch (error) {
console.error('添加药品失败:', error);
errors.push(`添加药品失败:${error.message || '未知错误'}`);
// 遍历药品列表,添加到处方单
let addedCount = 0;
for (const drugData of drugList) {
const success = prescriptionStore.addProducts(drugData);
if (success) {
addedCount++;
}
}
// 显示结果消息
if (addedCount > 0) {
message.success(`已成功添加 ${addedCount} 个药品到处方单`);
// 打开处方模态框
@@ -372,89 +318,11 @@ const handleAddPatientProductsToPrescription = async () => {
onPrescriptionSent: handlePrescriptionSent,
});
prescriptionModalApi.open();
} else if (skippedCount > 0) {
// 如果所有药品都无法添加,尝试添加最新的那个
// 按时间排序取最新的药品created_at 或 timestamp 最大的)
const sortedMessages = [...productMessages].sort((a, b) => {
const timeA = a.created_at ? new Date(a.created_at).getTime() : (a.timestamp || 0);
const timeB = b.created_at ? new Date(b.created_at).getTime() : (b.timestamp || 0);
return timeB - timeA; // 降序排列,最新的在前
});
if (sortedMessages.length > 0) {
const latestMessage = sortedMessages[0];
try {
// 解析最新消息的内容
let productData;
try {
productData = typeof latestMessage.message_content === 'string'
? JSON.parse(latestMessage.message_content)
: latestMessage.message_content;
} catch (e) {
console.error('解析商品消息失败:', e);
message.warning('当前聊天没有可添加的药品');
return;
}
// 获取药品名称
const drugName = productData.drug_name || productData.name;
if (!drugName) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 通过药品名称搜索药品
const res = await getProductListDoctorReception({
store_id: storeId,
type: 2, // 中成(西)药
name: drugName,
});
if (!res || res.length === 0) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 使用第一个匹配的药品
const drugData = res[0];
// 检查药品是否已在处方中
const existItem = prescriptionStore.currentDrugs.find(
(item) => item.index_id === drugData.id,
);
if (existItem) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 添加药品到处方单
const success = prescriptionStore.addProducts(drugData);
if (success) {
message.success(`已成功添加 ${drugData.drug.drug_name} 到处方单`);
// 打开处方模态框
prescriptionModalApi.setData({
registerId: prescriptionStore.currentRegisterId,
onPrescriptionSent: handlePrescriptionSent,
});
prescriptionModalApi.open();
} else {
message.warning('当前聊天没有可添加的药品');
}
} catch (error) {
console.error('添加最新药品失败:', error);
message.warning('当前聊天没有可添加的药品');
}
} else {
message.warning('当前聊天没有可添加的药品');
}
}
// 如果有错误,显示错误信息
if (errors.length > 0) {
console.warn('添加药品时的错误:', errors);
} else {
message.warning('所有药品已在处方中');
}
} catch (error) {
console.error('批量添加药品失败:', error);
console.error('添加药品失败:', error);
message.error('添加药品失败,请重试');
}
};
@@ -476,6 +344,9 @@ const handleAddPatientProductsToPrescription = async () => {
<!-- 处方模态框 - 使用 VbenModal 模式 -->
<PrescriptionModalComponent />
<!-- 订单列表浮动按钮 -->
<OrderListFloatButton :register-id="registerId" />
<!-- 左侧患者列表 -->
<div class="left-panel-wrapper">
<Button

View File

@@ -15,6 +15,7 @@ import { TableAction } from '#/components/table-action';
// 导入药店管理相关的API直接使用store的API
import {
batchSyncDrugPriceApi,
deleteStore,
openPcWindowsApiByStore,
openQrCodeApi,
@@ -175,6 +176,26 @@ const updateSubscribe = (id: number) => {
gridApi.query();
});
};
/**
* 批量同步总仓库药品
*/
const batchSyncDrugPrice = () => {
const records = gridApi.grid.getCheckboxRecords();
if (records.length === 0) {
message.warning('请至少选择一条记录!');
return;
}
const ids = records.map((item) => item.id);
batchSyncDrugPriceApi(ids)
.then((res) => {
message.success(`批量同步任务已创建,共 ${res.data.success} 个门店的任务已提交!`);
gridApi.query();
})
.catch(() => {
message.error('批量同步失败!');
});
};
</script>
<template>
@@ -203,6 +224,15 @@ const updateSubscribe = (id: number) => {
confirm: deleteApi.bind(null, false),
},
},
{
label: '同步总仓库药品',
icon: 'ant-design:sync-outlined',
ifShow: hasTopTableDropDownActions,
popConfirm: {
title: '确定要同步选中门店的总仓库药品吗同步将更新价格上架下架状态并自动添加新药品',
confirm: batchSyncDrugPrice,
},
},
]"
>
<template #more>

View File

@@ -139,3 +139,13 @@ export async function updateStoreDrugPricesApi(data: {
}) {
return requestClient.post<any>(`${prefix}update-store-drug-prices`, data);
}
/**
* 批量同步总仓库药品
* @param ids 门店ID数组
*/
export async function batchSyncDrugPriceApi(ids: number[]) {
return requestClient.post<any>(`${prefix}batch-sync-drug-price`, {
ids,
});
}

View File

@@ -15,6 +15,7 @@ import QrCodePreview from '#/components/modal/QrCodePreview.vue';
import { TableAction } from '#/components/table-action';
import {
batchSyncDrugPriceApi,
deleteStore,
openPcWindowsApiByStore,
openQrCodeApi,
@@ -168,6 +169,26 @@ const updateSubscribe = (id: number) => {
gridApi.query();
});
};
/**
* 批量同步总仓库药品
*/
const batchSyncDrugPrice = () => {
const records = gridApi.grid.getCheckboxRecords();
if (records.length === 0) {
message.warning('请至少选择一条记录!');
return;
}
const ids = records.map((item) => item.id);
batchSyncDrugPriceApi(ids)
.then((res) => {
message.success(`批量同步任务已创建,共 ${res.data.success} 个门店的任务已提交!`);
gridApi.query();
})
.catch(() => {
message.error('批量同步失败!');
});
};
</script>
<template>
@@ -199,6 +220,15 @@ const updateSubscribe = (id: number) => {
confirm: deleteApi.bind(null, false),
},
},
{
label: '同步总仓库药品',
icon: 'ant-design:sync-outlined',
ifShow: hasTopTableDropDownActions,
popConfirm: {
title: '确定要同步选中门店的总仓库药品吗同步将更新价格上架下架状态并自动添加新药品',
confirm: batchSyncDrugPrice,
},
},
]"
>
<template #more>