套餐设置套餐管理完善

This commit is contained in:
2026-01-29 15:32:21 +08:00
parent 638f853af6
commit 1c781c1224
4 changed files with 258 additions and 172 deletions

View File

@@ -1,115 +0,0 @@
-- 诊断:门诊划价检索不出诊疗项目的问题
-- 问题描述:在门诊划价页面,检索不出诊疗项目
-- 1. 检查是否有诊疗项目定义数据
SELECT
COUNT(*) as total_count,
COUNT(CASE WHEN delete_flag = '0' THEN 1 END) as active_count,
COUNT(CASE WHEN delete_flag != '0' THEN 1 END) as deleted_count
FROM wor_activity_definition;
-- 2. 检查是否有收费项目(诊疗项目类型)
SELECT
T1.id,
T1.encounter_id,
T1.context_enum,
T1.product_id,
T1.status_enum,
T1.delete_flag,
T2.id as activity_def_id,
T2.name as activity_name,
T2.delete_flag as activity_delete_flag
FROM adm_charge_item AS T1
LEFT JOIN wor_activity_definition AS T2
ON T1.context_enum = 'ACTIVITY' -- 诊疗项目类型
AND T1.product_id = T2.id
AND T2.delete_flag = '0'
WHERE T1.context_enum = 'ACTIVITY'
AND T1.delete_flag = '0'
LIMIT 20;
-- 3. 检查是否有诊疗项目定义但收费项目中的product_id无法匹配
SELECT
'诊疗项目定义存在,但收费项目无法匹配' as issue_type,
COUNT(*) as count
FROM wor_activity_definition wad
WHERE wad.delete_flag = '0'
AND NOT EXISTS (
SELECT 1
FROM adm_charge_item aci
WHERE aci.context_enum = 'ACTIVITY'
AND aci.product_id = wad.id
AND aci.delete_flag = '0'
);
-- 4. 检查收费项目中的诊疗项目,但定义表中没有对应数据
SELECT
'收费项目存在,但诊疗项目定义缺失' as issue_type,
COUNT(*) as count
FROM adm_charge_item aci
WHERE aci.context_enum = 'ACTIVITY'
AND aci.delete_flag = '0'
AND NOT EXISTS (
SELECT 1
FROM wor_activity_definition wad
WHERE wad.id = aci.product_id
AND wad.delete_flag = '0'
);
-- 5. 检查某个具体就诊的诊疗项目替换encounterId为实际值
-- SELECT
-- T1.encounter_id,
-- T1.id as charge_item_id,
-- T1.context_enum,
-- T1.product_id,
-- T1.status_enum,
-- T2.id as activity_def_id,
-- T2.name as activity_name,
-- T2.delete_flag as activity_delete_flag,
-- CASE
-- WHEN T2.id IS NULL THEN '诊疗项目定义不存在或已删除'
-- WHEN T2.delete_flag != '0' THEN '诊疗项目定义已删除'
-- ELSE '正常'
-- END as status
-- FROM adm_charge_item AS T1
-- LEFT JOIN wor_activity_definition AS T2
-- ON T1.context_enum = 'ACTIVITY'
-- AND T1.product_id = T2.id
-- WHERE T1.encounter_id = :encounterId -- 替换为实际的encounterId
-- AND T1.context_enum = 'ACTIVITY'
-- AND T1.delete_flag = '0'
-- AND T1.status_enum IN (1, 2, 3, 4, 5, 6); -- PLANNED, BILLABLE, BILLED, REFUNDING, REFUNDED, PART_REFUND

View File

@@ -135,10 +135,15 @@ export function updateAuthRole(data) {
}
// 查询部门下拉树结构
export function deptTreeSelect() {
// 默认只显示科室类型typeEnum=2如果需要其他类型可以传入 params 覆盖
export function deptTreeSelect(params = {}) {
return request({
url: '/base-data-manage/organization/organization',
method: 'get'
method: 'get',
params: {
typeEnum: 2, // 默认只显示科室
...params // 允许外部传入参数覆盖默认值
}
})
}

View File

@@ -469,7 +469,8 @@ async function getDiseaseTreatmentList() {
// 诊疗目录分类查询下拉树结d构
async function getImplDepartList() {
try {
const res = await getImplementDepartmentList();
// 只查询科室类型typeEnum=2不包含专业等其他类型
const res = await getImplementDepartmentList({ typeEnum: 2 });
if (res.code === 200) {
if (res.data.records.length > 0) {
organization.value = res.data.records.map((res) => {

View File

@@ -41,7 +41,7 @@
</thead>
<tbody>
<tr
v-for="(row, index) in tableData"
v-for="(row, index) in pagedTypeRows"
:key="row.id"
:class="{ 'editing': editingRowId === row.id }"
>
@@ -148,12 +148,16 @@
</div>
<!-- 页码区域 -->
<div class="pagination">
<div class="page-btn">上一页</div>
<div class="page-btn active">1</div>
<div class="page-btn">2</div>
<div class="page-btn">3</div>
<div class="page-btn">下一页</div>
<div class="pagination" v-if="typeTotalPages > 1">
<div class="page-btn" :class="{ disabled: typeCurrentPage === 1 }" @click="typePrevPage">上一页</div>
<div
v-for="p in typePageButtons"
:key="p"
class="page-btn"
:class="{ active: p === typeCurrentPage }"
@click="typeGoPage(p)"
>{{ p }}</div>
<div class="page-btn" :class="{ disabled: typeCurrentPage >= typeTotalPages }" @click="typeNextPage">下一页</div>
</div>
</template>
@@ -239,7 +243,7 @@
</thead>
<tbody>
<tr
v-for="(item, index) in filteredInspectionItems"
v-for="(item, index) in pagedInspectionItems"
:key="item.id"
:class="{ 'editing': editingRowId === item.id }"
>
@@ -372,12 +376,16 @@
</div>
<!-- 页码区域 -->
<div class="pagination">
<div class="page-btn">上一页</div>
<div class="page-btn active">1</div>
<div class="page-btn">2</div>
<div class="page-btn">3</div>
<div class="page-btn">下一页</div>
<div class="pagination" v-if="inspectionTotalPages > 1">
<div class="page-btn" :class="{ disabled: inspectionCurrentPage === 1 }" @click="inspectionPrevPage">上一页</div>
<div
v-for="p in inspectionPageButtons"
:key="p"
class="page-btn"
:class="{ active: p === inspectionCurrentPage }"
@click="inspectionGoPage(p)"
>{{ p }}</div>
<div class="page-btn" :class="{ disabled: inspectionCurrentPage >= inspectionTotalPages }" @click="inspectionNextPage">下一页</div>
</div>
</template>
@@ -687,9 +695,9 @@
<script setup>
import useUserStore from '@/store/modules/user';
import {computed, onMounted, ref, watch} from 'vue';
import {computed, nextTick, onActivated, onMounted, ref, watch} from 'vue';
import {ElLoading, ElMessage, ElMessageBox} from 'element-plus';
import {useRoute, useRouter} from 'vue-router';
import {onBeforeRouteUpdate, useRoute, useRouter} from 'vue-router';
import {
addInspectionType,
delInspectionType,
@@ -785,6 +793,97 @@ const activeNav = ref(0);
// 检验类型数据
const tableData = ref([]);
// ==============================
// 分页(检验类型 tab
// ==============================
const typeCurrentPage = ref(1);
// 每页条数按需求固定为10
const typePageSize = ref(10);
const typeTotalPages = computed(() => {
const total = tableData.value.length;
return Math.max(1, Math.ceil(total / typePageSize.value));
});
const typePageButtons = computed(() => {
const total = typeTotalPages.value;
return Array.from({ length: total }, (_, i) => i + 1);
});
// 按“大类编码(code)”排序后再分页展示(不要按序号 sortOrder 排)
function parseCodeParts(code) {
const s = (code ?? '').toString().trim();
// 支持 1、01、3-001、3_001 等:用 -/_ 分隔
const parts = s.split(/[-_]/g);
const mainRaw = parts[0] ?? '';
const subRaw = parts[1] ?? '';
const mainNum = Number.parseInt(mainRaw, 10);
const subNum = Number.parseInt(subRaw, 10);
return {
raw: s,
mainRaw,
subRaw,
mainIsNum: !Number.isNaN(mainNum),
subIsNum: !Number.isNaN(subNum),
mainNum,
subNum
};
}
const sortedTypeRows = computed(() => {
return [...tableData.value].sort((a, b) => {
const pa = parseCodeParts(a?.code);
const pb = parseCodeParts(b?.code);
// 先按主编码:数字优先按数值,否则按字符串
if (pa.mainIsNum && pb.mainIsNum) {
if (pa.mainNum !== pb.mainNum) return pa.mainNum - pb.mainNum;
} else {
const c = pa.mainRaw.localeCompare(pb.mainRaw, 'zh-Hans-CN', { numeric: true, sensitivity: 'base' });
if (c !== 0) return c;
}
// 同主编码时:无子编码的排在前(大类在子类之前)
const aHasSub = !!pa.subRaw;
const bHasSub = !!pb.subRaw;
if (aHasSub !== bHasSub) return aHasSub ? 1 : -1;
// 再按子编码
if (pa.subIsNum && pb.subIsNum) {
if (pa.subNum !== pb.subNum) return pa.subNum - pb.subNum;
} else {
const c = pa.subRaw.localeCompare(pb.subRaw, 'zh-Hans-CN', { numeric: true, sensitivity: 'base' });
if (c !== 0) return c;
}
// 最后兜底:按原字符串
return pa.raw.localeCompare(pb.raw, 'zh-Hans-CN', { numeric: true, sensitivity: 'base' });
});
});
const pagedTypeRows = computed(() => {
const start = (typeCurrentPage.value - 1) * typePageSize.value;
return sortedTypeRows.value.slice(start, start + typePageSize.value);
});
function typeGoPage(p) {
if (p < 1 || p > typeTotalPages.value) return;
typeCurrentPage.value = p;
}
function typePrevPage() {
if (typeCurrentPage.value <= 1) return;
typeCurrentPage.value -= 1;
}
function typeNextPage() {
if (typeCurrentPage.value >= typeTotalPages.value) return;
typeCurrentPage.value += 1;
}
watch(
() => tableData.value.length,
() => {
// 数据变化后,确保当前页有效
if (typeCurrentPage.value > typeTotalPages.value) {
typeCurrentPage.value = 1;
}
}
);
// 获取检验类型列表 - 从后端API获取
const getInspectionTypeList = () => {
listInspectionType().then(data => {
@@ -989,6 +1088,44 @@ const filteredInspectionItems = computed(() => {
});
});
// ==============================
// 分页(检验项目 tab
// ==============================
const inspectionCurrentPage = ref(1);
// 每页条数按需求固定为10
const inspectionPageSize = ref(10);
const inspectionTotalPages = computed(() => {
const total = filteredInspectionItems.value.length;
return Math.max(1, Math.ceil(total / inspectionPageSize.value));
});
const inspectionPageButtons = computed(() => {
const total = inspectionTotalPages.value;
return Array.from({ length: total }, (_, i) => i + 1);
});
const pagedInspectionItems = computed(() => {
const start = (inspectionCurrentPage.value - 1) * inspectionPageSize.value;
return filteredInspectionItems.value.slice(start, start + inspectionPageSize.value);
});
function inspectionGoPage(p) {
if (p < 1 || p > inspectionTotalPages.value) return;
inspectionCurrentPage.value = p;
}
function inspectionPrevPage() {
if (inspectionCurrentPage.value <= 1) return;
inspectionCurrentPage.value -= 1;
}
function inspectionNextPage() {
if (inspectionCurrentPage.value >= inspectionTotalPages.value) return;
inspectionCurrentPage.value += 1;
}
watch(
() => filteredInspectionItems.value.length,
() => {
// 过滤条件变化后,回到第一页,避免出现“当前页没数据但页码还在后面”的体验
inspectionCurrentPage.value = 1;
}
);
// 执行过滤
const filterItems = () => {
// 过滤逻辑已经在computed属性中实现这里可以添加额外的逻辑
@@ -1371,7 +1508,9 @@ const addNewRow = () => {
ElMessage.warning('请先保存或取消当前正在编辑的行');
return;
}
const newRow = { id: Date.now(), code: '', name: '', department: departments.value[0], sortOrder: tableData.value.length + 1, remark: '' };
// department 在表格里是字符串(科室名称);这里不要直接塞对象,否则后续 trim/校验会异常
const defaultDeptName = departments.value?.[0]?.name || '';
const newRow = { id: Date.now(), code: '', name: '', department: defaultDeptName, sortOrder: tableData.value.length + 1, remark: '' };
tableData.value.push(newRow);
editingRowId.value = newRow.id;
};
@@ -1392,40 +1531,27 @@ const handleConfirm = (row) => {
sortOrder: row.sortOrder ? Number(row.sortOrder) : 0
};
// 兼容 department 可能是对象的情况(例如 el-tree-select 返回节点对象)
if (submitData.department && typeof submitData.department === 'object') {
submitData.department = submitData.department.name || submitData.department.label || '';
}
console.log('原始row数据:', row);
console.log('提交前的submitData:', submitData);
// 验证必填字段,如果为空则删除该行
// 验证必填字段:为空则提示并保留该行(不要直接删行,否则用户“点确定就没了”体验很差)
if (!submitData.code || submitData.code.trim() === '') {
// 删除空的编辑行
const index = tableData.value.findIndex(r => r.id === row.id);
if (index !== -1) {
tableData.value.splice(index, 1);
editingRowId.value = null;
ElMessage.info('已删除空行');
}
ElMessage.warning('请输入大类编码');
return;
}
if (!submitData.name || submitData.name.trim() === '') {
// 删除空的编辑行
const index = tableData.value.findIndex(r => r.id === row.id);
if (index !== -1) {
tableData.value.splice(index, 1);
editingRowId.value = null;
ElMessage.info('已删除空行');
}
ElMessage.warning('请输入大类项目名称');
return;
}
if (!submitData.department || submitData.department.trim() === '') {
// 删除空的编辑行
const index = tableData.value.findIndex(r => r.id === row.id);
if (index !== -1) {
tableData.value.splice(index, 1);
editingRowId.value = null;
ElMessage.info('已删除空行');
}
if (!submitData.department || String(submitData.department).trim() === '') {
ElMessage.warning('请选择执行科室');
return;
}
@@ -1486,7 +1612,40 @@ const handleAdd = (row, index) => {
ElMessage.warning('请先保存或取消当前正在编辑的行');
return;
}
const newRow = { id: Date.now(), code: '', name: '', department: row.department, sortOrder: row.sortOrder + 1, remark: '' };
// 行内“+”:在当前大类下新增子类,编码按“父类编码-001”递增生成
const parentCode = (row?.code ?? '').toString().trim();
if (!parentCode) {
ElMessage.warning('请先填写并保存当前大类编码,再新增子类');
return;
}
// 找到该父类下已有的子类编码,取最大序号 + 1
// 支持如1-001、01-002 等
const prefix = `${parentCode}-`;
let maxSeq = 0;
for (const r of tableData.value) {
const code = (r?.code ?? '').toString();
if (code.startsWith(prefix)) {
const suffix = code.slice(prefix.length);
const n = Number.parseInt(suffix, 10);
if (!Number.isNaN(n)) {
maxSeq = Math.max(maxSeq, n);
}
}
}
const nextSeq = maxSeq + 1;
const childCode = `${parentCode}-${String(nextSeq).padStart(3, '0')}`;
const newRow = {
id: Date.now(),
code: childCode,
name: '',
department: row.department,
// 序号字段保持原逻辑:插入到父类下一行,默认沿用父类序号
sortOrder: row.sortOrder,
remark: ''
};
tableData.value.splice(index + 1, 0, newRow);
editingRowId.value = newRow.id;
};
@@ -2069,22 +2228,52 @@ onMounted(() => {
loadObservationItems();
// 加载检验套餐明细项目
loadPackageItemsFromAPI();
// 检查URL参数如果有tab参数则切换到对应导航项
const query = router.currentRoute.value.query;
if (query.tab === '0' || query.tab === '1' || query.tab === '2') {
activeNav.value = parseInt(query.tab);
}
// 检查URL参数中是否有packageId如果有则加载套餐数据用于编辑或查看
if (query.packageId) {
// 切换到套餐设置标签页
activeNav.value = 2;
// 加载套餐数据
loadInspectionPackage(query.packageId);
}
// 初始化计算套餐金额和服务费
// 初始化计算套餐金额和服务费
calculateAmounts();
});
/**
* 关键修复:
* 从“套餐管理”跳转到这里时,通常只是改变 querypackageId/mode/tab组件不会重新挂载
* 所以仅靠 onMounted 读取一次 query 会导致“查看/修改”出现空白,刷新才正常。
* 这里监听 query 变化,自动切换 tab 并加载套餐数据。
*/
watch(
() => route.query.tab,
(tab) => {
if (tab === '0' || tab === '1' || tab === '2') {
activeNav.value = parseInt(String(tab));
}
},
{ immediate: true }
);
const applyRouteForPackage = async (query) => {
const packageId = query?.packageId;
if (!packageId) return;
activeNav.value = 2;
await nextTick();
loadInspectionPackage(String(packageId));
};
watch(
() => route.query.packageId,
async () => {
await applyRouteForPackage(route.query);
},
{ immediate: true, flush: 'post' }
);
// 兜底:如果该页面被 keep-alive 缓存,从别的页面返回时不会触发 onMounted
onActivated(() => {
applyRouteForPackage(route.query);
});
// 兜底:同一路由复用/仅 query 变化时,确保能触发加载
onBeforeRouteUpdate((to) => {
applyRouteForPackage(to.query);
});
// 监听生成服务费选项变更
watch(generateServiceFee, (newVal) => {
calculateAmounts();
@@ -2422,6 +2611,12 @@ watch(packageItems, (newVal) => {
border-color: #1890ff;
}
.page-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* 套餐设置样式 */
.top-bar {
height: 48px;