feat:优化了订单、药品这些交互

This commit is contained in:
李琦
2026-07-21 07:36:52 +08:00
parent 7656c3ac49
commit ee4490a91c
23 changed files with 692 additions and 109 deletions

View File

@@ -561,7 +561,9 @@ export const usePrescriptionStore = defineStore('prescription', () => {
use_frequency: drugUseFrequency.value.find(
(item) => item.id === data.drug.frequency_id,
),
unit: drugUnit.value.find((item) => item.id === data.drug.unit_id),
unit:
data.drug?.unit ||
drugUnit.value.find((item) => item.id === data.drug.unit_id),
price: data.price,
buy_price: data.buy_price,
way_id: data.drug?.way_id,
@@ -658,6 +660,11 @@ export const usePrescriptionStore = defineStore('prescription', () => {
if (data) {
newDrugInfo.value.price = data.price;
newDrugInfo.value.name = data.drug.drug_name;
// 选药后立刻带上真实单位,供新行数量后缀展示
newDrugInfo.value.unit =
data.drug?.unit ||
drugUnit.value.find((item) => item.id === data.drug?.unit_id);
newDrugInfo.value.unit_id = data.drug?.unit_id;
nextTick(() => {
const input = document.querySelector(
'.new-number-input input',
@@ -693,7 +700,9 @@ export const usePrescriptionStore = defineStore('prescription', () => {
use_frequency: drugUseFrequency.value.find(
(item) => item.id === data.drug.frequency_id,
),
unit: drugUnit.value.find((item) => item.id === data.drug.unit_id),
unit:
data.drug?.unit ||
drugUnit.value.find((item) => item.id === data.drug.unit_id),
price: data.price,
buy_price: data.buy_price,
way_id: data.drug?.way_id,

View File

@@ -1124,7 +1124,7 @@ const cancelSaveCommonPrescription = () => {
prescriptionStore.updateChineseNumberGoNewDrug($event)
"
>
<template #addonAfter> g</template>
<template #addonAfter>{{ drug.unit?.name || 'g' }}</template>
</InputNumber>
<Select
@@ -1203,7 +1203,7 @@ const cancelSaveCommonPrescription = () => {
prescriptionStore.selectDrugByNewDrugInfo($event, true)
"
>
<template #addonAfter> g</template>
<template #addonAfter>{{ prescriptionStore.newDrugInfo.unit?.name || 'g' }}</template>
</InputNumber>
<Select

View File

@@ -403,14 +403,14 @@ function prescriptionStatusColor() {
<div class="mt-1 text-gray-600 dark:text-gray-400">
供货价{{
item.buy_price != null && item.buy_price !== ''
? `${item.buy_price} 元/g`
? `${item.buy_price} 元/${item.drug?.unit?.name || item.unit?.name || 'g'}`
: '—'
}}
</div>
<div class="mt-1 text-gray-600 dark:text-gray-400">
售价{{
item.buy_price != null && item.buy_price !== ''
? `${item.price} 元/g`
? `${item.price} 元/${item.drug?.unit?.name || item.unit?.name || 'g'}`
: '—'
}}
</div>
@@ -436,7 +436,7 @@ function prescriptionStatusColor() {
</div>
<div>
<h4>{{ item.drug_name }}</h4>
<p>规格: {{ item.drug.specification || 'g' }}</p>
<p>规格: {{ item.drug.specification || item.drug?.unit?.name || 'g' }}</p>
<p>数量: {{ item.number }}</p>
<p>单价: {{ item.price }} </p>
<p>

View File

@@ -17,7 +17,7 @@ export const gridOptions: VxeGridProps<RowType> = {
},
columns: [
{ type: 'expand', width: 80, slots: { content: 'expand-content' } },
{ field: 'id', align: 'left', title: 'ID', width: 100 },
{ field: 'id', align: 'left', title: 'ID', width: 50 },
{
field: 'order_no',
align: 'left',

View File

@@ -1394,14 +1394,14 @@ const openOrderAmountVerify = () => {
<div class="mt-1 text-gray-600 dark:text-gray-400">
供货价:{{
item.buy_price != null && item.buy_price !== ''
? `${item.buy_price} 元/g`
? `${item.buy_price} 元/${item.drug?.unit?.name || item.unit?.name || 'g'}`
: '—'
}}
</div>
<div class="mt-1 text-gray-600 dark:text-gray-400">
售价:{{
item.buy_price != null && item.buy_price !== ''
? `${item.price} 元/g`
? `${item.price} 元/${item.drug?.unit?.name || item.unit?.name || 'g'}`
: '—'
}}
</div>

View File

@@ -0,0 +1,34 @@
import { requestClient } from '#/api/request';
// 后端路由前缀patient-call-log-dictroutes/admin.php 中 cc_auto_route_register 注册)
const prefix = 'patient-call-log-dict/';
/** 列表(分页 + 搜索 name/type/status */
export async function getPatientCallLogDictList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
/** 下拉:可选 type 过滤type=1 结果 / type=2 标签) */
export async function getPatientCallLogDictOption(data?: any) {
return requestClient.get<any>(`${prefix}option`, { params: data });
}
/** 详情 */
export async function getPatientCallLogDictInfo(id: number) {
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
}
/** 新增 */
export async function createPatientCallLogDict(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}create`, data);
}
/** 更新 */
export async function updatePatientCallLogDict(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update`, data);
}
/** 删除软删ids 数组) */
export async function deletePatientCallLogDict(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}delete`, data);
}

View File

@@ -0,0 +1,88 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createPatientCallLogDict,
updatePatientCallLogDict,
} from '../api';
import { modalFormProps } from '../config/form';
const isUpdate = ref(false);
const gridApi = ref();
const [Form, formApi] = useVbenForm(modalFormProps);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
formApi.validate().then(async (e: any) => {
if (e.valid) {
const values = await formApi.getValues();
modalApi.setState({ loading: true, confirmLoading: true });
const submitApi = isUpdate.value
? updatePatientCallLogDict
: createPatientCallLogDict;
submitApi(values)
.then(() => {
message.success('保存成功');
gridApi.value?.reload();
modalApi.close();
})
.finally(() => {
modalApi.setState({ loading: false, confirmLoading: false });
});
}
});
await formApi.validateAndSubmitForm();
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (isOpen) {
const { values, update } = modalApi.getData<Record<string, any>>();
if (values && update) {
// 编辑:回填后端返回的字段
isUpdate.value = true;
formApi.setValues({
id: values.id || '',
type: values.type ?? 1,
name: values.name || '',
value: values.value || '',
color: values.color || '#6acdbb',
sort: values.sort ?? 0,
status: values.status ?? 1,
});
} else {
// 新增:默认值
isUpdate.value = false;
formApi.resetForm();
formApi.setValues({
id: '',
type: 1,
name: '',
value: '',
color: '#6acdbb',
sort: 0,
status: 1,
});
}
} else {
formApi.resetForm();
}
},
});
</script>
<template>
<Modal :title="`${isUpdate === true ? '编辑' : '新增'}回访字典`" class="w-[50%]">
<Form />
</Modal>
</template>

View File

@@ -0,0 +1,96 @@
import type { VbenFormProps } from '#/adapter/form';
export const modalFormProps: VbenFormProps = {
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
fieldName: 'id',
label: 'ID',
formItemClass: 'col-span-6',
defaultValue: '',
dependencies: {
show: false,
triggerFields: ['id'],
},
},
{
// 1 回访结果 / 2 回访标签;区分后续列表渲染
component: 'VbenSelect',
componentProps: {
placeholder: '请选择类型',
options: [
{ label: '回访结果', value: 1 },
{ label: '回访标签', value: 2 },
],
},
fieldName: 'type',
label: '类型',
rules: 'required',
defaultValue: 1,
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入展示名(如 接通/关怀)',
},
fieldName: 'name',
label: '名称',
rules: 'required',
defaultValue: '',
},
{
// 小程序存储的英文/拼音 value与历史记录匹配
component: 'VbenInput',
componentProps: {
placeholder: '请输入存储值(如 connected/care',
},
fieldName: 'value',
label: '存储值',
rules: 'required',
defaultValue: '',
},
{
// hex 颜色,用于小程序彩色 tag
component: 'VbenInput',
componentProps: {
placeholder: '请输入 hex 颜色,如 #6acdbb',
},
fieldName: 'color',
label: '颜色',
defaultValue: '#6acdbb',
},
{
component: 'VbenInputNumber',
componentProps: {
min: 0,
precision: 0,
},
fieldName: 'sort',
label: '排序',
defaultValue: 0,
},
{
component: 'VbenSelect',
componentProps: {
placeholder: '请选择状态',
options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
],
},
fieldName: 'status',
label: '状态',
formItemClass: 'col-span-6',
defaultValue: 1,
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,45 @@
import type { VbenFormProps } from '@vben/common-ui';
// 列表搜索:按名称模糊、类型精确、状态精确
export const formOptions: VbenFormProps = {
layout: 'inline',
showResetButton: true,
showSubmitButton: true,
schemas: [
{
fieldName: 'name',
component: 'Input',
label: '名称',
componentProps: {
placeholder: '请输入名称',
allowClear: true,
},
},
{
fieldName: 'type',
component: 'Select',
label: '类型',
componentProps: {
placeholder: '请选择类型',
allowClear: true,
options: [
{ label: '回访结果', value: 1 },
{ label: '回访标签', value: 2 },
],
},
},
{
fieldName: 'status',
component: 'Select',
label: '状态',
componentProps: {
placeholder: '请选择状态',
allowClear: true,
options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
],
},
},
],
};

View File

@@ -0,0 +1,79 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getPatientCallLogDictList } from '../api';
interface RowType {
id: number;
type: number;
type_txt: string;
name: string;
value: string;
color: string;
sort: number;
status: number;
status_txt: string;
created_at: string;
updated_at: string;
}
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: '',
},
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
isHover: true,
isCurrent: true,
},
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', align: 'left', title: 'ID', width: 80 },
{ field: 'type_txt', align: 'left', title: '类型', width: 110 },
{ field: 'name', align: 'left', title: '名称', minWidth: 140 },
{ field: 'value', align: 'left', title: '存储值', minWidth: 140 },
{
field: 'color',
align: 'left',
title: '颜色',
width: 120,
// 用色块+hex 直观预览,避免放函数调用导致渲染异常
slots: { default: 'color' },
},
{ field: 'sort', align: 'left', title: '排序', width: 90 },
{ field: 'status_txt', align: 'left', title: '状态', width: 90 },
{ field: 'created_at', title: '创建时间', width: 180 },
{ type: 'html', title: '操作', slots: { default: 'action' }, width: 160 },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getPatientCallLogDictList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
border: false,
toolbarConfig: {
search: true,
refresh: true,
print: false,
export: false,
zoom: true,
slots: {
buttons: 'toolbar-buttons',
},
custom: {
icon: 'vxe-icon-menu',
},
},
showOverflow: false,
};

View File

@@ -0,0 +1,124 @@
<script lang="ts" setup>
import type { VxeGridListeners } from '#/adapter/vxe-table';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import { deletePatientCallLogDict } from './api';
import DictModal from './components/modal.vue';
import { formOptions as searchFormOptions } from './config/search';
import { gridOptions } from './config/table';
const hasTopTableDropDownActions = ref(false);
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
checkboxAll() {
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: searchFormOptions,
gridOptions,
gridEvents,
});
const [DictFormModal, dictFormModalApi] = useVbenModal({
connectedComponent: DictModal,
});
const showDictModal = (data = {}, isUpdate = false) => {
dictFormModalApi.setData({
values: data,
update: isUpdate,
gridApi,
});
dictFormModalApi.open();
};
const deleteDictApi = (row: any) => {
let ids: (string | number)[] = [];
if (row) {
ids.push(row);
} else {
ids = gridApi.grid.getCheckboxRecords().map((item: any) => item.id);
}
deletePatientCallLogDict({ ids }).then(() => {
message.success('删除成功!');
gridApi.query();
});
};
</script>
<template>
<Page auto-content-height title="回访字典管理">
<DictFormModal />
<div class="p-4">
<TableAction
:actions="[
{
label: '新增',
type: 'primary',
icon: 'ant-design:plus-outlined',
onClick: () => showDictModal({}, false),
},
]"
/>
<Grid>
<template #toolbar-buttons>
<Button
v-if="hasTopTableDropDownActions"
danger
type="primary"
@click="deleteDictApi()"
>
删除
</Button>
</template>
<!-- 颜色列色块 + hex 直观预览 -->
<template #color="{ row }">
<div class="flex items-center gap-2">
<span
class="inline-block h-4 w-4 rounded"
:style="{ background: row.color, border: '1px solid #e5e7eb' }"
/>
<span>{{ row.color }}</span>
</div>
</template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '编辑',
onClick: () => showDictModal(row, true),
},
{
label: '删除',
color: 'error',
popConfirm: {
title: '确定要删除该项吗?',
onConfirm: () => deleteDictApi(row.id),
},
},
]"
/>
</template>
</Grid>
</div>
</Page>
</template>

View File

@@ -33,6 +33,8 @@ export interface ChineseDrugItem {
price: number;
buy_price?: number;
way_id: number;
unit_id?: number;
unit?: { id: number; name: string } | null;
}
const props = withDefaults(
@@ -123,6 +125,8 @@ const newDrugInfo = ref<{
number?: number;
price?: number;
way_id?: number;
unit_id?: number;
unit?: { id: number; name: string } | null;
}>({});
onMounted(async () => {
@@ -180,6 +184,10 @@ function selectNewDrug(drugId: number) {
newDrugInfo.value.name = selectedItem.drug?.drug_name || '';
newDrugInfo.value.price = selectedItem.price || 0;
newDrugInfo.value.way_id = 0;
newDrugInfo.value.unit =
selectedItem.drug?.unit || selectedItem.unit || null;
newDrugInfo.value.unit_id =
selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
nextTick(() => {
const numberInput = document.querySelector('.new-chinese-number input') as HTMLInputElement;
numberInput?.focus();
@@ -204,6 +212,8 @@ function addChineseDrug() {
number: newDrugInfo.value.number,
price: newDrugInfo.value.price || 0,
way_id: newDrugInfo.value.way_id || 0,
unit_id: newDrugInfo.value.unit_id || 0,
unit: newDrugInfo.value.unit || null,
});
newDrugInfo.value = {};
chineseSearchResults.value = [];
@@ -244,6 +254,8 @@ function changeChineseDrug(index: number, drugId: number) {
drug.drug_id = drugId;
drug.drug_name = selectedItem.drug?.drug_name || '';
drug.price = selectedItem.price || 0;
drug.unit = selectedItem.drug?.unit || selectedItem.unit || null;
drug.unit_id = selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
syncToParent();
nextTick(() => {
const numberInput = document.querySelector(`.chinese-number-${index} input`) as HTMLInputElement;
@@ -287,6 +299,8 @@ function loadDrugs(recipes: any[], dosage?: number, dayDosage?: number, drugPric
price: zeroPrice ? 0 : (configured?.sell_price ?? recipe.price ?? 0),
buy_price: zeroPrice ? 0 : (configured?.buy_price ?? recipe.buy_price ?? 0),
way_id: recipe.way_id || 0,
unit_id: recipe.unit_id || 0,
unit: recipe.unit || null,
};
});
if (dosage !== undefined) dosageLocal.value = dosage;
@@ -404,7 +418,7 @@ defineExpose({
<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>{{ drug.number }}{{ drug.unit?.name || 'g' }}</span>
<span class="chinese-drug-comma"></span>
<span>{{ getWayName(drug.way_id) }}</span>
</div>
@@ -439,7 +453,7 @@ defineExpose({
style="width: 60px"
@change="onDrugNumberChange"
/>
<span class="chinese-drug-unit">g</span>
<span class="chinese-drug-unit">{{ drug.unit?.name || 'g' }}</span>
<span class="chinese-drug-comma"></span>
<Select
v-model:value="drug.way_id"
@@ -535,7 +549,7 @@ defineExpose({
style="width: 60px"
@keydown="(e) => handleChineseKeydown(e, true)"
/>
<span class="chinese-drug-unit">g</span>
<span class="chinese-drug-unit">{{ newDrugInfo.unit?.name || 'g' }}</span>
<span class="chinese-drug-comma"></span>
<Select
v-model:value="newDrugInfo.way_id"

View File

@@ -142,7 +142,8 @@ function getDrugNames(recipes: any[], nameField = 'drug_name', showNumber = fals
return recipes.map((item) => {
const name = item[nameField] || item.name;
if (showNumber && item.number) {
return `${name}(${item.number}g)`;
const unit = item.unit?.name || item.unit_name || 'g';
return `${name}(${item.number}${unit})`;
}
return name;
}).join('、');

View File

@@ -250,7 +250,7 @@ function parseRecipeContent(content: string | Record<string, unknown>) {
<span class="drug-name">{{
drug?.name || drug?.drug_name
}}</span>
<span class="drug-quantity mr-5">{{ drug?.number }} /g</span>
<span class="drug-quantity mr-5">{{ drug?.number }} /{{ drug?.unit?.name || drug?.use_unit?.name || 'g' }}</span>
</div>
<div>
方法:

View File

@@ -122,6 +122,13 @@ const getDrugListByWesternModal = debounce(async (searchText = '') => {
drugList.value = res.map((item) => {
const matched = selectList.value.find((v) => v.index_id === item.id);
const unitId = matched?.unit_id || item.drug?.unit_id;
// 优先接口返回的 unit再回退本地字典保证中药网格展示真实单位
const unitObj =
matched?.unit ||
item.drug?.unit ||
item.unit ||
drugUnit.value.find((u) => u.id === unitId);
return {
...item,
select_number: matched?.select_number || 0,
@@ -133,7 +140,8 @@ const getDrugListByWesternModal = debounce(async (searchText = '') => {
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,
unit_id: unitId,
unit: unitObj,
},
};
});
@@ -190,8 +198,10 @@ function addProducts(data) {
use_frequency: drugUseFrequency.value.find(
(item) => item.id === data.drug.frequency_id,
),
// 药品单位信息
unit: drugUnit.value.find((item) => item.id === data.drug.unit_id),
// 药品单位:优先用选药接口返回的 unit再回退本地字典
unit:
data.drug?.unit ||
drugUnit.value.find((item) => item.id === data.drug.unit_id),
// 药品价格
price: data.price,
// 药品使用方式ID
@@ -751,7 +761,7 @@ function updateProductNumber(id, number) {
</div>
<div class="text-right">
<div class="text-red-500">¥{{ item.price }}</div>
<div class="text-xs text-gray-400 dark:text-gray-500">/g</div>
<div class="text-xs text-gray-400 dark:text-gray-500">/{{ item.drug?.unit?.name || item.unit?.name || 'g' }}</div>
</div>
</div>
@@ -785,7 +795,7 @@ function updateProductNumber(id, number) {
class="flex-1 !bg-transparent !border-0 shadow-none text-center"
@change="updateProductNumber(item.id, item.drug.number)"
/>
<span class="text-gray-400 dark:text-gray-500 text-xs px-1">g</span>
<span class="text-gray-400 dark:text-gray-500 text-xs px-1">{{ item.drug?.unit?.name || item.unit?.name || 'g' }}</span>
</div>
<!-- 加减按钮 (Action) -->

View File

@@ -1672,7 +1672,7 @@ function mapWestRepiceToProduct(recipe: any): Record<string, any> | null {
use_frequency: drugUseFrequency.value.find(
(item: any) => item.id === freqId,
),
unit: drugUnit.value.find((item: any) => item.id === unitId),
unit: drugObj.unit || drugUnit.value.find((item: any) => item.id === unitId),
price: Number(drugObj.sell_price ?? drugObj.price ?? 0),
way_id: drugObj.way_id,
use_ways: drugUseWay.value.find((item: any) => item.id === drugObj.way_id),
@@ -1755,7 +1755,7 @@ async function applyHistoricalPrescriptionDetail(detail: any): Promise<boolean>
use_frequency: drugUseFrequency.value.find(
(item: any) => item.id === d.frequency_id,
),
unit: drugUnit.value.find((item: any) => item.id === d.unit_id),
unit: d.unit || drugUnit.value.find((item: any) => item.id === d.unit_id),
time_id: d.time_id,
type_id: d.type_id,
frequency_id: d.frequency_id,
@@ -2103,8 +2103,13 @@ function selectNewDrugInfo() {
const data = drugList.value.find((v) => v.drug_id === newDrugInfo.value.id);
newDrugInfo.value.price = data.price;
newDrugInfo.value.name = data.drug.drug_name;
// 选药后立刻带上真实单位,供新行数量后缀展示(优先接口返回的 unit
newDrugInfo.value.unit =
data.drug?.unit ||
drugUnit.value.find((item) => item.id === data.drug?.unit_id);
newDrugInfo.value.unit_id = data.drug?.unit_id;
setTimeout(() => {
// 获取新药品卡片中的数输入框并聚焦
// 获取新药品卡片中的数输入框并聚焦
const newDrugNumberInput = document.querySelector(
'.new-number-input input',
);
@@ -2154,8 +2159,10 @@ function selectOldDrugInfo(id) {
use_frequency: drugUseFrequency.value.find(
(item) => item.id === data.drug.frequency_id,
),
// 药品单位信息
unit: drugUnit.value.find((item) => item.id === data.drug.unit_id),
// 药品单位:优先用选药接口返回的 unit再回退本地字典
unit:
data.drug?.unit ||
drugUnit.value.find((item) => item.id === data.drug.unit_id),
// 药品价格
price: data.price,
buy_price: data.buy_price,
@@ -2295,8 +2302,10 @@ function addProducts(data) {
use_frequency: drugUseFrequency.value.find(
(item) => item.id === data.drug.frequency_id,
),
// 药品单位信息
unit: drugUnit.value.find((item) => item.id === data.drug.unit_id),
// 药品单位:优先用选药接口返回的 unit再回退本地字典
unit:
data.drug?.unit ||
drugUnit.value.find((item) => item.id === data.drug.unit_id),
// 药品价格
price: data.price,
buy_price: data.buy_price,
@@ -2910,7 +2919,7 @@ watch(
@blur="updateChineseNumber"
@keydown="updateChineseNumberGoNewDrug($event)"
>
<template #addonAfter> g</template>
<template #addonAfter>{{ drug.unit?.name || 'g' }}</template>
</InputNumber>
,
<span>
@@ -2993,7 +3002,7 @@ watch(
@blur="newDrugBlur"
@keydown="selectDrugByNewDrugInfo($event, true)"
>
<template #addonAfter> g</template>
<template #addonAfter>{{ newDrugInfo.unit?.name || 'g' }}</template>
</InputNumber>
,
<span>
@@ -3067,7 +3076,7 @@ watch(
@keydown="selectDrugByNewDrugInfo($event, true)"
disabled
>
<template #addonAfter> g</template>
<template #addonAfter>{{ newDrugInfo.unit?.name || 'g' }}</template>
</InputNumber>
,
<span>
@@ -3141,7 +3150,7 @@ watch(
@keydown="selectDrugByNewDrugInfo($event, true)"
disabled
>
<template #addonAfter> g</template>
<template #addonAfter>{{ newDrugInfo.unit?.name || 'g' }}</template>
</InputNumber>
,
<span>
@@ -3270,7 +3279,7 @@ watch(
class="w-full"
@blur="updateChineseNumber"
>
<template #addonAfter> g</template>
<template #addonAfter>{{ drug.unit?.name || 'g' }}</template>
</InputNumber>
</div>
<div v-else class="quantity-control">

View File

@@ -187,6 +187,8 @@ const newDrugInfo = ref<{
number?: number;
price?: number;
way_id?: number;
unit_id?: number;
unit?: { id: number; name: string } | null;
}>({});
// ==================== 计算属性 ====================
@@ -418,8 +420,12 @@ function selectNewDrug(drugId: number) {
newDrugInfo.value.name = selectedItem.drug?.drug_name || '';
newDrugInfo.value.price = selectedItem.price || 0;
newDrugInfo.value.way_id = 0;
newDrugInfo.value.unit =
selectedItem.drug?.unit || selectedItem.unit || null;
newDrugInfo.value.unit_id =
selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
// 聚焦到数输入框
// 聚焦到数输入框
nextTick(() => {
const numberInput = document.querySelector('.new-chinese-number input') as HTMLInputElement;
if (numberInput) {
@@ -462,6 +468,8 @@ function addChineseDrug() {
number: newDrugInfo.value.number,
price: newDrugInfo.value.price || 0,
way_id: newDrugInfo.value.way_id || 0,
unit_id: newDrugInfo.value.unit_id || 0,
unit: newDrugInfo.value.unit || null,
});
// 清空并准备下一次输入
@@ -512,9 +520,11 @@ function changeChineseDrug(index: number, drugId: number) {
drug.drug_id = drugId;
drug.drug_name = selectedItem.drug?.drug_name || '';
drug.price = selectedItem.price || 0;
drug.unit = selectedItem.drug?.unit || selectedItem.unit || null;
drug.unit_id = selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
}
// 聚焦到数输入框
// 聚焦到数输入框
nextTick(() => {
const numberInput = document.querySelector(`.chinese-number-${index} input`) as HTMLInputElement;
if (numberInput) {
@@ -705,6 +715,8 @@ async function handleSave() {
number: drug.number || 1,
price: drug.price || 0,
way_id: drug.way_id || 0,
unit_id: drug.unit_id || 0,
unit: drug.unit || null,
}));
await saveChineseCommonPrescriptionApi({
@@ -731,6 +743,8 @@ async function handleSave() {
number: drug.number || 1,
price: drug.price || 0,
way_id: drug.way_id || 0,
unit_id: drug.unit_id || 0,
unit: drug.unit || null,
}));
await saveGranularCommonPrescriptionApi({
@@ -1118,7 +1132,7 @@ async function handleSave() {
:min="1"
style="width: 60px"
/>
<span class="chinese-drug-unit">g</span>
<span class="chinese-drug-unit">{{ drug.unit?.name || 'g' }}</span>
<span class="chinese-drug-comma"></span>
<Select
v-model:value="drug.way_id"
@@ -1181,7 +1195,7 @@ async function handleSave() {
style="width: 60px"
@keydown="(e) => handleChineseKeydown(e, true)"
/>
<span class="chinese-drug-unit">g</span>
<span class="chinese-drug-unit">{{ newDrugInfo.unit?.name || 'g' }}</span>
<span class="chinese-drug-comma"></span>
<Select
v-model:value="newDrugInfo.way_id"

View File

@@ -187,6 +187,8 @@ const newDrugInfo = ref<{
number?: number;
price?: number;
way_id?: number;
unit_id?: number;
unit?: { id: number; name: string } | null;
}>({});
// ==================== 计算属性 ====================
@@ -460,8 +462,13 @@ function selectNewDrug(drugId: number) {
newDrugInfo.value.name = selectedItem.drug?.drug_name || '';
newDrugInfo.value.price = selectedItem.price || 0;
newDrugInfo.value.way_id = 0;
// 选药后带上真实单位,供数量后缀展示
newDrugInfo.value.unit =
selectedItem.drug?.unit || selectedItem.unit || null;
newDrugInfo.value.unit_id =
selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
// 聚焦到数输入框
// 聚焦到数输入框
nextTick(() => {
const numberInput = document.querySelector('.edit-new-chinese-number input') as HTMLInputElement;
if (numberInput) {
@@ -504,6 +511,8 @@ function addChineseDrug() {
number: newDrugInfo.value.number,
price: newDrugInfo.value.price || 0,
way_id: newDrugInfo.value.way_id || 0,
unit_id: newDrugInfo.value.unit_id || 0,
unit: newDrugInfo.value.unit || null,
});
// 清空并准备下一次输入
@@ -554,9 +563,11 @@ function changeChineseDrug(index: number, drugId: number) {
drug.drug_id = drugId;
drug.drug_name = selectedItem.drug?.drug_name || '';
drug.price = selectedItem.price || 0;
drug.unit = selectedItem.drug?.unit || selectedItem.unit || null;
drug.unit_id = selectedItem.drug?.unit_id || selectedItem.unit_id || 0;
}
// 聚焦到数输入框
// 聚焦到数输入框
nextTick(() => {
const numberInput = document.querySelector(`.edit-chinese-number-${index} input`) as HTMLInputElement;
if (numberInput) {
@@ -733,6 +744,8 @@ async function handleSave() {
number: drug.number || 1,
price: drug.price || 0,
way_id: drug.way_id || 0,
unit_id: drug.unit_id || 0,
unit: drug.unit || null,
}));
await updateChineseCommonPrescriptionApi({
@@ -759,6 +772,8 @@ async function handleSave() {
number: drug.number || 1,
price: drug.price || 0,
way_id: drug.way_id || 0,
unit_id: drug.unit_id || 0,
unit: drug.unit || null,
}));
await updateGranularCommonPrescriptionApi({
@@ -1136,7 +1151,7 @@ async function handleSave() {
:min="1"
style="width: 60px"
/>
<span class="chinese-drug-unit">g</span>
<span class="chinese-drug-unit">{{ drug.unit?.name || 'g' }}</span>
<span class="chinese-drug-comma"></span>
<Select
v-model:value="drug.way_id"
@@ -1199,7 +1214,7 @@ async function handleSave() {
style="width: 60px"
@keydown="(e) => handleChineseKeydown(e, true)"
/>
<span class="chinese-drug-unit">g</span>
<span class="chinese-drug-unit">{{ newDrugInfo.unit?.name || 'g' }}</span>
<span class="chinese-drug-comma"></span>
<Select
v-model:value="newDrugInfo.way_id"

View File

@@ -16,13 +16,6 @@ export async function getDeliveryWarehouseOrderDetail(id: number | string) {
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
}
/**
* 按订单查物流轨迹(复用平台 express-detail 接口)
*/
export async function expressDetailByOrderId(data: Record<string, any>) {
return requestClient.post<any>(`express-detail/detail-by-order`, data);
}
/**
* 配送仓库订单发货
*/

View File

@@ -5,17 +5,13 @@ import { useVbenModal } from '@vben/common-ui';
import { Button, Descriptions, Image, Tabs, Tag, Timeline } from 'ant-design-vue';
import {
expressDetailByOrderId,
getDeliveryWarehouseOrderDetail,
} from '#/views/system/delivery-warehouse-order/api';
import { getDeliveryWarehouseOrderDetail } from '#/views/system/delivery-warehouse-order/api';
defineOptions({
name: 'DeliveryWarehouseOrderDetailModal',
});
const data = ref<Record<string, any>>({});
const expressDetail = ref<Record<string, any>>({});
const scrollToLogistics = ref(false);
const logisticsAnchorRef = ref<HTMLElement | null>(null);
/** 待发货时由列表传入,点击后关详情并打开发货弹窗 */
@@ -29,13 +25,9 @@ const canShip = computed(() => {
Number(data.value.status) === 1;
return (mineUnsent || legacy) && typeof onShipFn.value === 'function';
});
/** 仅使用仓侧详情返回的本仓 packages避免平台物流接口带出他仓商品 */
const packageTabs = computed(() => {
const packages = Array.isArray(data.value.packages)
? data.value.packages
: Array.isArray(expressDetail.value.packages)
? expressDetail.value.packages
: [];
return packages;
return Array.isArray(data.value.packages) ? data.value.packages : [];
});
const activePkg = ref('0');
@@ -58,27 +50,17 @@ const deliveryMethodMap: Record<number, string> = {
};
/**
* 拉取订单详情;已发货时继续拉物流轨迹
* 拉取仓侧订单详情(含本仓药品 + 本仓包裹物流)
*/
async function loadDetail(id: number | string) {
data.value = (await getDeliveryWarehouseOrderDetail(id)) || {};
expressDetail.value = {};
const sent =
Number(data.value.is_send) === 1 ||
Number(data.value.status) >= 2 ||
!!data.value.express_no_id;
if (sent && data.value.id) {
try {
expressDetail.value = await expressDetailByOrderId({
order_id: data.value.id,
});
} catch {
expressDetail.value = {};
}
}
activePkg.value = '0';
if (scrollToLogistics.value) {
await nextTick();
logisticsAnchorRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' });
logisticsAnchorRef.value?.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}
@@ -119,7 +101,6 @@ const [Modal, modalApi] = useVbenModal({
onOpenChange(isOpen: boolean) {
if (!isOpen) {
data.value = {};
expressDetail.value = {};
scrollToLogistics.value = false;
onShipFn.value = null;
return;
@@ -233,7 +214,11 @@ const [Modal, modalApi] = useVbenModal({
<section ref="logisticsAnchorRef">
<h3 class="section-title">物流信息</h3>
<Tabs v-if="packageTabs.length" v-model:active-key="activePkg" type="card">
<Tabs
v-if="packageTabs.length"
v-model:active-key="activePkg"
type="card"
>
<Tabs.TabPane
v-for="(pkg, idx) in packageTabs"
:key="String(idx)"
@@ -256,7 +241,9 @@ const [Modal, modalApi] = useVbenModal({
</Descriptions.Item>
</Descriptions>
<div
v-if="Array.isArray(pkg.express.detail) && pkg.express.detail.length"
v-if="
Array.isArray(pkg.express.detail) && pkg.express.detail.length
"
class="mt-4"
>
<Timeline>
@@ -264,7 +251,9 @@ const [Modal, modalApi] = useVbenModal({
v-for="(detail, dIdx) in pkg.express.detail"
:key="dIdx"
>
<Tag :color="getColor(detail.status)">{{ detail.status }}</Tag>
<Tag :color="getColor(detail.status)">{{
detail.status
}}</Tag>
<p>{{ detail.detail_at }}</p>
<p>{{ detail.detail }}</p>
</Timeline.Item>
@@ -279,8 +268,7 @@ const [Modal, modalApi] = useVbenModal({
v-else-if="
Number(data.is_send) === 1 ||
Number(data.status) >= 2 ||
expressNoRow().express_no ||
expressDetail.express_no
expressNoRow().express_no
"
>
<Descriptions
@@ -289,33 +277,12 @@ const [Modal, modalApi] = useVbenModal({
size="small"
>
<Descriptions.Item label="物流公司">
{{
expressDetail.express_company_name ||
expressNoRow().express_company_name ||
'-'
}}
{{ expressNoRow().express_company_name || '-' }}
</Descriptions.Item>
<Descriptions.Item label="运单号">
{{
expressDetail.express_no || expressNoRow().express_no || '-'
}}
</Descriptions.Item>
<Descriptions.Item label="最新状态">
{{ expressDetail.state_txt || '-' }}
{{ expressNoRow().express_no || '-' }}
</Descriptions.Item>
</Descriptions>
<div v-if="Array.isArray(expressDetail.detail) && expressDetail.detail.length" class="mt-4">
<Timeline>
<Timeline.Item
v-for="(detail, index) in expressDetail.detail"
:key="index"
>
<Tag :color="getColor(detail.status)">{{ detail.status }}</Tag>
<p>{{ detail.detail_at }}</p>
<p>{{ detail.detail }}</p>
</Timeline.Item>
</Timeline>
</div>
</template>
<div v-else class="empty-tip">暂无物流</div>
</section>

View File

@@ -48,9 +48,11 @@ const [Modal, modalApi] = useVbenModal({
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (isOpen) {
const { values, update } = modalApi.getData<Record<string, any>>();
const { values, update } = modalApi.getData<Record<string, any>>() || {};
isUpdate.value = !!update;
// 新建时清空表单,避免残留编辑态的 id/证照
formApi.resetForm();
if (values) {
isUpdate.value = update;
formApi.setValues(values);
}
}
@@ -60,7 +62,7 @@ const [Modal, modalApi] = useVbenModal({
<template>
<Modal
:title="`${isUpdate === true ? '编辑' : '新增'}配送仓库`"
class="w-[40%]"
class="w-[720px]"
>
<Form />
</Modal>

View File

@@ -1,8 +1,10 @@
import type { VbenFormProps } from '#/adapter/form';
/**
* 配送仓库新建/编辑:仅基本信息
* 合同与银行卡走独立报备入口,不在此表单填写
* 配送仓库新建/编辑
* - 三证必填,其余证照/合同选填
* - 联系人/手机号仅新建时展示,用于开通仓管理员
* - 银行卡仍走独立报备入口
*/
export const modalFormProps: VbenFormProps = {
wrapperClass: 'grid-cols-12',
@@ -41,6 +43,87 @@ export const modalFormProps: VbenFormProps = {
fieldName: 'introduce',
label: '仓库介绍',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '联系人姓名(将作为仓管理员昵称)',
},
fieldName: 'contact_name',
label: '联系人',
rules: 'required',
help: '新建时用于开通配送仓库管理员账号',
dependencies: {
// 仅新建展示;编辑改账号请走「配送仓管理员」菜单
if({ id }: { id?: number | string }) {
return !id;
},
triggerFields: ['id'],
},
},
{
component: 'VbenInput',
componentProps: {
placeholder: '手机号(仓管理员登录手机)',
},
fieldName: 'phone',
label: '手机号',
rules: 'required',
help: '默认初始密码 Xk123456@',
dependencies: {
if({ id }: { id?: number | string }) {
return !id;
},
triggerFields: ['id'],
},
},
{
component: 'Avatar',
fieldName: 'business_license_image',
label: '营业执照',
rules: 'required',
formItemClass: 'col-span-4',
},
{
component: 'Avatar',
fieldName: 'drug_license_image',
label: '药品经营许可证',
rules: 'required',
formItemClass: 'col-span-4',
},
{
component: 'Avatar',
fieldName: 'medical_device_license_image',
label: '医疗器械经营许可证',
rules: 'required',
formItemClass: 'col-span-4',
},
{
component: 'Avatar',
fieldName: 'food_license_image',
label: '食品经营许可证',
formItemClass: 'col-span-4',
},
{
component: 'Avatar',
fieldName: 'pharmacist_certificate_image',
label: '药师资格证',
formItemClass: 'col-span-4',
},
{
component: 'Avatar',
fieldName: 'internet_drug_license_image',
label: '互联网药品经营许可证',
formItemClass: 'col-span-4',
},
{
component: 'UploadOssFile',
componentProps: {
maxCount: 1,
},
fieldName: 'contract_attachment',
label: '合同附件',
formItemClass: 'col-span-12',
},
{
component: 'RadioGroup',
componentProps: {

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
/**
* 平台配送仓库列表
* 新建/编辑仅基本信息;银行卡报备对齐诊所,走独立弹窗
* 新建/编辑含证照与建仓开号;银行卡报备对齐诊所,走独立弹窗
*/
import type { VxeGridListeners } from '#/adapter/vxe-table';