This commit is contained in:
Your Name
2026-04-22 16:36:40 +08:00
parent 4872e4d2e6
commit 852bcd54a5
7 changed files with 528 additions and 66 deletions
+61 -17
View File
@@ -38,6 +38,18 @@
<el-radio-button label="not_passed">未通过</el-radio-button>
<el-radio-button label="rejected">驳回</el-radio-button>
</el-radio-group>
<span class="pl-toolbar__sep" aria-hidden="true" />
<span class="pl-toolbar__k" title="与列表「来源」列一致:系统代开 / 手工">来源</span>
<el-radio-group
v-model="formData.source_filter"
size="small"
class="pl-toolbar__rg pl-toolbar__rg--source"
@change="onSourceFilterChange"
>
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="manual">手工</el-radio-button>
<el-radio-button label="system">系统代开</el-radio-button>
</el-radio-group>
</div>
<div class="pl-toolbar__line pl-toolbar__line--second">
<el-form :model="formData" inline class="pl-toolbar__form">
@@ -189,7 +201,7 @@
查看
</el-button>
<el-button
v-if="!Number(row.has_prescription_order)"
v-if="!Number(row.has_prescription_order) && Number(row.void_status) !== 1"
type="success"
link
v-perms="['tcm.prescriptionOrder/create']"
@@ -444,6 +456,15 @@
class="mb-4"
:title="editReviveHint"
/>
<div
v-if="Number(editForm.diagnosis_id) > 0"
class="mb-3 flex flex-wrap items-center gap-2 pl-[120px]"
>
<el-button type="primary" size="small" @click="openDiagnosisPatientDetail">
查看患者诊单详情
</el-button>
<span class="text-xs text-gray-400">关联诊单 #{{ editForm.diagnosis_id }}</span>
</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="患者姓名" prop="patient_name">
@@ -1234,6 +1255,9 @@
<el-button type="danger" :loading="auditLoading" @click="submitAudit('reject')">驳回处方</el-button>
</template>
</el-dialog>
<!-- 与 tcm/diagnosis/edit 同一套诊单界面(只读 + 全部分页签) -->
<TcmDiagnosisEditView ref="diagnosisViewRef" />
</div>
</template>
@@ -1259,9 +1283,11 @@ import feedback from '@/utils/feedback'
import type { FormInstance, FormRules } from 'element-plus'
import DaterangePicker from '@/components/daterange-picker/index.vue'
import { Search } from '@element-plus/icons-vue'
import { computed, onMounted, reactive, ref, watch, nextTick } from 'vue'
import { computed, onMounted, reactive, ref, watch, nextTick, defineAsyncComponent } from 'vue'
import { useRouter } from 'vue-router'
const TcmDiagnosisEditView = defineAsyncComponent(() => import('@/views/tcm/diagnosis/edit.vue'))
/** 与 server/config/project.php prescription_audit_roles 保持一致 */
const PRESCRIPTION_AUDIT_ROLE_IDS = [0, 3]
@@ -1289,6 +1315,8 @@ const formData = reactive({
creator_ids: [] as number[],
/** all | passed | not_passed | pending | rejectedall 表示不限,接口传参会转为空) */
audit_filter: 'all' as string,
/** all | manual | system — 与 is_system_auto:手工(0) / 系统代开(1) */
source_filter: 'all' as string,
start_time: '',
end_time: ''
})
@@ -1621,13 +1649,13 @@ function resetCreateOrderForm() {
createOrderFormRef.value?.clearValidate()
}
/** 仅按定金门槛抬升「总金额」,不跟已选支付单合计联动(由用户自填) */
function syncCreateOrderAmountFromRules() {
const min = createOrderDepositMin.value
const paid = createOrderLinkedPaidTotal.value
const floor = min > 0 ? Math.round(min * 100) / 100 : 0
const need = min > 0 ? Math.max(paid, floor) : paid > 0 ? paid : 0
if (need > 0 && Number(createOrderForm.amount) < need) {
createOrderForm.amount = Math.round(need * 100) / 100
if (min <= 0) return
const need = Math.round(min * 100) / 100
if (Number(createOrderForm.amount) < need) {
createOrderForm.amount = need
}
}
@@ -1664,15 +1692,11 @@ watch(
}
)
watch(
() => [...createOrderForm.pay_order_ids],
() => {
syncCreateOrderAmountFromRules()
},
{ deep: true }
)
async function openCreateOrderFromPrescription(row: any) {
if (Number(row?.void_status) === 1) {
feedback.msgWarning('已作废处方不可创建业务订单')
return
}
resetCreateOrderForm()
createOrderPrescription.value = row
const did = Number(row.diagnosis_id) || 0
@@ -1889,6 +1913,19 @@ const editForm = reactive({
times_per_day: 2
})
const diagnosisViewRef = ref<{
openViewOnly: (id: number) => Promise<void>
} | null>(null)
function openDiagnosisPatientDetail() {
const id = Number(editForm.diagnosis_id)
if (!id) {
feedback.msgWarning('本处方未关联诊单,无法查看诊单患者信息')
return
}
void diagnosisViewRef.value?.openViewOnly(id)
}
// 标志:是否正在加载数据(防止watch函数覆盖已加载的值)
const isLoadingData = ref(false)
@@ -1940,10 +1977,12 @@ const rules: FormRules = {
}
async function fetchPrescriptionLists(params: Record<string, unknown>) {
const { audit_filter, creator_ids, ...rest } = params
const { audit_filter, source_filter, creator_ids, ...rest } = params
const payload: Record<string, unknown> = {
...rest,
audit_filter: audit_filter === 'all' || audit_filter === '' ? '' : audit_filter
audit_filter: audit_filter === 'all' || audit_filter === '' ? '' : audit_filter,
source_filter:
source_filter === 'all' || source_filter === '' || source_filter === undefined ? '' : source_filter
}
if (Array.isArray(creator_ids) && creator_ids.length > 0) {
payload.creator_ids = creator_ids
@@ -1993,6 +2032,10 @@ function onAuditFilterChange() {
resetPage()
}
function onSourceFilterChange() {
resetPage()
}
let syncingDateTab = false
watch(
() => [formData.start_time, formData.end_time],
@@ -2039,6 +2082,7 @@ function resetParams() {
formData.patient_name = ''
formData.creator_ids = []
formData.audit_filter = 'all'
formData.source_filter = 'all'
formData.start_time = ''
formData.end_time = ''
syncingDateTab = false
@@ -62,7 +62,77 @@
</el-card>
<el-card class="!border-none po-filter-card shadow-sm" shadow="never">
<div
class="po-list-stats grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5"
v-loading="pager.loading"
>
<div
class="po-stat-surface po-stat-surface--order relative overflow-hidden rounded-xl border border-slate-200/90 px-5 py-4 shadow-sm"
>
<div
class="po-stat-deco po-stat-deco--o pointer-events-none absolute -right-6 -top-8 h-28 w-28 rounded-full opacity-40"
/>
<div class="relative flex items-start justify-between gap-3">
<div>
<div class="flex items-center gap-2 text-slate-500 text-xs font-medium">
<el-icon :size="16" class="text-sky-500"><Calendar /></el-icon>
{{ listStats.orderHeading }}
</div>
<div class="mt-2 text-2xl sm:text-[26px] font-bold text-slate-800 tracking-tight tabular-nums">
¥{{ listStats.orderStr }}
</div>
</div>
<div
class="stat-icon-wrap h-10 w-10 shrink-0 rounded-lg bg-sky-500/10 flex items-center justify-center text-sky-600"
>
<el-icon :size="20"><Document /></el-icon>
</div>
</div>
<p class="relative mt-2 text-xs text-slate-500 leading-relaxed">
{{ listStats.periodLine }}
</p>
<p class="relative mt-1.5 text-xs text-slate-400 leading-relaxed">
{{ listStats.scopeHint }}
</p>
</div>
<div
class="po-stat-surface po-stat-surface--pay relative overflow-hidden rounded-xl border border-slate-200/90 px-5 py-4 shadow-sm"
>
<div
class="po-stat-deco po-stat-deco--p pointer-events-none absolute -right-6 -top-8 h-28 w-28 rounded-full opacity-40"
/>
<div class="relative flex items-start justify-between gap-3">
<div>
<div class="flex items-center gap-2 text-slate-500 text-xs font-medium">
<el-icon :size="16" class="text-emerald-500"><LinkIcon /></el-icon>
{{ listStats.payHeading }}
</div>
<div class="mt-2 text-2xl sm:text-[26px] font-bold text-slate-800 tracking-tight tabular-nums">
¥{{ listStats.payStr }}
</div>
</div>
<div
class="stat-icon-wrap h-10 w-10 shrink-0 rounded-lg bg-emerald-500/10 flex items-center justify-center text-emerald-600"
>
<el-icon :size="20"><Wallet /></el-icon>
</div>
</div>
<p class="relative mt-2 text-xs text-slate-500 leading-relaxed">
{{ listStats.periodLine }}
</p>
<p class="relative mt-1.5 text-xs text-slate-400 leading-relaxed">
所选时间内创建订单所关联的支付单金额实付(关联)含义一致不含创建时间时默认今日
</p>
</div>
</div>
<el-form class="ls-form" :model="queryParams" inline>
<el-form-item class="w-[360px] min-w-[240px] max-w-full" label="创建时间">
<DaterangePicker
v-model:startTime="queryParams.start_time"
v-model:endTime="queryParams.end_time"
class="!w-full"
/>
</el-form-item>
<el-form-item class="w-[260px]" label="订单号">
<el-input
v-model="queryParams.order_no"
@@ -429,19 +499,30 @@
<!-- 顶部数据概览卡片 -->
<el-row :gutter="16" class="mb-5">
<el-col :xs="12" :sm="8">
<el-col :xs="12" :sm="6">
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">总金额</div>
<div class="stat-value text-red-500 font-bold text-xl">¥{{ detailData.amount }}</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8">
<el-col :xs="12" :sm="6">
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付总额</div>
<div class="stat-value text-green-600 font-bold text-xl">¥{{ detailData.linked_pay_paid_total || 0 }}</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8">
<el-col :xs="12" :sm="6">
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">需代收</div>
<div
class="stat-value font-bold text-xl"
:class="detailAgencyToCollect > 0 ? 'text-amber-600' : 'text-gray-500'"
>
¥{{ formatMoney(detailAgencyToCollect) }}
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付笔数</div>
<div class="stat-value text-primary font-bold text-xl">{{ detailLinkedPayOrders.length }} </div>
@@ -1767,7 +1848,8 @@
<script lang="ts" setup name="prescriptionOrderList">
import { computed, onMounted, reactive, ref, nextTick, watch } from 'vue'
import { Refresh, Loading, ArrowDown, InfoFilled, Search } from '@element-plus/icons-vue'
import { Refresh, Loading, ArrowDown, InfoFilled, Search, Calendar, Document, Link as LinkIcon, Wallet } from '@element-plus/icons-vue'
import DaterangePicker from '@/components/daterange-picker/index.vue'
import {
prescriptionOrderAuditPayment,
prescriptionOrderAuditPrescription,
@@ -1938,6 +2020,9 @@ function handleFulfillmentStatusTabClick(value: number | '') {
}
const queryParams = reactive({
/** 创建时间区间(与列表筛选一致;不影响「今日」统计) */
start_time: '' as string,
end_time: '' as string,
order_no: '',
prescription_id: '' as string | number,
/** 诊单患者姓名/手机号(后台关联 diagnosis */
@@ -2001,6 +2086,10 @@ async function fetchLists(params: Record<string, unknown>) {
}
if (!p.order_no) delete p.order_no
if (!String(p.patient_keyword || '').trim()) delete p.patient_keyword
if (!String(p.start_time || '').trim() || !String(p.end_time || '').trim()) {
delete p.start_time
delete p.end_time
}
return prescriptionOrderLists(p)
}
@@ -2009,6 +2098,45 @@ const { pager, getLists, resetPage, resetParams } = usePaging({
params: queryParams
})
const listStats = computed(() => {
const ex = pager.extend as Record<string, unknown> | undefined
const o = ex?.stats_order_amount ?? ex?.today_order_amount
const pay = ex?.stats_linked_pay_amount ?? ex?.today_linked_pay_amount
const n = (v: unknown) => {
const x = Number(v)
if (!Number.isFinite(x)) return 0
return Math.round(x * 100) / 100
}
const fmt = (v: number) => v.toFixed(2)
const t0 = String(ex?.stats_time_start ?? '').trim()
const t1 = String(ex?.stats_time_end ?? '').trim()
const useRange = t0 && t1
const periodLine = useRange
? `统计时间:${t0} ${t1}`
: '统计时间:—'
const scope = String(ex?.stats_scope ?? 'restricted')
let scopeHint = '统计口径:与当前列表数据可见范围一致'
if (scope === 'all') {
scopeHint =
'统计口径:支付单审核白名单角色,按全量订单;与下方筛选项(医生/医助/患者等,不含「创建时间」)一致'
} else if (scope === 'assistant') {
scopeHint = '统计口径:仅计诊单医助为本人且满足下方其它筛选项的订单'
}
const hasCustomTime =
String(queryParams.start_time || '').trim() !== '' && String(queryParams.end_time || '').trim() !== ''
const headPrefix = hasCustomTime ? '区间' : '今日'
return {
order: n(o),
pay: n(pay),
orderStr: fmt(n(o)),
payStr: fmt(n(pay)),
orderHeading: `${headPrefix}业务订单额`,
payHeading: `${headPrefix}关联实付`,
periodLine,
scopeHint
}
})
watch(
showPrescriptionAuditFilter,
(show) => {
@@ -2021,6 +2149,8 @@ watch(
)
function handleReset() {
queryParams.start_time = ''
queryParams.end_time = ''
queryParams.order_no = ''
queryParams.prescription_id = ''
queryParams.patient_keyword = ''
@@ -2294,6 +2424,15 @@ const detailLinkedPayOrders = computed(() => {
return Array.isArray(arr) ? arr : []
})
/** 需代收:业务订单总金额 − 关联已付总额 */
const detailAgencyToCollect = computed(() => {
const d = detailData.value
if (!d) return 0
const total = Number(d.amount) || 0
const paid = Number(d.linked_pay_paid_total) || 0
return Math.round((total - paid) * 100) / 100
})
const detailUnlinkedPayOrders = ref<any[]>([])
async function loadDetailUnlinkedPayOrders(diagnosisId: number, prescriptionOrderId: number, linkedIds: number[]) {
@@ -3612,6 +3751,36 @@ async function downloadPrescriptionSlipPdf() {
padding-bottom: 12px;
}
.po-list-stats {
--po-stat-order-a: color-mix(in srgb, var(--el-color-primary) 12%, white);
--po-stat-order-b: color-mix(in srgb, var(--el-color-primary) 5%, transparent);
--po-stat-pay-a: color-mix(in srgb, #10b981 10%, white);
--po-stat-pay-b: color-mix(in srgb, #10b981 4%, transparent);
}
.po-stat-surface {
transition: box-shadow 0.2s ease, border-color 0.2s ease;
}
.po-stat-surface:hover {
box-shadow: 0 10px 28px -12px rgba(15, 23, 42, 0.18);
border-color: color-mix(in srgb, var(--el-color-primary) 22%, var(--el-border-color));
}
.po-stat-surface--order {
background: linear-gradient(135deg, var(--po-stat-order-a) 0%, #fff 48%, var(--po-stat-order-b) 100%);
}
.po-stat-surface--pay {
background: linear-gradient(135deg, var(--po-stat-pay-a) 0%, #fff 48%, var(--po-stat-pay-b) 100%);
}
.po-stat-deco--o {
background: radial-gradient(circle, color-mix(in srgb, var(--el-color-primary) 45%, transparent) 0%, transparent 70%);
}
.po-stat-deco--p {
background: radial-gradient(circle, color-mix(in srgb, #10b981 40%, transparent) 0%, transparent 70%);
}
.stat-icon-wrap {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.po-table-card {
border-radius: 8px;
}
+58 -30
View File
@@ -5,7 +5,7 @@
:title="drawerTitle"
size="60%"
:before-close="handleClose"
:z-index="1500"
:z-index="viewOnly ? 4000 : 1500"
:modal="true"
class="diagnosis-drawer"
>
@@ -18,6 +18,10 @@
:rules="formRules"
label-width="160px"
>
<fieldset
:disabled="viewOnly"
class="diagnosis-view-fieldset border-0 min-w-0 p-0 m-0"
>
<!-- 基本信息 -->
<el-divider content-position="left">基本信息</el-divider>
@@ -520,6 +524,7 @@
placeholder="请输入病史补充"
/>
</el-form-item>
</fieldset>
</el-form>
</el-tab-pane>
@@ -664,11 +669,11 @@
<template #footer>
<div class="flex justify-end gap-3 px-4 pb-4">
<el-button @click="handleClose">取消</el-button>
<el-button
v-if="activeTab === 'basic'"
type="primary"
@click="handleSubmit"
<el-button @click="handleClose">{{ viewOnly ? '关闭' : '取消' }}</el-button>
<el-button
v-if="!viewOnly && activeTab === 'basic'"
type="primary"
@click="handleSubmit"
:loading="submitting"
>
确定
@@ -711,6 +716,8 @@ const callRecordPanelRef = ref<{ refresh?: () => void } | null>(null)
const prescriptionRef = ref()
const visible = ref(false)
/** 只读查看:与 consumers 处方等处「查看诊单」一致,全部 Tab 仅展示不可保存 */
const viewOnly = ref(false)
const formRef = ref()
const mode = ref('add')
const submitting = ref(false)
@@ -721,7 +728,10 @@ const hasPhonePlainPermission = computed(() => hasPermission(['tcm.diagnosis/pho
const showPhoneMaskedEdit = computed(() => mode.value === 'edit' && !hasPhonePlainPermission.value)
const phoneRevealUnlocked = ref(false)
const activeTab = ref('basic')
const drawerTitle = computed(() => (mode.value === 'add' ? '新增诊单' : '编辑诊单'))
const drawerTitle = computed(() => {
if (viewOnly.value) return '诊单详情'
return mode.value === 'add' ? '新增诊单' : '编辑诊单'
})
watch([visible, activeTab], () => {
if (!visible.value) return
@@ -1075,7 +1085,22 @@ const getDictOptions = async () => {
}
}
async function loadDiagnosisDetailIntoForm(id: number): Promise<void> {
const data = await tcmDiagnosisDetail({ id })
originalPhone.value = data.phone || ''
originalIdCard.value = data.id_card || ''
data.phone = hasPhonePlainPermission.value ? data.phone || '' : maskPhone(data.phone || '')
data.id_card = maskIdCard(data.id_card || '')
formData.value = data
const y = data.diabetes_discovery_year
formData.value.diabetes_discovery_year = y == null || y === '' ? '' : String(y)
}
const open = async (type: string, id?: number) => {
viewOnly.value = false
mode.value = type
visible.value = true
activeTab.value = 'basic' // 重置到基本信息标签页
@@ -1085,27 +1110,27 @@ const open = async (type: string, id?: number) => {
await getDictOptions()
if (type === 'edit' && id) {
const data = await tcmDiagnosisDetail({ id })
// 保存原始完整数据
originalPhone.value = data.phone || ''
originalIdCard.value = data.id_card || ''
// 有「手机号明文」权限:表单中保持完整号码;否则表单内为脱敏(编辑无权限时用只读区+点击展开)
data.phone = hasPhonePlainPermission.value ? data.phone || '' : maskPhone(data.phone || '')
data.id_card = maskIdCard(data.id_card || '')
formData.value = data
const y = data.diabetes_discovery_year
formData.value.diabetes_discovery_year =
y == null || y === '' ? '' : String(y)
await loadDiagnosisDetailIntoForm(id)
} else if (type === 'add') {
const userStore = useUserStore()
formData.value.assistant_id = userStore.userInfo?.id || ''
}
}
/** 只读打开:与编辑同一套界面与全部 Tab,不可保存 */
const openViewOnly = async (id: number) => {
if (!id) return
viewOnly.value = true
mode.value = 'edit'
visible.value = true
activeTab.value = 'basic'
phoneRevealUnlocked.value = false
await getDictOptions()
await loadDiagnosisDetailIntoForm(id)
}
const handleSubmit = async () => {
if (viewOnly.value) return
await formRef.value?.validate()
submitting.value = true
@@ -1239,12 +1264,14 @@ const handleClose = () => {
isPhoneFocused.value = false
isIdCardFocused.value = false
phoneRevealUnlocked.value = false
viewOnly.value = false
visible.value = false
}
defineExpose({
open,
openViewOnly,
refreshCaseList: () => caseRecordListRef.value?.refresh?.()
})
</script>
@@ -1252,7 +1279,15 @@ defineExpose({
<style lang="scss" scoped>
.edit-drawer {
font-size: 12px;
.diagnosis-view-fieldset:disabled {
opacity: 1;
:deep(.el-input__wrapper),
:deep(.el-textarea__inner) {
box-shadow: none;
}
}
:deep(.el-form) {
padding-bottom: 20px;
font-size: 12px;
@@ -1415,14 +1450,7 @@ defineExpose({
z-index: 3000 !important;
}
/* 降低抽屉及其遮罩层的z-index */
.el-drawer {
z-index: 1500 !important;
}
.el-overlay {
z-index: 1499 ;
}
/* 勿对全局 .el-drawer / .el-overlay 写 z-index,会覆盖子组件的 :z-index(例如从消费处方里嵌套打开只读诊单时会被压在下面) */
/* 确保日期选择器和下拉选择器的弹出层在最上层 */
.el-picker__popper,