docs(release-notes): 添加住院护士站划价功能说明和发版记录 - 新增住院护士站划价服务流程说明文档,详细描述了从参数预处理到结果响应的五大阶段流程 - 包含耗材类医嘱和诊疗活动类医嘱的差异化处理逻辑 - 添加完整的发版内容记录,涵盖新增菜单功能和各模块优化点 - 记录了住院相关功能的新增和门诊业务流程的修复 ```
412 lines
10 KiB
Vue
412 lines
10 KiB
Vue
<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>
|