docs(release-notes): 添加住院护士站划价功能说明和发版记录

- 新增住院护士站划价服务流程说明文档,详细描述了从参数预处理到结果响应的五大阶段流程
- 包含耗材类医嘱和诊疗活动类医嘱的差异化处理逻辑
- 添加完整的发版内容记录,涵盖新增菜单功能和各模块优化点
- 记录了住院相关功能的新增和门诊业务流程的修复
```
This commit is contained in:
2025-12-25 14:13:14 +08:00
parent 85fcb7c2e2
commit abc0674531
920 changed files with 107068 additions and 14495 deletions

View File

@@ -0,0 +1,411 @@
<template>
<div class="table-layout-container">
<div class="card-content-wrapper">
<div
v-if="showSideQuery"
class="side-query-wrapper"
:class="{ collapsed: sideQueryCollapsed }"
>
<div v-if="!sideQueryCollapsed" class="side-query-header">
<el-input v-model="sideSearchKeyword" placeholder="搜索树节点" clearable size="small">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<div v-if="!sideQueryCollapsed" class="side-query-content">
<el-tree
ref="treeRef"
:data="treeDataWithAll"
:props="defaultProps"
:node-key="treeNodeKey"
:expand-on-click-node="false"
default-expand-all
highlight-current
@node-click="handleNodeClick"
@current-change="handleCurrentChange"
></el-tree>
</div>
</div>
<div v-if="showSideQuery" class="collapse-divider">
<el-button
circle
size="small"
class="collapse-btn"
@click="sideQueryCollapsed = !sideQueryCollapsed"
>
<el-icon>
<ArrowRight v-if="sideQueryCollapsed" />
<ArrowLeft v-else />
</el-icon>
</el-button>
</div>
<!-- 主内容区域 -->
<div
class="main-content-wrapper"
:class="{ 'with-side-query': showSideQuery && !sideQueryCollapsed }"
>
<Filter
v-if="showTopQuery"
ref="queryFormComponentRef"
:query-params="queryParams"
:form-items="formItems"
:show-default-buttons="showDefaultButtons"
@query="handleQuery"
@reset="resetQuery"
>
<template
v-for="item in customFormItems"
:key="item.prop"
v-slot:[item.slotName]="slotProps"
>
<slot :name="item.slotName" :item="slotProps.item" :queryParams="props.queryParams" />
</template>
<template #default="{ queryParams, handleQuery, resetQuery }">
<slot
name="topQuery"
:queryParams="queryParams"
:handleQuery="handleQuery"
:resetQuery="resetQuery"
/>
</template>
</Filter>
<!-- 操作按钮区域 -->
<div class="table-operation-bar">
<slot name="operations" />
</div>
<!-- 表格区域 -->
<Table
:table-data="tableData"
:loading="loading"
:border="border"
:stripe="stripe"
:size="size"
:table-height="tableHeight"
:max-height="maxHeight"
:row-key="rowKey"
:highlight-current-row="highlightCurrentRow"
:table-columns="tableColumns"
:show-pagination="showPagination"
:total="total"
:page-no="props.queryParams.pageNo"
:page-size="props.queryParams.pageSize"
@row-click="handleRowClick"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
@pagination="handlePagination"
>
<template v-for="(_, slotName) in $slots" :key="slotName" #[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps" />
</template>
</Table>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import Filter from './Filter.vue';
import Table from './Table.vue';
import type { TableLayoutProps, TreeNodeData } from '../types/TableLayout.d';
defineOptions({
name: 'TableLayout',
});
const props = withDefaults(defineProps<TableLayoutProps>(), {
tableData: () => [],
loading: false,
total: 0,
queryParams: () => ({
pageNo: 1,
pageSize: 20,
}),
sideQueryParams: () => ({}),
formItems: () => [],
showTopQuery: true,
showSideQuery: false,
showPagination: true,
showDefaultButtons: true,
sideWidth: 6,
border: true,
stripe: false,
size: 'default',
highlightCurrentRow: false,
siderData: () => [],
treeNodeKey: 'id',
tableColumns: () => [],
});
const emit = defineEmits<{
query: [queryParams: Record<string, any>];
reset: [];
pagination: [pagination: { page: number; limit: number }];
'row-click': [row: Record<string, any>, column: any, event: Event];
'selection-change': [selection: Record<string, any>[]];
'sort-change': [sortInfo: { column: any; prop: string; order: string }];
'side-query': [node: TreeNodeData];
'reset-side-query': [];
}>();
const queryFormRef = ref<InstanceType<typeof import('element-plus').ElForm> | null>(null);
import type { FilterExpose } from '../types/Filter.d';
const queryFormComponentRef = ref<FilterExpose | null>(null);
const sideSearchKeyword = ref<string>('');
const treeRef = ref<InstanceType<typeof import('element-plus').ElTree> | null>(null);
const currentTreeNode = ref<TreeNodeData | null>(null);
const sideQueryCollapsed = ref<boolean>(false);
const customFormItems = computed(() => {
return props.formItems
.filter((item) => item.type === 'custom')
.map((item) => ({
...item,
slotName: item.slot || item.prop,
}));
});
const defaultProps = {
children: 'children',
label: 'label',
};
const filteredSiderData = computed(() => {
if (!sideSearchKeyword.value || !props.siderData || props.siderData.length === 0) {
return props.siderData;
}
const keyword = sideSearchKeyword.value.toLowerCase();
const filterTree = (nodes: TreeNodeData[]): TreeNodeData[] => {
if (!nodes || nodes.length === 0) return [];
return nodes
.map((node: TreeNodeData) => {
const label = (node[defaultProps.label] || '').toLowerCase();
const match = label.includes(keyword);
const children = node[defaultProps.children];
let filteredChildren: TreeNodeData[] | null = null;
if (children && children.length > 0) {
filteredChildren = filterTree(children);
}
if (match || (filteredChildren && filteredChildren.length > 0)) {
return {
...node,
[defaultProps.children]: filteredChildren,
};
}
return null;
})
.filter(Boolean) as TreeNodeData[];
};
return filterTree(props.siderData);
});
const treeDataWithAll = computed(() => {
const children = filteredSiderData.value || [];
return [
{
[props.treeNodeKey]: '__ALL__',
[defaultProps.label]: '全部',
[defaultProps.children]: children || [],
},
];
});
const handleQuery = () => {
props.queryParams.pageNo = 1;
emit('query', props.queryParams);
if (currentTreeNode.value) {
emit('side-query', currentTreeNode.value);
}
};
const handleNodeClick = (data: TreeNodeData, node: any) => {
currentTreeNode.value = data;
if (treeRef.value && data && data[props.treeNodeKey]) {
treeRef.value.setCurrentKey(data[props.treeNodeKey]);
}
handleQuery();
};
const handleCurrentChange = (data: TreeNodeData, node: any) => {
currentTreeNode.value = data;
};
const resetQuery = () => {
if (queryFormComponentRef.value?.queryFormRef) {
queryFormComponentRef.value.queryFormRef.resetFields();
}
if (props.queryParams) {
Object.keys(props.queryParams).forEach((key) => {
if (key !== 'pageNo' && key !== 'pageSize') {
if (Array.isArray(props.queryParams[key])) {
props.queryParams[key] = [];
} else if (typeof props.queryParams[key] === 'object' && props.queryParams[key] !== null) {
props.queryParams[key] = null;
} else {
props.queryParams[key] = '';
}
}
});
if (Object.prototype.hasOwnProperty.call(props.queryParams, 'pageNo')) {
props.queryParams.pageNo = 1;
}
}
emit('reset');
handleQuery();
};
const handlePagination = (pagination) => {
if (props.queryParams) {
props.queryParams.pageNo = pagination.page;
props.queryParams.pageSize = pagination.limit;
}
emit('pagination', pagination);
emit('query', props.queryParams);
if (currentTreeNode.value) {
emit('side-query', currentTreeNode.value);
}
};
const handleRowClick = (row, column, event) => {
emit('row-click', row, column, event);
};
const handleSelectionChange = (selection) => {
emit('selection-change', selection);
};
const handleSortChange = ({ column, prop, order }) => {
emit('sort-change', { column, prop, order });
};
defineExpose({
queryFormRef: computed(() => queryFormComponentRef.value?.queryFormRef),
handleQuery,
resetQuery,
});
</script>
<style scoped lang="scss">
.table-layout-container {
height: 100%;
padding: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
.main-content-card {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
min-height: 0;
overflow: visible;
:deep(.el-card__body) {
flex: 1;
display: flex;
flex-direction: column;
padding: 16px 16px 8px 16px;
min-height: 0;
overflow: visible;
}
}
.card-content-wrapper {
flex: 1;
display: flex;
gap: 0;
min-height: 0;
position: relative;
}
.collapse-divider {
flex-shrink: 0;
width: 1px;
background-color: #ebeef5;
position: relative;
display: flex;
align-items: flex-start;
justify-content: center;
margin: 0 12px;
.collapse-btn {
position: absolute;
left: 50%;
top: 18px;
transform: translateX(-50%);
background-color: #fff;
border: 1px solid #ebeef5;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
z-index: 10;
width: 24px;
height: 24px;
&:hover {
background-color: #409eff;
color: #fff;
border-color: #409eff;
}
}
}
.side-query-wrapper {
flex-shrink: 0;
display: flex;
flex-direction: column;
transition: width 0.3s, opacity 0.3s;
overflow: hidden;
&.collapsed {
width: 0;
opacity: 0;
padding: 0;
border: none;
}
.side-query-header {
flex-shrink: 0;
margin-bottom: 12px;
display: flex;
align-items: center;
}
.side-query-content {
flex: 1;
min-height: 0;
overflow-y: auto;
:deep(.el-tree--highlight-current) {
background-color: #fff !important;
}
}
}
.main-content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
min-width: 0;
overflow: hidden;
}
.table-operation-bar {
flex-shrink: 0;
margin-bottom: 8px;
}
}
</style>