diff --git a/apps/web-antd/src/views/doctor/doctor-reception/index.vue b/apps/web-antd/src/views/doctor/doctor-reception/index.vue index a37c0f1d..4c05cec5 100644 --- a/apps/web-antd/src/views/doctor/doctor-reception/index.vue +++ b/apps/web-antd/src/views/doctor/doctor-reception/index.vue @@ -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(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) { + + + + +
+ +
+ +

处方已发送

+

已推送到患者微信 · 支付完成后门店按方发药

+
+ + +
+
+ +
+
+ 订单号 + {{ sentPrescriptionInfo?.order_no || '—' }} +
+
+ 患者 + {{ sentPrescriptionInfo?.patient_name || '—' }} +
+
+ 诊断 + {{ sentPrescriptionInfo.diagnosis }} +
+
+ 订单状态 + 待患者支付 +
+
+ 总金额 + 合计: ¥{{ sentPrescriptionInfo?.total_pay_price || '0.00' }} +
+
+ +
+ + + +
+
+
@@ -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; diff --git a/apps/web-antd/src/views/doctor/settings/api/index.ts b/apps/web-antd/src/views/doctor/settings/api/index.ts index 96cf0544..2a188359 100644 --- a/apps/web-antd/src/views/doctor/settings/api/index.ts +++ b/apps/web-antd/src/views/doctor/settings/api/index.ts @@ -327,6 +327,14 @@ export async function getDoctorMyDiseaseListApi() { export async function saveServicePriceApi(data: Record) { return requestClient.post(`${prefix}save-service-price`, data); } +/** + * 保存每日号源上限(yii_doctor_info.register_num,0 表示不放号) + * xk-api POST /doctor/save-register-num + * @param data { register_num: number } + */ +export async function saveRegisterNumApi(data: { register_num: number }) { + return requestClient.post(`${prefix}save-register-num`, data); +} /** * 创建医嘱 * @param content diff --git a/apps/web-antd/src/views/doctor/settings/index.vue b/apps/web-antd/src/views/doctor/settings/index.vue index c88b0f59..b653ea47 100644 --- a/apps/web-antd/src/views/doctor/settings/index.vue +++ b/apps/web-antd/src/views/doctor/settings/index.vue @@ -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 ? '开启' : '关闭' }}】 + +
+ 每日号源上限 + + + + +
+
diff --git a/apps/web-antd/src/views/system/department/api/index.ts b/apps/web-antd/src/views/system/department/api/index.ts new file mode 100644 index 00000000..0a243275 --- /dev/null +++ b/apps/web-antd/src/views/system/department/api/index.ts @@ -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(`${prefix}list`, { params: data }); +} + +/** + * 获取科室下拉选项(选择父级科室用,返回 [{id, name, pid, level}]) + * xk-api GET /department/option + */ +export async function getDepartmentOption() { + return requestClient.get(`${prefix}option`); +} + +/** + * 新增科室(level 由后端按 pid 推导,最多两级) + * xk-api POST /department/create + */ +export async function createDepartment(data: Record) { + return requestClient.post(`${prefix}create`, data); +} + +/** + * 编辑科室 + * xk-api POST /department/update + */ +export async function updateDepartment(data: Record) { + return requestClient.post(`${prefix}update`, data); +} + +/** + * 删除科室(物理删除,后端校验子科室与医生绑定) + * xk-api POST /department/delete + */ +export async function deleteDepartment(data: Record) { + return requestClient.post(`${prefix}delete`, data); +} diff --git a/apps/web-antd/src/views/system/department/components/modal.vue b/apps/web-antd/src/views/system/department/components/modal.vue new file mode 100644 index 00000000..c88e17b1 --- /dev/null +++ b/apps/web-antd/src/views/system/department/components/modal.vue @@ -0,0 +1,85 @@ + + diff --git a/apps/web-antd/src/views/system/department/config/form.ts b/apps/web-antd/src/views/system/department/config/form.ts new file mode 100644 index 00000000..a1e386a8 --- /dev/null +++ b/apps/web-antd/src/views/system/department/config/form.ts @@ -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', + }, + ], +}; diff --git a/apps/web-antd/src/views/system/department/config/search.ts b/apps/web-antd/src/views/system/department/config/search.ts new file mode 100644 index 00000000..ee752fe3 --- /dev/null +++ b/apps/web-antd/src/views/system/department/config/search.ts @@ -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, +}; diff --git a/apps/web-antd/src/views/system/department/config/table.ts b/apps/web-antd/src/views/system/department/config/table.ts new file mode 100644 index 00000000..7d37a876 --- /dev/null +++ b/apps/web-antd/src/views/system/department/config/table.ts @@ -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/pid),treeConfig.transform 自动组树; + * 树形结构下分页会截断父子关系,故关闭分页、一次性全量展示(科室量级很小) + */ +export const gridOptions: VxeGridProps = { + 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, +}; diff --git a/apps/web-antd/src/views/system/department/index.vue b/apps/web-antd/src/views/system/department/index.vue new file mode 100644 index 00000000..680cb4c4 --- /dev/null +++ b/apps/web-antd/src/views/system/department/index.vue @@ -0,0 +1,150 @@ + + +