feat: 优化了若干接口页面,可用性存疑、科室管理、处方发送成功页面

This commit is contained in:
李琦
2026-08-12 12:43:30 +08:00
parent f622743661
commit f15bf10553
9 changed files with 735 additions and 4 deletions

View File

@@ -11,6 +11,7 @@ import {
FileTextOutlined,
HistoryOutlined,
MedicineBoxOutlined,
CheckCircleFilled,
MenuFoldOutlined,
MenuUnfoldOutlined,
PlusOutlined,
@@ -790,6 +791,13 @@ const setVisible = (value, instruction = ''): void => {
}
};
/**
* 处方发送成功页数据
* 发送成功后右区切到成功面板tabType=3展示本单摘要
* 「代付收款」直接用这里的 order_id 拉代付码,医生不用再去处方记录抽屉里找
*/
const sentPrescriptionInfo = ref<any>(null);
/**
* 选择患者:已接诊直接进开方工作台;未接诊进轻量接诊操作区(不再进旧患者信息大页)
*/
@@ -814,6 +822,10 @@ const selectPatient = async (patient: Patient, isUpdateTabType = true) => {
if (patient.status !== 2 && tabType.value === 2) {
tabType.value = 1;
}
// 发送成功页数据不跨刷新:缓存里是成功页但数据已丢时,按接诊状态回落
if (tabType.value === 3 && !sentPrescriptionInfo.value) {
tabType.value = patient.status === 2 ? 2 : 1;
}
}
activePatient.value = patient.user_patient;
@@ -1207,6 +1219,14 @@ const sendPrescription = () => {
special_prescription_id: appliedSpecialPrescriptionId.value || 0,
}).then(async (res) => {
message.success('处方已发送');
// 先记录本次发送结果(下面会清空诊断/购物车,成功页要用的数据在这里先落住)
sentPrescriptionInfo.value = {
order_id: Number(res?.order_id || 0),
order_no: res?.order_no || '',
total_pay_price: res?.total_pay_price || '',
patient_name: activePatient.value?.name || '',
diagnosis: diagnosis.value,
};
// 有 VIP 时同步保存病历(诊断/医嘱已与处方同源)
if (canUseMedicalRecord.value) {
try {
@@ -1249,8 +1269,9 @@ const sendPrescription = () => {
// 清空当前数据
clearPrescriptionCart(false);
clearReceptionDraft(getRegisterId());
// 发送成功后留在开方工作台,刷新处方列表即可
tabType.value = 2;
// 转诊单要等中医诊所确认,留在开方工作台展示转方卡;
// 普通发送进入成功页tabType=3摘要 + 代付入口,不自动打开处方记录抽屉
tabType.value = needTransfer && transferPrescriptionId ? 2 : 3;
salespersonTransferPrescriptionId.value = 0;
checkSalespersonTransfer();
@@ -4317,6 +4338,69 @@ function onStoreSelectOpenChange(open: boolean) {
<!-- /rx-workspace -->
</Spin>
</Page>
<!-- 处方发送成功页摘要与代付入口都在面板上不自动打开处方记录抽屉 -->
<Page v-else-if="tabType === 3" class="prescription-panel">
<PatientBriefBar
:register="patientInfo"
:patient="activePatient"
:category="category"
:store-name="currentStoreName"
:is-online="false"
/>
<!-- 成功页布局对齐 C 端付款成功页shop/paied.vue
白色圆角卡居中大图标 + 胶囊小按钮明细行复用 C 端收银台record-pay.vue label/value 形态 -->
<div class="rx-sent-wrap">
<!-- 1成功卡大图标 + 标题 + 描边胶囊小按钮 -->
<div class="rx-sent-card rx-sent-hero">
<CheckCircleFilled class="rx-sent-icon" />
<h3 class="rx-sent-title">处方已发送</h3>
<p class="rx-sent-sub">已推送到患者微信 · 支付完成后门店按方发药</p>
<div class="rx-sent-pills">
<Button shape="round" size="small" @click="openPatientRxHistoryDrawer">处方记录</Button>
<Button shape="round" size="small" danger @click="endOfDiagnosis">结束诊断</Button>
</div>
</div>
<!-- 2订单明细label 加粗深色 / 值灰色合计行大号金额 -->
<div class="rx-sent-card rx-sent-detail">
<div class="rx-sent-row">
<span>订单号</span>
<b>{{ sentPrescriptionInfo?.order_no || '—' }}</b>
</div>
<div class="rx-sent-row">
<span>患者</span>
<b>{{ sentPrescriptionInfo?.patient_name || '—' }}</b>
</div>
<div v-if="sentPrescriptionInfo?.diagnosis" class="rx-sent-row">
<span>诊断</span>
<b>{{ sentPrescriptionInfo.diagnosis }}</b>
</div>
<div class="rx-sent-row">
<span>订单状态</span>
<Tag color="orange" class="rx-sent-tag">待患者支付</Tag>
</div>
<div class="rx-sent-total">
<span>总金额</span>
<span class="rx-sent-total-val">合计: <b>¥{{ sentPrescriptionInfo?.total_pay_price || '0.00' }}</b></span>
</div>
</div>
<!-- 底部操作胶囊主按钮代付收款+ 描边胶囊次按钮继续开方对齐 C 端底部通栏确认钮 -->
<div class="rx-sent-ops">
<!-- 代付入口患者不便操作手机时医生代其发起收款复用代付码弹窗 -->
<Button
v-if="sentPrescriptionInfo?.order_id"
type="primary"
shape="round"
size="large"
class="rx-sent-main"
@click="openProxyPayQrcode(Number(sentPrescriptionInfo.order_id))"
>
<QrcodeOutlined />
代付收款
</Button>
<Button shape="round" size="large" class="rx-sent-next" @click="tabType = 2">继续开方</Button>
</div>
</div>
</Page>
<div v-else-if="tabType === 0" class="prescription-panel">
<Empty/>
</div>
@@ -4604,6 +4688,10 @@ function onStoreSelectOpenChange(open: boolean) {
.prescription-panel {
padding: 1rem;
/* 轻量接诊面板(待接诊/空态)在锁高布局里自身滚动:
健康信息、特殊处方导入卡、转方卡叠加后内容会超一屏,父级 overflow:hidden 会把超出部分裁掉 */
min-height: 0;
overflow-y: auto;
}
/* 开方工作台:占满右侧主区,内部再拆「固定顶栏 / 可滚动内容」 */
.prescription-panel--rx {
@@ -4616,8 +4704,129 @@ function onStoreSelectOpenChange(open: boolean) {
/* 内边距交给内部 content避免双层 padding 挤掉高度 */
padding: 0 !important;
}
/* Page 根下的内容区(无 header 时即第一个/唯一子节点) */
.prescription-panel--rx :deep(> .h-full) {
/* 处方发送成功页:视觉对齐 C 端付款成功页(浅灰蓝底 + 白色圆角卡 + 胶囊按钮),
颜色走主题变量保证暗色可用,主色用 --primary与 C 端品牌蓝 #4175EE 同族) */
.rx-sent-wrap {
max-width: 480px;
margin: 20px auto 0;
padding: 14px 14px 22px;
display: flex;
flex-direction: column;
gap: 12px;
border-radius: 14px;
/* 对应 C 端页面底色 #F8FAFC用 muted 低透明度自动适配暗色 */
background: hsl(var(--muted) / 0.45);
}
.rx-sent-card {
border-radius: 10px;
background: hsl(var(--card, var(--background)));
border: 1px solid hsl(var(--border) / 0.55);
box-shadow: 0 1px 6px rgb(15 23 42 / 4%);
}
/* 成功卡C 端 paied.vue 头卡形态(大图标居中 + 加粗标题 + 描边胶囊小按钮) */
.rx-sent-hero {
padding: 28px 20px 20px;
text-align: center;
}
.rx-sent-icon {
font-size: 52px;
color: hsl(var(--primary));
animation: rx-sent-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes rx-sent-pop {
0% {
transform: scale(0.4);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.rx-sent-title {
margin: 14px 0 4px;
font-size: 17px;
font-weight: 700;
color: hsl(var(--foreground));
}
.rx-sent-sub {
margin: 0;
font-size: 12.5px;
color: hsl(var(--muted-foreground));
}
.rx-sent-pills {
margin-top: 18px;
display: flex;
justify-content: center;
gap: 10px;
}
/* 明细卡C 端 record-pay.vue 的 good_ 行形态label 加粗深色 / 值常规灰色) */
.rx-sent-detail {
padding: 6px 18px 14px;
}
.rx-sent-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-height: 42px;
}
.rx-sent-row > span {
flex-shrink: 0;
font-size: 13px;
font-weight: 600;
color: hsl(var(--foreground));
}
.rx-sent-row > b {
font-weight: 400;
font-size: 13px;
color: hsl(var(--muted-foreground));
text-align: right;
word-break: break-all;
}
.rx-sent-tag {
margin-inline-end: 0;
}
/* 合计行C 端「合计:」小字 + 大号金额 */
.rx-sent-total {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 12px;
margin-top: 4px;
border-top: 1px solid hsl(var(--border) / 0.55);
}
.rx-sent-total > span {
font-size: 13px;
font-weight: 600;
color: hsl(var(--foreground));
}
.rx-sent-total-val {
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.rx-sent-total-val b {
margin-left: 4px;
font-size: 19px;
font-weight: 600;
color: hsl(var(--foreground));
}
/* 底部操作:主按钮胶囊撑满(对齐 C 端底部通栏「确认」钮),次按钮等宽排布 */
.rx-sent-ops {
display: flex;
gap: 10px;
margin-top: 2px;
}
.rx-sent-main {
flex: 1.6;
}
.rx-sent-next {
flex: 1;
}
/* Page 根下的内容区:未开 autoContentHeight 时 class 是 flex-1开了才是 h-full
两种都要命中,否则「固定顶栏 + 内容滚动」的高度链路断裂,长处方整页无法滚动 */
.prescription-panel--rx :deep(> .h-full),
.prescription-panel--rx :deep(> .flex-1) {
flex: 1;
min-height: 0;
height: 100% !important;

View File

@@ -327,6 +327,14 @@ export async function getDoctorMyDiseaseListApi() {
export async function saveServicePriceApi(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}save-service-price`, data);
}
/**
* 保存每日号源上限yii_doctor_info.register_num0 表示不放号)
* xk-api POST /doctor/save-register-num
* @param data { register_num: number }
*/
export async function saveRegisterNumApi(data: { register_num: number }) {
return requestClient.post<any>(`${prefix}save-register-num`, data);
}
/**
* 创建医嘱
* @param content

View File

@@ -42,6 +42,7 @@ import {
getDoctorMyDiseaseListApi,
getDoctorMyDoctorOrderApi,
getDoctorMyInfoApi,
saveRegisterNumApi,
saveServicePriceApi,
} from '#/views/doctor/settings/api';
import type { CommonPrescriptionResponse } from '#/views/doctor/settings/api';
@@ -121,6 +122,21 @@ const saveServicePrice = () => {
// getMyInfo();
});
};
/**
* 保存每日号源上限
* register_num 在医生信息表yii_doctor_info与挂号开关/价格yii_doctor_service分表
* 所以单独走 save-register-num 接口0 表示不放号(患者端显示约满)
*/
const saveRegisterNum = () => {
saveRegisterNumApi({
register_num: data.value.register_num,
}).then(() => {
notification.success({
message: '保存成功',
duration: 2,
});
});
};
const getDoctorMyDoctorOrder = () => {
getDoctorMyDoctorOrderApi().then((res) => {
@@ -461,6 +477,22 @@ const handleViewSignature = () => {
{{ data.service.register_status === 1 ? '开启' : '关闭' }}
</div>
<!-- 每日号源上限挂号防超卖的额度来源0 表示不放号患者端显示约满 -->
<div class="mb-3 flex items-center gap-4">
每日号源上限
<InputNumber
v-model:value="data.register_num"
:min="0"
:max="999"
:precision="0"
style="min-width: 100px"
@keydown.enter="saveRegisterNum"
>
<template #addonAfter> / </template>
</InputNumber>
<Button type="primary" @click="saveRegisterNum">保存</Button>
</div>
<div class="flex flex-wrap gap-2"></div>
<div class="mt-4 space-y-1"></div>

View File

@@ -0,0 +1,44 @@
import { requestClient } from '#/api/request';
const prefix = 'department/';
/**
* 获取科室树形列表(平铺返回,前端 treeConfig transform 组树)
* xk-api GET /department/list
* @param data name 科室名称模糊搜索
*/
export async function getDepartmentList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
/**
* 获取科室下拉选项(选择父级科室用,返回 [{id, name, pid, level}]
* xk-api GET /department/option
*/
export async function getDepartmentOption() {
return requestClient.get<any>(`${prefix}option`);
}
/**
* 新增科室level 由后端按 pid 推导,最多两级)
* xk-api POST /department/create
*/
export async function createDepartment(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}create`, data);
}
/**
* 编辑科室
* xk-api POST /department/update
*/
export async function updateDepartment(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update`, data);
}
/**
* 删除科室(物理删除,后端校验子科室与医生绑定)
* xk-api POST /department/delete
*/
export async function deleteDepartment(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}delete`, data);
}

View File

@@ -0,0 +1,85 @@
<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 { createDepartment, getDepartmentOption, updateDepartment } from '../api';
import { modalFormProps } from '../config/form';
const isUpdate = ref(false);
const gridApi = ref();
const [Form, formApi] = useVbenForm(modalFormProps);
/**
* 拉取父级科室选项并注入 pid 下拉
* 只允许一级科室作为父级(后端强制两级封顶),编辑时排除自身
*/
const loadParentOptions = async (excludeId?: number) => {
const list = await getDepartmentOption();
const options = [
{ label: '作为一级科室(无父级)', value: 0 },
...(list || [])
.filter((item: any) => item.level === 1 && item.id !== excludeId)
.map((item: any) => ({ label: item.name, value: item.id })),
];
formApi.updateSchema([
{
fieldName: 'pid',
componentProps: {
options,
},
},
]);
};
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.lock();
const submitApi = isUpdate.value ? updateDepartment : createDepartment;
submitApi(values)
.then(() => {
message.success('保存成功');
gridApi.value?.query();
modalApi.close();
})
.finally(() => {
modalApi.unlock();
});
}
});
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (isOpen) {
const { values, update } = modalApi.getData<Record<string, any>>();
isUpdate.value = !!update;
// 先重置再回填,避免上一次打开的残留值
formApi.resetForm();
loadParentOptions(update ? values?.id : undefined);
if (values) {
formApi.setValues(values);
}
}
},
});
</script>
<template>
<Modal :title="`${isUpdate === true ? '编辑' : '新增'}科室`" class="w-[520px]">
<Form />
</Modal>
</template>

View File

@@ -0,0 +1,81 @@
import type { VbenFormProps } from '#/adapter/form';
/**
* 科室新增/编辑弹窗表单
* pid 的选项来自接口(仅一级科室可作为父级),在 modal.vue 的 onOpenChange 里
* 通过 formApi.updateSchema 动态注入,禁止在此写死
*/
export const modalFormProps: VbenFormProps = {
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
// 所有表单项
componentProps: {
class: 'w-full',
},
},
layout: 'horizontal',
showDefaultActions: false,
schema: [
{
component: 'VbenInput',
fieldName: 'id',
label: 'ID',
dependencies: {
show: false,
triggerFields: ['id'],
},
},
{
component: 'VbenInput',
componentProps: {
placeholder: '请输入科室名称',
maxlength: 50,
},
fieldName: 'name',
label: '科室名称',
rules: 'required',
},
{
component: 'Select',
componentProps: {
placeholder: '请选择父级科室(不选则为一级科室)',
options: [],
showSearch: true,
optionFilterProp: 'label',
},
defaultValue: 0,
fieldName: 'pid',
label: '父级科室',
rules: 'selectRequired',
},
{
component: 'VbenInput',
componentProps: {
placeholder: '拼音首拼(如:儿科=EK可不填',
maxlength: 50,
},
fieldName: 'pinyin_simple',
label: '拼音首拼',
},
{
component: 'RadioGroup',
componentProps: {
options: [
{
label: '否',
value: 0,
},
{
label: '是',
value: 1,
},
],
},
defaultValue: 0,
fieldName: 'feature',
label: '特色专科',
rules: 'required',
},
],
};

View File

@@ -0,0 +1,29 @@
import type { VbenFormProps } from '#/adapter/form';
/**
* 科室列表顶部搜索表单(按名称模糊搜索,后端会自动补齐父链保证树可渲染)
*/
export const formOptions: VbenFormProps = {
// 默认展开
collapsed: false,
schema: [
{
component: 'VbenInput',
componentProps: {
placeholder: '输入科室名称',
},
defaultValue: '',
fieldName: 'name',
label: '科室名称',
},
],
// 控制表单是否显示折叠按钮
showCollapseButton: false,
submitButtonOptions: {
content: '查询',
},
// 是否在字段值改变时提交表单
submitOnChange: true,
// 按下回车时是否提交表单
submitOnEnter: false,
};

View File

@@ -0,0 +1,93 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getDepartmentList } from '../api';
interface RowType {
id: number;
name: string;
pinyin_simple: string;
pid: number;
level: number;
feature: number;
doctor_count: number;
created_at: string;
updated_at: string;
}
/**
* 科室树形表格配置
* 后端返回平铺数据(含 id/pidtreeConfig.transform 自动组树;
* 树形结构下分页会截断父子关系,故关闭分页、一次性全量展示(科室量级很小)
*/
export const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: '',
},
columnConfig: {
useKey: true,
},
rowConfig: {
useKey: true,
},
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'name', align: 'left', title: '科室名称', treeNode: true, minWidth: 220 },
{ field: 'pinyin_simple', title: '拼音首拼', width: 120 },
{
field: 'level',
title: '层级',
width: 100,
slots: { default: 'level' },
},
{
field: 'feature',
title: '特色专科',
width: 110,
slots: { default: 'feature' },
},
{ field: 'doctor_count', title: '绑定医生数', width: 110 },
{ field: 'created_at', title: '创建时间', width: 180 },
{ field: 'updated_at', title: '更新时间', width: 180 },
{
type: 'html',
align: 'right',
title: '操作',
slots: { default: 'action' },
width: 240,
},
],
treeConfig: {
parentField: 'pid',
rowField: 'id',
transform: true,
expandAll: true,
},
keepSource: true,
pagerConfig: {
enabled: false,
},
proxyConfig: {
ajax: {
// 树形全量加载,不传分页参数
query: async (_params, formValues) => {
return await getDepartmentList({ ...formValues });
},
},
},
height: 'auto',
border: false,
toolbarConfig: {
// 是否显示搜索表单控制按钮
// @ts-ignore 正式环境时有完整的类型声明
search: true,
refresh: true, // 刷新
print: false, // 打印
export: false, // 导出
zoom: true, // 最大化最小化
slots: {
buttons: 'toolbar-actions',
},
},
showOverflow: false,
};

View File

@@ -0,0 +1,150 @@
<script lang="ts" setup>
// 科室字典管理yii_department老库表不迁库管理入口收敛到新后台
// 树形展示一级/二级科室,支持新增、编辑、添加子科室、删除(后端校验医生绑定)
import type { VxeGridListeners } from '#/adapter/vxe-table';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { message, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import { deleteDepartment } from './api';
import FormModalDemo from './components/modal.vue';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
defineOptions({ name: 'SystemDepartment' });
const hasTopTableDropDownActions = ref(false);
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
// eslint-disable-next-line no-use-before-define
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
checkboxAll() {
// eslint-disable-next-line no-use-before-define
const records = gridApi.grid.getCheckboxRecords();
hasTopTableDropDownActions.value = records.length > 0;
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents,
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
/**
* 打开新增/编辑弹窗
* 「添加子科室」复用新增弹窗:只预填 pid其余字段留空
*/
const showModal = (data: Record<string, any> = {}, isUpdate = false) => {
formModalApi.setData({
values: data,
update: isUpdate,
gridApi,
});
formModalApi.open();
};
/**
* 删除科室(单个或勾选批量)
* 物理删除,后端会拦截「有子科室 / 有医生绑定」的情况
*/
const deleteApi = (row: any) => {
let ids = [];
if (row) {
ids.push(row.id);
} else {
ids = gridApi.grid.getCheckboxRecords().map((item) => item.id);
}
deleteDepartment({ ids }).then(() => {
message.success('删除成功!');
gridApi.query();
});
};
</script>
<template>
<Page auto-content-height title="科室管理">
<FormModal />
<Grid>
<template #toolbar-actions>
<TableAction
:actions="[
{
label: '新增科室',
type: 'primary',
icon: 'ant-design:plus-outlined',
onClick: showModal.bind(null, {}, false),
},
]"
:drop-down-actions="[
{
label: '批量删除',
icon: 'ant-design:delete-outlined',
ifShow: hasTopTableDropDownActions,
popConfirm: {
title: '确定删除选中的科室吗',
confirm: deleteApi.bind(null, false),
},
},
]"
/>
</template>
<template #level="{ row }">
<Tag :color="row.level === 1 ? 'blue' : 'cyan'">
{{ row.level === 1 ? '一级科室' : '二级科室' }}
</Tag>
</template>
<template #feature="{ row }">
<Tag :color="row.feature === 1 ? 'gold' : 'default'">
{{ row.feature === 1 ? '特色专科' : '普通' }}
</Tag>
</template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '编辑',
type: 'link',
icon: 'uil:edit',
size: 'small',
onClick: showModal.bind(null, row, true),
},
{
label: '添加子科室',
type: 'link',
icon: 'ant-design:apartment-outlined',
size: 'small',
ifShow: row.level === 1,
onClick: showModal.bind(null, { pid: row.id }, false),
},
]"
:drop-down-actions="[
{
label: '删除',
type: 'link',
icon: 'ant-design:delete-outlined',
size: 'small',
popConfirm: {
title: '确定删除该科室吗',
confirm: deleteApi.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>