From 58c224a081f5ec0a50972f4164d795f418de015c Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 25 May 2026 18:32:17 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../consumer/prescription/order_list.vue | 74 ++++++++++- .../consumer/prescription/order_list_h5.vue | 74 ++++++++++- .../lists/tcm/PrescriptionOrderLists.php | 109 +++++++++++++++- .../logic/tcm/PrescriptionOrderLogic.php | 117 ++++++++++++++++++ server/config/project.php | 5 + 5 files changed, 376 insertions(+), 3 deletions(-) diff --git a/admin/src/views/consumer/prescription/order_list.vue b/admin/src/views/consumer/prescription/order_list.vue index 4dad5244..572b18ce 100644 --- a/admin/src/views/consumer/prescription/order_list.vue +++ b/admin/src/views/consumer/prescription/order_list.vue @@ -216,6 +216,22 @@ @keyup.enter="resetPage" /> + + + + + { + const u = userStore.userInfo + if (!u || Number(u.root) === 1) return false + const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : [] + if (!ids.includes(ORDER_PLACER_ROLE_ID)) return false + return !ORDER_PLACER_EXEMPT_ROLE_IDS.some((rid) => ids.includes(rid)) +}) + +/** 超管 / 经理 / 管理员:可按任意下单人筛选 */ +const canSearchAnyAuditAdmin = computed(() => { + const u = userStore.userInfo + if (!u) return false + if (Number(u.root) === 1) return true + const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : [] + return ORDER_PLACER_EXEMPT_ROLE_IDS.some((rid) => ids.includes(rid)) +}) + +/** 是否展示「下单人」筛选(下单角色 + 经理/管理员/超管) */ +const showAuditAdminFilter = computed( + () => isOrderPlacerAuditFilterRestricted.value || canSearchAnyAuditAdmin.value +) + +/** 下单人下拉(lists extend.audit_admin_options,下单角色用户) */ +const auditAdminOptions = ref>([]) + /** * 是否展示「处方审核」筛选:纯医助(含角色 2 且不具备处方审核角色)仅能用「支付单审核」筛选 */ @@ -2928,7 +2974,9 @@ const queryParams = reactive({ /** 服务渠道:'' 不限;'0' 未指派(库内 '' 或 '0') */ service_channel: '' as '' | '0', /** 列表排除履约已取消(4):重点看板「待处方审核」等用 */ - exclude_fulfillment_cancelled: 0 as number + exclude_fulfillment_cancelled: 0 as number, + /** 下单人(关联操作日志 audit_rx_* / audit_pay_*) */ + audit_admin_id: '' as number | '' }) const rxAuditTabs = [ @@ -3041,6 +3089,11 @@ function normalizePrescriptionOrderListQuery(params: Record): R } if (!p.order_no) delete p.order_no if (!String(p.patient_keyword || '').trim()) delete p.patient_keyword + if (p.audit_admin_id === '' || p.audit_admin_id === undefined || p.audit_admin_id === null) { + delete p.audit_admin_id + } else { + p.audit_admin_id = Number(p.audit_admin_id) + } if (!String(p.express_keyword || '').trim()) delete p.express_keyword if (!p.express_company || p.express_company === '') delete p.express_company if (p.service_channel === '' || p.service_channel === undefined || p.service_channel === null) { @@ -3076,6 +3129,24 @@ const { pager, getLists, resetPage, resetParams } = usePaging({ params: queryParams }) +watch( + () => (pager.extend as Record | undefined)?.audit_admin_options, + (raw) => { + if (!Array.isArray(raw)) return + auditAdminOptions.value = raw + .map((r: unknown) => { + if (r == null || typeof r !== 'object') return null + const row = r as Record + const id = Number(row.id) + const name = String(row.name ?? '').trim() + if (!Number.isFinite(id) || id <= 0 || name === '') return null + return { id, name } + }) + .filter((x): x is { id: number; name: string } => x != null) + }, + { immediate: true } +) + /** 业绩看板等入口:URL 携带创建区间、诊单医助部门 / 医助 id */ function applyRouteQueryToPrescriptionOrderList() { const q = route.query @@ -3267,6 +3338,7 @@ function handleReset() { queryParams.payment_slip_audit_status = '' queryParams.supply_mode = '' queryParams.service_channel = '' + queryParams.audit_admin_id = '' queryParams.exclude_fulfillment_cancelled = 0 resetParams() } diff --git a/admin/src/views/consumer/prescription/order_list_h5.vue b/admin/src/views/consumer/prescription/order_list_h5.vue index b8ccbc0e..403e43f9 100644 --- a/admin/src/views/consumer/prescription/order_list_h5.vue +++ b/admin/src/views/consumer/prescription/order_list_h5.vue @@ -234,6 +234,22 @@ @keyup.enter="resetPage" /> + + + + + { + const u = userStore.userInfo + if (!u || Number(u.root) === 1) return false + const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : [] + if (!ids.includes(ORDER_PLACER_ROLE_ID)) return false + return !ORDER_PLACER_EXEMPT_ROLE_IDS.some((rid) => ids.includes(rid)) +}) + +/** 超管 / 经理 / 管理员:可按任意下单人筛选 */ +const canSearchAnyAuditAdmin = computed(() => { + const u = userStore.userInfo + if (!u) return false + if (Number(u.root) === 1) return true + const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : [] + return ORDER_PLACER_EXEMPT_ROLE_IDS.some((rid) => ids.includes(rid)) +}) + +/** 是否展示「下单人」筛选(下单角色 + 经理/管理员/超管) */ +const showAuditAdminFilter = computed( + () => isOrderPlacerAuditFilterRestricted.value || canSearchAnyAuditAdmin.value +) + +/** 下单人下拉(lists extend.audit_admin_options,下单角色用户) */ +const auditAdminOptions = ref>([]) + /** * 是否展示「处方审核」筛选:纯医助(含角色 2 且不具备处方审核角色)仅能用「支付单审核」筛选 */ @@ -2673,7 +2719,9 @@ const queryParams = reactive({ prescription_audit_status: '' as number | '', payment_slip_audit_status: '' as number | '', /** 供货方式:甘草(已传甘草药方单号)/ 自营(无甘草单号) */ - supply_mode: '' as '' | 'gancao' | 'self' + supply_mode: '' as '' | 'gancao' | 'self', + /** 下单人(关联操作日志 audit_rx_* / audit_pay_*) */ + audit_admin_id: '' as number | '' }) const rxAuditTabs = [ @@ -2767,6 +2815,11 @@ async function fetchLists(params: Record) { } if (!p.order_no) delete p.order_no if (!String(p.patient_keyword || '').trim()) delete p.patient_keyword + if (p.audit_admin_id === '' || p.audit_admin_id === undefined || p.audit_admin_id === null) { + delete p.audit_admin_id + } else { + p.audit_admin_id = Number(p.audit_admin_id) + } if (!String(p.express_keyword || '').trim()) delete p.express_keyword if (!p.express_company || p.express_company === '') delete p.express_company if (!String(p.start_time || '').trim() || !String(p.end_time || '').trim()) { @@ -2789,6 +2842,24 @@ const { pager, getLists, resetPage, resetParams } = usePaging({ params: queryParams }) +watch( + () => (pager.extend as Record | undefined)?.audit_admin_options, + (raw) => { + if (!Array.isArray(raw)) return + auditAdminOptions.value = raw + .map((r: unknown) => { + if (r == null || typeof r !== 'object') return null + const row = r as Record + const id = Number(row.id) + const name = String(row.name ?? '').trim() + if (!Number.isFinite(id) || id <= 0 || name === '') return null + return { id, name } + }) + .filter((x): x is { id: number; name: string } => x != null) + }, + { immediate: true } +) + /** 选医助部门后,医助下拉仅显示该部门及子部门成员(与后台含子级一致) */ const filteredAssistantOptions = computed(() => { const deptRaw = queryParams.assistant_dept_id @@ -2950,6 +3021,7 @@ function handleReset() { queryParams.prescription_audit_status = '' queryParams.payment_slip_audit_status = '' queryParams.supply_mode = '' + queryParams.audit_admin_id = '' resetParams() } diff --git a/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php b/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php index 23b6a983..a4ba7490 100755 --- a/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php +++ b/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php @@ -14,10 +14,12 @@ use app\common\lists\ListsExtendInterface; use app\common\lists\ListsSearchInterface; use app\common\lists\Traits\HasDataScopeFilter; use app\common\model\Order; -use app\common\model\tcm\Diagnosis; +use app\common\model\auth\Admin; use app\common\model\auth\AdminDept; +use app\common\model\tcm\Diagnosis; use app\common\model\tcm\Prescription; use app\common\model\tcm\PrescriptionOrder; +use app\common\model\tcm\PrescriptionOrderLog; use app\common\model\tcm\PrescriptionOrderPayOrder; use app\common\model\ExpressTracking; use app\common\service\gancao\GancaoScmRecipelService; @@ -106,6 +108,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn $this->applyExpressKeywordFilter($query); $this->applyServiceChannelFilter($query); $this->applySupplyModeFilter($query); + $this->applyAuditAdminFilter($query); if (!$this->shouldBypassListVisibilityForDiagnosisEdit()) { // 业绩看板按部门点「合计业绩」/复诊下钻:部门或数据域内医助筛选已收口,勿再叠「仅本人订单」 if (!$this->shouldSkipCreatorOnlyForYejiDrawer()) { @@ -121,6 +124,105 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn YejiStatsLogic::applyYejiDrawerChannelFilterToPrescriptionOrderQuery($query, $this->params); } + /** + * 按下单人筛选:关联操作日志中该用户执行的处方/支付单审核记录 + */ + private function applyAuditAdminFilter($query): void + { + $auditAdminId = (int) ($this->params['audit_admin_id'] ?? 0); + $keyword = trim((string) ($this->params['audit_admin_keyword'] ?? '')); + + if ($auditAdminId <= 0 && $keyword === '') { + return; + } + + $restricted = PrescriptionOrderLogic::isOrderPlacerAuditFilterRestricted($this->adminInfo); + if ($restricted) { + $selfId = (int) $this->adminId; + if ($auditAdminId > 0 && $auditAdminId !== $selfId) { + $query->whereRaw('0 = 1'); + + return; + } + if ($keyword !== '') { + $selfName = trim((string) ($this->adminInfo['name'] ?? '')); + if ($selfName === '' + || (mb_stripos($selfName, $keyword) === false && mb_stripos($keyword, $selfName) === false)) { + $query->whereRaw('0 = 1'); + + return; + } + } + $auditAdminId = $selfId; + } elseif ($auditAdminId <= 0 && $keyword !== '') { + $roleIds = Config::get('project.prescription_order_placer_role_ids', [6]); + $roleIds = array_values(array_unique(array_filter(array_map( + 'intval', + is_array($roleIds) ? $roleIds : [] + ), static fn(int $id): bool => $id > 0))); + if ($roleIds === []) { + $roleIds = [6]; + } + $adminIds = Admin::alias('a') + ->join('admin_role ar', 'a.id = ar.admin_id') + ->whereLike('a.name', '%' . $keyword . '%') + ->whereIn('ar.role_id', $roleIds) + ->where('a.disable', 0) + ->whereNull('a.delete_time') + ->column('a.id'); + $adminIds = array_values(array_filter(array_map('intval', $adminIds), static function (int $id): bool { + return $id > 0; + })); + if ($adminIds === []) { + $query->whereRaw('0 = 1'); + + return; + } + $this->applyAuditedByAdminIdsFilter($query, $adminIds); + + return; + } + + if ($auditAdminId <= 0) { + return; + } + if (!PrescriptionOrderLogic::adminHasPlacerRole($auditAdminId)) { + $query->whereRaw('0 = 1'); + + return; + } + + $this->applyAuditedByAdminIdsFilter($query, [$auditAdminId]); + } + + /** + * @param int[] $adminIds + */ + private function applyAuditedByAdminIdsFilter($query, array $adminIds): void + { + $adminIds = array_values(array_unique(array_filter(array_map('intval', $adminIds), static function (int $id): bool { + return $id > 0; + }))); + if ($adminIds === []) { + $query->whereRaw('0 = 1'); + + return; + } + + $logTbl = (new PrescriptionOrderLog())->getTable(); + $poTbl = (new PrescriptionOrder())->getTable(); + $actions = PrescriptionOrderLogic::prescriptionOrderAuditLogActions(); + $actionIn = implode(',', array_map(static function (string $a): string { + return "'" . addslashes($a) . "'"; + }, $actions)); + $idIn = implode(',', $adminIds); + + $query->whereExists( + "SELECT 1 FROM `{$logTbl}` l WHERE l.`prescription_order_id` = `{$poTbl}`.`id`" + . " AND l.`admin_id` IN ({$idIn}) AND l.`action` IN ({$actionIn})" + ); + } + /** * 业绩看板:仅列出二中心复诊口径下的业务订单(须同时传 assistant_id、start_time、end_time;revisit_slot=0 为全部复诊分项合计)。 * admin 维度按订单创建人 o.creator_id(与部门表复诊列、排行榜复诊列同口径)。 @@ -388,6 +490,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn $query = PrescriptionOrder::where($this->searchWhereWithoutCreateTimeRange())->whereNull('delete_time'); $this->applyDoctorAssistantFilters($query); $this->applyPatientKeywordFilter($query); + $this->applyAuditAdminFilter($query); if ($this->shouldSkipCreatorOnlyForYejiDrawer()) { $this->applyDataScopeForPrescriptionOrder($query); } elseif (PrescriptionOrderLogic::canViewOrderListStatsAllScope($this->adminInfo)) { @@ -634,6 +737,10 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn 'focus_pending_pay_count' => $focus['pending_pay'], 'focus_pending_ship_count' => $focus['pending_ship'], 'focus_risk_count' => $focus['risk'], + /** 1=「下单」角色审核人筛选仅本人 */ + 'list_placer_audit_filter_restricted' => PrescriptionOrderLogic::isOrderPlacerAuditFilterRestricted($this->adminInfo) ? 1 : 0, + /** 下单人下拉(下单角色用户) */ + 'audit_admin_options' => PrescriptionOrderLogic::listAuditAdminOptions($this->adminId, $this->adminInfo), ]; $yejiConsult = $this->computeYejiDrawerConsultCountIfRequested(); if ($yejiConsult !== null) { diff --git a/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php b/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php index 2ff76319..f77663b4 100755 --- a/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php +++ b/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php @@ -115,6 +115,123 @@ class PrescriptionOrderLogic return self::roleIntersect($adminInfo, is_array($roles) ? $roles : []); } + /** + * 「下单」角色:列表「审核人」筛选仅允许查本人审核过的订单(经理/管理员等豁免) + */ + public static function isOrderPlacerAuditFilterRestricted(array $adminInfo): bool + { + if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) { + return false; + } + $placer = Config::get('project.prescription_order_placer_role_ids', [6]); + if (!self::roleIntersect($adminInfo, is_array($placer) ? $placer : [])) { + return false; + } + $exempt = Config::get('project.prescription_order_placer_exempt_role_ids', [3, 8]); + + return !self::roleIntersect($adminInfo, is_array($exempt) ? $exempt : []); + } + + /** @return string[] */ + public static function prescriptionOrderAuditLogActions(): array + { + return ['audit_rx_approve', 'audit_rx_reject', 'audit_pay_approve', 'audit_pay_reject']; + } + + /** 是否展示「下单人」筛选(下单 / 经理 / 管理员 / 超管) */ + public static function shouldShowAuditAdminFilter(array $adminInfo): bool + { + if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) { + return true; + } + if (self::isOrderPlacerAuditFilterRestricted($adminInfo)) { + return true; + } + $exempt = Config::get('project.prescription_order_placer_exempt_role_ids', [3, 8]); + + return self::roleIntersect($adminInfo, is_array($exempt) ? $exempt : []); + } + + /** + * 下单人下拉:直接取「下单」角色下的后台用户(project.prescription_order_placer_role_ids) + * + * @return array + */ + public static function listAuditAdminOptions(int $adminId, array $adminInfo): array + { + if ($adminId <= 0 || !self::shouldShowAuditAdminFilter($adminInfo)) { + return []; + } + + $roleIds = Config::get('project.prescription_order_placer_role_ids', [6]); + $roleIds = array_values(array_unique(array_filter(array_map( + 'intval', + is_array($roleIds) ? $roleIds : [] + ), static fn(int $id): bool => $id > 0))); + if ($roleIds === []) { + $roleIds = [6]; + } + + if (self::isOrderPlacerAuditFilterRestricted($adminInfo)) { + $name = trim((string) ($adminInfo['name'] ?? '')); + + return [ + [ + 'id' => $adminId, + 'name' => $name !== '' ? $name : ('管理员#' . $adminId), + ], + ]; + } + + $rows = Db::name('admin') + ->alias('a') + ->join('admin_role ar', 'a.id = ar.admin_id') + ->whereIn('ar.role_id', $roleIds) + ->where('a.disable', 0) + ->whereNull('a.delete_time') + ->field(['a.id', 'a.name']) + ->distinct(true) + ->order('a.name', 'asc') + ->select() + ->toArray(); + + $out = []; + foreach ($rows as $r) { + $id = (int) ($r['id'] ?? 0); + if ($id <= 0) { + continue; + } + $name = trim((string) ($r['name'] ?? '')); + if ($name === '') { + $name = '管理员#' . $id; + } + $out[] = ['id' => $id, 'name' => $name]; + } + + return $out; + } + + /** 指定后台账号是否拥有「下单」角色 */ + public static function adminHasPlacerRole(int $adminId): bool + { + if ($adminId <= 0) { + return false; + } + $roleIds = Config::get('project.prescription_order_placer_role_ids', [6]); + $roleIds = array_values(array_unique(array_filter(array_map( + 'intval', + is_array($roleIds) ? $roleIds : [] + ), static fn(int $id): bool => $id > 0))); + if ($roleIds === []) { + $roleIds = [6]; + } + + return Db::name('admin_role') + ->where('admin_id', $adminId) + ->whereIn('role_id', $roleIds) + ->count() > 0; + } + /** * 菜单权限:非全量角色可额外查看「关联处方开方人为本账号」的业务订单(医助代建单) * diff --git a/server/config/project.php b/server/config/project.php index d082a4db..88b8505a 100755 --- a/server/config/project.php +++ b/server/config/project.php @@ -131,6 +131,11 @@ return [ // 业务订单列表金额统计:医助角色 ID;非支付审核白名单的医助仅统计其诊单 assistant_id=本人 的业绩 'prescription_order_stats_assistant_role_id' => 2, + // 业务订单列表:「下单」角色(id=6)可按审核人姓名筛选本人审核过的订单 + 'prescription_order_placer_role_ids' => [6], + // 同时拥有以下角色时可按任意审核人筛选(如经理、管理员) + 'prescription_order_placer_exempt_role_ids' => [3, 8], + /* * 数据隔离(按部门):全局可用,角色上的 data_scope 控制范围 * 1=全部 2=本部门及下级 3=仅本部门 4=仅本人 From 5b233fb0ab42c8164e8457b80c55fda5a9738398 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 11:23:43 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../consumer/prescription/order_list.vue | 144 ++++++++++++++---- .../adminapi/logic/stats/YejiStatsLogic.php | 76 ++++++++- 2 files changed, 186 insertions(+), 34 deletions(-) diff --git a/admin/src/views/consumer/prescription/order_list.vue b/admin/src/views/consumer/prescription/order_list.vue index 572b18ce..d8995271 100644 --- a/admin/src/views/consumer/prescription/order_list.vue +++ b/admin/src/views/consumer/prescription/order_list.vue @@ -931,31 +931,75 @@ - - +
+
+ 主方: + + +
+
+ 辅方: + + +
+
-
- 每天次数: - {{ detailPrescription.times_per_day ? detailPrescription.times_per_day + ' 次' : '—' }} -
-
- 处方开立: - {{ - detailPrescription.usage_days != null && detailPrescription.usage_days !== '' - ? detailPrescription.usage_days + ' 天' - : '—' - }} -
+ +
订单设置: {{ @@ -2426,9 +2470,9 @@
-

主方服法:{{ rxUsageText }}

+

主服法:{{ rxUsageText }}

服法:{{ rxUsageText }}

-

辅方服法:{{ rxAuxUsageText }}

+

辅服法:{{ rxAuxUsageText }}

医嘱:{{ rxAdviceText }}

备注:{{ rxRemarkText }}

@@ -3856,6 +3900,20 @@ const detailLinkedAppointmentChannelText = computed(() => { const detailRxHerbs = computed(() => normalizeSlipHerbs(detailPrescription.value?.herbs)) +/** 详情:是否含辅方药材(用于决定是否展示辅方用量 / 用法) */ +const detailHasAuxHerbs = computed(() => { + const herbs = detailPrescription.value?.herbs + if (!Array.isArray(herbs)) return false + return herbs.some((h: any) => normalizeSlipFormulaType(h?.formula_type) === '辅方') +}) + +/** 详情:辅方用法(aux_usage JSON 经规范化后的对象,与处方类型一致) */ +const detailAuxUsage = computed(() => { + const rx = detailPrescription.value as any + if (!rx) return null + return normalizeSlipAuxUsageForm(rx.aux_usage, rx.prescription_type || '浓缩水丸') +}) + /** false=无权限;true/缺省兼容旧接口(旧版未下发该字段时仍展示药材) */ const detailHerbsVisible = computed(() => detailData.value?.prescription_detail_herbs_visible !== false) @@ -5571,6 +5629,22 @@ const slipPillGrams = computed(() => { return days * times * doseG * bags }) +/** 辅方出丸克数:仅含辅方药材且为浓缩水丸时计算;公式同主方,用 aux_usage 字段 */ +const slipAuxPillGrams = computed(() => { + const d = prescriptionViewData.value as any + if (!d || (d.prescription_type || '浓缩水丸') !== '浓缩水丸') return null + if (!slipAuxHerbs.value.length) return null + const aux = normalizeSlipAuxUsageForm(d.aux_usage, d.prescription_type || '浓缩水丸') + /** 辅方疗程优先用业务订单 medication_days(与主方共享一次配药周期),缺省回退辅方 usage_days */ + const days = Number(d.medication_days ?? aux.usage_days) + const times = Number(aux.times_per_day) + const doseG = Number(aux.dosage_amount) + const bags = Number(aux.dosage_bag_count) > 0 ? Number(aux.dosage_bag_count) : 1 + if (!Number.isFinite(days) || !Number.isFinite(times) || !Number.isFinite(doseG)) return null + if (days <= 0 || times <= 0 || doseG <= 0) return null + return days * times * doseG * bags +}) + /* ============================================================ * A4 处方笺(药房联)相关计算 —— 与 consumer/prescription/index.vue 保持一致 * ============================================================ */ @@ -5684,10 +5758,14 @@ const rxAuxUsageText = computed(() => { ) }) -/** 出丸:优先用 slipPillGrams(每袋用量×袋数×每天次数×服用天数),否则按药材总量×剂数 */ +/** 出丸:优先用 slipPillGrams / slipAuxPillGrams(含辅方时相加),否则按药材总量×剂数 */ const rxOutPelletGrams = computed(() => { - const pill = slipPillGrams.value - if (typeof pill === 'number' && isFinite(pill) && pill > 0) return formatNum(pill) + const main = slipPillGrams.value + const aux = slipAuxPillGrams.value + const mainNum = typeof main === 'number' && isFinite(main) && main > 0 ? main : 0 + const auxNum = typeof aux === 'number' && isFinite(aux) && aux > 0 ? aux : 0 + const sum = mainNum + auxNum + if (sum > 0) return formatNum(sum) const v = prescriptionViewData.value as any if (!v) return '' const explicit = v.out_pellet || v.total_weight @@ -5723,7 +5801,17 @@ const rxPharmacyRemarkText = computed(() => { const rxOutPelletText = computed(() => { const out = rxOutPelletGrams.value - return out ? `${out}克` : '' + if (!out) return '' + /** 含辅方时附带「主方 X克 + 辅方 Y克」明细,便于药房核对 */ + const main = slipPillGrams.value + const aux = slipAuxPillGrams.value + if ( + typeof main === 'number' && isFinite(main) && main > 0 && + typeof aux === 'number' && isFinite(aux) && aux > 0 + ) { + return `${out}克(主方 ${formatNum(main)}克 + 辅方 ${formatNum(aux)}克)` + } + return `${out}克` }) function formatNum(n: number): string { diff --git a/server/app/adminapi/logic/stats/YejiStatsLogic.php b/server/app/adminapi/logic/stats/YejiStatsLogic.php index 3973c6fd..df9f3672 100755 --- a/server/app/adminapi/logic/stats/YejiStatsLogic.php +++ b/server/app/adminapi/logic/stats/YejiStatsLogic.php @@ -1437,6 +1437,16 @@ class YejiStatsLogic * @param int[] $deptIdsForAdmin * @param array $tableRowSet * @param array> $deptById + * + * 多链路 admin 的归属选择:按**原始 admin_dept 链路的深度**择优(更深 = 更具体的岗位归属), + * 而非按解析出的表格行深度。否则同层中心兄弟节点在父视图(如 [郑州一中心, 郑州二中心])会 + * 以 id 大小决胜,而子视图(钻取到某中心后行集变为其子组)则只剩唯一候选——同一条挂号在两个 + * 视图被路由到不同行,导致 admin「已完成挂号」父行 < 子行合计。 + * + * 向下兜底(resolveFirstTableRowUnderDept)按「链路深度 ≥ 最浅表格行深度 - 1」收窄: + * 仅当链路本身就是用户筛选根(与表格行同层或刚刚高一层)时才允许向下归到首个子表格行; + * 否则像「只挂在甄养堂(root) / 一中心」等过宽链路,会在不同视图被任意攀附到第一个子节点, + * 同一挂号父视图归到第一个中心、子视图归到该中心第一个子组 → 父子合计错位。 */ private static function mapAdminDeptLinksToTableRow(array $deptIdsForAdmin, array $tableRowDeptIds, array $tableRowSet, array $deptById): int { @@ -1449,7 +1459,7 @@ class YejiStatsLogic } $hit = self::resolveDeptToNearestTableRowUpward($d, $tableRowSet, $deptById); if ($hit > 0) { - $depth = self::deptDepthFromRoot($hit, $deptById); + $depth = self::deptDepthFromRoot($d, $deptById); if ($depth > $bestDepth || ($depth === $bestDepth && ($best === 0 || $hit < $best))) { $best = $hit; $bestDepth = $depth; @@ -1459,11 +1469,22 @@ class YejiStatsLogic if ($best > 0) { return $best; } + $minTableRowDepth = PHP_INT_MAX; + foreach ($tableRowDeptIds as $tid) { + $td = self::deptDepthFromRoot((int) $tid, $deptById); + if ($td > 0 && $td < $minTableRowDepth) { + $minTableRowDepth = $td; + } + } + $linkDepthFloor = $minTableRowDepth === PHP_INT_MAX ? PHP_INT_MAX : ($minTableRowDepth - 1); foreach ($deptIdsForAdmin as $d) { $d = (int) $d; if ($d <= 0) { continue; } + if (self::deptDepthFromRoot($d, $deptById) < $linkDepthFloor) { + continue; + } $hit = self::resolveFirstTableRowUnderDept($d, $tableRowDeptIds, $deptById); if ($hit > 0) { return $hit; @@ -1618,10 +1639,31 @@ class YejiStatsLogic $aids[] = $d; } } + $aids = array_values(array_unique($aids)); $adminToTable = ($tableRowDeptIds !== [] && $deptById !== []) ? self::batchMapPerformanceAdminsToTableDept($aids, $tableRowDeptIds, $deptById) : []; + /** + * 视图无关的「该 admin 是否有 admin_dept 归属」。 + * 用途:仅当 admin 完全无部门归属时才允许由"医助"回退到"接诊医生"。 + * 否则父视图能解析到 admin 的中心行 → 计入 assistant;子视图(钻取另一中心) + * adminToTable / adminToPrimary 双双 0 时会回退到 doctor,造成同一挂号在父视图归 assistant、 + * 在子视图归 doctor,父行 < 子行合计。 + * + * @var array $adminHasAnyDept + */ + $adminHasAnyDept = []; + if ($aids !== []) { + $linkedAids = Db::name('admin_dept')->whereIn('admin_id', $aids)->column('admin_id'); + foreach ($linkedAids as $lid) { + $lid = (int) $lid; + if ($lid > 0) { + $adminHasAnyDept[$lid] = true; + } + } + } + $byDept = []; foreach ($rows as $r) { $effId = (int) ($r['eff_a'] ?? 0); @@ -1657,6 +1699,14 @@ class YejiStatsLogic $primary = $p; break; } + /** + * 视图无关地有部门归属,仅是当前视图行集 / primaryDeptIds 不含 → + * 视为该 admin 的归属(视图外不可见),不再回退到下一候选(医生), + * 以避免父子视图归属分裂。narrowTotalsToTable 视图下 unmapped 会被自动隐藏。 + */ + if (isset($adminHasAnyDept[$aid])) { + break; + } } if ($primary > 0) { @@ -4058,7 +4108,14 @@ class YejiStatsLogic if ($candidates === []) { return 0; } - /** @var int[] $matching */ + /** + * 每个命中的表格行追加 ['rid'=>表格行 id, 'link_depth'=>原始 admin_dept 链路深度]。 + * 用链路深度(而非表格行深度)择优,保证父视图(同层中心兄弟节点行集)与子视图(钻取单中心后行集为其子组) + * 对同一 admin 的归属选择一致——否则同 admin 在父行被路由到「错」的中心,在子行被路由到正确中心子组, + * 表现为父行计数 < 子行合计。 + * + * @var array $matching + */ $matching = []; foreach ($tableRowDeptIds as $rid) { $rid = (int) $rid; @@ -4069,6 +4126,7 @@ class YejiStatsLogic if ($flip === []) { continue; } + $bestLinkDepth = -1; foreach ($candidates as $aid => $_) { $aid = (int) $aid; if ($aid <= 0) { @@ -4077,11 +4135,16 @@ class YejiStatsLogic foreach ($adminDeptIdList[$aid] ?? [] as $d) { $d = (int) $d; if ($d > 0 && isset($flip[$d])) { - $matching[] = $rid; - break 2; + $linkDepth = self::deptDepthFromRoot($d, $deptById); + if ($linkDepth > $bestLinkDepth) { + $bestLinkDepth = $linkDepth; + } } } } + if ($bestLinkDepth >= 0) { + $matching[] = ['rid' => $rid, 'link_depth' => $bestLinkDepth]; + } } if ($matching === []) { foreach ($candidates as $aid => $_) { @@ -4099,8 +4162,9 @@ class YejiStatsLogic } $best = 0; $bestDepth = -1; - foreach ($matching as $rid) { - $depth = self::deptDepthFromRoot($rid, $deptById); + foreach ($matching as $m) { + $rid = $m['rid']; + $depth = $m['link_depth']; if ($depth > $bestDepth || ($depth === $bestDepth && ($best === 0 || $rid < $best))) { $best = $rid; $bestDepth = $depth; From 0dd9cdcba9ad29a4e888eb549f0cb4a9c8344d5f Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 18:05:48 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TUICallKit-Vue3/main.js | 2 +- TUICallKit-Vue3/manifest.json | 8 +- TUICallKit-Vue3/pages.json | 15 + TUICallKit-Vue3/pages/Card/Card.vue | 70 + TUICallKit-Vue3/tongji/pages/index.vue | 4220 +++++++++++++++++ .../tcm/diagnosis/components/DailyMatrix.vue | 87 +- server/app/api/controller/TcmController.php | 300 +- .../1.9.20260526/add_blood_record_source.sql | 11 + 8 files changed, 4703 insertions(+), 10 deletions(-) create mode 100644 TUICallKit-Vue3/tongji/pages/index.vue create mode 100644 server/sql/1.9.20260526/add_blood_record_source.sql diff --git a/TUICallKit-Vue3/main.js b/TUICallKit-Vue3/main.js index 5fd1d831..f1d13fa0 100644 --- a/TUICallKit-Vue3/main.js +++ b/TUICallKit-Vue3/main.js @@ -1,5 +1,5 @@ import App from './App' -var baseUrl ='https://admin.zhenyangtang.com.cn/'; +var baseUrl ='https://css.zhenyangtang.com.cn/'; // #ifndef VUE3 import Vue from 'vue' import './uni.promisify.adaptor' diff --git a/TUICallKit-Vue3/manifest.json b/TUICallKit-Vue3/manifest.json index 5389584a..2d9c65d6 100644 --- a/TUICallKit-Vue3/manifest.json +++ b/TUICallKit-Vue3/manifest.json @@ -57,7 +57,13 @@ "optimization" : { "subPackages" : true }, - "usingComponents" : true + "usingComponents" : true, + "plugins" : { + "WechatSI" : { + "version" : "0.3.5", + "provider" : "wx069ba97219f66d99" + } + } }, "mp-alipay" : { "usingComponents" : true diff --git a/TUICallKit-Vue3/pages.json b/TUICallKit-Vue3/pages.json index 8fe89074..f574c992 100644 --- a/TUICallKit-Vue3/pages.json +++ b/TUICallKit-Vue3/pages.json @@ -126,6 +126,21 @@ } } ] + }, + { + "root": "tongji", + "pages": [ + { + "path": "pages/index", + "style": { + "navigationBarTitleText": "日常记录", + "navigationBarBackgroundColor": "#0ea5a4", + "navigationBarTextStyle": "white", + "backgroundColor": "#f1f5f9", + "enablePullDownRefresh": true + } + } + ] } ], "tabBar": { diff --git a/TUICallKit-Vue3/pages/Card/Card.vue b/TUICallKit-Vue3/pages/Card/Card.vue index 0b800434..8393e98c 100644 --- a/TUICallKit-Vue3/pages/Card/Card.vue +++ b/TUICallKit-Vue3/pages/Card/Card.vue @@ -53,6 +53,18 @@ {{ card.create_time || '-' }} + + + + + 📈 + 日常记录 + + + 📝 + 查看 / 编辑 + + @@ -138,6 +150,16 @@ const viewCardDetail = (card) => { }) } +// 查看日常记录(血糖血压 / 饮食 / 运动 / 跟踪备注 + 波浪图) +const viewDailyRecord = (card) => { + const url = `/tongji/pages/index?diagnosis_id=${card.id}` + + `&patient_id=${card.patient_id || ''}` + + `&patient_name=${encodeURIComponent(card.patient_name || '')}` + + `&age=${card.age || ''}` + + `&gender=${card.gender || ''}` + uni.navigateTo({ url }) +} + // 新建就诊卡(patient_id 创建后自动生成,仅需登录) const createCard = () => { const token = uni.getStorageSync('token') @@ -310,6 +332,54 @@ const formatDate = (timestamp) => { border-top: 4rpx solid #e8eaed; } +.card-actions { + display: flex; + gap: 20rpx; + margin-top: 28rpx; + padding-top: 28rpx; + border-top: 4rpx solid #e8eaed; +} + +.card-action-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 12rpx; + padding: 24rpx 12rpx; + background: #f6faff; + border-radius: 20rpx; + border: 2rpx solid #d6e6fb; + min-height: 80rpx; + transition: all 0.2s; + + &:active { + transform: scale(0.98); + opacity: 0.85; + } + + &.primary { + background: linear-gradient(135deg, #1890ff, #0ea5a4); + border-color: transparent; + + .card-action-icon, + .card-action-text { + color: #ffffff; + } + } +} + +.card-action-icon { + font-size: 36rpx; + color: #1890ff; +} + +.card-action-text { + font-size: 30rpx; + color: #1890ff; + font-weight: 500; +} + .time-info { display: flex; align-items: center; diff --git a/TUICallKit-Vue3/tongji/pages/index.vue b/TUICallKit-Vue3/tongji/pages/index.vue new file mode 100644 index 00000000..a4627447 --- /dev/null +++ b/TUICallKit-Vue3/tongji/pages/index.vue @@ -0,0 +1,4220 @@ +