Compare commits

...
35 Commits
Author SHA1 Message Date
Your Name 928f75e016 更新 2026-05-14 11:00:56 +08:00
Your Name 37fa160c24 更新 2026-05-13 17:23:29 +08:00
Your Name 0eaacbb4ce 更新 2026-05-13 16:54:54 +08:00
Your Name 1e8b5c4646 修复bug 2026-05-13 09:49:47 +08:00
Your Name fabaa84373 新增 2026-05-12 18:14:20 +08:00
Your Name da017bdf20 更新 2026-05-12 18:10:40 +08:00
Your Name 5d94ffab3e 更新 2026-05-12 17:58:54 +08:00
Your Name 796ca12fb8 更新 2026-05-12 17:26:11 +08:00
Your Name c644f2554f 更新 2026-05-12 11:26:51 +08:00
Your Name e83dac756d 新增 2026-05-12 10:59:03 +08:00
Your Name 0c1709d589 Merge branch 'master' into yeji-5-11 2026-05-12 10:43:04 +08:00
Your Name a3892001d3 更新 2026-05-12 10:42:14 +08:00
Your Name 7add364e20 更新 2026-05-11 17:48:47 +08:00
Your Name 0ebea74fde 更新 2026-05-11 17:46:29 +08:00
Your Name 1156787327 更新 2026-05-11 16:34:44 +08:00
Your Name 15cf6324f6 更新 2026-05-11 16:10:06 +08:00
long 1338b8735c Merge branch '20260511-long' 2026-05-11 15:06:07 +08:00
long 82f229adf2 Update 2026_05_11_diagnosis_assign_read.sql 2026-05-11 15:06:01 +08:00
long 048e43a662 Merge branch '20260511-long' 2026-05-11 15:04:33 +08:00
long d8d4da197e 新指派医助提示 2026-05-11 15:04:24 +08:00
gr a0325a8fb1 更新 2026-05-11 12:35:46 +08:00
Your Name 091db50841 更新 2026-05-11 12:32:25 +08:00
long 1774a87dd7 update 2026-05-11 11:41:42 +08:00
Your Name 62d15efbaf 更新 2026-05-11 11:36:26 +08:00
long c468c57cda 关联支付单豁免 2026-05-11 11:25:03 +08:00
Your Name bacdd6386f 新增 2026-05-09 18:36:46 +08:00
long ea290c4c7b Merge branch 'long-0509' 2026-05-09 18:32:23 +08:00
long 31b4afdcb7 Update PrescriptionLogic.php 2026-05-09 18:32:16 +08:00
long 379d2ef404 Merge branch 'master' of https://gitee.com/v_cms/zyt 2026-05-09 18:07:38 +08:00
long 5cdb7ebb9e Merge branch 'long-0509' 2026-05-09 18:07:37 +08:00
long 496a2b870b 业务订单优化 2026-05-09 18:07:30 +08:00
Your Name 471bdf12b7 更新 2026-05-09 17:45:04 +08:00
long 8fe4683e64 Revert "1"
This reverts commit 0a37335f3c.
2026-05-09 17:35:33 +08:00
long c34409edde Merge branch 'long-0509' 2026-05-09 17:14:13 +08:00
long 0a37335f3c 1 2026-05-09 17:14:03 +08:00
449 changed files with 8783 additions and 1054 deletions
+3
View File
@@ -30,3 +30,6 @@ bin-release/
/.claude
/.claude
/.kbctx
/server/.spool
/server/.claude
/.spool
+5
View File
@@ -66,6 +66,11 @@ export function appointmentLists(params: any) {
return request.get({ url: '/doctor.appointment/lists', params })
}
/** 后台编辑挂号(预约日期/时段/类型/状态/备注/医助) */
export function appointmentAdminEdit(params: any) {
return request.post({ url: '/doctor.appointment/edit', params })
}
// 获取挂号详情
export function appointmentDetail(params: any) {
return request.get({ url: '/doctor.appointment/detail', params })
+5
View File
@@ -104,3 +104,8 @@ export function orderActionLogStats(params?: { start_time?: string; end_time?: s
export function orderBatchAssignAssistant(params: { order_ids: number[]; assistant_id: number }) {
return request.post({ url: '/order.order/assignAssistant', params })
}
/** 设置/取消支付单豁免权 */
export function orderSetExempt(params: { id: number; is_exempt: 0 | 1 }) {
return request.post({ url: '/order.order/setExempt', params })
}
+76
View File
@@ -103,3 +103,79 @@ export function doctorDailyStatsOverview(params: {
}) {
return request.get({ url: '/stats.doctorDailyStats/overview', params })
}
/** 提成结算接口计算较重,单独放宽超时(默认多为 60s) */
const COMMISSION_SETTLEMENT_TIMEOUT_MS = 120000
/** 提成结算业绩(独立于业绩看板 yejiStats) */
export function commissionSettlementOverview(params: {
settlement_month: string
/** 与 tcm.prescriptionOrder/lists 同源:create_time between */
start_time?: string
end_time?: string
/** 默认 3=履约完成,与列表 fulfillment_status 一致 */
fulfillment_status?: number
/** 传 1 时仅统计 is_system_auto=1;显式时段下默认不传(含手动) */
require_system_auto_prescription?: 0 | 1
dept_ids?: number[] | string
channel_code?: string
}) {
return request.get(
{ url: '/stats.commissionSettlement/overview', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
{ ignoreCancelToken: true }
)
}
export function commissionSettlementDeptOptions() {
return request.get({ url: '/stats.commissionSettlement/deptOptions' })
}
export function commissionSettlementChannelOptions() {
return request.get({ url: '/stats.commissionSettlement/channelOptions' })
}
/** 提成核对:订单明细分页 bucket: 空|current|deferredappt_channel_value 可与 assistant_id/doctor_id 组合(0=未匹配挂号渠道) */
export function commissionSettlementOrderLines(params: {
settlement_month: string
start_time?: string
end_time?: string
fulfillment_status?: number
require_system_auto_prescription?: 0 | 1
dept_ids?: number[] | string
channel_code?: string
page?: number
page_size?: number
bucket?: string
assistant_id?: number
doctor_id?: number
appt_channel_value?: number
}) {
return request.get(
{ url: '/stats.commissionSettlement/orderLines', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
{ ignoreCancelToken: true }
)
}
export function commissionSettlementConfirmStatus(params: {
settlement_month: string
dept_ids?: number[] | string
channel_code?: string
}) {
return request.get({ url: '/stats.commissionSettlement/confirmStatus', params })
}
export function commissionSettlementSaveReconcile(params: Record<string, any>) {
return request.post({ url: '/stats.commissionSettlement/saveReconcile', params })
}
export function commissionSettlementConfirmFinalize(params: Record<string, any>) {
return request.post(
{ url: '/stats.commissionSettlement/confirmFinalize', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
{ ignoreCancelToken: true }
)
}
/** 撤回「确定本期业绩」:清除顺延结转,状态变为可再次核对/确定 */
export function commissionSettlementConfirmRevoke(params: Record<string, any>) {
return request.post({ url: '/stats.commissionSettlement/confirmRevoke', params })
}
+5
View File
@@ -363,6 +363,11 @@ export function prescriptionOrderLists(params: any) {
return request.get({ url: '/tcm.prescriptionOrder/lists', params })
}
/** 处方业务订单导出(export=1 预估条数,export=2 下载 Excel */
export function prescriptionOrderExport(params: any) {
return request.get({ url: '/tcm.prescriptionOrder/export', params })
}
/**
* 诊单下可关联的支付单(已支付 zyt_order;已占用且未撤回的会排除;编辑时传 prescription_order_id 保留当前单已选)。
* 服务端仅返回创建时间在 2026-04-20(含)之后的支付单;编辑时本单已关联的旧单仍会出现在列表中。
@@ -13,6 +13,7 @@
<el-button>导出</el-button>
</template>
<div>
<p v-if="props.exportHint" class="text-sm text-gray-500 mb-3 leading-relaxed">{{ props.exportHint }}</p>
<el-form ref="formRef" :model="formData" label-width="120px" :rules="formRules">
<el-form-item label="数据量:">
预计导出{{ exportData.count }}条数据 {{ exportData.sum_page }}每页{{
@@ -79,6 +80,11 @@ const props = defineProps({
fetchFun: {
type: Function,
required: true
},
/** 可选:导出弹窗内提示文案(如说明与列表筛选一致) */
exportHint: {
type: String,
default: ''
}
})
const popupRef = shallowRef<InstanceType<typeof Popup>>()
@@ -0,0 +1,397 @@
<template>
<div class="guahao-list">
<el-card class="!border-none" shadow="never">
<el-form class="mb-[-16px]" :model="queryParams" :inline="true" @submit.prevent>
<el-form-item label="预约日期">
<daterange-picker
v-model:startTime="queryParams.start_date"
v-model:endTime="queryParams.end_date"
/>
</el-form-item>
<el-form-item label="患者姓名">
<el-input
v-model="queryParams.patient_name"
placeholder="模糊搜索"
clearable
class="!w-[160px]"
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item label="医生">
<el-input
v-model="queryParams.doctor_name"
placeholder="模糊搜索"
clearable
class="!w-[140px]"
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable class="!w-[130px]">
<el-option label="已预约" :value="1" />
<el-option label="已取消" :value="2" />
<el-option label="已完成" :value="3" />
<el-option label="已过号" :value="4" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="resetFilter">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<el-table v-loading="pager.loading" :data="pager.lists" size="large" stripe>
<el-table-column label="ID" prop="id" width="72" align="center" />
<el-table-column label="诊单/患者" min-width="140">
<template #default="{ row }">
<div class="text-sm">
<div class="font-medium">{{ row.patient_name || '—' }}</div>
<div class="text-gray-500">{{ maskPhone(row.patient_phone) }}</div>
<div class="text-xs text-gray-400">诊单 #{{ row.diagnosis_id ?? row.patient_id }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="医生" prop="doctor_name" width="100" show-overflow-tooltip />
<el-table-column label="医助" prop="assistant_name" width="100" show-overflow-tooltip />
<el-table-column label="预约时间" min-width="128">
<template #default="{ row }">
<div>{{ row.appointment_date }}</div>
<div class="text-gray-500">{{ formatHm(row.appointment_time) }} · {{ row.period_desc }}</div>
</template>
</el-table-column>
<el-table-column label="类型" prop="appointment_type_desc" width="100" />
<el-table-column label="渠道" min-width="130" show-overflow-tooltip>
<template #default="{ row }">
<div class="text-sm">{{ row.channel_source_desc || '—' }}</div>
<div v-if="row.channel_source_detail" class="text-xs text-gray-400 truncate">
{{ row.channel_source_detail }}
</div>
</template>
</el-table-column>
<el-table-column label="状态" width="96" align="center">
<template #default="{ row }">
<el-tag
:type="
row.status === 1 ? 'success' : row.status === 2 ? 'info' : row.status === 3 ? 'primary' : 'danger'
"
size="small"
effect="light"
>
{{ row.status_desc }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-button v-perms="['doctor.appointment/edit']" type="primary" link @click="openEdit(row)">
编辑
</el-button>
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
<el-dialog v-model="editVisible" title="编辑挂号" width="560px" destroy-on-close @closed="resetEditForm">
<el-form :model="editForm" label-width="96px">
<el-form-item label="预约日期" required>
<el-date-picker
v-model="editForm.appointment_date"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
class="!w-full"
/>
</el-form-item>
<el-form-item label="预约时间" required>
<el-time-picker
v-model="editForm.appointment_time"
format="HH:mm"
value-format="HH:mm"
placeholder="时间"
class="!w-full"
/>
</el-form-item>
<el-form-item label="时段" required>
<el-radio-group v-model="editForm.period">
<el-radio-button label="morning">上午</el-radio-button>
<el-radio-button label="afternoon">下午</el-radio-button>
<el-radio-button label="all">全天</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="问诊类型" required>
<el-select v-model="editForm.appointment_type" class="!w-full">
<el-option label="视频问诊" value="video" />
<el-option label="图文问诊" value="text" />
<el-option label="电话问诊" value="phone" />
</el-select>
</el-form-item>
<el-form-item label="渠道来源" required>
<el-select
v-model="editForm.channel_source"
placeholder="请选择渠道来源"
filterable
class="!w-full"
@change="onChannelSourceChange"
>
<el-option
v-for="item in channelOptions"
:key="String(item.value)"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item v-if="needsChannelSourceDetail" label="自媒体补充" required>
<el-input
v-model="editForm.channel_source_detail"
maxlength="128"
show-word-limit
placeholder="请输入自媒体相关补充内容"
clearable
class="!w-full"
/>
</el-form-item>
<el-form-item label="状态" required>
<el-select v-model="editForm.status" class="!w-full">
<el-option label="已预约" :value="1" />
<el-option label="已取消" :value="2" />
<el-option label="已完成" :value="3" />
<el-option label="已过号" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="挂号医助">
<el-select
v-model="editForm.assistant_id"
placeholder="不修改可留空"
clearable
filterable
class="!w-full"
>
<el-option
v-for="a in assistantOptions"
:key="a.id"
:label="a.name + (a.account ? ` (${a.account})` : '')"
:value="a.id"
/>
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="editForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editVisible = false">取消</el-button>
<el-button type="primary" :loading="editSaving" @click="submitEdit">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup name="consumerPrescriptionGuahao">
import DaterangePicker from '@/components/daterange-picker/index.vue'
import { getDictData } from '@/api/app'
import { appointmentAdminEdit, appointmentLists } from '@/api/doctor'
import { getAssistants } from '@/api/tcm'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
const queryParams = reactive({
start_date: '',
end_date: '',
patient_name: '',
doctor_name: '',
status: '' as number | ''
})
const queryInit = { ...queryParams }
const { pager, getLists, resetPage } = usePaging({
fetchFun: appointmentLists,
params: queryParams
})
const assistantOptions = ref<Array<{ id: number; name: string; account?: string }>>([])
/** 与诊单预约弹窗一致:这些字典 name 需填「自媒体补充」 */
const CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL = new Set([
'自媒体4H',
'自媒体3Q',
'自媒体3H',
'自媒体2H',
'自媒体2Q'
])
function channelNameRequiresSelfMediaDetail(name: string) {
return CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL.has(String(name ?? '').trim())
}
const channelOptions = ref<Array<{ name: string; value: string }>>([])
const selectedChannelDictName = computed(() => {
const v = editForm.channel_source
if (v === '' || v == null) return ''
const row = channelOptions.value.find((item: { value: string }) => String(item.value) === String(v))
return row ? String(row.name ?? '').trim() : ''
})
const needsChannelSourceDetail = computed(() => channelNameRequiresSelfMediaDetail(selectedChannelDictName.value))
function onChannelSourceChange(val: string) {
const row = channelOptions.value.find((item: { value: string }) => String(item.value) === String(val))
const name = row ? String(row.name ?? '').trim() : ''
if (!channelNameRequiresSelfMediaDetail(name)) {
editForm.channel_source_detail = ''
}
}
const editVisible = ref(false)
const editSaving = ref(false)
const editForm = reactive({
id: 0,
appointment_date: '',
appointment_time: '' as string,
period: 'morning' as 'morning' | 'afternoon' | 'all',
appointment_type: 'video',
status: 1,
remark: '',
assistant_id: undefined as number | undefined,
channel_source: '' as string,
channel_source_detail: '' as string
})
function maskPhone(phone?: string) {
const p = String(phone || '')
if (p.length >= 11) {
return p.slice(0, 3) + '****' + p.slice(-4)
}
return p || '—'
}
function formatHm(t?: string) {
const s = String(t || '')
return s.length >= 5 ? s.slice(0, 5) : s || '—'
}
function resetFilter() {
Object.assign(queryParams, queryInit)
resetPage()
}
function rowPeriod(row: Record<string, unknown>): 'morning' | 'afternoon' | 'all' {
const v = String(row.period ?? row.type ?? 'morning')
if (v === 'afternoon' || v === 'all') {
return v
}
return 'morning'
}
async function loadAssistants() {
try {
const res: any = await getAssistants()
assistantOptions.value = res?.lists ?? res ?? []
} catch {
assistantOptions.value = []
}
}
async function loadChannelOptions() {
try {
const data: any = await getDictData({ type: 'channels' })
const rows = (data?.channels || []).filter((row: { status?: number }) => row.status !== 0)
rows.sort((a: { sort?: number; id?: number }, b: { sort?: number; id?: number }) => {
const ds = Number(b?.sort ?? 0) - Number(a?.sort ?? 0)
if (ds !== 0) return ds
return Number(b?.id ?? 0) - Number(a?.id ?? 0)
})
channelOptions.value = rows
} catch {
channelOptions.value = []
}
}
function openEdit(row: Record<string, unknown>) {
editForm.id = Number(row.id)
editForm.appointment_date = String(row.appointment_date || '')
editForm.appointment_time = formatHm(String(row.appointment_time || ''))
editForm.period = rowPeriod(row)
editForm.appointment_type = String(row.appointment_type || 'video')
editForm.status = Number(row.status)
editForm.remark = String(row.remark || '')
const raw = row.appointment_assistant_id
editForm.assistant_id =
raw !== undefined && raw !== null && raw !== '' ? Number(raw) : undefined
editForm.channel_source = String(row.channel_source ?? row.channels ?? '')
editForm.channel_source_detail = String(row.channel_source_detail ?? '')
editVisible.value = true
}
function resetEditForm() {
editForm.id = 0
editForm.appointment_date = ''
editForm.appointment_time = ''
editForm.period = 'morning'
editForm.appointment_type = 'video'
editForm.status = 1
editForm.remark = ''
editForm.assistant_id = undefined
editForm.channel_source = ''
editForm.channel_source_detail = ''
}
async function submitEdit() {
if (!editForm.appointment_date) {
feedback.msgError('请选择预约日期')
return
}
if (!editForm.appointment_time) {
feedback.msgError('请选择预约时间')
return
}
if (!editForm.channel_source) {
feedback.msgError('请选择渠道来源')
return
}
if (needsChannelSourceDetail.value && !editForm.channel_source_detail.trim()) {
feedback.msgError('请填写自媒体补充说明')
return
}
editSaving.value = true
try {
const payload: Record<string, unknown> = {
id: editForm.id,
appointment_date: editForm.appointment_date,
appointment_time: editForm.appointment_time,
period: editForm.period,
appointment_type: editForm.appointment_type,
status: editForm.status,
remark: editForm.remark,
assistant_id: editForm.assistant_id ?? '',
channel_source: editForm.channel_source,
channel_source_detail: editForm.channel_source_detail.trim()
}
await appointmentAdminEdit(payload)
feedback.msgSuccess('保存成功')
editVisible.value = false
getLists()
} finally {
editSaving.value = false
}
}
onMounted(() => {
loadAssistants()
loadChannelOptions()
})
onActivated(() => {
getLists()
})
getLists()
</script>
@@ -548,12 +548,20 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="患者姓名" prop="patient_name">
<el-input v-model="editForm.patient_name" placeholder="请输入患者姓名" />
<el-input
v-model="editForm.patient_name"
placeholder="请输入患者姓名"
:disabled="editMode === 'edit'"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="门诊号" prop="visit_no">
<el-input v-model="editForm.visit_no" placeholder="自动生成或手动输入" />
<el-input
v-model="editForm.visit_no"
placeholder="自动生成或手动输入"
:disabled="editMode === 'edit'"
/>
</el-form-item>
</el-col>
</el-row>
@@ -266,6 +266,14 @@
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="handleReset">重置</el-button>
<export-data
v-perms="['tcm.prescriptionOrder/export']"
class="ml-2.5"
:fetch-fun="prescriptionOrderExport"
:params="prescriptionOrderExportParams"
:page-size="pager.size"
export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。"
/>
</el-form-item>
</el-form>
</el-card>
@@ -426,7 +434,37 @@
</el-table-column>
<el-table-column label="开方人" prop="doctor_name" width="90" show-overflow-tooltip />
<el-table-column label="创建人" prop="creator_name" width="90" show-overflow-tooltip />
<el-table-column min-width="128" show-overflow-tooltip>
<template #header>
<span>二中心·指派</span>
<el-tooltip
placement="top"
max-width="320"
content="与诊单「指派记录」快照一致:仅当本条业务单在操作瞬间为快照中的「关联业务单」(创建人+创建时间命中)时显示。绿色=新医助在名称含「二中心」的部门及其下级;灰色=系统自动释放诊单医助。"
>
<el-icon class="ml-0.5 align-middle text-gray-400 cursor-help"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<template #default="{ row }">
<template v-if="!Number(row.po_assign_snapshot_hit)">
<span class="text-gray-400"></span>
</template>
<template v-else-if="Number(row.po_assign_is_release)">
<el-tag type="info" size="small">已释放</el-tag>
</template>
<template v-else-if="Number(row.po_assign_to_er_center)">
<el-tag type="success" size="small">
{{ row.po_assign_to_assistant_name || ('医助#' + row.po_assign_to_assistant_id) }}
</el-tag>
</template>
<template v-else>
<el-tag type="warning" size="small" effect="plain">
非二中心 {{ row.po_assign_to_assistant_name || ('医助#' + row.po_assign_to_assistant_id) }}
</el-tag>
</template>
</template>
</el-table-column>
<el-table-column label="操作" width="310" fixed="right">
<template #default="{ row }">
<div class="flex items-center gap-1 flex-wrap">
@@ -621,6 +659,41 @@
</template>
</el-alert>
<!-- 物流轨迹含拒收/退回等抽屉内强提示 -->
<el-alert
v-if="detailLogisticsUrgent.show && String(detailData?.tracking_number || '').trim()"
type="error"
effect="dark"
:closable="false"
show-icon
class="mb-4 !items-start ring-2 ring-red-600/90 shadow-lg"
title="物流异常:轨迹中出现拒收 / 退回等字样,请优先跟进处理"
>
<template #default>
<div class="text-sm leading-relaxed space-y-1.5">
<div v-if="detailLogisticsUrgent.keywords.length" class="flex flex-wrap gap-1.5 items-center">
<span class="text-red-100 text-xs">命中关键词</span>
<el-tag
v-for="k in detailLogisticsUrgent.keywords"
:key="k"
type="danger"
effect="plain"
size="small"
class="!border-white/40 !text-white"
>
{{ k }}
</el-tag>
</div>
<ul v-if="detailLogisticsUrgent.lines.length" class="list-disc pl-5 space-y-1 text-red-50">
<li v-for="(line, idx) in detailLogisticsUrgent.lines" :key="idx">{{ line }}</li>
</ul>
<div class="text-xs text-red-100/90 pt-1 border-t border-white/15">
请核实是否需改址重发拦截退件或协调患者签收并在备注中记录处理结果
</div>
</div>
</template>
</el-alert>
<div class="bg-gray-50/80 border border-gray-100 rounded-lg p-5 mb-5 shadow-sm">
<el-steps :active="workflowActiveStep" align-center finish-status="success">
<el-step title="业务订单创建" :description="formatTime(detailData.create_time)" />
@@ -911,11 +984,12 @@
<span class="text-red-500">¥{{ row.amount }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="82" align="center">
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="payOrderStatusTag(row.status)" size="small">
{{ row.status_desc || orderStatusText(row.status) }}
</el-tag>
<el-tag v-if="row.is_exempt === 1" type="warning" size="small" class="ml-1">豁免</el-tag>
</template>
</el-table-column>
<el-table-column prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
@@ -968,11 +1042,12 @@
<span class="text-red-500">¥{{ row.amount }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="82" align="center">
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="payOrderStatusTag(row.status)" size="small">
{{ row.status_desc || orderStatusText(row.status) }}
</el-tag>
<el-tag v-if="row.is_exempt === 1" type="warning" size="small" class="ml-1">豁免</el-tag>
</template>
</el-table-column>
<el-table-column prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
@@ -1124,10 +1199,25 @@
<el-card
v-if="String(detailData.tracking_number || '').trim()"
shadow="never"
class="po-panel po-panel-logistics border-gray-100"
class="po-panel po-panel-logistics"
:class="
detailLogisticsUrgent.show
? '!border-2 !border-red-500 shadow-md shadow-red-500/10'
: 'border-gray-100'
"
>
<template #header>
<span class="font-medium text-[15px]">物流轨迹</span>
<div class="flex items-center gap-2 flex-wrap">
<span class="font-medium text-[15px]">物流轨迹</span>
<el-tag
v-if="detailLogisticsUrgent.show"
type="danger"
effect="dark"
size="small"
>
拒收/退回 · 待处理
</el-tag>
</div>
</template>
<p class="po-logistics-tip text-gray-400 text-xs mb-3 font-normal">
优先读库无记录时走快递100顺丰会校验单号+电话快递100要求完整手机号更易通过默认预填订单收货手机若仍提示验证码错误请改成与顺丰面单一致的号码
@@ -1198,7 +1288,14 @@
:timestamp="t.time"
placement="top"
>
<span :class="idx === 0 ? 'text-gray-800 font-medium' : 'text-gray-500'">{{ t.context }}</span>
<span
:class="[
idx === 0 ? 'text-gray-800 font-medium' : 'text-gray-500',
logisticsTraceLineUrgent(t.context)
? '!text-red-700 font-semibold bg-red-50 px-1.5 py-0.5 rounded'
: ''
]"
>{{ t.context }}</span>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无轨迹节点记录" :image-size="64" />
@@ -1813,21 +1910,56 @@
width="480px"
:close-on-click-modal="false"
destroy-on-close
@closed="completeOrderId = 0"
@closed="onCompleteOrderDialogClosed"
>
<p class="text-sm text-gray-600 mb-3">请选择本单结案状态。选择「已完成」时,实付将按已关联的支付单金额汇总。</p>
<el-select
v-model="completeFulfillmentStatus"
class="w-full"
placeholder="请选择"
>
<el-option
v-for="opt in completeOrderStatusOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<div v-loading="completeOrderLogisticsLoading" class="min-h-[52px]">
<el-alert
v-if="completeOrderLogisticsUrgent.show"
type="error"
effect="dark"
:closable="false"
show-icon
class="mb-3 !items-start ring-2 ring-red-600/70"
title="物流异常:轨迹含拒收/退回等,请先核实处理再结案"
>
<template #default>
<div class="text-xs leading-relaxed space-y-1.5">
<div v-if="completeOrderLogisticsUrgent.keywords.length" class="flex flex-wrap gap-1 items-center">
<span class="text-red-100/90">命中</span>
<el-tag
v-for="k in completeOrderLogisticsUrgent.keywords"
:key="k"
type="danger"
effect="plain"
size="small"
class="!border-white/35 !text-white"
>
{{ k }}
</el-tag>
</div>
<ul v-if="completeOrderLogisticsUrgent.lines.length" class="list-disc pl-4 space-y-0.5 text-red-50">
<li v-for="(line, idx) in completeOrderLogisticsUrgent.lines" :key="idx">{{ line }}</li>
</ul>
<div class="text-red-100/85 border-t border-white/15 pt-1.5">
确认已协调拒收/退回后续重发退款改址等后再完成订单
</div>
</div>
</template>
</el-alert>
<p class="text-sm text-gray-600 mb-3">请选择本单结案状态选择已完成实付将按已关联的支付单金额汇总</p>
<el-select
v-model="completeFulfillmentStatus"
class="w-full"
placeholder="请选择"
>
<el-option
v-for="opt in completeOrderStatusOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</div>
<template #footer>
<el-button @click="completeOrderDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="completeOrderSubmitting" @click="submitCompleteOrder">
@@ -2373,7 +2505,7 @@
<script lang="ts" setup name="prescriptionOrderList">
import { computed, onMounted, reactive, ref, nextTick, watch, defineAsyncComponent } from 'vue'
import { useRoute } from 'vue-router'
import { Refresh, Loading, ArrowDown, InfoFilled, Search, Calendar, Document, Link as LinkIcon, Wallet } from '@element-plus/icons-vue'
import { Refresh, Loading, ArrowDown, InfoFilled, QuestionFilled, Search, Calendar, Document, Link as LinkIcon, Wallet } from '@element-plus/icons-vue'
import DaterangePicker from '@/components/daterange-picker/index.vue'
import {
prescriptionOrderAuditPayment,
@@ -2381,6 +2513,7 @@ import {
prescriptionOrderDetail,
prescriptionOrderEdit,
prescriptionOrderLists,
prescriptionOrderExport,
prescriptionOrderPaidPayOrders,
prescriptionOrderWithdraw,
prescriptionOrderLogisticsTrace,
@@ -2729,21 +2862,35 @@ function handleFocusBoardSearch(key: 'pendingRx' | 'pendingPay' | 'pendingShip'
resetPage()
}
async function fetchLists(params: Record<string, unknown>) {
function normalizePrescriptionOrderListQuery(params: Record<string, unknown>): Record<string, unknown> {
const p: Record<string, unknown> = { ...params }
if (p.prescription_id === '' || p.prescription_id === undefined) {
delete p.prescription_id
} else {
p.prescription_id = Number(p.prescription_id)
}
if (p.fulfillment_status === '' || p.fulfillment_status === undefined) {
if (p.fulfillment_status === '' || p.fulfillment_status === undefined || p.fulfillment_status === null) {
delete p.fulfillment_status
} else {
p.fulfillment_status = Number(p.fulfillment_status)
}
if (p.prescription_audit_status === '' || p.prescription_audit_status === undefined) {
if (
p.prescription_audit_status === '' ||
p.prescription_audit_status === undefined ||
p.prescription_audit_status === null
) {
delete p.prescription_audit_status
} else {
p.prescription_audit_status = Number(p.prescription_audit_status)
}
if (p.payment_slip_audit_status === '' || p.payment_slip_audit_status === undefined) {
if (
p.payment_slip_audit_status === '' ||
p.payment_slip_audit_status === undefined ||
p.payment_slip_audit_status === null
) {
delete p.payment_slip_audit_status
} else {
p.payment_slip_audit_status = Number(p.payment_slip_audit_status)
}
if (!p.supply_mode || p.supply_mode === '') {
delete p.supply_mode
@@ -2772,7 +2919,6 @@ async function fetchLists(params: Record<string, unknown>) {
delete p.start_time
delete p.end_time
} else {
// 当前页面按“日”筛选:请求接口时展开为整天区间,避免结束日只落在 00:00:00。
const st = String(p.start_time || '').trim()
const et = String(p.end_time || '').trim()
if (/^\d{4}-\d{2}-\d{2}$/.test(st) && /^\d{4}-\d{2}-\d{2}$/.test(et)) {
@@ -2780,9 +2926,16 @@ async function fetchLists(params: Record<string, unknown>) {
p.end_time = `${et} 23:59:59`
}
}
return p
}
async function fetchLists(params: Record<string, unknown>) {
const p = normalizePrescriptionOrderListQuery({ ...params })
return prescriptionOrderLists(p)
}
const prescriptionOrderExportParams = computed(() => normalizePrescriptionOrderListQuery({ ...queryParams }))
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: fetchLists,
params: queryParams
@@ -3532,6 +3685,75 @@ const logisticsTraceList = computed(() => {
return p.traces as Array<{ time: string; context: string }>
})
/** 物流轨迹中需人工立即跟进的常见异常关键词 */
const LOGISTICS_URGENT_KEYWORDS = [
'拒收',
'拒签',
'退件',
'客户拒收',
'拦截退回',
'退回发件',
'派件退回',
'无人签收',
'退回快件'
]
function logisticsStringHasUrgentKeyword(s: unknown): boolean {
const t = String(s ?? '')
return LOGISTICS_URGENT_KEYWORDS.some((k) => t.includes(k))
}
function logisticsTraceLineUrgent(context: unknown): boolean {
return logisticsStringHasUrgentKeyword(context)
}
/** 根据单次拉取的物流 payload 判断是否含拒收/退回等(详情抽屉、完成订单弹窗共用) */
function analyzeLogisticsPayloadUrgent(p: Record<string, any> | null | undefined): {
show: boolean
keywords: string[]
lines: string[]
} {
if (!p) {
return { show: false, keywords: [], lines: [] }
}
const traces = Array.isArray(p.traces)
? (p.traces as Array<{ time?: string; context?: string }>)
: []
const chunks: string[] = []
if (p.state_text) chunks.push(String(p.state_text))
if (p.hint) chunks.push(String(p.hint))
for (const row of traces) {
if (row.context) chunks.push(String(row.context))
}
const joined = chunks.join('\n')
const keywords = LOGISTICS_URGENT_KEYWORDS.filter((k) => joined.includes(k))
if (!keywords.length) {
return { show: false, keywords: [], lines: [] }
}
const lines: string[] = []
for (const row of traces) {
const ctx = String(row.context || '')
if (logisticsStringHasUrgentKeyword(ctx)) {
lines.push(ctx)
if (lines.length >= 5) break
}
}
if (!lines.length && logisticsStringHasUrgentKeyword(p.state_text)) {
lines.push(`最新状态:${p.state_text}`)
}
if (!lines.length && logisticsStringHasUrgentKeyword(p.hint)) {
lines.push(String(p.hint))
}
return { show: true, keywords, lines }
}
/** 详情抽屉:轨迹/状态中是否出现拒收等字样,用于顶部与物流卡片强提示 */
const detailLogisticsUrgent = computed(() => analyzeLogisticsPayloadUrgent(logisticsTracePayload.value))
const completeOrderLogisticsPayload = ref<Record<string, any> | null>(null)
const completeOrderLogisticsLoading = ref(false)
const completeOrderLogisticsUrgent = computed(() => analyzeLogisticsPayloadUrgent(completeOrderLogisticsPayload.value))
function expressCompanyLabel(v: unknown) {
const s = String(v || '').toLowerCase()
if (s === 'sf') return '顺丰速运'
@@ -4242,6 +4464,61 @@ async function testGancaoPreviewFromDetail() {
if (d && d.success) {
gancaoPreviewData.value = d
gancaoPreviewDrawerVisible.value = true
// ( + + ) internal_cost canViewInternalCost
if (canViewFinanceFields() && d.fee) {
const internalTotal = calculateTotalFee(d.fee)
try {
const resDetail: any = await prescriptionOrderDetail({ id: detailData.value.id })
const od = resDetail?.data ?? resDetail
if (od?.id) {
const pkgRaw = od.service_package
const servicePackageStr = Array.isArray(pkgRaw)
? pkgRaw.join(',')
: String(pkgRaw || '')
await prescriptionOrderEdit({
id: od.id,
recipient_name: od.recipient_name || '',
recipient_phone: od.recipient_phone || '',
shipping_province: od.shipping_province || '',
shipping_city: od.shipping_city || '',
shipping_district: od.shipping_district || '',
shipping_address: od.shipping_address || '',
is_follow_up: od.is_follow_up ? 1 : 0,
medication_days:
od.medication_days != null && String(od.medication_days).trim() !== ''
? od.medication_days
: '',
dose_unit: od.dose_unit || '剂',
dose_count: od.dose_count > 0 ? od.dose_count : 1,
prev_staff: od.prev_staff || '',
service_channel: od.service_channel || '',
service_package: servicePackageStr,
express_company: od.express_company || 'auto',
tracking_number: od.tracking_number || '',
fee_type: Number(od.fee_type) || 3,
amount: Number(od.amount) || 0,
remark_extra: od.remark_extra || '',
remark_assistant: od.remark_assistant || '',
internal_cost: internalTotal,
pay_order_ids: Array.isArray(od.pay_order_ids) ? od.pay_order_ids : []
})
feedback.msgSuccess('内部成本已按预报价总计更新')
getLists()
if (detailVisible.value && Number(detailData.value?.id) === Number(od.id)) {
try {
const r: any = await prescriptionOrderDetail({ id: od.id })
const nd = r?.data ?? r ?? null
if (nd) detailData.value = nd
} catch {
detailData.value = { ...detailData.value, internal_cost: internalTotal }
}
}
}
} catch {
/* 拦截器已提示;仍保留抽屉中的预报价结果 */
}
}
//feedback.msgSuccess('')
} else {
// feedback.msgError(d?.error || '')
@@ -4439,6 +4716,37 @@ const completeOrderId = ref(0)
const completeFulfillmentStatus = ref<number>(3)
const completeOrderSubmitting = ref(false)
function onCompleteOrderDialogClosed() {
completeOrderId.value = 0
completeOrderLogisticsPayload.value = null
completeOrderLogisticsLoading.value = false
}
async function fetchCompleteOrderLogisticsForDialog(row: {
id: number
express_company?: unknown
recipient_phone?: unknown
}) {
completeOrderLogisticsLoading.value = true
completeOrderLogisticsPayload.value = null
try {
const digits = String(row.recipient_phone || '').replace(/\D/g, '')
const params: { id: number; express_company?: string; phone_tail?: string } = {
id: row.id,
express_company: String(row.express_company || 'auto') || 'auto'
}
if (digits.length >= 4) {
params.phone_tail = digits
}
const res: any = await prescriptionOrderLogisticsTrace(params)
completeOrderLogisticsPayload.value = (res?.data ?? res) as Record<string, any>
} catch {
completeOrderLogisticsPayload.value = null
} finally {
completeOrderLogisticsLoading.value = false
}
}
async function submitCompleteOrder() {
const id = completeOrderId.value
if (!id) return
@@ -4601,10 +4909,19 @@ async function submitAddPayOrder() {
}
// / +
function confirmComplete(row: { id: number }) {
function confirmComplete(row: {
id: number
tracking_number?: unknown
express_company?: unknown
recipient_phone?: unknown
}) {
completeOrderId.value = row.id
completeFulfillmentStatus.value = 3
completeOrderLogisticsPayload.value = null
completeOrderDialogVisible.value = true
if (String(row.tracking_number || '').trim()) {
void fetchCompleteOrderLogisticsForDialog(row)
}
}
//
@@ -925,11 +925,12 @@
<span class="text-red-500">¥{{ row.amount }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="82" align="center">
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="payOrderStatusTag(row.status)" size="small">
{{ row.status_desc || orderStatusText(row.status) }}
</el-tag>
<el-tag v-if="row.is_exempt === 1" type="warning" size="small" class="ml-1">豁免</el-tag>
</template>
</el-table-column>
<el-table-column prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
@@ -982,11 +983,12 @@
<span class="text-red-500">¥{{ row.amount }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="82" align="center">
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="payOrderStatusTag(row.status)" size="small">
{{ row.status_desc || orderStatusText(row.status) }}
</el-tag>
<el-tag v-if="row.is_exempt === 1" type="warning" size="small" class="ml-1">豁免</el-tag>
</template>
</el-table-column>
<el-table-column prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
File diff suppressed because it is too large Load Diff
+39 -2
View File
@@ -102,6 +102,18 @@
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item label="添加时间">
<el-date-picker
v-model="addTimeRange"
type="daterange"
value-format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
clearable
class="!w-[280px]"
@change="onAddTimeRangeChange"
/>
</el-form-item>
<el-form-item label="标签">
<el-select
v-model="queryParams.tag_ids"
@@ -557,12 +569,34 @@ const syncSettings = reactive({
interval: 3600
})
const queryParams = reactive<{ name: string; follow_user: string; tag_ids: string[] }>({
const queryParams = reactive<{
name: string
follow_user: string
tag_ids: string[]
add_time_start: string
add_time_end: string
}>({
name: '',
follow_user: '',
tag_ids: []
tag_ids: [],
add_time_start: '',
add_time_end: ''
})
/** 与列表「添加时间」列口径一致(库内 external_first_add_time / create_time */
const addTimeRange = ref<[string, string] | null>(null)
function onAddTimeRangeChange(val: [string, string] | null) {
if (val && val.length === 2 && val[0] && val[1]) {
queryParams.add_time_start = val[0]
queryParams.add_time_end = val[1]
} else {
queryParams.add_time_start = ''
queryParams.add_time_end = ''
}
resetPage()
}
// ── 标签维度(筛选下拉 + 抽屉面板共用同一份数据) ──────────────────────────
interface TagItem {
tag_id: string
@@ -793,6 +827,9 @@ function handleReset() {
queryParams.name = ''
queryParams.follow_user = ''
queryParams.tag_ids = []
queryParams.add_time_start = ''
queryParams.add_time_end = ''
addTimeRange.value = null
resetParams()
}
+438 -246
View File
@@ -290,12 +290,12 @@
</table>
</div>
<div class="yeji-table__foot yeji-table__foot--left">
<span>诊金单位 · 被指派数与部门业绩表同口径 · 接诊率 = 诊金 ÷ 进线/进线</span>
<span>诊金单位 · 诊金/接诊诊单/复诊均按订单创建人 = 该医助归属与部门合计业绩 / 接诊诊单 / 复诊同口径 · 被指派数与部门业绩表同口径 · 接诊率 = 诊金 ÷ 进线/进线</span>
<span v-if="lb.er_center_subtree">
· 复诊列为二中心口径同一诊单业务单序列第 2 笔起点击数字查看对应业务订单
</span>
<span>
· 预约诊单预约日期在统计区间内状态含已预约/已完成/已过号不含已取消医助归属与接诊单数一致各科组展示全部医助
· 预约诊单预约日期在统计区间内状态含已预约/已完成/已过号不含已取消医助归属与接诊单数一致按挂号 assistant_id 优先否则诊单 assistant_id各科组展示全部医助
</span>
</div>
</div>
@@ -317,75 +317,7 @@
</el-tab-pane>
<el-tab-pane label="甄养堂互联网医院诊金" name="zyyt" lazy>
<p class="yeji-zyyt-intro">
各中心部门诊金业绩进线与 ROI 与顶部渠道展示部门日期筛选联动本月 / 本周 / 今日等多张业绩表中预约诊单列数字可点开查看每条挂号记录
</p>
<!-- <el-alert
v-if="channelFilterNote"
:title="channelFilterNote"
type="info"
show-icon
:closable="false"
class="filter-alert"
/> -->
<!-- 目标进度卡片 -->
<div
v-if="targetProgressCard || targetProgressLoading"
v-loading="targetProgressLoading"
class="tp-wrap"
>
<div v-if="targetProgressCard" class="tp-card">
<div class="tp-card__title">{{ targetProgressCard.title }}</div>
<div class="tp-card__body">
<div
v-for="sec in targetProgressCard.sections"
:key="sec.dept_id"
class="tp-section"
>
<div class="tp-section__head">
<div class="tp-section__title-col">
<span v-if="sec.parent_dept_name" class="tp-section__parent">{{
sec.parent_dept_name
}}</span>
<span class="tp-section__name">{{ sec.dept_name }}目标</span>
</div>
<span class="tp-section__target">{{ formatMoneyW(sec.target_amount) }}</span>
</div>
<table class="tp-table">
<tbody>
<tr>
<td class="tp-lbl">昨日诊金</td>
<td class="tp-val">{{ formatInt(sec.yesterday_amount) }}</td>
</tr>
<tr v-if="sec.douyin_amount !== null">
<td class="tp-lbl">抖音总诊金</td>
<td class="tp-val">{{ formatInt(sec.douyin_amount) }}</td>
</tr>
<tr v-if="sec.tuifen_amount !== null">
<td class="tp-lbl">推粉总诊金</td>
<td class="tp-val">{{ formatInt(sec.tuifen_amount) }}</td>
</tr>
<tr>
<td class="tp-lbl">总诊金</td>
<td class="tp-val tp-val--total">{{ formatInt(sec.total_amount) }}</td>
</tr>
<tr>
<td class="tp-lbl">目标进度</td>
<td class="tp-val">{{ formatMoney(sec.target_progress) }}</td>
</tr>
<tr>
<td class="tp-lbl">完成进度</td>
<td class="tp-val" :class="achievementClass(sec.completion_rate)">{{ formatAchievement(sec.completion_rate) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 业绩表4 /N -->
<!-- 业绩表4 /N 目标看板见目标看板卡片Tab -->
<div v-loading="loading" class="tables-wrap">
<div v-if="!loading && tables.length === 0" class="empty-tip">
<el-empty description="暂无数据" />
@@ -438,7 +370,7 @@
<el-tooltip placement="top" effect="dark" :show-after="200">
<template #content>
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
<b>名称含二中心</b>的部门及其组织下级与本表区间一致按患者业务单 <b>create_time</b> 排序<b> 1 笔不计</b> 2 笔起计入合计分列复诊2复诊三含本部门及下级汇总订单条件同合计业绩非取消非二中心子树部门为 0
<b>名称含二中心</b>的部门及其组织下级候选订单为有诊单医助 <b>fulfillment_status</b> 4/9/10<b>NULL 计入</b>的业务单按患者 <b>create_time</b> 排序<b> 1 笔不计</b> 2 笔起计入合计分列复诊2复诊三<b>归属</b>按订单创建人的人事部门<b>合计业绩</b>列同口径非二中心子树部门为 0
</div>
</template>
<el-icon class="col-info"><InfoFilled /></el-icon>
@@ -463,8 +395,9 @@
<el-tooltip placement="top" effect="dark" :show-after="200">
<template #content>
<div style="max-width: 300px; line-height: 1.7; font-size: 12px">
按订单<b>创建时间</b>统计剔除履约已取消金额与<b>接诊诊单</b>列一致诊单<b>医助</b>或订单<b>创建人</b>的人事部门落在本展示行含下级计入多行命中时取最深的展示部门<br />
<b>不受渠道筛选影响</b>与侧栏按部门打开的业务订单列表对齐
按订单<b>创建时间</b>统计剔除履约已取消/拒收/退款 4·9·10金额与<b>接诊诊单</b>列一致仅按<b>订单创建人</b>的人事部门落在本展示行含下级计入多行命中时取最深的展示部门<b>医助排行榜诊金</b>同口径<br />
<b>不受渠道筛选影响</b>与侧栏按部门打开的业务订单列表对齐<br />
点击金额可查看对应业务订单明细侧栏默认合计业绩全量列表
</div>
</template>
<el-icon class="col-info"><InfoFilled /></el-icon>
@@ -476,8 +409,9 @@
<template #content>
<div style="max-width: 340px; line-height: 1.7; font-size: 12px">
{{ tb.channel_name }}渠道业绩与卡片标题渠道一致处方订单列表业绩<b>同口径</b>按订单
<b>create_time</b> 归日剔除履约已取消(4)<br />
渠道判定用 EXISTS该患者有任一挂号 <b>channels 命中字典 + status=3</b>不限挂号时点
<b>create_time</b> 归日剔除履约状态 4已取消/9拒收/10退款<br />
渠道判定用 EXISTS该患者有任一挂号 <b>channels 命中字典 + status=3</b>不限挂号时点<br />
点击金额可查看该部门本渠道订单明细侧栏渠道筛选列表
</div>
</template>
<el-icon class="col-info" aria-label="说明"><InfoFilled /></el-icon>
@@ -511,8 +445,12 @@
<el-tooltip placement="top" effect="dark" :show-after="200">
<template #content>
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
<b>业务订单条数</b>非取消订单 <b>create_time</b> 落入区间<b>fulfillment_status 4</b>与列表筛选
<b>assistant_dept_id</b> 时一致 <b>医助</b>或订单 <b>创建人</b>的人事部门落在该部门子树即计入表格多行命中时取最深的展示部门侧栏勾选与表格业绩对齐时的集合可能略有差异选定渠道时本列仍为全量
<<<<<<< HEAD
<b>业务订单条数</b>计业绩 <b>create_time</b> 落入区间<b>fulfillment_status {4,9,10}</b><b>NULL 计入</b><b>订单创建人</b>的人事部门落在该部门子树即计入表格多行命中时取最深的展示部门<b>合计业绩 / 医助排行榜接诊诊单</b>同口径选定渠道时本列仍为全量
=======
<b>业务订单条数</b>计业绩订单 <b>create_time</b> 落入区间<b>fulfillment_status {4,9,10}</b>与列表筛选
<b>assistant_dept_id</b> 时一致<b>创建人</b>人事部门优先无创建人则诊单 <b>医助</b>落在该部门子树即计入表格多行命中时取最深的展示部门与侧栏勾选与表格业绩对齐时的集合可能略有差异选定渠道时本列仍为全量
>>>>>>> master
</div>
</template>
<el-icon class="col-info"><InfoFilled /></el-icon>
@@ -523,7 +461,7 @@
<el-tooltip placement="top" effect="dark" :show-after="200">
<template #content>
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
{{ tb.channel_name }}渠道成交单数与渠道业绩同源区间内 <b>非取消业务订单</b> 条数按订单
{{ tb.channel_name }}渠道成交单数与渠道业绩同源区间内 <b>计业绩的业务订单</b> 条数剔除 4/9/10按订单
<b>create_time</b> 归日渠道判定同业绩列
</div>
</template>
@@ -611,8 +549,25 @@
>{{ formatInt(yejiRevisitSlotAt(r, slot)) }}</span>
<template v-else>{{ formatInt(yejiRevisitSlotAt(r, slot)) }}</template>
</td>
<td>{{ formatMoney(r.performance_amount) }}</td>
<td v-if="tb.channel_name" class="col-completed">{{ formatMoney(r.completed_performance_amount) }}</td>
<td>
<span
v-if="yejiPerfCellDrawerEligible(tb, r, 'total')"
class="yeji-lead-cell--link"
@click.stop="onYejiPerformanceCellClick(tb, r, 'total')"
>{{ formatMoney(r.performance_amount) }}</span>
<template v-else>{{ formatMoney(r.performance_amount) }}</template>
</td>
<td
v-if="tb.channel_name"
class="col-completed"
>
<span
v-if="yejiPerfCellDrawerEligible(tb, r, 'channel')"
class="yeji-lead-cell--link"
@click.stop="onYejiPerformanceCellClick(tb, r, 'channel')"
>{{ formatMoney(r.completed_performance_amount) }}</span>
<template v-else>{{ formatMoney(r.completed_performance_amount) }}</template>
</td>
<td @click.stop="onYejiDeptAppointmentBookedClick(tb, r)">
<span
v-if="
@@ -639,8 +594,25 @@
v-for="slot in yejiRevisitSlotColumns"
:key="'yjvt-' + idx + '-' + slot"
>{{ formatInt(yejiRevisitSlotAt(tb.total, slot)) }}</td>
<td>{{ formatMoney(tb.total.performance_amount) }}</td>
<td v-if="tb.channel_name" class="col-completed">{{ formatMoney(tb.total.completed_performance_amount) }}</td>
<td>
<span
v-if="yejiPerfTotalRowDrawerEligible(tb, 'total')"
class="yeji-lead-cell--link"
@click.stop="onYejiPerformanceTotalRowClick(tb, 'total')"
>{{ formatMoney(tb.total.performance_amount) }}</span>
<template v-else>{{ formatMoney(tb.total.performance_amount) }}</template>
</td>
<td
v-if="tb.channel_name"
class="col-completed"
>
<span
v-if="yejiPerfTotalRowDrawerEligible(tb, 'channel')"
class="yeji-lead-cell--link"
@click.stop="onYejiPerformanceTotalRowClick(tb, 'channel')"
>{{ formatMoney(tb.total.completed_performance_amount) }}</span>
<template v-else>{{ formatMoney(tb.total.completed_performance_amount) }}</template>
</td>
<td>{{ formatInt(tb.total.appointment_booked_count ?? 0) }}</td>
<td>{{ formatInt(tb.total.consult_count) }}</td>
<td>{{ formatInt(tb.total.deal_order_count) }}</td>
@@ -663,6 +635,72 @@
</div>
</el-tab-pane>
<el-tab-pane label="目标看板(卡片)" name="zyyt_matrix" lazy>
<div v-loading="targetProgressLoading" class="tp-matrix-wrap">
<template v-if="targetProgressCard">
<h2 class="tp-matrix__title">{{ targetProgressCard.title }}</h2>
<div class="tp-matrix" role="list">
<div
v-for="sec in targetProgressCard.sections"
:key="'tm-' + sec.dept_id"
class="tp-matrix-card"
role="listitem"
>
<div class="tp-matrix-card__head">
<div class="tp-matrix-card__titles">
<span v-if="sec.parent_dept_name" class="tp-matrix-card__parent">{{
sec.parent_dept_name
}}</span>
<span class="tp-matrix-card__name">{{ sec.dept_name }}目标</span>
</div>
<span class="tp-matrix-card__target">{{ formatMoneyW(sec.target_amount) }}</span>
</div>
<table class="tp-matrix-card__table">
<tbody>
<tr>
<td class="tp-matrix-card__lbl">昨日诊金</td>
<td class="tp-matrix-card__num">{{ formatInt(sec.yesterday_amount) }}</td>
</tr>
<tr v-if="sec.douyin_amount !== null">
<td class="tp-matrix-card__lbl">抖音总诊金</td>
<td class="tp-matrix-card__num">{{ formatInt(sec.douyin_amount) }}</td>
</tr>
<tr v-if="sec.tuifen_amount !== null">
<td class="tp-matrix-card__lbl">推粉总诊金</td>
<td class="tp-matrix-card__num">{{ formatInt(sec.tuifen_amount) }}</td>
</tr>
<tr>
<td class="tp-matrix-card__lbl">总诊金</td>
<td class="tp-matrix-card__num tp-matrix-card__num--hot">
{{ formatInt(sec.total_amount) }}
</td>
</tr>
<tr>
<td class="tp-matrix-card__lbl">目标进度</td>
<td class="tp-matrix-card__num">{{ formatMoney(sec.target_progress) }}</td>
</tr>
<tr>
<td class="tp-matrix-card__lbl">完成进度</td>
<td
class="tp-matrix-card__num tp-matrix-card__num--hot"
:class="achievementClass(sec.completion_rate)"
>
{{ formatAchievement(sec.completion_rate) }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<el-empty
v-else-if="!targetProgressLoading"
class="tp-matrix-empty"
description="暂无目标数据,请先配置当月部门目标或为部门设置诊金目标后刷新页面"
/>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
@@ -936,7 +974,7 @@
>
<span class="yeji-order-drawer__stat-label">
业务订单金额合计
<span class="yeji-order-drawer__stat-note">(不含履约已取消,与上方业绩口径一致)</span>
<span class="yeji-order-drawer__stat-note">(不含履约 4·9·10,与上方业绩口径一致)</span>
</span>
<span class="yeji-order-drawer__stat-value yeji-order-drawer__stat-value--money">
¥{{ formatOrderDrawerMoney(orderDrawerTotalAmount) }}
@@ -1047,6 +1085,7 @@
</template>
</el-table-column>
<el-table-column prop="doctor_name" label="开方人" width="88" show-overflow-tooltip />
<el-table-column prop="assistant_name" label="诊单医助" width="88" show-overflow-tooltip />
<el-table-column prop="creator_name" label="创建人" width="88" show-overflow-tooltip />
</el-table>
<el-empty v-if="!orderDrawerLoading && orderDrawerLists.length === 0" description="暂无业务订单" />
@@ -1197,6 +1236,7 @@
<el-table-column prop="patient_name" label="患者" min-width="92" show-overflow-tooltip />
<el-table-column prop="patient_phone" label="手机" min-width="116" show-overflow-tooltip />
<el-table-column prop="doctor_name" label="接诊医生" width="92" show-overflow-tooltip />
<el-table-column prop="assistant_name" label="医助" width="88" show-overflow-tooltip />
<el-table-column label="诊单ID" width="84" align="right">
<template #default="{ row }">{{ formatInt(row.diagnosis_id ?? 0) }}</template>
</el-table-column>
@@ -1279,6 +1319,7 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import axios from 'axios'
import { Search, RefreshRight, InfoFilled } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import vCharts from 'vue-echarts'
@@ -1623,7 +1664,7 @@ const appointmentLinesShowChannelColumn = computed(() =>
)
/** 图表 / 数据表切换 */
const yejiDisplayTab = ref<'doctor' | 'leaderboard' | 'zyyt'>('zyyt')
const yejiDisplayTab = ref<'doctor' | 'leaderboard' | 'zyyt' | 'zyyt_matrix'>('zyyt')
/** 与菜单 stats.doctorDailyStats/overview 一致;无权限则不展示医生统计表/图、不请求接口 */
const userStore = useUserStore()
@@ -2040,7 +2081,9 @@ async function loadDoctorDailyStats() {
if (res?.start_date && res?.end_date) {
doctorDailyRange.value = { start: res.start_date, end: res.end_date }
}
} catch (_e) {
} catch (e: unknown) {
// 与业绩主接口并发时,同源请求键相同会触发 axios 重复请求取消;勿清空状态以免覆盖后到的成功结果
if (axios.isCancel(e)) return
doctorDailyRows.value = []
doctorDailyTotal.value = {}
doctorDailyRange.value = null
@@ -2069,7 +2112,15 @@ const orderDrawerSplitPerfCache = ref<{
} | null>(null)
type OrderDrawerFilter =
| { mode: 'dept'; deptId: number; deptName: string; start: string; end: string }
| {
mode: 'dept'
deptId: number
deptName: string
start: string
end: string
/** 当前业绩表各展示行 dept_id,用于侧栏与「接诊诊单」列对齐(创建人优先 + 最深展示行) */
yejiTableRowDeptIds?: number[]
}
| {
mode: 'assistant'
assistantId: number
@@ -2081,7 +2132,7 @@ type OrderDrawerFilter =
}
const orderDrawerFilter = ref<OrderDrawerFilter | null>(null)
/** 列表接口 extend,含 stats_order_amount_performance(与当前筛选、不含已取消口径一致) */
/** 列表接口 extend,含 stats_order_amount_performance(与当前筛选一致:剔除履约 4/9/10 */
const orderDrawerExtend = ref<Record<string, any> | null>(null)
const orderDrawerTotalAmount = computed(() => {
@@ -2429,7 +2480,8 @@ async function loadLeaderboard(range: { start: string; end: string }) {
range_note: res.range_note || '',
leaderboards: res.leaderboards || [],
}
} catch (_e) {
} catch (e: unknown) {
if (axios.isCancel(e)) return
leaderboardBlock.value = null
} finally {
leaderboardsLoading.value = false
@@ -2484,8 +2536,10 @@ async function loadData() {
}))
const ymd = formatLocalYmd(new Date())
await loadLeaderboard({ start: ymd, end: ymd })
} catch (e: any) {
ElMessage.error(e?.msg || e?.message || '加载失败')
} catch (e: unknown) {
if (axios.isCancel(e)) return
const any = e as any
ElMessage.error(any?.msg || any?.message || '加载失败')
} finally {
loading.value = false
if (canViewDoctorDailyStats.value) {
@@ -2586,40 +2640,82 @@ function formatOrderDrawerTime(t: unknown): string {
return s.length >= 16 ? s.slice(0, 16) : s
}
async function fetchOrderDrawerPage() {
/** 业务订单侧栏列表筛选(不含分页) */
function buildOrderDrawerListParams(): Record<string, any> | null {
const f = orderDrawerFilter.value
if (!f) return
if (!f) return null
const params: Record<string, any> = {
start_time: `${f.start} 00:00:00`,
end_time: `${f.end} 23:59:59`,
/** 与业绩列口径一致:不列出履约 4(已取消)/9(拒收)/10(退款) */
exclude_fulfillment_cancelled: 1,
/**
* 业绩看板入口标志:后端 PrescriptionOrderLists::applyDoctorAssistantFilters 看到此标志后,
* assistant_id / assistant_dept_id 一律按订单创建人收窄(与表格诊金/合计业绩/复诊同口径),
* 而非默认的「创建人 ∪ 诊单医助」并集。同时启用 extend 字段(约诊数、合计业绩对照)。
*/
yeji_order_drawer: 1,
}
if (f.mode === 'dept') {
params.assistant_dept_id = f.deptId
if (selectedDeptIds.value.length > 0) {
params.dept_ids = selectedDeptIds.value.join(',')
}
} else {
params.assistant_id = f.assistantId
if (f.erCenterRevisitSlot !== undefined) {
params.yeji_er_center_revisit_only = 1
params.yeji_er_center_revisit_slot = f.erCenterRevisitSlot
}
}
if (
selectedChannel.value &&
orderDrawerVisible.value &&
orderDrawerListTab.value === 'channel'
) {
params.channel_code = selectedChannel.value
}
return params
}
async function hydrateOrderDrawerSplitPerfCache() {
const base = buildOrderDrawerListParams()
if (!base || !selectedChannel.value) return
const params: Record<string, any> = {
...base,
channel_code: selectedChannel.value,
page_no: 1,
page_size: 1,
}
try {
const res: any = await prescriptionOrderLists(params)
const ex = res?.extend && typeof res.extend === 'object' ? res.extend : null
if (ex && ex.stats_yeji_drawer_total_performance_amount != null) {
orderDrawerSplitPerfCache.value = {
totalAmount: Number(ex.stats_yeji_drawer_total_performance_amount),
totalOrderCount: Number(ex.stats_yeji_drawer_total_performance_order_count ?? 0),
channelAmount: Number(ex.stats_order_amount_performance ?? 0),
channelOrderCount: Number(res?.count ?? 0),
}
}
} catch {
// 对照指标失败不打断列表
}
}
async function fetchOrderDrawerPage() {
const base = buildOrderDrawerListParams()
if (!base) return
if (selectedChannel.value && orderDrawerListTab.value === 'total') {
orderDrawerSplitPerfCache.value = null
}
orderDrawerLoading.value = true
try {
const params: Record<string, any> = {
...base,
page_no: orderDrawerPage.value,
page_size: orderDrawerPageSize.value,
start_time: `${f.start} 00:00:00`,
end_time: `${f.end} 23:59:59`,
/** 与业绩列口径一致:不列出履约已取消(4) */
exclude_fulfillment_cancelled: 1,
/** 侧栏展示约诊数等 extend 字段 */
yeji_order_drawer: 1,
}
if (f.mode === 'dept') {
params.assistant_dept_id = f.deptId
/** 与看板「接诊诊单」列一致:处方列表默认 assistant_dept_id(诊单医助或创建人∪子树),不用业绩摊行医助集合 */
if (selectedDeptIds.value.length > 0) {
params.dept_ids = selectedDeptIds.value.join(',')
}
} else {
params.assistant_id = f.assistantId
if (f.erCenterRevisitSlot !== undefined) {
params.yeji_er_center_revisit_only = 1
params.yeji_er_center_revisit_slot = f.erCenterRevisitSlot
}
}
if (
selectedChannel.value &&
orderDrawerChannelSplitActive.value &&
orderDrawerListTab.value === 'channel'
) {
params.channel_code = selectedChannel.value
}
const res: any = await prescriptionOrderLists(params)
orderDrawerLists.value = Array.isArray(res?.lists) ? res.lists : []
@@ -2640,8 +2736,17 @@ async function fetchOrderDrawerPage() {
channelOrderCount: Number(res?.count ?? 0),
}
}
} catch (e: any) {
ElMessage.error(e?.msg || e?.message || '加载业务订单失败')
if (
selectedChannel.value &&
orderDrawerVisible.value &&
orderDrawerListTab.value === 'total'
) {
await hydrateOrderDrawerSplitPerfCache()
}
} catch (e: unknown) {
if (axios.isCancel(e)) return
const any = e as any
ElMessage.error(any?.msg || any?.message || '加载业务订单失败')
orderDrawerLists.value = []
orderDrawerCount.value = 0
orderDrawerExtend.value = null
@@ -2650,8 +2755,9 @@ async function fetchOrderDrawerPage() {
}
}
function openOrderDrawerByDept(tb: YejiTable, row: YejiRow) {
orderDrawerListTab.value = 'channel'
function openOrderDrawerByDept(tb: YejiTable, row: YejiRow, opts?: { listTab?: 'total' | 'channel' }) {
const channelSplit = Boolean(selectedChannel.value && tb.channel_name)
orderDrawerListTab.value = channelSplit ? opts?.listTab ?? 'channel' : 'channel'
orderDrawerSplitPerfCache.value = null
orderDrawerFilter.value = {
mode: 'dept',
@@ -2659,14 +2765,79 @@ function openOrderDrawerByDept(tb: YejiTable, row: YejiRow) {
deptName: row.dept_name,
start: tb.start_date,
end: tb.end_date,
yejiTableRowDeptIds: tb.rows.map(r => r.dept_id).filter((id): id is number => typeof id === 'number' && id > 0),
}
orderDrawerTitle.value = `业务订单 · ${row.dept_name}`
orderDrawerHint.value = `创建时间:${tb.start_date} ${tb.end_date} · 与「接诊诊单」同口径:诊单医助或订单创建人落在本部门(含下级)· 不含履约已取消`
orderDrawerHint.value = `创建时间:${tb.start_date} ${tb.end_date} · 与「合计业绩 / 接诊诊单」同口径:订单创建人人事部门 ∈ 本部门子树 · 不含履约 4/9/10`
orderDrawerPage.value = 1
orderDrawerVisible.value = true
void fetchOrderDrawerPage()
}
/** 顶部「展示部门」仅选中 1 个根节点时:合计行业绩对应该子树下的订单汇总 */
function openOrderDrawerByYejiTableScope(
tb: YejiTable,
rootDeptId: number,
listTab: 'total' | 'channel'
) {
const channelSplit = Boolean(selectedChannel.value && tb.channel_name)
orderDrawerListTab.value = channelSplit ? listTab : 'channel'
orderDrawerSplitPerfCache.value = null
orderDrawerFilter.value = {
mode: 'dept',
deptId: rootDeptId,
deptName: '当前展示合计',
start: tb.start_date,
end: tb.end_date,
yejiTableRowDeptIds: tb.rows.map(r => r.dept_id).filter((id): id is number => typeof id === 'number' && id > 0),
}
orderDrawerTitle.value = '业务订单 · 当前展示合计'
orderDrawerHint.value = `创建时间:${tb.start_date} ${tb.end_date} · 已选展示部门 1 个根节点(含其下全部展示行)· 不含履约 4/9/10`
orderDrawerPage.value = 1
orderDrawerVisible.value = true
void fetchOrderDrawerPage()
}
function yejiPerfCellDrawerEligible(tb: YejiTable, row: YejiRow, kind: 'total' | 'channel'): boolean {
if (isUnassignedYejiRow(row) || row.dept_id <= 0) {
return false
}
if (kind === 'channel' && !tb.channel_name) {
return false
}
return true
}
function yejiPerfTotalRowDrawerEligible(tb: YejiTable, kind: 'total' | 'channel'): boolean {
if (selectedDeptIds.value.length !== 1) {
return false
}
if (kind === 'channel' && !tb.channel_name) {
return false
}
return true
}
function onYejiPerformanceCellClick(tb: YejiTable, row: YejiRow, kind: 'total' | 'channel') {
if (!yejiPerfCellDrawerEligible(tb, row, kind)) {
return
}
const listTab: 'total' | 'channel' = kind === 'total' ? 'total' : 'channel'
openOrderDrawerByDept(tb, row, { listTab })
}
function onYejiPerformanceTotalRowClick(tb: YejiTable, kind: 'total' | 'channel') {
if (!yejiPerfTotalRowDrawerEligible(tb, kind)) {
return
}
const rootDeptId = selectedDeptIds.value[0]
if (!rootDeptId) {
return
}
const listTab: 'total' | 'channel' = kind === 'total' ? 'total' : 'channel'
openOrderDrawerByYejiTableScope(tb, rootDeptId, listTab)
}
function openOrderDrawerByAssistant(row: LeaderboardPack['leaderboards'][0]['rows'][0], start: string, end: string) {
orderDrawerListTab.value = 'channel'
orderDrawerSplitPerfCache.value = null
@@ -2678,7 +2849,7 @@ function openOrderDrawerByAssistant(row: LeaderboardPack['leaderboards'][0]['row
end,
}
orderDrawerTitle.value = `业务订单 · ${row.name}`
orderDrawerHint.value = `创建时间:${start} ${end} · 诊单医助 · 不含履约已取消`
orderDrawerHint.value = `创建时间:${start} ${end} · 创建人=${row.name}(与排行榜诊金同口径)· 不含履约 4/9/10`
orderDrawerPage.value = 1
orderDrawerVisible.value = true
void fetchOrderDrawerPage()
@@ -2702,7 +2873,7 @@ function openOrderDrawerByAssistantErCenterRevisit(
}
const slotLbl = revisitSlot === 0 ? '复诊合计' : yejiRevisitSlotColumnTitle(revisitSlot)
orderDrawerTitle.value = `业务订单 · ${assistant.name} · ${slotLbl}`
orderDrawerHint.value = `创建时间:${start} ${end} · 诊单医助 · 仅二中心复诊口径 · ${slotLbl} · 不含履约已取消`
orderDrawerHint.value = `创建时间:${start} ${end} · 创建人=${assistant.name}(与部门表 / 排行榜复诊同口径)· 仅二中心复诊口径 · ${slotLbl} · 不含履约 4/9/10`
orderDrawerPage.value = 1
orderDrawerVisible.value = true
void fetchOrderDrawerPage()
@@ -2815,8 +2986,10 @@ async function openYejiRevisitDeptBreakdown(tb: YejiTable, row: YejiRow, revisit
const res: any = await yejiStatsRevisitBreakdown(p as any)
revisitBreakdownRows.value = Array.isArray(res?.rows) ? res.rows : []
revisitBreakdownNote.value = typeof res?.note === 'string' ? res.note : ''
} catch (e: any) {
ElMessage.error(e?.msg || e?.message || '加载复诊拆解失败')
} catch (e: unknown) {
if (axios.isCancel(e)) return
const any = e as any
ElMessage.error(any?.msg || any?.message || '加载复诊拆解失败')
revisitBreakdownRows.value = []
revisitBreakdownNote.value = ''
} finally {
@@ -2855,8 +3028,10 @@ async function openUnassignedBreakdown(tb: YejiTable) {
const res: any = await yejiStatsUnassignedBreakdown(p as any)
unassignedDialogRows.value = Array.isArray(res?.rows) ? res.rows : []
unassignedDialogNote.value = typeof res?.note === 'string' ? res.note : ''
} catch (e: any) {
ElMessage.error(e?.msg || e?.message || '加载未归属拆解失败')
} catch (e: unknown) {
if (axios.isCancel(e)) return
const any = e as any
ElMessage.error(any?.msg || any?.message || '加载未归属拆解失败')
unassignedDialogRows.value = []
unassignedDialogNote.value = ''
} finally {
@@ -2897,8 +3072,10 @@ async function fetchLeadLinesPage() {
leadLinesRows.value = Array.isArray(res?.lists) ? res.lists : []
leadLinesCount.value = Number(res?.count ?? 0)
leadLinesApiNote.value = typeof res?.note === 'string' ? res.note : ''
} catch (e: any) {
ElMessage.error(e?.msg || e?.message || '加载进线明细失败')
} catch (e: unknown) {
if (axios.isCancel(e)) return
const any = e as any
ElMessage.error(any?.msg || any?.message || '加载进线明细失败')
leadLinesRows.value = []
leadLinesCount.value = 0
leadLinesApiNote.value = ''
@@ -2970,8 +3147,10 @@ async function fetchAppointmentLinesPage() {
appointmentLinesRows.value = Array.isArray(res?.lists) ? res.lists : []
appointmentLinesCount.value = Number(res?.count ?? 0)
appointmentLinesApiNote.value = typeof res?.note === 'string' ? res.note : ''
} catch (e: any) {
ElMessage.error(e?.msg || e?.message || '加载挂号明细失败')
} catch (e: unknown) {
if (axios.isCancel(e)) return
const any = e as any
ElMessage.error(any?.msg || any?.message || '加载挂号明细失败')
appointmentLinesRows.value = []
appointmentLinesCount.value = 0
appointmentLinesApiNote.value = ''
@@ -3946,118 +4125,7 @@ onMounted(async () => {
}
}
/* ── 目标进度卡片 ── */
.tp-wrap {
min-height: 40px;
margin-bottom: 20px;
}
.tp-card {
display: inline-flex;
flex-direction: column;
max-width: 100%;
border-radius: var(--yj-radius);
overflow: hidden;
font-size: 13px;
background: var(--yj-surface);
border: 1px solid var(--yj-line);
box-shadow: var(--yj-shadow-md);
&__title {
background: var(--yj-surface-muted);
color: var(--yj-ink);
font-weight: 700;
font-size: 14px;
text-align: center;
padding: 14px 20px;
letter-spacing: 0.04em;
border-bottom: 1px solid var(--yj-line);
}
&__body {
display: flex;
flex-wrap: wrap;
gap: 0;
}
}
.tp-section {
min-width: 200px;
flex: 1 1 200px;
border-right: 1px solid var(--yj-line);
&:last-child {
border-right: none;
}
&__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
background: var(--yj-brand-soft);
padding: 11px 14px;
border-bottom: 1px solid var(--yj-line);
}
&__title-col {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
&__parent {
font-size: 11px;
font-weight: 600;
color: var(--yj-muted);
line-height: 1.25;
}
&__name {
font-weight: 700;
font-size: 13px;
color: var(--yj-ink);
}
&__target {
font-size: 13px;
font-weight: 700;
color: var(--yj-accent);
}
}
.tp-table {
width: 100%;
border-collapse: collapse;
.tp-lbl {
padding: 8px 12px;
color: var(--yj-muted);
font-size: 12px;
border-bottom: 1px solid var(--yj-line);
white-space: nowrap;
}
.tp-val {
padding: 8px 14px;
font-weight: 600;
text-align: right;
border-bottom: 1px solid var(--yj-line);
white-space: nowrap;
color: var(--yj-ink);
font-variant-numeric: tabular-nums;
&--total {
color: var(--yj-accent);
}
}
tr:last-child td {
border-bottom: none;
}
}
/* 目标完成率色条:目标看板(卡片)Tab 复用 */
.tp-good {
color: #15803d !important;
font-weight: 700;
@@ -4071,6 +4139,126 @@ onMounted(async () => {
font-weight: 700;
}
/* 目标看板卡片栅格(与顶部「甄养堂」条带数据同源,8 列铺满) */
.tp-matrix-wrap {
min-height: 120px;
padding: 8px 4px 16px;
}
.tp-matrix__title {
margin: 0 0 18px;
text-align: center;
font-size: 16px;
font-weight: 700;
color: var(--yj-ink);
letter-spacing: 0.02em;
}
.tp-matrix {
display: grid;
grid-template-columns: repeat(8, minmax(0, 1fr));
gap: 12px;
align-items: stretch;
}
@media (max-width: 1600px) {
.tp-matrix {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (max-width: 900px) {
.tp-matrix {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.tp-matrix-card {
display: flex;
flex-direction: column;
min-width: 0;
border: 1px solid var(--yj-line);
border-radius: var(--yj-radius-sm);
background: var(--yj-surface);
box-shadow: var(--yj-shadow-sm);
font-size: 12px;
overflow: hidden;
&__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
padding: 10px 12px;
background: var(--yj-brand-soft);
border-bottom: 1px solid var(--yj-line);
}
&__titles {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
&__parent {
font-size: 11px;
font-weight: 600;
color: var(--yj-muted);
line-height: 1.2;
}
&__name {
font-weight: 700;
font-size: 12px;
color: var(--yj-ink);
line-height: 1.35;
}
&__target {
flex-shrink: 0;
font-size: 13px;
font-weight: 700;
color: var(--yj-accent);
font-variant-numeric: tabular-nums;
}
&__table {
width: 100%;
border-collapse: collapse;
}
&__lbl {
padding: 7px 10px;
color: var(--yj-muted);
font-size: 11px;
border-bottom: 1px solid var(--yj-line);
white-space: nowrap;
}
&__num {
padding: 7px 10px;
font-weight: 600;
text-align: right;
border-bottom: 1px solid var(--yj-line);
color: var(--yj-ink);
font-variant-numeric: tabular-nums;
&--hot {
color: var(--yj-accent);
font-weight: 700;
}
}
tr:last-child td {
border-bottom: none;
}
}
.tp-matrix-empty {
padding: 32px 16px;
}
.leaderboard-sub {
font-size: 13px;
font-weight: 500;
@@ -4640,6 +4828,10 @@ tbody tr.yeji-row--action:hover .yeji-lead-cell--link {
color: color-mix(in srgb, var(--yj-brand, #2563eb) 88%, #000);
}
tbody tr.total-row:hover .yeji-lead-cell--link {
color: color-mix(in srgb, var(--yj-brand, #2563eb) 88%, #000);
}
.yeji-leadlines-dialog__pager {
margin-top: 14px;
display: flex;
+25 -2
View File
@@ -180,11 +180,12 @@
</template>
</el-table-column>
<el-table-column label="订单状态" width="100">
<el-table-column label="订单状态" width="120">
<template #default="{ row }">
<el-tag :type="getStatusTag(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
<el-tag v-if="row.is_exempt === 1" type="warning" size="small" class="ml-1">豁免</el-tag>
</template>
</el-table-column>
@@ -270,6 +271,14 @@
>
删除
</el-button>
<el-button
v-perms="['order.order/setExempt']"
:type="row.is_exempt === 1 ? 'warning' : 'default'"
link
@click="handleToggleExempt(row)"
>
{{ row.is_exempt === 1 ? '取消豁免' : '设为豁免' }}
</el-button>
</template>
</el-table-column>
</el-table>
@@ -779,7 +788,8 @@ import {
orderActionLogs,
orderActionLogStats,
orderBatchAssignAssistant,
orderSplit
orderSplit,
orderSetExempt
} from '@/api/order'
import { getAssistants } from '@/api/tcm'
import { generateOrderQrcode } from '@/api/tcm'
@@ -1494,6 +1504,19 @@ const handleDelete = async (row: any) => {
}
}
const handleToggleExempt = async (row: any) => {
const newVal: 0 | 1 = row.is_exempt === 1 ? 0 : 1
const actionText = newVal === 1 ? '设为豁免' : '取消豁免'
try {
await feedback.confirm(`确定要${actionText}该支付单吗?豁免后关联此单的业务订单将跳过定金门槛校验。`)
await orderSetExempt({ id: row.id, is_exempt: newVal })
feedback.msgSuccess(`${actionText}成功`)
getLists()
} catch (error) {
// 用户取消操作
}
}
// 小程序码相关
const qrcodeDialogVisible = ref(false)
const qrcodeLoading = ref(false)
+72 -5
View File
@@ -49,15 +49,30 @@
v-model="form.channel_source"
placeholder="请选择渠道来源"
class="channel-source-select"
@change="onChannelSourceChange"
>
<el-option
v-for="item in channelOptions"
:key="item.value"
:key="String(item.value)"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="needsChannelSourceDetail"
label="自媒体补充:"
required
>
<el-input
v-model="form.channel_source_detail"
clearable
maxlength="128"
show-word-limit
placeholder="请输入自媒体相关补充内容"
class="channel-source-select"
/>
</el-form-item>
<!-- 预约医生 -->
<el-form-item label="预约医生:">
<div class="doctor-list">
@@ -233,9 +248,44 @@ const form = reactive({
date: '',
appointmentTime: '',
remark: '',
channel_source: '' as string
channel_source: '' as string,
channel_source_detail: '' as string
})
/** 仅这些渠道字典 name 需填「自媒体补充」(与后台字典名称完全一致) */
const CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL = new Set([
'自媒体4H',
'自媒体3Q',
'自媒体3H',
'自媒体2H',
'自媒体2Q'
])
function channelNameRequiresSelfMediaDetail(name: string) {
return CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL.has(String(name ?? '').trim())
}
/** 当前选中渠道字典名称(用于判断是否需填「自媒体补充」) */
const selectedChannelDictName = computed(() => {
const v = form.channel_source
if (v === '' || v == null) return ''
const row = channelOptions.value.find((item: any) => String(item.value) === String(v))
return row ? String(row.name ?? '').trim() : ''
})
/** 仅白名单内字典项需填写补充说明 */
const needsChannelSourceDetail = computed(() =>
channelNameRequiresSelfMediaDetail(selectedChannelDictName.value)
)
const onChannelSourceChange = (val: string) => {
const row = channelOptions.value.find((item: any) => String(item.value) === String(val))
const name = row ? String(row.name ?? '').trim() : ''
if (!channelNameRequiresSelfMediaDetail(name)) {
form.channel_source_detail = ''
}
}
// 生成未来7天的日期选项(只显示有排班的日期,且大于等于今天)
const dateOptions = computed<DateOption[]>(() => {
if (!selectedDoctorId.value || doctorRosterDates.value.length === 0) {
@@ -321,7 +371,8 @@ const canSubmit = computed(() => {
selectedDoctorId.value &&
form.date &&
form.appointmentTime &&
!!form.channel_source
!!form.channel_source &&
(!needsChannelSourceDetail.value || form.channel_source_detail.trim() !== '')
)
})
@@ -344,6 +395,7 @@ const open = async (patient: any) => {
form.appointmentTime = ''
form.remark = ''
form.channel_source = ''
form.channel_source_detail = ''
timeSlots.value = []
doctorRosterDates.value = []
lastVisit.value = ''
@@ -412,7 +464,17 @@ const loadLastVisit = async () => {
const loadChannelOptions = async () => {
try {
const data = await getDictData({ type: 'channels' })
channelOptions.value = (data?.channels || []).filter((row: any) => row.status !== 0)
const rows = (data?.channels || []).filter((row: any) => row.status !== 0)
rows.sort((a: any, b: any) => {
const ds = Number(b?.sort ?? 0) - Number(a?.sort ?? 0)
if (ds !== 0) return ds
return Number(b?.id ?? 0) - Number(a?.id ?? 0)
})
// 统一为字符串,避免字典 value 为数字时 el-select 与表单校验不一致导致看似选了但实际未绑定
channelOptions.value = rows.map((row: any) => ({
...row,
value: row?.value != null && row.value !== '' ? String(row.value) : ''
}))
} catch (e) {
console.error('加载渠道来源失败:', e)
channelOptions.value = []
@@ -612,6 +674,10 @@ const handleConfirm = async () => {
feedback.msgWarning('请选择渠道来源')
return
}
if (needsChannelSourceDetail.value && !form.channel_source_detail.trim()) {
feedback.msgWarning('请填写自媒体渠道补充信息')
return
}
if (blockedByTodayDuplicate.value) {
feedback.msgWarning(
'该患者今日已有挂号(已预约或已过号),不能重复预约今天,请改选其他日期。'
@@ -631,7 +697,8 @@ const handleConfirm = async () => {
appointment_time: form.appointmentTime,
appointment_type: form.appointmentType,
remark: form.remark,
channel_source: form.channel_source
channel_source: String(form.channel_source ?? ''),
channel_source_detail: form.channel_source_detail.trim()
}
console.log('提交预约参数:', params)
@@ -105,7 +105,9 @@ function channelLabel(row: Record<string, any>) {
const raw = row.channel_source ?? row.channels ?? ''
if (raw === '' || raw === null || raw === undefined) return '—'
const key = String(raw)
return channelMap.value[key] || key
const base = channelMap.value[key] || key
const extra = String(row.channel_source_detail ?? '').trim()
return extra !== '' ? `${base}${extra}` : base
}
function sortRows(list: any[]) {
@@ -141,6 +143,8 @@ const load = async () => {
await loadChannels()
const res = await appointmentLists({
patient_id: props.diagnosisId,
/** 诊单维度拉挂号:后端豁免医生/医助与数据范围收窄 */
diag_scope_relax: 1,
page_no: 1,
page_size: 500
})
@@ -39,19 +39,313 @@
{{ formatTime(row.create_time) }}
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['tcm.prescriptionOrder/detail']"
type="primary"
link
@click="openDetail(row.id)"
>查看详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-3">
<pagination v-model="pager" @change="getLists" />
</div>
</template>
<!-- 业务订单详情抽屉只读 order_list.vue 详情保持一致 -->
<el-drawer
v-model="detailVisible"
size="80%"
destroy-on-close
:close-on-click-modal="false"
append-to-body
>
<template #header>
<div class="flex items-center">
<span class="font-semibold text-lg">业务订单详情</span>
<el-tag v-if="detailData?.order_no" type="primary" effect="plain" round class="ml-3">{{ detailData.order_no }}</el-tag>
<el-tag v-if="detailData?.gancao_reciperl_order_no" type="success" effect="plain" round class="ml-2">甘草 {{ detailData.gancao_reciperl_order_no }}</el-tag>
</div>
</template>
<el-skeleton v-if="detailLoading" :rows="12" animated class="px-6 py-4" />
<div v-else-if="detailData" class="px-6 pb-6 overflow-y-auto">
<!-- 顶部数据概览 -->
<el-row :gutter="16" class="mb-5">
<el-col :xs="12" :sm="6">
<el-card shadow="never" class="border border-gray-100 bg-gray-50/50">
<div class="text-gray-500 text-xs mb-1">总金额</div>
<div class="text-red-500 font-bold text-xl">¥{{ detailData.amount }}</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card shadow="never" class="border border-gray-100 bg-gray-50/50">
<div class="text-gray-500 text-xs mb-1">已付总额</div>
<div class="text-green-600 font-bold text-xl">¥{{ detailData.linked_pay_paid_total || 0 }}</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card shadow="never" class="border border-gray-100 bg-gray-50/50">
<div class="text-gray-500 text-xs mb-1">需代收</div>
<div class="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="border border-gray-100 bg-gray-50/50">
<div class="text-gray-500 text-xs mb-1">已付笔数</div>
<div class="text-primary font-bold text-xl">{{ detailLinkedPayOrders.length }} </div>
</el-card>
</el-col>
</el-row>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<!-- 左侧处方单不含测试价格改患者信息药材明细 -->
<el-card shadow="never" class="border-gray-100 h-full relative overflow-hidden">
<div v-if="detailData.prescription_audit_status === 1" class="audit-stamp stamp-pass"><div class="stamp-inner">审核通过</div></div>
<div v-if="detailData.prescription_audit_status === 2" class="audit-stamp stamp-reject"><div class="stamp-inner">已驳回</div></div>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="font-medium text-[15px]">处方详情</span>
<span v-if="detailData.doctor_name" class="text-xs text-gray-500">
开方人<span class="text-primary font-medium">{{ detailData.doctor_name }}</span>
</span>
</div>
<span class="text-xs text-gray-400">ID: {{ detailData.prescription_id || '—' }}</span>
</div>
</template>
<template v-if="detailPrescription">
<el-descriptions :column="2" border size="small" class="bg-white">
<el-descriptions-item label="处方编号" :span="2">{{ detailPrescription.sn || '—' }}</el-descriptions-item>
<el-descriptions-item label="患者">{{ detailPrescription.patient_name || '—' }}</el-descriptions-item>
<el-descriptions-item label="性别 / 年龄">{{ detailPrescription.gender_desc || '—' }} · {{ detailPrescription.age ?? '—' }}</el-descriptions-item>
<el-descriptions-item label="手机">
<span :class="detailRecipientRxPhoneMismatch ? 'text-red-600 font-bold' : ''">{{ detailPrescription.phone || '—' }}</span>
<el-tag v-if="detailRecipientRxPhoneMismatch" type="danger" size="small" effect="dark" class="ml-2">与订单手机不一致</el-tag>
</el-descriptions-item>
<el-descriptions-item label="处方日期">{{ detailPrescription.prescription_date || '—' }}</el-descriptions-item>
<el-descriptions-item label="医师">{{ detailPrescription.doctor_name || '—' }}</el-descriptions-item>
<el-descriptions-item label="类型">{{ detailPrescription.prescription_type || '—' }}</el-descriptions-item>
<el-descriptions-item label="临床诊断" :span="2">{{ detailPrescription.clinical_diagnosis || '—' }}</el-descriptions-item>
<el-descriptions-item label="剂数 / 用法" :span="2">
{{ detailData.dose_count ?? detailPrescription.dose_count ?? '—' }} {{ detailData.dose_unit || '剂' }} ·
{{ detailPrescription.usage_instruction || detailPrescription.usage_method || '—' }}
</el-descriptions-item>
<el-descriptions-item label="处方金额">
<span class="text-red-500 font-medium">¥{{ detailPrescription.amount ?? '—' }}</span>
</el-descriptions-item>
<el-descriptions-item label="消费者审核">
<el-tag :type="consumerRxAuditTag(detailPrescription.audit_status)" size="small">{{ consumerRxAuditText(detailPrescription.audit_status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="用量">
<template v-if="detailPrescription.dosage_amount">
{{ detailPrescription.dosage_amount }}{{ detailPrescription.dosage_unit || 'g' }}
<template v-if="detailPrescription.prescription_type === '浓缩水丸'">
· {{ Number(detailPrescription.dosage_bag_count) > 0 ? Number(detailPrescription.dosage_bag_count) : 1 }}
</template>
<span v-if="detailPrescription.prescription_type === '饮片' && detailPrescription.need_decoction !== null" class="ml-2 text-gray-500">
({{ detailPrescription.need_decoction ? '代煎' : '不代煎' }})
</span>
</template>
<template v-else></template>
</el-descriptions-item>
<el-descriptions-item label="服用方式">
<div class="flex flex-col gap-1.5 text-sm leading-relaxed">
<div>
<span class="text-gray-500">每天次数</span>
{{ detailPrescription.times_per_day ? detailPrescription.times_per_day + ' 次' : '—' }}
</div>
<div>
<span class="text-gray-500">处方开立</span>
{{ detailPrescription.usage_days != null && detailPrescription.usage_days !== '' ? detailPrescription.usage_days + ' 天' : '—' }}
</div>
<div>
<span class="text-gray-500">订单设置</span>
{{ detailData.medication_days != null && String(detailData.medication_days).trim() !== '' ? detailData.medication_days + ' 天' : '—' }}
</div>
</div>
</el-descriptions-item>
<el-descriptions-item label="状态" :span="2">
<el-tag :type="Number(detailPrescription.void_status) === 1 ? 'danger' : 'success'" size="small">{{ Number(detailPrescription.void_status) === 1 ? '已作废' : '正常' }}</el-tag>
</el-descriptions-item>
</el-descriptions>
</template>
<el-empty v-else description="无处方数据" :image-size="72" />
</el-card>
<!-- 右侧未关联 + 已关联支付单 -->
<div class="flex flex-col gap-4 h-full">
<el-card shadow="never" class="border-gray-100">
<template #header>
<div class="flex items-center justify-between">
<span class="font-medium text-[15px]">未关联的收款记录</span>
<span class="text-xs text-gray-400">本诊单下未被占用的支付单</span>
</div>
</template>
<el-table v-if="detailUnlinkedPayOrders.length" :data="detailUnlinkedPayOrders" size="small" border stripe max-height="280">
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="order_no" label="单号" min-width="135" show-overflow-tooltip />
<el-table-column label="类型" width="90">
<template #default="{ row }">{{ row.order_type_desc || feeTypeText(row.order_type) }}</template>
</el-table-column>
<el-table-column label="金额" width="86" align="right">
<template #default="{ row }"><span class="text-red-500">¥{{ row.amount }}</span></template>
</el-table-column>
<el-table-column label="状态" width="82" align="center">
<template #default="{ row }">
<el-tag :type="payOrderStatusTag(row.status)" size="small">{{ row.status_desc || orderStatusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
<el-table-column label="创建时间" min-width="136">
<template #default="{ row }">{{ formatOrderTime(row.create_time) }}</template>
</el-table-column>
</el-table>
<el-empty v-else description="无未关联的收款单" :image-size="60" />
</el-card>
<el-card shadow="never" class="border-gray-100 relative overflow-hidden">
<div v-if="detailData.payment_slip_audit_status === 1" class="audit-stamp stamp-pass"><div class="stamp-inner">审核通过</div></div>
<div v-if="detailData.payment_slip_audit_status === 2" class="audit-stamp stamp-reject"><div class="stamp-inner">已驳回</div></div>
<template #header>
<div class="flex items-center gap-3">
<span class="font-medium text-[15px]">关联收款记录</span>
<span v-if="detailData.creator_name" class="text-xs text-gray-500">创建人:<span class="text-primary font-medium">{{ detailData.creator_name }}</span></span>
</div>
</template>
<el-table v-if="detailLinkedPayOrders.length" :data="detailLinkedPayOrders" size="small" border stripe max-height="280">
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="order_no" label="单号" min-width="135" show-overflow-tooltip />
<el-table-column label="类型" width="90">
<template #default="{ row }">{{ row.order_type_desc || feeTypeText(row.order_type) }}</template>
</el-table-column>
<el-table-column label="金额" width="86" align="right">
<template #default="{ row }"><span class="text-red-500">¥{{ row.amount }}</span></template>
</el-table-column>
<el-table-column label="状态" width="82" align="center">
<template #default="{ row }">
<el-tag :type="payOrderStatusTag(row.status)" size="small">{{ row.status_desc || orderStatusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
<el-table-column label="创建时间" min-width="136">
<template #default="{ row }">{{ formatOrderTime(row.create_time) }}</template>
</el-table-column>
</el-table>
<el-empty v-else description="未关联收款单" :image-size="60" />
</el-card>
</div>
</div>
<!-- 履约与收货信息 -->
<el-card shadow="never" class="border-gray-100 mb-4">
<template #header><span class="font-medium text-[15px]">履约与收货信息</span></template>
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="诊单ID" :span="3"><span class="font-mono">{{ detailData.diagnosis_id }}</span></el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatTime(detailData.create_time) }}</el-descriptions-item>
<el-descriptions-item label="业务订单创建人" :span="2">
<div v-if="detailData.creator_name" class="text-[13px] space-y-1">
<div><span class="font-medium text-gray-900">{{ detailData.creator_name || '—' }}</span></div>
<div v-if="detailOrderCreatorMetaLine" class="text-gray-600 text-xs">{{ detailOrderCreatorMetaLine }}</div>
</div>
<span v-else class="text-gray-400"></span>
</el-descriptions-item>
<el-descriptions-item label="创建人所属部门" :span="3">
<div v-if="detailOrderCreatorDeptBreadcrumbs.length" class="space-y-1.5">
<div v-for="(segments, idx) in detailOrderCreatorDeptBreadcrumbs" :key="idx">
<el-breadcrumb v-if="segments.length" class="text-[13px]" separator="/">
<el-breadcrumb-item v-for="(name, j) in segments" :key="j"><span class="text-gray-800">{{ name }}</span></el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
<span v-else class="text-gray-400"></span>
</el-descriptions-item>
<el-descriptions-item label="收货人">{{ detailData.recipient_name }}</el-descriptions-item>
<el-descriptions-item label="收货手机">
<span :class="detailRecipientRxPhoneMismatch ? 'text-red-600 font-bold font-mono' : 'font-mono'">{{ detailData.recipient_phone || '—' }}</span>
<el-tag v-if="detailRecipientRxPhoneMismatch" type="danger" size="small" effect="dark" class="ml-2">与处方手机不一致</el-tag>
</el-descriptions-item>
<el-descriptions-item label="收货地址">{{ detailFullAddress }}</el-descriptions-item>
<el-descriptions-item label="复诊">{{ detailData.is_follow_up ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="用药疗程">{{ detailData.medication_days ? detailData.medication_days + ' 天' : '—' }}</el-descriptions-item>
<el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item>
<el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item>
<el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item>
<el-descriptions-item label="快递单号">{{ detailData.tracking_number || '—' }}</el-descriptions-item>
<el-descriptions-item label="快递公司">{{ expressCompanyLabel(detailData.express_company) }}</el-descriptions-item>
<el-descriptions-item v-perms="['tcm.prescriptionOrder/editRemarkExtra']" label="药房备注" :span="3">{{ detailData.remark_extra || '' }}</el-descriptions-item>
<el-descriptions-item label="医助备注" :span="3">{{ detailData.remark_assistant || '—' }}</el-descriptions-item>
<el-descriptions-item label="处方审核意见" :span="3">{{ detailData.prescription_audit_remark || '—' }}</el-descriptions-item>
<el-descriptions-item label="支付审核意见" :span="3">{{ detailData.payment_slip_audit_remark || '—' }}</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 物流轨迹只读 -->
<el-card v-if="String(detailData.tracking_number || '').trim()" shadow="never" class="border-gray-100 mb-4">
<template #header><span class="font-medium text-[15px]">物流轨迹</span></template>
<div class="flex flex-wrap items-center gap-3 mb-4">
<el-input v-model="logisticsTracePhoneTail" clearable maxlength="20" placeholder="收件手机" class="w-[180px]" @keyup.enter="fetchLogisticsTrace" />
<el-select v-model="detailLogisticsExpress" placeholder="承运商" style="width: 140px">
<el-option label="自动识别" value="auto" />
<el-option label="顺丰速运" value="sf" />
<el-option label="京东快递" value="jd" />
<el-option label="极兔速递" value="jt" />
</el-select>
<el-button v-perms="['tcm.prescriptionOrder/logisticsTrace']" type="primary" plain size="default" :loading="logisticsTraceLoading" @click="fetchLogisticsTrace">刷新轨迹</el-button>
</div>
<div v-if="logisticsTraceLoading && !logisticsTracePayload" class="py-6 text-center text-sm text-gray-500">正在加载物流轨迹...</div>
<div v-else-if="logisticsTracePayload" class="bg-gray-50/50 p-4 rounded-md">
<div v-if="logisticsTracePayload.state_text" class="text-sm mb-4 font-medium flex items-center gap-2">
<span class="text-gray-800">状态{{ logisticsTracePayload.state_text }}</span>
<el-tag v-if="logisticsTracePayload.carrier_label" size="small" type="info">{{ logisticsTracePayload.carrier_label }}</el-tag>
</div>
<el-timeline v-if="logisticsTraceList.length" class="mt-2 pl-2">
<el-timeline-item v-for="(t, idx) in logisticsTraceList" :key="idx" :type="idx === 0 ? 'primary' : 'info'" :hollow="idx !== 0" :timestamp="t.time" placement="top">
<span :class="idx === 0 ? 'text-gray-800 font-medium' : 'text-gray-500'">{{ t.context }}</span>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无轨迹节点记录" :image-size="64" />
</div>
</el-card>
<!-- 操作日志 -->
<el-card v-perms="['tcm.prescriptionOrder/logs']" shadow="never" class="border-gray-100">
<template #header><span class="font-medium text-[15px]">操作日志</span></template>
<el-timeline v-if="detailLogs.length" class="mt-2 pl-2">
<el-timeline-item v-for="(log, idx) in detailLogs" :key="log.id" :type="idx === 0 ? 'primary' : 'info'" :hollow="idx !== 0" :timestamp="formatTime(log.create_time)" placement="top">
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-[13px]">{{ log.admin_name }}</span>
<el-tag size="small" type="info">{{ logActionText(log.action) }}</el-tag>
</div>
<span :class="idx === 0 ? 'text-gray-800 font-medium' : 'text-gray-500'">{{ log.summary }}</span>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无操作日志" :image-size="64" />
</el-card>
</div>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { computed, watch } from 'vue'
import { computed, watch, ref } from 'vue'
import { usePaging } from '@/hooks/usePaging'
import { prescriptionOrderLists } from '@/api/tcm'
import {
prescriptionOrderLists,
prescriptionOrderDetail,
prescriptionOrderLogs,
prescriptionOrderLogisticsTrace,
prescriptionOrderPaidPayOrders
} from '@/api/tcm'
import { hasPermission } from '@/utils/perm'
import feedback from '@/utils/feedback'
import useUserStore from '@/stores/modules/user'
const userStore = useUserStore()
const props = defineProps<{
diagnosisId: number
@@ -83,11 +377,171 @@ const buildParams = () => {
queryParams.scene = 'diagnosis_edit'
}
// ─── 详情抽屉 ───
const detailVisible = ref(false)
const detailLoading = ref(false)
const detailData = ref<any>(null)
const detailLogs = ref<any[]>([])
const detailUnlinkedPayOrders = ref<any[]>([])
const detailPrescription = computed(() => {
const p = detailData.value?.prescription
return p && typeof p === 'object' ? p : null
})
const detailLinkedPayOrders = computed(() => {
const arr = detailData.value?.linked_pay_orders
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 detailRecipientRxPhoneMismatch = computed(() => {
const rx = normalizeBizPhone(detailPrescription.value?.phone)
const biz = normalizeBizPhone(detailData.value?.recipient_phone)
if (!rx || !biz) return false
return rx !== biz
})
const detailFullAddress = computed(() => {
const d = detailData.value
if (!d) return '—'
const parts = []
if (d.shipping_province) parts.push(d.shipping_province)
if (d.shipping_city) parts.push(d.shipping_city)
if (d.shipping_district) parts.push(d.shipping_district)
if (d.shipping_address) parts.push(d.shipping_address)
return parts.length > 0 ? parts.join(' ') : '—'
})
const detailOrderCreatorMetaLine = computed(() => {
const d = detailData.value
if (!d) return ''
const account = String(d.creator_account ?? '').trim()
const mobile = String(d.creator_mobile ?? '').trim()
const parts: string[] = []
if (account) parts.push(`登录账号:${account}`)
if (mobile) parts.push(`手机:${mobile}`)
return parts.join(' · ')
})
const detailOrderCreatorDeptBreadcrumbs = computed(() => {
const raw = String(detailData.value?.order_creator_dept_path ?? detailData.value?.diagnosis_creator_dept_path ?? '').trim()
if (!raw) return [] as string[][]
return raw.split('').map((s) => s.trim()).filter(Boolean)
.map((p) => p.includes(' / ') ? p.split(' / ').map((x) => x.trim()).filter(Boolean) : p.split('/').map((x) => x.trim()).filter(Boolean))
.filter((segs) => segs.length > 0)
})
// ─── 物流轨迹 ───
const detailLogisticsExpress = ref<string>('auto')
const logisticsTraceLoading = ref(false)
const logisticsTracePayload = ref<Record<string, any> | null>(null)
const logisticsTracePhoneTail = ref('')
const logisticsTraceList = computed(() => {
const p = logisticsTracePayload.value
if (!p || !Array.isArray(p.traces)) return []
return p.traces as Array<{ time: string; context: string }>
})
async function fetchLogisticsTrace() {
const id = Number(detailData.value?.id)
if (!id) return
logisticsTraceLoading.value = true
try {
const digits = String(logisticsTracePhoneTail.value || '').replace(/\D/g, '')
const params: { id: number; express_company?: string; phone_tail?: string } = {
id,
express_company: detailLogisticsExpress.value
}
if (digits.length >= 4) params.phone_tail = digits
const res: any = await prescriptionOrderLogisticsTrace(params)
logisticsTracePayload.value = (res?.data ?? res) as Record<string, any>
} catch {
logisticsTracePayload.value = null
} finally {
logisticsTraceLoading.value = false
}
}
// ─── 打开详情 ───
async function openDetail(id: number) {
detailData.value = null
detailUnlinkedPayOrders.value = []
logisticsTracePayload.value = null
detailLogisticsExpress.value = 'auto'
logisticsTracePhoneTail.value = ''
detailLogs.value = []
detailVisible.value = true
detailLoading.value = true
try {
const res: any = await prescriptionOrderDetail({ id })
const d = res?.data ?? res ?? null
detailData.value = d
if (d) {
detailLogisticsExpress.value = String(d.express_company || 'auto') || 'auto'
const dig = String(d.recipient_phone || '').replace(/\D/g, '')
logisticsTracePhoneTail.value = dig.length >= 4 ? dig : ''
if (String(d.tracking_number || '').trim()) fetchLogisticsTrace()
fetchLogs(id)
if (d.diagnosis_id) {
loadDetailUnlinkedPayOrders(d.diagnosis_id, id, d.pay_order_ids || [])
}
}
} catch (e: any) {
feedback.msgError(e?.message || '加载详情失败')
detailVisible.value = false
} finally {
detailLoading.value = false
}
}
async function loadDetailUnlinkedPayOrders(diagnosisId: number, prescriptionOrderId: number, linkedIds: number[]) {
if (!diagnosisId) { detailUnlinkedPayOrders.value = []; return }
try {
const res: any = await prescriptionOrderPaidPayOrders({ diagnosis_id: diagnosisId, prescription_order_id: prescriptionOrderId })
const lists = res?.lists ?? res?.data?.lists ?? []
detailUnlinkedPayOrders.value = Array.isArray(lists) ? lists.filter((item: any) => !linkedIds.includes(item.id)) : []
} catch { detailUnlinkedPayOrders.value = [] }
}
function canViewPrescriptionOrderLogs() {
const p = userStore.perms || []
return p.includes('*') || p.includes('tcm.prescriptionOrder/logs')
}
async function fetchLogs(id: number) {
if (!canViewPrescriptionOrderLogs()) { detailLogs.value = []; return }
try {
const res: any = await prescriptionOrderLogs({ id })
detailLogs.value = res?.data ?? res ?? []
} catch { detailLogs.value = [] }
}
// ─── 工具函数 ───
function normalizeBizPhone(v: unknown): string {
if (v === null || v === undefined) return ''
return String(v).replace(/\s/g, '').trim()
}
const formatAmount = (value: unknown) => {
const n = Number(value)
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
}
function formatMoney(v: unknown) {
const n = Number(v)
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
}
const formatTime = (value: unknown) => {
if (value === null || value === undefined || value === '') return '—'
if (typeof value === 'number' && value > 1e9 && value < 1e11) {
@@ -98,21 +552,53 @@ const formatTime = (value: unknown) => {
return String(value)
}
function formatOrderTime(v: unknown) {
if (v === null || v === undefined || v === '') return '—'
if (typeof v === 'string' && String(v).includes('-')) return String(v)
return formatTime(v)
}
function feeTypeText(t: number | undefined) {
const m: Record<number, string> = { 1: '药费', 2: '诊费', 3: '药费+诊费', 4: '其他' }
return m[Number(t)] ?? '—'
}
function expressCompanyLabel(v: unknown) {
const s = String(v || '').toLowerCase()
if (s === 'sf') return '顺丰速运'
if (s === 'jd') return '京东快递'
if (s === 'jt' || s === 'jtexpress') return '极兔速递'
return '自动识别'
}
function consumerRxAuditText(s: number | undefined) {
const n = Number(s)
if (n === 1) return '已通过'
if (n === 2) return '已驳回'
return '待审核'
}
function consumerRxAuditTag(s: number | undefined): 'success' | 'warning' | 'danger' | 'info' {
const n = Number(s)
if (n === 1) return 'success'
if (n === 2) return 'danger'
return 'warning'
}
function payOrderStatusTag(s: number | undefined): 'success' | 'warning' | 'danger' | 'info' {
const n = Number(s)
if (n === 2) return 'success'
if (n === 1) return 'warning'
return 'info'
}
function orderStatusText(s: number | undefined) {
const m: Record<number, string> = { 0: '待支付', 1: '待支付', 2: '已支付', 3: '已取消' }
return m[Number(s)] ?? '—'
}
const fulfillmentText = (s: number | undefined) => {
const m: Record<number, string> = {
1: '待双审通过',
2: '待发货',
3: '已完成',
4: '已取消',
5: '已发货',
6: '已签收',
7: '进行中',
8: '暂不制药',
9: '拒收',
10: '退款',
11: '保留药方',
12: '制药缓发'
}
const m: Record<number, string> = { 1: '待双审通过', 2: '待发货', 3: '已完成', 4: '已取消', 5: '已发货', 6: '已签收', 7: '进行中', 8: '暂不制药', 9: '拒收', 10: '退款', 11: '保留药方', 12: '制药缓发' }
return m[Number(s)] ?? '—'
}
@@ -126,14 +612,22 @@ const fulfillmentTagType = (s: number | undefined): 'success' | 'warning' | 'dan
return 'info'
}
function logActionText(act: string) {
const m: Record<string, string> = {
create: '创建', edit: '编辑', audit_rx_approve: '处方审核', audit_rx_reject: '处方审核',
audit_pay_approve: '支付审核', audit_pay_reject: '支付审核', fill_tracking: '填快递单',
ship: '确认发货', withdraw: '撤销', link_pay_order: '关联支付单',
revoke_rx_audit: '撤回处方审核', revoke_pay_audit: '撤回支付审核',
gancao_submit: '甘草下单', patch_rx_patient: '处方患者信息',
update_amount: '修改订单金额', complete: '完成订单'
}
return m[act] || act
}
watch(
() => [props.diagnosisId, patientIdNum.value] as const,
() => {
if (!patientIdAvailable.value) {
pager.lists = []
pager.count = 0
return
}
if (!patientIdAvailable.value) { pager.lists = []; pager.count = 0; return }
buildParams()
resetPage()
},
@@ -147,8 +641,25 @@ defineExpose({ refresh: () => getLists() })
.patient-order-list {
padding: 4px 0;
}
.po-empty-tip {
padding: 24px 0;
}
.audit-stamp {
position: absolute;
top: 12px;
right: 16px;
transform: rotate(15deg);
opacity: 0.15;
pointer-events: none;
z-index: 1;
}
.stamp-inner {
border: 3px solid currentColor;
border-radius: 8px;
padding: 4px 12px;
font-size: 18px;
font-weight: bold;
}
.stamp-pass { color: #22c55e; }
.stamp-reject { color: #ef4444; }
</style>
+33 -14
View File
@@ -151,12 +151,14 @@
stripe
>
<el-table-column type="selection" width="48" align="center" />
<el-table-column label="ID" min-width="50">
<el-table-column label="ID" min-width="70">
<template #default="{ row }">
<div class="patient-cell">
<!-- <div class="patient-avatar">{{ (row.patient_name || '患').charAt(0) }}</div> -->
<div class="patient-info">
<div class="patient-name">{{ row.id }}</div>
<div class="patient-name">
{{ row.id }}
<el-tag v-if="row.assign_read_at === null && row.assistant_id > 0" type="danger" size="small" effect="dark" class="ml-1">NEW</el-tag>
</div>
</div>
</div>
</template>
@@ -795,6 +797,32 @@ function buildTcmDiagnosisListRequestPayload(req: Record<string, unknown>): Reco
}
}
/**
* 待分配「订单月份」:未选月份时,未进入 Tab 前默认当月(与 handlePendingAssignTabClick 一致);
* 在 Tab 内清空月份表示不限月份。
*/
function resolvePendingAssignOrderMonthForRequest(): string {
const trimmed = String(formData.pending_assign_order_month ?? '').trim()
if (trimmed !== '') return trimmed
if (formData.pending_assign === '1') return ''
return dayjs().format('YYYY-MM')
}
/** 待分配角标 count 请求:与列表同条件,且去掉其它顶部 Tab 残留(如默认「当天挂号」) */
function buildPendingAssignCountPayload(): Record<string, unknown> {
return buildTcmDiagnosisListRequestPayload({
...formData,
page_no: 1,
page_size: 1,
pending_assign: 1,
appointment_date: '',
has_appointment: '',
pending_booking: '',
completed_appointment: '',
pending_assign_order_month: resolvePendingAssignOrderMonthForRequest()
} as Record<string, unknown>) as Record<string, unknown>
}
const fetchTcmDiagnosisListsForPaging = (req: Record<string, unknown>) =>
tcmDiagnosisLists(buildTcmDiagnosisListRequestPayload(req) as any)
@@ -923,16 +951,7 @@ const fetchDateCounts = async () => {
tcmDiagnosisLists({ page_no: 1, page_size: 1 }),
tcmDiagnosisLists({ has_appointment: 0, page_no: 1, page_size: 1 }),
tcmDiagnosisLists({ completed_appointment: 1, page_no: 1, page_size: 1 }),
tcmDiagnosisLists(
buildTcmDiagnosisListRequestPayload({
pending_assign: 1,
page_no: 1,
page_size: 1,
pending_assign_order_month: formData.pending_assign_order_month,
pending_assign_keyword: formData.pending_assign_keyword,
keyword: formData.keyword
}) as any
)
tcmDiagnosisLists(buildPendingAssignCountPayload() as any)
])
dateCounts.value = {
[yesterdayStr.value]: yesterday?.count ?? 0,
@@ -1352,7 +1371,7 @@ const handleSingleAssign = (row: any) => {
currentRow.value = row
batchAssignIds.value = []
// 确保类型一致,转换为数字
assignForm.value.assistant_id = row.assistant_id ? Number(row.assistant_id) : null
assignForm.value.assistant_id = Number(row.assistant_id) > 0 ? Number(row.assistant_id) : null
assignDialogVisible.value = true
}
@@ -130,6 +130,7 @@
</div>
<div class="dh5-card-sub">
#{{ row.id }} · {{ row.diagnosis_date_text || formatDateTime(row.create_time) }}
<span v-if="row.assign_read_at === null && row.assistant_id > 0" class="dh5-tag-new">NEW</span>
</div>
</div>
<div class="dh5-card-tags">
@@ -1672,6 +1673,18 @@ $dh5-card-bg: #ffffff;
flex-wrap: wrap;
}
.dh5-tag-new {
display: inline-block;
background: #ef4444;
color: #fff;
font-size: 10px;
font-weight: 700;
padding: 1px 4px;
border-radius: 3px;
margin-left: 4px;
vertical-align: middle;
}
.dh5-meta-gender,
.dh5-meta-age {
font-size: 12px;
+6 -4
View File
@@ -97,12 +97,14 @@
stripe
>
<el-table-column type="selection" width="48" align="center" />
<el-table-column label="ID" min-width="50">
<el-table-column label="ID" min-width="70">
<template #default="{ row }">
<div class="patient-cell">
<!-- <div class="patient-avatar">{{ (row.patient_name || '患').charAt(0) }}</div> -->
<div class="patient-info">
<div class="patient-name">{{ row.id }}</div>
<div class="patient-name">
{{ row.id }}
<el-tag v-if="row.assign_read_at === null && row.assistant_id > 0" type="danger" size="small" effect="dark" class="ml-1">NEW</el-tag>
</div>
</div>
</div>
</template>
@@ -1088,7 +1090,7 @@ const handleSingleAssign = (row: any) => {
assignMode.value = 'single'
currentRow.value = row
// 确保类型一致,转换为数字
assignForm.value.assistant_id = row.assistant_id ? Number(row.assistant_id) : null
assignForm.value.assistant_id = Number(row.assistant_id) > 0 ? Number(row.assistant_id) : null
assignDialogVisible.value = true
}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
open_basedir=/www/wwwroot/zyt/server/:/tmp/
@@ -64,6 +64,24 @@ class AppointmentController extends BaseAdminController
return $this->dataLists(new AppointmentLists());
}
/**
* 后台编辑挂号记录(消费者处方-挂号列表等,perms: doctor.appointment/edit
*/
public function edit()
{
$params = (new AppointmentValidate())->post()->goCheck('adminEdit');
$post = $this->request->post();
if (array_key_exists('assistant_id', $post)) {
$params['assistant_id'] = $post['assistant_id'];
}
$result = AppointmentLogic::adminEdit($params, $this->adminId, $this->adminInfo);
if ($result === false) {
return $this->fail(AppointmentLogic::getError());
}
return $this->success('保存成功');
}
/**
* @notes 预约详情
* @return \think\response\Json
@@ -557,6 +557,24 @@ class OrderController extends BaseAdminController
}
}
public function setExempt()
{
$id = (int) $this->request->post('id', 0);
$isExempt = (int) $this->request->post('is_exempt', 0);
if ($id <= 0) {
return $this->fail('参数错误');
}
if (!in_array($isExempt, [0, 1], true)) {
return $this->fail('is_exempt 值无效');
}
$result = OrderLogic::setExempt($id, $isExempt);
if (!$result) {
return $this->fail(OrderLogic::getError());
}
$this->logOrderAction($id, 'set_exempt', $isExempt === 1 ? '设置豁免权' : '取消豁免权');
return $this->success($isExempt === 1 ? '已设置豁免权' : '已取消豁免权');
}
private function logOrderAction(int $orderId, string $action, string $summary = ''): void
{
OrderActionLogLogic::record($orderId, (int) $this->adminId, $this->adminInfo, $action, $summary);
@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\stats;
use app\adminapi\controller\BaseAdminController;
use app\adminapi\logic\stats\YejiStatsLogic;
/**
* 提成结算业绩(独立于业绩看板 yejiStats 接口)
*
* - GET stats.commissionSettlement/overview
* - GET stats.commissionSettlement/orderLines
* - GET stats.commissionSettlement/confirmStatus
* - POST stats.commissionSettlement/saveReconcile
* - POST stats.commissionSettlement/confirmFinalize
* - POST stats.commissionSettlement/confirmRevoke
* - GET stats.commissionSettlement/deptOptions
* - GET stats.commissionSettlement/channelOptions
*/
class CommissionSettlementController extends BaseAdminController
{
public function overview()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(YejiStatsLogic::commissionSettlementOverview($params, $this->adminId, $this->adminInfo));
}
public function orderLines()
{
@set_time_limit(120);
return $this->data(YejiStatsLogic::commissionSettlementOrderLines($this->request->get(), $this->adminId, $this->adminInfo));
}
public function confirmStatus()
{
return $this->data(YejiStatsLogic::commissionSettlementConfirmStatus($this->request->get(), $this->adminId, $this->adminInfo));
}
public function saveReconcile()
{
return $this->data(YejiStatsLogic::commissionSettlementSaveReconcile($this->request->post(), $this->adminId, $this->adminInfo));
}
public function confirmFinalize()
{
@set_time_limit(120);
return $this->data(YejiStatsLogic::commissionSettlementConfirmFinalize($this->request->post(), $this->adminId, $this->adminInfo));
}
public function confirmRevoke()
{
return $this->data(YejiStatsLogic::commissionSettlementConfirmRevoke($this->request->post(), $this->adminId, $this->adminInfo));
}
public function deptOptions()
{
return $this->data(YejiStatsLogic::deptOptions($this->adminId, $this->adminInfo));
}
public function channelOptions()
{
return $this->data(YejiStatsLogic::channelOptions(false));
}
}
@@ -117,6 +117,7 @@ class DiagnosisController extends BaseAdminController
$params = (new DiagnosisValidate())->goCheck('id');
$result = DiagnosisLogic::detail($params);
DiagnosisLogic::markAssignRead((int) ($params['id'] ?? 0), $this->adminId);
return $this->data($result);
}
@@ -135,7 +136,7 @@ class DiagnosisController extends BaseAdminController
if (empty($result)) {
return $this->fail(DiagnosisLogic::getError() ?: '诊单不存在或无权访问');
}
DiagnosisLogic::markAssignRead((int) ($params['id'] ?? 0), $this->adminId);
return $this->data($result);
}
@@ -20,6 +20,14 @@ class PrescriptionOrderController extends BaseAdminController
return $this->dataLists(new PrescriptionOrderLists());
}
/**
* 处方业务订单导出(与 lists 相同筛选条件,Excel;参数 export=1 预估 export=2 下载)
*/
public function export()
{
return $this->dataLists(new PrescriptionOrderLists());
}
/**
* 指定诊单下可关联的支付单(待支付/已支付,创建/编辑业务订单时多选关联)
*/
@@ -28,12 +28,9 @@ abstract class BaseAdminDataLists extends BaseDataLists
protected array $adminInfo;
protected int $adminId;
public function __construct()
protected function initAdminIdentity(): void
{
parent::__construct();
$this->adminInfo = $this->request->adminInfo;
$this->adminId = $this->request->adminId;
}
}
@@ -7,6 +7,7 @@ use app\common\model\DiagnosisViewRecord;
use app\common\model\doctor\Appointment;
use app\common\model\tcm\Prescription;
use app\common\model\auth\AdminRole;
use app\common\model\dict\DictData;
use app\common\lists\ListsExtendInterface;
use app\common\lists\ListsSearchInterface;
use app\common\lists\Traits\HasDataScopeFilter;
@@ -20,6 +21,16 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
{
use HasDataScopeFilter;
/**
* 诊单编辑/详情「挂号记录」Tab:按诊单 ID 拉全量挂号,不做医生/医助角色收窄与数据范围过滤。
* 须同时传 patient_id(挂号表存的是诊单 id)与本开关,避免列表页被滥用拓宽可见范围。
*/
private function appointmentListsScopeRelaxedForDiagnosis(): bool
{
return (int) ($this->params['diag_scope_relax'] ?? 0) === 1
&& (int) ($this->params['patient_id'] ?? 0) > 0;
}
/**
* 数据隔离:医生或医助(u.assistant_id = diag.assistant_id)命中可见集合;progress_board 场景放开(看板跨医生查看)
*/
@@ -28,6 +39,9 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
if ($progressBoard) {
return;
}
if ($this->appointmentListsScopeRelaxedForDiagnosis()) {
return;
}
if (!$this->dataScopeShouldApply()) {
return;
}
@@ -109,7 +123,7 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
->leftJoin('admin ad', 'a.doctor_id = ad.id')
->leftJoin('admin asst', 'u.assistant_id = asst.id')
->field('a.*, u.patient_name as patient_name, u.phone as patient_phone, u.gender as gender, u.age as age, u.weight as weight, u.height as height, u.assistant_id as assistant_id, ad.name as doctor_name, asst.name as assistant_name, u.id as diagnosis_id');
->field('a.*, u.patient_name as patient_name, u.phone as patient_phone, u.gender as gender, u.age as age, u.weight as weight, u.height as height, u.assistant_id as assistant_id, ad.name as doctor_name, asst.name as assistant_name, u.id as diagnosis_id, a.assistant_id as appointment_assistant_id');
if ($this->searchWhere !== []) {
$query->where($this->searchWhere);
}
@@ -128,7 +142,8 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
// 面诊进度看板:可切换查看各医生挂号,不按当前账号角色收窄
$progressBoard = (int) ($this->params['progress_board'] ?? 0) === 1;
if (!$progressBoard) {
$diagScopeRelax = $this->appointmentListsScopeRelaxedForDiagnosis();
if (!$progressBoard && !$diagScopeRelax) {
// 如果是医生角色(role_id=1),只显示挂自己号的预约
if (in_array(1, $roleIds)) {
$query->where('a.doctor_id', $this->adminId);
@@ -198,6 +213,9 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
}
}
}
$channelNameByValue = DictData::where('type_value', 'channels')->column('name', 'value');
foreach ($lists as &$item) {
$statusMap = [
1 => '已预约',
@@ -214,6 +232,24 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
];
$item['appointment_type_desc'] = $typeMap[$item['appointment_type']] ?? '未知';
$periodRaw = (string) ($item['period'] ?? ($item['type'] ?? ''));
$periodMap = [
'morning' => '上午',
'afternoon' => '下午',
'all' => '全天',
];
$item['period_desc'] = $periodMap[$periodRaw] ?? ($periodRaw !== '' ? $periodRaw : '—');
$srcKey = trim((string) ($item['channel_source'] ?? ''));
if ($srcKey === '' && isset($item['channels']) && $item['channels'] !== '' && $item['channels'] !== null) {
$srcKey = trim((string) $item['channels']);
}
if ($srcKey !== '') {
$item['channel_source_desc'] = (string) ($channelNameByValue[$srcKey] ?? $channelNameByValue[(string) (int) $srcKey] ?? $srcKey);
} else {
$item['channel_source_desc'] = '—';
}
$item['diagnosis_confirmed'] = isset($confirmedMap[$item['diagnosis_id'] ?? 0]) ? 1 : 0;
$item['has_prescription'] = isset($prescribedDiagnosisIds[$item['diagnosis_id'] ?? 0]) ? 1 : 0;
@@ -288,7 +324,8 @@ class AppointmentLists extends BaseAdminDataLists implements ListsSearchInterfac
}
$progressBoard = (int) ($this->params['progress_board'] ?? 0) === 1;
if (!$progressBoard) {
$diagScopeRelax = $this->appointmentListsScopeRelaxedForDiagnosis();
if (!$progressBoard && !$diagScopeRelax) {
if (in_array(1, $roleIds)) {
$query->where('a.doctor_id', $this->adminId);
}
@@ -77,6 +77,23 @@ class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
}
}
// 添加时间:与 lists 排序口径一致(external_first_add_time 优先,0 则 create_time
$addStart = trim((string) ($this->params['add_time_start'] ?? ''));
$addEnd = trim((string) ($this->params['add_time_end'] ?? ''));
$effExpr = 'COALESCE(NULLIF(external_first_add_time, 0), create_time)';
if ($addStart !== '') {
$t = strtotime($addStart . ' 00:00:00');
if ($t !== false) {
$query->whereRaw($effExpr . ' > 0 AND ' . $effExpr . ' >= ?', [$t]);
}
}
if ($addEnd !== '') {
$t = strtotime($addEnd . ' 23:59:59');
if ($t !== false) {
$query->whereRaw($effExpr . ' > 0 AND ' . $effExpr . ' <= ?', [$t]);
}
}
return $query;
}
@@ -177,14 +177,14 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
$isCompletedTab = !$pendingWideSearch && isset($this->params['completed_appointment']) && (string) $this->params['completed_appointment'] === '1';
if ($isCompletedTab) {
$maxCompletedAptExpr = '(SELECT MAX(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status = 3' . $minAptDateCond . ')';
$orderRaw = 'IFNULL(' . $maxCompletedAptExpr . ", '1970-01-01 00:00:00') DESC, {$diagTbl}.id DESC";
$orderRaw = $diagTbl . '.assign_read_at IS NULL DESC, IFNULL(' . $maxCompletedAptExpr . ", '1970-01-01 00:00:00') DESC, {$diagTbl}.id DESC";
} else {
$orderRaw = 'CASE IFNULL(' . $minAptStatusExpr . ', 999) WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END ASC, IFNULL(' . $minAptExpr . ", '9999-12-31 23:59:59') ASC, {$diagTbl}.id DESC";
$orderRaw = $diagTbl . '.assign_read_at IS NULL DESC, CASE IFNULL(' . $minAptStatusExpr . ', 999) WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END ASC, IFNULL(' . $minAptExpr . ", '9999-12-31 23:59:59') ASC, {$diagTbl}.id DESC";
}
$lists = $query
->with(['DiagnosisViewRecord'])
->field(['id', 'patient_id', 'patient_name', 'id_card', 'phone', 'gender', 'age', 'diagnosis_date', 'diagnosis_type', 'syndrome_type', 'assistant_id', 'status', 'create_time', 'update_time'])
->field(['id', 'patient_id', 'patient_name', 'id_card', 'phone', 'gender', 'age', 'diagnosis_date', 'diagnosis_type', 'syndrome_type', 'assistant_id', 'assign_read_at', 'status', 'create_time', 'update_time'])
->append(['gender_desc', 'status_desc', 'diagnosis_date_text'])
->orderRaw($orderRaw)
->limit($this->limitOffset, $this->limitLength)
@@ -8,6 +8,8 @@ use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\dept\DeptLogic;
use app\adminapi\logic\stats\YejiStatsLogic;
use app\adminapi\logic\tcm\PrescriptionOrderLogic;
use app\common\enum\ExportEnum;
use app\common\lists\ListsExcelInterface;
use app\common\lists\ListsExtendInterface;
use app\common\lists\ListsSearchInterface;
use app\common\lists\Traits\HasDataScopeFilter;
@@ -23,7 +25,7 @@ use think\facade\Config;
use think\facade\Db;
use think\db\Query;
class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface
class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchInterface, ListsExtendInterface, ListsExcelInterface
{
use HasDataScopeFilter;
@@ -107,7 +109,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$this->applyCreatorOrOwnPrescriptionVisibility($query);
$this->applyDataScopeForPrescriptionOrder($query);
}
// 业绩看板侧栏等:不展示履约已取消(4),与 stats 业绩统计口径一致
// 业绩看板侧栏等:不展示履约 4/9/10,与 stats 业绩口径一致
if ((int) ($this->params['exclude_fulfillment_cancelled'] ?? 0) === 1) {
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, '');
}
@@ -117,6 +119,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
/**
* 业绩看板:仅列出二中心复诊口径下的业务订单(须同时传 assistant_id、start_time、end_timerevisit_slot=0 为全部复诊分项合计)。
* admin 维度按订单创建人 o.creator_id(与部门表复诊列、排行榜复诊列同口径)。
*/
private function applyYejiErCenterRevisitOrderFilter($query): void
{
@@ -138,7 +141,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
if ($t0 === false || $t1 === false) {
return;
}
$ids = DeptLogic::listErCenterRevisitOrderIdsForAssistant(
$ids = DeptLogic::listErCenterRevisitOrderIdsForCreator(
$assistantId,
$slot,
(int) $t0,
@@ -377,7 +380,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$this->applyDataScopeForPrescriptionOrder($query);
}
}
// 与看板「合计业绩」一致:NULL 安全剔除履约已取消(4),勿用 `<>` 以免误丢 NULL 状态行
// 与看板「合计业绩」一致:NULL 安全剔除履约 4/9/10
if ((int) ($this->params['exclude_fulfillment_cancelled'] ?? 0) === 1) {
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, '');
}
@@ -551,8 +554,14 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
}
unset($item);
$this->appendPrescriptionOrderAssignSnapshotErCenterFlags($lists);
if ((int) ($this->params['yeji_order_drawer'] ?? 0) === 1) {
PrescriptionOrderLogic::appendLinkedAppointmentsToListRows($lists);
PrescriptionOrderLogic::appendLinkedAppointmentsToListRows($lists, $this->params);
}
if ((int) $this->export === ExportEnum::EXPORT) {
PrescriptionOrderLogic::appendPrescriptionOrderListExportRows($lists, $this->adminInfo);
}
return $lists;
@@ -569,13 +578,13 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
'stats_order_amount' => $s['order_amount'],
'stats_order_amount_not_cancelled' => $s['order_amount_not_cancelled'],
'stats_order_amount_cancelled' => $s['order_amount_cancelled'],
/** 业绩:业务订单金额,剔除履约已取消(fulfillment_status=4),其余状态全部计入 */
'stats_order_amount_performance' => $s['order_amount_not_cancelled'],
/** 业绩:业务订单金额,剔除履约已取消/拒收/退款(4/9/10)NULL 与其它状态计入 */
'stats_order_amount_performance' => $s['order_amount_performance'] ?? $s['order_amount_not_cancelled'],
'stats_linked_pay_amount' => $s['linked_pay_amount'],
'stats_linked_pay_amount_not_cancelled' => $s['linked_pay_amount_not_cancelled'],
'stats_linked_pay_amount_cancelled' => $s['linked_pay_amount_cancelled'],
/** 业绩-关联实付:剔除履约已取消订单的关联支付金额 */
'stats_linked_pay_amount_performance' => $s['linked_pay_amount_not_cancelled'],
/** 业绩-关联实付:剔除履约 4/9/10 订单的关联支付金额 */
'stats_linked_pay_amount_performance' => $s['linked_pay_amount_performance'] ?? $s['linked_pay_amount_not_cancelled'],
'stats_time_start' => $s['label_start'],
'stats_time_end' => $s['label_end'],
/** 统计口径:all=支付审核白名单全量;assistant=医助仅诊单协助业绩;restricted=与列表同域 */
@@ -598,6 +607,39 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
return $base;
}
public function setFileName(): string
{
return '处方业务订单';
}
public function setExcelFields(): array
{
return [
'export_fulfillment_status_text' => '履约状态',
'export_order_time' => '创建时间',
'doctor_name' => '医生姓名',
'export_patient_name' => '患者姓名',
'export_patient_gender' => '性别',
'export_patient_age' => '年龄',
'export_patient_phone' => '手机号',
'assistant_name' => '医助名字',
'export_center_label' => '一中心还是二中心',
'export_assistant_dept' => '医助部门',
'export_channel' => '渠道',
'export_medication_form' => '药品形态',
'export_medication_days' => '天数',
'export_amount' => '总金额',
'export_paid_amount' => '已付金额',
'export_agency_collect' => '代收金额',
'export_tracking_number' => '快递单号',
'export_supply_mode' => '甘草还是自营',
'export_gancao_prescription_cost' => '处方成本',
'export_first_visit_assistant' => '初诊医助',
'export_rx_audit_time' => '审核时间',
'export_rx_auditor' => '审核人',
];
}
/**
* 业绩看板侧栏专用:约诊/接诊条数,与 YejiStatsLogic::applyConsultFiltersAlignedWithAppointmentLists 同口径
* doctor_appointment.status=3appointment_date 落入区间,有效医助 COALESCE(挂号.assistant_id,诊单.assistant_id))。
@@ -692,7 +734,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$t1 = $tmp;
}
$q = $this->buildBaseQueryForStats(false)->whereBetween('create_time', [$t0, $t1]);
// 全量「合计业绩」与看板列同口径:始终剔除履约已取消(4),不依赖 exclude 参数
// 全量「合计业绩」与看板列同口径:始终剔除履约 4/9/10,不依赖 exclude 参数
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, '');
$amount = round((float) (clone $q)->sum('amount'), 2);
$count = (int) (clone $q)->count();
@@ -714,7 +756,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($pendingRxQuery, '');
return [
// 处方审核待处理(不含履约已取消
// 处方审核待处理(不含履约 4/9/10
'pending_rx' => (int) $pendingRxQuery->where('prescription_audit_status', 0)->count(),
// 支付审核待处理(前提:处方已通过)
'pending_pay' => (int) (clone $query)->where('prescription_audit_status', 1)->where('payment_slip_audit_status', 0)->count(),
@@ -760,8 +802,8 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
}
$q = $this->buildBaseQueryForStats()->whereBetween('create_time', [$t0, $t1]);
$orderTotal = round((float) (clone $q)->sum('amount'), 2);
$orderCancelled = round((float) (clone $q)->where('fulfillment_status', 4)->sum('amount'), 2);
$orderTotal = round((float) (clone $q)->sum('amount'), 2);
$orderCancelled = round((float) (clone $q)->where('fulfillment_status', 4)->sum('amount'), 2);
$orderNotCancelled = round($orderTotal - $orderCancelled, 2);
$idsAll = (clone $q)->column('id');
@@ -769,7 +811,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
return $id > 0;
}));
if ($idsAll === []) {
return $this->statsExtendPayload(
$empty = $this->statsExtendPayload(
$orderTotal,
$orderNotCancelled,
$orderCancelled,
@@ -779,6 +821,12 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$l0,
$l1
);
$qPerfEmpty = clone $q;
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($qPerfEmpty, '');
$empty['order_amount_performance'] = round((float) $qPerfEmpty->sum('amount'), 2);
$empty['linked_pay_amount_performance'] = 0.0;
return $empty;
}
$idsCancel = (clone $q)->where('fulfillment_status', 4)->column('id');
$idsCancel = array_values(array_filter(array_map('intval', $idsCancel), static function (int $id): bool {
@@ -790,7 +838,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$linkedNotCancelled = $this->sumLinkedPayForPrescriptionOrderIds($idsNotCancelled);
$linkedCancelled = $this->sumLinkedPayForPrescriptionOrderIds($idsCancel);
return $this->statsExtendPayload(
$base = $this->statsExtendPayload(
$orderTotal,
$orderNotCancelled,
$orderCancelled,
@@ -800,6 +848,21 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$l0,
$l1
);
$qPerf = clone $q;
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($qPerf, '');
$base['order_amount_performance'] = round((float) $qPerf->sum('amount'), 2);
$idsExcludedPerf = (clone $q)
->whereIn('fulfillment_status', YejiStatsLogic::PRESCRIPTION_ORDER_FULFILLMENT_EXCLUDED_FROM_PERFORMANCE)
->column('id');
$idsExcludedPerf = array_values(array_filter(array_map('intval', $idsExcludedPerf), static function (int $id): bool {
return $id > 0;
}));
$idsPerf = array_values(array_diff($idsAll, $idsExcludedPerf));
$base['linked_pay_amount_performance'] = $this->sumLinkedPayForPrescriptionOrderIds($idsPerf);
return $base;
}
/**
@@ -807,7 +870,11 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
*/
private function statsZeroPayload(): array
{
return $this->statsExtendPayload(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, '', '');
$z = $this->statsExtendPayload(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, '', '');
$z['order_amount_performance'] = 0.0;
$z['linked_pay_amount_performance'] = 0.0;
return $z;
}
/**
@@ -870,16 +937,23 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
$query->whereExists("SELECT 1 FROM {$rxTbl} rx WHERE rx.id = {$poTbl}.prescription_id AND rx.delete_time IS NULL AND rx.creator_id = {$doctorId}");
}
/** 业绩看板入口(yeji_order_drawer=1)所有筛选统一按订单创建人;其它入口(处方订单列表 / 患者跟进等)保留旧口径(创建人 ∪ 诊单医助) */
$yejiPanel = (int) ($this->params['yeji_order_drawer'] ?? 0) === 1;
if (isset($this->params['assistant_id']) && (int) $this->params['assistant_id'] > 0) {
$assistantId = (int) $this->params['assistant_id'];
$diagTbl = (new Diagnosis())->getTable();
$query->where(function ($q) use ($poTbl, $diagTbl, $assistantId) {
$q->where('creator_id', $assistantId);
$q->whereOrRaw(
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id`"
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$assistantId})"
);
});
if ($yejiPanel) {
$query->where('creator_id', $assistantId);
} else {
$diagTbl = (new Diagnosis())->getTable();
$query->where(function ($q) use ($poTbl, $diagTbl, $assistantId) {
$q->where('creator_id', $assistantId);
$q->whereOrRaw(
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$poTbl}`.`diagnosis_id`"
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$assistantId})"
);
});
}
}
if (isset($this->params['assistant_dept_id']) && $this->params['assistant_dept_id'] !== '' && (int) $this->params['assistant_dept_id'] > 0) {
@@ -898,6 +972,25 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
return;
}
$yejiTableRowDeptIds = [];
if (isset($this->params['yeji_table_row_dept_ids']) && trim((string) $this->params['yeji_table_row_dept_ids']) !== '') {
foreach (explode(',', (string) $this->params['yeji_table_row_dept_ids']) as $p) {
$v = (int) trim((string) $p);
if ($v > 0) {
$yejiTableRowDeptIds[] = $v;
}
}
}
if ((int) ($this->params['yeji_order_drawer'] ?? 0) === 1 && $yejiTableRowDeptIds !== []) {
YejiStatsLogic::applyPrescriptionOrderListAlignedWinnerDeptFilter(
$query,
(int) $this->params['assistant_dept_id'],
$yejiTableRowDeptIds,
$this->params['dept_ids'] ?? null
);
return;
}
$rootDeptId = (int) $this->params['assistant_dept_id'];
$deptIds = DeptLogic::getSelfAndDescendantIds($rootDeptId);
$deptIds = array_values(array_filter(array_map('intval', $deptIds), static function (int $id): bool {
@@ -909,23 +1002,31 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
return;
}
$inList = implode(',', $deptIds);
$diagTbl = (new Diagnosis())->getTable();
$adTbl = (new AdminDept())->getTable();
$query->where(function ($q) use ($poTbl, $diagTbl, $adTbl, $inList) {
$q->whereExists(
"SELECT 1 FROM `{$adTbl}` ad INNER JOIN `{$diagTbl}` dg ON ad.admin_id = dg.assistant_id AND dg.delete_time IS NULL "
. "WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` AND ad.dept_id IN ({$inList})"
if ($yejiPanel) {
/** 业绩看板部门侧栏:仅订单创建人人事部门 ∈ 子树(与「合计业绩」/「接诊诊单」/「复诊」聚合同口径) */
$query->whereExists(
"SELECT 1 FROM `{$adTbl}` adc WHERE adc.`admin_id` = `{$poTbl}`.`creator_id` AND adc.`dept_id` IN ({$inList})"
);
$q->whereExists(
"SELECT 1 FROM `{$adTbl}` adc WHERE adc.`admin_id` = `{$poTbl}`.`creator_id` AND adc.`dept_id` IN ({$inList})",
'OR'
);
});
} else {
$diagTbl = (new Diagnosis())->getTable();
$query->where(function ($q) use ($poTbl, $diagTbl, $adTbl, $inList) {
$q->whereExists(
"SELECT 1 FROM `{$adTbl}` ad INNER JOIN `{$diagTbl}` dg ON ad.admin_id = dg.assistant_id AND dg.delete_time IS NULL "
. "WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` AND ad.dept_id IN ({$inList})"
);
$q->whereExists(
"SELECT 1 FROM `{$adTbl}` adc WHERE adc.`admin_id` = `{$poTbl}`.`creator_id` AND adc.`dept_id` IN ({$inList})",
'OR'
);
});
}
}
}
/**
* 业绩看板侧栏:只保留「业绩归属医助」落在给定 admin_id 集合的订单(与 YejiStatsLogic::sqlPerformanceAttributionAdminExpr 一致)。
* 业绩看板侧栏:只保留「业绩归属医助」落在给定 admin_id 集合的订单
* (与 YejiStatsLogic::sqlPerformanceAttributionAdminExpr 一致——按订单创建人)。
*
* @param int[] $adminIds
*/
@@ -939,18 +1040,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
return;
}
$poTbl = (new PrescriptionOrder())->getTable();
$diagTbl = (new Diagnosis())->getTable();
$rxTbl = (new Prescription())->getTable();
$inList = implode(',', $adminIds);
$query->whereRaw(
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg "
. "LEFT JOIN `{$rxTbl}` rx ON rx.`id` = `{$poTbl}`.`prescription_id` AND rx.`delete_time` IS NULL "
. "WHERE dg.`id` = `{$poTbl}`.`diagnosis_id` AND dg.`delete_time` IS NULL AND `{$poTbl}`.`diagnosis_id` > 0 AND "
. '(CASE WHEN dg.`assistant_id` > 0 THEN dg.`assistant_id` '
. "WHEN `{$poTbl}`.`creator_id` > 0 AND (rx.`id` IS NULL OR NOT (`{$poTbl}`.`creator_id` <=> rx.`creator_id`)) THEN `{$poTbl}`.`creator_id` "
. 'ELSE 0 END) IN (' . $inList . '))'
);
$query->whereIn('creator_id', $adminIds);
}
/**
@@ -1020,4 +1110,118 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
. ")"
);
}
/**
* 列表行标注:指派日志快照(related_po_creator_id + related_po_create_time)是否指向本业务单,
* 以及该次操作的新医助(to_assistant_id)是否归属「二中心」部门子树(与 DeptLogic / 业绩看板一致)。
*
* @param array<int, array<string, mixed>> $lists
*/
private function appendPrescriptionOrderAssignSnapshotErCenterFlags(array &$lists): void
{
if ($lists === []) {
return;
}
$diagIds = [];
foreach ($lists as $row) {
$d = (int) ($row['diagnosis_id'] ?? 0);
if ($d > 0) {
$diagIds[$d] = true;
}
}
$diagIdList = array_keys($diagIds);
$latestSnapLogByTriple = [];
if ($diagIdList !== []) {
$logRows = Db::name('tcm_diagnosis_assign_log')
->whereIn('diagnosis_id', $diagIdList)
->field(['id', 'diagnosis_id', 'to_assistant_id', 'related_po_creator_id', 'related_po_create_time'])
->order('id', 'desc')
->select()
->toArray();
foreach ($logRows as $log) {
$d = (int) ($log['diagnosis_id'] ?? 0);
$rcp = (int) ($log['related_po_creator_id'] ?? 0);
$rpct = (int) ($log['related_po_create_time'] ?? 0);
if ($d <= 0 || $rcp <= 0 || $rpct <= 0) {
continue;
}
$tripleKey = $d . ':' . $rcp . ':' . $rpct;
if (!isset($latestSnapLogByTriple[$tripleKey])) {
$latestSnapLogByTriple[$tripleKey] = $log;
}
}
}
$toAssistantIdsHit = [];
foreach ($lists as $item) {
$diagId = (int) ($item['diagnosis_id'] ?? 0);
$creatorId = (int) ($item['creator_id'] ?? 0);
$poCt = (int) ($item['create_time'] ?? 0);
$tripleKey = $diagId . ':' . $creatorId . ':' . $poCt;
$log = $latestSnapLogByTriple[$tripleKey] ?? null;
if (\is_array($log)) {
$tid = (int) ($log['to_assistant_id'] ?? 0);
if ($tid > 0) {
$toAssistantIdsHit[$tid] = true;
}
}
}
$toAssistantIdList = array_keys($toAssistantIdsHit);
$erDeptSet = DeptLogic::getErCenterSubtreeDeptIdSet();
$adminErCenter = [];
if ($toAssistantIdList !== [] && $erDeptSet !== []) {
$adRows = AdminDept::whereIn('admin_id', $toAssistantIdList)
->field(['admin_id', 'dept_id'])
->select()
->toArray();
foreach ($adRows as $ad) {
$aid = (int) ($ad['admin_id'] ?? 0);
$did = (int) ($ad['dept_id'] ?? 0);
if ($aid > 0 && $did > 0 && isset($erDeptSet[$did])) {
$adminErCenter[$aid] = true;
}
}
}
$nameMap = [];
if ($toAssistantIdList !== []) {
$nameMap = \app\common\model\auth\Admin::whereIn('id', $toAssistantIdList)->column('name', 'id');
}
foreach ($lists as &$item) {
$diagId = (int) ($item['diagnosis_id'] ?? 0);
$creatorId = (int) ($item['creator_id'] ?? 0);
$poCt = (int) ($item['create_time'] ?? 0);
$item['po_assign_snapshot_hit'] = 0;
$item['po_assign_to_assistant_id'] = 0;
$item['po_assign_to_assistant_name'] = '';
$item['po_assign_to_er_center'] = 0;
$item['po_assign_is_release'] = 0;
if ($diagId <= 0 || $creatorId <= 0 || $poCt <= 0) {
continue;
}
$tripleKey = $diagId . ':' . $creatorId . ':' . $poCt;
$log = $latestSnapLogByTriple[$tripleKey] ?? null;
if (!\is_array($log)) {
continue;
}
$item['po_assign_snapshot_hit'] = 1;
$toId = (int) ($log['to_assistant_id'] ?? 0);
$item['po_assign_to_assistant_id'] = $toId;
if ($toId <= 0) {
$item['po_assign_is_release'] = 1;
continue;
}
$item['po_assign_to_assistant_name'] = (string) ($nameMap[$toId] ?? '');
$item['po_assign_to_er_center'] = isset($adminErCenter[$toId]) ? 1 : 0;
}
unset($item);
}
}
+56 -72
View File
@@ -14,6 +14,7 @@
namespace app\adminapi\logic\dept;
use app\adminapi\logic\stats\YejiStatsLogic;
use app\common\logic\BaseLogic;
use app\common\model\auth\Admin;
use app\common\model\auth\AdminDept;
@@ -212,28 +213,31 @@ class DeptLogic extends BaseLogic
->join('tcm_diagnosis dg', 'dg.id = o.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
->whereNull('o.delete_time')
->where('o.diagnosis_id', '>', 0)
->where('dg.assistant_id', '>', 0)
->whereRaw('NOT (o.fulfillment_status <=> ?)', [4]);
->where('dg.assistant_id', '>', 0);
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, 'o');
if ($orderStartTs !== null && $orderEndTs !== null) {
$q->where('o.create_time', 'between', [$orderStartTs, $orderEndTs]);
}
return $q
->field(['o.diagnosis_id', 'o.create_time', 'o.id', 'dg.assistant_id'])
->field(['o.diagnosis_id', 'o.create_time', 'o.id', 'dg.assistant_id', 'o.creator_id'])
->order(['o.diagnosis_id' => 'asc', 'o.create_time' => 'asc', 'o.id' => 'asc'])
->select()
->toArray();
}
/**
* 二中心子树:各部门复诊 rollup;同时返回按诊单医助聚合的分项(供医助排行榜)。
* 二中心子树:各部门复诊 rollup(按订单创建人 creator_id 归属)。
* 候选订单仍要求 dg.assistant_id > 0(保持「er-center 案例 = 有医助的诊单」的定义),
* **复诊归属**改为订单创建人——leaf 部门归属取 creator_id 在二中心子树内的 canonical 部门,
* 并产出 by_creator_slots 供医助排行榜复诊列复用,与诊金 / 接诊诊单口径完全一致。
*
* @param list<int> $subtreeDeptIds
*
* @return array{
* totals: array<int, int>,
* slots: array<int, array<int, int>>,
* by_assistant_slots: array<int, array<int, int>>
* by_creator_slots: array<int, array<int, int>>
* }
*/
private static function computeErCenterRevisitFullPack(
@@ -249,12 +253,12 @@ class DeptLogic extends BaseLogic
$emptyTotals[$did] = 0;
}
if ($subtreeDeptIds === []) {
return ['totals' => [], 'slots' => [], 'by_assistant_slots' => []];
return ['totals' => [], 'slots' => [], 'by_creator_slots' => []];
}
$subtreeSet = array_fill_keys($subtreeDeptIds, true);
$orderRows = self::fetchErCenterRevisitCandidateOrders($orderStartTs, $orderEndTs);
if ($orderRows === []) {
return ['totals' => $emptyTotals, 'slots' => $emptySlots, 'by_assistant_slots' => []];
return ['totals' => $emptyTotals, 'slots' => $emptySlots, 'by_creator_slots' => []];
}
$byPatient = [];
@@ -266,29 +270,30 @@ class DeptLogic extends BaseLogic
$byPatient[$pid][] = $r;
}
$assistantNeed = [];
$creatorNeed = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
if ($aid > 0) {
$assistantNeed[$aid] = true;
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
if ($cid > 0) {
$creatorNeed[$cid] = true;
}
}
}
$assistantIds = array_keys($assistantNeed);
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree($assistantIds, $subtreeSet);
$creatorIds = array_keys($creatorNeed);
/** 复用 buildAssistantCanonicalDeptInSubtree —— 该方法实际是 admin → 子树内 canonical 部门,与 admin 角色无关 */
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree($creatorIds, $subtreeSet);
/** @var array<int, array<int, int>> $leafByDeptSlot dept_id => [ slot => cnt ]slot≥2 */
$leafByDeptSlot = [];
/** @var array<int, array<int, int>> $byAssistantSlot */
$byAssistantSlot = [];
/** @var array<int, array<int, int>> $byCreatorSlot 复诊按订单创建人聚合(与诊金/接诊诊单口径一致) */
$byCreatorSlot = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$slot = $i + 1;
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
$deptId = $canonicalDept[$aid] ?? null;
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
$deptId = $canonicalDept[$cid] ?? null;
if ($deptId === null || !isset($subtreeSet[$deptId])) {
continue;
}
@@ -296,11 +301,11 @@ class DeptLogic extends BaseLogic
$leafByDeptSlot[$deptId] = [];
}
$leafByDeptSlot[$deptId][$slot] = ($leafByDeptSlot[$deptId][$slot] ?? 0) + 1;
if ($aid > 0) {
if (!isset($byAssistantSlot[$aid])) {
$byAssistantSlot[$aid] = [];
if ($cid > 0) {
if (!isset($byCreatorSlot[$cid])) {
$byCreatorSlot[$cid] = [];
}
$byAssistantSlot[$aid][$slot] = ($byAssistantSlot[$aid][$slot] ?? 0) + 1;
$byCreatorSlot[$cid][$slot] = ($byCreatorSlot[$cid][$slot] ?? 0) + 1;
}
}
}
@@ -313,16 +318,16 @@ class DeptLogic extends BaseLogic
$rollupTotals[$id] = array_sum($rollupSlots[$id] ?? []);
}
foreach ($byAssistantSlot as $aid => $sm) {
foreach ($byCreatorSlot as $cid => $sm) {
if ($sm !== []) {
ksort($byAssistantSlot[$aid], SORT_NUMERIC);
ksort($byCreatorSlot[$cid], SORT_NUMERIC);
}
}
return [
'totals' => $rollupTotals,
'slots' => $rollupSlots,
'by_assistant_slots' => $byAssistantSlot,
'by_creator_slots' => $byCreatorSlot,
];
}
@@ -345,11 +350,11 @@ class DeptLogic extends BaseLogic
}
/**
* 二中心子树内:各医助成交的复诊业务单笔数(合计 + 复诊2/3/…分项),口径与部门 rollup 同源
* 二中心子树复诊:按订单创建人 creator_id 聚合(与部门 rollup 同源,与诊金/接诊诊单口径一致)
*
* @return array{totals: array<int, int>, slots: array<int, array<int, int>>}
*/
public static function getErCenterRevisitCountsByAssistant(?int $orderStartTs = null, ?int $orderEndTs = null): array
public static function getErCenterRevisitCountsByCreator(?int $orderStartTs = null, ?int $orderEndTs = null): array
{
$erRoots = self::findErCenterRootDeptIds();
$subtreeIds = self::unionErCenterSubtreeDeptIds($erRoots);
@@ -357,42 +362,36 @@ class DeptLogic extends BaseLogic
return ['totals' => [], 'slots' => []];
}
$pack = self::computeErCenterRevisitFullPack($subtreeIds, $orderStartTs, $orderEndTs);
$by = $pack['by_assistant_slots'];
$by = $pack['by_creator_slots'];
$totals = [];
$slots = [];
foreach ($by as $aid => $slotMap) {
$aid = (int) $aid;
$totals[$aid] = (int) array_sum($slotMap);
$slots[$aid] = $slotMap;
foreach ($by as $cid => $slotMap) {
$cid = (int) $cid;
$totals[$cid] = (int) array_sum($slotMap);
$slots[$cid] = $slotMap;
}
return ['totals' => $totals, 'slots' => $slots];
}
/**
* 某医助在区间内、计入二中心复诊的业务订单 id(用于看板侧栏按复诊笔数下钻)。
* 二中心子树复诊:列出某「订单创建人」在区间内、复诊序列(slot≥2)命中的业务订单 id(用于看板侧栏按复诊笔数下钻)。
* $revisitSlot = 0 表示累计全部复诊(第 2 笔起);≥2 时仅该分项。
*
* @return list<int>
*/
public static function listErCenterRevisitOrderIdsForAssistant(
int $assistantId,
public static function listErCenterRevisitOrderIdsForCreator(
int $creatorId,
int $revisitSlot,
int $orderStartTs,
int $orderEndTs
): array {
if ($assistantId <= 0 || $orderStartTs <= 0 || $orderEndTs < $orderStartTs) {
if ($creatorId <= 0 || $orderStartTs <= 0 || $orderEndTs < $orderStartTs) {
return [];
}
if ($revisitSlot < 0 || $revisitSlot === 1) {
return [];
}
$erRoots = self::findErCenterRootDeptIds();
$subtreeIds = self::unionErCenterSubtreeDeptIds($erRoots);
if ($subtreeIds === []) {
return [];
}
$subtreeSet = array_fill_keys($subtreeIds, true);
$orderRows = self::fetchErCenterRevisitCandidateOrders($orderStartTs, $orderEndTs);
if ($orderRows === []) {
return [];
@@ -405,31 +404,16 @@ class DeptLogic extends BaseLogic
}
$byPatient[$pid][] = $r;
}
$assistantNeed = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
if ($aid > 0) {
$assistantNeed[$aid] = true;
}
}
}
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree(array_keys($assistantNeed), $subtreeSet);
$ids = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$slot = $i + 1;
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
if ($aid !== $assistantId) {
continue;
}
if ($revisitSlot > 0 && $slot !== $revisitSlot) {
continue;
}
$dcan = $canonicalDept[$aid] ?? null;
if ($dcan === null || !isset($subtreeSet[$dcan])) {
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
if ($cid !== $creatorId) {
continue;
}
$oid = (int) ($orders[$i]['id'] ?? 0);
@@ -443,8 +427,8 @@ class DeptLogic extends BaseLogic
}
/**
* 某展示部门行(组织架构上含下级 rollup)内,二中心复诊按诊单医助拆解笔数。
* 与看板行内「复诊」列口径一致:诊单下订单序列第 2 笔起;医助 canonical 部门须落在二中心子树且在该部门行的子树内。
* 某展示部门行(组织架构上含下级 rollup)内,二中心复诊按**订单创建人**拆解笔数。
* 与看板行内「复诊」列口径一致:订单序列第 2 笔起;创建人 canonical 部门须落在二中心子树且在该部门行的子树内。
*
* @return array{rows: list<array{admin_id: int, name: string, order_count: int}>}
*/
@@ -484,17 +468,17 @@ class DeptLogic extends BaseLogic
}
$byPatient[$pid][] = $r;
}
$assistantNeed = [];
$creatorNeed = [];
foreach ($byPatient as $orders) {
$n = \count($orders);
for ($i = 1; $i < $n; $i++) {
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
if ($aid > 0) {
$assistantNeed[$aid] = true;
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
if ($cid > 0) {
$creatorNeed[$cid] = true;
}
}
}
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree(array_keys($assistantNeed), $erSubtreeSet);
$canonicalDept = self::buildAssistantCanonicalDeptInSubtree(array_keys($creatorNeed), $erSubtreeSet);
/** @var array<int, int> $counts */
$counts = [];
foreach ($byPatient as $orders) {
@@ -504,15 +488,15 @@ class DeptLogic extends BaseLogic
if ($revisitSlot > 0 && $slot !== $revisitSlot) {
continue;
}
$aid = (int) ($orders[$i]['assistant_id'] ?? 0);
if ($aid <= 0) {
$cid = (int) ($orders[$i]['creator_id'] ?? 0);
if ($cid <= 0) {
continue;
}
$canon = $canonicalDept[$aid] ?? null;
$canon = $canonicalDept[$cid] ?? null;
if ($canon === null || !isset($erSubtreeSet[$canon]) || !isset($descFlip[$canon])) {
continue;
}
$counts[$aid] = ($counts[$aid] ?? 0) + 1;
$counts[$cid] = ($counts[$cid] ?? 0) + 1;
}
}
if ($counts === []) {
@@ -527,10 +511,10 @@ class DeptLogic extends BaseLogic
->column('name', 'id');
}
$rows = [];
foreach ($counts as $aid => $cnt) {
foreach ($counts as $cid => $cnt) {
$rows[] = [
'admin_id' => (int) $aid,
'name' => (string) ($nameRows[$aid] ?? ('#' . $aid)),
'admin_id' => (int) $cid,
'name' => (string) ($nameRows[$cid] ?? ('#' . $cid)),
'order_count' => (int) $cnt,
];
}
@@ -9,6 +9,8 @@ use app\common\service\doctor\RosterSegmentService;
use app\common\model\tcm\Diagnosis;
use app\common\model\tcm\DiagnosisGuahaoLog;
use app\common\model\auth\Admin;
use app\common\model\auth\AdminRole;
use app\common\service\DataScope\DataScopeService;
use app\api\logic\ChatNotifyLogic;
use app\adminapi\logic\tcm\PrescriptionLogic;
use app\adminapi\logic\tcm\DiagnosisLogic;
@@ -28,6 +30,75 @@ use think\facade\Log;
*/
class AppointmentLogic extends BaseLogic
{
/** 仅这些字典名称需填写 channel_source_detail(与 admin 预约弹窗白名单一致,按 name 全等) */
private const CHANNEL_NAMES_REQUIRING_SOURCE_DETAIL = [
'自媒体4H',
'自媒体3Q',
'自媒体3H',
'自媒体2H',
'自媒体2Q',
];
private static function channelDictNameRequiresSourceDetail(string $name): bool
{
return in_array($name, self::CHANNEL_NAMES_REQUIRING_SOURCE_DETAIL, true);
}
/**
* @param array<int|string, mixed> $cols Db::name('doctor_appointment')->getTableFields()
*/
private static function assertAppointmentChannelWritable(array $cols, string $chSrc): ?string
{
$hasChannelSource = in_array('channel_source', $cols, true);
$hasChannels = in_array('channels', $cols, true);
if (!$hasChannelSource && !$hasChannels) {
return '挂号表缺少渠道字段(channel_source 或 channels),无法保存渠道来源';
}
if (!$hasChannelSource && $hasChannels && $chSrc !== '' && !is_numeric($chSrc)) {
return '当前挂号表仅有数值型渠道字段 channels,无法写入该渠道;请在数据库增加 channel_source 列';
}
return null;
}
/**
* @param array<string, mixed> $row
* @param array<int|string, mixed> $cols
*/
private static function mergeAppointmentChannelIntoRow(array &$row, array $cols, string $chSrc, string $chDetail): void
{
$hasChannelSource = in_array('channel_source', $cols, true);
$hasChannelDetail = in_array('channel_source_detail', $cols, true);
$hasChannels = in_array('channels', $cols, true);
if ($hasChannelSource) {
$row['channel_source'] = $chSrc;
}
if ($hasChannelDetail) {
$row['channel_source_detail'] = $chDetail;
}
if ($hasChannels && is_numeric($chSrc)) {
$row['channels'] = (int) $chSrc;
}
}
/**
* @param array<string, mixed> $row
* @param array<int|string, mixed> $cols
*
* @return array<string, mixed>
*/
private static function filterAppointmentRowByExistingColumns(array $row, array $cols): array
{
$out = [];
foreach ($row as $k => $v) {
if (in_array((string) $k, $cols, true)) {
$out[$k] = $v;
}
}
return $out;
}
/**
* @notes 获取可用时间段
* @param array $params
@@ -233,24 +304,64 @@ class AppointmentLogic extends BaseLogic
Db::rollback();
return false;
}
$data = [
'patient_id' => $params['patient_id'],
'assistant_id'=>$params['assistant_id'],
'doctor_id' => $params['doctor_id'],
'roster_id' => 0, // 不再关联排班表
$chSrc = trim((string) ($params['channel_source'] ?? ''));
$chDetail = trim((string) ($params['channel_source_detail'] ?? ''));
$channelDictName = trim((string) (DictData::where('type_value', 'channels')
->where('value', $chSrc)
->value('name') ?? ''));
if ($channelDictName !== '' && self::channelDictNameRequiresSourceDetail($channelDictName)) {
if ($chDetail === '') {
self::setError('请填写自媒体渠道补充信息');
Db::rollback();
return false;
}
}
$cols = Db::name('doctor_appointment')->getTableFields();
$cols = is_array($cols) ? $cols : [];
$channelErr = self::assertAppointmentChannelWritable($cols, $chSrc);
if ($channelErr !== null) {
self::setError($channelErr);
Db::rollback();
return false;
}
$insert = [
'patient_id' => (int) $params['patient_id'],
'doctor_id' => (int) $params['doctor_id'],
'appointment_date' => $params['appointment_date'],
'period' => $params['period'] ?? 'all',
'appointment_time' => $appointmentTime,
'appointment_type' => $params['appointment_type'] ?? 'video',
'remark' => $params['remark'] ?? '',
// 表字段为 channel_source(与字典 type_value=channels 的 value 一致);勿再写入不存在的 channels 列
'channel_source' => trim((string) ($params['channel_source'] ?? '')),
'status' => 1,
'create_time' => time(),
'update_time' => time(),
];
if (in_array('roster_id', $cols, true)) {
$insert['roster_id'] = 0;
}
if (in_array('assistant_id', $cols, true)) {
$insert['assistant_id'] = (int) ($params['assistant_id'] ?? 0);
}
$periodVal = $params['period'] ?? 'all';
if (in_array('period', $cols, true)) {
$insert['period'] = $periodVal;
} elseif (in_array('type', $cols, true)) {
$insert['type'] = $periodVal;
}
self::mergeAppointmentChannelIntoRow($insert, $cols, $chSrc, $chDetail);
$appointment = Appointment::create($data);
$insert = self::filterAppointmentRowByExistingColumns($insert, $cols);
$newId = (int) Db::name('doctor_appointment')->insertGetId($insert);
if ($newId <= 0) {
self::setError('预约创建失败');
Db::rollback();
return false;
}
Db::commit();
@@ -258,21 +369,21 @@ class AppointmentLogic extends BaseLogic
$hm = substr((string) $appointmentTime, 0, 5);
$summary = sprintf(
'挂号 #%d:医生「%s」,%s %s',
(int) $appointment->id,
$newId,
$doctorName !== '' ? $doctorName : ('ID:' . (int) $params['doctor_id']),
(string) $params['appointment_date'],
$hm
);
self::appendDiagnosisGuahaoLog(
(int) $params['patient_id'],
(int) $appointment->id,
$newId,
$operatorAdminId,
$operatorAdminInfo,
'create',
$summary
);
return ['id' => $appointment->id];
return ['id' => $newId];
} catch (\Exception $e) {
Db::rollback();
self::setError($e->getMessage());
@@ -757,6 +868,244 @@ class AppointmentLogic extends BaseLogic
}
}
/**
* AppointmentLists 一致的可见性(不含 progress_board / diag_scope_relax
*/
private static function appointmentRowManageableByAdmin(
Appointment $appointment,
?Diagnosis $diag,
int $adminId,
array $adminInfo
): bool {
$roleIds = array_map('intval', AdminRole::where('admin_id', $adminId)->column('role_id'));
if (in_array(1, $roleIds, true) && (int) $appointment->doctor_id !== $adminId) {
return false;
}
if (in_array(2, $roleIds, true)) {
$asst = $diag ? (int) $diag->assistant_id : 0;
if ($asst !== $adminId) {
return false;
}
}
if (!DataScopeService::isEnabled()) {
return true;
}
$ids = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
if ($ids === []) {
return false;
}
if ($ids === null) {
return true;
}
$docId = (int) $appointment->doctor_id;
$asstId = $diag ? (int) $diag->assistant_id : 0;
return in_array($docId, $ids, true)
|| ($asstId > 0 && in_array($asstId, $ids, true));
}
/**
* 后台编辑挂号(预约日期/时段/类型/状态/备注/医助)
*
* @param array<string, mixed> $params
*/
public static function adminEdit(array $params, int $adminId, array $adminInfo): bool
{
try {
$id = (int) ($params['id'] ?? 0);
if ($id <= 0) {
self::setError('参数错误');
return false;
}
$appointment = Appointment::findOrEmpty($id);
if ($appointment->isEmpty()) {
self::setError('预约记录不存在');
return false;
}
$diag = Diagnosis::where('id', (int) $appointment->patient_id)->whereNull('delete_time')->find();
if (!self::appointmentRowManageableByAdmin($appointment, $diag ?: null, $adminId, $adminInfo)) {
self::setError('无权限编辑');
return false;
}
$period = trim((string) ($params['period'] ?? ''));
if (!in_array($period, ['morning', 'afternoon', 'all'], true)) {
self::setError('时段无效');
return false;
}
$appointmentDate = trim((string) ($params['appointment_date'] ?? ''));
if ($appointmentDate === '') {
self::setError('预约日期不能为空');
return false;
}
$rawTime = trim((string) ($params['appointment_time'] ?? ''));
if ($rawTime === '') {
self::setError('预约时间不能为空');
return false;
}
$appointmentTime = strlen($rawTime) === 5 ? $rawTime . ':00' : $rawTime;
$status = (int) ($params['status'] ?? 0);
if ($status < 1 || $status > 4) {
self::setError('状态无效');
return false;
}
$appointmentType = trim((string) ($params['appointment_type'] ?? ''));
if (!in_array($appointmentType, ['video', 'text', 'phone'], true)) {
self::setError('预约类型无效');
return false;
}
$remark = isset($params['remark']) ? trim((string) $params['remark']) : '';
if (mb_strlen($remark) > 500) {
self::setError('备注过长');
return false;
}
$chSrc = trim((string) ($params['channel_source'] ?? ''));
if ($chSrc === '') {
self::setError('请选择渠道来源');
return false;
}
$chDetail = trim((string) ($params['channel_source_detail'] ?? ''));
$channelDictName = trim((string) (DictData::where('type_value', 'channels')
->where('value', $chSrc)
->value('name') ?? ''));
if ($channelDictName !== '' && self::channelDictNameRequiresSourceDetail($channelDictName)) {
if ($chDetail === '') {
self::setError('请填写自媒体渠道补充信息');
return false;
}
}
if (mb_strlen($chDetail) > 128) {
self::setError('渠道补充说明过长');
return false;
}
$postAssistant = array_key_exists('assistant_id', $params);
$assistantId = null;
if ($postAssistant) {
$assistantRaw = $params['assistant_id'];
if ($assistantRaw !== null && $assistantRaw !== '') {
$aid = (int) $assistantRaw;
$assistantId = $aid > 0 ? $aid : null;
}
}
$patientId = (int) $appointment->patient_id;
$doctorId = (int) $appointment->doctor_id;
if (in_array($status, [1, 4], true)) {
$patientDup = Appointment::where('patient_id', $patientId)
->where('appointment_date', $appointmentDate)
->whereIn('status', [1, 4])
->where('id', '<>', $id)
->find();
if ($patientDup) {
self::setError('该诊单在所选日期已有其他有效挂号(已预约/已过号)');
return false;
}
}
if ($status === 1) {
$slotDup = Appointment::where('doctor_id', $doctorId)
->where('appointment_date', $appointmentDate)
->where('appointment_time', $appointmentTime)
->where('status', 1)
->where('id', '<>', $id)
->find();
if ($slotDup) {
self::setError('该医生在所选时段已被占用');
return false;
}
}
$beforeDate = (string) ($appointment->appointment_date ?? '');
$beforeTimeRaw = (string) ($appointment->appointment_time ?? '');
$beforeHm = strlen($beforeTimeRaw) >= 5 ? substr($beforeTimeRaw, 0, 5) : $beforeTimeRaw;
$snap = $appointment->toArray();
$beforePeriod = (string) ($snap['period'] ?? $snap['type'] ?? '');
$beforeStatus = (int) $appointment->status;
$tblFields = Db::name('doctor_appointment')->getTableFields();
$cols = is_array($tblFields) ? $tblFields : [];
$channelErr = self::assertAppointmentChannelWritable($cols, $chSrc);
if ($channelErr !== null) {
self::setError($channelErr);
return false;
}
Db::startTrans();
$saveData = [
'appointment_date' => $appointmentDate,
'appointment_time' => $appointmentTime,
'appointment_type' => $appointmentType,
'status' => $status,
'remark' => $remark,
'update_time' => time(),
];
if (in_array('assistant_id', $cols, true) && $postAssistant) {
$saveData['assistant_id'] = $assistantId;
}
if (in_array('period', $cols, true)) {
$saveData['period'] = $period;
} elseif (in_array('type', $cols, true)) {
$saveData['type'] = $period;
}
self::mergeAppointmentChannelIntoRow($saveData, $cols, $chSrc, $chDetail);
Db::name('doctor_appointment')->where('id', $id)->update($saveData);
Db::commit();
$hm = substr($appointmentTime, 0, 5);
$summary = sprintf(
'后台修改挂号 #%d:日期 %s %s→%s %s,时段 %s→%s,状态 %d→%d',
$id,
$beforeDate,
$beforeHm,
$appointmentDate,
$hm,
$beforePeriod !== '' ? $beforePeriod : '—',
$period,
$beforeStatus,
$status
);
self::appendDiagnosisGuahaoLog($patientId, $id, $adminId, $adminInfo, 'admin_edit', $summary);
return true;
} catch (\Throwable $e) {
Db::rollback();
self::setError($e->getMessage());
return false;
}
}
/**
* 诊单列表维度:记录谁在后台发起挂号 / 取消挂号(写入失败不影响主流程)
*/
+13 -1
View File
@@ -1213,7 +1213,7 @@ class OrderLogic
}
$rows = $q
->field(['id', 'order_no', 'order_type', 'amount', 'status', 'create_time', 'creator_id', 'remark'])
->field(['id', 'order_no', 'order_type', 'amount', 'status', 'create_time', 'creator_id', 'remark', 'is_exempt'])
->order('id', 'desc')
->select()
->toArray();
@@ -1400,4 +1400,16 @@ class OrderLogic
'errors' => $errors,
];
}
public static function setExempt(int $id, int $isExempt): bool
{
$order = Order::find($id);
if (!$order) {
self::setError('订单不存在');
return false;
}
$order->is_exempt = $isExempt;
$order->save();
return true;
}
}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace app\adminapi\logic\stats;
use app\adminapi\logic\dept\DeptLogic;
use app\adminapi\logic\stats\YejiStatsLogic;
use app\adminapi\logic\tcm\DiagnosisLogic;
use app\common\model\finance\AccountCost;
use app\common\service\DataScope\DataScopeService;
@@ -1041,8 +1042,9 @@ class ConversionLogic
->alias('po')
->leftJoin('tcm_diagnosis dg', 'dg.id = po.diagnosis_id')
->whereNull('po.delete_time')
->where('po.create_time', 'between', [$startTimestamp, $endTimestamp])
->where('po.fulfillment_status', '<>', 4)
->where('po.create_time', 'between', [$startTimestamp, $endTimestamp]);
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($businessQuery, 'po');
$businessQuery
->fieldRaw("{$businessSourceExpr} AS source_admin_id, SUM(po.amount) AS total_amount")
->group($businessSourceExpr);
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace app\adminapi\logic\stats;
use app\adminapi\logic\stats\YejiStatsLogic;
use app\common\model\auth\Admin;
use app\common\service\DataScope\DataScopeService;
use think\facade\Db;
@@ -14,7 +15,7 @@ use think\facade\Db;
* 医生范围:<b>admin_role.role_id = 1</b> 且管理员 <b>未软删</b>delete_time 为空);再按账号「数据范围」收窄。
*
* - 系统/手动开方:tcm_prescription.prescription_date [start,end];渠道/标签与业绩渠道列同源 EXISTS / 诊单标签
* - 成交:订单 create_time、排除履约(4),按处方 creator_id;渠道/标签同 sumPerformance 口径
* - 成交:订单 create_time、排除履约 4/9/10,按处方 creator_id;渠道/标签同 sumPerformance 口径
* - 挂号:appointment_date [start,end];选渠道时挂号 channels 命中字典值;标签渠道时 patient_id 标签诊单集
*/
class DoctorDailyStatsLogic
@@ -300,9 +301,9 @@ class DoctorDailyStatsLogic
->alias('o')
->join('tcm_prescription rx', 'rx.id = o.prescription_id AND rx.delete_time IS NULL', 'INNER')
->whereNull('o.delete_time')
->whereBetween('o.create_time', [$startTs, $endTs])
->whereRaw('NOT (o.fulfillment_status <=> 4)')
->whereIn('rx.creator_id', $doctorIds)
->whereBetween('o.create_time', [$startTs, $endTs]);
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, 'o');
$q->whereIn('rx.creator_id', $doctorIds)
->where('o.diagnosis_id', '>', 0);
$normCh = self::normalizeAppointmentChannelInts($appointmentChannelValues);
File diff suppressed because it is too large Load Diff
@@ -535,6 +535,19 @@ class DiagnosisLogic extends BaseLogic
return 0;
}
public static function markAssignRead(int $diagnosisId, int $adminId): void
{
if ($diagnosisId <= 0 || $adminId <= 0) {
return;
}
Db::name('tcm_diagnosis')
->where('id', $diagnosisId)
->where('assistant_id', $adminId)
->whereNull('assign_read_at')
->whereNull('delete_time')
->update(['assign_read_at' => time()]);
}
/**
* @notes 指派医助(每次成功更新写入一条操作记录,批量指派为多次接口调用各记一条)。
* **原医助**:优先诊单 `assistant_id`;若为 0 且本次为指派到某人,则从**最近一条**指派日志推断(覆盖发货释放后库为 0、业务上仍有「前任」医助的情况)。
@@ -588,7 +601,10 @@ class DiagnosisLogic extends BaseLogic
Db::name('tcm_diagnosis')
->where('id', $id)
->whereNull('delete_time')
->update(['assistant_id' => $toAssistantId]);
->update([
'assistant_id' => $toAssistantId,
'assign_read_at' => $toAssistantId > 0 ? null : 0,
]);
$req = request();
$admin = $req->adminInfo ?? [];
@@ -10,6 +10,7 @@ use app\common\model\tcm\Prescription;
use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\Diagnosis;
use app\common\service\wechat\WechatWorkAppMessageService;
use app\adminapi\logic\auth\AuthLogic;
use think\facade\Config;
use think\facade\Log;
@@ -122,6 +123,12 @@ class PrescriptionLogic
return true;
}
// 有业务订单详情权限的用户可以查看关联处方(只读)
$perms = AuthLogic::getAuthByAdminId($adminId);
if (in_array('tcm.prescriptionOrder/detail', $perms, true)) {
return true;
}
return false;
}
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace app\adminapi\logic\tcm;
use app\adminapi\logic\stats\YejiStatsLogic;
use app\adminapi\logic\auth\AuthLogic;
use app\adminapi\logic\order\OrderLogic;
use app\common\model\Order;
@@ -18,6 +19,7 @@ use app\common\model\auth\Admin;
use app\common\service\DataScope\DataScopeService;
use app\common\service\ExpressTrackService;
use app\common\service\gancao\GancaoScmRecipelService;
use think\db\Query;
use think\facade\Config;
use think\facade\Db;
use think\facade\Log;
@@ -267,6 +269,166 @@ class PrescriptionOrderLogic
return self::roleIntersect($adminInfo, $allow);
}
/**
* 与业务订单列表金额统计一致:命中「统计医助」角色且非农单支付审核档时,业务侧按「仅限本人关联诊单的业绩域」收口。
* 提成结算等对业绩归因 SQL 收窄时需与之对齐。
*/
public static function statsRestrictedToOwnAssistantPerformanceDomain(array $adminInfo): bool
{
if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) {
return false;
}
if (self::canViewOrderListStatsAllScope($adminInfo)) {
return false;
}
$assistantRid = (int) Config::get('project.prescription_order_stats_assistant_role_id', 2);
return $assistantRid > 0 && self::roleIntersect($adminInfo, [$assistantRid]);
}
/**
* 提成结算等与列表对齐:按开方医生 / 诊单医助 订单创建人 筛选(与 PrescriptionOrderLists::applyDoctorAssistantFilters 在非业绩抽屉场景一致)。
*
* @param array<string, mixed> $params
*/
public static function applyCommissionOrderDoctorAssistantFilters(Query $query, string $alias, array $params): void
{
if (isset($params['doctor_id']) && (int) $params['doctor_id'] > 0) {
$doctorId = (int) $params['doctor_id'];
$rxTbl = (new Prescription())->getTable();
$query->whereExists(
"SELECT 1 FROM `{$rxTbl}` rx WHERE rx.`id` = `{$alias}`.`prescription_id` "
. "AND rx.`delete_time` IS NULL AND rx.`creator_id` = {$doctorId}"
);
}
if (isset($params['assistant_id']) && (int) $params['assistant_id'] > 0) {
$assistantId = (int) $params['assistant_id'];
$diagTbl = (new Diagnosis())->getTable();
$query->where(function ($q) use ($alias, $diagTbl, $assistantId): void {
$q->where("{$alias}.creator_id", $assistantId);
$q->whereOrRaw(
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$alias}`.`diagnosis_id`"
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$assistantId})"
);
});
}
}
/**
* 与处方订单列表 HasDataScopeFilter + applyCreatorOrOwnPrescriptionVisibility 等价;主业务订单别名 $alias。
*
* @param array<string, mixed> $params 可含 assistant_id(显式收窄时校验数据域)
*/
public static function applyPrescriptionOrderListAccessForCommissionQuery(
Query $query,
string $alias,
int $viewerAdminId,
array $viewerAdminInfo,
array $params = []
): void {
if ($viewerAdminId <= 0) {
return;
}
self::applyCommissionOrderDoctorAssistantFilters($query, $alias, $params);
if (self::canSeeAllPrescriptionOrders($viewerAdminInfo)) {
self::applyCommissionOrderDataScope($query, $alias, $viewerAdminId, $viewerAdminInfo);
return;
}
$rxTbl = (new Prescription())->getTable();
$diagTbl = (new Diagnosis())->getTable();
$explicitAssistant = (int) ($params['assistant_id'] ?? 0);
$allowExplicitAssistantVisibility = false;
if ($explicitAssistant > 0) {
if (!DataScopeService::isEnabled()) {
$allowExplicitAssistantVisibility = true;
} else {
$visibleIds = DataScopeService::getVisibleAdminIds($viewerAdminId, $viewerAdminInfo);
if ($visibleIds === null || in_array($explicitAssistant, $visibleIds, true)) {
$allowExplicitAssistantVisibility = true;
}
}
}
$query->where(function ($q) use (
$alias,
$rxTbl,
$diagTbl,
$viewerAdminId,
$explicitAssistant,
$allowExplicitAssistantVisibility,
$viewerAdminInfo
): void {
if (self::canViewOrdersForOwnPrescription($viewerAdminInfo)) {
$q->where(function ($qq) use ($alias, $rxTbl, $diagTbl, $viewerAdminId): void {
$qq->where("{$alias}.creator_id", $viewerAdminId);
$qq->whereOrRaw(
"EXISTS (SELECT 1 FROM `{$rxTbl}` rx WHERE rx.`id` = `{$alias}`.`prescription_id`"
. " AND rx.`delete_time` IS NULL AND rx.`creator_id` = {$viewerAdminId})"
);
$qq->whereOrRaw(
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$alias}`.`diagnosis_id`"
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$viewerAdminId})"
);
});
} else {
$q->where(function ($qq) use ($alias, $diagTbl, $viewerAdminId): void {
$qq->where("{$alias}.creator_id", $viewerAdminId);
$qq->whereOrRaw(
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$alias}`.`diagnosis_id`"
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$viewerAdminId})"
);
});
}
if ($allowExplicitAssistantVisibility) {
$q->whereOr("{$alias}.creator_id", $explicitAssistant);
$q->whereOrRaw(
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$alias}`.`diagnosis_id`"
. " AND dg.`delete_time` IS NULL AND dg.`assistant_id` = {$explicitAssistant})"
);
}
});
self::applyCommissionOrderDataScope($query, $alias, $viewerAdminId, $viewerAdminInfo);
}
/**
* 列表数据域:创建人 可见 诊单医助 可见。
*/
private static function applyCommissionOrderDataScope(
Query $query,
string $alias,
int $viewerAdminId,
array $viewerAdminInfo
): void {
if (!DataScopeService::isEnabled()) {
return;
}
$ids = DataScopeService::getVisibleAdminIds($viewerAdminId, $viewerAdminInfo);
if ($ids === null) {
return;
}
if ($ids === []) {
$query->whereRaw('0 = 1');
return;
}
$diagTbl = (new Diagnosis())->getTable();
$inList = implode(',', array_map('intval', $ids));
$query->where(function ($q) use ($alias, $diagTbl, $inList): void {
$q->whereIn("{$alias}.creator_id", explode(',', $inList));
$q->whereOrRaw(
"EXISTS (SELECT 1 FROM `{$diagTbl}` dg WHERE dg.`id` = `{$alias}`.`diagnosis_id` "
. "AND dg.`delete_time` IS NULL AND dg.`assistant_id` IN ({$inList}))"
);
});
}
/**
* @param array<string,mixed> $row
*/
@@ -323,7 +485,16 @@ class PrescriptionOrderLogic
if ($payOrderIds === []) {
return '未关联支付单,关联支付单总金额须大于等于定金门槛 ¥' . $min;
}
// 豁免权:关联的支付单中有豁免单则跳过金额校验
$hasExempt = Order::whereIn('id', $payOrderIds)
->whereNull('delete_time')
->where('is_exempt', 1)
->count();
if ($hasExempt > 0) {
return null;
}
// 检查关联支付单的总金额
$rows = Order::whereIn('id', $payOrderIds)->whereNull('delete_time')->column('amount', 'id');
$totalAmount = 0.0;
@@ -503,7 +674,7 @@ class PrescriptionOrderLogic
return [];
}
$rows = Order::whereIn('id', $ids)->whereNull('delete_time')
->field(['id', 'order_no', 'order_type', 'amount', 'status', 'create_time', 'creator_id', 'remark'])
->field(['id', 'order_no', 'order_type', 'amount', 'status', 'create_time', 'creator_id', 'remark', 'is_exempt'])
->order('id', 'asc')
->select()
->toArray();
@@ -1049,11 +1220,13 @@ class PrescriptionOrderLogic
}
/**
* 业务订单列表行批量附加 linked_appointment(解析规则与详情一致:优先处方 appointment_id,否则诊单最近一条预约
* 业务订单列表行批量附加 linked_appointment(解析规则与详情一致:优先处方 appointment_id,否则诊单最近一条预约
* 业绩侧栏且带 channel_code 时优先展示与渠道收窄同源的已完成挂号,避免列表行属自媒体2Q 但展示关联为自媒体1
*
* @param array<int, array<string, mixed>> $lists
* @param array<string, mixed> $listsParams PrescriptionOrderLists 请求参数(可选)
*/
public static function appendLinkedAppointmentsToListRows(array &$lists): void
public static function appendLinkedAppointmentsToListRows(array &$lists, array $listsParams = []): void
{
if ($lists === []) {
return;
@@ -1063,6 +1236,23 @@ class PrescriptionOrderLogic
}
unset($item);
$yejiChannelHighlightByDiag = [];
if (
(int) ($listsParams['yeji_order_drawer'] ?? 0) === 1
&& trim((string) ($listsParams['channel_code'] ?? '')) !== ''
) {
$allDiagIdsForYeji = array_values(array_unique(array_filter(
array_map(static fn ($x): int => (int) ($x['diagnosis_id'] ?? 0), $lists),
static fn (int $id): bool => $id > 0
)));
if ($allDiagIdsForYeji !== []) {
$yejiChannelHighlightByDiag = YejiStatsLogic::batchResolveYejiChannelDrawerHighlightAppointmentIds(
$allDiagIdsForYeji,
$listsParams
);
}
}
$rxIds = array_values(array_unique(array_filter(array_map(
static fn ($x) => (int) ($x['prescription_id'] ?? 0),
$lists
@@ -1113,6 +1303,10 @@ class PrescriptionOrderLogic
if ($resolved <= 0 && $dgId > 0) {
$resolved = (int) ($latestApptByDiag[$dgId] ?? 0);
}
$preferredYeji = $dgId > 0 ? (int) ($yejiChannelHighlightByDiag[$dgId] ?? 0) : 0;
if ($preferredYeji > 0) {
$resolved = $preferredYeji;
}
if ($resolved > 0) {
$resolvedPairs[$idx] = [$resolved, $rxApptId];
}
@@ -2230,7 +2424,7 @@ class PrescriptionOrderLogic
return $out;
}
private static function fulfillmentStatusLabel(int $fs): string
public static function fulfillmentStatusLabel(int $fs): string
{
$m = [
1 => '待双审通过',
@@ -2250,6 +2444,209 @@ class PrescriptionOrderLogic
return $m[$fs] ?? ('状态' . (string) $fs);
}
/**
* 导出用:诊单医助在 admin_dept 中的部门路径(多部门「;」、路径内「 / 」)
*/
public static function formatAssistantDeptPathForExport(int $assistantAdminId): string
{
if ($assistantAdminId <= 0) {
return '';
}
$deptIds = Db::name('admin_dept')->where('admin_id', $assistantAdminId)->column('dept_id');
$pathSegments = [];
foreach ($deptIds as $did) {
$p = self::buildDeptPath((int) $did);
if ($p !== '') {
$pathSegments[] = $p;
}
}
$pathSegments = array_values(array_unique($pathSegments));
return $pathSegments === [] ? '' : implode('', $pathSegments);
}
/**
* 导出用:药品形态(丸剂 / 饮片+代煎|自煎等)
*
* @param array<string, mixed> $rx
*/
public static function formatMedicationFormForExport(array $rx): string
{
$type = trim((string) ($rx['prescription_type'] ?? ''));
if ($type === '') {
return '';
}
if ($type === '饮片') {
$nd = (int) ($rx['need_decoction'] ?? 0);
return $type . ($nd === 1 ? '(代煎)' : '(自煎)');
}
return $type;
}
/**
* 业务订单列表导出:写入与 PrescriptionOrderLists::setExcelFields 对应的字段(export=2 时由列表类调用)
*
* @param array<int, array<string, mixed>> $lists
*/
public static function appendPrescriptionOrderListExportRows(array &$lists, array $adminInfo): void
{
if ($lists === []) {
return;
}
$diagIds = [];
$rxIds = [];
$poIds = [];
foreach ($lists as $row) {
$poIds[] = (int) ($row['id'] ?? 0);
$d = (int) ($row['diagnosis_id'] ?? 0);
if ($d > 0) {
$diagIds[$d] = true;
}
$r = (int) ($row['prescription_id'] ?? 0);
if ($r > 0) {
$rxIds[$r] = true;
}
}
$poIds = array_values(array_filter(array_unique($poIds), static fn (int $id): bool => $id > 0));
$diagIdList = array_keys($diagIds);
$rxIdList = array_keys($rxIds);
$diagById = [];
if ($diagIdList !== []) {
$diagRows = Diagnosis::whereIn('id', $diagIdList)->whereNull('delete_time')
->field(['id', 'patient_name', 'gender', 'age', 'phone', 'assistant_id'])
->select()
->toArray();
foreach ($diagRows as $dr) {
$diagById[(int) $dr['id']] = $dr;
}
}
$rxById = [];
if ($rxIdList !== []) {
$rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time')
->field(['id', 'prescription_type', 'need_decoction', 'dose_unit'])
->select()
->toArray();
foreach ($rxRows as $xr) {
$rxById[(int) $xr['id']] = $xr;
}
}
$logRows = PrescriptionOrderLog::whereIn('prescription_order_id', $poIds)
->whereIn('action', ['audit_rx_approve', 'audit_rx_reject', 'revoke_rx_audit'])
->field(['prescription_order_id', 'id', 'action', 'admin_name', 'create_time'])
->order('id', 'asc')
->select()
->toArray();
$auditEffectiveByPo = [];
foreach ($logRows as $lg) {
$pid = (int) ($lg['prescription_order_id'] ?? 0);
if ($pid <= 0) {
continue;
}
$act = (string) ($lg['action'] ?? '');
if ($act === 'revoke_rx_audit') {
$auditEffectiveByPo[$pid] = null;
continue;
}
if ($act === 'audit_rx_approve' || $act === 'audit_rx_reject') {
$auditEffectiveByPo[$pid] = $lg;
}
}
$assistantDeptCache = [];
foreach ($lists as &$item) {
$poId = (int) ($item['id'] ?? 0);
$fs = (int) ($item['fulfillment_status'] ?? 0);
$item['export_fulfillment_status_text'] = self::fulfillmentStatusLabel($fs);
$ct = (int) ($item['create_time'] ?? 0);
$item['export_order_time'] = $ct > 0 ? date('Y-m-d H:i:s', $ct) : '';
$diagId = (int) ($item['diagnosis_id'] ?? 0);
$dg = $diagById[$diagId] ?? null;
$item['export_patient_name'] = \is_array($dg) ? (string) ($dg['patient_name'] ?? '') : '';
if (\is_array($dg)) {
$g = (int) ($dg['gender'] ?? 0);
$item['export_patient_gender'] = $g === 1 ? '男' : '女';
$item['export_patient_age'] = (string) ($dg['age'] ?? '');
$item['export_patient_phone'] = (string) ($dg['phone'] ?? '');
$astId = (int) ($dg['assistant_id'] ?? 0);
if ($astId > 0) {
if (!isset($assistantDeptCache[$astId])) {
$assistantDeptCache[$astId] = self::formatAssistantDeptPathForExport($astId);
}
$item['export_assistant_dept'] = $assistantDeptCache[$astId];
} else {
$item['export_assistant_dept'] = '';
}
} else {
$item['export_patient_gender'] = '';
$item['export_patient_age'] = '';
$item['export_patient_phone'] = '';
$item['export_assistant_dept'] = '';
}
$rxId = (int) ($item['prescription_id'] ?? 0);
$rx = $rxById[$rxId] ?? [];
$item['export_medication_form'] = self::formatMedicationFormForExport(\is_array($rx) ? $rx : []);
$item['export_channel'] = (string) ($item['service_channel'] ?? '');
$md = $item['medication_days'] ?? null;
$item['export_medication_days'] = $md !== null && $md !== '' ? (string) $md : '';
$amt = round((float) ($item['amount'] ?? 0), 2);
$paid = round((float) ($item['linked_pay_paid_total'] ?? 0), 2);
$item['export_amount'] = number_format($amt, 2, '.', '');
$item['export_paid_amount'] = number_format($paid, 2, '.', '');
$item['export_agency_collect'] = number_format(round($amt - $paid, 2), 2, '.', '');
$item['export_tracking_number'] = (string) ($item['tracking_number'] ?? '');
$isGc = trim((string) ($item['gancao_reciperl_order_no'] ?? '')) !== '';
$item['export_supply_mode'] = $isGc ? '甘草' : '自营';
// 「处方成本」列:与列表/详情一致,取订单 internal_cost(含「测试价格」预报价写入;甘草/自营均导出)
if (self::canViewInternalCost($adminInfo)) {
$rawCost = $item['internal_cost'] ?? null;
$item['export_gancao_prescription_cost'] = $rawCost !== null && $rawCost !== ''
? number_format(round((float) $rawCost, 2), 2, '.', '')
: '';
} else {
$item['export_gancao_prescription_cost'] = '';
}
$isFu = (int) ($item['is_follow_up'] ?? 0) === 1;
$item['export_first_visit_assistant'] = $isFu ? (string) ($item['prev_staff'] ?? '') : '';
$hit = (int) ($item['po_assign_snapshot_hit'] ?? 0);
$rel = (int) ($item['po_assign_is_release'] ?? 0);
$er = (int) ($item['po_assign_to_er_center'] ?? 0);
if ($hit !== 1) {
$item['export_center_label'] = '—';
} elseif ($rel === 1) {
$item['export_center_label'] = '已释放';
} elseif ($er === 1) {
$item['export_center_label'] = '二中心';
} else {
$item['export_center_label'] = '一中心';
}
$paSt = (int) ($item['prescription_audit_status'] ?? 0);
$aud = $auditEffectiveByPo[$poId] ?? null;
if (($paSt === 1 || $paSt === 2) && \is_array($aud)) {
$at = (int) ($aud['create_time'] ?? 0);
$item['export_rx_audit_time'] = $at > 0 ? date('Y-m-d H:i:s', $at) : '';
$item['export_rx_auditor'] = (string) ($aud['admin_name'] ?? '');
} else {
$item['export_rx_audit_time'] = '';
$item['export_rx_auditor'] = '';
}
}
unset($item);
}
/**
* 完成订单时把关联诊单的 assistant_id 清空,让患者回到「待分配」状态
* 仅当该患者没有其它进行中的处方订单且无指派日志时才清空,避免误覆盖
@@ -21,10 +21,13 @@ class AppointmentValidate extends BaseValidate
'doctor_id' => 'require|integer',
'diagnosis_id' => 'require|integer',
'appointment_date' => 'require|date',
'period' => 'in:morning,afternoon,all',
'appointment_time' => 'require',
'appointment_type' => 'in:video,text,phone',
'period' => 'in:morning,afternoon,all',
'appointment_type' => 'require|in:video,text,phone',
'status' => 'require|integer|between:1,4',
'remark' => 'max:500',
'channel_source' => 'require',
'channel_source_detail' => 'max:128',
'note_id' => 'require|integer',
'image_type' => 'require|in:tongue_images,report_files',
'image_path' => 'require',
@@ -40,10 +43,14 @@ class AppointmentValidate extends BaseValidate
'doctor_id' => '医生ID',
'diagnosis_id' => '诊单ID',
'appointment_date' => '预约日期',
'period' => '时段',
'appointment_time' => '预约时间',
'period' => '时段',
'appointment_type' => '预约类型',
'status' => '状态',
'remark' => '备注',
'assistant_id' => '医助',
'channel_source' => '渠道来源',
'channel_source_detail' => '渠道补充说明',
];
/**
@@ -52,7 +59,7 @@ class AppointmentValidate extends BaseValidate
*/
public function sceneCreate()
{
return $this->only(['patient_id', 'doctor_id', 'appointment_date', 'period', 'appointment_time', 'appointment_type', 'channel_source']);
return $this->only(['patient_id', 'doctor_id', 'appointment_date', 'period', 'appointment_time', 'appointment_type', 'channel_source', 'channel_source_detail']);
}
/**
@@ -123,4 +130,22 @@ class AppointmentValidate extends BaseValidate
{
return $this->only(['note_id', 'image_type', 'image_path']);
}
/**
* 后台挂号编辑(消费者处方-挂号列表等)
*/
public function sceneAdminEdit()
{
return $this->only([
'id',
'appointment_date',
'appointment_time',
'period',
'appointment_type',
'status',
'remark',
'channel_source',
'channel_source_detail',
]);
}
}
+3 -3
View File
@@ -18,7 +18,7 @@ use think\console\Output;
* 配置 crontab(每 10 分钟):拉快递 100 + 按履约「已发货/已签收」核对释放诊单医助(与是否甘草单无关)
* 0,10,20,30,40,50 * * * * cd /path/to/server && php think express:auto-update >> /dev/null 2>&1
*
* 已对非二中心医助执行发货/签收自动释放的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at
* 已对「业务订单创建人非二中心」(或创建人为开方医生时按待释放身份判定)执行发货/签收自动释放的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at
* 后续履约核对不再重复扫描(需先执行 sql/1.9.20260507/add_diagnosis_shipped_non_er_assistant_cleared_at.sql)。
*/
class ExpressAutoUpdate extends Command
@@ -36,8 +36,8 @@ class ExpressAutoUpdate extends Command
$startTime = microtime(true);
try {
$result = ExpressTrackingService::autoUpdateBatch(500);
$recon = ExpressTrackingService::reconcileAssistantReleaseForShippedPrescriptionOrders(500);
$result = ExpressTrackingService::autoUpdateBatch(1000);
$recon = ExpressTrackingService::reconcileAssistantReleaseForShippedPrescriptionOrders(1000);
$duration = round(microtime(true) - $startTime, 2);
@@ -29,7 +29,7 @@ use think\console\Output;
* 建议 crontab(每 30 分钟执行一次):
* 0,30 * * * * cd /path/to/server && php think gancao:sync-logistics --limit=200 >> runtime/log/gancao_sync.log 2>&1
*
* 已对非二中心医助执行发货/签收自动释放的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at
* 已对「业务订单创建人非二中心」(发货/签收自动释放规则见 ExpressTrackingService的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at
* 后续履约核对不再重复扫描(需先执行 sql/1.9.20260507/add_diagnosis_shipped_non_er_assistant_cleared_at.sql)。
*/
class GancaoSyncLogisticsRoute extends Command
@@ -0,0 +1,279 @@
<?php
declare(strict_types=1);
namespace app\command;
use app\adminapi\logic\tcm\PrescriptionOrderLogic;
use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\PrescriptionOrderLog;
use app\common\service\gancao\GancaoScmRecipelService;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
/**
* 历史 internal_cost 回填:对每条业务订单调用甘草预下单(CTM_PREVIEW),
* 将返回 fee 的「药材成本 + 制作费 + 物流费」合计写入 zyt_tcm_prescription_order.internal_cost。
*
* php think tcm:backfill-internal-cost --dry-run
* php think tcm:backfill-internal-cost --limit=30 --sleep-ms=400
* php think tcm:backfill-internal-cost --order-id=123
* php think tcm:backfill-internal-cost --force
* php think tcm:backfill-internal-cost --null-or-zero --min-id=1000
* php think tcm:backfill-internal-cost --fulfillment-status=5,6,3
*
* 履约状态代码(fulfillment_status):1待双审通过 2待发货 3已完成 4已取消 5已发货 6已签收
* 7进行中 8暂不制药 9拒收 10退款 11保留药方 12制药缓发
*
* 说明:使用 root 上下文仅用于绕过后台「谁能看哪张处方/订单」校验;不落管理员登录态。
* 甘草未配置、药材无法匹配、规则拦截、缺药等会跳过并在末尾汇总。
*/
class TcmBackfillPrescriptionOrderInternalCost extends Command
{
protected function configure(): void
{
$this->setName('tcm:backfill-internal-cost')
->setDescription('批量用甘草预报价回填处方业务订单 internal_cost')
->addOption('dry-run', null, Option::VALUE_NONE, '只演练不写库')
->addOption('limit', 'l', Option::VALUE_OPTIONAL, '最多处理条数', 100)
->addOption('order-id', null, Option::VALUE_OPTIONAL, '仅处理指定 prescription_order.id', null)
->addOption('force', 'f', Option::VALUE_NONE, '覆盖已有 internal_cost(默认只填 NULL')
->addOption('null-or-zero', null, Option::VALUE_NONE, '与默认一致且同时处理 internal_cost=0 的行(仍可用 --force 覆盖任意值)')
->addOption('min-id', null, Option::VALUE_OPTIONAL, '仅 id>=该值', null)
->addOption('max-id', null, Option::VALUE_OPTIONAL, '仅 id<=该值', null)
->addOption(
'fulfillment-status',
null,
Option::VALUE_OPTIONAL,
'仅处理指定履约状态,逗号分隔数字,如 5,6,3=已发货/已签收/已完成;不传则不限',
null
)
->addOption('sleep-ms', null, Option::VALUE_OPTIONAL, '每条成功调用后休眠毫秒,减轻开放平台压力', 250)
->addOption('detail', 'd', Option::VALUE_NONE, '打印每条明细');
}
/**
* @return array{admin_id:int, root:int, name:string, role_id:int[]}
*/
private static function cliAdminInfo(): array
{
return [
'admin_id' => 0,
'root' => 1,
'name' => 'CLI-internal-cost',
'role_id' => [],
];
}
/**
* @param array<string, mixed> $fee
*/
private static function totalFeeFromPreview(array $fee): float
{
$m = (float) ($fee['m_cost'] ?? 0);
$p = (float) ($fee['proces_cost'] ?? 0);
$l = (float) ($fee['lis_cost'] ?? 0);
return round($m + $p + $l, 2);
}
/**
* 解析 CLI 传入的履约状态列表(112,与 PrescriptionOrderLogic::fulfillmentStatusLabel 一致)
*
* @return int[] 去重后的状态码;$raw 非空但解析不到合法值时返回 null 表示调用方应报错
*/
private static function parseFulfillmentStatusOption(?string $raw): ?array
{
if ($raw === null) {
return [];
}
$raw = trim($raw);
if ($raw === '') {
return [];
}
$seen = [];
foreach (explode(',', $raw) as $part) {
$n = (int) trim($part);
if ($n >= 1 && $n <= 12) {
$seen[$n] = true;
}
}
$ids = array_keys($seen);
sort($ids);
return $ids !== [] ? $ids : null;
}
protected function execute(Input $input, Output $output): int
{
$dryRun = (bool) $input->getOption('dry-run');
$verbose = (bool) $input->getOption('detail');
$force = (bool) $input->getOption('force');
$nullOrZero = (bool) $input->getOption('null-or-zero');
$limit = max(1, (int) $input->getOption('limit'));
$sleepMs = max(0, (int) $input->getOption('sleep-ms'));
$onlyOrderId = $input->getOption('order-id');
$onlyOrderId = $onlyOrderId !== null && $onlyOrderId !== '' ? (int) $onlyOrderId : null;
$minId = $input->getOption('min-id');
$minId = $minId !== null && $minId !== '' ? (int) $minId : null;
$maxId = $input->getOption('max-id');
$maxId = $maxId !== null && $maxId !== '' ? (int) $maxId : null;
$fsRaw = $input->getOption('fulfillment-status');
$fsRaw = $fsRaw !== null ? (string) $fsRaw : null;
$fulfillmentStatuses = self::parseFulfillmentStatusOption($fsRaw);
if ($fulfillmentStatuses === null) {
$output->error('--fulfillment-status 格式无效,请传入 1–12 的数字,英文逗号分隔,例如:5,6,3');
return 1;
}
$adminInfo = self::cliAdminInfo();
$adminId = 0;
$output->writeln('========================================');
$output->writeln('批量回填 internal_cost(甘草 CTM_PREVIEW');
$output->writeln('========================================');
if (!GancaoScmRecipelService::isConfigured()) {
$output->error('甘草 SCM 未配置:' . GancaoScmRecipelService::whyNotConfigured());
return 1;
}
$q = PrescriptionOrder::whereNull('delete_time')->where('prescription_id', '>', 0)->order('id', 'asc');
if ($fulfillmentStatuses !== []) {
$q->whereIn('fulfillment_status', $fulfillmentStatuses);
}
if ($onlyOrderId !== null && $onlyOrderId > 0) {
$q->where('id', $onlyOrderId);
} else {
if (!$force) {
if ($nullOrZero) {
$q->whereRaw('(internal_cost IS NULL OR internal_cost = 0)');
} else {
$q->whereNull('internal_cost');
}
}
if ($minId !== null && $minId > 0) {
$q->where('id', '>=', $minId);
}
if ($maxId !== null && $maxId > 0) {
$q->where('id', '<=', $maxId);
}
$q->limit($limit);
}
$rows = $q->field(['id', 'order_no', 'dose_count', 'medication_days', 'internal_cost', 'fulfillment_status'])->select()->toArray();
if ($rows === []) {
$output->warning('没有符合条件的订单。');
return 0;
}
$fsLabel = '';
if ($fulfillmentStatuses !== []) {
$parts = [];
foreach ($fulfillmentStatuses as $code) {
$parts[] = (string) $code . '=' . PrescriptionOrderLogic::fulfillmentStatusLabel((int) $code);
}
$fsLabel = ' fulfillment-status=[' . implode(',', $parts) . ']';
}
$output->writeln(sprintf(
'待处理 %d 条(dry-run=%s force=%s sleep-ms=%d%s',
count($rows),
$dryRun ? 'yes' : 'no',
$force ? 'yes' : 'no',
$sleepMs,
$fsLabel
));
$ok = 0;
$fail = 0;
$skipped = 0;
$reasons = [];
foreach ($rows as $row) {
$id = (int) ($row['id'] ?? 0);
if ($id <= 0) {
continue;
}
if (!$force && !$nullOrZero && $row['internal_cost'] !== null && $row['internal_cost'] !== '') {
$skipped++;
continue;
}
$params = [];
$dc = (int) ($row['dose_count'] ?? 0);
if ($dc > 0) {
$params['dose_count'] = $dc;
}
$md = $row['medication_days'] ?? null;
if ($md !== null && $md !== '' && (int) $md > 0) {
$params['medication_days'] = (int) $md;
}
$ret = PrescriptionOrderLogic::previewGancaoRecipel($id, $adminId, $adminInfo, $params);
if ($ret === false) {
$fail++;
$err = PrescriptionOrderLogic::getError();
$reasons[$err] = ($reasons[$err] ?? 0) + 1;
if ($verbose) {
$output->writeln("<error>#{$id} 失败:{$err}</error>");
}
continue;
}
$fee = is_array($ret['fee'] ?? null) ? $ret['fee'] : [];
$total = self::totalFeeFromPreview($fee);
if ($verbose) {
$ono = (string) ($row['order_no'] ?? '');
$fs = (int) ($row['fulfillment_status'] ?? 0);
$fst = PrescriptionOrderLogic::fulfillmentStatusLabel($fs);
$output->writeln(sprintf('#%d %s [%s] → internal_cost=%.2f', $id, $ono, $fst, $total));
}
if (!$dryRun) {
PrescriptionOrder::where('id', $id)->whereNull('delete_time')->update([
'internal_cost' => $total,
]);
$log = new PrescriptionOrderLog();
$log->prescription_order_id = $id;
$log->admin_id = 0;
$log->admin_name = 'CLI';
$log->action = 'cli_backfill_internal_cost';
$log->summary = mb_substr(sprintf('甘草预报价回填 internal_cost=%.2f', $total), 0, 500);
$log->create_time = time();
try {
$log->save();
} catch (\Throwable $e) {
// 忽略
}
}
$ok++;
if ($sleepMs > 0 && !$dryRun) {
usleep($sleepMs * 1000);
}
}
$output->writeln('');
$output->writeln(sprintf('完成:成功 %d,失败 %d,跳过 %d', $ok, $fail, $skipped));
if ($reasons !== []) {
$output->writeln('失败原因统计:');
foreach ($reasons as $msg => $cnt) {
$output->writeln(' [' . $cnt . 'x] ' . $msg);
}
}
return $fail > 0 ? 2 : 0;
}
}
@@ -58,6 +58,12 @@ abstract class BaseDataLists implements ListsInterface
public string $export;
/**
* 管理端列表:在 request 就绪后写入 adminId/adminInfo(见 BaseAdminDataLists
*/
protected function initAdminIdentity(): void
{
}
public function __construct()
{
@@ -66,6 +72,8 @@ abstract class BaseDataLists implements ListsInterface
//请求参数设置
$this->request = request();
// admin 列表子类在 initExport 中可能触发 count/lists,须先于 initPage/initExport 写入身份
$this->initAdminIdentity();
$this->params = $this->request->param();
//分页初始化
@@ -249,7 +249,7 @@ class ExpressTrackingService
/**
* 处方业务订单履约「已发货(5) / 已签收(6)」时:清空关联诊单(患者)医助,并写入 tcm_diagnosis_assign_log(含关联业务订单 creator_id / create_time 快照,便于核对医助创建订单时间)。
* 快递 100 定时拉轨迹、甘草路由、甘草回调与履约核对共用同一规则。
* 不自动清空的情形仅两种:(1患者当前医助在「二中心」及其全部子部门(与后台部门树一致)下任职;(2)诊单已有 shipped_non_er_assistant_cleared_at assistant_id 已为 0(视为已处理过,重新指派医助后可再次处理)
* 不自动清空的情形:(1按业务订单 creator_id(代建场景)或回退身份判断是否在「二中心」部门子树;(2)诊单已有 shipped_non_er_assistant_cleared_at assistant_id 已为 0(视为已处理);(3)指派日志存在后台人工指派:`operator_admin_id>0` `to_assistant_id>0`(系统任务为 operator_admin_id=0);或 related_po_creator_id=from_assistant_id to>0 的旧数据兼容。避免「系统·已发货履约核对」覆盖人工指派链
*
* @param array{tracking_id?:int,tracking_number?:string,source?:string} $meta
* @return array{
@@ -288,36 +288,55 @@ class ExpressTrackingService
if (!$diag) {
return null;
}
// 后台曾人工指派到新医助(operator_admin_id>0 且 to>0,与系统写入的 operator=0 区分)时不再自动清空;另保留快照 related_po=from 的旧数据兼容
if (self::diagnosisShouldSkipAutoAssistantReleaseDueToManualAssignLog($diagnosisId)) {
Log::info('Skipped auto assistant release (shipped/signed): manual assign history exists (human operator + to_assistant_id>0, or snapshot-tied pattern)', [
'diagnosis_id' => $diagnosisId,
'prescription_order_id' => $orderId,
'source' => $source,
]);
return null;
}
$shippedClearedAt = (int) ($diag->getAttr('shipped_non_er_assistant_cleared_at') ?? 0);
$diagAssistantIdNow = (int) ($diag->getAttr('assistant_id') ?? 0);
if ($shippedClearedAt > 0 && $diagAssistantIdNow === 0) {
return null;
}
$poCreatorId = (int) ($order->getAttr('creator_id') ?? 0);
$rxId = (int) ($order->getAttr('prescription_id') ?? 0);
$rxCreator = 0;
if ($rxId > 0) {
$rxCreator = (int) Prescription::where('id', $rxId)->whereNull('delete_time')->value('creator_id');
}
// 诊单 assistant_id 与列表「医助」筛选一致:医助常代建单但诊单未写 assistant_id,此时用创建人作为待释放身份(创建人即开方医生时不采用,避免误记指派日志)
$fromAssistantId = (int) ($diag->getAttr('assistant_id') ?? 0);
if ($fromAssistantId <= 0) {
$creatorId = (int) ($order->getAttr('creator_id') ?? 0);
if ($creatorId > 0) {
$rxId = (int) ($order->getAttr('prescription_id') ?? 0);
$rxCreator = 0;
if ($rxId > 0) {
$rxCreator = (int) Prescription::where('id', $rxId)->whereNull('delete_time')->value('creator_id');
}
if ($rxId <= 0 || $creatorId !== $rxCreator) {
$fromAssistantId = $creatorId;
}
if ($poCreatorId > 0 && ($rxId <= 0 || $poCreatorId !== $rxCreator)) {
$fromAssistantId = $poCreatorId;
}
}
if ($fromAssistantId <= 0) {
return null;
}
if (self::currentAssistantBelongsToErzhongxinDeptTree($fromAssistantId)) {
Log::info('Skipped auto assistant release (shipped/signed): assistant under 二中心 dept tree', [
'diagnosis_id' => $diagnosisId,
'prescription_order_id' => $orderId,
'source' => $source,
'current_assistant_id' => $fromAssistantId,
/** 是否因「二中心」跳过释放:优先看业务订单创建人 creator_id(代建场景);创建人即开方医生时用 fromAssistantId */
$erZhongxinCheckAdminId = ($poCreatorId > 0 && ($rxId <= 0 || $poCreatorId !== $rxCreator))
? $poCreatorId
: $fromAssistantId;
if ($erZhongxinCheckAdminId > 0 && self::currentAssistantBelongsToErzhongxinDeptTree($erZhongxinCheckAdminId)) {
Log::info('Skipped auto assistant release (shipped/signed): order creator (or fallback from-id) under 二中心 dept tree', [
'diagnosis_id' => $diagnosisId,
'prescription_order_id' => $orderId,
'source' => $source,
'er_zhongxin_check_admin_id' => $erZhongxinCheckAdminId,
'prescription_order_creator_id' => $poCreatorId,
'from_assistant_id_for_log' => $fromAssistantId,
]);
return null;
@@ -440,6 +459,39 @@ class ExpressTrackingService
return false;
}
/**
* 若诊单曾被后台人工指派到具体医助,则不再执行发运/签收自动清空(避免「系统·已发货履约核对」覆盖人工链)。
* 判定:operator_admin_id>0 to_assistant_id>0(人工);系统写入的释放/同步为 operator_admin_id=0
* 另保留 related_po_creator_id=from_assistant_id to>0 的窄条件,兼容早期数据。
*/
private static function diagnosisShouldSkipAutoAssistantReleaseDueToManualAssignLog(int $diagnosisId): bool
{
if ($diagnosisId <= 0) {
return false;
}
$tbl = 'tcm_diagnosis_assign_log';
$humanReassign = (int) Db::name($tbl)
->where('diagnosis_id', $diagnosisId)
->where('to_assistant_id', '>', 0)
->where('operator_admin_id', '>', 0)
->count();
if ($humanReassign > 0) {
return true;
}
$snapshotTied = (int) Db::name($tbl)
->where('diagnosis_id', $diagnosisId)
->where('to_assistant_id', '>', 0)
->where('from_assistant_id', '>', 0)
->where('related_po_creator_id', '>', 0)
->whereRaw('`related_po_creator_id` = `from_assistant_id`')
->count();
return $snapshotTied > 0;
}
private static function maybeClearDiagnosisAssistantWhenShippedPrescription(ExpressTracking $tracking): ?array
{
if ((string) $tracking->order_type !== 'prescription') {
@@ -619,7 +671,7 @@ class ExpressTrackingService
/**
* 按处方订单履约核对:已发货(5)/已签收(6) 时符合条件的订单调用 applyAssistantReleaseForShippedPrescriptionOrder。
* 与快递 100 / 甘草来自哪个物流渠道无关;是否清空医助以该方法为准(仅二中心不释放;已 shipped 释放且 assistant_id=0 视为已处理)。
* 与快递 100 / 甘草来自哪个物流渠道无关;是否清空医助以该方法为准(**二中心豁免看业务单创建人**参见 apply;已 shipped 释放且 assistant_id=0 视为已处理)。
*
* @return array{scanned: int, cleared: int, lines: list<string>}
*/
@@ -810,4 +862,237 @@ class ExpressTrackingService
return $data;
}
/**
* PrescriptionOrderLogic::logisticsTrace 一致:优先库表 express_tracking + express_trace
* 库内算不出签收时(或库无记录),可选走快递100(与详情「刷新轨迹」同源)。
*
* 提成结算 orderLines 等对 SQL JOIN 无法覆盖「仅 API 有轨迹」的运单做补算。
*
* @param bool $queryKuaidiWhenDbUnusable true 时与 logisticsTrace 完全同路径;false 时仅数据库推导(overview 大批量避免 HTTP
*/
public static function resolveSignUnixTimeForCommission(
string $trackingNumber,
string $expressCompany,
string $recipientPhone,
bool $queryKuaidiWhenDbUnusable = true
): int {
$num = trim($trackingNumber);
if ($num === '') {
return 0;
}
$cacheKey = $num . '|' . ($queryKuaidiWhenDbUnusable ? '1' : '0');
static $memo = [];
if (isset($memo[$cacheKey])) {
return $memo[$cacheKey];
}
$detail = self::loadTrackingDetailNormalized($num);
if (is_array($detail)) {
$fromDb = self::effectiveSignUnixFromTrackingDetail($detail);
if ($fromDb > 0) {
$memo[$cacheKey] = $fromDb;
return $fromDb;
}
}
if (!$queryKuaidiWhenDbUnusable) {
$memo[$cacheKey] = 0;
return 0;
}
$ec = strtolower(trim($expressCompany));
if ($ec === '') {
$ec = 'auto';
}
$api = ExpressTrackService::query($ec, $num, $recipientPhone, '');
$fromApi = self::effectiveSignUnixFromKuaidiPayload($api);
$memo[$cacheKey] = $fromApi;
return $fromApi;
}
/**
* 单号精确匹配失败时尝试 TRIM(TrackingNumber)(历史数据首尾空格)。
*
* @return array<string, mixed>|null
*/
private static function loadTrackingDetailNormalized(string $trackingNumber): ?array
{
$n = trim($trackingNumber);
if ($n === '') {
return null;
}
$first = self::getDetailByTrackingNumber($n);
if ($first !== null) {
return $first;
}
$tracking = ExpressTracking::with(['traces' => function ($query) {
$query->order('trace_time_stamp', 'desc');
}])
->whereRaw('TRIM(`tracking_number`) = ?', [$n])
->whereNull('delete_time')
->find();
if (!$tracking) {
return null;
}
$data = $tracking->toArray();
$data['traces'] = $tracking->traces ? $tracking->traces->toArray() : [];
return $data;
}
/**
* YejiStatsLogic 物流子查询中每条 express_tracking GREATEST 语义对齐。
*
* @param array<string, mixed> $detail getDetail* / loadTrackingDetailNormalized 结果
*/
private static function effectiveSignUnixFromTrackingDetail(array $detail): int
{
$a = (int) ($detail['sign_time'] ?? 0);
$traces = $detail['traces'] ?? [];
if (!is_array($traces)) {
$traces = [];
}
$b = 0;
foreach ($traces as $tr) {
if (!is_array($tr)) {
continue;
}
if (!self::dbTraceRowLooksSigned($tr)) {
continue;
}
$ts = self::traceRowToUnix($tr);
if ($ts > 0) {
$b = max($b, $ts);
}
}
$c = 0;
$stateOk = ((string) ($detail['current_state'] ?? '') === ExpressTracking::STATE_SIGNED
|| (int) ($detail['is_signed'] ?? 0) === 1);
if ($stateOk) {
foreach ($traces as $tr) {
if (!is_array($tr)) {
continue;
}
$ts = self::traceRowToUnix($tr);
if ($ts > 0) {
$c = max($c, $ts);
}
}
}
return max($a, $b, $c);
}
/**
* @param array<string, mixed> $tr express_trace
*/
private static function dbTraceRowLooksSigned(array $tr): bool
{
$code = (string) ($tr['status_code'] ?? '');
if (in_array($code, ['3', '301', '302', '304'], true)) {
return true;
}
$st = (string) ($tr['status'] ?? '');
$ctx = (string) ($tr['trace_context'] ?? '');
$hay = $st . ' ' . $ctx;
foreach (['签收', '妥投', '已送达', '本人签收'] as $k) {
if (mb_stripos($hay, $k) !== false) {
return true;
}
}
return false;
}
/**
* @param array<string, mixed> $tr
*/
private static function traceRowToUnix(array $tr): int
{
$ts = (int) ($tr['trace_time_stamp'] ?? 0);
if ($ts > 0) {
return $ts;
}
$raw = trim((string) ($tr['trace_time'] ?? ''));
if ($raw === '') {
return 0;
}
$p = strtotime($raw);
return $p !== false ? (int) $p : 0;
}
/**
* @param array<string, mixed> $api ExpressTrackService::query 返回值
*/
private static function effectiveSignUnixFromKuaidiPayload(array $api): int
{
$traces = $api['traces'] ?? [];
if (!is_array($traces) || $traces === []) {
return 0;
}
$state = (string) ($api['state'] ?? '');
$best = 0;
foreach ($traces as $row) {
if (!is_array($row)) {
continue;
}
$tstr = (string) ($row['time'] ?? '');
$ctx = (string) ($row['context'] ?? '');
$p = strtotime($tstr);
$ts = $p !== false ? (int) $p : 0;
if ($ts <= 0) {
continue;
}
$signedLike = self::plainContextLooksSigned($ctx);
if ($signedLike) {
$best = max($best, $ts);
}
}
if ($best > 0) {
return $best;
}
if ($state === '3') {
foreach ($traces as $row) {
if (!is_array($row)) {
continue;
}
$tstr = (string) ($row['time'] ?? '');
$p = strtotime($tstr);
$ts = $p !== false ? (int) $p : 0;
if ($ts > 0) {
$best = max($best, $ts);
}
}
}
return $best;
}
private static function plainContextLooksSigned(string $context): bool
{
foreach (['签收', '妥投', '已送达', '本人签收', '快件已送达'] as $k) {
if (mb_stripos($context, $k) !== false) {
return true;
}
}
return false;
}
}
+22
View File
@@ -21,6 +21,7 @@ use app\common\enum\ExportEnum;
use app\common\lists\BaseDataLists;
use app\common\lists\ListsExcelInterface;
use app\common\lists\ListsExtendInterface;
use think\facade\Config;
use think\Response;
use think\response\Json;
use think\exception\HttpResponseException;
@@ -120,12 +121,16 @@ class JsonService
{
//获取导出信息
if ($lists->export == ExportEnum::INFO && $lists instanceof ListsExcelInterface) {
self::relaxLimitsForExcelExport();
return self::data($lists->excelInfo());
}
//获取导出文件的下载链接
if ($lists->export == ExportEnum::EXPORT && $lists instanceof ListsExcelInterface) {
self::relaxLimitsForExcelExport();
$exportDownloadUrl = $lists->createExcel($lists->setExcelFields(), $lists->lists());
return self::success('', ['url' => $exportDownloadUrl], 2);
}
@@ -141,4 +146,21 @@ class JsonService
}
return self::success('', $data, 1, 0);
}
/**
* Excel 导出:拉数 + PhpSpreadsheet 易超过默认 max_execution_time=30
*/
private static function relaxLimitsForExcelExport(): void
{
@set_time_limit(0);
$max = Config::get('project.lists.export_max_execution_time', 600);
$max = is_numeric($max) ? (int) $max : 600;
if ($max > 0) {
@ini_set('max_execution_time', (string) $max);
}
$mem = Config::get('project.lists.export_memory_limit', '512M');
if (is_string($mem) && $mem !== '') {
@ini_set('memory_limit', $mem);
}
}
}
@@ -74,7 +74,14 @@ final class GancaoLogisticsRouteService
}
$result = $resp['result'];
$trackingNumber = trim((string) ($result['sp_order_no'] ?? ($order->tracking_number ?? '')));
$apiTracking = trim((string) ($result['sp_order_no'] ?? ''));
$localTracking = trim((string) ($order->tracking_number ?? ''));
// 业务订单已录快递单号时:不改用甘草返回的 sp_order_no,轨迹仍写入本地单号对应的运单记录
if ($localTracking !== '') {
$trackingNumber = mb_substr($localTracking, 0, 80);
} else {
$trackingNumber = mb_substr($apiTracking, 0, 80);
}
$spName = trim((string) ($result['sp_name'] ?? ($order->gancao_shipping_name ?? '')));
$routeList = is_array($result['route_list'] ?? null) ? $result['route_list'] : [];
@@ -87,7 +94,7 @@ final class GancaoLogisticsRouteService
// 回写订单的快递单号 / 物流公司(如果之前空着)+ 签收时升级 fulfillment_status
$orderDirty = false;
if (trim((string) $order->tracking_number) === '') {
if ($localTracking === '') {
$order->tracking_number = mb_substr($trackingNumber, 0, 80);
$orderDirty = true;
}
+2
View File
@@ -32,6 +32,8 @@ return [
'qywx:sync-msg-archive' => 'app\\command\\QywxSyncMsgArchive',
// 甘草订单物流路由同步(GET_TASK_ROUTE_LIST
'gancao:sync-logistics' => 'app\\command\\GancaoSyncLogisticsRoute',
// 历史 internal_cost:批量甘草预报价回填(CTM_PREVIEW
'tcm:backfill-internal-cost' => 'app\\command\\TcmBackfillPrescriptionOrderInternalCost',
// 迁移诊单图片到医生备注表
'migrate:images-to-doctor-note' => 'app\\command\\MigrateImagesToDoctorNote',
// 诊单待办事项:扫描到点的待执行项并向创建人发送企业微信消息
+13
View File
@@ -47,6 +47,9 @@ return [
'lists' => [
'page_size_max' => 25000,//列表页查询数量限制(列表页每页数量、导出每页数量)
'page_size' => 25, //默认每页数量
/** Excel 导出:接口内放宽 PHP 限制(JsonService::relaxLimitsForExcelExport */
'export_max_execution_time' => 600,
'export_memory_limit' => '512M',
],
// 各种默认图片
@@ -137,6 +140,16 @@ return [
'exempt_roles' => [],
],
/*
* 提成结算 stats.commissionSettlementYejiStatsLogic
* require_system_auto_prescriptiontrue(默认)仅系统代开口径(is_system_auto=1),常与订单列表「全部已完成」条数不一致。
* false 时与列表一致纳入手动开方单(仍需履约完成、业绩归因等其它条件)。
* .env 可选:commission_settlement.require_system_auto = 1|0 ,未配置时读本项。
*/
'commission_settlement' => [
'require_system_auto_prescription' => (int) env('commission_settlement.require_system_auto', 1) === 1,
],
// 腾讯云实时音视频(TRTC)配置
'trtc' => [
'sdkAppId' => (int)env('trtc.sdk_app_id', 1600127710),
@@ -0,0 +1,7 @@
-- 二诊医助:新指派患者已读标记
ALTER TABLE `zyt_tcm_diagnosis`
ADD COLUMN `assign_read_at` int(10) unsigned DEFAULT NULL
COMMENT '医助查看指派时间戳,NULL 未读(新指派红旗)' AFTER `assistant_id`;
-- 已有数据标记为已读,避免全部显示红旗
UPDATE `zyt_tcm_diagnosis` SET `assign_read_at` = UNIX_TIMESTAMP() WHERE `assistant_id` > 0;
@@ -0,0 +1,13 @@
-- 支付单:设置/取消豁免权(权限节点)
SET @order_menu_id := (SELECT id FROM zyt_system_menu WHERE perms = 'order.order/lists' LIMIT 1);
INSERT INTO zyt_system_menu (
pid, type, name, icon, sort, perms, paths, component,
selected, params, is_cache, is_show, is_disable, create_time, update_time
)
SELECT
@order_menu_id, 'A', '设置豁免权', '', 0, 'order.order/setExempt', '', '',
'', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
FROM DUAL
WHERE @order_menu_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM zyt_system_menu WHERE perms = 'order.order/setExempt');
@@ -0,0 +1,3 @@
ALTER TABLE `zyt_order`
ADD COLUMN `is_exempt` tinyint(1) unsigned NOT NULL DEFAULT 0
COMMENT '豁免权:1=关联此单时跳过定金门槛校验' AFTER `status`;
@@ -1 +1 @@
import r from"./error-v6z53F3Y.js";import{f as p,ak as i,I as m,a as e,aN as s,J as o}from"./@vue/runtime-core-tF_2qJHK.js";import"./element-plus-NSC30APV.js";import"./@vue/runtime-dom-CEiKlVMn.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-BmZ-20sQ.js";import"./@element-plus/icons-vue-CCtJZkK4.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-BjeWxBI0.js";import"./index-CwtlLwRh.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-CTWOnonq.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-QlbCbZWN.js";import"./@vueuse/shared-Bl-RnC0s.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-Bm7NQW9r.js";const a="/admin/assets/no_perms-jDxcYpYC.png",n={class:"error404"},W=p({__name:"403",setup(c){return(_,t)=>(i(),m("div",n,[e(r,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:s(()=>[...t[0]||(t[0]=[o("div",{class:"flex justify-center"},[o("img",{class:"w-[150px] h-[150px]",src:a,alt:""})],-1)])]),_:1})]))}});export{W as default};
import r from"./error-CyjCcUCY.js";import{f as p,ak as i,I as m,a as e,aN as s,J as o}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-DFTWCWyi.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-DBREMm_z.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const a="/admin/assets/no_perms-jDxcYpYC.png",n={class:"error404"},W=p({__name:"403",setup(c){return(_,t)=>(i(),m("div",n,[e(r,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:s(()=>[...t[0]||(t[0]=[o("div",{class:"flex justify-center"},[o("img",{class:"w-[150px] h-[150px]",src:a,alt:""})],-1)])]),_:1})]))}});export{W as default};
@@ -1 +1 @@
import o from"./error-v6z53F3Y.js";import{f as r,ak as t,I as m,a as p}from"./@vue/runtime-core-tF_2qJHK.js";import"./element-plus-NSC30APV.js";import"./@vue/runtime-dom-CEiKlVMn.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-BmZ-20sQ.js";import"./@element-plus/icons-vue-CCtJZkK4.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-BjeWxBI0.js";import"./index-CwtlLwRh.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-CTWOnonq.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-QlbCbZWN.js";import"./@vueuse/shared-Bl-RnC0s.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-Bm7NQW9r.js";const i={class:"error404"},T=r({__name:"404",setup(e){return(a,s)=>(t(),m("div",i,[p(o,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{T as default};
import o from"./error-CyjCcUCY.js";import{f as r,ak as t,I as m,a as p}from"./@vue/runtime-core-C6bnekPw.js";import"./element-plus-DFTWCWyi.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/shared-mAAVTE9n.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./vue-router-QlpZ4wdW.js";import"./index-DBREMm_z.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const i={class:"error404"},T=r({__name:"404",setup(e){return(a,s)=>(t(),m("div",i,[p(o,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{T as default};
@@ -1 +1 @@
import{H as u}from"../highlight.js-Bxt7hFFy.js";import{f as c,i as g,w as s,A as n}from"../@vue/runtime-core-tF_2qJHK.js";import{n as h}from"../@vue/reactivity-BmZ-20sQ.js";var i=c({props:{code:{type:String,required:!0},language:{type:String,default:""},autodetect:{type:Boolean,default:!0},ignoreIllegals:{type:Boolean,default:!0}},setup:function(e){var t=h(e.language);s((function(){return e.language}),(function(a){t.value=a}));var r=n((function(){return e.autodetect||!t.value})),o=n((function(){return!r.value&&!u.getLanguage(t.value)}));return{className:n((function(){return o.value?"":"hljs "+t.value})),highlightedCode:n((function(){var a;if(o.value)return console.warn('The language "'+t.value+'" you specified could not be found.'),e.code.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;");if(r.value){var l=u.highlightAuto(e.code);return t.value=(a=l.language)!==null&&a!==void 0?a:"",l.value}return(l=u.highlight(e.code,{language:t.value,ignoreIllegals:e.ignoreIllegals})).value}))}},render:function(){return g("pre",{},[g("code",{class:this.className,innerHTML:this.highlightedCode})])}}),v={install:function(e){e.component("highlightjs",i)},component:i};export{v as o};
import{H as u}from"../highlight.js-Bxt7hFFy.js";import{f as c,i as g,w as s,A as n}from"../@vue/runtime-core-C6bnekPw.js";import{n as h}from"../@vue/reactivity-DiY1c2vO.js";var i=c({props:{code:{type:String,required:!0},language:{type:String,default:""},autodetect:{type:Boolean,default:!0},ignoreIllegals:{type:Boolean,default:!0}},setup:function(e){var t=h(e.language);s((function(){return e.language}),(function(a){t.value=a}));var r=n((function(){return e.autodetect||!t.value})),o=n((function(){return!r.value&&!u.getLanguage(t.value)}));return{className:n((function(){return o.value?"":"hljs "+t.value})),highlightedCode:n((function(){var a;if(o.value)return console.warn('The language "'+t.value+'" you specified could not be found.'),e.code.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;");if(r.value){var l=u.highlightAuto(e.code);return t.value=(a=l.language)!==null&&a!==void 0?a:"",l.value}return(l=u.highlight(e.code,{language:t.value,ignoreIllegals:e.ignoreIllegals})).value}))}},render:function(){return g("pre",{},[g("code",{class:this.className,innerHTML:this.highlightedCode})])}}),v={install:function(e){e.component("highlightjs",i)},component:i};export{v as o};
@@ -1 +1 @@
import"./uikit-base-component-vue3-CTQKyL8o.js";import{A as r}from"../tuikit-atomicx-vue3-Bf7F3z1j.js";import{f as s,ak as c,I as l,aq as m,G as u,at as i,H as p,A as d}from"../@vue/runtime-core-tF_2qJHK.js";import{y as v}from"../@vue/reactivity-BmZ-20sQ.js";import"./chat-uikit-engine-zx802ozq.js";const _=(t,o)=>{const a=t.__vccOpts||t;for(const[e,n]of o)a[e]=n;return a},f={key:0,class:"chat"},h=s({name:"Chat",__name:"Chat",props:{PlaceholderEmpty:{default:null}},setup(t){const{activeConversation:o}=r(),a=d(()=>{var e;return!((e=o.value)!=null&&e.conversationID)});return(e,n)=>v(o)?(c(),l("div",f,[m(e.$slots,"default",{},void 0,!0)])):a.value&&t.PlaceholderEmpty?(c(),u(i(t.PlaceholderEmpty),{key:1})):p("",!0)}}),A=_(h,[["__scopeId","data-v-1c9c77cd"]]);typeof window<"u"&&(window.__CHAT_ATOMICX_VUE3__={name:"@tencentcloud/chat-uikit-vue3",version:"4.5.4"},console.log("[@tencentcloud/chat-uikit-vue3] v4.5.4"));export{A as E};
import"./uikit-base-component-vue3-YgTqL4da.js";import{A as r}from"../tuikit-atomicx-vue3-Dln8Zi6e.js";import{f as s,ak as c,I as l,aq as m,G as u,at as i,H as p,A as d}from"../@vue/runtime-core-C6bnekPw.js";import{y as v}from"../@vue/reactivity-DiY1c2vO.js";import"./chat-uikit-engine-zx802ozq.js";const _=(t,o)=>{const a=t.__vccOpts||t;for(const[e,n]of o)a[e]=n;return a},f={key:0,class:"chat"},h=s({name:"Chat",__name:"Chat",props:{PlaceholderEmpty:{default:null}},setup(t){const{activeConversation:o}=r(),a=d(()=>{var e;return!((e=o.value)!=null&&e.conversationID)});return(e,n)=>v(o)?(c(),l("div",f,[m(e.$slots,"default",{},void 0,!0)])):a.value&&t.PlaceholderEmpty?(c(),u(i(t.PlaceholderEmpty),{key:1})):p("",!0)}}),A=_(h,[["__scopeId","data-v-1c9c77cd"]]);typeof window<"u"&&(window.__CHAT_ATOMICX_VUE3__={name:"@tencentcloud/chat-uikit-vue3",version:"4.5.4"},console.log("[@tencentcloud/chat-uikit-vue3] v4.5.4"));export{A as E};
@@ -1,4 +1,4 @@
import{g as Ue}from"../lodash-D3kF6u-c.js";import{c as Ut,B as jt,n as rt,a as at,o as Wt,q as zt,b as qt,w as Gt,d as kt,e as Xt,f as Jt,g as B,F as ct,S as Qt,h as Yt,i as Zt,j as te,u as ee,k as se,l as ne,s as Z,r as tt,C as je,D as We,E as ze,m as qe,K as Ge,p as ke,T as Xe,t as Je,v as Qe,x as Ye,y as Ze,z as ts,A as es,G as ss,H as ns,I as os,J as is,L as rs,M as as,N as cs,O as ls,P as fs,Q as us,R as ps,U as ds,V as hs,W as ms,X as gs,Y as _s,Z as Ss,_ as bs,$ as Cs,a0 as ys,a1 as vs,a2 as Ts,a3 as ws,a4 as Es,a5 as As,a6 as Rs,a7 as Ps,a8 as Ms,a9 as Ns,aa as xs,ab as Os,ac as Vs,ad as Ds,ae as Ls,af as Is,ag as Bs,ah as Hs,ai as $s,aj as Fs,ak as Ks,al as Us,am as js,an as Ws,ao as zs,ap as qs,aq as Gs,ar as ks,as as Xs,at as Js,au as Qs,av as Ys,aw as Zs,ax as tn,ay as en,az as sn,aA as nn,aB as on,aC as rn,aD as an,aE as cn,aF as ln,aG as fn,aH as un,aI as pn,aJ as dn,aK as hn,aL as mn,aM as gn,aN as _n,aO as Sn,aP as bn,aQ as Cn,aR as yn}from"./runtime-core-tF_2qJHK.js";import{j as oe,n as L,C as v,e as T,h as vn,z as M,l as ie,s as Tn,D as wn,E as X,N as En,i as g,K as q,k as H,F as I,A as lt,I as et,L as re,f as An,M as Rn,O as Pn,u as Mn,G as ae,a as Nn,o as xn,P as On,p as Vn,Q as Dn,B as Ln}from"./shared-mAAVTE9n.js";import{y as ce,t as le,E as In,R as Bn,T as Hn,z as $n,q as Fn,A as Kn,B as Un,C as jn,D as Wn,i as zn,o as qn,x as Gn,a as kn,v as Xn,m as Jn,F as Qn,G as Yn,p as Zn,r as to,H as eo,n as so,s as no,c as oo,u as io,I as ro,J as ao,K as co,L as lo,M as fo}from"./reactivity-BmZ-20sQ.js";/**
import{g as Ue}from"../lodash-D3kF6u-c.js";import{c as Ut,B as jt,n as rt,a as at,o as Wt,q as zt,b as qt,w as Gt,d as kt,e as Xt,f as Jt,g as B,F as ct,S as Qt,h as Yt,i as Zt,j as te,u as ee,k as se,l as ne,s as Z,r as tt,C as je,D as We,E as ze,m as qe,K as Ge,p as ke,T as Xe,t as Je,v as Qe,x as Ye,y as Ze,z as ts,A as es,G as ss,H as ns,I as os,J as is,L as rs,M as as,N as cs,O as ls,P as fs,Q as us,R as ps,U as ds,V as hs,W as ms,X as gs,Y as _s,Z as Ss,_ as bs,$ as Cs,a0 as ys,a1 as vs,a2 as Ts,a3 as ws,a4 as Es,a5 as As,a6 as Rs,a7 as Ps,a8 as Ms,a9 as Ns,aa as xs,ab as Os,ac as Vs,ad as Ds,ae as Ls,af as Is,ag as Bs,ah as Hs,ai as $s,aj as Fs,ak as Ks,al as Us,am as js,an as Ws,ao as zs,ap as qs,aq as Gs,ar as ks,as as Xs,at as Js,au as Qs,av as Ys,aw as Zs,ax as tn,ay as en,az as sn,aA as nn,aB as on,aC as rn,aD as an,aE as cn,aF as ln,aG as fn,aH as un,aI as pn,aJ as dn,aK as hn,aL as mn,aM as gn,aN as _n,aO as Sn,aP as bn,aQ as Cn,aR as yn}from"./runtime-core-C6bnekPw.js";import{j as oe,n as L,C as v,e as T,h as vn,z as M,l as ie,s as Tn,D as wn,E as X,N as En,i as g,K as q,k as H,F as I,A as lt,I as et,L as re,f as An,M as Rn,O as Pn,u as Mn,G as ae,a as Nn,o as xn,P as On,p as Vn,Q as Dn,B as Ln}from"./shared-mAAVTE9n.js";import{y as ce,t as le,E as In,R as Bn,T as Hn,z as $n,u as Fn,A as Kn,B as Un,C as jn,D as Wn,i as zn,o as qn,x as Gn,a as kn,v as Xn,m as Jn,F as Qn,G as Yn,p as Zn,r as to,H as eo,n as so,s as no,c as oo,q as io,I as ro,J as ao,K as co,L as lo,M as fo}from"./reactivity-DiY1c2vO.js";/**
* @vue/runtime-dom v3.5.29
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{C as A,F as C,a as w,L as h,u as D,J as R,H as g,q as k,n as W}from"../@vue/reactivity-BmZ-20sQ.js";import{w as b,b as x,n as L,g as F,$ as M,a5 as P}from"../@vue/runtime-core-tF_2qJHK.js";function J(e){return A()?(C(e),!0):!1}const d=new WeakMap,z=(...e)=>{var t;const r=e[0],n=(t=F())==null?void 0:t.proxy;if(n==null&&!M())throw new Error("injectLocal must be called in setup");return n&&d.has(n)&&r in d.get(n)?d.get(n)[r]:P(...e)},B=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const K=e=>typeof e<"u",I=Object.prototype.toString,Q=e=>I.call(e)==="[object Object]",m=()=>{};function j(e,t){function r(...n){return new Promise((a,o)=>{Promise.resolve(e(()=>t.apply(this,n),{fn:t,thisArg:this,args:n})).then(a).catch(o)})}return r}const S=e=>e();function V(...e){let t=0,r,n=!0,a=m,o,s,i,u,c;!w(e[0])&&typeof e[0]=="object"?{delay:s,trailing:i=!0,leading:u=!0,rejectOnCancel:c=!1}=e[0]:[s,i=!0,u=!0,c=!1]=e;const f=()=>{r&&(clearTimeout(r),r=void 0,a(),a=m)};return O=>{const l=h(s),v=Date.now()-t,p=()=>o=O();return f(),l<=0?(t=Date.now(),p()):(v>l&&(u||!n)?(t=Date.now(),p()):i&&(o=new Promise((y,T)=>{a=c?T:y,r=setTimeout(()=>{t=Date.now(),n=!0,y(p()),f()},Math.max(0,l-v))})),!u&&!r&&(r=setTimeout(()=>n=!0,l)),n=!1,o)}}function E(e=S,t={}){const{initialState:r="active"}=t,n=N(r==="active");function a(){n.value=!1}function o(){n.value=!0}const s=(...i)=>{n.value&&e(...i)};return{isActive:g(n),pause:a,resume:o,eventFilter:s}}function U(e){return e.endsWith("rem")?Number.parseFloat(e)*16:Number.parseFloat(e)}function G(e){return F()}function X(e){return Array.isArray(e)?e:[e]}function N(...e){if(e.length!==1)return R(...e);const t=e[0];return typeof t=="function"?g(k(()=>({get:t,set:m}))):W(t)}function Y(e,t=200,r=!1,n=!0,a=!1){return j(V(t,r,n,a),e)}function _(e,t,r={}){const{eventFilter:n=S,...a}=r;return b(e,j(n,t),a)}function Z(e,t,r={}){const{eventFilter:n,initialState:a="active",...o}=r,{eventFilter:s,pause:i,resume:u,isActive:c}=E(n,{initialState:a});return{stop:_(e,t,{...o,eventFilter:s}),pause:i,resume:u,isActive:c}}function ee(e,t=!0,r){G()?x(e,r):t?e():L(e)}function te(e=!1,t={}){const{truthyValue:r=!0,falsyValue:n=!1}=t,a=w(e),o=D(e);function s(i){if(arguments.length)return o.value=i,o.value;{const u=h(r);return o.value=o.value===u?h(n):u,o.value}}return a?s:[o,s]}function ne(e,t,r){return b(e,t,{...r,immediate:!0})}export{N as a,ee as b,Q as c,X as d,Z as e,z as f,K as g,Y as h,B as i,U as p,J as t,te as u,ne as w};
import{C as A,F as C,a as w,L as h,q as D,J as R,H as g,u as k,n as W}from"../@vue/reactivity-DiY1c2vO.js";import{w as b,b as x,n as L,g as F,$ as M,a5 as P}from"../@vue/runtime-core-C6bnekPw.js";function J(e){return A()?(C(e),!0):!1}const d=new WeakMap,z=(...e)=>{var t;const r=e[0],n=(t=F())==null?void 0:t.proxy;if(n==null&&!M())throw new Error("injectLocal must be called in setup");return n&&d.has(n)&&r in d.get(n)?d.get(n)[r]:P(...e)},B=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const K=e=>typeof e<"u",I=Object.prototype.toString,Q=e=>I.call(e)==="[object Object]",m=()=>{};function j(e,t){function r(...n){return new Promise((a,o)=>{Promise.resolve(e(()=>t.apply(this,n),{fn:t,thisArg:this,args:n})).then(a).catch(o)})}return r}const S=e=>e();function V(...e){let t=0,r,n=!0,a=m,o,s,i,u,c;!w(e[0])&&typeof e[0]=="object"?{delay:s,trailing:i=!0,leading:u=!0,rejectOnCancel:c=!1}=e[0]:[s,i=!0,u=!0,c=!1]=e;const f=()=>{r&&(clearTimeout(r),r=void 0,a(),a=m)};return O=>{const l=h(s),v=Date.now()-t,p=()=>o=O();return f(),l<=0?(t=Date.now(),p()):(v>l&&(u||!n)?(t=Date.now(),p()):i&&(o=new Promise((y,T)=>{a=c?T:y,r=setTimeout(()=>{t=Date.now(),n=!0,y(p()),f()},Math.max(0,l-v))})),!u&&!r&&(r=setTimeout(()=>n=!0,l)),n=!1,o)}}function E(e=S,t={}){const{initialState:r="active"}=t,n=N(r==="active");function a(){n.value=!1}function o(){n.value=!0}const s=(...i)=>{n.value&&e(...i)};return{isActive:g(n),pause:a,resume:o,eventFilter:s}}function U(e){return e.endsWith("rem")?Number.parseFloat(e)*16:Number.parseFloat(e)}function G(e){return F()}function X(e){return Array.isArray(e)?e:[e]}function N(...e){if(e.length!==1)return R(...e);const t=e[0];return typeof t=="function"?g(k(()=>({get:t,set:m}))):W(t)}function Y(e,t=200,r=!1,n=!0,a=!1){return j(V(t,r,n,a),e)}function _(e,t,r={}){const{eventFilter:n=S,...a}=r;return b(e,j(n,t),a)}function Z(e,t,r={}){const{eventFilter:n,initialState:a="active",...o}=r,{eventFilter:s,pause:i,resume:u,isActive:c}=E(n,{initialState:a});return{stop:_(e,t,{...o,eventFilter:s}),pause:i,resume:u,isActive:c}}function ee(e,t=!0,r){G()?x(e,r):t?e():L(e)}function te(e=!1,t={}){const{truthyValue:r=!0,falsyValue:n=!1}=t,a=w(e),o=D(e);function s(i){if(arguments.length)return o.value=i,o.value;{const u=h(r);return o.value=o.value===u?h(n):u,o.value}}return a?s:[o,s]}function ne(e,t,r){return b(e,t,{...r,immediate:!0})}export{N as a,ee as b,Q as c,X as d,Z as e,z as f,K as g,Y as h,B as i,U as p,J as t,te as u,ne as w};
@@ -1,2 +1,2 @@
import{i as C,Q as y,a as E}from"./editor-Cyf37SuL.js";import{ak as g,I as h,f as w,b as P,w as O,aJ as b}from"../@vue/runtime-core-tF_2qJHK.js";import{n as d,t as $,u as F}from"../@vue/reactivity-BmZ-20sQ.js";var B=Object.defineProperty,D=Object.defineProperties,j=Object.getOwnPropertyDescriptors,m=Object.getOwnPropertySymbols,H=Object.prototype.hasOwnProperty,S=Object.prototype.propertyIsEnumerable,_=(e,t,o)=>t in e?B(e,t,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[t]=o,A=(e,t)=>{for(var o in t||(t={}))H.call(t,o)&&_(e,o,t[o]);if(m)for(var o of m(t))S.call(t,o)&&_(e,o,t[o]);return e},M=(e,t)=>D(e,j(t));function u(e){let t=`请使用 '@${e}' 事件,不要放在 props 中`;return t+=`
Please use '@${e}' event instead of props`,t}var v=(e,t)=>{for(const[o,a]of t)e[o]=a;return e};const V=w({props:{mode:{type:String,default:"default"},defaultContent:{type:Array,default:[]},defaultHtml:{type:String,default:""},defaultConfig:{type:Object,default:{}},modelValue:{type:String,default:""}},setup(e,t){const o=d(null),a=F(null),i=d(""),s=()=>{if(!o.value)return;const f=$(e.defaultContent);C({selector:o.value,mode:e.mode,content:f||[],html:e.defaultHtml||e.modelValue||"",config:M(A({},e.defaultConfig),{onCreated(r){if(a.value=r,t.emit("onCreated",r),e.defaultConfig.onCreated){const n=u("onCreated");throw new Error(n)}},onChange(r){const n=r.getHtml();if(i.value=n,t.emit("update:modelValue",n),t.emit("onChange",r),e.defaultConfig.onChange){const l=u("onChange");throw new Error(l)}},onDestroyed(r){if(t.emit("onDestroyed",r),e.defaultConfig.onDestroyed){const n=u("onDestroyed");throw new Error(n)}},onMaxLength(r){if(t.emit("onMaxLength",r),e.defaultConfig.onMaxLength){const n=u("onMaxLength");throw new Error(n)}},onFocus(r){if(t.emit("onFocus",r),e.defaultConfig.onFocus){const n=u("onFocus");throw new Error(n)}},onBlur(r){if(t.emit("onBlur",r),e.defaultConfig.onBlur){const n=u("onBlur");throw new Error(n)}},customAlert(r,n){if(t.emit("customAlert",r,n),e.defaultConfig.customAlert){const l=u("customAlert");throw new Error(l)}},customPaste:(r,n)=>{if(e.defaultConfig.customPaste){const c=u("customPaste");throw new Error(c)}let l;return t.emit("customPaste",r,n,c=>{l=c}),l}})})};function p(f){const r=a.value;r!=null&&r.setHtml(f)}return P(()=>{s()}),O(()=>e.modelValue,f=>{f!==i.value&&p(f)}),{box:o}}}),I={ref:"box",style:{height:"100%"}};function L(e,t,o,a,i,s){return g(),h("div",I,null,512)}var N=v(V,[["render",L]]);const T=w({props:{editor:{type:Object},mode:{type:String,default:"default"},defaultConfig:{type:Object,default:{}}},setup(e){const t=d(null),o=a=>{if(t.value){if(a==null)throw new Error("Not found instance of Editor when create <Toolbar/> component");y.getToolbar(a)||E({editor:a,selector:t.value||"<div></div>",mode:e.mode,config:e.defaultConfig})}};return b(()=>{const{editor:a}=e;a!=null&&o(a)}),{selector:t}}}),R={ref:"selector"};function k(e,t,o,a,i,s){return g(),h("div",R,null,512)}var q=v(T,[["render",k]]);export{N as E,q as T};
import{i as C,Q as y,a as E}from"./editor-Cyf37SuL.js";import{ak as g,I as h,f as w,b as P,w as O,aJ as b}from"../@vue/runtime-core-C6bnekPw.js";import{n as d,t as $,q as F}from"../@vue/reactivity-DiY1c2vO.js";var B=Object.defineProperty,D=Object.defineProperties,j=Object.getOwnPropertyDescriptors,m=Object.getOwnPropertySymbols,H=Object.prototype.hasOwnProperty,S=Object.prototype.propertyIsEnumerable,_=(e,t,o)=>t in e?B(e,t,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[t]=o,A=(e,t)=>{for(var o in t||(t={}))H.call(t,o)&&_(e,o,t[o]);if(m)for(var o of m(t))S.call(t,o)&&_(e,o,t[o]);return e},M=(e,t)=>D(e,j(t));function u(e){let t=`请使用 '@${e}' 事件,不要放在 props 中`;return t+=`
Please use '@${e}' event instead of props`,t}var v=(e,t)=>{for(const[o,a]of t)e[o]=a;return e};const V=w({props:{mode:{type:String,default:"default"},defaultContent:{type:Array,default:[]},defaultHtml:{type:String,default:""},defaultConfig:{type:Object,default:{}},modelValue:{type:String,default:""}},setup(e,t){const o=d(null),a=F(null),i=d(""),s=()=>{if(!o.value)return;const f=$(e.defaultContent);C({selector:o.value,mode:e.mode,content:f||[],html:e.defaultHtml||e.modelValue||"",config:M(A({},e.defaultConfig),{onCreated(r){if(a.value=r,t.emit("onCreated",r),e.defaultConfig.onCreated){const n=u("onCreated");throw new Error(n)}},onChange(r){const n=r.getHtml();if(i.value=n,t.emit("update:modelValue",n),t.emit("onChange",r),e.defaultConfig.onChange){const l=u("onChange");throw new Error(l)}},onDestroyed(r){if(t.emit("onDestroyed",r),e.defaultConfig.onDestroyed){const n=u("onDestroyed");throw new Error(n)}},onMaxLength(r){if(t.emit("onMaxLength",r),e.defaultConfig.onMaxLength){const n=u("onMaxLength");throw new Error(n)}},onFocus(r){if(t.emit("onFocus",r),e.defaultConfig.onFocus){const n=u("onFocus");throw new Error(n)}},onBlur(r){if(t.emit("onBlur",r),e.defaultConfig.onBlur){const n=u("onBlur");throw new Error(n)}},customAlert(r,n){if(t.emit("customAlert",r,n),e.defaultConfig.customAlert){const l=u("customAlert");throw new Error(l)}},customPaste:(r,n)=>{if(e.defaultConfig.customPaste){const c=u("customPaste");throw new Error(c)}let l;return t.emit("customPaste",r,n,c=>{l=c}),l}})})};function p(f){const r=a.value;r!=null&&r.setHtml(f)}return P(()=>{s()}),O(()=>e.modelValue,f=>{f!==i.value&&p(f)}),{box:o}}}),I={ref:"box",style:{height:"100%"}};function L(e,t,o,a,i,s){return g(),h("div",I,null,512)}var J=v(V,[["render",L]]);const T=w({props:{editor:{type:Object},mode:{type:String,default:"default"},defaultConfig:{type:Object,default:{}}},setup(e){const t=d(null),o=a=>{if(t.value){if(a==null)throw new Error("Not found instance of Editor when create <Toolbar/> component");y.getToolbar(a)||E({editor:a,selector:t.value||"<div></div>",mode:e.mode,config:e.defaultConfig})}};return b(()=>{const{editor:a}=e;a!=null&&o(a)}),{selector:t}}}),R={ref:"selector"};function k(e,t,o,a,i,s){return g(),h("div",R,null,512)}var N=v(T,[["render",k]]);export{J as E,N as T};
@@ -1 +0,0 @@
.cell-stack[data-v-05090333]{display:flex;flex-direction:column;gap:2px;line-height:1.35}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.cell-stack[data-v-4de87dfa]{display:flex;flex-direction:column;gap:2px;line-height:1.35}
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{L as x,N as y,r as I,d as $,M as C}from"./element-plus-NSC30APV.js";import{a6 as D}from"./@element-plus/icons-vue-CCtJZkK4.js";import{$ as L}from"./tcm-BNagiAsz.js";import{f as T,w as P,ak as g,I as A,aP as E,G as k,aN as n,a,O as m,J as B}from"./@vue/runtime-core-tF_2qJHK.js";import{Q as p}from"./@vue/shared-mAAVTE9n.js";import{y as F,n as b}from"./@vue/reactivity-BmZ-20sQ.js";import{_ as M}from"./index-CwtlLwRh.js";import"./@vue/runtime-dom-CEiKlVMn.js";import"./lodash-D3kF6u-c.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-BjeWxBI0.js";import"./pinia-CTWOnonq.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-QlbCbZWN.js";import"./@vueuse/shared-Bl-RnC0s.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-Bm7NQW9r.js";const V={class:"assign-log-panel"},K=T({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(h,{expose:w}){const c=h,d=b(!1),_=b([]);function v(o){const e=o.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function N(o){const e=o.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const i=new Date(t*1e3);if(Number.isNaN(i.getTime()))return"—";const r=s=>String(s).padStart(2,"0");return`${i.getFullYear()}-${r(i.getMonth()+1)}-${r(i.getDate())} ${r(i.getHours())}:${r(i.getMinutes())}:${r(i.getSeconds())}`}function u(o,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",i=e==="from"?"from_assistant_id":"to_assistant_id",r=o[t];if(r!=null&&String(r).trim()!==""&&String(r)!=="—")return String(r);const s=Number(o[i]);return Number.isFinite(s)&&s>0?`ID:${s}`:"—"}const f=async()=>{if(c.diagnosisId){d.value=!0;try{const o=await L({id:c.diagnosisId}),e=Array.isArray(o)?o:[];_.value=e}catch(o){console.error(o),_.value=[]}finally{d.value=!1}}};return P(()=>c.diagnosisId,()=>{f()},{immediate:!0}),w({refresh:f}),(o,e)=>{const t=y,i=$,r=I,s=C,S=x;return g(),A("div",V,[E((g(),k(s,{data:_.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:n(()=>[a(t,{label:"操作时间",width:"175",prop:"create_time_text"}),a(t,{label:"原医助","min-width":"120"},{default:n(({row:l})=>[m(p(u(l,"from")),1)]),_:1}),a(t,{label:"新医助","min-width":"120"},{default:n(({row:l})=>[m(p(u(l,"to")),1)]),_:1}),a(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:n(({row:l})=>[m(p(v(l)),1)]),_:1}),a(t,{label:"快照·业务单创建时间",width:"190"},{header:n(()=>[e[0]||(e[0]=B("span",null,"快照·业务单创建时间",-1)),a(r,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:n(()=>[a(i,{class:"assign-log-col-hint"},{default:n(()=>[a(F(D))]),_:1})]),_:1})]),default:n(({row:l})=>[m(p(N(l)),1)]),_:1}),a(t,{label:"操作人",width:"110",prop:"operator_name"}),a(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),a(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[S,d.value]])])}}}),yt=M(K,[["__scopeId","data-v-5465d5eb"]]);export{yt as default};
import{M as x,N as y,r as I,d as C,L as $}from"./element-plus-DFTWCWyi.js";import{W as D}from"./@element-plus/icons-vue-HOEUG8sr.js";import{a0 as L}from"./tcm-BcqaE0lo.js";import{f as T,w as P,ak as g,I as A,aP as E,G as k,aN as n,a,O as m,J as B}from"./@vue/runtime-core-C6bnekPw.js";import{Q as p}from"./@vue/shared-mAAVTE9n.js";import{y as F,n as b}from"./@vue/reactivity-DiY1c2vO.js";import{_ as M}from"./index-DBREMm_z.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const V={class:"assign-log-panel"},K=T({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(h,{expose:w}){const c=h,d=b(!1),_=b([]);function v(o){const e=o.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function N(o){const e=o.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(o.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const i=new Date(t*1e3);if(Number.isNaN(i.getTime()))return"—";const r=s=>String(s).padStart(2,"0");return`${i.getFullYear()}-${r(i.getMonth()+1)}-${r(i.getDate())} ${r(i.getHours())}:${r(i.getMinutes())}:${r(i.getSeconds())}`}function u(o,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",i=e==="from"?"from_assistant_id":"to_assistant_id",r=o[t];if(r!=null&&String(r).trim()!==""&&String(r)!=="—")return String(r);const s=Number(o[i]);return Number.isFinite(s)&&s>0?`ID:${s}`:"—"}const f=async()=>{if(c.diagnosisId){d.value=!0;try{const o=await L({id:c.diagnosisId}),e=Array.isArray(o)?o:[];_.value=e}catch(o){console.error(o),_.value=[]}finally{d.value=!1}}};return P(()=>c.diagnosisId,()=>{f()},{immediate:!0}),w({refresh:f}),(o,e)=>{const t=y,i=C,r=I,s=x,S=$;return g(),A("div",V,[E((g(),k(s,{data:_.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:n(()=>[a(t,{label:"操作时间",width:"175",prop:"create_time_text"}),a(t,{label:"原医助","min-width":"120"},{default:n(({row:l})=>[m(p(u(l,"from")),1)]),_:1}),a(t,{label:"新医助","min-width":"120"},{default:n(({row:l})=>[m(p(u(l,"to")),1)]),_:1}),a(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:n(({row:l})=>[m(p(v(l)),1)]),_:1}),a(t,{label:"快照·业务单创建时间",width:"190"},{header:n(()=>[e[0]||(e[0]=B("span",null,"快照·业务单创建时间",-1)),a(r,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:n(()=>[a(i,{class:"assign-log-col-hint"},{default:n(()=>[a(F(D))]),_:1})]),_:1})]),default:n(({row:l})=>[m(p(N(l)),1)]),_:1}),a(t,{label:"操作人",width:"110",prop:"operator_name"}),a(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),a(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[S,d.value]])])}}}),yt=M(K,[["__scopeId","data-v-5465d5eb"]]);export{yt as default};
@@ -1 +0,0 @@
.call-record-panel .call-record-toolbar[data-v-ccdd4cff]{display:flex;align-items:center;gap:12px;margin-bottom:12px}.call-record-panel .call-record-empty[data-v-ccdd4cff]{padding:28px 12px;color:var(--el-text-color-secondary);text-align:center}.call-record-panel .call-record-empty__title[data-v-ccdd4cff]{font-size:14px;color:var(--el-text-color-primary)}.call-record-panel .call-record-empty__desc[data-v-ccdd4cff]{margin-top:8px;font-size:12px;line-height:1.6}.call-record-panel .recording-list[data-v-ccdd4cff]{display:flex;flex-direction:column;gap:8px}.call-record-panel .recording-item[data-v-ccdd4cff]{display:flex;flex-direction:column;gap:6px}.call-record-panel .recording-video[data-v-ccdd4cff]{max-width:100%;max-height:180px;border-radius:4px;background:#000}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.call-record-panel .call-record-toolbar[data-v-78d5c9e4]{display:flex;align-items:center;gap:12px;margin-bottom:12px}.call-record-panel .call-record-empty[data-v-78d5c9e4]{padding:28px 12px;color:var(--el-text-color-secondary);text-align:center}.call-record-panel .call-record-empty__title[data-v-78d5c9e4]{font-size:14px;color:var(--el-text-color-primary)}.call-record-panel .call-record-empty__desc[data-v-78d5c9e4]{margin-top:8px;font-size:12px;line-height:1.6}
@@ -1 +1 @@
import{i as L,L as T,N as V,T as D,M as z,R as M}from"./element-plus-NSC30APV.js";import{a7 as O}from"./tcm-BNagiAsz.js";import{f as P,b as F,w as R,ak as a,I as n,a as i,aN as s,O as m,H as w,aP as j,G as g,F as A,J as H}from"./@vue/runtime-core-tF_2qJHK.js";import{y as d,n as k}from"./@vue/reactivity-BmZ-20sQ.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{_ as G}from"./index-CwtlLwRh.js";import"./@vue/runtime-dom-CEiKlVMn.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-CCtJZkK4.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-BjeWxBI0.js";import"./pinia-CTWOnonq.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-QlbCbZWN.js";import"./@vueuse/shared-Bl-RnC0s.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-Bm7NQW9r.js";const J={class:"case-record-list"},Q={key:0,class:"mb-3 flex justify-end"},Y={key:0},q={key:1,class:"text-gray-400"},K={class:"void-detail text-xs text-gray-500 mt-1"},U=P({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const _=y,h=C,l=k([]),p=k(!1),u=async()=>{if(_.diagnosisId){p.value=!0;try{const e=await O({diagnosis_id:_.diagnosisId});l.value=Array.isArray(e)?e:[]}catch(e){console.error("获取病历记录失败:",e),l.value=[]}finally{p.value=!1}}},S=e=>{if(!e)return"";const t=new Date(e*1e3);return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")} ${String(t.getHours()).padStart(2,"0")}:${String(t.getMinutes()).padStart(2,"0")}`},$=e=>{h("view",e)},B=()=>{h("openPrescription")};return F(()=>{u()}),R(()=>_.diagnosisId,()=>{u()}),x({refresh:u}),(e,t)=>{const b=L,r=V,v=D,E=z,N=M,I=T;return a(),n("div",J,[y.readOnly?w("",!0):(a(),n("div",Q,[i(b,{type:"primary",size:"small",onClick:B},{default:s(()=>[...t[0]||(t[0]=[m("开方",-1)])]),_:1})])),j((a(),g(E,{data:d(l),border:""},{default:s(()=>[i(r,{prop:"prescription_date",label:"就诊日期",width:"120"}),i(r,{prop:"visit_no",label:"门诊号",width:"120"}),i(r,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),i(r,{label:"处方摘要","min-width":"180"},{default:s(({row:o})=>[o.herbs&&o.herbs.length?(a(),n("span",Y,c(o.herbs.slice(0,3).map(f=>`${f.name}${f.dosage}`).join("、"))+c(o.herbs.length>3?"...":""),1)):(a(),n("span",q,"—"))]),_:1}),i(r,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),i(r,{label:"状态",width:"140",align:"center"},{default:s(({row:o})=>[o.void_status===1?(a(),n(A,{key:0},[i(v,{type:"danger",size:"small"},{default:s(()=>[...t[1]||(t[1]=[m("已作废",-1)])]),_:1}),H("div",K,c(o.void_by_name||"—")+" "+c(S(o.void_time)),1)],64)):(a(),g(v,{key:1,type:"success",size:"small"},{default:s(()=>[...t[2]||(t[2]=[m("正常",-1)])]),_:1}))]),_:1}),i(r,{label:"操作",width:"120",fixed:"right"},{default:s(({row:o})=>[i(b,{link:"",type:"primary",size:"small",onClick:f=>$(o)},{default:s(()=>[...t[3]||(t[3]=[m(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[I,d(p)]]),!d(p)&&d(l).length===0?(a(),g(N,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):w("",!0)])}}}),zt=G(U,[["__scopeId","data-v-043d2738"]]);export{zt as default};
import{i as L,L as T,N as V,T as D,M as z,R as M}from"./element-plus-DFTWCWyi.js";import{a8 as O}from"./tcm-BcqaE0lo.js";import{f as P,b as F,w as R,ak as a,I as n,a as i,aN as s,O as m,H as w,aP as j,G as g,F as A,J as H}from"./@vue/runtime-core-C6bnekPw.js";import{y as d,n as k}from"./@vue/reactivity-DiY1c2vO.js";import{Q as c}from"./@vue/shared-mAAVTE9n.js";import{_ as G}from"./index-DBREMm_z.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const J={class:"case-record-list"},Q={key:0,class:"mb-3 flex justify-end"},Y={key:0},q={key:1,class:"text-gray-400"},K={class:"void-detail text-xs text-gray-500 mt-1"},U=P({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const _=y,h=C,l=k([]),p=k(!1),u=async()=>{if(_.diagnosisId){p.value=!0;try{const e=await O({diagnosis_id:_.diagnosisId});l.value=Array.isArray(e)?e:[]}catch(e){console.error("获取病历记录失败:",e),l.value=[]}finally{p.value=!1}}},S=e=>{if(!e)return"";const t=new Date(e*1e3);return`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")} ${String(t.getHours()).padStart(2,"0")}:${String(t.getMinutes()).padStart(2,"0")}`},$=e=>{h("view",e)},B=()=>{h("openPrescription")};return F(()=>{u()}),R(()=>_.diagnosisId,()=>{u()}),x({refresh:u}),(e,t)=>{const b=L,r=V,v=D,E=z,N=M,I=T;return a(),n("div",J,[y.readOnly?w("",!0):(a(),n("div",Q,[i(b,{type:"primary",size:"small",onClick:B},{default:s(()=>[...t[0]||(t[0]=[m("开方",-1)])]),_:1})])),j((a(),g(E,{data:d(l),border:""},{default:s(()=>[i(r,{prop:"prescription_date",label:"就诊日期",width:"120"}),i(r,{prop:"visit_no",label:"门诊号",width:"120"}),i(r,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),i(r,{label:"处方摘要","min-width":"180"},{default:s(({row:o})=>[o.herbs&&o.herbs.length?(a(),n("span",Y,c(o.herbs.slice(0,3).map(f=>`${f.name}${f.dosage}`).join("、"))+c(o.herbs.length>3?"...":""),1)):(a(),n("span",q,"—"))]),_:1}),i(r,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),i(r,{label:"状态",width:"140",align:"center"},{default:s(({row:o})=>[o.void_status===1?(a(),n(A,{key:0},[i(v,{type:"danger",size:"small"},{default:s(()=>[...t[1]||(t[1]=[m("已作废",-1)])]),_:1}),H("div",K,c(o.void_by_name||"—")+" "+c(S(o.void_time)),1)],64)):(a(),g(v,{key:1,type:"success",size:"small"},{default:s(()=>[...t[2]||(t[2]=[m("正常",-1)])]),_:1}))]),_:1}),i(r,{label:"操作",width:"120",fixed:"right"},{default:s(({row:o})=>[i(b,{link:"",type:"primary",size:"small",onClick:f=>$(o)},{default:s(()=>[...t[3]||(t[3]=[m(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[I,d(p)]]),!d(p)&&d(l).length===0?(a(),g(N,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):w("",!0)])}}}),zt=G(U,[["__scopeId","data-v-043d2738"]]);export{zt as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.diagnosis-todo-list .toolbar[data-v-c136d72f]{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:12px}.diagnosis-todo-list .todo-table[data-v-c136d72f]{width:100%}.diagnosis-todo-list .pagination-wrap[data-v-c136d72f]{margin-top:12px;display:flex;justify-content:flex-end}.diagnosis-todo-list .text-danger[data-v-c136d72f]{color:var(--el-color-danger)}.diagnosis-todo-list .text-muted[data-v-c136d72f]{color:var(--el-text-color-placeholder)}
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
.diagnosis-todo-list .toolbar[data-v-f319b354]{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:12px}.diagnosis-todo-list .todo-table[data-v-f319b354]{width:100%}.diagnosis-todo-list .pagination-wrap[data-v-f319b354]{margin-top:12px;display:flex;justify-content:flex-end}.diagnosis-todo-list .text-danger[data-v-f319b354]{color:var(--el-color-danger)}.diagnosis-todo-list .text-muted[data-v-f319b354]{color:var(--el-text-color-placeholder)}
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
import{v as W,d as Z,R as q,a as K}from"./element-plus-NSC30APV.js";import{_ as Y}from"./picker-Z4o1kfTj.js";import{e as ee,c as te,i as S,_ as ie}from"./index-CwtlLwRh.js";import{s as A}from"./@vue/runtime-dom-CEiKlVMn.js";import{b as B,G as se}from"./@element-plus/icons-vue-CCtJZkK4.js";import{a as R,d as oe}from"./patient-D0BwU5eh.js";import{f as ne,w as re,ak as i,I as n,a as l,aN as c,J as a,H as d,F as v,ap as k,G as I}from"./@vue/runtime-core-tF_2qJHK.js";import{n as w,y as C}from"./@vue/reactivity-BmZ-20sQ.js";import{Q as z}from"./@vue/shared-mAAVTE9n.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./lodash-D3kF6u-c.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-CgAEaJvF.js";import"./index-DOquUsOX.js";import"./index.vue_vue_type_script_setup_true_lang-D53L8u8U.js";import"./index-LZuIxEs_.js";import"./index-CFOWc_EU.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-ByJn2Em4.js";import"./index.vue_vue_type_script_setup_true_lang-DXsNLQSJ.js";import"./@vueuse/core-QlbCbZWN.js";import"./@vueuse/shared-Bl-RnC0s.js";import"./usePaging-BIG1oWvJ.js";import"./vuedraggable-Bl4C18MR.js";import"./vue-BEnQNo4t.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-BjeWxBI0.js";import"./pinia-CTWOnonq.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-Bm7NQW9r.js";const le={class:"note-timeline-wrap"},ae={key:0,class:"timeline-actions"},me={class:"upload-trigger"},de={class:"upload-trigger"},pe={key:1,class:"note-timeline"},ce={class:"timeline-date"},ue={class:"timeline-body"},ge={key:0,class:"timeline-content"},_e={key:1,class:"timeline-images"},fe={key:2,class:"timeline-images"},ve={key:0,class:"thumb-wrap"},he={key:1,class:"file-wrap"},ye=["href","title"],ke={class:"file-name"},T=8e3,Ie=ne({__name:"NoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(m,{emit:G}){const u=m,b=G,L=ee(),g=t=>L.getImageUrl(t),x=w([]),E=w([]),_=w(0),f=w(0),$=["jpg","jpeg","png","gif","bmp","webp","svg"],N=t=>{var s;const e=((s=t.split(".").pop())==null?void 0:s.toLowerCase().split("?")[0])||"";return $.includes(e)},M=t=>{var s;const e=t.split("/");return decodeURIComponent(((s=e[e.length-1])==null?void 0:s.split("?")[0])||"文件")},j=t=>t.filter(N).map(g),O=(t,e)=>{const s=t.filter(N),h=t[e];return s.indexOf(h)},X=t=>t?t.split(`
`).filter(Boolean):[],H=t=>{if(!u.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=_.value){_.value=e.length;return}const s=e.slice(_.value);_.value=e.length,s.length>0&&R({diagnosis_id:u.diagnosisId,tongue_images:s}).then(()=>{S.msgSuccess("舌苔照片已添加"),b("refresh")})},J=t=>{if(!u.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=f.value){f.value=e.length;return}const s=e.slice(f.value);f.value=e.length,s.length>0&&R({diagnosis_id:u.diagnosisId,report_files:s}).then(()=>{S.msgSuccess("检查报告已添加"),b("refresh")})};re(()=>u.notes,()=>{x.value=[],E.value=[],_.value=0,f.value=0});const V=async(t,e,s)=>{try{await K.confirm("确认删除?","提示",{type:"warning"})}catch{return}await oe({note_id:t,image_type:e,image_path:s}),S.msgSuccess("已删除"),b("refresh")};return(t,e)=>{const s=te,h=Y,U=W,y=Z,Q=q;return i(),n("div",le,[!m.readonly&&m.diagnosisId?(i(),n("div",ae,[l(h,{modelValue:x.value,"onUpdate:modelValue":e[0]||(e[0]=o=>x.value=o),limit:99,type:"image","exclude-domain":!0,onChange:H},{upload:c(()=>[a("div",me,[l(s,{size:20,name:"el-icon-Plus"}),e[2]||(e[2]=a("span",null,"舌苔照片",-1))])]),_:1},8,["modelValue"]),l(h,{modelValue:E.value,"onUpdate:modelValue":e[1]||(e[1]=o=>E.value=o),limit:99,type:"file","exclude-domain":!0,onChange:J},{upload:c(()=>[a("div",de,[l(s,{size:20,name:"el-icon-Plus"}),e[3]||(e[3]=a("span",null,"检查报告",-1))])]),_:1},8,["modelValue"])])):d("",!0),m.notes.length?(i(),n("div",pe,[(i(!0),n(v,null,k(m.notes,o=>{var D,F;return i(),n("div",{key:o.id,class:"timeline-node"},[e[6]||(e[6]=a("div",{class:"timeline-dot"},null,-1)),a("div",ce,z(o.note_date),1),a("div",ue,[o.content?(i(),n("div",ge,[(i(!0),n(v,null,k(X(o.content),(r,p)=>(i(),n("div",{key:p,class:"content-line"},z(r),1))),128))])):d("",!0),(D=o.tongue_images)!=null&&D.length?(i(),n("div",_e,[e[4]||(e[4]=a("span",{class:"images-label"},"舌苔照片",-1)),(i(!0),n(v,null,k(o.tongue_images,(r,p)=>(i(),n("div",{key:p,class:"thumb-wrap"},[l(U,{src:g(r),"preview-src-list":o.tongue_images.map(g),"initial-index":p,"z-index":T,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),m.readonly?d("",!0):(i(),I(y,{key:0,class:"thumb-delete",onClick:A(P=>V(o.id,"tongue_images",r),["stop"])},{default:c(()=>[l(C(B))]),_:1},8,["onClick"]))]))),128))])):d("",!0),(F=o.report_files)!=null&&F.length?(i(),n("div",fe,[e[5]||(e[5]=a("span",{class:"images-label"},"检查报告",-1)),(i(!0),n(v,null,k(o.report_files,(r,p)=>(i(),n(v,{key:p},[N(r)?(i(),n("div",ve,[l(U,{src:g(r),"preview-src-list":j(o.report_files),"initial-index":O(o.report_files,p),"z-index":T,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),m.readonly?d("",!0):(i(),I(y,{key:0,class:"thumb-delete",onClick:A(P=>V(o.id,"report_files",r),["stop"])},{default:c(()=>[l(C(B))]),_:1},8,["onClick"]))])):(i(),n("div",he,[a("a",{href:g(r),target:"_blank",class:"file-link",title:M(r)},[l(y,{size:20},{default:c(()=>[l(C(se))]),_:1}),a("span",ke,z(M(r)),1)],8,ye),m.readonly?d("",!0):(i(),I(y,{key:0,class:"file-delete",onClick:A(P=>V(o.id,"report_files",r),["stop"])},{default:c(()=>[l(C(B))]),_:1},8,["onClick"]))]))],64))),128))])):d("",!0)])])}),128))])):d("",!0),!m.notes.length&&m.readonly?(i(),I(Q,{key:2,description:"暂无备注","image-size":48})):d("",!0)])}}}),It=ie(Ie,[["__scopeId","data-v-f77bb3f4"]]);export{It as default};
import{R as W,v as Z,d as q,a as K}from"./element-plus-DFTWCWyi.js";import{_ as Y}from"./picker-BCRrEZaA.js";import{e as ee,c as te,i as S,_ as ie}from"./index-DBREMm_z.js";import{s as A}from"./@vue/runtime-dom-DDAG46FW.js";import{b as B,G as se}from"./@element-plus/icons-vue-HOEUG8sr.js";import{d as oe,a as R}from"./patient-DdWkVUZB.js";import{f as ne,w as re,ak as i,I as n,a as l,aN as c,J as a,H as d,F as v,ap as k,G as I}from"./@vue/runtime-core-C6bnekPw.js";import{n as w,y as C}from"./@vue/reactivity-DiY1c2vO.js";import{Q as z}from"./@vue/shared-mAAVTE9n.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./lodash-D3kF6u-c.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./index-BwF7vczx.js";import"./index-BiPYXDtP.js";import"./index.vue_vue_type_script_setup_true_lang-CcvIuIWy.js";import"./index-CbfZXeGu.js";import"./index-CRoZWY-W.js";import"./cos-js-sdk-v5-DSMN0LUT.js";import"./file-BimpIRb_.js";import"./index.vue_vue_type_script_setup_true_lang-x0oL2ShL.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./usePaging-VsbTxSU0.js";import"./vuedraggable-5bKFmC7X.js";import"./vue-BVs68q8v.js";import"./@vue/compiler-dom-CqayqxS7.js";import"./@vue/compiler-core-CvbQOXIO.js";import"./sortablejs-dsEAUXlE.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const le={class:"note-timeline-wrap"},ae={key:0,class:"timeline-actions"},me={class:"upload-trigger"},de={class:"upload-trigger"},pe={key:1,class:"note-timeline"},ce={class:"timeline-date"},ue={class:"timeline-body"},ge={key:0,class:"timeline-content"},_e={key:1,class:"timeline-images"},fe={key:2,class:"timeline-images"},ve={key:0,class:"thumb-wrap"},he={key:1,class:"file-wrap"},ye=["href","title"],ke={class:"file-name"},T=8e3,Ie=ne({__name:"NoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(m,{emit:G}){const u=m,b=G,L=ee(),g=t=>L.getImageUrl(t),x=w([]),E=w([]),_=w(0),f=w(0),$=["jpg","jpeg","png","gif","bmp","webp","svg"],N=t=>{var s;const e=((s=t.split(".").pop())==null?void 0:s.toLowerCase().split("?")[0])||"";return $.includes(e)},M=t=>{var s;const e=t.split("/");return decodeURIComponent(((s=e[e.length-1])==null?void 0:s.split("?")[0])||"文件")},j=t=>t.filter(N).map(g),O=(t,e)=>{const s=t.filter(N),h=t[e];return s.indexOf(h)},X=t=>t?t.split(`
`).filter(Boolean):[],H=t=>{if(!u.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=_.value){_.value=e.length;return}const s=e.slice(_.value);_.value=e.length,s.length>0&&R({diagnosis_id:u.diagnosisId,tongue_images:s}).then(()=>{S.msgSuccess("舌苔照片已添加"),b("refresh")})},J=t=>{if(!u.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=f.value){f.value=e.length;return}const s=e.slice(f.value);f.value=e.length,s.length>0&&R({diagnosis_id:u.diagnosisId,report_files:s}).then(()=>{S.msgSuccess("检查报告已添加"),b("refresh")})};re(()=>u.notes,()=>{x.value=[],E.value=[],_.value=0,f.value=0});const V=async(t,e,s)=>{try{await K.confirm("确认删除?","提示",{type:"warning"})}catch{return}await oe({note_id:t,image_type:e,image_path:s}),S.msgSuccess("已删除"),b("refresh")};return(t,e)=>{const s=te,h=Y,U=Z,y=q,Q=W;return i(),n("div",le,[!m.readonly&&m.diagnosisId?(i(),n("div",ae,[l(h,{modelValue:x.value,"onUpdate:modelValue":e[0]||(e[0]=o=>x.value=o),limit:99,type:"image","exclude-domain":!0,onChange:H},{upload:c(()=>[a("div",me,[l(s,{size:20,name:"el-icon-Plus"}),e[2]||(e[2]=a("span",null,"舌苔照片",-1))])]),_:1},8,["modelValue"]),l(h,{modelValue:E.value,"onUpdate:modelValue":e[1]||(e[1]=o=>E.value=o),limit:99,type:"file","exclude-domain":!0,onChange:J},{upload:c(()=>[a("div",de,[l(s,{size:20,name:"el-icon-Plus"}),e[3]||(e[3]=a("span",null,"检查报告",-1))])]),_:1},8,["modelValue"])])):d("",!0),m.notes.length?(i(),n("div",pe,[(i(!0),n(v,null,k(m.notes,o=>{var D,F;return i(),n("div",{key:o.id,class:"timeline-node"},[e[6]||(e[6]=a("div",{class:"timeline-dot"},null,-1)),a("div",ce,z(o.note_date),1),a("div",ue,[o.content?(i(),n("div",ge,[(i(!0),n(v,null,k(X(o.content),(r,p)=>(i(),n("div",{key:p,class:"content-line"},z(r),1))),128))])):d("",!0),(D=o.tongue_images)!=null&&D.length?(i(),n("div",_e,[e[4]||(e[4]=a("span",{class:"images-label"},"舌苔照片",-1)),(i(!0),n(v,null,k(o.tongue_images,(r,p)=>(i(),n("div",{key:p,class:"thumb-wrap"},[l(U,{src:g(r),"preview-src-list":o.tongue_images.map(g),"initial-index":p,"z-index":T,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),m.readonly?d("",!0):(i(),I(y,{key:0,class:"thumb-delete",onClick:A(P=>V(o.id,"tongue_images",r),["stop"])},{default:c(()=>[l(C(B))]),_:1},8,["onClick"]))]))),128))])):d("",!0),(F=o.report_files)!=null&&F.length?(i(),n("div",fe,[e[5]||(e[5]=a("span",{class:"images-label"},"检查报告",-1)),(i(!0),n(v,null,k(o.report_files,(r,p)=>(i(),n(v,{key:p},[N(r)?(i(),n("div",ve,[l(U,{src:g(r),"preview-src-list":j(o.report_files),"initial-index":O(o.report_files,p),"z-index":T,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),m.readonly?d("",!0):(i(),I(y,{key:0,class:"thumb-delete",onClick:A(P=>V(o.id,"report_files",r),["stop"])},{default:c(()=>[l(C(B))]),_:1},8,["onClick"]))])):(i(),n("div",he,[a("a",{href:g(r),target:"_blank",class:"file-link",title:M(r)},[l(y,{size:20},{default:c(()=>[l(C(se))]),_:1}),a("span",ke,z(M(r)),1)],8,ye),m.readonly?d("",!0):(i(),I(y,{key:0,class:"file-delete",onClick:A(P=>V(o.id,"report_files",r),["stop"])},{default:c(()=>[l(C(B))]),_:1},8,["onClick"]))]))],64))),128))])):d("",!0)])])}),128))])):d("",!0),!m.notes.length&&m.readonly?(i(),I(Q,{key:2,description:"暂无备注","image-size":48})):d("",!0)])}}}),It=ie(Ie,[["__scopeId","data-v-f77bb3f4"]]);export{It as default};
@@ -1 +1 @@
import{m as x,f as y,a as B}from"./diag-display-DCz_VAqj.js";import{f as w,ak as I,I as P,J as a,H as N}from"./@vue/runtime-core-tF_2qJHK.js";import{Q as i}from"./@vue/shared-mAAVTE9n.js";import{y as e}from"./@vue/reactivity-BmZ-20sQ.js";import{_ as V}from"./index-CwtlLwRh.js";import"./lodash-D3kF6u-c.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-BjeWxBI0.js";import"./pinia-CTWOnonq.js";import"./axios-C80V62Fs.js";import"./element-plus-NSC30APV.js";import"./@vue/runtime-dom-CEiKlVMn.js";import"./@element-plus/icons-vue-CCtJZkK4.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./@vueuse/core-QlbCbZWN.js";import"./@vueuse/shared-Bl-RnC0s.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-Bm7NQW9r.js";const D={class:"card patient-card"},E={class:"patient-hero"},G={class:"patient-name"},H={class:"patient-meta"},J={key:0},Q=w({__name:"PatientInfoCard",props:{apt:{default:()=>({})},diag:{default:()=>({})}},setup(t){return(S,o)=>{var m,r,n,d,p,s,c,l,g,f,u,h,v,k,C;return I(),P("div",D,[o[0]||(o[0]=a("div",{class:"card-title"},"患者信息",-1)),a("div",E,[a("div",G,i(((m=t.apt)==null?void 0:m.patient_name)||"—"),1),a("div",H,[a("div",null,i(e(x)((r=t.apt)==null?void 0:r.patient_phone))+" · "+i(e(y)((n=t.diag)==null?void 0:n.gender))+" · "+i(((d=t.diag)==null?void 0:d.age)!=null?t.diag.age+"岁":"—"),1),a("div",null,i((p=t.diag)!=null&&p.height?t.diag.height+"cm":"—")+" / "+i((s=t.diag)!=null&&s.weight?t.diag.weight+"kg":"—")+" · "+i(((c=t.diag)==null?void 0:c.region)||"—"),1),a("div",null," 预约:"+i((l=t.apt)==null?void 0:l.appointment_date)+" "+i((g=t.apt)==null?void 0:g.appointment_time)+" · "+i(e(B)((f=t.apt)==null?void 0:f.period)),1),a("div",null,"医生:"+i(((u=t.apt)==null?void 0:u.doctor_name)||"—")+" 医助:"+i(((h=t.apt)==null?void 0:h.assistant_name)||"—"),1),a("div",null," 状态:"+i(((v=t.apt)==null?void 0:v.status_desc)||"—")+" · "+i((k=t.apt)!=null&&k.has_prescription?"已开方":"未开方"),1),(C=t.apt)!=null&&C.remark?(I(),P("div",J,"备注:"+i(t.apt.remark),1)):N("",!0)])])])}}}),Ct=V(Q,[["__scopeId","data-v-e3476da5"]]);export{Ct as default};
import{m as x,f as y,a as B}from"./diag-display-DCz_VAqj.js";import{f as w,ak as I,I as P,J as a,H as N}from"./@vue/runtime-core-C6bnekPw.js";import{Q as i}from"./@vue/shared-mAAVTE9n.js";import{y as e}from"./@vue/reactivity-DiY1c2vO.js";import{_ as V}from"./index-DBREMm_z.js";import"./lodash-D3kF6u-c.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./element-plus-DFTWCWyi.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const D={class:"card patient-card"},E={class:"patient-hero"},G={class:"patient-name"},H={class:"patient-meta"},J={key:0},Q=w({__name:"PatientInfoCard",props:{apt:{default:()=>({})},diag:{default:()=>({})}},setup(t){return(S,o)=>{var m,r,n,d,p,s,c,l,g,f,u,h,v,k,C;return I(),P("div",D,[o[0]||(o[0]=a("div",{class:"card-title"},"患者信息",-1)),a("div",E,[a("div",G,i(((m=t.apt)==null?void 0:m.patient_name)||"—"),1),a("div",H,[a("div",null,i(e(x)((r=t.apt)==null?void 0:r.patient_phone))+" · "+i(e(y)((n=t.diag)==null?void 0:n.gender))+" · "+i(((d=t.diag)==null?void 0:d.age)!=null?t.diag.age+"岁":"—"),1),a("div",null,i((p=t.diag)!=null&&p.height?t.diag.height+"cm":"—")+" / "+i((s=t.diag)!=null&&s.weight?t.diag.weight+"kg":"—")+" · "+i(((c=t.diag)==null?void 0:c.region)||"—"),1),a("div",null," 预约:"+i((l=t.apt)==null?void 0:l.appointment_date)+" "+i((g=t.apt)==null?void 0:g.appointment_time)+" · "+i(e(B)((f=t.apt)==null?void 0:f.period)),1),a("div",null,"医生:"+i(((u=t.apt)==null?void 0:u.doctor_name)||"—")+" 医助:"+i(((h=t.apt)==null?void 0:h.assistant_name)||"—"),1),a("div",null," 状态:"+i(((v=t.apt)==null?void 0:v.status_desc)||"—")+" · "+i((k=t.apt)!=null&&k.has_prescription?"已开方":"未开方"),1),(C=t.apt)!=null&&C.remark?(I(),P("div",J,"备注:"+i(t.apt.remark),1)):N("",!0)])])])}}}),Ct=V(Q,[["__scopeId","data-v-e3476da5"]]);export{Ct as default};
@@ -1 +0,0 @@
import{R as V,L as C,N as D,T as O,M as S}from"./element-plus-NSC30APV.js";import{_ as B}from"./index.vue_vue_type_script_setup_true_lang-D53L8u8U.js";import{u as j}from"./usePaging-BIG1oWvJ.js";import{j as A}from"./tcm-BNagiAsz.js";import{f as M,w as z,ak as d,I as g,a as i,F as R,aP as q,G,aN as n,J as h,O as c,A as w}from"./@vue/runtime-core-tF_2qJHK.js";import{y as u,a as H,r as J}from"./@vue/reactivity-BmZ-20sQ.js";import{Q as p}from"./@vue/shared-mAAVTE9n.js";import{_ as Q}from"./index-CwtlLwRh.js";import"./@vue/runtime-dom-CEiKlVMn.js";import"./lodash-D3kF6u-c.js";import"./@element-plus/icons-vue-CCtJZkK4.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-BjeWxBI0.js";import"./pinia-CTWOnonq.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-QlbCbZWN.js";import"./@vueuse/shared-Bl-RnC0s.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-Bm7NQW9r.js";const U={class:"patient-order-list"},Y={key:0,class:"po-empty-tip"},K={class:"text-red-500 font-semibold"},W={class:"flex justify-end mt-3"},X=M({__name:"PatientOrderList",props:{diagnosisId:{},patientId:{}},setup(y,{expose:v}){const l=y,f=w(()=>{const e=Number(l.patientId);return Number.isFinite(e)&&e>0?e:0}),_=w(()=>f.value>0),m=J({}),{pager:s,getLists:b,resetPage:x}=j({fetchFun:A,params:m,size:10}),N=()=>{Object.keys(m).forEach(e=>delete m[e]),l.diagnosisId>0&&(m.context_diagnosis_id=l.diagnosisId),_.value&&(m.patient_id=f.value),m.scene="diagnosis_edit"},I=e=>{const t=Number(e);return Number.isFinite(t)?t.toFixed(2):"0.00"},T=e=>{if(e==null||e==="")return"—";if(typeof e=="number"&&e>1e9&&e<1e11){const t=new Date(e*1e3),a=r=>String(r).padStart(2,"0");return`${t.getFullYear()}-${a(t.getMonth()+1)}-${a(t.getDate())} ${a(t.getHours())}:${a(t.getMinutes())}:${a(t.getSeconds())}`}return String(e)},k=e=>({1:"待双审通过",2:"待发货",3:"已完成",4:"已取消",5:"已发货",6:"已签收",7:"进行中",8:"暂不制药",9:"拒收",10:"退款",11:"保留药方",12:"制药缓发"})[Number(e)]??"—",E=e=>{const t=Number(e);return t===3||t===6?"success":t===5?"primary":t===4||t===9||t===10?"danger":t===2||t===7?"warning":"info"};return z(()=>[l.diagnosisId,f.value],()=>{if(!_.value){s.lists=[],s.count=0;return}N(),x()},{immediate:!0}),v({refresh:()=>b()}),(e,t)=>{const a=V,r=D,F=O,P=S,$=B,L=C;return d(),g("div",U,[_.value?(d(),g(R,{key:1},[q((d(),G(P,{data:u(s).lists,border:"",stripe:"","empty-text":"暂无业务订单"},{default:n(()=>[i(r,{label:"订单编号",prop:"order_no","min-width":"200","show-overflow-tooltip":""}),i(r,{label:"金额",width:"120",align:"right"},{default:n(({row:o})=>[h("span",K,"¥"+p(I(o.amount)),1)]),_:1}),i(r,{label:"医生",prop:"doctor_name",width:"110","show-overflow-tooltip":""},{default:n(({row:o})=>[c(p(o.doctor_name||"—"),1)]),_:1}),i(r,{label:"医助",prop:"assistant_name",width:"110","show-overflow-tooltip":""},{default:n(({row:o})=>[c(p(o.assistant_name||"—"),1)]),_:1}),i(r,{label:"履约状态",width:"110"},{default:n(({row:o})=>[i(F,{type:E(o.fulfillment_status),size:"small",effect:"light"},{default:n(()=>[c(p(k(o.fulfillment_status)),1)]),_:2},1032,["type"])]),_:1}),i(r,{label:"创建时间","min-width":"170"},{default:n(({row:o})=>[c(p(T(o.create_time)),1)]),_:1})]),_:1},8,["data"])),[[L,u(s).loading]]),h("div",W,[i($,{modelValue:u(s),"onUpdate:modelValue":t[0]||(t[0]=o=>H(s)?s.value=o:null),onChange:u(b)},null,8,["modelValue","onChange"])])],64)):(d(),g("div",Y,[i(a,{description:"当前诊单未携带患者ID,无法列出业务订单"})]))])}}}),At=Q(X,[["__scopeId","data-v-e5ebc4c4"]]);export{At as default};
@@ -1 +0,0 @@
.patient-order-list[data-v-e5ebc4c4]{padding:4px 0}.po-empty-tip[data-v-e5ebc4c4]{padding:24px 0}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
.patient-order-list[data-v-b8d83d0c]{padding:4px 0}.po-empty-tip[data-v-b8d83d0c]{padding:24px 0}.audit-stamp[data-v-b8d83d0c]{position:absolute;top:12px;right:16px;transform:rotate(15deg);opacity:.15;pointer-events:none;z-index:1}.stamp-inner[data-v-b8d83d0c]{border:3px solid currentColor;border-radius:8px;padding:4px 12px;font-size:18px;font-weight:700}.stamp-pass[data-v-b8d83d0c]{color:#22c55e}.stamp-reject[data-v-b8d83d0c]{color:#ef4444}
@@ -0,0 +1 @@
.recording-list[data-v-d67b2e91]{display:flex;flex-direction:column;gap:8px}.recording-item[data-v-d67b2e91]{display:flex;flex-direction:column;gap:6px;transform:translateZ(0)}.recording-alternates[data-v-d67b2e91]{margin-top:4px;padding-top:8px;border-top:1px dashed var(--el-border-color-lighter)}.recording-alternates__label[data-v-d67b2e91]{display:block;margin-bottom:6px;font-size:12px;color:var(--el-text-color-secondary)}.recording-alternates__links[data-v-d67b2e91]{display:flex;flex-wrap:wrap;gap:8px 12px}.recording-alternates__link[data-v-d67b2e91]{font-size:12px}
@@ -0,0 +1 @@
import{_ as L}from"./element-plus-DFTWCWyi.js";import B from"./RecordingVideoPlayer-DJ_BP9x7.js";import{e as H,_ as I}from"./index-DBREMm_z.js";import{f as w,ak as n,I as m,J as p,F as _,G as u,aN as v,O as k,H as y,ap as R,A as d}from"./@vue/runtime-core-C6bnekPw.js";import{Q as q}from"./@vue/shared-mAAVTE9n.js";import"./@vue/runtime-dom-DDAG46FW.js";import"./lodash-D3kF6u-c.js";import"./@vue/reactivity-DiY1c2vO.js";import"./@element-plus/icons-vue-HOEUG8sr.js";import"./lodash-es-C2A-Pj28.js";import"./@popperjs/core-C5az9QE8.js";import"./dayjs-DG77mNTn.js";import"./@ctrl/tinycolor-BuyEbX8F.js";import"./async-validator-CFA_igpM.js";import"./normalize-wheel-es-BQoi3Ox2.js";import"./jspdf-BsthCvR5.js";import"./@babel/runtime-BanGtE2-.js";import"./fflate-_ayOYiT1.js";import"./vue-router-QlpZ4wdW.js";import"./pinia-B8cvXbiZ.js";import"./axios-C80V62Fs.js";import"./@vueuse/core-1S4fJlii.js";import"./@vueuse/shared-Cl3lVqjz.js";import"./css-color-function-DI01JAaZ.js";import"./balanced-match-BdS7OldZ.js";import"./color-B2zxaWkI.js";import"./clone-DBZ6_OiU.js";import"./color-convert-OrFsTA_V.js";import"./color-name-Dju3oUBS.js";import"./color-string-pUQDDJII.js";import"./ms-CzQ2E3wO.js";import"./vue-clipboard3-DByT_rEQ.js";import"./clipboard-CHgxszqY.js";import"./echarts-CNLwa9wG.js";import"./tslib-BDyQ-Jie.js";import"./zrender-DRNXw7y3.js";import"./highlight.js-Bxt7hFFy.js";import"./@highlightjs/vue-plugin-C4onkBvW.js";const C={key:0,class:"recording-list"},N={class:"recording-item"},P={key:1,class:"recording-alternates"},V={class:"recording-alternates__links"},A={key:1,class:"text-gray-400"},E=w({__name:"RecordingPlaybackBlock",props:{recordId:{},urls:{}},setup(c){const $=c,h=H(),l=d(()=>{const e=$.urls||[],t=new Set,r=[];for(const s of e){const o=String(s??"").trim();!o||t.has(o)||(t.add(o),r.push(o))}return r}),a=d(()=>{const e=l.value;if(!e.length)return null;const t=e.find(i=>/\.mp4(\?|#|$)/i.test(i));if(t)return t;const r=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/vod-qcloud\.com/i.test(i));if(r)return r;const s=e.find(i=>/\.m3u8(\?|#|$)/i.test(i)&&/\.cos\.[^/]+\.myqcloud\.com/i.test(i));if(s)return s;const o=e.find(i=>/\.m3u8(\?|#|$)/i.test(i));return o||e[0]}),f=d(()=>{const e=l.value,t=a.value;return t?e.filter(r=>r!==t):e.slice(1)});function b(e){if(!e||typeof e!="string")return!1;const t=e.trim();return/^https?:\/\//i.test(t)?/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t):/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t)||t.startsWith("/uploads/")}function g(e){return h.getImageUrl(String(e||"").trim())}function S(e,t){const r=e.trim();return/\.mp4(\?|#|$)/i.test(r)?`MP4 ${t+1}`:/vod-qcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`点播 ${t+1}`:/\.cos\.[^/]+\.myqcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`COS HLS ${t+1}`:/\.m3u8/i.test(r)?`HLS ${t+1}`:`链接 ${t+1}`}return(e,t)=>{const r=L;return l.value.length?(n(),m("div",C,[p("div",N,[a.value?(n(),m(_,{key:0},[b(a.value)?(n(),u(B,{key:`${c.recordId}-${a.value}`,src:a.value},null,8,["src"])):(n(),u(r,{key:1,href:g(a.value),target:"_blank",type:"primary"},{default:v(()=>[...t[0]||(t[0]=[k(" 打开回放 ",-1)])]),_:1},8,["href"]))],64)):y("",!0),f.value.length?(n(),m("div",P,[t[1]||(t[1]=p("span",{class:"recording-alternates__label"},"备用地址",-1)),p("div",V,[(n(!0),m(_,null,R(f.value,(s,o)=>(n(),u(r,{key:`${c.recordId}-alt-${o}-${s.slice(-32)}`,href:g(s),target:"_blank",type:"primary",class:"recording-alternates__link"},{default:v(()=>[k(q(S(s,o)),1)]),_:2},1032,["href"]))),128))])])):y("",!0)])])):(n(),m("span",A,"暂无"))}}}),ht=I(E,[["__scopeId","data-v-d67b2e91"]]);export{ht as default};
@@ -0,0 +1 @@
.recording-playback[data-v-749b0199]{width:100%;min-height:1px}.recording-playback__shell[data-v-749b0199]{position:relative;width:100%}.recording-video[data-v-749b0199]{display:block;width:100%;max-width:100%;max-height:180px;height:auto;aspect-ratio:16/9;-o-object-fit:contain;object-fit:contain;border-radius:4px;background:#000;transform:translateZ(0)}.recording-playback__overlay[data-v-749b0199]{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:10px 12px;border-radius:4px;background:#0000009e;color:#fff;font-size:12px;line-height:1.45;text-align:center;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.recording-playback__overlay-title[data-v-749b0199]{font-weight:600}.recording-playback__overlay-desc[data-v-749b0199]{opacity:.92;font-size:11px}.recording-playback__spin[data-v-749b0199]{font-size:24px;animation:recording-spin-749b0199 .9s linear infinite}@keyframes recording-spin-749b0199{to{transform:rotate(360deg)}}.recording-playback__hint[data-v-749b0199]{margin-top:4px;font-size:12px;color:var(--el-text-color-secondary);line-height:1.4}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More