Merge branch '20260604-long'

This commit is contained in:
2026-06-04 17:37:29 +08:00
15 changed files with 796 additions and 201 deletions
-4
View File
@@ -26,10 +26,6 @@ bin-release/
/.cursor /.cursor
/.codex /.codex
/.agents /.agents
/.claude
/.claude
/.claude
/.kbctx
/server/.spool /server/.spool
/server/.claude /server/.claude
/.spool /.spool
@@ -0,0 +1,96 @@
<template>
<template v-if="variant === 'split'">
<el-form-item :label="label" :class="{ 'is-compact': compact }">
<el-radio-group v-model="timeType" :size="compact ? 'small' : 'default'" @change="emit('time-type-change')">
<el-radio-button v-if="showAll" label="all">全部</el-radio-button>
<el-radio-button label="today">今天</el-radio-button>
<el-radio-button label="yesterday">昨天</el-radio-button>
<el-radio-button label="week">最近7天</el-radio-button>
<el-radio-button label="month">本月</el-radio-button>
<el-radio-button label="custom">自定义</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="timeType === 'custom'">
<el-date-picker
v-model="dateRangeModel"
type="daterange"
value-format="YYYY-MM-DD"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
:size="compact ? 'small' : 'default'"
:style="{ width: pickerWidth }"
@change="emit('custom-change')"
/>
</el-form-item>
</template>
<el-form-item v-else :label="label" :class="{ 'is-compact': compact }">
<div class="list-time-filter-inline">
<el-radio-group v-model="timeType" :size="compact ? 'small' : 'default'" @change="emit('time-type-change')">
<el-radio-button v-if="showAll" label="all">全部</el-radio-button>
<el-radio-button label="today">今天</el-radio-button>
<el-radio-button label="yesterday">昨天</el-radio-button>
<el-radio-button label="week">最近7天</el-radio-button>
<el-radio-button label="month">本月</el-radio-button>
<el-radio-button label="custom">自定义</el-radio-button>
</el-radio-group>
<el-date-picker
v-if="timeType === 'custom'"
v-model="dateRangeModel"
type="daterange"
value-format="YYYY-MM-DD"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
:size="compact ? 'small' : 'default'"
:style="{ width: pickerWidth }"
@change="emit('custom-change')"
/>
</div>
</el-form-item>
</template>
<script lang="ts" setup>
import type { ListTimeType } from '@/hooks/useListTimeFilter'
withDefaults(
defineProps<{
label?: string
/** split:与自录统计一致,两个 form-item;inline:处方订单筛选用单行 */
variant?: 'split' | 'inline'
pickerWidth?: string
/** 紧凑模式:small 尺寸 radio / 日期 */
compact?: boolean
/** 是否显示「全部」(不限时间)选项 */
showAll?: boolean
}>(),
{
label: '时间范围',
variant: 'split',
pickerWidth: '260px',
compact: false,
showAll: false,
}
)
const timeType = defineModel<ListTimeType>('timeType', { required: true })
const dateRangeModel = defineModel<string[]>('dateRange', { default: () => [] })
const emit = defineEmits<{
'time-type-change': []
'custom-change': []
}>()
</script>
<style scoped lang="scss">
.list-time-filter-inline {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 8px;
}
:deep(.is-compact.el-form-item) {
margin-bottom: 0;
}
</style>
+56 -11
View File
@@ -88,13 +88,17 @@
<strong>{{ formData.herbs.length }}</strong> 主方 {{ mainHerbCount }} / 辅方 <strong>{{ formData.herbs.length }}</strong> 主方 {{ mainHerbCount }} / 辅方
{{ auxHerbCount }}· 可手动添加从处方库或粘贴药方批量录入 {{ auxHerbCount }}· 可手动添加从处方库或粘贴药方批量录入
</p> </p>
<p v-if="herbsLocked" class="rp-toolbar__lock-tip">
<el-tag type="danger" size="small" effect="plain">禁用修改</el-tag>
禁用修改处方库模板药材已锁定不可增删改如需调整请重新从处方库导入覆盖
</p>
</div> </div>
</div> </div>
<div class="rp-toolbar__actions" role="toolbar" aria-label="处方药材快捷操作"> <div class="rp-toolbar__actions" role="toolbar" aria-label="处方药材快捷操作">
<el-button type="primary" size="small" :icon="CirclePlus" @click="handleAddHerb('主方')"> <el-button type="primary" size="small" :icon="CirclePlus" :disabled="herbsLocked" @click="handleAddHerb('主方')">
添加主方 添加主方
</el-button> </el-button>
<el-button type="primary" plain size="small" @click="handleAddHerb('辅方')"> <el-button type="primary" plain size="small" :disabled="herbsLocked" @click="handleAddHerb('辅方')">
添加辅方 添加辅方
</el-button> </el-button>
<el-button <el-button
@@ -135,7 +139,7 @@
:class="{ 'herb-editor-card--dup': isDuplicateHerbName(row.index) }" :class="{ 'herb-editor-card--dup': isDuplicateHerbName(row.index) }"
> >
<div class="herb-editor-card__title">主方 {{ row.index + 1 }}</div> <div class="herb-editor-card__title">主方 {{ row.index + 1 }}</div>
<MedicineNameSelect v-model="formData.herbs[row.index].name" class="herb-editor-card__select" /> <MedicineNameSelect v-model="formData.herbs[row.index].name" :disabled="herbsLocked" class="herb-editor-card__select" />
<div <div
v-if="isDuplicateHerbName(row.index)" v-if="isDuplicateHerbName(row.index)"
class="herb-editor-card__dup-hint text-amber-600 text-xs mb-1" class="herb-editor-card__dup-hint text-amber-600 text-xs mb-1"
@@ -148,11 +152,12 @@
:min="0.1" :min="0.1"
:step="1" :step="1"
:precision="1" :precision="1"
:disabled="herbsLocked"
placeholder="剂量(克)" placeholder="剂量(克)"
class="herb-editor-card__dose" class="herb-editor-card__dose"
/> />
<span class="text-gray-500 shrink-0"></span> <span class="text-gray-500 shrink-0"></span>
<el-button type="danger" size="small" link @click="handleRemoveHerb(row.index)"> <el-button v-if="!herbsLocked" type="danger" size="small" link @click="handleRemoveHerb(row.index)">
删除 删除
</el-button> </el-button>
</div> </div>
@@ -171,7 +176,7 @@
:class="{ 'herb-editor-card--dup': isDuplicateHerbName(row.index) }" :class="{ 'herb-editor-card--dup': isDuplicateHerbName(row.index) }"
> >
<div class="herb-editor-card__title">辅方</div> <div class="herb-editor-card__title">辅方</div>
<MedicineNameSelect v-model="formData.herbs[row.index].name" class="herb-editor-card__select" /> <MedicineNameSelect v-model="formData.herbs[row.index].name" :disabled="herbsLocked" class="herb-editor-card__select" />
<div <div
v-if="isDuplicateHerbName(row.index)" v-if="isDuplicateHerbName(row.index)"
class="herb-editor-card__dup-hint text-amber-600 text-xs mb-1" class="herb-editor-card__dup-hint text-amber-600 text-xs mb-1"
@@ -184,11 +189,12 @@
:min="0.1" :min="0.1"
:step="1" :step="1"
:precision="1" :precision="1"
:disabled="herbsLocked"
placeholder="剂量(克)" placeholder="剂量(克)"
class="herb-editor-card__dose" class="herb-editor-card__dose"
/> />
<span class="text-gray-500 shrink-0"></span> <span class="text-gray-500 shrink-0"></span>
<el-button type="danger" size="small" link @click="handleRemoveHerb(row.index)"> <el-button v-if="!herbsLocked" type="danger" size="small" link @click="handleRemoveHerb(row.index)">
删除 删除
</el-button> </el-button>
</div> </div>
@@ -1028,6 +1034,8 @@ interface Herb {
name: string name: string
dosage: number dosage: number
formula_type?: FormulaType formula_type?: FormulaType
/** 来自「禁用修改」处方库模板:锁定后不可增删改,只能再次导入覆盖 */
locked?: boolean
} }
type AuxUsageForm = { type AuxUsageForm = {
@@ -1044,11 +1052,15 @@ function normalizeFormulaType(v: unknown): FormulaType {
} }
function normalizeHerbRow(raw: any): Herb { function normalizeHerbRow(raw: any): Herb {
return { const row: Herb = {
name: String(raw?.name ?? '').trim(), name: String(raw?.name ?? '').trim(),
dosage: Number(raw?.dosage) || 0, dosage: Number(raw?.dosage) || 0,
formula_type: normalizeFormulaType(raw?.formula_type) formula_type: normalizeFormulaType(raw?.formula_type)
} }
if (raw?.locked === true || raw?.locked === 1) {
row.locked = true
}
return row
} }
function defaultAuxUsage(prescriptionType = '浓缩水丸'): AuxUsageForm { function defaultAuxUsage(prescriptionType = '浓缩水丸'): AuxUsageForm {
@@ -1430,6 +1442,12 @@ const auxHerbRows = computed(() =>
const mainHerbCount = computed(() => mainHerbRows.value.length) const mainHerbCount = computed(() => mainHerbRows.value.length)
const auxHerbCount = computed(() => auxHerbRows.value.length) const auxHerbCount = computed(() => auxHerbRows.value.length)
/**
* 整张处方是否被锁定:只要任一药材来自「禁用修改」模板即锁定。
* 锁定后:不可增删改任意药材,仅可再次「从处方库导入」覆盖。
*/
const herbsLocked = computed(() => formData.herbs.some((h) => (h as Herb).locked === true))
const libraryDoctorId = computed(() => { const libraryDoctorId = computed(() => {
const uid = Number(userStore.userInfo?.id) const uid = Number(userStore.userInfo?.id)
return uid > 0 ? uid : 0 return uid > 0 ? uid : 0
@@ -2029,10 +2047,18 @@ function findDuplicateHerbNames(): string[] {
} }
const handleAddHerb = (formulaType: FormulaType = '主方') => { const handleAddHerb = (formulaType: FormulaType = '主方') => {
if (herbsLocked.value) {
feedback.msgWarning('该处方含「禁用修改」模板,药材已锁定,请重新从处方库导入覆盖')
return
}
formData.herbs.push({ name: '', dosage: 6, formula_type: formulaType }) formData.herbs.push({ name: '', dosage: 6, formula_type: formulaType })
} }
const handleRemoveHerb = (index: number) => { const handleRemoveHerb = (index: number) => {
if (herbsLocked.value) {
feedback.msgWarning('该处方含「禁用修改」模板,药材已锁定,不可删除')
return
}
formData.herbs.splice(index, 1) formData.herbs.splice(index, 1)
} }
@@ -2089,10 +2115,18 @@ const handleImportLibrary = (row: any) => {
} }
const formulaType = normalizeFormulaType(row.formula_type) const formulaType = normalizeFormulaType(row.formula_type)
const imported = (JSON.parse(JSON.stringify(row.herbs)) as any[]).map((h) => ({ // 「禁用修改」模板:导入的药材打上锁定标记 → 整张处方锁定(不可增删改,只能再次导入覆盖)
...normalizeHerbRow(h), const isLockedTemplate = Number(row?.disable_edit) === 1
formula_type: formulaType const imported = (JSON.parse(JSON.stringify(row.herbs)) as any[]).map((h) => {
})) const herb = normalizeHerbRow(h)
herb.formula_type = formulaType
if (isLockedTemplate) {
herb.locked = true
} else {
delete herb.locked
}
return herb
})
if (libraryImportMode.value === 'replace') { if (libraryImportMode.value === 'replace') {
const kept = formData.herbs.filter((h) => normalizeFormulaType(h.formula_type) !== formulaType) const kept = formData.herbs.filter((h) => normalizeFormulaType(h.formula_type) !== formulaType)
formData.herbs = [...kept, ...imported] formData.herbs = [...kept, ...imported]
@@ -2532,6 +2566,17 @@ defineExpose({
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
} }
.rp-toolbar__lock-tip {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
margin: 6px 0 0;
font-size: 12px;
line-height: 1.55;
color: var(--el-color-danger);
}
.rp-toolbar__actions { .rp-toolbar__actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
+88
View File
@@ -0,0 +1,88 @@
export type ListTimeType = 'all' | 'today' | 'yesterday' | 'week' | 'month' | 'custom'
export function formatYmd(d: Date): string {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
/** 列表时间筛选:month 为本自然月 1 日~月末 */
export function resolveListTimeRange(timeType: ListTimeType, dateRange: string[]): [string, string] {
const today = formatYmd(new Date())
switch (timeType) {
case 'all':
return ['', '']
case 'yesterday': {
const d = new Date()
d.setDate(d.getDate() - 1)
const y = formatYmd(d)
return [y, y]
}
case 'week': {
const end = new Date()
const start = new Date()
start.setDate(start.getDate() - 6)
return [formatYmd(start), formatYmd(end)]
}
case 'month': {
const now = new Date()
const start = new Date(now.getFullYear(), now.getMonth(), 1)
const end = new Date(now.getFullYear(), now.getMonth() + 1, 0)
return [formatYmd(start), formatYmd(end)]
}
case 'custom': {
let start = dateRange[0] || today
let end = dateRange[1] || today
if (start > end) {
;[start, end] = [end, start]
}
return [start, end]
}
case 'today':
default:
return [today, today]
}
}
export function useListTimeFilter(defaultType: ListTimeType = 'today') {
const time_type = ref<ListTimeType>(defaultType)
const dateRange = ref<string[]>([])
const resolve = () => resolveListTimeRange(time_type.value, dateRange.value)
const handleTimeTypeChange = (onQuery: () => void) => {
if (time_type.value !== 'custom') {
dateRange.value = []
onQuery()
}
}
const handleCustomDateChange = (onQuery: () => void) => {
if (dateRange.value.length === 2) {
onQuery()
}
}
const resetTimeFilter = () => {
time_type.value = defaultType
dateRange.value = []
}
const applyCustomRange = (start: string, end: string) => {
if (!start || !end) return
time_type.value = 'custom'
dateRange.value = [start, end]
}
return {
time_type,
dateRange,
resolve,
handleTimeTypeChange,
handleCustomDateChange,
resetTimeFilter,
applyCustomRange,
}
}
+46 -12
View File
@@ -679,17 +679,21 @@
粘贴文本解析药名与剂量;须与药品库名称完全一致才录入(导入至主方) 粘贴文本解析药名与剂量;须与药品库名称完全一致才录入(导入至主方)
</span> </span>
</div> </div>
<div v-if="herbsLocked" class="flex items-center gap-2 flex-wrap mt-2 text-xs text-[var(--el-color-danger)]">
<el-tag type="danger" size="small" effect="plain">禁用修改</el-tag>
含「禁用修改」处方库模板,药材已锁定,不可增删改,如需调整请重新「从处方库导入」覆盖
</div>
<div class="herb-formula-block mt-3"> <div class="herb-formula-block mt-3">
<div class="herb-formula-block__head"> <div class="herb-formula-block__head">
<el-tag type="primary" size="small">主方</el-tag> <el-tag type="primary" size="small">主方</el-tag>
<el-button type="primary" size="small" @click="addHerb('主方')">添加主方药材</el-button> <el-button type="primary" size="small" :disabled="herbsLocked" @click="addHerb('主方')">添加主方药材</el-button>
</div> </div>
<el-table :data="mainHerbRows" class="mt-2" border empty-text="暂无主方药材"> <el-table :data="mainHerbRows" class="mt-2" border empty-text="暂无主方药材">
<el-table-column label="序号" type="index" width="60" /> <el-table-column label="序号" type="index" width="60" />
<el-table-column label="药材名称" min-width="220"> <el-table-column label="药材名称" min-width="220">
<template #default="{ row }"> <template #default="{ row }">
<MedicineNameSelect v-model="editForm.herbs[row.index].name" class="w-full" /> <MedicineNameSelect v-model="editForm.herbs[row.index].name" :disabled="herbsLocked" class="w-full" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="剂量(克)" min-width="120"> <el-table-column label="剂量(克)" min-width="120">
@@ -699,6 +703,7 @@
:min="0" :min="0"
:precision="1" :precision="1"
:step="0.5" :step="0.5"
:disabled="herbsLocked"
placeholder="剂量" placeholder="剂量"
class="w-full" class="w-full"
/> />
@@ -706,7 +711,8 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" width="80"> <el-table-column label="操作" width="80">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="danger" link @click="removeHerb(row.index)">删除</el-button> <el-button v-if="!herbsLocked" type="danger" link @click="removeHerb(row.index)">删除</el-button>
<span v-else class="text-gray-400 text-xs">已锁定</span>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -715,7 +721,7 @@
<div class="herb-formula-block mt-4"> <div class="herb-formula-block mt-4">
<div class="herb-formula-block__head"> <div class="herb-formula-block__head">
<el-tag type="warning" size="small">辅方</el-tag> <el-tag type="warning" size="small">辅方</el-tag>
<el-button type="primary" size="small" plain @click="addHerb('辅方')"> <el-button type="primary" size="small" plain :disabled="herbsLocked" @click="addHerb('辅方')">
添加辅方药材 添加辅方药材
</el-button> </el-button>
</div> </div>
@@ -723,7 +729,7 @@
<el-table-column label="序号" type="index" width="60" /> <el-table-column label="序号" type="index" width="60" />
<el-table-column label="药材名称" min-width="220"> <el-table-column label="药材名称" min-width="220">
<template #default="{ row }"> <template #default="{ row }">
<MedicineNameSelect v-model="editForm.herbs[row.index].name" class="w-full" /> <MedicineNameSelect v-model="editForm.herbs[row.index].name" :disabled="herbsLocked" class="w-full" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="剂量(克)" min-width="120"> <el-table-column label="剂量(克)" min-width="120">
@@ -733,6 +739,7 @@
:min="0" :min="0"
:precision="1" :precision="1"
:step="0.5" :step="0.5"
:disabled="herbsLocked"
placeholder="剂量" placeholder="剂量"
class="w-full" class="w-full"
/> />
@@ -740,7 +747,8 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" width="80"> <el-table-column label="操作" width="80">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="danger" link @click="removeHerb(row.index)">删除</el-button> <el-button v-if="!herbsLocked" type="danger" link @click="removeHerb(row.index)">删除</el-button>
<span v-else class="text-gray-400 text-xs">已锁定</span>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -1698,7 +1706,7 @@ import jsPDF from 'jspdf'
const TcmDiagnosisEditView = defineAsyncComponent(() => import('@/views/tcm/diagnosis/edit.vue')) const TcmDiagnosisEditView = defineAsyncComponent(() => import('@/views/tcm/diagnosis/edit.vue'))
type FormulaType = '主方' | '辅方' type FormulaType = '主方' | '辅方'
type HerbRow = { name: string; dosage: number; formula_type: FormulaType } type HerbRow = { name: string; dosage: number; formula_type: FormulaType; locked?: boolean }
type AuxUsageForm = { type AuxUsageForm = {
dosage_amount?: number dosage_amount?: number
@@ -1798,11 +1806,15 @@ function normalizeFormulaType(v: unknown): FormulaType {
} }
function normalizeHerbRow(raw: any): HerbRow { function normalizeHerbRow(raw: any): HerbRow {
return { const row: HerbRow = {
name: String(raw?.name ?? '').trim(), name: String(raw?.name ?? '').trim(),
dosage: Number(raw?.dosage) || 0, dosage: Number(raw?.dosage) || 0,
formula_type: normalizeFormulaType(raw?.formula_type) formula_type: normalizeFormulaType(raw?.formula_type)
} }
if (raw?.locked === true || raw?.locked === 1) {
row.locked = true
}
return row
} }
function normalizeSlipHerbs(raw: unknown): HerbRow[] { function normalizeSlipHerbs(raw: unknown): HerbRow[] {
@@ -2481,6 +2493,12 @@ const auxHerbRows = computed(() =>
.filter(({ herb }) => normalizeFormulaType(herb.formula_type) === '辅方') .filter(({ herb }) => normalizeFormulaType(herb.formula_type) === '辅方')
) )
/**
* 整张处方是否被锁定:只要任一药材来自「禁用修改」处方库模板即锁定。
* 锁定后不可增删改任意药材,只能再次「从处方库导入」覆盖。
*/
const herbsLocked = computed(() => editForm.herbs.some((h) => (h as HerbRow).locked === true))
const slipDietaryText = computed(() => { const slipDietaryText = computed(() => {
const d = slipView.value?.dietary_taboo const d = slipView.value?.dietary_taboo
if (Array.isArray(d)) return d.filter(Boolean).join('、') if (Array.isArray(d)) return d.filter(Boolean).join('、')
@@ -3441,10 +3459,18 @@ const handleImportLibrary = (row: any) => {
return return
} }
const formulaType = normalizeFormulaType(row.formula_type) const formulaType = normalizeFormulaType(row.formula_type)
const imported = (JSON.parse(JSON.stringify(row.herbs)) as any[]).map((h) => ({ // 「禁用修改」模板:导入药材打上锁定标记 → 整张处方锁定(不可增删改,只能再次导入覆盖)
...normalizeHerbRow(h), const isLockedTemplate = Number(row?.disable_edit) === 1
formula_type: formulaType const imported = (JSON.parse(JSON.stringify(row.herbs)) as any[]).map((h) => {
})) const herb = normalizeHerbRow(h)
herb.formula_type = formulaType
if (isLockedTemplate) {
herb.locked = true
} else {
delete herb.locked
}
return herb
})
if (libraryImportMode.value === 'replace') { if (libraryImportMode.value === 'replace') {
const kept = editForm.herbs.filter((h) => normalizeFormulaType(h.formula_type) !== formulaType) const kept = editForm.herbs.filter((h) => normalizeFormulaType(h.formula_type) !== formulaType)
editForm.herbs = [...kept, ...imported] editForm.herbs = [...kept, ...imported]
@@ -3650,6 +3676,10 @@ async function handlePasteRecipeImport() {
// 添加药材(主方 / 辅方) // 添加药材(主方 / 辅方)
const addHerb = (formulaType: FormulaType = '主方') => { const addHerb = (formulaType: FormulaType = '主方') => {
if (herbsLocked.value) {
feedback.msgWarning('该处方含「禁用修改」模板,药材已锁定,请重新从处方库导入覆盖')
return
}
editForm.herbs.push({ editForm.herbs.push({
name: '', name: '',
dosage: 0, dosage: 0,
@@ -3659,6 +3689,10 @@ const addHerb = (formulaType: FormulaType = '主方') => {
// 删除药材 // 删除药材
const removeHerb = (index: number) => { const removeHerb = (index: number) => {
if (herbsLocked.value) {
feedback.msgWarning('该处方含「禁用修改」模板,药材已锁定,不可删除')
return
}
editForm.herbs.splice(index, 1) editForm.herbs.splice(index, 1)
} }
+27 -1
View File
@@ -71,6 +71,13 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="禁用修改" min-width="100">
<template #default="{ row }">
<el-tag :type="row.disable_edit ? 'danger' : 'info'">
{{ row.disable_edit ? '已禁用' : '可修改' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建人" prop="creator_name" min-width="100" /> <el-table-column label="创建人" prop="creator_name" min-width="100" />
<el-table-column label="创建时间" prop="create_time" min-width="160" /> <el-table-column label="创建时间" prop="create_time" min-width="160" />
<el-table-column label="操作" width="180" fixed="right"> <el-table-column label="操作" width="180" fixed="right">
@@ -193,6 +200,21 @@
勾选后所有医生都可以查看和使用此处方模板 勾选后所有医生都可以查看和使用此处方模板
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="禁用修改" prop="disable_edit">
<div class="flex flex-col gap-1 leading-none">
<el-switch
v-model="editForm.disable_edit"
:active-value="1"
:inactive-value="0"
active-text="开启"
inactive-text="关闭"
/>
<div class="text-xs text-gray-400 leading-relaxed">
开启后导入此模板到处方时将锁定整张处方的药材不可增删改只能再次导入覆盖
</div>
</div>
</el-form-item>
</el-form> </el-form>
<template #footer v-if="editMode !== 'view'"> <template #footer v-if="editMode !== 'view'">
@@ -236,7 +258,8 @@ const editForm = reactive({
prescription_name: '', prescription_name: '',
formula_type: '主方', formula_type: '主方',
herbs: [] as Array<{ name: string; dosage: number }>, herbs: [] as Array<{ name: string; dosage: number }>,
is_public: 0 is_public: 0,
disable_edit: 0
}) })
// 表单验证规则 // 表单验证规则
@@ -293,6 +316,7 @@ const resetForm = () => {
editForm.formula_type = '主方' editForm.formula_type = '主方'
editForm.herbs = [] editForm.herbs = []
editForm.is_public = 0 editForm.is_public = 0
editForm.disable_edit = 0
} }
// 新增处方 // 新增处方
@@ -310,6 +334,7 @@ const handleView = (row: any) => {
editForm.formula_type = row.formula_type || '主方' editForm.formula_type = row.formula_type || '主方'
editForm.herbs = row.herbs ? JSON.parse(JSON.stringify(row.herbs)) : [] editForm.herbs = row.herbs ? JSON.parse(JSON.stringify(row.herbs)) : []
editForm.is_public = row.is_public ?? 0 editForm.is_public = row.is_public ?? 0
editForm.disable_edit = row.disable_edit ?? 0
showEdit.value = true showEdit.value = true
} }
@@ -321,6 +346,7 @@ const handleEdit = (row: any) => {
editForm.formula_type = row.formula_type || '主方' editForm.formula_type = row.formula_type || '主方'
editForm.herbs = row.herbs ? JSON.parse(JSON.stringify(row.herbs)) : [] editForm.herbs = row.herbs ? JSON.parse(JSON.stringify(row.herbs)) : []
editForm.is_public = row.is_public ?? 0 editForm.is_public = row.is_public ?? 0
editForm.disable_edit = row.disable_edit ?? 0
showEdit.value = true showEdit.value = true
} }
@@ -173,17 +173,16 @@
</div> </div>
</div> </div>
<el-form class="ls-form" :model="queryParams" inline> <el-form class="ls-form" :model="queryParams" inline>
<el-form-item class="w-[360px] min-w-[240px] max-w-full" label="创建时间"> <list-time-filter
<DaterangePicker v-model:time-type="time_type"
v-model:startTime="queryParams.start_time" v-model:date-range="dateRange"
v-model:endTime="queryParams.end_time" label="创建时间"
picker-type="daterange" variant="inline"
value-format="YYYY-MM-DD" show-all
start-placeholder="开始日期" picker-width="230px"
end-placeholder="结束日期" @time-type-change="onTimeTypeChange"
class="!w-full" @custom-change="onCustomDateChange"
/> />
</el-form-item>
<el-form-item class="w-[260px]" label="订单号"> <el-form-item class="w-[260px]" label="订单号">
<el-input <el-input
v-model="queryParams.order_no" v-model="queryParams.order_no"
@@ -797,49 +796,50 @@
</div> </div>
<!-- 顶部数据概览卡片 --> <!-- 顶部数据概览卡片 -->
<el-row :gutter="16" class="mb-5"> <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-5">
<el-col :xs="12" :sm="6"> <el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50"> <div class="flex items-center justify-between mb-1">
<div class="flex items-center justify-between mb-1"> <div class="stat-title text-gray-500 text-xs">总金额</div>
<div class="stat-title text-gray-500 text-xs">总金额</div> <el-button
<el-button v-if="canUpdateAmount(detailData)"
v-if="canUpdateAmount(detailData)" v-perms="['tcm.prescriptionOrder/updateAmount']"
v-perms="['tcm.prescriptionOrder/updateAmount']" type="primary"
type="primary" size="small"
size="small" link
link @click="openUpdateAmount"
@click="openUpdateAmount"
>
修改
</el-button>
</div>
<div class="stat-value 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="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付总额</div>
<div class="stat-value text-green-600 font-bold text-xl">¥{{ detailData.linked_pay_paid_total || 0 }}</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">需代收</div>
<div
class="stat-value font-bold text-xl"
:class="detailAgencyToCollect > 0 ? 'text-amber-600' : 'text-gray-500'"
> >
¥{{ formatMoney(detailAgencyToCollect) }} 修改
</div> </el-button>
</el-card> </div>
</el-col> <div class="stat-value text-red-500 font-bold text-xl">¥{{ detailData.amount }}</div>
<el-col :xs="12" :sm="6"> </el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50"> <el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付笔数</div> <div class="stat-title text-gray-500 text-xs mb-1">已付总额</div>
<div class="stat-value text-primary font-bold text-xl">{{ detailLinkedPayOrders.length }} </div> <div class="stat-value text-green-600 font-bold text-xl">¥{{ detailData.linked_pay_paid_total || 0 }}</div>
</el-card> </el-card>
</el-col> <el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
</el-row> <div class="stat-title text-gray-500 text-xs mb-1">退款金额</div>
<div
class="stat-value font-bold text-xl"
:class="Number(detailData.refund_amount) > 0 ? 'text-rose-500' : 'text-gray-400'"
>
¥{{ formatMoney(detailData.refund_amount || 0) }}
</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">需代收</div>
<div
class="stat-value font-bold text-xl"
:class="detailAgencyToCollect > 0 ? 'text-amber-600' : 'text-gray-500'"
>
¥{{ formatMoney(detailAgencyToCollect) }}
</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付笔数</div>
<div class="stat-value text-primary font-bold text-xl">{{ detailLinkedPayOrders.length }} </div>
</el-card>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<!-- 左侧处方单 --> <!-- 左侧处方单 -->
@@ -2856,7 +2856,8 @@
import { computed, onMounted, reactive, ref, nextTick, watch, defineAsyncComponent } from 'vue' import { computed, onMounted, reactive, ref, nextTick, watch, defineAsyncComponent } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { Refresh, Loading, ArrowDown, InfoFilled, QuestionFilled, 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 ListTimeFilter from '@/components/list-time-filter/index.vue'
import { useListTimeFilter } from '@/hooks/useListTimeFilter'
import { import {
prescriptionOrderAuditPayment, prescriptionOrderAuditPayment,
prescriptionOrderAuditPrescription, prescriptionOrderAuditPrescription,
@@ -3162,9 +3163,6 @@ function handleExpressCompanyTabClick(value: '' | 'sf' | 'jd') {
} }
const queryParams = reactive({ const queryParams = reactive({
/** 创建时间区间(与列表筛选一致;不影响「今日」统计) */
start_time: '' as string,
end_time: '' as string,
order_no: '', order_no: '',
prescription_id: '' as string | number, prescription_id: '' as string | number,
/** 诊单患者姓名/手机号(后台关联 diagnosis */ /** 诊单患者姓名/手机号(后台关联 diagnosis */
@@ -3252,6 +3250,24 @@ function handleFocusBoardSearch(key: 'pendingRx' | 'pendingPay' | 'pendingShip'
resetPage() resetPage()
} }
const {
time_type,
dateRange,
resolve: resolveCreateTimeRange,
handleTimeTypeChange,
handleCustomDateChange,
resetTimeFilter,
applyCustomRange,
} = useListTimeFilter('all')
const handleQuery = () => {
pager.page = 1
getLists()
}
const onTimeTypeChange = () => handleTimeTypeChange(handleQuery)
const onCustomDateChange = () => handleCustomDateChange(handleQuery)
function normalizePrescriptionOrderListQuery(params: Record<string, unknown>): Record<string, unknown> { function normalizePrescriptionOrderListQuery(params: Record<string, unknown>): Record<string, unknown> {
const p: Record<string, unknown> = { ...params } const p: Record<string, unknown> = { ...params }
if (p.prescription_id === '' || p.prescription_id === undefined) { if (p.prescription_id === '' || p.prescription_id === undefined) {
@@ -3331,11 +3347,15 @@ function normalizePrescriptionOrderListQuery(params: Record<string, unknown>): R
} }
async function fetchLists(params: Record<string, unknown>) { async function fetchLists(params: Record<string, unknown>) {
const p = normalizePrescriptionOrderListQuery({ ...params }) const [start_time, end_time] = resolveCreateTimeRange()
const p = normalizePrescriptionOrderListQuery({ ...params, start_time, end_time })
return prescriptionOrderLists(p) return prescriptionOrderLists(p)
} }
const prescriptionOrderExportParams = computed(() => normalizePrescriptionOrderListQuery({ ...queryParams })) const prescriptionOrderExportParams = computed(() => {
const [start_time, end_time] = resolveCreateTimeRange()
return normalizePrescriptionOrderListQuery({ ...queryParams, start_time, end_time })
})
const { pager, getLists, resetPage, resetParams } = usePaging({ const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: fetchLists, fetchFun: fetchLists,
@@ -3370,8 +3390,9 @@ function applyRouteQueryToPrescriptionOrderList() {
} }
const st = normDate(q.start_time) const st = normDate(q.start_time)
const et = normDate(q.end_time) const et = normDate(q.end_time)
if (st) queryParams.start_time = st if (st && et) {
if (et) queryParams.end_time = et applyCustomRange(st, et)
}
const ad = q.assistant_dept_id const ad = q.assistant_dept_id
if (ad !== undefined && ad !== null && String(ad) !== '') { if (ad !== undefined && ad !== null && String(ad) !== '') {
const n = Number(ad) const n = Number(ad)
@@ -3462,9 +3483,7 @@ const listStats = computed(() => {
scopeHint = scopeHint =
'统计口径:本人创建的订单 + 关联处方开方人为本人的订单(含医助代建);与下方筛选项(不含「创建时间」)一致' '统计口径:本人创建的订单 + 关联处方开方人为本人的订单(含医助代建);与下方筛选项(不含「创建时间」)一致'
} }
const hasCustomTime = const headPrefix = time_type.value === 'all' ? '全部' : time_type.value === 'today' ? '今日' : '区间'
String(queryParams.start_time || '').trim() !== '' && String(queryParams.end_time || '').trim() !== ''
const headPrefix = hasCustomTime ? '区间' : '今日'
return { return {
order: n(o), order: n(o),
pay: n(pay), pay: n(pay),
@@ -3536,8 +3555,7 @@ watch(
function handleReset() { function handleReset() {
activeFocusKey.value = '' activeFocusKey.value = ''
queryParams.start_time = '' resetTimeFilter()
queryParams.end_time = ''
queryParams.order_no = '' queryParams.order_no = ''
queryParams.prescription_id = '' queryParams.prescription_id = ''
queryParams.patient_keyword = '' queryParams.patient_keyword = ''
@@ -190,18 +190,17 @@
<ArrowDown /> <ArrowDown />
</el-icon> </el-icon>
</div> </div>
<el-form v-show="!h5FilterCollapsed" class="ls-form" :model="queryParams" inline> <el-form v-show="!h5FilterCollapsed" class="ls-form stats-filter-form" :model="queryParams" inline>
<el-form-item class="w-[360px] min-w-[240px] max-w-full" label="创建时间"> <list-time-filter
<DaterangePicker v-model:time-type="time_type"
v-model:startTime="queryParams.start_time" v-model:date-range="dateRange"
v-model:endTime="queryParams.end_time" label="创建时间"
picker-type="daterange" variant="inline"
value-format="YYYY-MM-DD" show-all
start-placeholder="开始日期" picker-width="260px"
end-placeholder="结束日期" @time-type-change="onTimeTypeChange"
class="!w-full" @custom-change="onCustomDateChange"
/> />
</el-form-item>
<el-form-item class="w-[260px]" label="订单号"> <el-form-item class="w-[260px]" label="订单号">
<el-input <el-input
v-model="queryParams.order_no" v-model="queryParams.order_no"
@@ -703,49 +702,50 @@
</div> </div>
<!-- 顶部数据概览卡片 --> <!-- 顶部数据概览卡片 -->
<el-row :gutter="16" class="mb-5"> <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-5">
<el-col :xs="12" :sm="6"> <el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50"> <div class="flex items-center justify-between mb-1">
<div class="flex items-center justify-between mb-1"> <div class="stat-title text-gray-500 text-xs">总金额</div>
<div class="stat-title text-gray-500 text-xs">总金额</div> <el-button
<el-button v-if="canUpdateAmount(detailData)"
v-if="canUpdateAmount(detailData)" v-perms="['tcm.prescriptionOrder/updateAmount']"
v-perms="['tcm.prescriptionOrder/updateAmount']" type="primary"
type="primary" size="small"
size="small" link
link @click="openUpdateAmount"
@click="openUpdateAmount"
>
修改
</el-button>
</div>
<div class="stat-value 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="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付总额</div>
<div class="stat-value text-green-600 font-bold text-xl">¥{{ detailData.linked_pay_paid_total || 0 }}</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="6">
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">需代收</div>
<div
class="stat-value font-bold text-xl"
:class="detailAgencyToCollect > 0 ? 'text-amber-600' : 'text-gray-500'"
> >
¥{{ formatMoney(detailAgencyToCollect) }} 修改
</div> </el-button>
</el-card> </div>
</el-col> <div class="stat-value text-red-500 font-bold text-xl">¥{{ detailData.amount }}</div>
<el-col :xs="12" :sm="6"> </el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50"> <el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付笔数</div> <div class="stat-title text-gray-500 text-xs mb-1">已付总额</div>
<div class="stat-value text-primary font-bold text-xl">{{ detailLinkedPayOrders.length }} </div> <div class="stat-value text-green-600 font-bold text-xl">¥{{ detailData.linked_pay_paid_total || 0 }}</div>
</el-card> </el-card>
</el-col> <el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
</el-row> <div class="stat-title text-gray-500 text-xs mb-1">退款金额</div>
<div
class="stat-value font-bold text-xl"
:class="Number(detailData.refund_amount) > 0 ? 'text-rose-500' : 'text-gray-400'"
>
¥{{ formatMoney(detailData.refund_amount || 0) }}
</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">需代收</div>
<div
class="stat-value font-bold text-xl"
:class="detailAgencyToCollect > 0 ? 'text-amber-600' : 'text-gray-500'"
>
¥{{ formatMoney(detailAgencyToCollect) }}
</div>
</el-card>
<el-card shadow="never" class="stat-card border border-gray-100 bg-gray-50/50">
<div class="stat-title text-gray-500 text-xs mb-1">已付笔数</div>
<div class="stat-value text-primary font-bold text-xl">{{ detailLinkedPayOrders.length }} </div>
</el-card>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<!-- 左侧处方单 --> <!-- 左侧处方单 -->
@@ -2507,7 +2507,8 @@ import {
Wallet, Wallet,
User User
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import DaterangePicker from '@/components/daterange-picker/index.vue' import ListTimeFilter from '@/components/list-time-filter/index.vue'
import { useListTimeFilter } from '@/hooks/useListTimeFilter'
import { import {
prescriptionOrderAuditPayment, prescriptionOrderAuditPayment,
prescriptionOrderAuditPrescription, prescriptionOrderAuditPrescription,
@@ -2805,9 +2806,6 @@ function handleExpressCompanyTabClick(value: '' | 'sf' | 'jd') {
} }
const queryParams = reactive({ const queryParams = reactive({
/** 创建时间区间(与列表筛选一致;不影响「今日」统计) */
start_time: '' as string,
end_time: '' as string,
order_no: '', order_no: '',
prescription_id: '' as string | number, prescription_id: '' as string | number,
/** 诊单患者姓名/手机号(后台关联 diagnosis */ /** 诊单患者姓名/手机号(后台关联 diagnosis */
@@ -2886,8 +2884,26 @@ function handleFocusBoardSearch(key: 'pendingRx' | 'pendingPay' | 'pendingShip'
resetPage() resetPage()
} }
const {
time_type,
dateRange,
resolve: resolveCreateTimeRange,
handleTimeTypeChange,
handleCustomDateChange,
resetTimeFilter,
} = useListTimeFilter('all')
const handleQuery = () => {
pager.page = 1
getLists()
}
const onTimeTypeChange = () => handleTimeTypeChange(handleQuery)
const onCustomDateChange = () => handleCustomDateChange(handleQuery)
async function fetchLists(params: Record<string, unknown>) { async function fetchLists(params: Record<string, unknown>) {
const p: Record<string, unknown> = { ...params } const [start_time, end_time] = resolveCreateTimeRange()
const p: Record<string, unknown> = { ...params, start_time, end_time }
if (p.prescription_id === '' || p.prescription_id === undefined) { if (p.prescription_id === '' || p.prescription_id === undefined) {
delete p.prescription_id delete p.prescription_id
} else { } else {
@@ -3041,9 +3057,7 @@ const listStats = computed(() => {
scopeHint = scopeHint =
'统计口径:本人创建的订单 + 关联处方开方人为本人的订单(含医助代建);与下方筛选项(不含「创建时间」)一致' '统计口径:本人创建的订单 + 关联处方开方人为本人的订单(含医助代建);与下方筛选项(不含「创建时间」)一致'
} }
const hasCustomTime = const headPrefix = time_type.value === 'all' ? '全部' : time_type.value === 'today' ? '今日' : '区间'
String(queryParams.start_time || '').trim() !== '' && String(queryParams.end_time || '').trim() !== ''
const headPrefix = hasCustomTime ? '区间' : '今日'
return { return {
order: n(o), order: n(o),
pay: n(pay), pay: n(pay),
@@ -3114,8 +3128,7 @@ watch(
function handleReset() { function handleReset() {
activeFocusKey.value = '' activeFocusKey.value = ''
queryParams.start_time = '' resetTimeFilter()
queryParams.end_time = ''
queryParams.order_no = '' queryParams.order_no = ''
queryParams.prescription_id = '' queryParams.prescription_id = ''
queryParams.patient_keyword = '' queryParams.patient_keyword = ''
@@ -3381,11 +3394,11 @@ const h5FilterCollapsed = ref(true)
const h5ActiveFilterCount = computed(() => { const h5ActiveFilterCount = computed(() => {
const q = queryParams as any const q = queryParams as any
const keys = [ const keys = [
'start_time', 'end_time', 'order_no', 'prescription_id', 'order_no', 'prescription_id',
'patient_keyword', 'express_keyword', 'doctor_id', 'patient_keyword', 'express_keyword', 'doctor_id',
'assistant_dept_id', 'assistant_id' 'assistant_dept_id', 'assistant_id'
] ]
let n = 0 let n = time_type.value !== 'all' ? 1 : 0
for (const k of keys) { for (const k of keys) {
const v = q[k] const v = q[k]
if (v === null || v === undefined) continue if (v === null || v === undefined) continue
@@ -5520,6 +5533,12 @@ async function downloadPrescriptionSlipPdf() {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.stats-filter-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.prescription-order-page { .prescription-order-page {
margin: 0 auto; margin: 0 auto;
+50 -17
View File
@@ -14,13 +14,13 @@
</el-card> </el-card>
<el-card class="!border-none" shadow="never"> <el-card class="!border-none" shadow="never">
<el-form class="mb-[-16px]" :model="queryParams" inline> <el-form :inline="true" :model="queryParams" class="stats-filter-form">
<el-form-item label="日期范围"> <list-time-filter
<daterange-picker v-model:time-type="time_type"
v-model:startTime="queryParams.start_date" v-model:date-range="dateRange"
v-model:endTime="queryParams.end_date" @time-type-change="onTimeTypeChange"
/> @custom-change="onCustomDateChange"
</el-form-item> />
<el-form-item label="自媒体渠道"> <el-form-item label="自媒体渠道">
<el-select <el-select
v-model="queryParams.media_channel_code" v-model="queryParams.media_channel_code"
@@ -67,11 +67,11 @@
v-model="queryParams.remark" v-model="queryParams.remark"
placeholder="备注 / 创建人 / 修改人" placeholder="备注 / 创建人 / 修改人"
clearable clearable
@keyup.enter="resetPage" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button> <el-button type="primary" :loading="pager.loading" @click="handleQuery">查询</el-button>
<el-button @click="handleReset">重置</el-button> <el-button @click="handleReset">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -148,7 +148,9 @@
<script lang="ts" setup name="accountCostList"> <script lang="ts" setup name="accountCostList">
import { accountCostDelete, accountCostLists } from '@/api/finance' import { accountCostDelete, accountCostLists } from '@/api/finance'
import { usePaging } from '@/hooks/usePaging' import { usePaging } from '@/hooks/usePaging'
import { useListTimeFilter } from '@/hooks/useListTimeFilter'
import feedback from '@/utils/feedback' import feedback from '@/utils/feedback'
import ListTimeFilter from '@/components/list-time-filter/index.vue'
import EditPopup from './edit.vue' import EditPopup from './edit.vue'
@@ -172,18 +174,40 @@ const editRef = shallowRef<InstanceType<typeof EditPopup>>()
const showEdit = ref(false) const showEdit = ref(false)
const queryParams = reactive({ const queryParams = reactive({
start_date: '',
end_date: '',
media_channel_code: '', media_channel_code: '',
dept_id: '', dept_id: '',
remark: '', remark: '',
}) })
const { pager, getLists, resetPage } = usePaging({ const {
fetchFun: accountCostLists, time_type,
dateRange,
resolve,
handleTimeTypeChange,
handleCustomDateChange,
resetTimeFilter,
} = useListTimeFilter('today')
const { pager, getLists } = usePaging({
fetchFun: async (params: Record<string, any>) => {
const [start_date, end_date] = resolve()
return accountCostLists({
...params,
start_date,
end_date,
})
},
params: queryParams, params: queryParams,
}) })
const handleQuery = () => {
pager.page = 1
getLists()
}
const onTimeTypeChange = () => handleTimeTypeChange(handleQuery)
const onCustomDateChange = () => handleCustomDateChange(handleQuery)
const mediaChannelOptionsFlat = computed<MediaChannelOption[]>(() => { const mediaChannelOptionsFlat = computed<MediaChannelOption[]>(() => {
const options = pager.extend.media_channel_options const options = pager.extend.media_channel_options
if (!Array.isArray(options)) return [] if (!Array.isArray(options)) return []
@@ -258,18 +282,27 @@ const handleDelete = async (id: number) => {
} }
const handleReset = () => { const handleReset = () => {
queryParams.start_date = '' resetTimeFilter()
queryParams.end_date = ''
queryParams.media_channel_code = '' queryParams.media_channel_code = ''
queryParams.dept_id = '' queryParams.dept_id = ''
queryParams.remark = '' queryParams.remark = ''
resetPage() pager.page = 1
pager.size = 15
getLists()
} }
getLists() onMounted(() => {
getLists()
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.stats-filter-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.account-cost-channel-select { .account-cost-channel-select {
:deep(.channel-opt-row) { :deep(.channel-opt-row) {
display: flex; display: flex;
@@ -14,13 +14,14 @@
</el-card> </el-card>
<el-card class="!border-none" shadow="never"> <el-card class="!border-none" shadow="never">
<el-form class="mb-[-16px]" :model="queryParams" inline> <el-form :inline="true" :model="queryParams" class="stats-filter-form">
<el-form-item label="日期范围"> <list-time-filter
<daterange-picker v-model:time-type="time_type"
v-model:startTime="queryParams.start_date" v-model:date-range="dateRange"
v-model:endTime="queryParams.end_date" @time-type-change="onTimeTypeChange"
/> @custom-change="onCustomDateChange"
</el-form-item> />
<el-form-item label="部门"> <el-form-item label="部门">
<el-tree-select <el-tree-select
v-model="queryParams.dept_id" v-model="queryParams.dept_id"
@@ -33,7 +34,7 @@
clearable clearable
filterable filterable
class="w-[220px]" class="w-[220px]"
@change="resetPage" @change="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="自媒体来源"> <el-form-item label="自媒体来源">
@@ -44,7 +45,7 @@
placeholder="全部来源" placeholder="全部来源"
:allow-create="false" :allow-create="false"
select-class="w-[220px]" select-class="w-[220px]"
@change="resetPage" @change="handleQuery"
@visible-change="onMediaSourceDropdownVisible" @visible-change="onMediaSourceDropdownVisible"
/> />
</el-form-item> </el-form-item>
@@ -53,11 +54,11 @@
v-model="queryParams.remark" v-model="queryParams.remark"
placeholder="备注 / 创建人" placeholder="备注 / 创建人"
clearable clearable
@keyup.enter="resetPage" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button> <el-button type="primary" :loading="pager.loading" @click="handleQuery">查询</el-button>
<el-button @click="handleReset">重置</el-button> <el-button @click="handleReset">重置</el-button>
<el-button v-if="selfInputPath" class="ml-2" @click="goSelfInput">返回统计</el-button> <el-button v-if="selfInputPath" class="ml-2" @click="goSelfInput">返回统计</el-button>
</el-form-item> </el-form-item>
@@ -146,6 +147,8 @@ import feedback from '@/utils/feedback'
import CostEditPopup from './cost-edit.vue' import CostEditPopup from './cost-edit.vue'
import MediaSourceSelect from './MediaSourceSelect.vue' import MediaSourceSelect from './MediaSourceSelect.vue'
import { useMediaSourceOptions } from './useMediaSourceOptions' import { useMediaSourceOptions } from './useMediaSourceOptions'
import ListTimeFilter from '@/components/list-time-filter/index.vue'
import { useListTimeFilter } from '@/hooks/useListTimeFilter'
const router = useRouter() const router = useRouter()
const editRef = shallowRef<InstanceType<typeof CostEditPopup>>() const editRef = shallowRef<InstanceType<typeof CostEditPopup>>()
@@ -173,25 +176,48 @@ const goSelfInput = () => {
} }
const queryParams = reactive({ const queryParams = reactive({
start_date: '',
end_date: '',
media_source: '', media_source: '',
dept_id: undefined as number | undefined, dept_id: undefined as number | undefined,
remark: '', remark: '',
}) })
const {
time_type,
dateRange,
resolve,
handleTimeTypeChange,
handleCustomDateChange,
resetTimeFilter,
} = useListTimeFilter('today')
const { pager, getLists, resetPage } = usePaging({ const { pager, getLists, resetPage } = usePaging({
fetchFun: personalAccountCostLists, fetchFun: async (params: Record<string, any>) => {
const [start_date, end_date] = resolve()
return personalAccountCostLists({
...params,
start_date,
end_date,
})
},
params: queryParams, params: queryParams,
}) })
const handleQuery = () => {
pager.page = 1
getLists()
}
const onTimeTypeChange = () => handleTimeTypeChange(handleQuery)
const onCustomDateChange = () => handleCustomDateChange(handleQuery)
const handleReset = () => { const handleReset = () => {
queryParams.start_date = '' resetTimeFilter()
queryParams.end_date = ''
queryParams.media_source = '' queryParams.media_source = ''
queryParams.dept_id = undefined queryParams.dept_id = undefined
queryParams.remark = '' queryParams.remark = ''
resetPage() pager.page = 1
pager.size = 15
getLists()
} }
const onMediaSourceDropdownVisible = (visible: boolean) => { const onMediaSourceDropdownVisible = (visible: boolean) => {
@@ -236,9 +262,10 @@ const loadDeptOptions = async () => {
} }
} }
onMounted(() => { onMounted(async () => {
loadDeptOptions() loadDeptOptions()
loadMediaSourceOptions() loadMediaSourceOptions()
await getLists()
}) })
</script> </script>
@@ -247,6 +274,12 @@ onMounted(() => {
padding: 20px; padding: 20px;
} }
.stats-filter-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.dept-cell { .dept-cell {
cursor: help; cursor: help;
border-bottom: 1px dashed #c0c4cc; border-bottom: 1px dashed #c0c4cc;
@@ -56,7 +56,7 @@ class PrescriptionLibraryLists extends BaseAdminDataLists implements ListsSearch
public function lists(): array public function lists(): array
{ {
$field = [ $field = [
'id', 'prescription_name', 'formula_type', 'herbs', 'is_public', 'id', 'prescription_name', 'formula_type', 'herbs', 'is_public', 'disable_edit',
'creator_id', 'creator_name', 'create_time', 'update_time' 'creator_id', 'creator_name', 'create_time', 'update_time'
]; ];
@@ -778,6 +778,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
'export_channel' => '渠道', 'export_channel' => '渠道',
'export_guahao_channel_source' => '自媒体渠道(挂号渠道来源)', 'export_guahao_channel_source' => '自媒体渠道(挂号渠道来源)',
'export_medication_form' => '药品形态', 'export_medication_form' => '药品形态',
'export_prescription_name' => '药方名称',
'export_service_package' => '服务套餐', 'export_service_package' => '服务套餐',
'export_medication_days' => '天数', 'export_medication_days' => '天数',
'export_amount' => '总金额', 'export_amount' => '总金额',
@@ -11,6 +11,7 @@ use app\common\model\Order;
use app\common\model\tcm\Diagnosis; use app\common\model\tcm\Diagnosis;
use app\common\model\tcm\DiagnosisAssignLog; use app\common\model\tcm\DiagnosisAssignLog;
use app\common\model\tcm\Prescription; use app\common\model\tcm\Prescription;
use app\common\model\tcm\PrescriptionLibrary;
use app\common\model\tcm\PrescriptionOrder; use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\PrescriptionOrderLog; use app\common\model\tcm\PrescriptionOrderLog;
use app\common\model\tcm\PrescriptionOrderPayOrder; use app\common\model\tcm\PrescriptionOrderPayOrder;
@@ -3116,7 +3117,7 @@ class PrescriptionOrderLogic
$rxById = []; $rxById = [];
if ($rxIdList !== []) { if ($rxIdList !== []) {
$rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time') $rxRows = Prescription::whereIn('id', $rxIdList)->whereNull('delete_time')
->field(['id', 'prescription_type', 'need_decoction', 'dose_unit', 'assistant_id', 'appointment_id']) ->field(['id', 'prescription_type', 'need_decoction', 'dose_unit', 'assistant_id', 'appointment_id', 'prescription_name', 'aux_usage', 'herbs', 'creator_id'])
->select() ->select()
->toArray(); ->toArray();
foreach ($rxRows as $xr) { foreach ($rxRows as $xr) {
@@ -3124,6 +3125,10 @@ class PrescriptionOrderLogic
} }
} }
// 药方名称反查:处方库(开方医师 creator_id + 公开模板),按 formula_type + 药材集合匹配,
// 复刻前端 resolveSlipAuxLibraryName 口径(主方/辅方均如此;处方记录 prescription_name 仅做兜底)。
[$libByDoctor, $libPublic] = self::buildPrescriptionLibraryNameIndexForExport($rxById);
if ($needGuahaoChannel) { if ($needGuahaoChannel) {
$yejiChannelHighlightByDiag = []; $yejiChannelHighlightByDiag = [];
if ( if (
@@ -3329,6 +3334,22 @@ class PrescriptionOrderLogic
} }
$item['export_medication_form'] = self::formatMedicationFormForExport(\is_array($rx) ? $rx : []); $item['export_medication_form'] = self::formatMedicationFormForExport(\is_array($rx) ? $rx : []);
// 药方名称:主方/辅方均反查处方库取名称(与处方笺一致),带标签;甘草/无匹配留空
[$mainRxName, $auxRxName] = self::resolvePrescriptionNamesForExport(
\is_array($rx) ? $rx : [],
$libByDoctor,
$libPublic
);
$rxNameParts = [];
if ($mainRxName !== '') {
$rxNameParts[] = '主方:' . $mainRxName;
}
if ($auxRxName !== '') {
$rxNameParts[] = '辅方:' . $auxRxName;
}
$item['export_prescription_name'] = implode(' ', $rxNameParts);
$item['export_service_package'] = self::formatServicePackageForExport( $item['export_service_package'] = self::formatServicePackageForExport(
$item['service_package'] ?? '', $item['service_package'] ?? '',
$packageNameByValue $packageNameByValue
@@ -3401,6 +3422,186 @@ class PrescriptionOrderLogic
unset($item); unset($item);
} }
/**
* 导出药方名称反查:批量加载相关开方医师处方库(creator_id 命中 + 公开模板),
* 建立 [creator_id][formula_type][herbKey] => name 与 公开 [formula_type][herbKey] => name 两级索引。
*
* @param array<int, array> $rxById 处方 id => 处方行(需含 herbs / creator_id
*
* @return array{0: array<int, array<string, array<string, string>>>, 1: array<string, array<string, string>>}
*/
private static function buildPrescriptionLibraryNameIndexForExport(array $rxById): array
{
$libByDoctor = [];
$libPublic = [];
if ($rxById === []) {
return [$libByDoctor, $libPublic];
}
$doctorIds = [];
foreach ($rxById as $rx) {
$cid = (int) ($rx['creator_id'] ?? 0);
if ($cid > 0) {
$doctorIds[$cid] = true;
}
}
$doctorIdList = array_keys($doctorIds);
try {
$libRows = PrescriptionLibrary::whereNull('delete_time')
->where(function ($q) use ($doctorIdList) {
if ($doctorIdList !== []) {
$q->whereIn('creator_id', $doctorIdList)->whereOr('is_public', 1);
} else {
$q->where('is_public', 1);
}
})
->field(['prescription_name', 'formula_type', 'herbs', 'creator_id', 'is_public'])
->select()
->toArray();
} catch (\Throwable $e) {
Log::warning('buildPrescriptionLibraryNameIndexForExport failed: ' . $e->getMessage());
return [$libByDoctor, $libPublic];
}
foreach ($libRows as $lib) {
$name = trim((string) ($lib['prescription_name'] ?? ''));
if ($name === '') {
continue;
}
$herbsRaw = $lib['herbs'] ?? null;
if (\is_string($herbsRaw) && $herbsRaw !== '') {
$decoded = json_decode($herbsRaw, true);
$herbsRaw = \is_array($decoded) ? $decoded : [];
}
if (!\is_array($herbsRaw) || $herbsRaw === []) {
continue;
}
$key = self::herbSetMatchKeyForExport($herbsRaw);
if ($key === '') {
continue;
}
$ft = ((string) ($lib['formula_type'] ?? '')) === '辅方' ? '辅方' : '主方';
if ((int) ($lib['is_public'] ?? 0) === 1) {
$libPublic[$ft][$key] ??= $name;
}
$cid = (int) ($lib['creator_id'] ?? 0);
if ($cid > 0) {
$libByDoctor[$cid][$ft][$key] ??= $name;
}
}
return [$libByDoctor, $libPublic];
}
/**
* 反查单张处方的主方 / 辅方名称(与处方笺展示一致):
* - 主方:主方药材集合匹配开方医师/公开处方库(formula_type=主方);兜底取处方 prescription_name。
* - 辅方:优先 aux_usage.prescription_name / library_name,否则辅方药材集合匹配(formula_type=辅方)。
*
* @param array $rx 处方行(含 herbs / creator_id / prescription_name / aux_usage
* @param array<int, array<string, array<string, string>>> $libByDoctor
* @param array<string, array<string, string>> $libPublic
*
* @return array{0: string, 1: string} [主方名称, 辅方名称]
*/
private static function resolvePrescriptionNamesForExport(array $rx, array $libByDoctor, array $libPublic): array
{
if ($rx === []) {
return ['', ''];
}
$doctorId = (int) ($rx['creator_id'] ?? 0);
$herbs = $rx['herbs'] ?? null;
if (\is_string($herbs) && $herbs !== '') {
$decoded = json_decode($herbs, true);
$herbs = \is_array($decoded) ? $decoded : [];
}
if (!\is_array($herbs)) {
$herbs = [];
}
$mainHerbs = [];
$auxHerbs = [];
foreach ($herbs as $h) {
if (!\is_array($h)) {
continue;
}
if (((string) ($h['formula_type'] ?? '')) === '辅方') {
$auxHerbs[] = $h;
} else {
$mainHerbs[] = $h;
}
}
$lookup = static function (string $ft, array $hs) use ($doctorId, $libByDoctor, $libPublic): string {
if ($hs === []) {
return '';
}
$key = self::herbSetMatchKeyForExport($hs);
if ($key === '') {
return '';
}
if ($doctorId > 0 && isset($libByDoctor[$doctorId][$ft][$key])) {
return $libByDoctor[$doctorId][$ft][$key];
}
return (string) ($libPublic[$ft][$key] ?? '');
};
// 主方
$mainName = $lookup('主方', $mainHerbs);
if ($mainName === '') {
$mainName = trim((string) ($rx['prescription_name'] ?? ''));
}
// 辅方:先读持久化字段
$auxName = '';
$auxUsage = $rx['aux_usage'] ?? null;
if (\is_string($auxUsage) && $auxUsage !== '') {
$decodedAux = json_decode($auxUsage, true);
$auxUsage = \is_array($decodedAux) ? $decodedAux : null;
}
if (\is_array($auxUsage)) {
$auxName = trim((string) ($auxUsage['prescription_name'] ?? ($auxUsage['library_name'] ?? '')));
}
if ($auxName === '') {
$auxName = $lookup('辅方', $auxHerbs);
}
return [$mainName, $auxName];
}
/**
* 药材集合匹配 key:每味 name:dosagedosage 规整去尾零)排序后拼接,与处方库反查双方同口径。
*/
private static function herbSetMatchKeyForExport(array $herbs): string
{
$parts = [];
foreach ($herbs as $h) {
if (!\is_array($h)) {
continue;
}
$name = trim((string) ($h['name'] ?? ''));
if ($name === '') {
continue;
}
$dosage = (float) ($h['dosage'] ?? 0);
if (floor($dosage) === $dosage) {
$dStr = (string) (int) $dosage;
} else {
$dStr = rtrim(rtrim(number_format($dosage, 4, '.', ''), '0'), '.');
}
$parts[] = $name . ':' . $dStr;
}
sort($parts);
return implode('|', $parts);
}
/** /**
* 完成订单时把关联诊单的 assistant_id 清空,让患者回到「待分配」状态 * 完成订单时把关联诊单的 assistant_id 清空,让患者回到「待分配」状态
* 仅当该患者没有其它进行中的处方订单且无指派日志时才清空,避免误覆盖 * 仅当该患者没有其它进行中的处方订单且无指派日志时才清空,避免误覆盖
@@ -16,7 +16,8 @@ class PrescriptionLibraryValidate extends BaseValidate
'prescription_name' => 'require|max:100', 'prescription_name' => 'require|max:100',
'formula_type' => 'in:主方,辅方', 'formula_type' => 'in:主方,辅方',
'herbs' => 'require|array', 'herbs' => 'require|array',
'is_public' => 'in:0,1' 'is_public' => 'in:0,1',
'disable_edit' => 'in:0,1'
]; ];
protected $message = [ protected $message = [
@@ -27,7 +28,8 @@ class PrescriptionLibraryValidate extends BaseValidate
'formula_type.in' => '处方类型只能是主方或辅方', 'formula_type.in' => '处方类型只能是主方或辅方',
'herbs.require' => '药材列表不能为空', 'herbs.require' => '药材列表不能为空',
'herbs.array' => '药材列表格式错误', 'herbs.array' => '药材列表格式错误',
'is_public.in' => '是否公开参数错误' 'is_public.in' => '是否公开参数错误',
'disable_edit.in' => '禁用修改参数错误'
]; ];
/** /**
@@ -35,7 +37,7 @@ class PrescriptionLibraryValidate extends BaseValidate
*/ */
public function sceneAdd() public function sceneAdd()
{ {
return $this->only(['prescription_name', 'formula_type', 'herbs', 'is_public']); return $this->only(['prescription_name', 'formula_type', 'herbs', 'is_public', 'disable_edit']);
} }
/** /**
@@ -43,7 +45,7 @@ class PrescriptionLibraryValidate extends BaseValidate
*/ */
public function sceneEdit() public function sceneEdit()
{ {
return $this->only(['id', 'prescription_name', 'formula_type', 'herbs', 'is_public']); return $this->only(['id', 'prescription_name', 'formula_type', 'herbs', 'is_public', 'disable_edit']);
} }
/** /**
@@ -0,0 +1,3 @@
-- 处方库:禁用修改(导入处方后锁定药材,不可增删改,只能再次导入覆盖)
ALTER TABLE `zyt_prescription_library`
ADD COLUMN `disable_edit` tinyint(1) NOT NULL DEFAULT 0 COMMENT '导入处方后是否禁止修改/删除药材:0否 1是' AFTER `is_public`;