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-07-03 16:50:16 +08:00
parent c8b15cd9dd
commit 7907c7cbf2
12 changed files with 1313 additions and 104 deletions

View File

@@ -33,3 +33,18 @@ export async function deleteSpecialPrescription(data: Record<string, any>) {
export async function applySpecialPrescriptionApi(data: { register_id: number }) {
return requestClient.post<any>(`${prefix}apply`, data);
}
/** 自动均分药品单价 */
export async function distributeDrugPricesApi(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}distribute-drug-prices`, data);
}
/** 价格配置详情(路由与 cc_auto_route_register 生成的 price-config-detail 对齐) */
export async function getSpecialPrescriptionPriceConfig(id: number) {
return requestClient.get<any>(`${prefix}price-config-detail`, { params: { id } });
}
/** 保存价格配置 */
export async function saveSpecialPrescriptionPriceConfig(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}save-price-config`, data);
}

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { nextTick, onMounted, ref } from 'vue';
import { computed, nextTick, onMounted, ref } from 'vue';
import { useUserStore } from '@vben/stores';
@@ -22,6 +22,8 @@ import {
getProductListDoctorReception,
} from '#/views/doctor/doctor-reception/api';
import PrescriptionDiagnosisOrderCards from './PrescriptionDiagnosisOrderCards.vue';
export interface ChineseDrugItem {
_key?: number;
id: number;
@@ -29,6 +31,7 @@ export interface ChineseDrugItem {
drug_name: string;
number: number;
price: number;
buy_price?: number;
way_id: number;
}
@@ -37,11 +40,26 @@ const props = withDefaults(
modelValue?: ChineseDrugItem[];
dosage?: number;
dayDosage?: number;
/** 固定价/SKU方案下允许编辑单价 */
priceEditable?: boolean;
/** 价格配置弹窗:药名/克数/用法只读,仅价格可改 */
structureReadonly?: boolean;
/** 预选诊断(导入特色方时带入) */
clinicalDiagnose?: string;
/** 预选医嘱 */
doctorOrder?: string;
/** 是否展示诊断/医嘱气泡卡(价格配置等场景可关闭) */
showDiagnosisOrder?: boolean;
}>(),
{
modelValue: () => [],
dosage: 7,
dayDosage: 2,
priceEditable: false,
structureReadonly: false,
clinicalDiagnose: '',
doctorOrder: '',
showDiagnosisOrder: true,
},
);
@@ -49,12 +67,33 @@ const emit = defineEmits<{
'update:modelValue': [value: ChineseDrugItem[]];
'update:dosage': [value: number];
'update:dayDosage': [value: number];
'update:clinicalDiagnose': [value: string];
'update:doctorOrder': [value: string];
}>();
const userStore = useUserStore();
const drugList = ref<ChineseDrugItem[]>([]);
const dosageLocal = ref(props.dosage);
const dayDosageLocal = ref(props.dayDosage);
const diagnosisOrderCardsRef = ref<InstanceType<typeof PrescriptionDiagnosisOrderCards>>();
/** 价格配置只读模式下不展示诊断/医嘱区 */
const showDiagnosisOrderPanel = computed(
() => props.showDiagnosisOrder && !props.structureReadonly,
);
/** 预选诊断双向绑定 */
const clinicalDiagnoseModel = computed({
get: () => props.clinicalDiagnose ?? '',
set: (val: string) => emit('update:clinicalDiagnose', val),
});
/** 预选医嘱双向绑定 */
const doctorOrderModel = computed({
get: () => props.doctorOrder ?? '',
set: (val: string) => emit('update:doctorOrder', val),
});
const drugUseWay = ref<any[]>([]);
const chineseSearchResults = ref<any[]>([]);
const newDrugInfo = ref<{
@@ -75,6 +114,12 @@ onMounted(async () => {
}
});
function getWayName(wayId: number) {
if (!wayId) return '煎服';
const item = drugUseWay.value.find((w) => Number(w.id) === Number(wayId));
return item?.name || '煎服';
}
function syncToParent() {
emit('update:modelValue', [...drugList.value]);
emit('update:dosage', dosageLocal.value);
@@ -200,21 +245,66 @@ function onDosageChange() {
syncToParent();
}
function loadDrugs(recipes: any[], dosage?: number, dayDosage?: number) {
drugList.value = (recipes || []).map((recipe) => ({
_key: Date.now() + Math.random(),
id: recipe.drug_id || recipe.id,
drug_id: recipe.drug_id || recipe.id,
drug_name: recipe.drug_name || recipe.name || '',
number: recipe.number || 1,
price: recipe.price || 0,
way_id: recipe.way_id || 0,
}));
function loadDrugs(recipes: any[], dosage?: number, dayDosage?: number, drugPrices?: any[]) {
const priceMap = new Map<number, { buy_price: number; sell_price: number }>();
(drugPrices || []).forEach((item) => {
priceMap.set(Number(item.drug_id), {
buy_price: Number(item.buy_price || 0),
sell_price: Number(item.sell_price || item.price || 0),
});
});
drugList.value = (recipes || []).map((recipe) => {
const drugId = recipe.drug_id || recipe.id;
const configured = priceMap.get(Number(drugId));
return {
_key: Date.now() + Math.random(),
id: drugId,
drug_id: drugId,
drug_name: recipe.drug_name || recipe.name || '',
number: recipe.number || 1,
price: configured?.sell_price ?? recipe.price ?? 0,
buy_price: configured?.buy_price ?? recipe.buy_price ?? 0,
way_id: recipe.way_id || 0,
};
});
if (dosage !== undefined) dosageLocal.value = dosage;
if (dayDosage !== undefined) dayDosageLocal.value = dayDosage;
syncToParent();
}
function applyDistributedPrices(items: Array<{ drug_id: number; buy_price: number | string; sell_price: number | string }>) {
const priceMap = new Map<number, { buy_price: number; sell_price: number }>();
items.forEach((item) => {
priceMap.set(Number(item.drug_id), {
buy_price: Number(item.buy_price || 0),
sell_price: Number(item.sell_price || 0),
});
});
drugList.value.forEach((drug) => {
const configured = priceMap.get(Number(drug.drug_id || drug.id));
if (configured) {
drug.price = configured.sell_price;
drug.buy_price = configured.buy_price;
}
});
syncToParent();
}
function getDrugPricesPayload() {
return drugList.value.map((drug) => ({
drug_id: drug.drug_id || drug.id,
buy_price: drug.buy_price ?? 0,
sell_price: drug.price ?? 0,
}));
}
function getDrugsForDistribute() {
return drugList.value.map((drug) => ({
drug_id: drug.drug_id || drug.id,
number: drug.number || 1,
}));
}
function getDrugsPayload() {
return drugList.value.map((drug) => ({
drug_id: drug.drug_id || drug.id,
@@ -227,14 +317,22 @@ function getDrugsPayload() {
defineExpose({
getDrugsPayload,
getDrugPricesPayload,
getDrugsForDistribute,
applyDistributedPrices,
loadDrugs,
drugList,
getDiagnosisOrderPayload: () =>
diagnosisOrderCardsRef.value?.getDiagnosisOrderPayload?.() ?? {
clinical_diagnose: props.clinicalDiagnose || '',
doctor_order: props.doctorOrder || '',
},
});
</script>
<template>
<div class="chinese-drug-editor">
<Row :gutter="16" class="mb-4">
<Row v-if="!structureReadonly" :gutter="16" class="mb-4">
<Col :span="12">
<FormItem label="剂量(天/剂)">
<InputNumber
@@ -258,8 +356,17 @@ defineExpose({
</FormItem>
</Col>
</Row>
<div v-else-if="dosageLocal || dayDosageLocal" class="readonly-meta mb-3 text-gray-500">
剂量 {{ dosageLocal }} / · 频次 {{ dayDosageLocal }} /
</div>
<FormItem label="药品列表" required>
<FormItem :label="structureReadonly ? '' : '药品列表'" :required="!structureReadonly">
<PrescriptionDiagnosisOrderCards
v-if="showDiagnosisOrderPanel"
ref="diagnosisOrderCardsRef"
v-model:clinical-diagnose="clinicalDiagnoseModel"
v-model:doctor-order="doctorOrderModel"
/>
<Row :gutter="[12, 12]">
<Col
v-for="(drug, index) in drugList"
@@ -272,7 +379,14 @@ defineExpose({
<Card size="small" class="chinese-drug-card">
<div class="chinese-drug-content">
<span class="chinese-drug-index">{{ index + 1 }}</span>
<div class="chinese-drug-form">
<div v-if="structureReadonly" class="chinese-drug-readonly">
<span class="chinese-drug-name-text">{{ drug.drug_name }}</span>
<span class="chinese-drug-comma"></span>
<span>{{ drug.number }}g</span>
<span class="chinese-drug-comma"></span>
<span>{{ getWayName(drug.way_id) }}</span>
</div>
<div v-else class="chinese-drug-form">
<Select
v-model:value="drug.id"
show-search
@@ -322,16 +436,50 @@ defineExpose({
</Select>
</div>
<div class="chinese-drug-price">
{{ ((drug.price || 0) * (drug.number || 1)).toFixed(2) }}
<template v-if="priceEditable">
<div class="price-edit-row">
<span></span>
<InputNumber
v-model:value="drug.price"
:min="0"
:precision="4"
:controls="false"
size="small"
style="width: 72px"
@change="onDrugNumberChange"
/>
<span></span>
<InputNumber
v-model:value="drug.buy_price"
:min="0"
:precision="4"
:controls="false"
size="small"
style="width: 72px"
@change="onDrugNumberChange"
/>
</div>
<div class="price-line-total">
小计 {{ ((drug.price || 0) * (drug.number || 1)).toFixed(2) }}
</div>
</template>
<template v-else>
{{ ((drug.price || 0) * (drug.number || 1)).toFixed(2) }}
</template>
</div>
<Button type="link" class="chinese-drug-delete" @click="removeDrug(index)">
<Button
v-if="!structureReadonly"
type="link"
class="chinese-drug-delete"
@click="removeDrug(index)"
>
<DeleteTwoTone two-tone-color="#ff4d4f" />
</Button>
</div>
</Card>
</Col>
<Col :xs="24" :sm="12" :md="8" :lg="8">
<Col v-if="!structureReadonly" :xs="24" :sm="12" :md="8" :lg="8">
<Card size="small" class="chinese-drug-card chinese-drug-card-new">
<div class="chinese-drug-content">
<span class="chinese-drug-index">{{ drugList.length + 1 }}</span>
@@ -427,6 +575,16 @@ defineExpose({
max-width: 120px;
}
.chinese-drug-readonly {
display: inline;
padding-left: 24px;
line-height: 1.6;
}
.chinese-drug-name-text {
font-weight: 500;
}
.chinese-drug-comma {
@apply text-muted-foreground;
}
@@ -440,6 +598,19 @@ defineExpose({
right: 24px;
bottom: 0;
@apply text-destructive font-medium;
text-align: right;
}
.price-edit-row {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
}
.price-line-total {
font-size: 12px;
margin-top: 2px;
}
.chinese-drug-delete {

View File

@@ -18,6 +18,8 @@ const prescriptionType = ref<'chinese' | 'west' | 'granular'>('chinese');
const chineseDrugEditorRef = ref<InstanceType<typeof ChineseDrugEditor>>();
const dosage = ref(7);
const dayDosage = ref(2);
const clinicalDiagnose = ref('');
const doctorOrder = ref('');
const [Modal, modalApi] = useVbenModal({
fullscreenButton: true,
@@ -38,12 +40,19 @@ const [Modal, modalApi] = useVbenModal({
message.warning('请至少添加一种中药');
return;
}
const diagnosisOrder =
chineseDrugEditorRef.value?.getDiagnosisOrderPayload?.() ?? {
clinical_diagnose: clinicalDiagnose.value,
doctor_order: doctorOrder.value,
};
const payload = buildSpecialPrescriptionPayload(detailRes.value, {
drugs,
dosage: dosage.value,
day_dosage: dayDosage.value,
rule_type: 1,
package_method_id: 2,
clinical_diagnose: diagnosisOrder.clinical_diagnose ?? '',
doctor_order: diagnosisOrder.doctor_order ?? '',
});
modalApi.setState({ loading: true, confirmLoading: true });
updateSpecialPrescription(payload)
@@ -65,12 +74,15 @@ const [Modal, modalApi] = useVbenModal({
if (!res) return;
detailRes.value = res;
prescriptionType.value = res.prescription_type || 'chinese';
clinicalDiagnose.value = res.clinical_diagnose || '';
doctorOrder.value = res.doctor_order || '';
if (res.prescription_type === 'chinese' && res.prescription_detail?.length) {
const first = res.prescription_detail[0];
chineseDrugEditorRef.value?.loadDrugs(
res.prescription_detail,
first?.dosage ?? 7,
first?.consumption ?? 2,
res.drug_prices || [],
);
dosage.value = first?.dosage ?? 7;
dayDosage.value = first?.consumption ?? 2;
@@ -80,6 +92,8 @@ const [Modal, modalApi] = useVbenModal({
});
} else {
detailRes.value = null;
clinicalDiagnose.value = '';
doctorOrder.value = '';
}
},
});
@@ -90,6 +104,8 @@ const [Modal, modalApi] = useVbenModal({
<div v-if="prescriptionType === 'chinese'">
<ChineseDrugEditor
ref="chineseDrugEditorRef"
v-model:clinical-diagnose="clinicalDiagnose"
v-model:doctor-order="doctorOrder"
v-model:dosage="dosage"
v-model:day-dosage="dayDosage"
/>

View File

@@ -0,0 +1,126 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Card, Col, Row, Textarea } from 'ant-design-vue';
import DiagnosisModal from '#/views/doctor/doctor-reception/components/DiagnosisModal.vue';
import DoctorOrderModal from '#/views/doctor/doctor-reception/components/DoctorOrderModal.vue';
/** 预选诊断、预选医嘱(与接诊开方交互一致) */
const clinicalDiagnose = defineModel<string>('clinicalDiagnose', { default: '' });
const doctorOrder = defineModel<string>('doctorOrder', { default: '' });
const [DiagnosisModals, DiagnosisModalApi] = useVbenModal({
connectedComponent: DiagnosisModal,
});
const [DoctorOrderModals, DoctorOrderModalApi] = useVbenModal({
connectedComponent: DoctorOrderModal,
});
/** 打开常用诊断弹窗 */
function openDiagnosisModal() {
DiagnosisModalApi.setData({
values: clinicalDiagnose.value,
updateDiagnosis,
});
DiagnosisModalApi.open();
}
/** 常用诊断弹窗回调:写回预选诊断 */
function updateDiagnosis(values: string) {
clinicalDiagnose.value = values;
}
/** 打开常用医嘱弹窗 */
function openDoctorOrderModal() {
DoctorOrderModalApi.setData({
values: doctorOrder.value,
updateDoctorOrder,
});
DoctorOrderModalApi.open();
}
/** 常用医嘱弹窗回调:写回预选医嘱 */
function updateDoctorOrder(values: string) {
doctorOrder.value = values;
}
/** 供父组件读取当前值 */
const payload = computed(() => ({
clinical_diagnose: clinicalDiagnose.value || '',
doctor_order: doctorOrder.value || '',
}));
defineExpose({
getDiagnosisOrderPayload: () => payload.value,
});
</script>
<template>
<Row :gutter="[12, 12]" class="prescription-meta-row mb-3">
<Col :xs="24" :md="12">
<Card size="small" class="chinese-drug-card prescription-meta-card">
<div class="prescription-meta-card__title">预选诊断</div>
<Button type="primary" class="mb-2" @click="openDiagnosisModal">
常用诊断
</Button>
<Textarea
v-model:value="clinicalDiagnose"
placeholder="输入诊断结果..."
:rows="3"
/>
</Card>
</Col>
<Col :xs="24" :md="12">
<Card size="small" class="chinese-drug-card prescription-meta-card">
<div class="prescription-meta-card__title">预选医嘱</div>
<Button type="primary" class="mb-2" @click="openDoctorOrderModal">
常用医嘱
</Button>
<Textarea
v-model:value="doctorOrder"
placeholder="输入医嘱..."
:rows="3"
/>
</Card>
</Col>
</Row>
<DiagnosisModals />
<DoctorOrderModals />
</template>
<style lang="scss" scoped>
.chinese-drug-card {
position: relative;
:deep(.ant-card-body) {
padding: 12px;
}
}
.prescription-meta-card {
min-height: 140px;
:deep(.ant-card-body) {
padding: 16px;
}
}
.prescription-meta-card__title {
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
}
.prescription-meta-row :deep(textarea) {
width: 100%;
min-height: 72px;
padding: 8px;
border: 1px solid hsl(var(--border));
border-radius: 4px;
resize: vertical;
}
</style>

View File

@@ -0,0 +1,339 @@
<script lang="ts" setup>
import { computed, nextTick, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Col, InputNumber, message, Radio, RadioGroup, Row } from 'ant-design-vue';
import ChineseDrugEditor from './ChineseDrugEditor.vue';
import SkuEditor, { type SkuItem } from './SkuEditor.vue';
import {
distributeDrugPricesApi,
getSpecialPrescriptionPriceConfig,
saveSpecialPrescriptionPriceConfig,
} from '../api';
/** 药品分摊价条目 */
interface DrugPriceItem {
drug_id: number;
buy_price: number;
sell_price: number;
}
const gridApi = ref<any>();
const configId = ref(0);
const configName = ref('');
const priceCalcScheme = ref(3);
const buyPricePerDose = ref(0);
const salePricePerDose = ref(0);
const skus = ref<SkuItem[]>([]);
const chineseDrugEditorRef = ref<InstanceType<typeof ChineseDrugEditor>>();
const skuEditorRef = ref<InstanceType<typeof SkuEditor>>();
/** 当前分摊视图0=单帖,>0=SKU id */
const activeSkuId = ref(0);
/** 各 scope 分摊价缓存 sku_id -> prices[] */
const drugPricesCache = ref<Record<number, DrugPriceItem[]>>({});
const prescriptionDetailCache = ref<any[]>([]);
const prescriptionDosage = ref(7);
const prescriptionDayDosage = ref(2);
const showFixedPriceBlock = computed(() => priceCalcScheme.value === 1);
/** 左侧标题:单帖或 SKU 名称 */
const allocationTitle = computed(() => {
if (activeSkuId.value <= 0) {
return '单帖均摊价格';
}
const sku = skus.value.find((item) => Number(item.id) === Number(activeSkuId.value));
const name = sku?.sku_name || `${sku?.dose_count || ''}`;
return `${name} 均摊价格`;
});
const [Modal, modalApi] = useVbenModal({
fullscreenButton: true,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
if (configId.value <= 0) {
return;
}
persistCurrentScopePrices();
const payload: Record<string, any> = {
id: configId.value,
price_calc_scheme: priceCalcScheme.value,
skus: skuEditorRef.value?.getSkusPayload() || skus.value,
};
if (priceCalcScheme.value === 1) {
payload.buy_price_per_dose = buyPricePerDose.value;
payload.sale_price_per_dose = salePricePerDose.value;
payload.drug_prices = drugPricesCache.value[0] || [];
const skuDrugPrices: Array<{ sku_id: number; drug_prices: DrugPriceItem[] }> = [];
Object.keys(drugPricesCache.value).forEach((key) => {
const skuId = Number(key);
if (skuId > 0 && drugPricesCache.value[skuId]?.length) {
skuDrugPrices.push({
sku_id: skuId,
drug_prices: drugPricesCache.value[skuId],
});
}
});
payload.sku_drug_prices = skuDrugPrices;
}
modalApi.setState({ loading: true, confirmLoading: true });
saveSpecialPrescriptionPriceConfig(payload)
.then(() => {
message.success('价格配置保存成功');
gridApi.value?.reload();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const { values, gridApi: grid } = modalApi.getData<Record<string, any>>();
gridApi.value = grid;
configId.value = values?.id || 0;
activeSkuId.value = 0;
drugPricesCache.value = {};
if (!configId.value) {
return;
}
getSpecialPrescriptionPriceConfig(configId.value).then((res: any) => {
if (!res) return;
configName.value = res.name || '';
priceCalcScheme.value = res.price_calc_scheme ?? 3;
buyPricePerDose.value = res.buy_price_per_dose ?? 0;
salePricePerDose.value = res.sale_price_per_dose ?? res.price_per_dose ?? 0;
skus.value = res.skus || [];
skuEditorRef.value?.setSkus(res.skus || []);
prescriptionDetailCache.value = res.prescription_detail || [];
const first = prescriptionDetailCache.value[0];
prescriptionDosage.value = first?.dosage ?? 7;
prescriptionDayDosage.value = first?.consumption ?? 2;
drugPricesCache.value[0] = (res.drug_prices || []).map((item: any) => ({
drug_id: Number(item.drug_id),
buy_price: Number(item.buy_price || 0),
sell_price: Number(item.sell_price || item.price || 0),
}));
const bySku = res.drug_prices_by_sku || {};
Object.keys(bySku).forEach((skuKey) => {
drugPricesCache.value[Number(skuKey)] = (bySku[skuKey] || []).map((item: any) => ({
drug_id: Number(item.drug_id),
buy_price: Number(item.buy_price || 0),
sell_price: Number(item.sell_price || item.price || 0),
}));
});
nextTick(() => {
switchAllocationView(0);
});
});
}
},
});
watch(priceCalcScheme, (scheme) => {
if (scheme === 1) {
nextTick(() => switchAllocationView(activeSkuId.value));
}
});
/** 将编辑器当前分摊写入缓存 */
function persistCurrentScopePrices() {
const prices = chineseDrugEditorRef.value?.getDrugPricesPayload() || [];
drugPricesCache.value[activeSkuId.value] = prices.map((item) => ({
drug_id: Number(item.drug_id),
buy_price: Number(item.buy_price || 0),
sell_price: Number(item.sell_price || 0),
}));
}
/** 切换单帖 / SKU 分摊视图 */
function switchAllocationView(skuId: number) {
persistCurrentScopePrices();
activeSkuId.value = skuId;
const prices = drugPricesCache.value[skuId] || drugPricesCache.value[0] || [];
chineseDrugEditorRef.value?.loadDrugs(
prescriptionDetailCache.value,
prescriptionDosage.value,
prescriptionDayDosage.value,
prices,
);
}
function handleSkuSelectForPrice(skuId: number) {
switchAllocationView(skuId);
}
function backToDoseAllocation() {
switchAllocationView(0);
}
/** 自动均分:单帖用 per_doseSKU 用套餐总价 */
async function handleDistributePrices() {
const drugs = chineseDrugEditorRef.value?.getDrugsForDistribute() || [];
if (drugs.length === 0) {
message.warning('请先配置特色方药品');
return;
}
let buyPrice = buyPricePerDose.value;
let salePrice = salePricePerDose.value;
if (activeSkuId.value > 0) {
const sku = skus.value.find((item) => Number(item.id) === Number(activeSkuId.value));
if (!sku) {
message.warning('请先保存 SKU 后再均分');
return;
}
const dose = Math.max(1, Number(sku.dose_count || 1));
buyPrice = Number(sku.buy_price || 0) / dose;
salePrice = Number(sku.sale_price || 0) / dose;
}
try {
const result = await distributeDrugPricesApi({
drugs,
buy_price_per_dose: buyPrice,
sale_price_per_dose: salePrice,
buy_price: buyPrice,
sale_price: salePrice,
});
chineseDrugEditorRef.value?.applyDistributedPrices(result || []);
persistCurrentScopePrices();
message.success('已自动均分药品单价');
} catch (error) {
console.error(error);
}
}
</script>
<template>
<Modal :title="`价格配置 - ${configName}`" class="w-[90%]">
<div class="price-config-modal">
<div class="section-title">价格计算方案</div>
<RadioGroup v-model:value="priceCalcScheme">
<Radio :value="1">固定单价 SKU 快捷选贴</Radio>
<Radio :value="3">门店浮动</Radio>
</RadioGroup>
<div v-if="showFixedPriceBlock" class="mt-4">
<Row :gutter="16">
<Col :span="12">
<div class="allocation-panel">
<div class="allocation-header">
<span class="allocation-title">{{ allocationTitle }}</span>
<Button
v-if="activeSkuId > 0"
type="link"
size="small"
@click="backToDoseAllocation"
>
返回单帖均摊
</Button>
</div>
<ChineseDrugEditor
ref="chineseDrugEditorRef"
:price-editable="true"
:structure-readonly="true"
/>
</div>
</Col>
<Col :span="12">
<div class="section-title">固定单价</div>
<div class="price-row">
<div class="price-field">
<span class="label">一贴供货价</span>
<InputNumber
v-model:value="buyPricePerDose"
:min="0"
:precision="2"
style="width: 100%"
/>
</div>
<div class="price-field">
<span class="label">一贴销售价</span>
<InputNumber
v-model:value="salePricePerDose"
:min="0"
:precision="2"
style="width: 100%"
/>
</div>
<Button type="primary" ghost @click="handleDistributePrices">
自动均分药品单价
</Button>
</div>
<div class="section-title mt-4">SKU 规格点击行查看分摊</div>
<SkuEditor
ref="skuEditorRef"
v-model="skus"
:selected-price-sku-id="activeSkuId"
@select-for-price="handleSkuSelectForPrice"
/>
</Col>
</Row>
</div>
<div v-else class="mt-4 store-float-tip">
门店浮动方案无需配置药品分摊SKU 仍可配置快捷选贴数
<div class="section-title mt-4">SKU 规格</div>
<SkuEditor ref="skuEditorRef" v-model="skus" />
</div>
</div>
</Modal>
</template>
<style scoped>
.section-title {
font-weight: 600;
margin-bottom: 12px;
}
.allocation-panel {
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 12px;
min-height: 360px;
}
.allocation-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.allocation-title {
font-weight: 600;
font-size: 15px;
color: #333;
}
.price-row {
display: flex;
align-items: flex-end;
gap: 16px;
flex-wrap: wrap;
}
.price-field {
width: 160px;
}
.price-field .label {
display: block;
margin-bottom: 4px;
color: #666;
font-size: 13px;
}
.store-float-tip {
color: #666;
font-size: 13px;
}
</style>

View File

@@ -0,0 +1,278 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
import {
Button,
Input,
InputNumber,
Radio,
Select,
SelectOption,
Table,
} from 'ant-design-vue';
export interface SkuItem {
id?: number;
_key?: number;
sku_name: string;
dose_count: number;
buy_price?: number;
sale_price?: number;
status: number;
is_default?: number;
sort: number;
store_id?: number;
}
const props = withDefaults(
defineProps<{
modelValue?: SkuItem[];
/** 当前查看分摊的 SKU0=单帖,在 PriceConfigModal 中由父组件控制) */
selectedPriceSkuId?: number;
}>(),
{
modelValue: () => [],
selectedPriceSkuId: 0,
},
);
const emit = defineEmits<{
'update:modelValue': [value: SkuItem[]];
/** 点击行切换左侧分摊视图 */
'select-for-price': [skuId: number];
}>();
const skuList = ref<SkuItem[]>([]);
const columns = [
{ title: '默认', key: 'is_default', width: 70 },
{ title: 'SKU名称', key: 'sku_name', width: 120 },
{ title: '贴数', key: 'dose_count', width: 90 },
{ title: '供货总价', key: 'buy_price', width: 110 },
{ title: '销售总价', key: 'sale_price', width: 110 },
{ title: '状态', key: 'status', width: 90 },
{ title: '排序', key: 'sort', width: 80 },
{ title: '操作', key: 'action', width: 70 },
];
watch(
() => props.modelValue,
(val) => {
skuList.value = (val || []).map((item, index) => ({
...item,
_key: item._key ?? item.id ?? Date.now() + index,
sort: item.sort ?? index,
store_id: item.store_id ?? 0,
status: item.status ?? 1,
is_default: item.is_default ?? 0,
buy_price: item.buy_price ?? 0,
sale_price: item.sale_price ?? 0,
}));
},
{ immediate: true, deep: true },
);
function syncToParent() {
emit('update:modelValue', [...skuList.value]);
}
function addSku() {
skuList.value.push({
_key: Date.now(),
sku_name: '',
dose_count: 7,
buy_price: 0,
sale_price: 0,
status: 1,
is_default: skuList.value.length === 0 ? 1 : 0,
sort: skuList.value.length,
store_id: 0,
});
syncToParent();
}
function removeSku(index: number) {
const removed = skuList.value[index];
const removedDefault = removed?.is_default === 1;
const removedId = Number(removed?.id || 0);
skuList.value.splice(index, 1);
if (removedDefault && skuList.value.length > 0) {
skuList.value[0].is_default = 1;
}
syncToParent();
if (removedId > 0 && props.selectedPriceSkuId === removedId) {
emit('select-for-price', 0);
}
}
function setDefault(index: number) {
skuList.value.forEach((item, i) => {
item.is_default = i === index ? 1 : 0;
});
syncToParent();
}
function onFieldChange() {
syncToParent();
}
/** 点击行查看该 SKU 分摊(需已保存有 id */
function onRowClick(record: SkuItem) {
const skuId = Number(record.id || 0);
if (skuId <= 0) {
return;
}
emit('select-for-price', skuId);
}
function customRow(record: SkuItem) {
const skuId = Number(record.id || 0);
return {
class: skuId > 0 && skuId === Number(props.selectedPriceSkuId)
? 'sku-row--price-active'
: '',
onClick: () => onRowClick(record),
};
}
defineExpose({
getSkusPayload: () => skuList.value,
setSkus: (skus: SkuItem[]) => {
skuList.value = (skus || []).map((item, index) => ({
...item,
_key: item._key ?? item.id ?? Date.now() + index,
sort: item.sort ?? index,
store_id: item.store_id ?? 0,
is_default: item.is_default ?? 0,
buy_price: item.buy_price ?? 0,
sale_price: item.sale_price ?? 0,
}));
syncToParent();
},
});
</script>
<template>
<div class="sku-editor">
<div class="sku-editor-header">
<span class="font-medium">SKU 规格快捷选贴数价格为套餐总价</span>
<Button type="dashed" size="small" @click="addSku">
<PlusOutlined />
添加 SKU
</Button>
</div>
<Table
:columns="columns"
:data-source="skuList"
:pagination="false"
:custom-row="customRow"
row-key="_key"
size="small"
bordered
class="mt-2 sku-table"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'is_default'">
<Radio
:checked="record.is_default === 1"
@click.stop="setDefault(index)"
/>
</template>
<template v-else-if="column.key === 'sku_name'">
<Input
v-model:value="record.sku_name"
placeholder="如 7贴"
@click.stop
@change="onFieldChange"
/>
</template>
<template v-else-if="column.key === 'dose_count'">
<InputNumber
v-model:value="record.dose_count"
:min="1"
:max="99"
style="width: 100%"
@click.stop
@change="onFieldChange"
/>
</template>
<template v-else-if="column.key === 'buy_price'">
<InputNumber
v-model:value="record.buy_price"
:min="0"
:precision="2"
style="width: 100%"
@click.stop
@change="onFieldChange"
/>
</template>
<template v-else-if="column.key === 'sale_price'">
<InputNumber
v-model:value="record.sale_price"
:min="0"
:precision="2"
style="width: 100%"
@click.stop
@change="onFieldChange"
/>
</template>
<template v-else-if="column.key === 'status'">
<Select
v-model:value="record.status"
style="width: 100%"
@click.stop
@change="onFieldChange"
>
<SelectOption :value="1">上架</SelectOption>
<SelectOption :value="0">下架</SelectOption>
</Select>
</template>
<template v-else-if="column.key === 'sort'">
<InputNumber
v-model:value="record.sort"
:min="0"
style="width: 100%"
@click.stop
@change="onFieldChange"
/>
</template>
<template v-else-if="column.key === 'action'">
<Button type="link" danger size="small" @click.stop="removeSku(index)">
<DeleteOutlined />
</Button>
</template>
</template>
</Table>
<div v-if="skuList.length === 0" class="sku-empty-tip">
暂无 SKU用户端将使用手动输入贴数
</div>
<div v-else class="sku-hint">
点击已保存的 SKU 行可在左侧查看/编辑该规格分摊价
</div>
</div>
</template>
<style scoped>
.sku-editor-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.sku-empty-tip,
.sku-hint {
margin-top: 8px;
color: #999;
font-size: 12px;
}
:deep(.sku-row--price-active) {
background-color: #e6f4ff !important;
cursor: pointer;
}
:deep(.sku-table .ant-table-row) {
cursor: pointer;
}
</style>

View File

@@ -22,6 +22,9 @@ const chineseDrugEditorRef = ref<InstanceType<typeof ChineseDrugEditor>>();
const dosage = ref(7);
const dayDosage = ref(2);
const drugsChanged = ref(false);
/** 预选诊断/医嘱(在药品列表气泡卡中编辑) */
const clinicalDiagnose = ref('');
const doctorOrder = ref('');
const [Form, formApi] = useVbenForm(modalFormProps);
@@ -64,6 +67,14 @@ const [Modal, modalApi] = useVbenModal({
.filter(Boolean),
};
const diagnosisOrder =
chineseDrugEditorRef.value?.getDiagnosisOrderPayload?.() ?? {
clinical_diagnose: clinicalDiagnose.value,
doctor_order: doctorOrder.value,
};
payload.clinical_diagnose = diagnosisOrder.clinical_diagnose ?? '';
payload.doctor_order = diagnosisOrder.doctor_order ?? '';
if (prescriptionType.value === 'chinese' && drugs.length > 0) {
payload.drugs = drugs;
payload.dosage = dosage.value;
@@ -110,6 +121,8 @@ const [Modal, modalApi] = useVbenModal({
intro_text: res.intro_text || '',
introduction_images: res.introduction_images || [],
});
clinicalDiagnose.value = res.clinical_diagnose || '';
doctorOrder.value = res.doctor_order || '';
if (res.prescription_type === 'chinese' && res.prescription_detail?.length) {
const first = res.prescription_detail[0];
chineseDrugEditorRef.value?.loadDrugs(
@@ -140,6 +153,8 @@ const [Modal, modalApi] = useVbenModal({
intro_text: '',
introduction_images: [],
});
clinicalDiagnose.value = '';
doctorOrder.value = '';
chineseDrugEditorRef.value?.loadDrugs([], 7, 2);
}
} else {
@@ -168,6 +183,8 @@ function bindPrescriptionTypeChange() {
<div v-if="prescriptionType === 'chinese'" class="mt-4 border-t pt-4">
<ChineseDrugEditor
ref="chineseDrugEditorRef"
v-model:clinical-diagnose="clinicalDiagnose"
v-model:doctor-order="doctorOrder"
v-model:dosage="dosage"
v-model:day-dosage="dayDosage"
@update:model-value="drugsChanged = true"

View File

@@ -70,7 +70,7 @@ export const modalFormProps: VbenFormProps = {
{
component: 'InputNumber',
componentProps: {
placeholder: '每剂价格',
placeholder: '展示兜底价/贴',
min: 0,
precision: 2,
style: { width: '100%' },
@@ -106,6 +106,20 @@ export const modalFormProps: VbenFormProps = {
formItemClass: 'col-span-6',
defaultValue: 1,
},
{
component: 'VbenSelect',
componentProps: {
placeholder: '是否包邮',
options: [
{ label: '不包邮', value: 0 },
{ label: '包邮', value: 1 },
],
},
fieldName: 'is_free_shipping',
label: '是否包邮',
formItemClass: 'col-span-6',
defaultValue: 0,
},
{
component: 'VbenSelect',
componentProps: {

View File

@@ -15,6 +15,7 @@ import {
updateSpecialPrescriptionStatus,
} from './api';
import EditPrescriptionModal from './components/EditPrescriptionModal.vue';
import PriceConfigModal from './components/PriceConfigModal.vue';
import SpecialPrescriptionModal from './components/modal.vue';
import { formOptions as searchFormOptions } from './config/search';
import { gridOptions } from './config/table';
@@ -47,6 +48,10 @@ const [EditPrescriptionModalComp, editPrescriptionModalApi] = useVbenModal({
connectedComponent: EditPrescriptionModal,
});
const [PriceConfigModalComp, priceConfigModalApi] = useVbenModal({
connectedComponent: PriceConfigModal,
});
const showModal = (data = {}, isUpdate = false) => {
formModalApi.setData({
values: data,
@@ -64,6 +69,14 @@ const showEditPrescriptionModal = (row: any) => {
editPrescriptionModalApi.open();
};
const showPriceConfigModal = (row: any) => {
priceConfigModalApi.setData({
values: row,
gridApi,
});
priceConfigModalApi.open();
};
/** 点击状态 Tag 切换上下架 */
const toggleStatus = async (row: any) => {
if (statusLoadingId.value === row.id) {
@@ -98,6 +111,7 @@ const deleteApi = (row: any) => {
<Page auto-content-height title="特色方管理">
<FormModal />
<EditPrescriptionModalComp />
<PriceConfigModalComp />
<div class="p-4">
<TableAction
@@ -161,6 +175,10 @@ const deleteApi = (row: any) => {
ifShow: row.prescription_sub_type === 1,
onClick: () => showEditPrescriptionModal(row),
},
{
label: '价格配置',
onClick: () => showPriceConfigModal(row),
},
{
label: '删除',
color: 'error',

View File

@@ -9,6 +9,8 @@ export function buildSpecialPrescriptionPayload(
day_dosage?: number;
rule_type?: number;
package_method_id?: number;
clinical_diagnose?: string;
doctor_order?: string;
},
) {
const payload: Record<string, any> = {
@@ -20,9 +22,12 @@ export function buildSpecialPrescriptionPayload(
price_per_dose: detail.price_per_dose ?? 0,
sales_count: detail.sales_count ?? 0,
status: detail.status ?? 1,
is_free_shipping: detail.is_free_shipping ?? 0,
prescription_type: detail.prescription_type || 'chinese',
intro_text: detail.intro_text || '',
introduction_images: detail.introduction_images || [],
clinical_diagnose: detail.clinical_diagnose || '',
doctor_order: detail.doctor_order || '',
};
if (drugOverrides?.drugs?.length) {
payload.drugs = drugOverrides.drugs;
@@ -31,5 +36,11 @@ export function buildSpecialPrescriptionPayload(
payload.rule_type = drugOverrides.rule_type ?? 1;
payload.package_method_id = drugOverrides.package_method_id ?? 2;
}
if (drugOverrides?.clinical_diagnose !== undefined) {
payload.clinical_diagnose = drugOverrides.clinical_diagnose;
}
if (drugOverrides?.doctor_order !== undefined) {
payload.doctor_order = drugOverrides.doctor_order;
}
return payload;
}

View File

@@ -0,0 +1,109 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Image, Tag } from 'ant-design-vue';
/** 患者选方记录含特色方封面、SKU 等) */
const props = withDefaults(
defineProps<{
record: Record<string, any> | null;
purchaseLabel?: string;
loading?: boolean;
/** 是否可点击导入(有待导入/已应用记录时) */
clickable?: boolean;
}>(),
{
purchaseLabel: '',
loading: false,
clickable: true,
},
);
const emit = defineEmits<{
apply: [];
}>();
/** 封面图 */
const coverImage = computed(
() => props.record?.special_prescription?.cover_image || '',
);
/** 药方名称 */
const prescriptionName = computed(
() => props.record?.special_prescription?.name || '特色方',
);
/** 状态文案与颜色 */
const statusMeta = computed(() => {
const status = Number(props.record?.status ?? -1);
if (status === 0) return { text: '待导入', color: 'orange' as const };
if (status === 1) return { text: '已应用', color: 'green' as const };
return { text: '已完成', color: 'default' as const };
});
/**
* 点击卡片触发导入
* loading 或不可点击时不响应
*/
function handleCardClick() {
if (!props.clickable || props.loading) return;
emit('apply');
}
</script>
<template>
<div
v-if="record"
class="sp-import-card mt-4 rounded-lg border p-3 transition-shadow
bg-[#fffaf5] border-[#ffe0c2]
dark:bg-[#2c2419] dark:border-[#5c4a32]"
:class="{
'sp-import-card--clickable cursor-pointer hover:shadow-md dark:hover:shadow-[0_2px_8px_rgba(255,152,0,0.25)]':
clickable && !loading,
'sp-import-card--loading': loading,
}"
@click="handleCardClick"
>
<div
class="sp-import-card__hint mb-2.5 text-[13px] text-[#fa8c16] dark:text-[#ffa940]"
>
点击卡片导入药方
</div>
<div class="sp-import-card__body flex flex-wrap items-center gap-4">
<Image
v-if="coverImage"
:src="coverImage"
:width="80"
:height="80"
class="sp-import-card__cover shrink-0 rounded object-cover"
/>
<div
v-else
class="sp-import-card__cover sp-import-card__cover--placeholder flex h-20 w-20 shrink-0 items-center justify-center rounded text-xs
bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400"
>
无封面
</div>
<div class="sp-import-card__info min-w-0 flex-1">
<div
class="sp-import-card__name text-base font-semibold text-gray-900 dark:text-gray-100"
>
{{ prescriptionName }}
</div>
<div
class="sp-import-card__sku mt-1 text-sm text-gray-600 dark:text-gray-400"
>
规格{{ purchaseLabel || '单剂' }}
</div>
<Tag class="sp-import-card__tag mt-2" :color="statusMeta.color">
{{ statusMeta.text }}
</Tag>
</div>
</div>
</div>
</template>
<style scoped>
.sp-import-card--loading {
opacity: 0.75;
pointer-events: none;
}
</style>

View File

@@ -52,6 +52,7 @@ import {
withdrawPrescriptionApi,
} from '#/views/doctor/doctor-reception/api';
import SpecialPrescriptionImportCard from './components/SpecialPrescriptionImportCard.vue';
import DiagnosisModal from './components/DiagnosisModal.vue';
import DoctorOrderModal from './components/DoctorOrderModal.vue';
import PrescriptionDetail from './components/PrescriptionDetail.vue';
@@ -108,6 +109,8 @@ interface Patient {
status: number;
special_prescription_id: number;
dose_count: number;
sku_id?: number;
sku_name?: string;
special_prescription?: {
id: number;
name: string;
@@ -346,7 +349,40 @@ const hasSpecialPrescriptionRecord = computed(
const specialPrescriptionRecord = computed(
() => patientInfo.value?.special_prescription_patient_record ?? null,
);
/** 导入卡片规格SKU 名或单剂 */
function resolveSpecialPrescriptionPurchaseLabel(
record?: { sku_id?: number; sku_name?: string } | null,
) {
if (!record) return '';
return Number(record.sku_id) > 0
? (record.sku_name || 'SKU套餐')
: '单剂';
}
const specialPrescriptionPurchaseLabel = computed(() =>
resolveSpecialPrescriptionPurchaseLabel(specialPrescriptionRecord.value),
);
const applyingSpecialPrescription = ref(false);
/** 医生已导入的特色方 ID仅 >0 时提交走特色方计价 */
const appliedSpecialPrescriptionId = ref(0);
/** apply 返回的 SKU/贴数套餐总价,用于底部合计 */
const appliedSpecialPrescriptionExpectedTotal = ref<number | null>(null);
function clearAppliedSpecialPrescription() {
appliedSpecialPrescriptionId.value = 0;
appliedSpecialPrescriptionExpectedTotal.value = null;
}
/** 已应用特色方时锁定药方(不可改删增) */
const isSpecialPrescriptionCartLocked = computed(
() => appliedSpecialPrescriptionId.value > 0,
);
const SP_CART_LOCK_MSG = '特色方药方不可修改,请先清空药方';
/** 特色方锁定中则提示并返回 true */
function guardSpecialPrescriptionCartEdit(): boolean {
if (isSpecialPrescriptionCartLocked.value) {
message.warning(SP_CART_LOCK_MSG);
return true;
}
return false;
}
const diagnosis = ref('');
const medicalAdvice = ref('');
const treatmentPrice = ref(0);
@@ -398,6 +434,7 @@ function selectProductChange(id) {
* @param id
*/
function selectDrugUseWayChange(id) {
if (guardSpecialPrescriptionCartEdit()) return;
if (
selectProductIndex.value < 0 ||
selectProductIndex.value >= currentDrugs.value.length
@@ -417,6 +454,14 @@ function selectDrugUseWayChange(id) {
* 计算商品总价
*/
const totalProductCost = computed(() => {
const expected = appliedSpecialPrescriptionExpectedTotal.value;
if (
expected != null
&& !Number.isNaN(Number(expected))
&& Number(expected) > 0
) {
return Number(expected);
}
if (currentDrugs.value.length === 0) {
return 0;
}
@@ -509,6 +554,7 @@ const setVisible = (value, instruction = ''): void => {
* @param isUpdateTabType
*/
const selectPatient = (patient: Patient, isUpdateTabType = true) => {
clearAppliedSpecialPrescription();
selectPatientId.value = patient.id;
receptionStatus.value = patient.status;
if (isUpdateTabType === true) {
@@ -561,12 +607,14 @@ const selectPatient = (patient: Patient, isUpdateTabType = true) => {
// 数量增加
const increment = (index: number) => {
if (guardSpecialPrescriptionCartEdit()) return;
currentDrugs.value[index].select_number++;
saveToLocalStorage();
};
// 数量减少
const decrement = (index: number) => {
if (guardSpecialPrescriptionCartEdit()) return;
if (currentDrugs.value[index].select_number > 1) {
currentDrugs.value[index].select_number--;
saveToLocalStorage();
@@ -575,6 +623,7 @@ const decrement = (index: number) => {
// 切换药品用法编辑状态
const toggleEditDrug = (index: number) => {
if (guardSpecialPrescriptionCartEdit()) return;
currentDrugs.value[index].isEditing = !currentDrugs.value[index].isEditing;
};
@@ -635,6 +684,7 @@ function clearCartPriceDiscount() {
const priceDiscountLabel = computed(() => formatPriceDiscountLabel(priceDiscount.value));
function openOrderPriceAdjust() {
if (guardSpecialPrescriptionCartEdit()) return;
if (!priceAdjustEnabled.value || !currentDrugs.value.length) return;
currentDrugs.value = snapshotDrugOrigins(currentDrugs.value);
const discount = Number(priceDiscount.value);
@@ -654,6 +704,7 @@ function openOrderPriceAdjust() {
* @param index
*/
const removeDrug = (index: number) => {
if (guardSpecialPrescriptionCartEdit()) return;
currentDrugs.value.splice(index, 1);
updateLocalStorage();
};
@@ -790,6 +841,7 @@ const sendPrescription = () => {
doctor_second_sign: doctorSecondSign.value,
salesperson_transfer_prescription_id: salespersonTransferPrescriptionId.value || undefined,
price_discount: priceDiscount.value,
special_prescription_id: appliedSpecialPrescriptionId.value || 0,
}).then((res) => {
message.success('处方已发送');
@@ -807,17 +859,10 @@ const sendPrescription = () => {
}
// 清空当前数据
currentDrugs.value = [];
diagnosis.value = '';
medicalAdvice.value = '';
packageMethodId.value = 2;
processRulePrice.value = 0;
dosage.value = 7;
dayDosage.value = 2;
clearPrescriptionCart(false);
tabType.value = 1;
salespersonTransferPrescriptionId.value = 0;
newDrugInfo.value = {};
updateLocalStorage();
processRulePrice.value = 0;
checkSalespersonTransfer();
// 刷新患者信息
@@ -898,7 +943,7 @@ const [DoctorOrderModals, DoctorOrderModalApi] = useVbenModal({
});
const openDoctorOrderModal = () => {
// 打开常用诊断模态框逻辑
// 打开常用医嘱模态框逻辑
DoctorOrderModalApi.setData({
values: medicalAdvice.value,
updateDoctorOrder,
@@ -953,6 +998,7 @@ function openCommonPrescriptionModal() {
* @description 选择常用方后,将药品列表填充到当前处方中
*/
async function handleSelectCommonPrescription(data: any, type: number) {
clearAppliedSpecialPrescription();
const { prescription, recipes } = data;
// 根据类型处理药品:整表覆盖为常用方内容,不与当前处方按药品 ID 去重合并
@@ -1069,6 +1115,10 @@ async function handleApplySpecialPrescription(switchToPrescription = false) {
typeMap[prescriptionType] || 2,
);
appliedSpecialPrescriptionId.value = Number(res.special_prescription_id) || 0;
appliedSpecialPrescriptionExpectedTotal.value =
res.expected_total_price != null ? Number(res.expected_total_price) : null;
if (selectPatientId.value) {
const value = await getPatientItem(selectPatientId.value);
patientInfo.value = value;
@@ -1197,6 +1247,7 @@ const cancelSaveCommonPrescription = () => {
};
const openWesternModal = () => {
if (guardSpecialPrescriptionCartEdit()) return;
// 判断是否为简单产品类型3,5,6,7
const simpleProductTypes = [3, 5, 6, 7];
if (simpleProductTypes.includes(activeCategory.value)) {
@@ -1674,6 +1725,7 @@ loadProcessRuleData();
* @param id
*/
function selectProcessRule(id) {
if (guardSpecialPrescriptionCartEdit()) return;
processRuleId.value = id;
loadProcessRuleData(id); // 加载煎法选项
}
@@ -1683,6 +1735,7 @@ function selectProcessRule(id) {
* @param id
*/
function selectProcessRuleNot(id) {
if (guardSpecialPrescriptionCartEdit()) return;
childProcessRuleId.value = id;
loadProcessRuleData(0, id); // 加载备注选项
}
@@ -1692,6 +1745,7 @@ function selectProcessRuleNot(id) {
* @param id
*/
function selectProcessRuleNotCommit(id) {
if (guardSpecialPrescriptionCartEdit()) return;
processRuleNoteId.value = id;
}
@@ -1702,6 +1756,7 @@ const packageMethod = [
];
function selectPackageMethod(id) {
if (guardSpecialPrescriptionCartEdit()) return;
packageMethodId.value = id;
}
@@ -1709,6 +1764,41 @@ function selectPackageMethod(id) {
const dosage = ref(7);
const dayDosage = ref(2);
/**
* 清空药方:退出特色方 SKU 计价状态,恢复可编辑
* @param showToast 是否提示成功
*/
function clearPrescriptionCart(showToast = true) {
currentDrugs.value = [];
diagnosis.value = '';
medicalAdvice.value = '';
packageMethodId.value = 2;
dosage.value = 7;
dayDosage.value = 2;
newDrugInfo.value = {};
clearAppliedSpecialPrescription();
priceDiscount.value = 100;
updateLocalStorage();
if (showToast) {
message.success('已清空药方');
}
}
/** 清空药方(有药品时二次确认) */
function handleClearPrescriptionCart() {
if (currentDrugs.value.length === 0 && !appliedSpecialPrescriptionId.value) {
message.info('当前无药方内容');
return;
}
AntModal.confirm({
title: '清空药方',
content: '确定清空当前药方吗?特色方将需重新应用。',
okText: '确定',
cancelText: '取消',
onOk: () => clearPrescriptionCart(),
});
}
function updateChineseNumber() {
updateLocalStorage();
}
@@ -1833,6 +1923,7 @@ const selectChineseId = ref(0);
* @param id
*/
function selectOldDrugInfo(id) {
if (guardSpecialPrescriptionCartEdit()) return;
const check = JSON.parse(
localStorage.getItem(getStorageKey(activePatient.value?.id, activeCategory.value)) || '[]',
).find((v) => v.id === id);
@@ -1923,6 +2014,7 @@ function searchOption(inputValue) {
* 添加中药
*/
function addDrugByChinese() {
if (guardSpecialPrescriptionCartEdit()) return;
// 如果是新药品输入框,调用添加新药品方法
const check = currentDrugs.value.find(
(v) => v.id === newDrugInfo.value.id,
@@ -1976,6 +2068,7 @@ function selectDrugByNewDrugInfo(event: KeyboardEvent, isNewDrug: boolean) {
* @param {object} data - 商品对象
*/
function addProducts(data) {
if (guardSpecialPrescriptionCartEdit()) return;
// 查找是否已存在于选择列表中
const existItem = currentDrugs.value.find(
(item) => item.index_id === data.id,
@@ -2046,6 +2139,7 @@ function addProducts(data) {
* @param drug 选中的药品数据
*/
function handleSimpleProductSelect(drug: any) {
if (guardSpecialPrescriptionCartEdit()) return;
// 检查是否已存在
const existItem = currentDrugs.value.find(
(item) => item.id === (drug._drugId || drug.drug_id || drug.drug?.id),
@@ -2285,52 +2379,13 @@ watch(
}}
</Descriptions.Item>
</Descriptions>
<Card
v-if="specialPrescriptionRecord"
class="mt-5"
title="特色方"
>
<div class="flex flex-wrap items-center gap-4">
<Image
v-if="specialPrescriptionRecord.special_prescription?.cover_image"
:src="specialPrescriptionRecord.special_prescription.cover_image"
:width="80"
:height="80"
class="rounded object-cover"
/>
<div class="flex-1">
<div class="text-base font-medium">
{{ specialPrescriptionRecord.special_prescription?.name || '特色方' }}
</div>
<div class="mt-1 text-gray-500">
剂量{{ specialPrescriptionRecord.dose_count }}
</div>
<Tag
v-if="specialPrescriptionRecord.status === 0"
class="mt-2"
color="orange"
>
待导入
</Tag>
<Tag
v-else-if="specialPrescriptionRecord.status === 1"
class="mt-2"
color="green"
>
已应用
</Tag>
<Tag v-else class="mt-2" color="default">已完成</Tag>
</div>
<Button
v-if="hasSpecialPrescriptionRecord"
type="primary"
:loading="applyingSpecialPrescription"
@click="handleApplySpecialPrescription(true)"
>
导入
</Button>
</div>
</Card>
<SpecialPrescriptionImportCard
v-if="specialPrescriptionRecord && hasSpecialPrescriptionRecord"
:record="specialPrescriptionRecord"
:purchase-label="specialPrescriptionPurchaseLabel"
:loading="applyingSpecialPrescription"
@apply="handleApplySpecialPrescription(true)"
/>
<Descriptions
v-if="userPatientHealthInquiry"
:column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 2, xs: 2 }"
@@ -2520,15 +2575,23 @@ watch(
</button>
</div>
<SpecialPrescriptionImportCard
v-if="hasSpecialPrescriptionRecord"
class="mt-5 max-w-xl"
:record="specialPrescriptionRecord"
:purchase-label="specialPrescriptionPurchaseLabel"
:loading="applyingSpecialPrescription"
@apply="() => handleApplySpecialPrescription()"
/>
<div class="mt-5">
<Button
v-if="activeCategory === 2"
v-if="activeCategory === 2 && !isSpecialPrescriptionCartLocked"
type="primary"
@click="openWesternModal"
>
添加商品
</Button>
<!-- 常用方按钮(产品服务包/非药品/医疗器械无模板接口) -->
<Button
v-if="canUseCommonPrescription"
type="primary"
@@ -2537,15 +2600,6 @@ watch(
>
选择常用方
</Button>
<Button
v-if="hasSpecialPrescriptionRecord"
type="primary"
class="ml-3"
:loading="applyingSpecialPrescription"
@click="() => handleApplySpecialPrescription()"
>
应用特色方
</Button>
<Button
v-if="canUseCommonPrescription"
type="default"
@@ -2555,6 +2609,14 @@ watch(
<SaveOutlined />
保存常用方
</Button>
<Button
type="default"
danger
class="ml-3"
@click="handleClearPrescriptionCart"
>
清空药方
</Button>
<Button
v-if="activeCategory === 1 && hasSalespersonTransfer"
type="default"
@@ -2613,6 +2675,7 @@ watch(
<p>
<Select
v-model:value="drug.id"
:disabled="isSpecialPrescriptionCartLocked"
:filter-option="false"
class="old-select-drug-name"
style="min-width: 100px"
@@ -2639,6 +2702,7 @@ watch(
,
<InputNumber
v-model:value="drug.number"
:disabled="isSpecialPrescriptionCartLocked"
:class="`w-1/5 old-number-input-${index}`"
style="min-width: 100px"
:controls="false"
@@ -2651,6 +2715,7 @@ watch(
<span>
<Select
:value="drug?.way_id"
:disabled="isSpecialPrescriptionCartLocked"
placeholder="用法"
@change="selectDrugUseWayChange"
@dropdown-visible-change="
@@ -2677,6 +2742,7 @@ watch(
</span>
</p>
<Button
v-if="!isSpecialPrescriptionCartLocked"
class="card-delete"
type="link"
@click="removeDrug(index)"
@@ -2688,6 +2754,7 @@ watch(
</Card>
</Col>
<Col
v-if="!isSpecialPrescriptionCartLocked"
:lg="12"
:md="12"
:sm="12"
@@ -2943,6 +3010,7 @@ watch(
<span>用法:</span>
<Select
:value="drug.use_type?.id"
:disabled="isSpecialPrescriptionCartLocked"
size="small"
style="width: 80px"
@change="(val) => updateDrugUsage(index, 'use_type', drugUseType.find(item => item.id === val))"
@@ -2953,6 +3021,7 @@ watch(
</Select>
<Select
:value="drug.use_frequency?.id"
:disabled="isSpecialPrescriptionCartLocked"
size="small"
style="width: 100px; margin-left: 4px"
@change="(val) => updateDrugUsage(index, 'use_frequency', drugUseFrequency.find(item => item.id === val))"
@@ -2963,6 +3032,7 @@ watch(
</Select>
<Select
:value="drug.use_num?.id"
:disabled="isSpecialPrescriptionCartLocked"
size="small"
style="width: 80px; margin-left: 4px"
@change="(val) => updateDrugUsage(index, 'use_num', drugTime.find(item => item.id === val))"
@@ -2974,12 +3044,14 @@ watch(
<span style="margin-left: 4px">每次</span>
<InputNumber
v-model:value="drug.number"
:disabled="isSpecialPrescriptionCartLocked"
:min="1"
size="small"
style="width: 60px; margin-left: 4px"
/>
<Select
:value="drug.unit?.id"
:disabled="isSpecialPrescriptionCartLocked"
size="small"
style="width: 60px; margin-left: 4px"
@change="(val) => updateDrugUsage(index, 'unit', drugUnit.find(item => item.id === val))"
@@ -3001,15 +3073,15 @@ watch(
</InputNumber>
</div>
<div v-else class="quantity-control">
<Button type="primary" @click="decrement(index)">-</Button>
<Button type="primary" :disabled="isSpecialPrescriptionCartLocked" @click="decrement(index)">-</Button>
<span style="margin: 0 20px">{{ drug.select_number }}</span>
<Button type="primary" @click="increment(index)">+</Button>
<Button type="primary" :disabled="isSpecialPrescriptionCartLocked" @click="increment(index)">+</Button>
</div>
</div>
<span>{{ drug.price }}</span>
<div>
<Button
v-if="activeCategory !== 1 && !drug.isEditing"
v-if="activeCategory !== 1 && !drug.isEditing && !isSpecialPrescriptionCartLocked"
type="link"
@click="toggleEditDrug(index)"
>编辑</Button>
@@ -3018,7 +3090,7 @@ watch(
type="link"
@click="saveEditDrug(index)"
>保存</Button>
<Button type="link" @click="removeDrug(index)">删除</Button>
<Button v-if="!isSpecialPrescriptionCartLocked" type="link" @click="removeDrug(index)">删除</Button>
<Button
v-if="drug.instruction !== ''"
type="link"
@@ -3036,6 +3108,7 @@ watch(
<!-- 搜索组件 -->
<div class="mb-5">
<DrugSearchSelect
v-if="!isSpecialPrescriptionCartLocked"
:type="activeCategory"
:store-id="myStoreId"
:placeholder="`输入${categories.find(c => c.value === activeCategory)?.label || '商品'}名称搜索`"
@@ -3067,14 +3140,14 @@ watch(
</div>
<div>
<div class="quantity-control">
<Button type="primary" @click="decrement(index)">-</Button>
<Button type="primary" :disabled="isSpecialPrescriptionCartLocked" @click="decrement(index)">-</Button>
<span style="margin: 0 20px">{{ drug.select_number }}</span>
<Button type="primary" @click="increment(index)">+</Button>
<Button type="primary" :disabled="isSpecialPrescriptionCartLocked" @click="increment(index)">+</Button>
</div>
</div>
<span>{{ drug.price }}</span>
<div>
<Button type="link" @click="removeDrug(index)">删除</Button>
<Button v-if="!isSpecialPrescriptionCartLocked" type="link" @click="removeDrug(index)">删除</Button>
<Button
v-if="drug.instruction !== ''"
type="link"
@@ -3091,7 +3164,7 @@ watch(
<!-- 诊断和医嘱 -->
<div class="diagnosis-area">
<div v-if="activeCategory === 1" class="mb-5">
<RadioGroup v-model:value="ruleType">
<RadioGroup v-model:value="ruleType" :disabled="isSpecialPrescriptionCartLocked">
<RadioButton :value="1">自制剂</RadioButton>
<RadioButton :value="2">委托调剂</RadioButton>
</RadioGroup>
@@ -3099,6 +3172,7 @@ watch(
<!-- 选择包法-->
<Select
:value="packageMethodId"
:disabled="isSpecialPrescriptionCartLocked"
class="mt-3 w-1/5"
placeholder="请选择包法"
@change="selectPackageMethod"
@@ -3116,6 +3190,7 @@ watch(
<!-- 选择制剂-->
<Select
v-model:value="processRuleId"
:disabled="isSpecialPrescriptionCartLocked"
class="mt-3 w-1/5"
placeholder="请选择制剂"
@change="selectProcessRule"
@@ -3131,6 +3206,7 @@ watch(
<!-- 选择规格-->
<Select
v-model:value="childProcessRuleId"
:disabled="isSpecialPrescriptionCartLocked"
class="ml-3 mt-3 w-1/5"
placeholder="请选择煎法"
@change="selectProcessRuleNot"
@@ -3146,6 +3222,7 @@ watch(
<!-- 选择备注-->
<Select
v-model:value="processRuleNoteId"
:disabled="isSpecialPrescriptionCartLocked"
class="ml-3 mt-3 w-1/5"
placeholder="请选择备注"
@change="selectProcessRuleNotCommit"
@@ -3161,17 +3238,17 @@ watch(
</div>
<div class="mt-5">
用量:
<InputNumber v-model:value="dosage" class="w-1/5">
<InputNumber v-model:value="dosage" :disabled="isSpecialPrescriptionCartLocked" class="w-1/5">
<template #addonBefore>
<MinusOutlined
key="prop"
@click="dosage = dosage > 1 ? dosage - 1 : 1"
@click="!isSpecialPrescriptionCartLocked && (dosage = dosage > 1 ? dosage - 1 : 1)"
/>
</template>
<template #addonAfter>
<PlusOutlined
key="add"
@click="dosage = dosage < 100 ? dosage + 1 : 100"
@click="!isSpecialPrescriptionCartLocked && (dosage = dosage < 100 ? dosage + 1 : 100)"
/>
</template>
</InputNumber>
@@ -3179,27 +3256,37 @@ watch(
</div>
<div class="mt-5">
频次:
<InputNumber v-model:value="dayDosage" class="w-1/5">
<InputNumber v-model:value="dayDosage" :disabled="isSpecialPrescriptionCartLocked" class="w-1/5">
<template #addonBefore>
<MinusOutlined
key="prop"
@click="dayDosage = dayDosage > 1 ? dayDosage - 1 : 1"
@click="!isSpecialPrescriptionCartLocked && (dayDosage = dayDosage > 1 ? dayDosage - 1 : 1)"
/>
</template>
<template #addonAfter>
<PlusOutlined
key="add"
@click="dayDosage = dayDosage < 100 ? dayDosage + 1 : 100"
@click="!isSpecialPrescriptionCartLocked && (dayDosage = dayDosage < 100 ? dayDosage + 1 : 100)"
/>
</template>
</InputNumber>
次/天
</div>
</div>
<Button type="primary" @click="openDiagnosisModal">常用诊断</Button>
<Textarea v-model:value="diagnosis" placeholder="输入诊断结果..."/>
<Button type="primary" @click="openDoctorOrderModal">常用医嘱</Button>
<Textarea v-model:value="medicalAdvice" placeholder="输入医嘱..."/>
<Button type="primary" @click="openDiagnosisModal">
常用诊断
</Button>
<Textarea
v-model:value="diagnosis"
placeholder="输入诊断结果..."
/>
<Button type="primary" @click="openDoctorOrderModal">
常用医嘱
</Button>
<Textarea
v-model:value="medicalAdvice"
placeholder="输入医嘱..."
/>
诊疗费用:
<InputNumber
v-model:value="treatmentPrice"
@@ -3215,7 +3302,7 @@ watch(
<div class="total-cost">
商品价格:
<Button
v-if="priceAdjustEnabled && currentDrugs.length"
v-if="priceAdjustEnabled && currentDrugs.length && !isSpecialPrescriptionCartLocked"
type="link"
class="!p-0"
@click="openOrderPriceAdjust"
@@ -3257,6 +3344,14 @@ watch(
>
发送处方
</Button>
<Button
v-if="canUseCommonPrescription && currentDrugs.length"
type="link"
class="mt-2 w-full"
@click="openSaveCommonPrescriptionModal"
>
保存为常用方
</Button>
</div>
</Page>
<div v-else-if="tabType === 0" class="prescription-panel">