1. 推广员功能增强
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-06-09 14:54:52 +08:00
parent a74f1cb106
commit 2f37bf2909
9 changed files with 484 additions and 29 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
@@ -90,6 +90,16 @@ const packageMethod = [
// 使用 Pinia store
const prescriptionStore = usePrescriptionStore();
const allowInsuranceCategory = computed(
() => Number(prescriptionStore.registerStoreInfo?.allow_insurance_category ?? 0) === 1,
);
watch(allowInsuranceCategory, (allow) => {
if (!allow) {
prescriptionStore.category = 1;
}
}, { immediate: true });
const submittingType = ref(false);
const [Modal, modalApi] = useVbenModal({
draggable: true,
@@ -861,7 +871,7 @@ const cancelSaveCommonPrescription = () => {
<!-- 费用类型选择和添加商品按钮 -->
<div class="mb-4 flex items-center justify-between">
<RadioGroup v-model:value="prescriptionStore.category">
<RadioGroup v-if="allowInsuranceCategory" v-model:value="prescriptionStore.category">
<RadioButton :value="1">自费</RadioButton>
<RadioButton :value="2">医保</RadioButton>
</RadioGroup>

View File

@@ -0,0 +1,27 @@
import { requestClient } from '#/api/request';
const prefix = 'transfer-prescription/';
export async function getSalespersonTransferListApi(params: Record<string, any>) {
return requestClient.get<any>(`${prefix}salesperson-list`, { params });
}
export async function getSalespersonTransferDetailApi(id: number) {
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
}
export async function getSalespersonTransferByRegisterApi(registerId: number) {
return requestClient.get<any>(`${prefix}get-salesperson-by-register`, {
params: { register_id: registerId },
});
}
export async function markSalespersonTransferImportedApi(
id: number,
registerId: number,
) {
return requestClient.post<any>(`${prefix}mark-salesperson-imported`, {
id,
register_id: registerId,
});
}

View File

@@ -0,0 +1,84 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { Modal, Table } from 'ant-design-vue';
import { getSalespersonTransferDetailApi } from '../api';
const visible = ref(false);
const loading = ref(false);
const detail = ref<any>(null);
const drugColumns = [
{ title: '药品名称', dataIndex: 'drug_name', key: 'drug_name', customRender: ({ record }: any) => record.drug_name || record.name || '-' },
{ title: '数量', dataIndex: 'number', key: 'number', width: 80 },
];
const drugList = computed(() => {
const d = detail.value;
if (!d) return [];
if (Array.isArray(d.drug_list) && d.drug_list.length) return d.drug_list;
if (d.prescription?.content && Array.isArray(d.prescription.content)) {
return d.prescription.content;
}
const content = d.content;
if (!content || typeof content !== 'object') return [];
const list: any[] = [];
if (Array.isArray(content.repice)) {
for (const recipe of content.repice) {
let rc = recipe.content;
if (typeof rc === 'string') {
try {
rc = JSON.parse(rc);
} catch {
rc = [];
}
}
if (Array.isArray(rc)) list.push(...rc);
}
}
return list;
});
async function open(id: number) {
visible.value = true;
loading.value = true;
try {
detail.value = await getSalespersonTransferDetailApi(id);
} finally {
loading.value = false;
}
}
function close() {
visible.value = false;
detail.value = null;
}
defineExpose({ open });
</script>
<template>
<Modal
v-model:open="visible"
title="传方药品详情"
width="720"
:footer="null"
destroy-on-close
@cancel="close"
>
<div v-if="detail" class="mb-3 text-sm text-gray-500">
<div>患者{{ detail.patient_name }} {{ detail.patient_mobile }}</div>
<div>诊断{{ detail.clinical_diagnose || '-' }}</div>
<div>医嘱{{ detail.doctor_order || '-' }}</div>
</div>
<Table
:loading="loading"
:columns="drugColumns"
:data-source="drugList"
:pagination="false"
row-key="drug_id"
size="small"
/>
</Modal>
</template>

View File

@@ -0,0 +1,82 @@
<script setup lang="ts">
import { h, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, Empty, Spin, Table, Tag } from 'ant-design-vue';
import DetailModal from './components/DetailModal.vue';
import { getSalespersonTransferListApi } from './api';
const loading = ref(true);
const list = ref<any[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(20);
const detailRef = ref<InstanceType<typeof DetailModal>>();
const columns = [
{ title: '诊所', dataIndex: 'store_name', key: 'store_name' },
{ title: '推广员', dataIndex: 'salesperson_name', key: 'salesperson_name' },
{ title: '患者', key: 'patient', customRender: ({ record }: any) => `${record.patient_name || ''} ${record.patient_mobile || ''}` },
{ title: '传方时间', dataIndex: 'transfer_time_text', key: 'transfer_time_text' },
{
title: '是否已导入',
key: 'is_imported',
customRender: ({ record }: any) =>
record.is_imported
? h(Tag, { color: 'success' }, () => '已导入')
: h(Tag, { color: 'warning' }, () => '待导入'),
},
{
title: '处方',
key: 'action',
customRender: ({ record }: any) =>
h(Button, { type: 'link', onClick: () => detailRef.value?.open(record.id) }, () => '查看'),
},
];
async function loadList() {
loading.value = true;
try {
const res = await getSalespersonTransferListApi({
page: page.value,
pageSize: pageSize.value,
});
list.value = res?.items ?? [];
total.value = res?.total ?? 0;
} finally {
loading.value = false;
}
}
onMounted(loadList);
function onPageChange(p: number, ps: number) {
page.value = p;
pageSize.value = ps;
loadList();
}
</script>
<template>
<Page auto-content-height title="传方记录">
<Spin :spinning="loading">
<Empty v-if="!loading && !list.length" description="暂无传方记录" />
<Table
v-else
:columns="columns"
:data-source="list"
:pagination="{
current: page,
pageSize,
total,
showSizeChanger: true,
onChange: onPageChange,
}"
row-key="id"
/>
</Spin>
<DetailModal ref="detailRef" />
</Page>
</template>

View File

@@ -0,0 +1,109 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { Button, Drawer, Empty, List, message, Tag } from 'ant-design-vue';
import { getSalespersonTransferByRegisterApi } from '#/views/business/salesperson-transfer-prescription/api';
const props = defineProps<{
open: boolean;
registerId: number;
}>();
const emit = defineEmits<{
'update:open': [boolean];
import: [any];
}>();
const loading = ref(false);
const list = ref<any[]>([]);
function parseDrugList(item: any) {
if (item.drug_list?.length) return item.drug_list;
if (item.prescription?.content?.length) return item.prescription.content;
const content = item.content;
if (!content?.repice) return [];
const drugs: any[] = [];
for (const recipe of content.repice) {
let rc = recipe.content;
if (typeof rc === 'string') {
try {
rc = JSON.parse(rc);
} catch {
rc = [];
}
}
if (Array.isArray(rc)) drugs.push(...rc);
}
return drugs;
}
async function loadList() {
if (!props.registerId) return;
loading.value = true;
try {
const res = await getSalespersonTransferByRegisterApi(props.registerId);
list.value = Array.isArray(res) ? res : [];
} catch (e) {
console.error(e);
list.value = [];
} finally {
loading.value = false;
}
}
watch(
() => [props.open, props.registerId],
([open]) => {
if (open) loadList();
},
);
function matchTypeLabel(matchType?: string) {
if (matchType === 'mobile') return '手机号匹配';
if (matchType === 'store_pending') return '本店待导入';
return '';
}
function handleImport(item: any) {
const drugList = parseDrugList(item);
if (!drugList.length) {
message.warning('传方药品为空');
return;
}
emit('import', {
transferId: item.id,
clinical_diagnose: item.clinical_diagnose || item.content?.clinical_diagnose || '',
doctor_order: item.doctor_order || item.content?.doctor_order || '',
drugList,
});
emit('update:open', false);
}
function close() {
emit('update:open', false);
}
</script>
<template>
<Drawer :open="open" title="推广员传方记录" width="560" @close="close">
<Empty v-if="!loading && !list.length" description="暂无待导入传方" />
<List v-else :loading="loading" :data-source="list" item-layout="horizontal">
<template #renderItem="{ item }">
<List.Item>
<List.Item.Meta
:title="`${item.patient_name || ''} ${item.patient_mobile || ''}`"
:description="`推广员:${item.salesperson_name || '-'} · ${item.transfer_time_text || ''}`"
/>
<template #actions>
<Tag v-if="matchTypeLabel(item.match_type)" :color="item.match_type === 'mobile' ? 'success' : 'processing'">
{{ matchTypeLabel(item.match_type) }}
</Tag>
<Tag color="warning">待导入</Tag>
<Button type="link" @click="handleImport(item)">导入</Button>
</template>
</List.Item>
</template>
</List>
</Drawer>
</template>

View File

@@ -73,7 +73,9 @@ import {
} from '#/views/doctor/settings/api';
// 转诊相关
import TransferPrescriptionCard from '#/views/doctor/online-consultation/components/TransferPrescriptionCard.vue';
import SalespersonTransferDrawer from '#/views/doctor/doctor-reception/components/SalespersonTransferDrawer.vue';
import { getTransferPrescriptionByRegisterApi } from '#/views/business/chat/api/transferPrescription';
import { getSalespersonTransferByRegisterApi } from '#/views/business/salesperson-transfer-prescription/api';
import {
calcItemMarginPercent,
calcTotalMarginPercent,
@@ -135,6 +137,23 @@ const userStore = useUserStore();
const myStoreId = ref(userStore.userInfo.store_id);
/** 当前取价门店是否允许查看毛利率0/1 */
const seeRate = ref(0);
/** 开方是否可选医保0=仅自费) */
const allowInsuranceCategory = ref(0);
/** 推广员传方导入关联ID */
const salespersonTransferPrescriptionId = ref(0);
const salespersonTransferDrawerOpen = ref(false);
const hasSalespersonTransfer = ref(false);
/** 当前接诊挂号 ID模板中不可直接使用 localStorage */
const doctorReceptionRegisterId = computed(() => {
if (typeof window === 'undefined' || !window.localStorage) {
return 0;
}
return Number.parseInt(
window.localStorage.getItem('doctorReception-id') || '0',
10,
);
});
async function fetchStoreSeeRate() {
try {
@@ -150,11 +169,58 @@ async function fetchStoreSeeRate() {
}
const res = await getCurrentStoreTypeApi(params);
seeRate.value = Number(res?.see_rate ?? 0);
allowInsuranceCategory.value = Number(res?.allow_insurance_category ?? 0);
if (allowInsuranceCategory.value !== 1) {
category.value = 1;
}
} catch (error) {
console.error('获取毛利率权限失败:', error);
}
}
async function checkSalespersonTransfer() {
const registerId = Number.parseInt(
localStorage.getItem('doctorReception-id') || '0',
10,
);
if (!registerId || activeCategory.value !== 1) {
hasSalespersonTransfer.value = false;
return;
}
try {
const res = await getSalespersonTransferByRegisterApi(registerId);
hasSalespersonTransfer.value = Array.isArray(res) && res.length > 0;
} catch {
hasSalespersonTransfer.value = false;
}
}
function handleSalespersonTransferImport(payload: {
transferId: number;
clinical_diagnose: string;
doctor_order: string;
drugList: any[];
}) {
salespersonTransferPrescriptionId.value = payload.transferId;
diagnosis.value = payload.clinical_diagnose || '';
medicalAdvice.value = payload.doctor_order || '';
activeCategory.value = 1;
tabType.value = 2;
currentDrugs.value = payload.drugList.map((drug: any) => ({
index_id: drug.index_id ?? drug.drug_id ?? drug.id,
id: drug.drug_id || drug.id,
drug_id: drug.drug_id || drug.id,
drug_name: drug.drug_name || drug.name,
name: drug.drug_name || drug.name,
number: drug.number || 1,
way_id: drug.way_id || 0,
price: drug.price ?? 0,
buy_price: drug.buy_price,
}));
updateLocalStorage();
message.success('传方已导入,请确认后发送处方');
}
watch(myStoreId, () => {
fetchStoreSeeRate();
});
@@ -411,7 +477,7 @@ const selectPatient = (patient: Patient, isUpdateTabType = true) => {
}
}
activePatient.value = patient.user_patient;
// 恢复之前保存的 activeCategory
const savedCategory = localStorage.getItem(`activeCategory${patient.user_patient?.id}`);
if (savedCategory) {
@@ -431,8 +497,9 @@ const selectPatient = (patient: Patient, isUpdateTabType = true) => {
// 如果恢复的处方类型是中药,检测并显示转诊提示
if (activeCategory.value === 1) {
checkAndShowTransferTip();
checkSalespersonTransfer();
}
getPatientItem(patient.id).then((value) => {
patientInfo.value = value;
userPatientHealthInquiry.value = value.user_patient_health_inquiry;
@@ -638,13 +705,14 @@ const sendPrescription = () => {
day_dosage: dayDosage.value,
// 是否二次签名
doctor_second_sign: doctorSecondSign.value,
salesperson_transfer_prescription_id: salespersonTransferPrescriptionId.value || undefined,
}).then((res) => {
message.success('处方已发送');
// 检查是否需要转诊
const needTransfer = res?.result?.need_transfer || res?.data?.need_transfer || res?.need_transfer;
const transferPrescriptionId = res?.result?.transfer_prescription_id || res?.data?.transfer_prescription_id || res?.transfer_prescription_id;
if (needTransfer && transferPrescriptionId) {
// 如果需要转诊,获取转诊信息
fetchTransferPrescription(transferPrescriptionId);
@@ -653,7 +721,7 @@ const sendPrescription = () => {
// 不需要转诊,清空转诊信息
transferPrescription.value = null;
}
// 清空当前数据
currentDrugs.value = [];
diagnosis.value = '';
@@ -663,9 +731,11 @@ const sendPrescription = () => {
dosage.value = 7;
dayDosage.value = 2;
tabType.value = 1;
salespersonTransferPrescriptionId.value = 0;
newDrugInfo.value = {};
updateLocalStorage();
checkSalespersonTransfer();
// 刷新患者信息
if (selectPatientId.value) {
getPatientItem(selectPatientId.value).then((value) => {
@@ -842,7 +912,7 @@ async function handleSelectCommonPrescription(data: any, type: number) {
dosage.value = prescription.dosage ?? 7;
dayDosage.value = prescription.day_dosage ?? 2;
packageMethodId.value = prescription.package_method_id ?? 2;
// 如果是委托调剂,写入完整加工规则配置 - 只有有效值才写入大于0级联加载
if (prescription.process_rule_id && prescription.process_rule_id > 0) {
processRuleId.value = prescription.process_rule_id;
@@ -1039,11 +1109,14 @@ function tabChange(id) {
getStorageKey(activePatient.value?.id, activeCategory.value),
JSON.stringify(currentDrugs.value),
);
// 更新Tab类型
localStorage.setItem(`activeCategory${activePatient.value?.id}`, id);
activeCategory.value = id;
if (id === 1) {
checkSalespersonTransfer();
}
// 加载新Tab对应的药品数据
currentDrugs.value = JSON.parse(
localStorage.getItem(getStorageKey(activePatient.value?.id, id)) || '[]',
@@ -1873,7 +1946,7 @@ const newDrugInfo = ref({});
*/
const fetchTransferPrescription = async (transferPrescriptionId: number) => {
if (!transferPrescriptionId) return;
loadingTransfer.value = true;
try {
// 通过转诊处方ID获取转诊信息
@@ -2171,13 +2244,13 @@ watch(
item.created_at
}}</span>
<Tag class="ml-5" :color="item.category === 1? '' : '#455cda'">
{{
item.prescription_type === 1 ? '中药处方' :
item.prescription_type === 2 ? '西药处方' :
item.prescription_type === 3 ? '保健食品' :
item.prescription_type === 5 ? '产品服务包' :
item.prescription_type === 6 ? '非药品' :
item.prescription_type === 7 ? '医疗器械' :
{{
item.prescription_type === 1 ? '中药处方' :
item.prescription_type === 2 ? '西药处方' :
item.prescription_type === 3 ? '保健食品' :
item.prescription_type === 5 ? '产品服务包' :
item.prescription_type === 6 ? '非药品' :
item.prescription_type === 7 ? '医疗器械' :
'未知类型'
}} /
{{ item.category === 1 ? '自费' : '医保' }}
@@ -2207,12 +2280,12 @@ watch(
</Timeline>
</Descriptions.Item>
</Descriptions>
<!-- 转诊处方卡片 -->
<div v-if="transferPrescription" class="transfer-section mt-5">
<TransferPrescriptionCard
:transfer-data="transferPrescription"
:register-id="Number.parseInt(localStorage.getItem(`doctorReception-id`))"
:register-id="doctorReceptionRegisterId"
@import-success="handleTransferImportSuccess"
/>
</div>
@@ -2266,7 +2339,16 @@ watch(
<SaveOutlined />
保存常用方
</Button>
<Button
v-if="activeCategory === 1 && hasSalespersonTransfer"
type="default"
class="ml-3"
@click="salespersonTransferDrawerOpen = true"
>
查看传方
</Button>
<RadioGroup
v-if="allowInsuranceCategory === 1"
v-model:value="category"
class="ml-5"
>
@@ -2274,6 +2356,11 @@ watch(
<RadioButton :value="2">医保</RadioButton>
</RadioGroup>
</div>
<SalespersonTransferDrawer
v-model:open="salespersonTransferDrawerOpen"
:register-id="doctorReceptionRegisterId"
@import="handleSalespersonTransferImport"
/>
<div style="width: 0; height: 0; overflow: hidden">
<ImagePreviewGroup
:preview="{
@@ -2921,20 +3008,20 @@ watch(
</div>
<div class="total-cost">总计:¥{{ totalCost.toFixed(2) }}</div>
<Button
<Button
v-if="activeCategory === 1"
class="mt-5 w-full"
style="display: block"
type="primary"
class="mt-5 w-full"
style="display: block"
type="primary"
@click="checkChineseMedicineConflict"
>
发送处方
</Button>
<Button
<Button
v-else
class="mt-5 w-full"
style="display: block"
type="primary"
class="mt-5 w-full"
style="display: block"
type="primary"
@click="sendPrescription"
>
发送处方

View File

@@ -166,6 +166,14 @@ export async function updateClinicTypeApi(data: { id: number; clinic_type: numbe
return requestClient.post<any>(`${prefix}update-clinic-type`, data);
}
export async function updateAllowInsuranceCategoryApi(data: { id: number }) {
return requestClient.post<any>(`${prefix}update-allow-insurance-category`, data);
}
export async function toggleSalespersonSeePriceApi(data: { store_id: number }) {
return requestClient.post<any>('salesperson-store-config/toggle-see-price', data);
}
/**
* 单独更新 ERP ID 或 MES ID列表快捷编辑
*/

View File

@@ -61,6 +61,20 @@ export const gridOptions: VxeGridProps<RowType> = {
slots: { default: 'see_rate' },
width: 120,
},
{
field: 'allow_insurance_category',
align: 'left',
title: '医保可选',
slots: { default: 'allow_insurance_category' },
width: 110,
},
{
field: 'salesperson_see_price',
align: 'left',
title: '推广员可见价格',
slots: { default: 'salesperson_see_price' },
width: 130,
},
{
field: 'qr_code',
align: 'left',

View File

@@ -20,7 +20,9 @@ import {
deleteStore,
openPcWindowsApiByStore,
openQrCodeApi,
updateAllowInsuranceCategoryApi,
updateClinicTypeApi,
toggleSalespersonSeePriceApi,
updateStoreShippingFree,
updateStoreSeeRateStatus,
updateStoreSubscribeStatus,
@@ -215,6 +217,20 @@ const updateSeeRate = (id: number) => {
});
};
const updateAllowInsurance = (id: number) => {
updateAllowInsuranceCategoryApi({ id }).then(() => {
message.success('修改成功!');
gridApi.query();
});
};
const updateSalespersonSeePrice = (storeId: number) => {
toggleSalespersonSeePriceApi({ store_id: storeId }).then(() => {
message.success('修改成功!');
gridApi.query();
});
};
/**
* 批量同步总仓库药品
*/
@@ -440,6 +456,24 @@ const handleSwitchClinicType = (row: any) => {
{{ Number(row.see_rate) === 1 ? '可查看' : '不可查看' }}
</Tag>
</template>
<template #allow_insurance_category="{ row }">
<Tag
:color="Number(row.allow_insurance_category) === 1 ? 'success' : 'default'"
style="cursor: pointer"
@click="updateAllowInsurance(row.id)"
>
{{ Number(row.allow_insurance_category) === 1 ? '可选医保' : '仅自费' }}
</Tag>
</template>
<template #salesperson_see_price="{ row }">
<Tag
:color="Number(row.salesperson_see_price) === 1 ? 'success' : 'default'"
style="cursor: pointer"
@click="updateSalespersonSeePrice(row.id)"
>
{{ Number(row.salesperson_see_price) === 1 ? '可见' : '不可见' }}
</Tag>
</template>
<template #start-time="{ row }">
<Tag color="success">{{ row.start_time }}</Tag>
<br />